diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml index 2ce2d23d..37fc24c1 100644 --- a/.github/workflows/tests.yaml +++ b/.github/workflows/tests.yaml @@ -4,13 +4,9 @@ jobs: drive: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v1 - - name: Write firebase_options.dart - run: echo '{{ secrets.FIREBASE_OPTIONS }}' > lib/firebase_options.dart - - name: Write GoogleService-info.plist - run: echo '{{ secrets.GOOGLE_SERVICE_INFO_PLIST }}' > ios/Runner/GoogleService-Info.plist - - name: Write google-services.json - run: echo '{{ secrets.GOOGLE_SERVICES_JSON }}' > android/app/google-services.json - - uses: subosito/flutter-action@v2.8.0 - - name: Run Flutter tests - run: flutter test + - uses: actions/checkout@v1 + - uses: subosito/flutter-action@v2.8.0 + - run: flutter pub get + - run: flutter pub run build_runner build --delete-conflicting-outputs + - name: Run Flutter tests + run: flutter test diff --git a/.gitignore b/.gitignore index 37f3e0cc..ce2f3351 100644 --- a/.gitignore +++ b/.gitignore @@ -9,6 +9,7 @@ .buildlog/ .history .svn/ +*.jsk # IntelliJ related *.iml @@ -17,7 +18,7 @@ .idea/ # Visual Studio Code related -#.vscode/ +.vscode/ # Flutter repo-specific /bin/cache/ @@ -46,6 +47,13 @@ unlinked.ds unlinked_spec.ds .flutter-plugins-dependencies +# Ignoring unsupported platforms +linux +macos +windows +web + + # Android related **/android/**/gradle-wrapper.jar **/android/.gradle @@ -58,6 +66,10 @@ unlinked_spec.ds *.jks # iOS/XCode related +# commenting out the Podfile.lock because it is frequently changing and should be generated by the build +# want to avoid constant branch conflicts +/ios/Podfile.lock + **/ios/**/*.mode1v3 **/ios/**/*.mode2v3 **/ios/**/*.moved-aside @@ -102,4 +114,17 @@ macos/firebase_app_id_file.json android/app/google-services.json # Firebase hosting -.firebase/ \ No newline at end of file +.firebase/ + +.env +.env/ + +# contains responses from Danlaw API +api-response.yml + +**/*.g.dart +**/*.reflectable.dart + +# bundle files +*.apk +*.aab \ No newline at end of file diff --git a/README.md b/README.md index f096bd37..a7746156 100644 --- a/README.md +++ b/README.md @@ -1,102 +1,105 @@ -# Time Tracking app with Flutter & Firebase +

Danlaw Smart Charger

-A time tracking application built with Flutter & Firebase: +## Table of Contents -![](/.github/images/time-tracker-screenshots.png) +- [Description](#description) +- [Features](#features) +- [Roadmap](#roadmap) +- [Packages](#packages) +- [Support](#support) +- [Design](#design) +- [Screenshots](#screenshots) +- [License](#license) -This is intended as a **reference app** based on my [Riverpod Architecture](https://codewithandrea.com/articles/flutter-app-architecture-riverpod-introduction/). - -> **Note**: this project used to be called "Started Architecture for Flutter & Firebase" (based on this [old article](https://codewithandrea.com/videos/starter-architecture-flutter-firebase/)). As of January 2023, it follows my updated [Riverpod Architecture](https://codewithandrea.com/articles/flutter-app-architecture-riverpod-introduction/), using the latest packages. - -## Flutter web preview - -A Flutter web preview of the app is available here: - -- [Time Tracker | Flutter web demo](https://starter-architecture-flutter.web.app) +## Description +Danlaw Smart Charger provides a simple interface for users to monitor the charging status of their EVs, plan custom charging schedules and setup various alerts. + ## Features -- **Simple onboarding page** -- **Full authentication flow** (using email & password) -- **Jobs**: users can view, create, edit, and delete their own private jobs (each job has a name and hourly rate) -- **Entries**: for each job, user can view, create, edit, and delete the corresponding entries (an entry is a task with a start and end time, with an optional comment) -- **A report page** that shows a daily breakdown of all jobs, hours worked and pay, along with the totals. - -All the data is persisted with Firestore and is kept in sync across multiple devices. +- **Account**: New users need to create an account to access the features of the app. +- **Authentication**: After account creation, users can login with email & password. +- **Household**: + - Users can add up to 3 households per account. + - Household details can be changed. +- **Charger**: + - Users can add up to 3 chargers per household. + - Chargers can be added with scanning a QR code or Bluetooth flow. +- **Vehicle**: + - Users can add up to 6 vehicles per household. + - Vehicles can be added with scanning a QR code or Bluetooth flow. + - Diagnostic Trouble Codes can be accessed from details page. +- **Alert**: + - Alerts are grouped by Vehicles and Chargers. + - Users can be alerted by Push Notifications, Email and SMS. + - Alerts can be set based on remaining miles, charge status, etc.. +- **Schedule**: Users can see and set charging schedules for the week. +- **Reports**: Energy reports are available for all the vehicles and chargers. Table can be grouped by chargers. ## Roadmap -- [ ] Add missing tests -- [x] Stateful Nested Navigation (available since GoRouter 7.1) -- [ ] Use controllers / notifiers consistently across the app (some code still needs to be updated) -- [ ] Add localization -- [ ] Use the new Firebase UI packages where useful -- [ ] Responsive UI - -> This is a tentative roadmap. There is no ETA for any of the points above. This is a low priority project and I don't have much time to maintain it. - -## Relevant Articles +- [ ] Update Add Charger Flow +- [ ] Update Add Vehicle Flow +- [ ] Add more tests +- [ ] Add language selection -The app is based on my Flutter Riverpod architecture, which is explained in detail here: +>**Note:** Some of the items on the roadmap might be tackled at the next phase of development. -- [Flutter App Architecture with Riverpod: An Introduction](https://codewithandrea.com/articles/flutter-app-architecture-riverpod-introduction/) -- [Flutter Project Structure: Feature-first or Layer-first?](https://codewithandrea.com/articles/flutter-project-structure/) -- [Flutter App Architecture: The Repository Pattern](https://codewithandrea.com/articles/flutter-repository-pattern/) - -More more info on Riverpod, read this: - -- [Flutter Riverpod 2.0: The Ultimate Guide](https://codewithandrea.com/articles/flutter-state-management-riverpod/) - -## Packages in use +## Packages These are the main packages used in the app: - [Flutter Riverpod](https://pub.dev/packages/flutter_riverpod) for data caching, dependency injection, and more - [Riverpod Generator](https://pub.dev/packages/riverpod_generator) and [Riverpod Lint](https://pub.dev/packages/riverpod_lint) for the latest Riverpod APIs - [GoRouter](https://pub.dev/packages/go_router) for navigation -- [Firebase Auth](https://pub.dev/packages/firebase_auth) and [Firebase UI Auth](https://pub.dev/packages/firebase_ui_auth) for authentication -- [Cloud Firestore](https://pub.dev/packages/cloud_firestore) as a realtime database -- [Firebase UI for Firestore](https://pub.dev/packages/firebase_ui_firestore) for the `FirestoreListView` widget with pagination support -- [RxDart](https://pub.dev/packages/rxdart) for combining multiple Firestore collections as needed -- [Intl](https://pub.dev/packages/intl) for currency, date, time formatting +- [Intl](https://pub.dev/packages/intl) for date, time formatting - [Mocktail](https://pub.dev/packages/mocktail) for testing - [Equatable](https://pub.dev/packages/equatable) to reduce boilerplate code in model classes +- [flutter_blue_plus](https://pub.dev/packages/flutter_blue_plus) to communicate with Charger and Vehicle via Bluetooth +- [Reflectable](https://pub.dev/packages/reflectable) to automate widget generation from bespoke objects See the [pubspec.yaml](pubspec.yaml) file for the complete list. -## Running the project with Firebase - -To use this project with Firebase, follow these steps: - -- Create a new project with the Firebase console -- Enable Firebase Authentication, along with the Email/Password Authentication Sign-in provider in the Firebase Console (Authentication > Sign-in method > Email/Password > Edit > Enable > Save) -- Enable Cloud Firestore - -Then, follow one of the two approaches below. 👇 +## Support +Application support by [Danlaw](https://danlawtechnologies.com/contactus). -### 1. Using the CLI +Platform support: +- Android: SDK 34+ +- iOS: iOS 17.0+ -Make sure you have the Firebase CLI and [FlutterFire CLI](https://pub.dev/packages/flutterfire_cli) installed. +Localization support: +- United States English +- Canadian French +- Mexican Spanish -Then run this on the terminal from the root of this project: +Currently application uses default language of the device. If device language is not supported defaults to Engligh. -- Run `firebase login` so you have access to the Firebase project you have created -- Run `flutterfire configure` and follow all the steps +## Design -For more info, follow this guide: +Current designs for this application can be found at [Figma](https://www.figma.com/design/Ji08JojX9t4N5U4uhjgH55/DAN003---EV-Charger-App?node-id=315-20822&t=r8H62xjxlTNpKI6r-0). -- [How to add Firebase to a Flutter app with FlutterFire CLI](https://codewithandrea.com/articles/flutter-firebase-flutterfire-cli/) +## Screenshots -### 2. Manual way (not recommended) +

+       +       + +

+

+       +       + +

+

+ + + +

-If you don't want to use FlutterFire CLI, follow these steps instead: -- Register separate iOS, Android, and web apps in the Firebase project settings. -- On Android, use `com.example.starter_architecture_flutter_firebase` as the package name. -- then, [download and copy](https://firebase.google.com/docs/flutter/setup#configure_an_android_app) `google-services.json` into `android/app`. -- On iOS, use `com.example.starterArchitectureFlutterFirebase` as the bundle ID. -- then, [download and copy](https://firebase.google.com/docs/flutter/setup#configure_an_ios_app) `GoogleService-Info.plist` into `iOS/Runner`, and add it to the Runner target in Xcode. +## License +Copyright © 2024. All rights reserved by [Danlaw](https://danlawtechnologies.com/terms-conditions). -That's it. Have fun! +

-## [License: MIT](LICENSE.md) +[Back to Top](#top) \ No newline at end of file diff --git a/android/.gitignore b/android/.gitignore deleted file mode 100644 index 6f568019..00000000 --- a/android/.gitignore +++ /dev/null @@ -1,13 +0,0 @@ -gradle-wrapper.jar -/.gradle -/captures/ -/gradlew -/gradlew.bat -/local.properties -GeneratedPluginRegistrant.java - -# Remember to never publicly share your keystore. -# See https://flutter.dev/docs/deployment/android#reference-the-keystore-from-the-app -key.properties -**/*.keystore -**/*.jks diff --git a/android/app/build.gradle b/android/app/build.gradle deleted file mode 100644 index 2c6b23e4..00000000 --- a/android/app/build.gradle +++ /dev/null @@ -1,67 +0,0 @@ -plugins { - id "com.android.application" - id "kotlin-android" - id "dev.flutter.flutter-gradle-plugin" -} - -def localProperties = new Properties() -def localPropertiesFile = rootProject.file('local.properties') -if (localPropertiesFile.exists()) { - localPropertiesFile.withReader('UTF-8') { reader -> - localProperties.load(reader) - } -} - -def flutterVersionCode = localProperties.getProperty('flutter.versionCode') -if (flutterVersionCode == null) { - flutterVersionCode = '1' -} - -def flutterVersionName = localProperties.getProperty('flutter.versionName') -if (flutterVersionName == null) { - flutterVersionName = '1.0' -} - -android { - namespace "com.example.starter_architecture_flutter_firebase" - compileSdkVersion flutter.compileSdkVersion - ndkVersion flutter.ndkVersion - - compileOptions { - sourceCompatibility JavaVersion.VERSION_1_8 - targetCompatibility JavaVersion.VERSION_1_8 - } - - kotlinOptions { - jvmTarget = '1.8' - } - - sourceSets { - main.java.srcDirs += 'src/main/kotlin' - } - - defaultConfig { - // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). - applicationId "com.example.starter_architecture_flutter_firebase" - // You can update the following values to match your application needs. - // For more information, see: https://docs.flutter.dev/deployment/android#reviewing-the-gradle-build-configuration. - minSdkVersion localProperties.getProperty('flutter.minSdkVersion').toInteger() - targetSdkVersion localProperties.getProperty('flutter.targetSdkVersion').toInteger() - versionCode flutterVersionCode.toInteger() - versionName flutterVersionName - } - - buildTypes { - release { - // TODO: Add your own signing config for the release build. - // Signing with the debug keys for now, so `flutter run --release` works. - signingConfig signingConfigs.debug - } - } -} - -flutter { - source '../..' -} - -dependencies {} diff --git a/android/app/src/debug/AndroidManifest.xml b/android/app/src/debug/AndroidManifest.xml deleted file mode 100644 index 399f6981..00000000 --- a/android/app/src/debug/AndroidManifest.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - - diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml deleted file mode 100644 index be681d49..00000000 --- a/android/app/src/main/AndroidManifest.xml +++ /dev/null @@ -1,33 +0,0 @@ - - - - - - - - - - - - - - diff --git a/android/app/src/main/kotlin/com/example/starter_architecture_flutter_firebase/MainActivity.kt b/android/app/src/main/kotlin/com/example/starter_architecture_flutter_firebase/MainActivity.kt deleted file mode 100644 index 057eaaf7..00000000 --- a/android/app/src/main/kotlin/com/example/starter_architecture_flutter_firebase/MainActivity.kt +++ /dev/null @@ -1,6 +0,0 @@ -package com.example.starter_architecture_flutter_firebase - -import io.flutter.embedding.android.FlutterActivity - -class MainActivity: FlutterActivity() { -} diff --git a/android/app/src/main/res/drawable-v21/launch_background.xml b/android/app/src/main/res/drawable-v21/launch_background.xml deleted file mode 100644 index f74085f3..00000000 --- a/android/app/src/main/res/drawable-v21/launch_background.xml +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - - - diff --git a/android/app/src/main/res/drawable/launch_background.xml b/android/app/src/main/res/drawable/launch_background.xml deleted file mode 100644 index 304732f8..00000000 --- a/android/app/src/main/res/drawable/launch_background.xml +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - - - diff --git a/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/android/app/src/main/res/mipmap-hdpi/ic_launcher.png deleted file mode 100644 index db77bb4b..00000000 Binary files a/android/app/src/main/res/mipmap-hdpi/ic_launcher.png and /dev/null differ diff --git a/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/android/app/src/main/res/mipmap-mdpi/ic_launcher.png deleted file mode 100644 index 17987b79..00000000 Binary files a/android/app/src/main/res/mipmap-mdpi/ic_launcher.png and /dev/null differ diff --git a/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png deleted file mode 100644 index 09d43914..00000000 Binary files a/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png and /dev/null differ diff --git a/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png deleted file mode 100644 index d5f1c8d3..00000000 Binary files a/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png and /dev/null differ diff --git a/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png deleted file mode 100644 index 4d6372ee..00000000 Binary files a/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png and /dev/null differ diff --git a/android/app/src/main/res/values-night/styles.xml b/android/app/src/main/res/values-night/styles.xml deleted file mode 100644 index 06952be7..00000000 --- a/android/app/src/main/res/values-night/styles.xml +++ /dev/null @@ -1,18 +0,0 @@ - - - - - - - diff --git a/android/app/src/main/res/values/styles.xml b/android/app/src/main/res/values/styles.xml deleted file mode 100644 index cb1ef880..00000000 --- a/android/app/src/main/res/values/styles.xml +++ /dev/null @@ -1,18 +0,0 @@ - - - - - - - diff --git a/android/app/src/profile/AndroidManifest.xml b/android/app/src/profile/AndroidManifest.xml deleted file mode 100644 index 399f6981..00000000 --- a/android/app/src/profile/AndroidManifest.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - - diff --git a/android/build.gradle b/android/build.gradle deleted file mode 100644 index f7eb7f63..00000000 --- a/android/build.gradle +++ /dev/null @@ -1,31 +0,0 @@ -buildscript { - ext.kotlin_version = '1.7.10' - repositories { - google() - mavenCentral() - } - - dependencies { - classpath 'com.android.tools.build:gradle:7.3.0' - classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" - } -} - -allprojects { - repositories { - google() - mavenCentral() - } -} - -rootProject.buildDir = '../build' -subprojects { - project.buildDir = "${rootProject.buildDir}/${project.name}" -} -subprojects { - project.evaluationDependsOn(':app') -} - -tasks.register("clean", Delete) { - delete rootProject.buildDir -} diff --git a/android/gradle.properties b/android/gradle.properties deleted file mode 100644 index 94adc3a3..00000000 --- a/android/gradle.properties +++ /dev/null @@ -1,3 +0,0 @@ -org.gradle.jvmargs=-Xmx1536M -android.useAndroidX=true -android.enableJetifier=true diff --git a/android/gradle/wrapper/gradle-wrapper.properties b/android/gradle/wrapper/gradle-wrapper.properties deleted file mode 100644 index 3c472b99..00000000 --- a/android/gradle/wrapper/gradle-wrapper.properties +++ /dev/null @@ -1,5 +0,0 @@ -distributionBase=GRADLE_USER_HOME -distributionPath=wrapper/dists -zipStoreBase=GRADLE_USER_HOME -zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.5-all.zip diff --git a/android/settings.gradle b/android/settings.gradle deleted file mode 100644 index 55c4ca8b..00000000 --- a/android/settings.gradle +++ /dev/null @@ -1,20 +0,0 @@ -pluginManagement { - def flutterSdkPath = { - def properties = new Properties() - file("local.properties").withInputStream { properties.load(it) } - def flutterSdkPath = properties.getProperty("flutter.sdk") - assert flutterSdkPath != null, "flutter.sdk not set in local.properties" - return flutterSdkPath - } - settings.ext.flutterSdkPath = flutterSdkPath() - - includeBuild("${settings.ext.flutterSdkPath}/packages/flutter_tools/gradle") - - plugins { - id "dev.flutter.flutter-gradle-plugin" version "1.0.0" apply false - } -} - -include ":app" - -apply from: "${settings.ext.flutterSdkPath}/packages/flutter_tools/gradle/app_plugin_loader.gradle" diff --git a/assets/AppIcon-no_background.svg b/assets/AppIcon-no_background.svg new file mode 100644 index 00000000..fd6335bd --- /dev/null +++ b/assets/AppIcon-no_background.svg @@ -0,0 +1,41 @@ + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/assets/add_button_icon.svg b/assets/add_button_icon.svg new file mode 100644 index 00000000..0a3a6b5b --- /dev/null +++ b/assets/add_button_icon.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/bottom_navigation_bar/account.svg b/assets/bottom_navigation_bar/account.svg new file mode 100644 index 00000000..bcc8d3de --- /dev/null +++ b/assets/bottom_navigation_bar/account.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/bottom_navigation_bar/alerts.svg b/assets/bottom_navigation_bar/alerts.svg new file mode 100644 index 00000000..6833cf6f --- /dev/null +++ b/assets/bottom_navigation_bar/alerts.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/bottom_navigation_bar/home.svg b/assets/bottom_navigation_bar/home.svg new file mode 100644 index 00000000..2c8e19f8 --- /dev/null +++ b/assets/bottom_navigation_bar/home.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/bottom_navigation_bar/reports.svg b/assets/bottom_navigation_bar/reports.svg new file mode 100644 index 00000000..79ac7e62 --- /dev/null +++ b/assets/bottom_navigation_bar/reports.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/bottom_navigation_bar/schedule.svg b/assets/bottom_navigation_bar/schedule.svg new file mode 100644 index 00000000..e9ae8a44 --- /dev/null +++ b/assets/bottom_navigation_bar/schedule.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/close_button.svg b/assets/close_button.svg new file mode 100644 index 00000000..e45ff8a1 --- /dev/null +++ b/assets/close_button.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/assets/detail_icon/check_mark.svg b/assets/detail_icon/check_mark.svg new file mode 100644 index 00000000..78746f22 --- /dev/null +++ b/assets/detail_icon/check_mark.svg @@ -0,0 +1,4 @@ + + + + diff --git a/assets/detail_icon/empty_mark.svg b/assets/detail_icon/empty_mark.svg new file mode 100644 index 00000000..cd229755 --- /dev/null +++ b/assets/detail_icon/empty_mark.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/detail_icon/left_arrow.svg b/assets/detail_icon/left_arrow.svg new file mode 100644 index 00000000..afe36748 --- /dev/null +++ b/assets/detail_icon/left_arrow.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/detail_icon/question_mark.svg b/assets/detail_icon/question_mark.svg new file mode 100644 index 00000000..b4d08d7b --- /dev/null +++ b/assets/detail_icon/question_mark.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/detail_icon/right_arrow.svg b/assets/detail_icon/right_arrow.svg new file mode 100644 index 00000000..e7e524b2 --- /dev/null +++ b/assets/detail_icon/right_arrow.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/detail_icon/wifi_charger_icon.svg b/assets/detail_icon/wifi_charger_icon.svg new file mode 100644 index 00000000..65f0d753 --- /dev/null +++ b/assets/detail_icon/wifi_charger_icon.svg @@ -0,0 +1,98 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/detail_icon/wifi_icon.svg b/assets/detail_icon/wifi_icon.svg new file mode 100644 index 00000000..60909455 --- /dev/null +++ b/assets/detail_icon/wifi_icon.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/external_link.svg b/assets/external_link.svg new file mode 100644 index 00000000..8cc586db --- /dev/null +++ b/assets/external_link.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/group.png b/assets/group.png new file mode 100644 index 00000000..7e5ecf65 Binary files /dev/null and b/assets/group.png differ diff --git a/assets/hamburger_menu_icon.svg b/assets/hamburger_menu_icon.svg new file mode 100644 index 00000000..a5e31bbd --- /dev/null +++ b/assets/hamburger_menu_icon.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/images/house.png b/assets/images/house.png new file mode 100644 index 00000000..6fb41519 Binary files /dev/null and b/assets/images/house.png differ diff --git a/assets/launch_icon.ico b/assets/launch_icon.ico new file mode 100644 index 00000000..b8523793 Binary files /dev/null and b/assets/launch_icon.ico differ diff --git a/assets/locale/en-US.json b/assets/locale/en-US.json new file mode 100644 index 00000000..48125adb --- /dev/null +++ b/assets/locale/en-US.json @@ -0,0 +1,455 @@ +{ + "common": { + "name": "Name", + "home": "Home", + "schedule": "Schedule", + "account": "Account", + "reports": "Reports", + "alerts": "Alerts", + "details": "Details", + "start": "Start", + "set": "Set", + "select": "Select", + "next": "Next", + "finish": "Finish", + "cancel": "Cancel", + "back": "Back", + "time": "Time", + "distance": "Distance", + "duration": "Duration", + "cost": "Cost", + "used": "Used", + "remove": "Remove", + "centerQR": "Center QR Code in camera view", + "username": "Username", + "password": "Password", + "vehicle": "Vehicle", + "charger": "Charger", + "add": "Add", + "selected":"Selected" , + "unknonwn": "Unknown" + }, + "btn": { + "ok": "OK", + "getStarted": "Get Started", + "exitSetup": "Exit Setup", + "haveAccount": "Already have an account?", + "tryAgain": "Try Again", + "login": "Log In", + "loggingIn": "Logging In...", + "logout": "Logout", + "forgotPassword": "Forgot Password", + "resetPassword": "Reset Password", + "updatePassword": "Update Password", + "acceptEula": "I have read and accept the EULA", + "tryAnotherSsid": "Try Another SSID", + "openCamera": "Open Camera", + "addVehicleWDongle": "Add Vehicle with Dongle", + "removeVehicle": "Remove Vehicle", + "removeDongle": "Remove Dongle", + "removeCharger": "Remove Charger", + "removeHousehold":"Remove Household", + "changeIcon": "Change Icon", + "addHousehold": "Add Household", + "support": "Support", + "accessaries": "Accessories", + "appHome": "Go To App Home", + "scanQRCode": "Scan QR Code", + "orScanQRCode": "Or Scan QR Code", + "tryAnotherWayToConnect": "Try Another Way To Connect", + "confirm": "Confirm", + "goBack": "Go Back", + "selectDiffNetwork": "Select A Different Network", + "loginPage": "Go to Log In Page" + }, + "hint": { + "choose": "Choose", + "enter": "Enter", + "vehicleName": "Enter Vehicle Name", + "nameYourCharger": "Name Your Charger", + "customRates":"Set Custom Rates", + "optional": "Optional", + "phoneNumber": "Enter Phone Number", + "email": "Enter Email", + "emailOrUsername": "Enter Email or Username", + "password": "Enter Password", + "address": "Enter Address", + "address2": "Address 2 (optional)", + "city": "Enter City", + "zipCode": "Enter Zip Code" + }, + "validation": { + "textfield": { + "username": "Enter username", + "validUsername": "Enter a valid username", + "password": "Enter a password", + "validPassword": "Enter a valid password", + "confPassword": "Entered password doesn't match", + "verificationCode": "Enter verification code", + "validVerificationCode": "Enter a valid verification code", + "chargerName": "Enter a name for your charger", + "validChargerName": "Enter a valid name for your charger", + "vehicleName": "Enter vehicle name", + "validVehicleName": "Enter a valid name for your vehicle", + "householdName": "Enter a valid household name", + "streetAddress": "Enter a valid street address", + "city": "Enter a valid city", + "zipCode": "Enter a valid zip code", + "email": "Enter your email", + "validEmail": "Enter a valid email ID", + "phone": "Enter your phone number", + "validPhone": "Enter a valid phone number", + "lowMilesThreshold": "Enter a valid low miles threshold", + "arrivalAlertTime": "Enter valid time in seconds", + "minimumMilesThreshold": "Enter valid Minimum miles Threshold", + "maxChargeCost": "Enter a maximum charge cost", + "validMaxChargeCost": "Enter a valid maximum charge cost" + }, + "field": { + "breakerSize": "Select a Breaker Size", + "maxChargeCurrent": "Select a Max charge Current", + "reportingUnits": "Select reporting units", + "state": "Select a valid state", + "country": "Select a valid country", + "method": "Select a valid Pricing Method", + "utility": "Select a valid Utility", + "rateProgram": "Select a valid Rate Program", + "rate": "Enter valid rates", + "alertTime": "Please enter a valid alert time" + } + }, + "status": { + "charging": "Charging", + "idle": "Idle", + "pluggedIn": "Plugged in", + "finishedCharging": "Finished Charging", + "scheduled": "Scheduled", + "chargingError": "Charging Error" + }, + + "signalStrength": { + "strong": "Strong", + "moderate": "Moderate", + "weak": "Weak" + }, + + "connectionStatus": { + "connected": "Connected", + "disconnected": "Disconnected" + + }, + + "oob": { + "setup": "Setup", + "setupGuideline": "Follow the guided steps, below, to set up and start using your new charger.", + "accountWizDesc": "Set up address, contact and utility pricing", + "chargerWizDesc": "Install and configure your charger", + "vehicleWizDesc": "Connect dongle and configure your vehicle", + "setupComplete": "Your initial setup is complete", + "congratulations": "Congratulations" + }, + "dialog": { + "exitSetup": "Are you sure you want to exit setup?", + "resourceRemovalDesc": "This {resource} will be removed from your account.", + "removalConfirmation": "{resource} removed from your account" + }, + "charger": { + "wizard": { + "addChargerTitle": "Add Charger", + "pluginYourCharger": "Plug In Your Charger", + "configureCharger": "Configure Charger", + "pluginYourChargerDesc": "Once your charger is plugged in and the red, wifi light is on, you are ready to add it to your account.", + "connectYourCharger": "Connect Your Charger", + "bluetoothConnectionDesc": "Stand close to your charger and use the number on the side to select it from this list of available Bluetooth devices: ", + "bluetoothDevices": "Bluetooth Devices", + "connectYourChargerDesc": "Use your camera to scan the QR code on the size of your charger.", + "wifiSetup": "Set Up Charger WiFi", + "wifiSelectionDesc": "Select a WiFi connection for your charger", + "wifiNetworks": "Networks", + "wifiPasswordTitle": "WiFi Password for {name}", + "confirmConnection": "Confirm Charger Connection", + "confirmConnectionDesc": "The charger WiFi LED will blink Blue when connected to the Internet.", + "connecting": "Charger connecting to WiFi...", + "connected": "Charger Connected", + "failed": "Could not connect charger!" + }, + "startCharging": "Start Charging", + "stopCharging": "Stop Charging", + "scheduledStart": "Scheduled Start", + "scheduleCharging": "Schedule Charging", + "chargeRate": "Charge Rate", + "estChargeTime": "Est. Charge Time", + "estChargeCost": "Est. Charge Cost", + "completeBy": "Complete By", + "info": "Information", + "status": "Status", + "connectedVehicle": "Connected Vehicle", + "breakerSize": "Breaker Size", + "maxChargeCurrent": "Max Charge Current", + "modelNumber": "Model Number", + "serialNumber": "Serial Number", + "softwareVersion": "Software Version", + "panelLedLighting": "Panel LED Lighting", + "wifiSec": "WiFi Settings", + "wifi": { + "ssid": "SSID", + "mac": "MAC", + "connectionStatus": "Connection Status", + "signalStrength": "Signal Strength", + "rssi": "RSSI", + "ipAddress": "IP Address", + "lastContact": "Last Contact" + }, + "authSec": "Authorization", + "auth": { + "none": "None", + "always": "Ask every time", + "devicePresent": "When authorized device present" + }, + "specialOpsSec": "Special Operations", + "manageAlerts": "Manage Device Alerts", + "reboot": "Reboot Charger", + "factoryReset": "Restore Factory Defaults", + "resetFault": "Reset Charger Fault Error" + }, + "schedule": { + "options": "Schedule Options", + "custom": "Custom Schedule", + "onAllDay": "On All Day", + "scheduledCharging": "Scheduled Charging", + "smart": "Smart Schedule", + "monday": "Monday", + "tuesday": "Tuesday", + "wednesday": "Wednesday", + "thursday": "Thursday", + "friday": "Friday", + "saturday": "Saturday", + "sunday": "Sunday", + "selectHours": "Select the hours when charging is available", + "timeSelection": { + "ampm": "Charging will be available from {morningSession} and {eveningSession}. Please note all hours are in the charger's time zone.", + "am": "Charging will be available from {morningSession}. Please note all hours are in the charger's time zone.", + "pm": "Charging will be available from {eveningSession}. Please note all hours are in the charger's time zone.", + "none": "No charging hours available. Please configure your schedule." + } + }, + + "account": { + "phoneNumber": "Phone Number", + "email": "Email", + "wizard": { + "title": "Create Account", + "welcomeDesc": "Create your account with your home address and select your utility information.", + "createCredentialsDesc": "Create Your Username and Password", + "contactInformationDesc": "Default Contact Information" + }, + "reset": { + "resetPassSuccess":"Reset Password Success", + "forgotPassDesc": "Enter the username for your Danlaw account", + "checkYourEmailDesc": "Please check your email for a message from us with a code to reset your password.", + "resetPassSuccessDesc": "Your password was updated. Please log in with your new password" + }, + "confPassword": "Confirm Password", + "verification": "Verification Code" + }, + "household": { + "title": "Household", + "wizard": { + "title": "Create Household", + "nameYourHouseholdDesc": "Name your Household", + "householdAddressDesc": "Create Your Charger Household Address", + "useMyLocation": "Use my current location", + "pricingMethodDesc": "Select Charge Pricing Method", + "utilityCompanyDesc": "Select Utility Company", + "rateProgramDesc": "Select Rate Program", + "customRatesDesc": "Select Custom Rates", + "customRates": { + "subTitle": "Enter the rates per kWh for these standard rate times", + "weekdayDaytime": "Weekday Daytime Rate", + "weekdayNighttime": "Weekday Nighttime Rate", + "weekendRate": "Weekend Rate" + } + }, + "address": "Address", + "electricityPricing": "Electricity Pricing", + "chargers": "Chargers", + "streetAddress1": "Street Address 1", + "streetAddress2": "Street Address 2", + "city": "City", + "state": "State", + "zipCode": "Zip Code", + "country": "Country", + "pricingMethod": { + "short": "Method", + "long": "Pricing Method", + "chargerEstCost": "Charger Estimates Costs", + "utilityRates": "Utility company-based", + "manual": "Manual" + }, + "utility": "Utility", + "rateProgram": "Rate Program", + "rate": "Rate" + }, + "vehicle": { + "title": "Vehicle", + "wizard": { + "addVehicleTitle": "Add Vehicle", + "addDongle": "Add Dongle", + "connectDongle": "Connect Your Dongle", + "dongleDesc": "Locate the dongle you want to assign to your account", + "vehicleIconDesc": "Choose An Icon For Your Vehicle", + "scanDongleQR": "Scan Dongle QR Code", + "scanDongleQRDesc": "Use your camera to scan the QR code on the dongle.", + "installDongle": "Install Dongle", + "installDongleInVehicle": "Install dongle in your vehicle", + "installDongleInfo": "Once the dongle is plugged into your vehicle the app will confirm your connection.", + "noDongleFound": "No Dongle Found", + "installDongleLEDConf": "Make sure the LED on the dongle lights up. If not, please turn your vehicle on.", + "verifyingComms": "Verifying Communications", + "primaryUserInfo": "Enter Primary User Information", + "contactPhone": "Contact Phone", + "contactEmail": "Contact Email" + }, + "nameVehicle": "Name Your Vehicle", + "lastVehicleInfo": "Last Vehicle Attached Information", + "status": "Status", + "vin": "Vin", + "year": "Year", + "make": "Make", + "model": "Model", + "battery": "Battery", + "stateOfCharge": "State of Charge", + "batteryLife": "Battery Life", + "estMilesAvailable": "Estimated Miles Available", + "failureCodes": "Failure Codes", + "diagnosticsCodes": "Diagnostic Trouble Codes", + "dongleInfo": "Dongle Information", + "dongleId": "Dongle Id", + "lastConnected": "Last Connected", + "alerts": "Alerts", + "manageAlerts": "Manage Vehicle Alerts", + "ass": { + "title": "Active Safety System", + "abs": "Anti-lock Braking System (ABS)", + "esc": "Electronic Stability Control (ESC)", + "tc": "Traction Control", + "tpms": "Tire Pressure Monitory System Type (TPMS)", + "note": "Active Safety System Note" + }, + "eng": { + "title": "Engine", + "numCylinders": "Engine Number of Cylinders", + "displacementCC": "Displacement (CC)", + "displacementCI": "Displacement (CI)", + "displacementL": "Displacement (L)", + "engineModel": "Engine Model" + }, + "body": { + "title": "Exterior/Body", + "bodyClass": "Body Class", + "doors": "Doors", + "windows": "Windows" + } + }, + "reports": { + "vehicleEnergyReport": "Vehicle Energy Report", + "tw": { + "title": "Time Window", + "past31Days": "Past 31 Days", + "thisWeek": "This Week", + "thisMonth": "This Month", + "thisYear": "This year" + }, + "info": { + "barSelection": "Select a bar segment to see more details" + }, + "viewTrips": "View Trips", + "trips": "Trips", + "viewEvents": "View Charge Events", + "bar": { + "legendHome": "Home", + "legendpub": "Public", + "unknownVehicles": "Unknown Vehicles" + }, + "settings": "Settings", + "vehiclesInReport": "Vehicles In Report" + }, + "alerts": { + "charger": "Charger Alerts", + "vehicle": "Vehicle Alerts", + "options": "Alert Options", + "usePush": "Use Push Alerts", + "useText": "Send Text Alerts", + "useEmail": "Send Email Alerts", + "recipientPhone": "Recipient Phone", + "recipientEmail": "Recipient Email", + "pluginReminder": "Plug-in Reminder", + "chargingStatus": "Charging Status", + "nightly": "Nightly Alert", + "nightlyLowMiles": "Nightly Low Miles Alert", + "time": "Alert Time", + "lowMilesThreshold": "Low Miles Threshold", + "arrivalTime": "Arrival Alert Time (Seconds)", + "fullyCharged": "Vehicle Fully Charged Alert", + "chargingInterrupted": "Charging Interrupted Alert", + "minimumMilesMet": "Minimum Miles Met", + "minimumMilesThreshold": "Minimum Miles Threshold", + "highCost": "High Cost Alert", + "maxChargeCost": "Maximum Charge Cost" + }, + + "error": { + "noInfo": "No information provided" + }, + + "apiResponse": { + "success": { + "general": "Request successful.", + "fetch": "{resource} retrieved successfully.", + "fetchLoc": "Localized data retrieved successfully.", + "create": "{resource} created successfully.", + "update": "{resource} updated successfully.", + "delete": "{resource} deleted successfully.", + "remove": "{resource} removed successfully", + "auth": { + "login": "Logged in successfully.", + "logout": "Logged out successfully.", + "passwordReset": { + "codeSent": "Reset code sent successfully. Please check your email.", + "passwordUpdated": "Password reset successfully." + } + }, + "eula": { + "fetched": "EULA loaded successfully.", + "submitted": "EULA acceptance submitted successfully." + }, + "qrScanned": "QR code for {resource} scanned successfully.", + "bluetoothConnected": "Connected to {resource} via Bluetooth successfully.", + "operationCompleted": "{operation} on {resource} completed successfully." + }, + "error": { + "general": "An error occurred. Please try again later.", + "fetch": "Failed to retrieve {resource}. Please try again later.", + "fetchLoc": "Failed to retrieve Localized data. Defaulting to English.", + "create": "Failed to create {resource}. Please check your input and try again.", + "update": "Failed to update {resource}. Please check your input and try again.", + "delete": "Failed to delete {resource}. Please try again later.", + "missingParameter": "Oops! {field} is missing. Please provide all necessary information and try again.", + "invalidQR": "Invalid QR code for {resource}. Please try again.", + "limitReached": "You've reached the maximum limit of {limit} for {resource}.", + "unauthorized": "Unauthorized. Please log in again.", + "loginFailed": "Login failed. Please check your credentials and try again.", + "accessDenied": "Access forbidden. You don't have permission to perform this action.", + "notFound": "{Resource} not found. Please check and try again.", + "timeout": "Request timed out. Please try again later.", + "invalidData": "Unable to process the request. Please check your input and try again.", + "serverError": "Server error. Please try again later.", + "serviceUnavailable": "Service unavailable. Please try again later.", + "duplicateEntry": "A {resource} with this information already exists.", + "dependencyIssue": "Unable to perform this action due to dependencies on {resource}." + }, + "networkError": { + "general": "Unable to connect. Please check your internet connection." + } + } +} \ No newline at end of file diff --git a/assets/location.png b/assets/location.png new file mode 100644 index 00000000..b4d6fd09 Binary files /dev/null and b/assets/location.png differ diff --git a/assets/logo.png b/assets/logo.png new file mode 100644 index 00000000..e4e3f3c2 Binary files /dev/null and b/assets/logo.png differ diff --git a/assets/readme_assets/add_charger.png b/assets/readme_assets/add_charger.png new file mode 100644 index 00000000..31aa63ff Binary files /dev/null and b/assets/readme_assets/add_charger.png differ diff --git a/assets/readme_assets/add_dongle.png b/assets/readme_assets/add_dongle.png new file mode 100644 index 00000000..d013da5c Binary files /dev/null and b/assets/readme_assets/add_dongle.png differ diff --git a/assets/readme_assets/alert_config.png b/assets/readme_assets/alert_config.png new file mode 100644 index 00000000..8f0d9118 Binary files /dev/null and b/assets/readme_assets/alert_config.png differ diff --git a/assets/readme_assets/alert_main.png b/assets/readme_assets/alert_main.png new file mode 100644 index 00000000..cf687a39 Binary files /dev/null and b/assets/readme_assets/alert_main.png differ diff --git a/assets/readme_assets/landing_empty.png b/assets/readme_assets/landing_empty.png new file mode 100644 index 00000000..d07ec6f5 Binary files /dev/null and b/assets/readme_assets/landing_empty.png differ diff --git a/assets/readme_assets/landing_full.png b/assets/readme_assets/landing_full.png new file mode 100644 index 00000000..fd167765 Binary files /dev/null and b/assets/readme_assets/landing_full.png differ diff --git a/assets/readme_assets/report_main.png b/assets/readme_assets/report_main.png new file mode 100644 index 00000000..98e09339 Binary files /dev/null and b/assets/readme_assets/report_main.png differ diff --git a/assets/readme_assets/report_table.png b/assets/readme_assets/report_table.png new file mode 100644 index 00000000..7715c09e Binary files /dev/null and b/assets/readme_assets/report_table.png differ diff --git a/assets/readme_assets/start.png b/assets/readme_assets/start.png new file mode 100644 index 00000000..775a130f Binary files /dev/null and b/assets/readme_assets/start.png differ diff --git a/assets/setup_charger/bluetooth_icon.svg b/assets/setup_charger/bluetooth_icon.svg new file mode 100644 index 00000000..4719d4c1 --- /dev/null +++ b/assets/setup_charger/bluetooth_icon.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/setup_charger/connection_fail.svg b/assets/setup_charger/connection_fail.svg new file mode 100644 index 00000000..415a3d61 --- /dev/null +++ b/assets/setup_charger/connection_fail.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/assets/setup_charger/connection_success.svg b/assets/setup_charger/connection_success.svg new file mode 100644 index 00000000..37458d06 --- /dev/null +++ b/assets/setup_charger/connection_success.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/setup_charger/empty_circle_large.svg b/assets/setup_charger/empty_circle_large.svg new file mode 100644 index 00000000..1df6b4d8 --- /dev/null +++ b/assets/setup_charger/empty_circle_large.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/setup_charger/phone.png b/assets/setup_charger/phone.png new file mode 100644 index 00000000..1e035494 Binary files /dev/null and b/assets/setup_charger/phone.png differ diff --git a/assets/setup_charger/phone_background.png b/assets/setup_charger/phone_background.png new file mode 100644 index 00000000..631d9826 Binary files /dev/null and b/assets/setup_charger/phone_background.png differ diff --git a/assets/setup_charger/plug_charger.svg b/assets/setup_charger/plug_charger.svg new file mode 100644 index 00000000..b01ee937 --- /dev/null +++ b/assets/setup_charger/plug_charger.svg @@ -0,0 +1,186 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/setup_charger/qr_charger.png b/assets/setup_charger/qr_charger.png new file mode 100644 index 00000000..1a5742d6 Binary files /dev/null and b/assets/setup_charger/qr_charger.png differ diff --git a/assets/setup_charger/qr_charger.svg b/assets/setup_charger/qr_charger.svg new file mode 100644 index 00000000..7840d4dd --- /dev/null +++ b/assets/setup_charger/qr_charger.svg @@ -0,0 +1,55 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/setup_dongle/dongle.png b/assets/setup_dongle/dongle.png new file mode 100644 index 00000000..549d3a05 Binary files /dev/null and b/assets/setup_dongle/dongle.png differ diff --git a/assets/setup_dongle/install_image.png b/assets/setup_dongle/install_image.png new file mode 100644 index 00000000..9df7dcf8 Binary files /dev/null and b/assets/setup_dongle/install_image.png differ diff --git a/assets/setup_dongle/vehicle_icon.png b/assets/setup_dongle/vehicle_icon.png new file mode 100644 index 00000000..b3496e16 Binary files /dev/null and b/assets/setup_dongle/vehicle_icon.png differ diff --git a/assets/splash.jpg b/assets/splash.jpg new file mode 100644 index 00000000..3367ef3a Binary files /dev/null and b/assets/splash.jpg differ diff --git a/assets/splash.svg b/assets/splash.svg new file mode 100644 index 00000000..6a4459ed --- /dev/null +++ b/assets/splash.svg @@ -0,0 +1,52 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/vehicle_icon/motorcycle.svg b/assets/vehicle_icon/motorcycle.svg new file mode 100644 index 00000000..d6ca0ab5 --- /dev/null +++ b/assets/vehicle_icon/motorcycle.svg @@ -0,0 +1,4 @@ + + + + diff --git a/assets/vehicle_icon/pickup.svg b/assets/vehicle_icon/pickup.svg new file mode 100644 index 00000000..ce77e688 --- /dev/null +++ b/assets/vehicle_icon/pickup.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/vehicle_icon/sedan.svg b/assets/vehicle_icon/sedan.svg new file mode 100644 index 00000000..75715250 --- /dev/null +++ b/assets/vehicle_icon/sedan.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/vehicle_icon/suv.svg b/assets/vehicle_icon/suv.svg new file mode 100644 index 00000000..023f3cab --- /dev/null +++ b/assets/vehicle_icon/suv.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/vehicle_icon/van.svg b/assets/vehicle_icon/van.svg new file mode 100644 index 00000000..b2890511 --- /dev/null +++ b/assets/vehicle_icon/van.svg @@ -0,0 +1,3 @@ + + + diff --git a/bootstrap-with-starter-project.sh b/bootstrap-with-starter-project.sh new file mode 100755 index 00000000..569cedcb --- /dev/null +++ b/bootstrap-with-starter-project.sh @@ -0,0 +1,64 @@ +#!/bin/bash + +# Check if a destination path was provided +if [ "$#" -ne 1 ]; then + echo "Usage: $0 " + exit 1 +fi + +# Set the remote repository URL directly in the script +remote_repo_url="https://github.com/symphonize/flutter_starter_base_app.git" + +# Assign command line argument to variable for the destination path +destination_path="$1" + +# Expand the destination path to an absolute path, handling tilde and relative paths +if [[ "$destination_path" == "~"* ]]; then + # Handle tilde expansion manually + destination_path="$HOME${destination_path:1}" +fi + +# Resolve potential relative path (including `.`) to an absolute path +destination_path="$(cd "$destination_path" && pwd)" + +# Use system temp directory or fallback to /tmp +temp_repo="${TMPDIR:-/tmp}/temp_repo" + +# Prompt for confirmation before proceeding +echo "This will bootstrap '$destination_path' with the Flutter starter project from '$remote_repo_url'." +read -p "Are you sure you want to proceed? (y/n): " confirmation +if [[ "$confirmation" != "y" && "$confirmation" != "Y" ]]; then + echo "Operation canceled." + exit 0 +fi + +# Clone, archive, and extract +git clone --bare "$remote_repo_url" "$temp_repo" && \ +cd "$temp_repo" && \ +git archive master | tar -x -C "$destination_path" && \ +cd .. && \ +rm -rf "$temp_repo" + +# Derive new package name from the destination directory name +new_package_name=$(basename "$destination_path") + +# Derive old package name from the remote repository URL +old_package_name=$(basename "$remote_repo_url" .git) + +# Echo the old and new package names +echo "Old package name: $old_package_name" +echo "New package name: $new_package_name" + +# Update Flutter package names in pubspec.yaml +echo "Updating Flutter package names in pubspec.yaml files..." +find "$destination_path" -type f -name "pubspec.yaml" -exec sh -c 'sed -i "" "s/^name:.*/name: $1/" "$2" && echo "Updated $2"' _ "$new_package_name" {} \; + +# Update Flutter package imports in Dart files within the /lib directory +echo "Updating Flutter package imports in Dart files..." +lib_path="${destination_path}/lib" +if [ -d "$lib_path" ]; then + find "$lib_path" -type f -name "*.dart" -exec sh -c 'sed -i "" "s/package:$2\//package:$1\//g" "$3" && echo "Updated $3"' _ "$new_package_name" "$old_package_name" {} \; +fi + +# Inform user of success +echo "The destination has been successfully bootstrapped with the starter project." diff --git a/flutter_native_splash.yaml b/flutter_native_splash.yaml new file mode 100644 index 00000000..b34cc85c --- /dev/null +++ b/flutter_native_splash.yaml @@ -0,0 +1,11 @@ +# Save changes with dart run flutter_native_splash:create --path=flutter_native_splash.yaml +flutter_native_splash: + background_image: "assets/splash.jpg" + background_image_dark: "assets/splash.jpg" + android_12: + image: "assets/splash.jpg" + image_dark: "assets/splash.jpg" + fullscreen: true + android_gravity: center + ios_content_mode: center + color: "#0f1941" diff --git a/ios/.gitignore b/ios/.gitignore deleted file mode 100644 index 7a7f9873..00000000 --- a/ios/.gitignore +++ /dev/null @@ -1,34 +0,0 @@ -**/dgph -*.mode1v3 -*.mode2v3 -*.moved-aside -*.pbxuser -*.perspectivev3 -**/*sync/ -.sconsign.dblite -.tags* -**/.vagrant/ -**/DerivedData/ -Icon? -**/Pods/ -**/.symlinks/ -profile -xcuserdata -**/.generated/ -Flutter/App.framework -Flutter/Flutter.framework -Flutter/Flutter.podspec -Flutter/Generated.xcconfig -Flutter/ephemeral/ -Flutter/app.flx -Flutter/app.zip -Flutter/flutter_assets/ -Flutter/flutter_export_environment.sh -ServiceDefinitions.json -Runner/GeneratedPluginRegistrant.* - -# Exceptions to above rules. -!default.mode1v3 -!default.mode2v3 -!default.pbxuser -!default.perspectivev3 diff --git a/ios/Flutter/AppFrameworkInfo.plist b/ios/Flutter/AppFrameworkInfo.plist deleted file mode 100644 index 9625e105..00000000 --- a/ios/Flutter/AppFrameworkInfo.plist +++ /dev/null @@ -1,26 +0,0 @@ - - - - - CFBundleDevelopmentRegion - en - CFBundleExecutable - App - CFBundleIdentifier - io.flutter.flutter.app - CFBundleInfoDictionaryVersion - 6.0 - CFBundleName - App - CFBundlePackageType - FMWK - CFBundleShortVersionString - 1.0 - CFBundleSignature - ???? - CFBundleVersion - 1.0 - MinimumOSVersion - 11.0 - - diff --git a/ios/Flutter/Debug.xcconfig b/ios/Flutter/Debug.xcconfig deleted file mode 100644 index ec97fc6f..00000000 --- a/ios/Flutter/Debug.xcconfig +++ /dev/null @@ -1,2 +0,0 @@ -#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" -#include "Generated.xcconfig" diff --git a/ios/Flutter/Release.xcconfig b/ios/Flutter/Release.xcconfig deleted file mode 100644 index c4855bfe..00000000 --- a/ios/Flutter/Release.xcconfig +++ /dev/null @@ -1,2 +0,0 @@ -#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" -#include "Generated.xcconfig" diff --git a/ios/Podfile b/ios/Podfile deleted file mode 100644 index 1271d919..00000000 --- a/ios/Podfile +++ /dev/null @@ -1,63 +0,0 @@ -# Uncomment this line to define a global platform for your project -platform :ios, '11.0' - -# CocoaPods analytics sends network stats synchronously affecting flutter build latency. -ENV['COCOAPODS_DISABLE_STATS'] = 'true' - -project 'Runner', { - 'Debug' => :debug, - 'Profile' => :release, - 'Release' => :release, -} - -def flutter_root - generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__) - unless File.exist?(generated_xcode_build_settings_path) - raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first" - end - - File.foreach(generated_xcode_build_settings_path) do |line| - matches = line.match(/FLUTTER_ROOT\=(.*)/) - return matches[1].strip if matches - end - raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get" -end - -require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) - -flutter_ios_podfile_setup - -target 'Runner' do - pod 'FirebaseFirestore', :git => 'https://github.com/invertase/firestore-ios-sdk-frameworks.git', :tag => '10.18.0' - use_frameworks! - use_modular_headers! - - flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__)) -end - -post_install do |installer| - # Ensure pods also use the minimum deployment target set above - # https://stackoverflow.com/a/64385584/436422 - puts 'Determining pod project minimum deployment target' - - pods_project = installer.pods_project - deployment_target_key = 'IPHONEOS_DEPLOYMENT_TARGET' - deployment_targets = pods_project.build_configurations.map{ |config| config.build_settings[deployment_target_key] } - minimum_deployment_target = deployment_targets.min_by{ |version| Gem::Version.new(version) } - - puts 'Minimal deployment target is ' + minimum_deployment_target - puts 'Setting each pod deployment target to ' + minimum_deployment_target - - installer.pods_project.targets.each do |target| - flutter_additional_ios_build_settings(target) - target.build_configurations.each do |config| - #config.build_settings['ENABLE_BITCODE'] = 'NO' - config.build_settings[deployment_target_key] = minimum_deployment_target - # https://stackoverflow.com/a/77142190 - xcconfig_path = config.base_configuration_reference.real_path - xcconfig = File.read(xcconfig_path) - xcconfig_mod = xcconfig.gsub(/DT_TOOLCHAIN_DIR/, "TOOLCHAIN_DIR") - File.open(xcconfig_path, "w") { |file| file << xcconfig_mod } - end - end -end diff --git a/ios/Podfile.lock b/ios/Podfile.lock deleted file mode 100644 index 03e6925f..00000000 --- a/ios/Podfile.lock +++ /dev/null @@ -1,154 +0,0 @@ -PODS: - - cloud_firestore (4.14.0): - - Firebase/Firestore (= 10.18.0) - - firebase_core - - Flutter - - nanopb (< 2.30910.0, >= 2.30908.0) - - desktop_webview_auth (0.0.1): - - Flutter - - Firebase/Auth (10.18.0): - - Firebase/CoreOnly - - FirebaseAuth (~> 10.18.0) - - Firebase/CoreOnly (10.18.0): - - FirebaseCore (= 10.18.0) - - Firebase/DynamicLinks (10.18.0): - - Firebase/CoreOnly - - FirebaseDynamicLinks (~> 10.18.0) - - Firebase/Firestore (10.18.0): - - Firebase/CoreOnly - - FirebaseFirestore (~> 10.18.0) - - firebase_auth (4.16.0): - - Firebase/Auth (= 10.18.0) - - firebase_core - - Flutter - - firebase_core (2.24.2): - - Firebase/CoreOnly (= 10.18.0) - - Flutter - - firebase_dynamic_links (5.4.8): - - Firebase/DynamicLinks (= 10.18.0) - - firebase_core - - Flutter - - FirebaseAppCheckInterop (10.19.0) - - FirebaseAuth (10.18.0): - - FirebaseAppCheckInterop (~> 10.17) - - FirebaseCore (~> 10.0) - - GoogleUtilities/AppDelegateSwizzler (~> 7.8) - - GoogleUtilities/Environment (~> 7.8) - - GTMSessionFetcher/Core (< 4.0, >= 2.1) - - RecaptchaInterop (~> 100.0) - - FirebaseCore (10.18.0): - - FirebaseCoreInternal (~> 10.0) - - GoogleUtilities/Environment (~> 7.12) - - GoogleUtilities/Logger (~> 7.12) - - FirebaseCoreInternal (10.19.0): - - "GoogleUtilities/NSData+zlib (~> 7.8)" - - FirebaseDynamicLinks (10.18.0): - - FirebaseCore (~> 10.0) - - FirebaseFirestore (10.18.0): - - FirebaseFirestore/AutodetectLeveldb (= 10.18.0) - - FirebaseFirestore/AutodetectLeveldb (10.18.0): - - FirebaseFirestore/Base - - FirebaseFirestore/WithLeveldb - - FirebaseFirestore/Base (10.18.0) - - FirebaseFirestore/WithLeveldb (10.18.0): - - FirebaseFirestore/Base - - Flutter (1.0.0) - - GoogleUtilities/AppDelegateSwizzler (7.12.0): - - GoogleUtilities/Environment - - GoogleUtilities/Logger - - GoogleUtilities/Network - - GoogleUtilities/Environment (7.12.0): - - PromisesObjC (< 3.0, >= 1.2) - - GoogleUtilities/Logger (7.12.0): - - GoogleUtilities/Environment - - GoogleUtilities/Network (7.12.0): - - GoogleUtilities/Logger - - "GoogleUtilities/NSData+zlib" - - GoogleUtilities/Reachability - - "GoogleUtilities/NSData+zlib (7.12.0)" - - GoogleUtilities/Reachability (7.12.0): - - GoogleUtilities/Logger - - GTMSessionFetcher/Core (3.2.0) - - nanopb (2.30909.1): - - nanopb/decode (= 2.30909.1) - - nanopb/encode (= 2.30909.1) - - nanopb/decode (2.30909.1) - - nanopb/encode (2.30909.1) - - PromisesObjC (2.3.1) - - RecaptchaInterop (100.0.0) - - shared_preferences_foundation (0.0.1): - - Flutter - - FlutterMacOS - -DEPENDENCIES: - - cloud_firestore (from `.symlinks/plugins/cloud_firestore/ios`) - - desktop_webview_auth (from `.symlinks/plugins/desktop_webview_auth/ios`) - - firebase_auth (from `.symlinks/plugins/firebase_auth/ios`) - - firebase_core (from `.symlinks/plugins/firebase_core/ios`) - - firebase_dynamic_links (from `.symlinks/plugins/firebase_dynamic_links/ios`) - - FirebaseFirestore (from `https://github.com/invertase/firestore-ios-sdk-frameworks.git`, tag `10.18.0`) - - Flutter (from `Flutter`) - - shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`) - -SPEC REPOS: - trunk: - - Firebase - - FirebaseAppCheckInterop - - FirebaseAuth - - FirebaseCore - - FirebaseCoreInternal - - FirebaseDynamicLinks - - GoogleUtilities - - GTMSessionFetcher - - nanopb - - PromisesObjC - - RecaptchaInterop - -EXTERNAL SOURCES: - cloud_firestore: - :path: ".symlinks/plugins/cloud_firestore/ios" - desktop_webview_auth: - :path: ".symlinks/plugins/desktop_webview_auth/ios" - firebase_auth: - :path: ".symlinks/plugins/firebase_auth/ios" - firebase_core: - :path: ".symlinks/plugins/firebase_core/ios" - firebase_dynamic_links: - :path: ".symlinks/plugins/firebase_dynamic_links/ios" - FirebaseFirestore: - :git: https://github.com/invertase/firestore-ios-sdk-frameworks.git - :tag: 10.18.0 - Flutter: - :path: Flutter - shared_preferences_foundation: - :path: ".symlinks/plugins/shared_preferences_foundation/darwin" - -CHECKOUT OPTIONS: - FirebaseFirestore: - :git: https://github.com/invertase/firestore-ios-sdk-frameworks.git - :tag: 10.18.0 - -SPEC CHECKSUMS: - cloud_firestore: 73eece22ce25a0565238c283ee9990f1618d8063 - desktop_webview_auth: d645139460ef203d50bd0cdb33356785dd939cce - Firebase: 414ad272f8d02dfbf12662a9d43f4bba9bec2a06 - firebase_auth: 8e9ec02991ca4659111cc671c84d0c010b6bfb26 - firebase_core: 0af4a2b24f62071f9bf283691c0ee41556dcb3f5 - firebase_dynamic_links: b626a11f5eb02033981ae377377c3f297eb4c1b0 - FirebaseAppCheckInterop: 37884781f3e16a1ba47e7ec80a1e805f987788e3 - FirebaseAuth: 12314b438fa76048540c8fb86d6cfc9e08595176 - FirebaseCore: 2322423314d92f946219c8791674d2f3345b598f - FirebaseCoreInternal: b444828ea7cfd594fca83046b95db98a2be4f290 - FirebaseDynamicLinks: c37307441c53838d66a9650dabca9e0459502527 - FirebaseFirestore: 584e3f563142f63d20e9ec9c505370d674d44eba - Flutter: f04841e97a9d0b0a8025694d0796dd46242b2854 - GoogleUtilities: 0759d1a57ebb953965c2dfe0ba4c82e95ccc2e34 - GTMSessionFetcher: 41b9ef0b4c08a6db4b7eb51a21ae5183ec99a2c8 - nanopb: d4d75c12cd1316f4a64e3c6963f879ecd4b5e0d5 - PromisesObjC: c50d2056b5253dadbd6c2bea79b0674bd5a52fa4 - RecaptchaInterop: 7d1a4a01a6b2cb1610a47ef3f85f0c411434cb21 - shared_preferences_foundation: 5b919d13b803cadd15ed2dc053125c68730e5126 - -PODFILE CHECKSUM: 960305be9bb7690791069373e00dd3df7a0aae70 - -COCOAPODS: 1.14.2 diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj deleted file mode 100644 index 0f6ca4ee..00000000 --- a/ios/Runner.xcodeproj/project.pbxproj +++ /dev/null @@ -1,577 +0,0 @@ -// !$*UTF8*$! -{ - archiveVersion = 1; - classes = { - }; - objectVersion = 54; - objects = { - -/* Begin PBXBuildFile section */ - 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; - 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; - 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; }; - 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; - 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; - 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; - FDFBBE3698D6DEE111B2295A /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = BE5E729672CE82DA371BE171 /* Pods_Runner.framework */; }; - FEBDC9609C680EC02937034B /* GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = D4DF1EAB0E660ABB5816D63F /* GoogleService-Info.plist */; }; -/* End PBXBuildFile section */ - -/* Begin PBXCopyFilesBuildPhase section */ - 9705A1C41CF9048500538489 /* Embed Frameworks */ = { - isa = PBXCopyFilesBuildPhase; - buildActionMask = 2147483647; - dstPath = ""; - dstSubfolderSpec = 10; - files = ( - ); - name = "Embed Frameworks"; - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXCopyFilesBuildPhase section */ - -/* Begin PBXFileReference section */ - 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; }; - 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; - 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; - 4717B7480637AA09CAEB3FC0 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; - 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = ""; }; - 74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; - 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; - 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; }; - 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = ""; }; - 97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; }; - 97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; - 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; - 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; - 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; - B1282A8CCA46FCB860DC6348 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; - BE5E729672CE82DA371BE171 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; - D1CF56C09A84454F93ABC256 /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = ""; }; - D4DF1EAB0E660ABB5816D63F /* GoogleService-Info.plist */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.xml; name = "GoogleService-Info.plist"; path = "Runner/GoogleService-Info.plist"; sourceTree = ""; }; -/* End PBXFileReference section */ - -/* Begin PBXFrameworksBuildPhase section */ - 97C146EB1CF9000F007C117D /* Frameworks */ = { - isa = PBXFrameworksBuildPhase; - buildActionMask = 2147483647; - files = ( - FDFBBE3698D6DEE111B2295A /* Pods_Runner.framework in Frameworks */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXFrameworksBuildPhase section */ - -/* Begin PBXGroup section */ - 9740EEB11CF90186004384FC /* Flutter */ = { - isa = PBXGroup; - children = ( - 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */, - 9740EEB21CF90195004384FC /* Debug.xcconfig */, - 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, - 9740EEB31CF90195004384FC /* Generated.xcconfig */, - ); - name = Flutter; - sourceTree = ""; - }; - 97C146E51CF9000F007C117D = { - isa = PBXGroup; - children = ( - 9740EEB11CF90186004384FC /* Flutter */, - 97C146F01CF9000F007C117D /* Runner */, - 97C146EF1CF9000F007C117D /* Products */, - D4DF1EAB0E660ABB5816D63F /* GoogleService-Info.plist */, - F90C4DD405ACFD15095CAEB7 /* Pods */, - BB5370A4696621AC47A50471 /* Frameworks */, - ); - sourceTree = ""; - }; - 97C146EF1CF9000F007C117D /* Products */ = { - isa = PBXGroup; - children = ( - 97C146EE1CF9000F007C117D /* Runner.app */, - ); - name = Products; - sourceTree = ""; - }; - 97C146F01CF9000F007C117D /* Runner */ = { - isa = PBXGroup; - children = ( - 97C146FA1CF9000F007C117D /* Main.storyboard */, - 97C146FD1CF9000F007C117D /* Assets.xcassets */, - 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */, - 97C147021CF9000F007C117D /* Info.plist */, - 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */, - 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */, - 74858FAE1ED2DC5600515810 /* AppDelegate.swift */, - 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */, - ); - path = Runner; - sourceTree = ""; - }; - BB5370A4696621AC47A50471 /* Frameworks */ = { - isa = PBXGroup; - children = ( - BE5E729672CE82DA371BE171 /* Pods_Runner.framework */, - ); - name = Frameworks; - sourceTree = ""; - }; - F90C4DD405ACFD15095CAEB7 /* Pods */ = { - isa = PBXGroup; - children = ( - B1282A8CCA46FCB860DC6348 /* Pods-Runner.debug.xcconfig */, - 4717B7480637AA09CAEB3FC0 /* Pods-Runner.release.xcconfig */, - D1CF56C09A84454F93ABC256 /* Pods-Runner.profile.xcconfig */, - ); - name = Pods; - path = Pods; - sourceTree = ""; - }; -/* End PBXGroup section */ - -/* Begin PBXNativeTarget section */ - 97C146ED1CF9000F007C117D /* Runner */ = { - isa = PBXNativeTarget; - buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; - buildPhases = ( - 703CB65CA5A94374E71D4B9A /* [CP] Check Pods Manifest.lock */, - 9740EEB61CF901F6004384FC /* Run Script */, - 97C146EA1CF9000F007C117D /* Sources */, - 97C146EB1CF9000F007C117D /* Frameworks */, - 97C146EC1CF9000F007C117D /* Resources */, - 9705A1C41CF9048500538489 /* Embed Frameworks */, - 3B06AD1E1E4923F5004D2608 /* Thin Binary */, - 48CDBEA72553743FA74C2372 /* [CP] Embed Pods Frameworks */, - 3FB93D246001D3A79E3BB749 /* [CP] Copy Pods Resources */, - ); - buildRules = ( - ); - dependencies = ( - ); - name = Runner; - productName = Runner; - productReference = 97C146EE1CF9000F007C117D /* Runner.app */; - productType = "com.apple.product-type.application"; - }; -/* End PBXNativeTarget section */ - -/* Begin PBXProject section */ - 97C146E61CF9000F007C117D /* Project object */ = { - isa = PBXProject; - attributes = { - LastUpgradeCheck = 1430; - ORGANIZATIONNAME = ""; - TargetAttributes = { - 97C146ED1CF9000F007C117D = { - CreatedOnToolsVersion = 7.3.1; - LastSwiftMigration = 1100; - }; - }; - }; - buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */; - compatibilityVersion = "Xcode 9.3"; - developmentRegion = en; - hasScannedForEncodings = 0; - knownRegions = ( - en, - Base, - ); - mainGroup = 97C146E51CF9000F007C117D; - productRefGroup = 97C146EF1CF9000F007C117D /* Products */; - projectDirPath = ""; - projectRoot = ""; - targets = ( - 97C146ED1CF9000F007C117D /* Runner */, - ); - }; -/* End PBXProject section */ - -/* Begin PBXResourcesBuildPhase section */ - 97C146EC1CF9000F007C117D /* Resources */ = { - isa = PBXResourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */, - 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */, - 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */, - 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */, - FEBDC9609C680EC02937034B /* GoogleService-Info.plist in Resources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXResourcesBuildPhase section */ - -/* Begin PBXShellScriptBuildPhase section */ - 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { - isa = PBXShellScriptBuildPhase; - alwaysOutOfDate = 1; - buildActionMask = 2147483647; - files = ( - ); - inputPaths = ( - "${TARGET_BUILD_DIR}/${INFOPLIST_PATH}", - ); - name = "Thin Binary"; - outputPaths = ( - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin"; - }; - 3FB93D246001D3A79E3BB749 /* [CP] Copy Pods Resources */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputFileListPaths = ( - "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-input-files.xcfilelist", - ); - name = "[CP] Copy Pods Resources"; - outputFileListPaths = ( - "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-output-files.xcfilelist", - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh\"\n"; - showEnvVarsInLog = 0; - }; - 48CDBEA72553743FA74C2372 /* [CP] Embed Pods Frameworks */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputFileListPaths = ( - "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist", - ); - name = "[CP] Embed Pods Frameworks"; - outputFileListPaths = ( - "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist", - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; - showEnvVarsInLog = 0; - }; - 703CB65CA5A94374E71D4B9A /* [CP] Check Pods Manifest.lock */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputFileListPaths = ( - ); - inputPaths = ( - "${PODS_PODFILE_DIR_PATH}/Podfile.lock", - "${PODS_ROOT}/Manifest.lock", - ); - name = "[CP] Check Pods Manifest.lock"; - outputFileListPaths = ( - ); - outputPaths = ( - "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt", - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; - showEnvVarsInLog = 0; - }; - 9740EEB61CF901F6004384FC /* Run Script */ = { - isa = PBXShellScriptBuildPhase; - alwaysOutOfDate = 1; - buildActionMask = 2147483647; - files = ( - ); - inputPaths = ( - ); - name = "Run Script"; - outputPaths = ( - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build"; - }; -/* End PBXShellScriptBuildPhase section */ - -/* Begin PBXSourcesBuildPhase section */ - 97C146EA1CF9000F007C117D /* Sources */ = { - isa = PBXSourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */, - 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXSourcesBuildPhase section */ - -/* Begin PBXVariantGroup section */ - 97C146FA1CF9000F007C117D /* Main.storyboard */ = { - isa = PBXVariantGroup; - children = ( - 97C146FB1CF9000F007C117D /* Base */, - ); - name = Main.storyboard; - sourceTree = ""; - }; - 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = { - isa = PBXVariantGroup; - children = ( - 97C147001CF9000F007C117D /* Base */, - ); - name = LaunchScreen.storyboard; - sourceTree = ""; - }; -/* End PBXVariantGroup section */ - -/* Begin XCBuildConfiguration section */ - 249021D3217E4FDB00AE95B9 /* Profile */ = { - isa = XCBuildConfiguration; - buildSettings = { - ALWAYS_SEARCH_USER_PATHS = NO; - CLANG_ANALYZER_NONNULL = YES; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; - CLANG_CXX_LIBRARY = "libc++"; - CLANG_ENABLE_MODULES = YES; - CLANG_ENABLE_OBJC_ARC = YES; - CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; - CLANG_WARN_BOOL_CONVERSION = YES; - CLANG_WARN_COMMA = YES; - CLANG_WARN_CONSTANT_CONVERSION = YES; - CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; - CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; - CLANG_WARN_EMPTY_BODY = YES; - CLANG_WARN_ENUM_CONVERSION = YES; - CLANG_WARN_INFINITE_RECURSION = YES; - CLANG_WARN_INT_CONVERSION = YES; - CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; - CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; - CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; - CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; - CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; - CLANG_WARN_STRICT_PROTOTYPES = YES; - CLANG_WARN_SUSPICIOUS_MOVE = YES; - CLANG_WARN_UNREACHABLE_CODE = YES; - CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; - "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; - COPY_PHASE_STRIP = NO; - DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; - ENABLE_NS_ASSERTIONS = NO; - ENABLE_STRICT_OBJC_MSGSEND = YES; - GCC_C_LANGUAGE_STANDARD = gnu99; - GCC_NO_COMMON_BLOCKS = YES; - GCC_WARN_64_TO_32_BIT_CONVERSION = YES; - GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; - GCC_WARN_UNDECLARED_SELECTOR = YES; - GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; - GCC_WARN_UNUSED_FUNCTION = YES; - GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 11.0; - MTL_ENABLE_DEBUG_INFO = NO; - SDKROOT = iphoneos; - SUPPORTED_PLATFORMS = iphoneos; - TARGETED_DEVICE_FAMILY = "1,2"; - VALIDATE_PRODUCT = YES; - }; - name = Profile; - }; - 249021D4217E4FDB00AE95B9 /* Profile */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; - buildSettings = { - ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; - CLANG_ENABLE_MODULES = YES; - CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; - DEVELOPMENT_TEAM = M54ZVB688G; - ENABLE_BITCODE = NO; - INFOPLIST_FILE = Runner/Info.plist; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/Frameworks", - ); - PRODUCT_BUNDLE_IDENTIFIER = com.example.starterArchitectureFlutterFirebase; - PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; - SWIFT_VERSION = 5.0; - VERSIONING_SYSTEM = "apple-generic"; - }; - name = Profile; - }; - 97C147031CF9000F007C117D /* Debug */ = { - isa = XCBuildConfiguration; - buildSettings = { - ALWAYS_SEARCH_USER_PATHS = NO; - CLANG_ANALYZER_NONNULL = YES; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; - CLANG_CXX_LIBRARY = "libc++"; - CLANG_ENABLE_MODULES = YES; - CLANG_ENABLE_OBJC_ARC = YES; - CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; - CLANG_WARN_BOOL_CONVERSION = YES; - CLANG_WARN_COMMA = YES; - CLANG_WARN_CONSTANT_CONVERSION = YES; - CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; - CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; - CLANG_WARN_EMPTY_BODY = YES; - CLANG_WARN_ENUM_CONVERSION = YES; - CLANG_WARN_INFINITE_RECURSION = YES; - CLANG_WARN_INT_CONVERSION = YES; - CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; - CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; - CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; - CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; - CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; - CLANG_WARN_STRICT_PROTOTYPES = YES; - CLANG_WARN_SUSPICIOUS_MOVE = YES; - CLANG_WARN_UNREACHABLE_CODE = YES; - CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; - "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; - COPY_PHASE_STRIP = NO; - DEBUG_INFORMATION_FORMAT = dwarf; - ENABLE_STRICT_OBJC_MSGSEND = YES; - ENABLE_TESTABILITY = YES; - GCC_C_LANGUAGE_STANDARD = gnu99; - GCC_DYNAMIC_NO_PIC = NO; - GCC_NO_COMMON_BLOCKS = YES; - GCC_OPTIMIZATION_LEVEL = 0; - GCC_PREPROCESSOR_DEFINITIONS = ( - "DEBUG=1", - "$(inherited)", - ); - GCC_WARN_64_TO_32_BIT_CONVERSION = YES; - GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; - GCC_WARN_UNDECLARED_SELECTOR = YES; - GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; - GCC_WARN_UNUSED_FUNCTION = YES; - GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 11.0; - MTL_ENABLE_DEBUG_INFO = YES; - ONLY_ACTIVE_ARCH = YES; - SDKROOT = iphoneos; - TARGETED_DEVICE_FAMILY = "1,2"; - }; - name = Debug; - }; - 97C147041CF9000F007C117D /* Release */ = { - isa = XCBuildConfiguration; - buildSettings = { - ALWAYS_SEARCH_USER_PATHS = NO; - CLANG_ANALYZER_NONNULL = YES; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; - CLANG_CXX_LIBRARY = "libc++"; - CLANG_ENABLE_MODULES = YES; - CLANG_ENABLE_OBJC_ARC = YES; - CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; - CLANG_WARN_BOOL_CONVERSION = YES; - CLANG_WARN_COMMA = YES; - CLANG_WARN_CONSTANT_CONVERSION = YES; - CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; - CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; - CLANG_WARN_EMPTY_BODY = YES; - CLANG_WARN_ENUM_CONVERSION = YES; - CLANG_WARN_INFINITE_RECURSION = YES; - CLANG_WARN_INT_CONVERSION = YES; - CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; - CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; - CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; - CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; - CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; - CLANG_WARN_STRICT_PROTOTYPES = YES; - CLANG_WARN_SUSPICIOUS_MOVE = YES; - CLANG_WARN_UNREACHABLE_CODE = YES; - CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; - "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; - COPY_PHASE_STRIP = NO; - DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; - ENABLE_NS_ASSERTIONS = NO; - ENABLE_STRICT_OBJC_MSGSEND = YES; - GCC_C_LANGUAGE_STANDARD = gnu99; - GCC_NO_COMMON_BLOCKS = YES; - GCC_WARN_64_TO_32_BIT_CONVERSION = YES; - GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; - GCC_WARN_UNDECLARED_SELECTOR = YES; - GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; - GCC_WARN_UNUSED_FUNCTION = YES; - GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 11.0; - MTL_ENABLE_DEBUG_INFO = NO; - SDKROOT = iphoneos; - SUPPORTED_PLATFORMS = iphoneos; - SWIFT_COMPILATION_MODE = wholemodule; - SWIFT_OPTIMIZATION_LEVEL = "-O"; - TARGETED_DEVICE_FAMILY = "1,2"; - VALIDATE_PRODUCT = YES; - }; - name = Release; - }; - 97C147061CF9000F007C117D /* Debug */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; - buildSettings = { - ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; - CLANG_ENABLE_MODULES = YES; - CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; - DEVELOPMENT_TEAM = M54ZVB688G; - ENABLE_BITCODE = NO; - INFOPLIST_FILE = Runner/Info.plist; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/Frameworks", - ); - PRODUCT_BUNDLE_IDENTIFIER = com.example.starterArchitectureFlutterFirebase; - PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; - SWIFT_OPTIMIZATION_LEVEL = "-Onone"; - SWIFT_VERSION = 5.0; - VERSIONING_SYSTEM = "apple-generic"; - }; - name = Debug; - }; - 97C147071CF9000F007C117D /* Release */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; - buildSettings = { - ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; - CLANG_ENABLE_MODULES = YES; - CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; - DEVELOPMENT_TEAM = M54ZVB688G; - ENABLE_BITCODE = NO; - INFOPLIST_FILE = Runner/Info.plist; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/Frameworks", - ); - PRODUCT_BUNDLE_IDENTIFIER = com.example.starterArchitectureFlutterFirebase; - PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; - SWIFT_VERSION = 5.0; - VERSIONING_SYSTEM = "apple-generic"; - }; - name = Release; - }; -/* End XCBuildConfiguration section */ - -/* Begin XCConfigurationList section */ - 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - 97C147031CF9000F007C117D /* Debug */, - 97C147041CF9000F007C117D /* Release */, - 249021D3217E4FDB00AE95B9 /* Profile */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; - 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - 97C147061CF9000F007C117D /* Debug */, - 97C147071CF9000F007C117D /* Release */, - 249021D4217E4FDB00AE95B9 /* Profile */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; -/* End XCConfigurationList section */ - }; - rootObject = 97C146E61CF9000F007C117D /* Project object */; -} diff --git a/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata deleted file mode 100644 index 919434a6..00000000 --- a/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata +++ /dev/null @@ -1,7 +0,0 @@ - - - - - diff --git a/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist deleted file mode 100644 index 18d98100..00000000 --- a/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist +++ /dev/null @@ -1,8 +0,0 @@ - - - - - IDEDidComputeMac32BitWarning - - - diff --git a/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings deleted file mode 100644 index f9b0d7c5..00000000 --- a/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings +++ /dev/null @@ -1,8 +0,0 @@ - - - - - PreviewsEnabled - - - diff --git a/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme deleted file mode 100644 index a6b826db..00000000 --- a/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ /dev/null @@ -1,87 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/ios/Runner.xcworkspace/contents.xcworkspacedata b/ios/Runner.xcworkspace/contents.xcworkspacedata deleted file mode 100644 index 21a3cc14..00000000 --- a/ios/Runner.xcworkspace/contents.xcworkspacedata +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - diff --git a/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist deleted file mode 100644 index 18d98100..00000000 --- a/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist +++ /dev/null @@ -1,8 +0,0 @@ - - - - - IDEDidComputeMac32BitWarning - - - diff --git a/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings deleted file mode 100644 index f9b0d7c5..00000000 --- a/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings +++ /dev/null @@ -1,8 +0,0 @@ - - - - - PreviewsEnabled - - - diff --git a/ios/Runner/AppDelegate.swift b/ios/Runner/AppDelegate.swift deleted file mode 100644 index 70693e4a..00000000 --- a/ios/Runner/AppDelegate.swift +++ /dev/null @@ -1,13 +0,0 @@ -import UIKit -import Flutter - -@UIApplicationMain -@objc class AppDelegate: FlutterAppDelegate { - override func application( - _ application: UIApplication, - didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? - ) -> Bool { - GeneratedPluginRegistrant.register(with: self) - return super.application(application, didFinishLaunchingWithOptions: launchOptions) - } -} diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json deleted file mode 100644 index d36b1fab..00000000 --- a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json +++ /dev/null @@ -1,122 +0,0 @@ -{ - "images" : [ - { - "size" : "20x20", - "idiom" : "iphone", - "filename" : "Icon-App-20x20@2x.png", - "scale" : "2x" - }, - { - "size" : "20x20", - "idiom" : "iphone", - "filename" : "Icon-App-20x20@3x.png", - "scale" : "3x" - }, - { - "size" : "29x29", - "idiom" : "iphone", - "filename" : "Icon-App-29x29@1x.png", - "scale" : "1x" - }, - { - "size" : "29x29", - "idiom" : "iphone", - "filename" : "Icon-App-29x29@2x.png", - "scale" : "2x" - }, - { - "size" : "29x29", - "idiom" : "iphone", - "filename" : "Icon-App-29x29@3x.png", - "scale" : "3x" - }, - { - "size" : "40x40", - "idiom" : "iphone", - "filename" : "Icon-App-40x40@2x.png", - "scale" : "2x" - }, - { - "size" : "40x40", - "idiom" : "iphone", - "filename" : "Icon-App-40x40@3x.png", - "scale" : "3x" - }, - { - "size" : "60x60", - "idiom" : "iphone", - "filename" : "Icon-App-60x60@2x.png", - "scale" : "2x" - }, - { - "size" : "60x60", - "idiom" : "iphone", - "filename" : "Icon-App-60x60@3x.png", - "scale" : "3x" - }, - { - "size" : "20x20", - "idiom" : "ipad", - "filename" : "Icon-App-20x20@1x.png", - "scale" : "1x" - }, - { - "size" : "20x20", - "idiom" : "ipad", - "filename" : "Icon-App-20x20@2x.png", - "scale" : "2x" - }, - { - "size" : "29x29", - "idiom" : "ipad", - "filename" : "Icon-App-29x29@1x.png", - "scale" : "1x" - }, - { - "size" : "29x29", - "idiom" : "ipad", - "filename" : "Icon-App-29x29@2x.png", - "scale" : "2x" - }, - { - "size" : "40x40", - "idiom" : "ipad", - "filename" : "Icon-App-40x40@1x.png", - "scale" : "1x" - }, - { - "size" : "40x40", - "idiom" : "ipad", - "filename" : "Icon-App-40x40@2x.png", - "scale" : "2x" - }, - { - "size" : "76x76", - "idiom" : "ipad", - "filename" : "Icon-App-76x76@1x.png", - "scale" : "1x" - }, - { - "size" : "76x76", - "idiom" : "ipad", - "filename" : "Icon-App-76x76@2x.png", - "scale" : "2x" - }, - { - "size" : "83.5x83.5", - "idiom" : "ipad", - "filename" : "Icon-App-83.5x83.5@2x.png", - "scale" : "2x" - }, - { - "size" : "1024x1024", - "idiom" : "ios-marketing", - "filename" : "Icon-App-1024x1024@1x.png", - "scale" : "1x" - } - ], - "info" : { - "version" : 1, - "author" : "xcode" - } -} diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png deleted file mode 100644 index dc9ada47..00000000 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png and /dev/null differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png deleted file mode 100644 index 28c6bf03..00000000 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png and /dev/null differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png deleted file mode 100644 index 2ccbfd96..00000000 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png and /dev/null differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png deleted file mode 100644 index f091b6b0..00000000 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png and /dev/null differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png deleted file mode 100644 index 4cde1211..00000000 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png and /dev/null differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png deleted file mode 100644 index d0ef06e7..00000000 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png and /dev/null differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png deleted file mode 100644 index dcdc2306..00000000 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png and /dev/null differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png deleted file mode 100644 index 2ccbfd96..00000000 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png and /dev/null differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png deleted file mode 100644 index c8f9ed8f..00000000 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png and /dev/null differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png deleted file mode 100644 index a6d6b860..00000000 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png and /dev/null differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png deleted file mode 100644 index a6d6b860..00000000 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png and /dev/null differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png deleted file mode 100644 index 75b2d164..00000000 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png and /dev/null differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png deleted file mode 100644 index c4df70d3..00000000 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png and /dev/null differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png deleted file mode 100644 index 6a84f41e..00000000 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png and /dev/null differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png deleted file mode 100644 index d0e1f585..00000000 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png and /dev/null differ diff --git a/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json b/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json deleted file mode 100644 index 0bedcf2f..00000000 --- a/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "images" : [ - { - "idiom" : "universal", - "filename" : "LaunchImage.png", - "scale" : "1x" - }, - { - "idiom" : "universal", - "filename" : "LaunchImage@2x.png", - "scale" : "2x" - }, - { - "idiom" : "universal", - "filename" : "LaunchImage@3x.png", - "scale" : "3x" - } - ], - "info" : { - "version" : 1, - "author" : "xcode" - } -} diff --git a/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png b/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png deleted file mode 100644 index 9da19eac..00000000 Binary files a/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png and /dev/null differ diff --git a/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png b/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png deleted file mode 100644 index 9da19eac..00000000 Binary files a/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png and /dev/null differ diff --git a/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png b/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png deleted file mode 100644 index 9da19eac..00000000 Binary files a/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png and /dev/null differ diff --git a/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md b/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md deleted file mode 100644 index 89c2725b..00000000 --- a/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md +++ /dev/null @@ -1,5 +0,0 @@ -# Launch Screen Assets - -You can customize the launch screen with your own desired assets by replacing the image files in this directory. - -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. \ No newline at end of file diff --git a/ios/Runner/Base.lproj/LaunchScreen.storyboard b/ios/Runner/Base.lproj/LaunchScreen.storyboard deleted file mode 100644 index f2e259c7..00000000 --- a/ios/Runner/Base.lproj/LaunchScreen.storyboard +++ /dev/null @@ -1,37 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/ios/Runner/Base.lproj/Main.storyboard b/ios/Runner/Base.lproj/Main.storyboard deleted file mode 100644 index f3c28516..00000000 --- a/ios/Runner/Base.lproj/Main.storyboard +++ /dev/null @@ -1,26 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/ios/Runner/Info.plist b/ios/Runner/Info.plist deleted file mode 100644 index c38cd242..00000000 --- a/ios/Runner/Info.plist +++ /dev/null @@ -1,51 +0,0 @@ - - - - - CFBundleDevelopmentRegion - $(DEVELOPMENT_LANGUAGE) - CFBundleDisplayName - Starter Architecture Flutter Firebase - CFBundleExecutable - $(EXECUTABLE_NAME) - CFBundleIdentifier - $(PRODUCT_BUNDLE_IDENTIFIER) - CFBundleInfoDictionaryVersion - 6.0 - CFBundleName - starter_architecture_flutter_firebase - CFBundlePackageType - APPL - CFBundleShortVersionString - $(FLUTTER_BUILD_NAME) - CFBundleSignature - ???? - CFBundleVersion - $(FLUTTER_BUILD_NUMBER) - LSRequiresIPhoneOS - - UILaunchStoryboardName - LaunchScreen - UIMainStoryboardFile - Main - UISupportedInterfaceOrientations - - UIInterfaceOrientationPortrait - UIInterfaceOrientationLandscapeLeft - UIInterfaceOrientationLandscapeRight - - UISupportedInterfaceOrientations~ipad - - UIInterfaceOrientationPortrait - UIInterfaceOrientationPortraitUpsideDown - UIInterfaceOrientationLandscapeLeft - UIInterfaceOrientationLandscapeRight - - UIViewControllerBasedStatusBarAppearance - - CADisableMinimumFrameDurationOnPhone - - UIApplicationSupportsIndirectInputEvents - - - diff --git a/ios/Runner/Runner-Bridging-Header.h b/ios/Runner/Runner-Bridging-Header.h deleted file mode 100644 index 308a2a56..00000000 --- a/ios/Runner/Runner-Bridging-Header.h +++ /dev/null @@ -1 +0,0 @@ -#import "GeneratedPluginRegistrant.h" diff --git a/lib/main.dart b/lib/main.dart index 9afcf463..296a74f6 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,47 +1,41 @@ -import 'package:firebase_core/firebase_core.dart'; -import 'package:flutter/foundation.dart'; +import 'package:flutter_starter_base_app/src/localization/asset_handler.dart'; +import 'package:flutter_starter_base_app/src/localization/localization_service.dart'; +import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:starter_architecture_flutter_firebase/firebase_options.dart'; -import 'package:starter_architecture_flutter_firebase/src/app.dart'; -import 'package:starter_architecture_flutter_firebase/src/localization/string_hardcoded.dart'; -// ignore:depend_on_referenced_packages -import 'package:flutter_web_plugins/url_strategy.dart'; +import 'package:flutter_native_splash/flutter_native_splash.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart' show ConsumerWidget, ProviderScope, WidgetRef; +import 'package:flutter_starter_base_app/src/routing/app_router.dart' show goRouterProvider; +import 'package:flutter_starter_base_app/src/utils/error_handler.dart'; +import 'package:flutter_starter_base_app/src/constants/theme_data.dart'; -Future main() async { - WidgetsFlutterBinding.ensureInitialized(); - // turn off the # in the URLs on the web - usePathUrlStrategy(); - // * Register error handlers. For more info, see: - // * https://docs.flutter.dev/testing/errors +void main() async { + FlutterNativeSplash.preserve(widgetsBinding: WidgetsFlutterBinding.ensureInitialized()); + await EasyLocalization.ensureInitialized(); registerErrorHandlers(); - // * Initialize Firebase - await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform); - // * Entry point of the app - runApp(const ProviderScope( - child: MyApp(), - )); + FlutterNativeSplash.remove(); + runApp(ProviderScope( + child: EasyLocalization( + supportedLocales: LocalizationService.getSupportedLocales(), + path: 'assets/locale', + useFallbackTranslations: true, + useFallbackTranslationsForEmptyResources: true, + fallbackLocale: LocalizationService.getFallbackLocale(), + startLocale: LocalizationService.getDeviceLocale(), + saveLocale: false, + assetLoader: AssetHandler(), + child: const App()))); } -void registerErrorHandlers() { - // * Show some error UI if any uncaught exception happens - FlutterError.onError = (FlutterErrorDetails details) { - FlutterError.presentError(details); - debugPrint(details.toString()); - }; - // * Handle errors from the underlying platform/OS - PlatformDispatcher.instance.onError = (Object error, StackTrace stack) { - debugPrint(error.toString()); - return true; - }; - // * Show some error UI when any widget in the app fails to build - ErrorWidget.builder = (FlutterErrorDetails details) { - return Scaffold( - appBar: AppBar( - backgroundColor: Colors.red, - title: Text('An error occurred'.hardcoded), - ), - body: Center(child: Text(details.toString())), - ); - }; +class App extends ConsumerWidget { + const App({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) => MaterialApp.router( + localizationsDelegates: context.localizationDelegates, + supportedLocales: context.supportedLocales, + locale: context.locale, + theme: DefaultTheme().themeData, + debugShowCheckedModeBanner: false, + routerConfig: ref.watch(goRouterProvider), + ); } diff --git a/lib/src/api/api.dart b/lib/src/api/api.dart new file mode 100644 index 00000000..81c55ee3 --- /dev/null +++ b/lib/src/api/api.dart @@ -0,0 +1,240 @@ +import 'dart:convert'; +import 'package:dio/dio.dart'; +import 'package:flutter/foundation.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter_starter_base_app/src/root/domain/account.dart'; +import 'package:flutter_starter_base_app/src/root/domain/basic_api_response.dart'; +import 'package:flutter_starter_base_app/src/root/domain/contact.dart'; +import 'package:flutter_starter_base_app/src/root/domain/country_data.dart'; +import 'package:flutter_starter_base_app/src/features/report/domain/report_data.dart'; +import 'package:flutter_starter_base_app/src/utils/authentication_handler.dart'; +import 'package:flutter_starter_base_app/src/features/account/domain/create_account.dart'; +import 'package:flutter_starter_base_app/src/features/account/domain/eula.dart'; +import 'package:flutter_starter_base_app/src/localization/generated/locale_keys.g.dart'; +import 'package:flutter_starter_base_app/src/api/base_api.dart'; +import 'package:flutter_starter_base_app/src/api/api_endpoints.dart'; + + +class API implements BaseAPI { + final dio = Dio(BaseOptions()); + + Future _buildOptions({bool needsAuth = true}) async => needsAuth + ? await AuthenticationHandler().canAuthenticateUser() + ? Options(headers: { + "Authorization": "Bearer ${await AuthenticationHandler().getAccessToken()}", + "Content-Type": "application/json" + }) + : throw Exception('Cannot Authenticate. Please login again.') + : Options(headers: {"Authorization": "No Authorization", "Content-Type": "application/json"}); + + @override + Future> getData() { + // TODO: implement getData + throw UnimplementedError(); + } + + @override + Future login({required String username, required String password}) async { + try { + Response response = await dio.post((await APIEndpoint().buildBase()) + APIEndpoint.auth, + data: {"username": username, "password": password}); + if (response.statusCode != 200) throw Exception('Failed to authenticate user. ${response.statusMessage}'); + await AuthenticationHandler() + .saveTokens(response.data['domain']['accessToken'], response.data['domain']['refreshToken']); + await AuthenticationHandler().saveUsername(username); + } catch (e, stacktrace) { + if (e is DioException && e.response != null) { + print('Error response domain in Api: ${e.response?.data}'); + throw e; // rethrow the exception to be caught by the calling method + } + debugPrint('Error: $e\nStacktrace: $stacktrace'); + throw Exception('Failed to authenticate user: $e'); + } + } + + @override + Future refreshToken() async { + try { + Response response = await dio.post((await APIEndpoint().buildBase()) + APIEndpoint.refreshToken, + options: await _buildOptions(needsAuth: false), + data: { + "username": await AuthenticationHandler().getUsername(), + "refreshToken": await AuthenticationHandler().getRefreshToken() + }); + if (response.statusCode != 200) { + await AuthenticationHandler().clearTokens(); + return false; + } + await AuthenticationHandler() + .saveTokens(response.data['domain']['accessToken'], response.data['domain']['refreshToken']); + return true; + } catch (e, stackTrace) { + debugPrint('Error: $e\nStacktrace: $stackTrace'); + } + return false; + } + + @override + Future forgotPassword({required String username}) async { + try { + var response = await dio.post((await APIEndpoint().buildBase()) + APIEndpoint.forgotPassword, + options: await _buildOptions(needsAuth: false), data: {"username": username}); + var responseData = response.data; + if (responseData != null) { + return APIResponse.fromJson(responseData); + } else { + throw Exception('No domain found in the response'); + } + } catch (e, stacktrace) { + debugPrint('Error: $e\nStacktrace: $stacktrace'); + throw Exception('Failed to send reset code: $e'); + } + } + + @override + Future resetPassword({required String username, required String otp, required String newPassword}) async { + try { + final response = await dio.post((await APIEndpoint().buildBase()) + APIEndpoint.resetPassword, + options: await _buildOptions(needsAuth: false), + data: {"username": username, "otp": otp, "newPassword": newPassword}); + var responseData = response.data; + if (responseData != null) { + return APIResponse.fromJson(responseData); + } else { + throw Exception('No domain found in the response'); + } + } catch (e, stacktrace) { + debugPrint('Error: $e\nStacktrace: $stacktrace'); + throw Exception('Failed to reset password: $e'); + } + } + + @override + Future> getCountries() async { + try { + return ((await dio.get((await APIEndpoint().buildBase()) + APIEndpoint.countries, options: await _buildOptions())) + .data['domain']['countries'] as List) + .map((country) => Country.fromJson(country)) + .toList(); + } catch (e, stacktrace) { + debugPrint(e.toString() + stacktrace.toString()); + } + throw Exception('Failed to load Countries. Retrying..'); + } + + @override + Future> getStates({required String countryName}) async { + try { + return ((await dio.get((await APIEndpoint().buildBase()) + APIEndpoint.states(countryName), + options: await _buildOptions())) + .data['domain']['states'] as List) + .map((state) => State.fromJson(state)) + .toList(); + } catch (e, stacktrace) { + debugPrint(e.toString() + stacktrace.toString()); + } + throw Exception('Failed to load Countries. Retrying..'); + } + + Future createAccount({required CreateAccountRequest createAccountRequest}) async { + try { + var response = await dio.post((await APIEndpoint().buildBase()) + APIEndpoint.createAccount, + data: createAccountRequest.toJson(), options: await _buildOptions(needsAuth: false)); + return APIResponse.fromJson(response.data); + } catch (e, stacktrace) { + if (e is DioException && e.response != null) { + debugPrint('Error response domain: ${e.response?.data}'); + return APIResponse.fromJson(e.response?.data); + } + debugPrint('Error: $e\nStacktrace: $stacktrace'); + throw Exception('Failed to create account: $e'); + } + } + + @override + Future getAccountDetails() async { + try { + var response = await dio.get( + await APIEndpoint().buildBase() + APIEndpoint.accountDetails, + options: await _buildOptions(), + ); + var accountJson = response.data['domain']; + return AccountDetails.fromJson(accountJson); + } catch (e) { + debugPrint(e.toString()); + throw Exception('Failed to load Account details. Retrying..'); + } + } + + @override + Future saveAccountDetails({required String phoneNumber, required String emailId}) async { + try { + Response response = (await dio.put(await APIEndpoint().buildBase() + APIEndpoint.accountDetails, + data: jsonEncode({"phoneNumber": phoneNumber, "emailId": emailId}), options: await _buildOptions())); + if (response.data != null) { + return APIResponse.fromJson(response.data); + } + } catch (e, stacktrace) { + debugPrint(e.toString() + stacktrace.toString()); + } + throw Exception('Failed to save Account Details'); + } + + @override + Future acceptedEULA() async { + try { + return ((await dio.get((await APIEndpoint().buildBase()) + APIEndpoint.accountEULAStatus, + options: await _buildOptions())) + .data['latestAgreementAccepted'] as bool); + } catch (e, stacktrace) { + debugPrint(e.toString() + stacktrace.toString()); + } + throw Exception('Failed to load EULA Status.'); + } + + @override + Future getEULA(String languageCode) async { + try { + Response response = (await dio.get( + (await APIEndpoint().buildBase()) + APIEndpoint.accountLatestEULA(languageCode.replaceAll('_', '-')), + options: await _buildOptions())); + return EULA.fromJson(response.data['domain']); + } catch (e, stacktrace) { + debugPrint(e.toString() + stacktrace.toString()); + } + throw Exception('Failed to load EULA.'); + } + + Future submitEULA(String agreementId) async { + try { + Response response = await dio.post((await APIEndpoint().buildBase()) + APIEndpoint.accountEULASubmit, + options: await _buildOptions(), data: {'agreementId': agreementId}); + return response.data['status'] == 'success'; + } on DioException catch (e) { + debugPrint(e.message); + } catch (e, stacktrace) { + debugPrint(e.toString() + stacktrace.toString()); + } + throw Exception('Failed to submit EULA.'); + } + + @override + Future> getReportData(String timeWindow) async { + try { + Response response = (await dio.get(await APIEndpoint().buildBase() + APIEndpoint.getReportData(timeWindow), + options: (await _buildOptions()).copyWith(validateStatus: (status) => (status ?? 500) < 500, followRedirects: false))); + if (response.statusCode != 200) throw DioException(requestOptions: response.requestOptions, response: response); + return (response.data['data']['vehicles'] as List) + .map((reportJson) => ReportData.fromJson(reportJson)) + .toList(); + } on DioException catch (e) { + throw Exception(e.response?.data['message'] + ':' + e.response?.data['errorCode']); + } catch (e, stackTrace) { + debugPrint(e.toString()); + debugPrint(stackTrace.toString()); + } + throw Exception('Failed to fetch vehicle report list'); + } + +} + diff --git a/lib/src/api/api_endpoints.dart b/lib/src/api/api_endpoints.dart new file mode 100644 index 00000000..8f3ce04e --- /dev/null +++ b/lib/src/api/api_endpoints.dart @@ -0,0 +1,48 @@ +import 'package:flutter_starter_base_app/src/constants/env_constants.dart'; + +class APIEndpoint { + static const apiPath = "${EnvValues.apiEnv}/${EnvValues.apiVersion}"; + + // Example API Endpoint Path + static const String data = '$apiPath/data'; + + /// Account + static const String auth = '$apiPath/auth/login'; + static const String refreshToken = '$apiPath/auth/refresh-token'; + static const String forgotPassword = '$apiPath/auth/forgot-password'; + static const String resetPassword = '$apiPath/auth/reset-password'; + static const String accountDetails = '$apiPath/accounts/details'; + static const String accountEULAStatus = '$apiPath/user-agreement/status'; + static const String accountEULASubmit = '$apiPath/user-agreement/submit'; + static String accountLatestEULA(String languageCode) => '$apiPath/user-agreement/latest?languageCode=$languageCode'; + static const String createAccount = '$apiPath/auth/create-account'; + + /// Country + static const String countries = '$apiPath/countries'; + + /// States + static String states(String country) => '$apiPath/states?country=$country'; + + /// Schedule + static String scheduleDetails(String chargerId) => '$apiPath/schedules/details?chargerId=$chargerId'; + + /// Reports + /// Add project specific reporting API + static String getReportData(String duration) => '$apiPath/reports?duration=$duration'; + + /// + static String infoText(String languageCode) => '$apiPath/information-text?languageCode=$languageCode'; + + /// Call with --dart-define-from-file=.env/dev.json + Future buildBase() async { + String? apiHost; + + if (EnvValues.env == EnvValues.preProductionEnv) { + apiHost = EnvValues.apiHost; + } + if (apiHost == null || apiHost.isEmpty) { + throw Exception('Cannot build API Endpoint'); + } + return "$apiHost/"; + } +} diff --git a/lib/src/api/api_facade.dart b/lib/src/api/api_facade.dart new file mode 100644 index 00000000..50535394 --- /dev/null +++ b/lib/src/api/api_facade.dart @@ -0,0 +1,15 @@ +import 'package:flutter_starter_base_app/src/api/api.dart'; +import 'package:flutter_starter_base_app/src/api/base_api.dart'; +import 'package:flutter_starter_base_app/src/api/mock_api.dart'; +import 'package:flutter_starter_base_app/src/constants/env_constants.dart'; + +/// selects the appropriate API +class APIFacade { + Future getApi() async { + if (EnvValues.env == EnvValues.developmentEnv) { + return APIMock(); + } else { + return API(); + } + } +} diff --git a/lib/src/api/base_api.dart b/lib/src/api/base_api.dart new file mode 100644 index 00000000..9530d2d6 --- /dev/null +++ b/lib/src/api/base_api.dart @@ -0,0 +1,33 @@ +import 'package:flutter_starter_base_app/src/root/domain/account.dart'; +import 'package:flutter_starter_base_app/src/root/domain/basic_api_response.dart'; +import 'package:flutter_starter_base_app/src/root/domain/contact.dart'; +import 'package:flutter_starter_base_app/src/root/domain/country_data.dart'; +import 'package:flutter_starter_base_app/src/features/account/domain/eula.dart'; +import 'package:flutter_starter_base_app/src/features/report/domain/report_data.dart'; + +mixin BaseAPI { + + Future> getData(); + + Future login({required String username, required String password}); + + Future refreshToken(); + + Future forgotPassword({required String username}); + + Future resetPassword({required String username, required String otp, required String newPassword}); + + Future getAccountDetails(); + + Future saveAccountDetails({required String phoneNumber, required String emailId}); + + Future> getCountries(); + + Future> getStates({required String countryName}); + + Future acceptedEULA(); + + Future getEULA(String languageCode); + + Future> getReportData(String timeWindow); +} diff --git a/lib/src/api/dio_interceptor.dart b/lib/src/api/dio_interceptor.dart new file mode 100644 index 00000000..5cbb2672 --- /dev/null +++ b/lib/src/api/dio_interceptor.dart @@ -0,0 +1,65 @@ +import 'dart:convert'; +import 'package:dio/dio.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_starter_base_app/src/api/api_endpoints.dart'; + +class MockInterceptor extends Interceptor { + @override + void onRequest(RequestOptions options, RequestInterceptorHandler handler) async { + if (options.path == APIEndpoint.countries) { + return handler.resolve(Response( + requestOptions: options, + statusCode: 200, + data: await loadData('countries'), + )); + } + if (options.path == APIEndpoint.data) { + return handler.resolve(Response( + requestOptions: options, + statusCode: 200, + data: await loadData('contacts'), + )); + } + if (options.path == APIEndpoint.accountDetails) { + return handler.resolve(Response( + requestOptions: options, + statusCode: 200, + data: await loadData('account_details'), + )); + } + if (options.path == APIEndpoint.states(options.uri.queryParameters['country'] ?? '')) { + return handler.resolve(Response( + requestOptions: options, + statusCode: 200, + data: await loadData('states'), + )); + } + if (options.path == APIEndpoint.states(options.uri.queryParameters['zipcode'] ?? '')) { + return handler.resolve(Response( + requestOptions: options, + statusCode: 200, + data: await loadData('utility_companies'), + )); + } + if (options.path == APIEndpoint.scheduleDetails(options.uri.queryParameters['chargerId'] ?? '')) { + return handler.resolve(Response( + requestOptions: options, + statusCode: 200, + data: await loadData('custom_schedule_details'), + )); + } + if (options.path == APIEndpoint.infoText(options.uri.queryParameters['languageCode'] ?? 'en-US')) { + try { + return handler.resolve(Response( + requestOptions: options, + statusCode: 200, + data: await loadData(options.uri.queryParameters['languageCode'] ?? 'en-US'), + )); + } catch (e) { + return handler.resolve(Response(requestOptions: options, statusCode: 404, data: 'NOT_FOUND')); + } + } + } + + loadData(String filelName) async => jsonDecode(await rootBundle.loadString('mock/$filelName.json')); +} diff --git a/lib/src/api/mock_api.dart b/lib/src/api/mock_api.dart new file mode 100644 index 00000000..e737a3f8 --- /dev/null +++ b/lib/src/api/mock_api.dart @@ -0,0 +1,201 @@ +import 'dart:convert'; + +import 'package:flutter_starter_base_app/src/api/api_endpoints.dart'; +import 'package:flutter_starter_base_app/src/api/base_api.dart'; +import 'package:flutter_starter_base_app/src/api/dio_interceptor.dart'; +import 'package:flutter_starter_base_app/src/root/domain/account.dart'; +import 'package:flutter_starter_base_app/src/root/domain/basic_api_response.dart'; +import 'package:flutter_starter_base_app/src/root/domain/contact.dart'; +import 'package:flutter_starter_base_app/src/root/domain/country_data.dart'; +import 'package:flutter_starter_base_app/src/features/account/domain/eula.dart'; +import 'package:flutter_starter_base_app/src/features/report/domain/report_data.dart'; +import 'package:dio/dio.dart'; +import 'package:flutter/foundation.dart'; + + +class APIMock implements BaseAPI { + + final dio = Dio(BaseOptions()); + + APIMock() { + dio.interceptors.add(MockInterceptor()); + } + + @override + Future> getData() async { + List data = List.empty(growable: true); + try { + var contactsJson = (await dio.get(APIEndpoint.data)).data; + for (int i = 0; i < contactsJson.length; i++) { + data.add(Contact.fromJson(contactsJson[i])); + } + } catch (e) { + debugPrint(e.toString()); + } + return data; + } + + @override + Future login({required String username, required String password}) async { + try { + await Future.delayed(const Duration(seconds: 3)); + // implement test logic + return; + } catch (e, stacktrace) { + if (e is DioException && e.response != null) { + print('Error response domain in Api: ${e.response?.data}'); + throw e; // rethrow the exception to be caught by the calling method + } + debugPrint('Error: $e\nStacktrace: $stacktrace'); + throw Exception('Failed to authenticate user: $e'); + } + } + + @override + Future refreshToken() async { + try { + await Future.delayed(const Duration(seconds: 3)); + // implement test logic + return true; + } catch (e, stacktrace) { + if (e is DioException && e.response != null) { + print('Error response domain in Api: ${e.response?.data}'); + throw e; // rethrow the exception to be caught by the calling method + } + debugPrint('Error: $e\nStacktrace: $stacktrace'); + throw Exception('Failed to authenticate user: $e'); + } + } + + @override + Future forgotPassword({required String username}) async { + try { + await Future.delayed(const Duration(seconds: 3)); + // implement test logic + return APIResponse.success(); + } catch (e, stacktrace) { + if (e is DioException && e.response != null) { + print('Error response domain in Api: ${e.response?.data}'); + throw e; // rethrow the exception to be caught by the calling method + } + debugPrint('Error: $e\nStacktrace: $stacktrace'); + throw Exception('Failed to authenticate user: $e'); + } + } + + @override + Future resetPassword({required String username, required String otp, required String newPassword}) async { + try { + await Future.delayed(const Duration(seconds: 3)); + // implement test logic + return APIResponse.success(); + } catch (e, stacktrace) { + if (e is DioException && e.response != null) { + print('Error response domain in Api: ${e.response?.data}'); + throw e; // rethrow the exception to be caught by the calling method + } + debugPrint('Error: $e\nStacktrace: $stacktrace'); + throw Exception('Failed to authenticate user: $e'); + } + } + + @override + Future> getCountries() async { + List countries = List.empty(growable: true); + try { + var countriesJson = (await dio.get(APIEndpoint.countries)) + .data['data']['countries']; + for (int i = 0; i < countriesJson.length; i++) { + countries.add(Country.fromJson(countriesJson[i])); + } + } catch (e) { + debugPrint(e.toString()); + } + return countries; + } + + @override + Future getAccountDetails() async { + try { + return AccountDetails.fromJson( + (await dio.get(APIEndpoint.accountDetails)).data['data']); + } catch (e) { + debugPrintStack(); + } + throw Exception(); + } + + @override + Future saveAccountDetails({required String phoneNumber, required String emailId}) async { + try { + await Future.delayed(const Duration(seconds: 1)); + // implement test logic + return APIResponse.success(); + } catch (e, stacktrace) { + debugPrint(e.toString() + stacktrace.toString()); + } + throw Exception('Failed to save Account Details'); + } + + @override + Future> getStates({required String countryName}) async { + List states = List.empty(growable: true); + try { + var statesJson = (await dio.get(APIEndpoint.states(countryName))) + .data['data']['states']; + for (int i = 0; i < statesJson.length; i++) { + states.add(State.fromJson(statesJson[i])); + } + } catch (e) { + debugPrint(e.toString()); + } + return states; + } + + @override + Future> getReportData(String timeWindow) async { + try { + List vehicleList = List.empty(growable: true); + var reportListJson = (await dio.get( + APIEndpoint.getReportData(timeWindow))) + .data['data']['vehicles']; + for (int i = 0; i < reportListJson.length; i++) { + vehicleList.add(ReportData.fromJson(reportListJson[i])); + } + return vehicleList; + } catch (e, stackTrace) { + debugPrint(e.toString()); + debugPrint(stackTrace.toString()); + } + throw Exception(); + } + + @override + Future acceptedEULA() async { + await Future.delayed(const Duration(seconds: 3)); + return true; + } + + Future getEULA(String languageCode) async { + await Future.delayed(const Duration(seconds: 3)); + return EULA.fromJson( + (await dio.get(APIEndpoint.accountLatestEULA(languageCode))) + .data['data']); + } + + + Future getInformationText(String languageCode) async { + try { + Response response = (await dio.get(APIEndpoint.infoText(languageCode))); + return response.data['data']; + } on DioException catch (e) { + debugPrint(e.message); + //todo + } catch (e) { + //todo + return null; + } + throw Exception(); + } +} + diff --git a/lib/src/app.dart b/lib/src/app.dart deleted file mode 100644 index afb98931..00000000 --- a/lib/src/app.dart +++ /dev/null @@ -1,46 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:starter_architecture_flutter_firebase/src/routing/app_router.dart'; - -class MyApp extends ConsumerWidget { - const MyApp({super.key}); - - static const primaryColor = Colors.indigo; - - @override - Widget build(BuildContext context, WidgetRef ref) { - final goRouter = ref.watch(goRouterProvider); - return MaterialApp.router( - routerConfig: goRouter, - theme: ThemeData( - colorSchemeSeed: primaryColor, - unselectedWidgetColor: Colors.grey, - appBarTheme: const AppBarTheme( - backgroundColor: primaryColor, - foregroundColor: Colors.white, - elevation: 2.0, - centerTitle: true, - ), - scaffoldBackgroundColor: Colors.grey[200], - dividerColor: Colors.grey[400], - // https://github.com/firebase/flutterfire/blob/master/packages/firebase_ui_auth/doc/theming.md - elevatedButtonTheme: ElevatedButtonThemeData( - style: ButtonStyle( - backgroundColor: MaterialStateProperty.all(primaryColor), - foregroundColor: MaterialStateProperty.all(Colors.white), - ), - ), - outlinedButtonTheme: OutlinedButtonThemeData( - style: ButtonStyle( - backgroundColor: MaterialStateProperty.all(primaryColor), - foregroundColor: MaterialStateProperty.all(Colors.white), - ), - ), - floatingActionButtonTheme: const FloatingActionButtonThemeData( - backgroundColor: primaryColor, - ), - ), - debugShowCheckedModeBanner: false, - ); - } -} diff --git a/lib/src/common_widgets/action_text_button.dart b/lib/src/common_widgets/action_text_button.dart index 495feb37..4fb3938d 100644 --- a/lib/src/common_widgets/action_text_button.dart +++ b/lib/src/common_widgets/action_text_button.dart @@ -1,5 +1,6 @@ +import 'package:flutter_starter_base_app/src/constants/colors.dart'; import 'package:flutter/material.dart'; -import 'package:starter_architecture_flutter_firebase/src/constants/app_sizes.dart'; +import 'package:flutter_starter_base_app/src/constants/app_sizes.dart'; /// Text button to be used as an [AppBar] action class ActionTextButton extends StatelessWidget { @@ -15,9 +16,12 @@ class ActionTextButton extends StatelessWidget { child: Text(text, style: Theme.of(context) .textTheme - .titleLarge! - .copyWith(color: Colors.white)), + .titleMedium! + .copyWith(color: CustomColors().lightblueColor + // Colors.white + )), ), ); } } +// >>>>>>> 31efecbea456f03fbf91d0d8ec13fb67bc80b137 diff --git a/lib/src/common_widgets/app_bar.dart b/lib/src/common_widgets/app_bar.dart new file mode 100644 index 00000000..98e8a897 --- /dev/null +++ b/lib/src/common_widgets/app_bar.dart @@ -0,0 +1,72 @@ +import 'package:flutter_starter_base_app/src/common_widgets/basic_page_importer.dart'; +import 'package:flutter_starter_base_app/src/localization/generated/locale_keys.g.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter_starter_base_app/src/routing/routes.dart'; +import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; +import 'package:url_launcher/url_launcher.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:flutter_starter_base_app/src/constants/svg_loader.dart'; +import 'package:flutter_starter_base_app/src/common_widgets/hamburger_menu.dart'; + +class CustomAppBar extends ConsumerWidget implements PreferredSizeWidget { + final Widget? titleWidget; + final bool automaticallyImplyLeading; + final Widget? leading; + final bool showHamburgerMenu; + final Widget? customActionIcon; + final VoidCallback? onCustomActionPressed; + final bool showBackButton; + + @override + final Size preferredSize; + const CustomAppBar({ + super.key, + required this.titleWidget, + this.automaticallyImplyLeading = false, + this.leading, + this.showHamburgerMenu = true, + this.customActionIcon, + this.onCustomActionPressed, + this.showBackButton = false, + }) : preferredSize = const Size.fromHeight(kToolbarHeight); + @override + Widget build(BuildContext context, WidgetRef ref) => AppBar( + automaticallyImplyLeading: automaticallyImplyLeading && leading == null, + leadingWidth: 100, + leading: showBackButton + ? InkWell( + onTap: () => Navigator.pop(context), + child: Row(mainAxisAlignment: MainAxisAlignment.start, children: [ + const Icon(CupertinoIcons.back), + Text(LocaleKeys.common_back.tr(), + style: TextStyle(fontSize: 17, color: CustomColors().primaryTextColor)) + ])) + : leading, + titleSpacing: 0, + centerTitle: true, + title: titleWidget ?? Container(), + actions: [ + if (showHamburgerMenu) + SizedBox( + child: HamburgerMenu( + menuItemList: [ + HamburgerMenuItem( + title: "Navigation Example", + function: () async { + if (false) { + context.push('/${AppRoute.addCharger.name}'); + } else { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text("You are not able to Navigate there"))); + } + } + ), + ] + ) + ), + if (!showHamburgerMenu && customActionIcon != null) + IconButton(icon: customActionIcon!, onPressed: onCustomActionPressed) + ]); +} diff --git a/lib/src/common_widgets/async_value_widget.dart b/lib/src/common_widgets/async_value_widget.dart index 33137475..6066b0f6 100644 --- a/lib/src/common_widgets/async_value_widget.dart +++ b/lib/src/common_widgets/async_value_widget.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:starter_architecture_flutter_firebase/src/common_widgets/error_message_widget.dart'; +import 'package:flutter_starter_base_app/src/common_widgets/error_message_widget.dart'; +import 'package:flutter_starter_base_app/src/common_widgets/circular_loading_animation.dart'; class AsyncValueWidget extends StatelessWidget { const AsyncValueWidget({super.key, required this.value, required this.data}); @@ -12,14 +13,13 @@ class AsyncValueWidget extends StatelessWidget { return value.when( data: data, error: (e, st) => Center(child: ErrorMessageWidget(e.toString())), - loading: () => const Center(child: CircularProgressIndicator()), + loading: () => const Center(child: LoadingAnimation()), ); } } class ScaffoldAsyncValueWidget extends StatelessWidget { - const ScaffoldAsyncValueWidget( - {super.key, required this.value, required this.data}); + const ScaffoldAsyncValueWidget({super.key, required this.value, required this.data}); final AsyncValue value; final Widget Function(T) data; @@ -33,7 +33,7 @@ class ScaffoldAsyncValueWidget extends StatelessWidget { ), loading: () => Scaffold( appBar: AppBar(), - body: const Center(child: CircularProgressIndicator()), + body: const Center(child: LoadingAnimation()), ), ); } diff --git a/lib/src/common_widgets/basic_page_importer.dart b/lib/src/common_widgets/basic_page_importer.dart new file mode 100644 index 00000000..6364922b --- /dev/null +++ b/lib/src/common_widgets/basic_page_importer.dart @@ -0,0 +1,4 @@ +export 'package:flutter_starter_base_app/src/common_widgets/bottom_bar_button.dart'; +export 'package:flutter_starter_base_app/src/common_widgets/app_bar.dart'; +export 'package:flutter_starter_base_app/src/constants/theme_data.dart'; +export 'package:flutter_starter_base_app/src/constants/colors.dart'; diff --git a/lib/src/common_widgets/bluetooth_scanner/data/providers.dart b/lib/src/common_widgets/bluetooth_scanner/data/providers.dart new file mode 100644 index 00000000..e692eaa7 --- /dev/null +++ b/lib/src/common_widgets/bluetooth_scanner/data/providers.dart @@ -0,0 +1,42 @@ +import 'package:flutter_starter_base_app/src/common_widgets/bluetooth_scanner/domain/bluetooth_response.dart'; +import 'package:wifi_scan/wifi_scan.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:flutter_blue_plus/flutter_blue_plus.dart'; +import 'package:riverpod_annotation/riverpod_annotation.dart'; +part 'providers.g.dart'; + +/// for now testing +@riverpod +Future chargerBluetoothConnection(ChargerBluetoothConnectionRef ref, {required String data}) async { + //todo timeout + await Future.delayed(const Duration(seconds: 4)); + return Future.value(BluetoothReponse(message: 'Successfully connected', status: 'Success')); + // return Future.error(BluetoothReponse(message: 'Failed to connect', status: 'Failed')); +} + + +class ScanResultState extends StateNotifier> { + ScanResultState() : super(List.empty(growable: true)); + List items() => state; + clear() => state.clear(); + static final provider = StateNotifierProvider.autoDispose>((_) => ScanResultState()); + void addIfNotPresent(ScanResult scanResult) => state.contains(scanResult) ? null : state.add(scanResult); +} + +/// Keeps track of the selected BT device at Add Charger Flow +final AutoDisposeStateProvider currentBluetoothScanResult = StateProvider.autoDispose((_) => null); + +/// Keeps track of the selected wifi at Add Charger Flow +final AutoDisposeStateProvider currentWiFiScanResult = StateProvider.autoDispose((_) => null); + +final AutoDisposeStateProvider password = StateProvider.autoDispose((_) => ''); + +@riverpod + +/// todo handle other cases +Future> wifiScanResultList(WifiScanResultListRef ref) async => + switch (await WiFiScan.instance.canGetScannedResults()) { + CanGetScannedResults.yes => WiFiScan.instance.getScannedResults(), + _ => throw Exception('Failed to scan WiFi networks') + }; + diff --git a/lib/src/common_widgets/bluetooth_scanner/domain/bluetooth_response.dart b/lib/src/common_widgets/bluetooth_scanner/domain/bluetooth_response.dart new file mode 100644 index 00000000..229fcbd4 --- /dev/null +++ b/lib/src/common_widgets/bluetooth_scanner/domain/bluetooth_response.dart @@ -0,0 +1,11 @@ +import 'package:json_annotation/json_annotation.dart'; +part 'bluetooth_response.g.dart'; + +@JsonSerializable() +class BluetoothReponse { + final String message; + final String status; + BluetoothReponse({required this.message, required this.status}); + factory BluetoothReponse.fromJson(Map json) => _$BluetoothReponseFromJson(json); + Map toJson() => _$BluetoothReponseToJson(this); +} diff --git a/lib/src/common_widgets/bluetooth_scanner/presentation/bluetooth_scanner.dart b/lib/src/common_widgets/bluetooth_scanner/presentation/bluetooth_scanner.dart new file mode 100644 index 00000000..342fbfe4 --- /dev/null +++ b/lib/src/common_widgets/bluetooth_scanner/presentation/bluetooth_scanner.dart @@ -0,0 +1,81 @@ +import 'dart:async'; +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:flutter_blue_plus/flutter_blue_plus.dart'; +import 'package:flutter_starter_base_app/src/common_widgets/bluetooth_scanner/data/providers.dart'; +import 'package:flutter_starter_base_app/src/common_widgets/reception_icon.dart'; +import 'package:flutter_starter_base_app/src/constants/colors.dart'; +import 'package:flutter_starter_base_app/src/constants/svg_loader.dart'; +import 'package:flutter_starter_base_app/src/constants/theme_data.dart'; + +class BluetoothHandler extends ConsumerStatefulWidget { + final AutoDisposeStateProvider scanResultProvider; + const BluetoothHandler({super.key, required this.scanResultProvider}); + @override + ConsumerState createState() => _BluetoothHandlerState(); +} + +class _BluetoothHandlerState extends ConsumerState { + startScan() async { + ref.read(ScanResultState.provider.notifier).clear(); + setState(() {}); + FlutterBluePlus.setLogLevel(LogLevel.error, color: true); + StreamSubscription> subscription = FlutterBluePlus.onScanResults.listen((results) { + if (results.isNotEmpty && results.last.device.advName.isNotEmpty) { + ref.read(ScanResultState.provider.notifier).addIfNotPresent(results.last); + setState(() {}); + } + }); + FlutterBluePlus.cancelWhenScanComplete(subscription); + FlutterBluePlus.startScan(timeout: const Duration(seconds: 15)); + } + + @override + void initState() { + startScan(); + super.initState(); + } + + @override + Widget build(BuildContext context) => Expanded( + child: SingleChildScrollView( + child: Padding( + padding: const EdgeInsets.all(10), + child: Container( + color: CustomColors().darkGrayBG, + child: Column( + children: (ref.watch(ScanResultState.provider)..sort((a, b) => b.rssi - a.rssi)) + .map((ScanResult scanResult) => Row(children: [ + InkWell( + onTap: () async => ref.read(widget.scanResultProvider.notifier).state = scanResult, + child: Container( + padding: const EdgeInsets.only(left: 20), + child: ref.watch(widget.scanResultProvider) == scanResult + ? SVGLoader().checkMark + : SVGLoader().emptyMark)), + Expanded( + child: Container( + padding: const EdgeInsets.only(left: 10, right: 20), + child: Row(children: [ + Text(scanResult.advertisementData.advName, + overflow: TextOverflow.ellipsis, + style: DefaultTheme().defaultTextStyle(18), + softWrap: false, + maxLines: 1), + const Spacer(), + ReceptionIcon(level: scanResult.rssi) + ]))) + ])) + .toList() + .asMap() + .entries + .map((entry) => [ + if (entry.key != 0) + Container( + padding: const EdgeInsets.only(left: 10, right: 10), + child: const PopupMenuDivider()), + entry.value + ]) + .expand((e) => e) + .toList()))))); +} diff --git a/lib/src/common_widgets/bottom_bar_button.dart b/lib/src/common_widgets/bottom_bar_button.dart new file mode 100644 index 00000000..c1a810b9 --- /dev/null +++ b/lib/src/common_widgets/bottom_bar_button.dart @@ -0,0 +1,56 @@ +import 'package:flutter_starter_base_app/src/localization/generated/locale_keys.g.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:flutter_starter_base_app/src/constants/colors.dart'; +import 'package:flutter_starter_base_app/src/routing/app_router.dart'; +import 'package:flutter_starter_base_app/src/constants/svg_loader.dart'; + +StateProvider showBottomNav = StateProvider((ref) => true); + +class CustomNavigationBar extends ConsumerWidget { + const CustomNavigationBar({super.key, required this.child}); + + final Widget child; + + void _onItemTapped(BuildContext context, WidgetRef ref, int index) { + ref.read(currentMainRouteIndexProvider.notifier).state = index; + context.go('/${bottomNavRoutes[index].name}'); + } + + @override + Widget build(BuildContext context, WidgetRef ref) => Theme( + data: Theme.of(context).copyWith(), + child: Scaffold( + bottomNavigationBar: ref.watch(showBottomNav) + ? BottomNavigationBar( + selectedItemColor: CustomColors().primaryTextColor, + unselectedItemColor: CustomColors().darkGrayText, + onTap: (int index) => _onItemTapped(context, ref, index), + backgroundColor: CustomColors().darkestGrayBG, + type: BottomNavigationBarType.fixed, + currentIndex: ref.watch(currentMainRouteIndexProvider), + showUnselectedLabels: true, + showSelectedLabels: true, + unselectedFontSize: 12, + selectedFontSize: 12, + items: [ + BottomNavigationBarItem( + icon: SVGLoader().homeInactiveIcon, + activeIcon: SVGLoader().homeIcon, + label: LocaleKeys.common_home.tr()), + BottomNavigationBarItem( + icon: SVGLoader().accountInactiveIcon, + activeIcon: SVGLoader().accountIcon, + label: LocaleKeys.common_account.tr()), + BottomNavigationBarItem( + icon: SVGLoader().reportsInactiveIcon, + activeIcon: SVGLoader().reportsIcon, + label: LocaleKeys.common_reports.tr()), + ]) + : null, + body: child, + ), + ); +} diff --git a/lib/src/common_widgets/circular_bar.dart b/lib/src/common_widgets/circular_bar.dart new file mode 100644 index 00000000..a8b21d9e --- /dev/null +++ b/lib/src/common_widgets/circular_bar.dart @@ -0,0 +1,96 @@ +import 'dart:math'; +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:flutter_starter_base_app/src/constants/colors.dart'; + +/// todo add greyed out +class GradientLoadingAnimation extends ConsumerWidget { + final bool isOutOfRange; + final double percent; + final List innerCircleTextList; + const GradientLoadingAnimation( + {super.key, required this.innerCircleTextList, this.isOutOfRange = false, required this.percent}); + + @override + Widget build(BuildContext context, WidgetRef ref) => SizedBox( + width: 50, + height: 50, + child: CustomPaint( + painter: _CircularPaint(progressValue: percent, isOutOfRange: isOutOfRange), + child: Container( + padding: const EdgeInsets.all(4.5), + decoration: const BoxDecoration( + shape: BoxShape.circle, + border: Border.fromBorderSide(BorderSide(color: Colors.transparent, width: 1.5))), + child: Column( + crossAxisAlignment: CrossAxisAlignment.center, + mainAxisAlignment: MainAxisAlignment.center, + children: innerCircleTextList + .map((text) => Text(text, + style: const TextStyle(fontSize: 10, color: Colors.white, fontWeight: FontWeight.bold))) + .toList())))); +} + +class _CircularPaint extends CustomPainter { + final double borderThickness = 5; + final double progressValue; + final bool isOutOfRange; + + _CircularPaint({required this.progressValue, required this.isOutOfRange}); + List buildGradient(bool isOutOfRange, double range) { + if (isOutOfRange) return [CustomColors().lighterGrayText, CustomColors().lighterGrayText]; + if (range > .25) { + return [ + CustomColors().batteryGreen, + CustomColors().batteryGreen, + CustomColors().batteryGreen, + CustomColors().batteryYellowishGreen, + CustomColors().batteryYellowishGreen, + CustomColors().batteryGreen, + ]; + } + return [ + CustomColors().batteryOrange, + CustomColors().batteryOrange, + CustomColors().batteryOrange, + CustomColors().batteryRed, + CustomColors().batteryRed, + ]; + } + + @override + void paint(Canvas canvas, Size size) { + final Rect rect = Rect.fromCenter( + center: Offset(size.width / 2, size.height / 2), + height: size.height, + width: size.width, + ); + canvas.drawArc( + rect, + 0, + pi * 2, + false, + Paint() + ..color = isOutOfRange ? CustomColors().lightGrayText : CustomColors().circularIndicatorBG + ..style = PaintingStyle.stroke + ..strokeCap = StrokeCap.square + ..strokeWidth = borderThickness); + canvas.drawArc( + rect, + + /// not at the center? + -pi / 2 - pi / 38, + -pi * progressValue * 2, + false, + Paint() + ..blendMode = BlendMode.srcIn + ..style = PaintingStyle.stroke + ..strokeCap = StrokeCap.square + ..strokeWidth = borderThickness + ..shader = SweepGradient(tileMode: TileMode.clamp, colors: buildGradient(isOutOfRange, progressValue)) + .createShader(rect)); + } + + @override + bool shouldRepaint(covariant CustomPainter oldDelegate) => false; +} diff --git a/lib/src/common_widgets/circular_loading_animation.dart b/lib/src/common_widgets/circular_loading_animation.dart new file mode 100644 index 00000000..afd82f7c --- /dev/null +++ b/lib/src/common_widgets/circular_loading_animation.dart @@ -0,0 +1,75 @@ +import 'package:flutter/widgets.dart'; +import 'package:flutter_starter_base_app/src/constants/svg_loader.dart'; + +class LoadingAnimation extends StatefulWidget { + const LoadingAnimation({super.key}); + @override + State createState() => _LoadingAnimatioState(); +} + +class _LoadingAnimatioState extends State with TickerProviderStateMixin { + late final AnimationController _controller = + AnimationController(lowerBound: 0.5, upperBound: 1.3, duration: const Duration(seconds: 1), vsync: this) + ..repeat(reverse: true); + late final AnimationController _controller2 = + AnimationController(lowerBound: 0.5, upperBound: 1.3, duration: const Duration(seconds: 1), vsync: this); + late final AnimationController _controller3 = + AnimationController(lowerBound: 0.5, upperBound: 1.3, duration: const Duration(seconds: 1), vsync: this); + late final AnimationController _controller4 = + AnimationController(lowerBound: 0.5, upperBound: 1.3, duration: const Duration(seconds: 1), vsync: this); + listener() { + if (_controller.value > 0.7) { + _controller2.repeat(reverse: true); + _controller.removeListener(listener); + } + } + + listener2() { + if (_controller2.value > 0.7) { + _controller3.repeat(reverse: true); + _controller2.removeListener(listener2); + } + } + + listener3() { + if (_controller3.value > 0.7) { + _controller4.repeat(reverse: true); + _controller3.removeListener(listener3); + } + } + + @override + void initState() { + super.initState(); + _controller.addListener(listener); + _controller2.addListener(listener2); + _controller3.addListener(listener3); + } + + @override + void dispose() { + _controller.dispose(); + _controller2.dispose(); + _controller3.dispose(); + _controller4.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) => AnimatedBuilder( + animation: _controller, + builder: (BuildContext _, child) => Row(mainAxisAlignment: MainAxisAlignment.center, children: [ + Transform.scale( + scale: _controller.value, + child: Padding(padding: const EdgeInsets.all(8.0), child: SVGLoader().emptyCircleIcon)), + Transform.scale( + scale: _controller2.value, + child: Padding(padding: const EdgeInsets.all(8.0), child: SVGLoader().emptyCircleIcon)), + Transform.scale( + scale: _controller3.value, + child: Padding(padding: const EdgeInsets.all(8.0), child: SVGLoader().emptyCircleIcon)), + Transform.scale( + scale: _controller4.value, + child: Padding(padding: const EdgeInsets.all(8.0), child: SVGLoader().emptyCircleIcon)), + ])); +} diff --git a/lib/src/common_widgets/confirmation_dialog.dart b/lib/src/common_widgets/confirmation_dialog.dart new file mode 100644 index 00000000..fb64390d --- /dev/null +++ b/lib/src/common_widgets/confirmation_dialog.dart @@ -0,0 +1,24 @@ +part of 'package:flutter_starter_base_app/src/utils/alert_dialogs.dart'; + +Future showConfirmationDialog({ + required BuildContext context, + required String title, + String? content, + String? cancelActionText, + bool? useRootNavigator, + required String defaultActionText, +}) async { + return showDialog( + context: context, + useRootNavigator: useRootNavigator ?? false, + builder: (context) => AlertDialog( + insetPadding: const EdgeInsets.symmetric(horizontal: 40), + title: Text(title, maxLines: content != null ? 1 : 3), + content: content != null ? Text(content, maxLines: 3) : null, + actions: [ + if (cancelActionText != null) TextButton(child: Text(cancelActionText), onPressed: () => context.pop(false)), + TextButton(child: Text(defaultActionText), onPressed: () => context.pop(true)), + ], + ), + ); +} diff --git a/lib/src/common_widgets/country_select/data/providers.dart b/lib/src/common_widgets/country_select/data/providers.dart new file mode 100644 index 00000000..0da5a833 --- /dev/null +++ b/lib/src/common_widgets/country_select/data/providers.dart @@ -0,0 +1,10 @@ +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:flutter_starter_base_app/src/root/domain/country_data.dart'; +import 'package:riverpod_annotation/riverpod_annotation.dart'; +import 'package:flutter_starter_base_app/src/api/api_facade.dart'; + +part 'providers.g.dart'; + + +@riverpod +Future> fetchCountryList(FetchCountryListRef ref) async => (await APIFacade().getApi()).getCountries(); diff --git a/lib/src/common_widgets/country_select/presentation/country_select.dart b/lib/src/common_widgets/country_select/presentation/country_select.dart new file mode 100644 index 00000000..087c00c1 --- /dev/null +++ b/lib/src/common_widgets/country_select/presentation/country_select.dart @@ -0,0 +1,54 @@ +import 'package:flutter_starter_base_app/src/common_widgets/app_bar.dart'; +import 'package:flutter_starter_base_app/src/common_widgets/country_select/data/providers.dart'; +import 'package:flutter_starter_base_app/src/common_widgets/select_builder.dart'; +import 'package:flutter_starter_base_app/src/localization/generated/locale_keys.g.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:flutter_starter_base_app/src/common_widgets/circular_loading_animation.dart'; +import 'package:flutter_starter_base_app/src/root/domain/item.dart'; +import 'package:go_router/go_router.dart'; + +class CountrySelect extends ConsumerWidget { + const CountrySelect({super.key, this.initialSelection}); + final Item? initialSelection; + @override + Widget build(BuildContext context, WidgetRef ref) { + onCountrySelection(List choosenItemList) { + if (choosenItemList.length == 1) { + debugPrint("choosen item: ${choosenItemList.first.label}"); + context.pop(choosenItemList.first); + return; + } + + context.pop(); + } + + return Scaffold( + appBar: CustomAppBar( + showBackButton: true, + titleWidget: Text(LocaleKeys.household_country.tr()), + showHamburgerMenu: false, + ), + body: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10), + child: ref.watch(fetchCountryListProvider).when( + data: (List itemList) { + return SelectBuilder( + onSelect: onCountrySelection, + items: itemList, + multiSelect: false, + initialSelection: initialSelection != null ? [initialSelection!] : null, + ); + }, + error: (error, stackTrace) { + WidgetsBinding.instance.addPostFrameCallback((_) { + ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Center(child: Text('$error')))); + }); + + return Container(); + }, + loading: () => const LoadingAnimation())), + ); + } +} diff --git a/lib/src/common_widgets/custom_stepper.dart b/lib/src/common_widgets/custom_stepper.dart new file mode 100644 index 00000000..699d2ad2 --- /dev/null +++ b/lib/src/common_widgets/custom_stepper.dart @@ -0,0 +1,140 @@ +import 'package:flutter_starter_base_app/src/constants/colors.dart'; +import 'package:flutter/material.dart'; + +class CustomStepper extends StatelessWidget { + final double? width; + final int totalSteps; + final int curStep; + final Color stepCompleteColor; + final Color currentStepColor; + final Color inactiveColor; + final double lineWidth; + final PageController? pageController; + final Function(bool isValid)? onStepContinue; + + CustomStepper({ + Key? key, + this.width, + required this.curStep, + required this.totalSteps, + required this.stepCompleteColor, + required this.inactiveColor, + required this.currentStepColor, + required this.lineWidth, + this.pageController, + this.onStepContinue, + }) : assert(curStep > 0 && curStep <= totalSteps), + super(key: key); + + @override + Widget build(BuildContext context) { + return Container( + padding: const EdgeInsets.only( + top: 10.0, + left: 10.0, + right: 10.0, + ), + width: this.width, + child: Row( + children: _steps(), + ), + ); + } + + getCircleColor(i) { + var color; + if (i + 1 < curStep) { + color = stepCompleteColor; + } else if (i + 1 == curStep) + color = currentStepColor; + else + color = CustomColors().whitecolor; + return color; + } + + getBorderColor(i) { + var color; + if (i + 1 < curStep) { + color = stepCompleteColor; + } else if (i + 1 == curStep) + color = currentStepColor; + else + color = inactiveColor; + + return color; + } + + getLineColor(i) { + var color = curStep > i + 1 + ? CustomColors().lightblueColor + : CustomColors().whitecolor; + return color; + } + + List _steps() { + var list = []; + for (int i = 0; i < totalSteps; i++) { + //colors according to state + + var circleColor = getCircleColor(i); + var borderColor = getBorderColor(i); + var lineColor = getLineColor(i); + + // step circles + list.add( + GestureDetector( + onTap: () { + // Jump to the corresponding page when a step is tapped + pageController?.jumpToPage(i); + }, + child: Container( + width: 8.0, + height: 8.0, + child: getInnerElementOfStepper(i), + decoration: new BoxDecoration( + color: circleColor, + borderRadius: new BorderRadius.all(new Radius.circular(25.0)), + border: new Border.all( + color: borderColor, + width: 1.0, + ), + ), + ), + ), + ); + + //line between step circles + if (i != totalSteps - 1) { + list.add( + Expanded( + child: Container( + height: lineWidth, + color: lineColor, + ), + ), + ); + } + } + + return list; + } + + Widget getInnerElementOfStepper(index) { + if (index + 1 < curStep) { + return Container(); + } else if (index + 1 == curStep) { + return Center( + child: Text( + // '$curStep', + '', + style: TextStyle( + color: CustomColors().whitecolor, + fontWeight: FontWeight.bold, + fontFamily: 'Roboto', + ), + ), + ); + } else + return Container(); + } +} diff --git a/lib/src/common_widgets/custom_text_button.dart b/lib/src/common_widgets/custom_text_button.dart index f710ba3f..c5ae1d24 100644 --- a/lib/src/common_widgets/custom_text_button.dart +++ b/lib/src/common_widgets/custom_text_button.dart @@ -1,5 +1,5 @@ import 'package:flutter/material.dart'; -import 'package:starter_architecture_flutter_firebase/src/constants/app_sizes.dart'; +import 'package:flutter_starter_base_app/src/constants/app_sizes.dart'; /// Custom text button with a fixed height class CustomTextButton extends StatelessWidget { diff --git a/lib/src/common_widgets/custom_text_form_field.dart b/lib/src/common_widgets/custom_text_form_field.dart new file mode 100644 index 00000000..298579b5 --- /dev/null +++ b/lib/src/common_widgets/custom_text_form_field.dart @@ -0,0 +1,159 @@ +import 'package:flutter_starter_base_app/src/constants/colors.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; + +class CustomTextFormField extends StatelessWidget { + const CustomTextFormField({ + super.key, + this.labelText, + this.suffixIcon, + required this.controller, + required this.textInputType, + // this.label, + this.errorText, + this.hintText, + this.onPressed, + this.onTap, + this.onChanged, + this.onFieldSubmitted, + this.onSaved, + this.validator, + this.enabled, + this.prefixIcon, + this.maxLines, + this.minimumLines, + this.onEditing, + this.prefixtext, + this.helptext, + this.focusNode, + this.obscureText = false, + this.autovalidateMode = AutovalidateMode.disabled, + this.suffixtext, + this.readOnly = false, + this.inputFormatter, + }); + + final String? labelText; + final String? hintText; + final int? maxLines; + final int? minimumLines; + final TextEditingController controller; + final TextInputType textInputType; + final IconData? suffixIcon; + final VoidCallback? onPressed; + final VoidCallback? onTap; + final ValueChanged? onFieldSubmitted; + final ValueChanged? onChanged; + final FormFieldValidator? validator; + final FormFieldSetter? onSaved; + final bool? enabled; + final Widget? prefixIcon; + final void Function()? onEditing; + final String? prefixtext; + final String? helptext; + final String? suffixtext; + final FocusNode? focusNode; + final String? errorText; + final bool obscureText; + final AutovalidateMode autovalidateMode; + final bool readOnly; + final TextInputFormatter? inputFormatter; + + @override + Widget build(BuildContext context) { + return TextFormField( + onFieldSubmitted: onFieldSubmitted, + onChanged: onChanged, + controller: controller, + onTap: onTap, + keyboardType: textInputType, + validator: validator, + maxLines: maxLines, + minLines: minimumLines, + enabled: enabled, + onEditingComplete: onEditing, + onSaved: onSaved, + obscureText: obscureText, + autovalidateMode: autovalidateMode, + inputFormatters: inputFormatter == null ? [] : [inputFormatter!], + // autofocus: true, + // textDirection: TextDirection.rtl, + readOnly: readOnly, + textAlign: TextAlign.right, + style: TextStyle( + overflow: TextOverflow.ellipsis, + color: CustomColors().whitecolor, // Set the text color + ), + + // style: Theme.of(context).textTheme.bodySmall, + cursorColor: CustomColors().whitecolor, + decoration: decoration(), + ); + } + + InputDecoration decoration() { + return InputDecoration( + contentPadding: const EdgeInsets.all(12.0), + hintMaxLines: 1, + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(0.0), + borderSide: BorderSide( + color: CustomColors().grayColor, + )), + enabledBorder: OutlineInputBorder( + borderSide: BorderSide(color: CustomColors().grayColor, width: 0), + borderRadius: BorderRadius.circular(0.0), + ), + focusedBorder: OutlineInputBorder( + borderSide: BorderSide(color: CustomColors().grayColor, width: 0), + borderRadius: BorderRadius.circular(0.0), + ), + errorBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(0.0), + borderSide: BorderSide( + color: CustomColors().red, + width: 1, + ), + ), + errorText: errorText, + errorStyle: TextStyle(color: CustomColors().red), + helperText: helptext, + suffixText: suffixtext, + suffixStyle: TextStyle(color: CustomColors().whitecolor, fontSize: 17, fontWeight: FontWeight.normal), + labelText: labelText, + hintTextDirection: TextDirection.rtl, + floatingLabelBehavior: FloatingLabelBehavior.never, + labelStyle: TextStyle(color: CustomColors().lightblueColor, fontSize: 17, fontWeight: FontWeight.normal), + fillColor: CustomColors().grayColor, + filled: true, + isDense: true, + prefixText: controller.text.isEmpty ? prefixtext : null, + prefixStyle: TextStyle(color: CustomColors().whitecolor, fontSize: 17, fontWeight: FontWeight.normal), + hintText: hintText, + hintStyle: TextStyle( + overflow: TextOverflow.ellipsis, + color: CustomColors().lightblueColor, + fontSize: 17, + fontWeight: FontWeight.normal), + prefixIcon: prefixIcon, + prefixIconConstraints: prefixIcon != null + ? const BoxConstraints( + minWidth: 30, + minHeight: 30, + ) + : const BoxConstraints(), + suffixIcon: suffixIcon != null + ? IconButton( + icon: Icon( + suffixIcon, + color: CustomColors().lightblueColor, + size: 20, + ), + color: CustomColors().lightblueColor, + onPressed: onPressed, + iconSize: 20, + ) + : null, + ); + } +} \ No newline at end of file diff --git a/lib/src/common_widgets/date_time_picker.dart b/lib/src/common_widgets/date_time_picker.dart index 9702e4f8..4f23b5ef 100644 --- a/lib/src/common_widgets/date_time_picker.dart +++ b/lib/src/common_widgets/date_time_picker.dart @@ -1,9 +1,9 @@ import 'dart:async'; import 'package:flutter/material.dart'; -import 'package:starter_architecture_flutter_firebase/src/common_widgets/input_dropdown.dart'; -import 'package:starter_architecture_flutter_firebase/src/constants/app_sizes.dart'; -import 'package:starter_architecture_flutter_firebase/src/utils/format.dart'; +import 'package:flutter_starter_base_app/src/common_widgets/input_dropdown.dart'; +import 'package:flutter_starter_base_app/src/constants/app_sizes.dart'; +import 'package:flutter_starter_base_app/src/utils/format.dart'; class DateTimePicker extends StatelessWidget { const DateTimePicker({ diff --git a/lib/src/common_widgets/empty_placeholder_widget.dart b/lib/src/common_widgets/empty_placeholder_widget.dart deleted file mode 100644 index 3bc0c139..00000000 --- a/lib/src/common_widgets/empty_placeholder_widget.dart +++ /dev/null @@ -1,43 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:go_router/go_router.dart'; -import 'package:starter_architecture_flutter_firebase/src/common_widgets/primary_button.dart'; -import 'package:starter_architecture_flutter_firebase/src/constants/app_sizes.dart'; -import 'package:starter_architecture_flutter_firebase/src/features/authentication/data/firebase_auth_repository.dart'; -import 'package:starter_architecture_flutter_firebase/src/routing/app_router.dart'; - -/// Placeholder widget showing a message and CTA to go back to the home screen. -class EmptyPlaceholderWidget extends ConsumerWidget { - const EmptyPlaceholderWidget({super.key, required this.message}); - final String message; - - @override - Widget build(BuildContext context, WidgetRef ref) { - return Padding( - padding: const EdgeInsets.all(Sizes.p16), - child: Center( - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Text( - message, - style: Theme.of(context).textTheme.headlineMedium, - textAlign: TextAlign.center, - ), - gapH32, - PrimaryButton( - onPressed: () { - final isLoggedIn = - ref.watch(authRepositoryProvider).currentUser != null; - context.goNamed( - isLoggedIn ? AppRoute.jobs.name : AppRoute.signIn.name); - }, - text: 'Go Home', - ) - ], - ), - ), - ); - } -} diff --git a/lib/src/common_widgets/hamburger_menu.dart b/lib/src/common_widgets/hamburger_menu.dart new file mode 100644 index 00000000..44c148b1 --- /dev/null +++ b/lib/src/common_widgets/hamburger_menu.dart @@ -0,0 +1,40 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:flutter_starter_base_app/src/constants/svg_loader.dart'; +import 'package:flutter_starter_base_app/src/common_widgets/basic_page_importer.dart'; + +class HamburgerMenu extends ConsumerWidget { + final List menuItemList; + const HamburgerMenu({super.key, required this.menuItemList}); + @override + Widget build(BuildContext context, WidgetRef ref) => SafeArea( + child: PopupMenuButton( + icon: Container(child: SVGLoader().hamburgerMenuIcon), + clipBehavior: Clip.hardEdge, + offset: const Offset(20, 0), + padding: const EdgeInsets.all(0), + onSelected: (HamburgerMenuItem item) => item.function(), + itemBuilder: (_) { + List> list = List.empty(growable: true); + for (int i = 0; i < menuItemList.length; i++) { + list.add(PopupMenuItem(height: 25, value: menuItemList[i], child: menuItemList[i])); + if (i < menuItemList.length - 1) list.add(const PopupMenuDivider()); + } + return list; + })); +} + +class HamburgerMenuItem extends StatelessWidget { + final String title; + final Widget? tailIcon; + final Widget? leadIcon; + final Function function; + const HamburgerMenuItem({super.key, required this.title, required this.function, this.leadIcon, this.tailIcon}); + @override + Widget build(BuildContext context) => Row(children: [ + if (leadIcon != null) leadIcon!, + Text(title, style: TextStyle(color: CustomColors().whitecolor)), + const Spacer(), + if (tailIcon != null) tailIcon! + ]); +} diff --git a/lib/src/common_widgets/info_text_dialog.dart b/lib/src/common_widgets/info_text_dialog.dart new file mode 100644 index 00000000..d7952b03 --- /dev/null +++ b/lib/src/common_widgets/info_text_dialog.dart @@ -0,0 +1,27 @@ +part of 'package:flutter_starter_base_app/src/utils/alert_dialogs.dart'; + +Future showInfoDialog( + BuildContext context, { + required String label, + required String content, +}) async { + return showDialog( + context: context, + builder: (context) => AlertDialog( + shape: const RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(12))), + insetPadding: const EdgeInsets.symmetric(horizontal: 40), + title: Text(label), + titleTextStyle: DefaultTheme().defaultTextStyle(14), + content: Text(content, maxLines: 5), + contentTextStyle: DefaultTheme().defaultTextStyle(14), + actionsAlignment: MainAxisAlignment.start, + actions: [ + TextButton(child: const Text("OK"), onPressed: () => context.pop()), + ], + ), + ); +} + + +// Usage +// showInfoDialog(context, label: "label", content: "content"); \ No newline at end of file diff --git a/lib/src/common_widgets/list_items_builder.dart b/lib/src/common_widgets/list_items_builder.dart index f5117345..21b0e5da 100644 --- a/lib/src/common_widgets/list_items_builder.dart +++ b/lib/src/common_widgets/list_items_builder.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:starter_architecture_flutter_firebase/src/common_widgets/empty_content.dart'; +import 'package:flutter_starter_base_app/src/common_widgets/empty_content.dart'; +import 'package:flutter_starter_base_app/src/common_widgets/circular_loading_animation.dart'; typedef ItemWidgetBuilder = Widget Function(BuildContext context, T item); @@ -28,7 +29,7 @@ class ListItemsBuilder extends StatelessWidget { }, ) : const EmptyContent(), - loading: () => const Center(child: CircularProgressIndicator()), + loading: () => const Center(child: LoadingAnimation()), error: (_, __) => const EmptyContent( title: 'Something went wrong', message: 'Can\'t load items right now', diff --git a/lib/src/common_widgets/main_action_button.dart b/lib/src/common_widgets/main_action_button.dart new file mode 100644 index 00000000..4ad5e38a --- /dev/null +++ b/lib/src/common_widgets/main_action_button.dart @@ -0,0 +1,21 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:flutter_starter_base_app/src/constants/colors.dart'; + +class MainActionButton extends ConsumerWidget { + final Function onTap; + final bool isDisabled; + final String buttonText; + const MainActionButton({required this.buttonText, required this.onTap, this.isDisabled = false}); + @override + Widget build(BuildContext context, WidgetRef ref) => Container( + width: double.infinity, + child: ElevatedButton( + onPressed: () => isDisabled ? null : onTap(), + style: ButtonStyle( + backgroundColor: WidgetStateProperty.resolveWith( + (states) => isDisabled ? CustomColors().darkGrayBG : CustomColors().primaryTextColor)), + child: Text(buttonText, + style: TextStyle( + color: isDisabled ? CustomColors().lighterGrayText : CustomColors().secondaryButtonTextColor)))); +} diff --git a/lib/src/common_widgets/password_input.dart b/lib/src/common_widgets/password_input.dart new file mode 100644 index 00000000..0ae4f4c3 --- /dev/null +++ b/lib/src/common_widgets/password_input.dart @@ -0,0 +1,48 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:flutter_starter_base_app/src/constants/svg_loader.dart'; +import 'package:flutter_starter_base_app/src/common_widgets/basic_page_importer.dart'; + +class PasswordInputField extends ConsumerWidget { + final String mainText; + final String hintText; + + final AutoDisposeStateProvider password; + + const PasswordInputField({super.key, required this.password, required this.hintText, required this.mainText}); + @override + Widget build(BuildContext context, WidgetRef ref) => Padding( + padding: const EdgeInsets.only(left: 10, right: 10, top: 5), + child: Container( + color: CustomColors().darkGrayBG, + padding: const EdgeInsets.only(left: 17, right: 17), + child: Row(mainAxisSize: MainAxisSize.min, children: [ + Flexible( + flex: 1, + fit: FlexFit.tight, + child: Row(children: [ + Text(mainText, style: DefaultTheme().defaultTextStyle(18)), + Tooltip( + message: 'description', + child: Container(padding: const EdgeInsets.only(left: 10), child: SVGLoader().questionMark)) + ])), + Flexible( + flex: 2, + fit: FlexFit.tight, + child: TextField( + autofocus: true, + obscureText: true, + obscuringCharacter: "*", + cursorColor: CustomColors().primaryTextColor, + onChanged: (newPassword) => ref.watch(password.notifier).update((state) => newPassword), + style: DefaultTheme().defaultTextStyle(15).copyWith(color: CustomColors().primaryTextColor), + decoration: InputDecoration( + filled: true, + hintText: hintText, + border: InputBorder.none, + fillColor: Colors.transparent, + hintTextDirection: TextDirection.rtl, + hintStyle: + DefaultTheme().defaultTextStyle(15).copyWith(color: CustomColors().primaryTextColor)))) + ]))); +} diff --git a/lib/src/common_widgets/primary_button.dart b/lib/src/common_widgets/primary_button.dart index d425905b..f9bec473 100644 --- a/lib/src/common_widgets/primary_button.dart +++ b/lib/src/common_widgets/primary_button.dart @@ -1,5 +1,7 @@ +import 'package:flutter_starter_base_app/src/constants/colors.dart'; import 'package:flutter/material.dart'; -import 'package:starter_architecture_flutter_firebase/src/constants/app_sizes.dart'; +import 'package:flutter_starter_base_app/src/constants/app_sizes.dart'; +import 'package:flutter_starter_base_app/src/common_widgets/circular_loading_animation.dart'; /// Primary button based on [ElevatedButton]. /// Useful for CTAs in the app. @@ -8,26 +10,38 @@ import 'package:starter_architecture_flutter_firebase/src/constants/app_sizes.da /// the text. /// @param onPressed - callback to be called when the button is pressed. class PrimaryButton extends StatelessWidget { - const PrimaryButton( - {super.key, required this.text, this.isLoading = false, this.onPressed}); + const PrimaryButton({ + super.key, + required this.text, + this.isLoading = false, + this.onPressed, + this.backgroundColor, + }); final String text; final bool isLoading; final VoidCallback? onPressed; + final Color? backgroundColor; + @override Widget build(BuildContext context) { return SizedBox( height: Sizes.p48, + width: double.infinity, child: ElevatedButton( onPressed: onPressed, + style: ElevatedButton.styleFrom( + backgroundColor: backgroundColor ?? + Theme.of(context).primaryColor, // Use provided background color or fallback to theme color + ), child: isLoading - ? const CircularProgressIndicator() + ? const LoadingAnimation() : Text( text, textAlign: TextAlign.center, - style: Theme.of(context) - .textTheme - .titleLarge! - .copyWith(color: Colors.white), + style: Theme.of(context).textTheme.titleMedium!.copyWith( + color: CustomColors().darkblueColor, + fontWeight: FontWeight.bold, // Set font weight to bold + ), // Use provided text color or fallback to white ), ), ); diff --git a/lib/src/common_widgets/reception_icon.dart b/lib/src/common_widgets/reception_icon.dart new file mode 100644 index 00000000..c532ff82 --- /dev/null +++ b/lib/src/common_widgets/reception_icon.dart @@ -0,0 +1,16 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_starter_base_app/src/constants/colors.dart'; + +/// https://www.mokoblue.com/measures-of-bluetooth-rssi/ +/// >>> For example, a pretty good value is below -50, a fairly reasonable value is between -70 to -80, while -100 indicates no signal at all. +// https://www.metageek.com/training/resources/understanding-rssi/ +class ReceptionIcon extends StatelessWidget { + final int level; + const ReceptionIcon({super.key, required this.level}); + @override + Widget build(BuildContext context) => switch (level) { + < -80 => Icon(Icons.wifi_1_bar, color: CustomColors().primaryTextColor), + < -60 => Icon(Icons.wifi_2_bar, color: CustomColors().primaryTextColor), + _ => Icon(Icons.wifi, color: CustomColors().primaryTextColor) + }; +} diff --git a/lib/src/common_widgets/responsive_center.dart b/lib/src/common_widgets/responsive_center.dart index 95d4d5d4..f7027b2f 100644 --- a/lib/src/common_widgets/responsive_center.dart +++ b/lib/src/common_widgets/responsive_center.dart @@ -1,5 +1,5 @@ import 'package:flutter/material.dart'; -import 'package:starter_architecture_flutter_firebase/src/constants/breakpoints.dart'; +import 'package:flutter_starter_base_app/src/constants/breakpoints.dart'; /// Reusable widget for showing a child with a maximum content width constraint. /// If available width is larger than the maximum width, the child will be diff --git a/lib/src/common_widgets/responsive_scrollable_card.dart b/lib/src/common_widgets/responsive_scrollable_card.dart index cf2e90fa..519e885d 100644 --- a/lib/src/common_widgets/responsive_scrollable_card.dart +++ b/lib/src/common_widgets/responsive_scrollable_card.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; -import 'package:starter_architecture_flutter_firebase/src/common_widgets/responsive_center.dart'; -import 'package:starter_architecture_flutter_firebase/src/constants/app_sizes.dart'; -import 'package:starter_architecture_flutter_firebase/src/constants/breakpoints.dart'; +import 'package:flutter_starter_base_app/src/common_widgets/responsive_center.dart'; +import 'package:flutter_starter_base_app/src/constants/app_sizes.dart'; +import 'package:flutter_starter_base_app/src/constants/breakpoints.dart'; /// Scrollable widget that shows a responsive card with a given child widget. /// Useful for displaying forms and other widgets that need to be scrollable. diff --git a/lib/src/common_widgets/secondary_action_button.dart b/lib/src/common_widgets/secondary_action_button.dart new file mode 100644 index 00000000..454725a2 --- /dev/null +++ b/lib/src/common_widgets/secondary_action_button.dart @@ -0,0 +1,15 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:flutter_starter_base_app/src/common_widgets/basic_page_importer.dart'; + +class SecondaryActionButton extends ConsumerWidget { + final Function onTap; + final bool isDisabled; + final String buttonText; + const SecondaryActionButton({super.key, required this.buttonText, required this.onTap, this.isDisabled = false}); + @override + Widget build(BuildContext context, WidgetRef ref) => InkWell( + onTap: () => onTap(), + child: Center( + child: Text(buttonText, style: TextStyle(color: CustomColors().primaryTextColor).copyWith(height: 5)))); +} diff --git a/lib/src/common_widgets/select_builder.dart b/lib/src/common_widgets/select_builder.dart new file mode 100644 index 00000000..2ccce43a --- /dev/null +++ b/lib/src/common_widgets/select_builder.dart @@ -0,0 +1,72 @@ +import 'package:flutter_starter_base_app/src/common_widgets/basic_page_importer.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_starter_base_app/src/root/domain/item.dart'; + + +class SelectBuilder extends StatefulWidget { + const SelectBuilder({ + super.key, + required this.onSelect, + required this.items, + this.initialSelection, + this.multiSelect = false, + }); + + final void Function(List) onSelect; + final List items; + final List? initialSelection; + final bool multiSelect; + + @override + State createState() => _SelectBuilderState(); +} + +class _SelectBuilderState extends State { + Set _selectedItems = {}; + + @override + void initState() { + super.initState(); + if (widget.initialSelection != null) { + _selectedItems = Set.from(widget.initialSelection!); + } + } + + @override + Widget build(BuildContext context) { + return ListView.separated( + separatorBuilder: (context, index) => Divider(color: Colors.grey.shade900, height: 2), + itemCount: widget.items.length, + shrinkWrap: true, + itemBuilder: (context, index) { + final item = widget.items[index]; + final isSelected = _selectedItems.any((element) => element.value == item.value); + + return ListTile( + contentPadding: EdgeInsets.zero, + minVerticalPadding: 11, + minTileHeight: 11, + title: Text(item.label, style: DefaultTheme().defaultTextStyle(17)), + trailing: isSelected ? Icon(CupertinoIcons.check_mark, color: CustomColors().primaryTextColor) : null, + onTap: () => _onItemTap(item), + ); + }, + ); + } + + void _onItemTap(Item item) { + setState(() { + if (widget.multiSelect) { + if (_selectedItems.any((element) => element.value == item.value)) { + _selectedItems.removeWhere((element) => element.value == item.value); + } else { + _selectedItems.add(item); + } + } else { + _selectedItems = {item}; + } + }); + widget.onSelect(_selectedItems.toList()); + } +} diff --git a/lib/src/common_widgets/select_time_widget.dart b/lib/src/common_widgets/select_time_widget.dart new file mode 100644 index 00000000..d5887da5 --- /dev/null +++ b/lib/src/common_widgets/select_time_widget.dart @@ -0,0 +1,29 @@ +import 'package:flutter_starter_base_app/src/common_widgets/app_bar.dart'; +import 'package:flutter_starter_base_app/src/constants/colors.dart'; +import 'package:flutter/material.dart'; + +class SelectTimeWidget extends StatelessWidget { + const SelectTimeWidget({super.key, this.pageTitle, this.initialTime}); + + final String? pageTitle; + final TimeOfDay? initialTime; + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: CustomColors().darkGray, + appBar: CustomAppBar( + showBackButton: true, + titleWidget: Text(pageTitle != null ? pageTitle! : "Alert Time"), + showHamburgerMenu: false), + body: Column( + children: [ + const SizedBox(height: 5), + TimePickerDialog( + initialTime: initialTime ?? TimeOfDay.now(), + ), + ], + ), + ); + } +} diff --git a/lib/src/common_widgets/state_select/data/providers.dart b/lib/src/common_widgets/state_select/data/providers.dart new file mode 100644 index 00000000..bb8065ae --- /dev/null +++ b/lib/src/common_widgets/state_select/data/providers.dart @@ -0,0 +1,12 @@ +import 'package:flutter_starter_base_app/src/api/api_facade.dart'; +import 'package:flutter_starter_base_app/src/common_widgets/bluetooth_scanner/domain/bluetooth_response.dart'; +import 'package:flutter_starter_base_app/src/root/domain/country_data.dart'; +import 'package:wifi_scan/wifi_scan.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:flutter_blue_plus/flutter_blue_plus.dart'; +import 'package:riverpod_annotation/riverpod_annotation.dart'; +part 'providers.g.dart'; + +@riverpod +Future> fetchStateList(FetchStateListRef ref, {required String countryName}) async => + (await APIFacade().getApi()).getStates(countryName: countryName); \ No newline at end of file diff --git a/lib/src/common_widgets/state_select/presentation/state_select.dart b/lib/src/common_widgets/state_select/presentation/state_select.dart new file mode 100644 index 00000000..82d88b11 --- /dev/null +++ b/lib/src/common_widgets/state_select/presentation/state_select.dart @@ -0,0 +1,56 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:flutter_starter_base_app/src/common_widgets/select_builder.dart'; +import 'package:flutter_starter_base_app/src/common_widgets/state_select/data/providers.dart'; +import 'package:flutter_starter_base_app/src/root/domain/item.dart'; +import 'package:go_router/go_router.dart'; +import 'package:flutter_starter_base_app/src/common_widgets/app_bar.dart'; +import 'package:flutter_starter_base_app/src/localization/generated/locale_keys.g.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter_starter_base_app/src/common_widgets/circular_loading_animation.dart'; + +class StateSelect extends ConsumerWidget { + const StateSelect({super.key, required this.countryName, this.initialSelection}); + + final String countryName; + final Item? initialSelection; + + @override + Widget build(BuildContext context, WidgetRef ref) { + void onStateSelection(List choosenItemList) { + if (choosenItemList.length == 1) { + debugPrint("choosen item: ${choosenItemList.first.label}"); + context.pop(choosenItemList.first); + return; + } + context.pop(); + } + + return Scaffold( + appBar: CustomAppBar( + showBackButton: true, + titleWidget: Text(LocaleKeys.household_state.tr()), + showHamburgerMenu: false, + ), + body: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10), + child: ref.watch(fetchStateListProvider(countryName: countryName)).when( + data: (List itemList) { + return SelectBuilder( + onSelect: onStateSelection, + items: itemList, + initialSelection: initialSelection != null ? [initialSelection!] : null, + ); + }, + error: (error, stackTrace) { + WidgetsBinding.instance.addPostFrameCallback((_) { + ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Center(child: Text('$error')))); + }); + return Container(); + }, + loading: () => const LoadingAnimation(), + ), + ), + ); + } +} diff --git a/lib/src/common_widgets/subsection_title.dart b/lib/src/common_widgets/subsection_title.dart new file mode 100644 index 00000000..0ca19f12 --- /dev/null +++ b/lib/src/common_widgets/subsection_title.dart @@ -0,0 +1,13 @@ +import 'package:flutter/widgets.dart'; +import 'package:flutter_starter_base_app/src/constants/colors.dart'; +import 'package:flutter_starter_base_app/src/constants/theme_data.dart'; + +class SubSectionTitle extends StatelessWidget { + final String text; + + const SubSectionTitle({super.key, required this.text}); + @override + Widget build(BuildContext context) => Padding( + padding: const EdgeInsets.only(left: 15, right: 10, top: 15, bottom: 5), + child: Text(text, style: DefaultTheme().defaultTextStyle(12).copyWith(color: CustomColors().lightGrayText))); +} diff --git a/lib/src/common_widgets/text_input.dart b/lib/src/common_widgets/text_input.dart new file mode 100644 index 00000000..a524fc3f --- /dev/null +++ b/lib/src/common_widgets/text_input.dart @@ -0,0 +1,57 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:flutter_starter_base_app/src/constants/svg_loader.dart'; +import 'package:flutter_starter_base_app/src/common_widgets/basic_page_importer.dart'; + +class TextInputField extends ConsumerWidget { + final String mainText; + final String hintText; + final String? Function(String?)? validator; + final TextInputFormatter? inputFormatter; + final AutoDisposeStateProvider textProvider; + + const TextInputField({ + super.key, + required this.textProvider, + required this.hintText, + required this.mainText, + this.inputFormatter, + this.validator, + }); + @override + Widget build(BuildContext context, WidgetRef ref) => Padding( + padding: const EdgeInsets.only(left: 10, right: 10, top: 5), + child: Container( + color: CustomColors().darkGrayBG, + padding: const EdgeInsets.only(left: 17, right: 17), + child: Row(mainAxisSize: MainAxisSize.min, children: [ + Flexible( + flex: 1, + fit: FlexFit.tight, + child: Row(children: [ + Text(mainText, style: DefaultTheme().defaultTextStyle(18)), + Tooltip( + message: 'description', + child: Container(padding: const EdgeInsets.only(left: 10), child: SVGLoader().questionMark)) + ])), + Flexible( + flex: 1, + fit: FlexFit.tight, + child: TextFormField( + autofocus: true, + cursorColor: CustomColors().primaryTextColor, + onChanged: (newPassword) => ref.watch(textProvider.notifier).update((state) => newPassword), + style: DefaultTheme().defaultTextStyle(15).copyWith(color: CustomColors().primaryTextColor), + validator: validator, + inputFormatters: inputFormatter == null ? [] : [inputFormatter!], + decoration: InputDecoration( + filled: true, + hintText: hintText, + border: InputBorder.none, + fillColor: Colors.transparent, + hintTextDirection: TextDirection.rtl, + hintStyle: + DefaultTheme().defaultTextStyle(15).copyWith(color: CustomColors().primaryTextColor)))) + ]))); +} diff --git a/lib/src/constants/colors.dart b/lib/src/constants/colors.dart new file mode 100644 index 00000000..71d92060 --- /dev/null +++ b/lib/src/constants/colors.dart @@ -0,0 +1,101 @@ +import 'package:flutter/material.dart' show Color; +import 'package:hexcolor/hexcolor.dart' show HexColor; + +enum ColorPattern { green, blue, red, gray } + +Map colorPatternMap = { + CustomColors().chargerGreen: ColorPattern.green, + CustomColors().chargerBlue: ColorPattern.blue, + CustomColors().chargerRed: ColorPattern.red, + CustomColors().darkGrayBG: ColorPattern.gray, +}; + +/// Stores Custom Theme colors +class CustomColors { + Color get red => HexColor('#CE0B00'); + + /// Hamburger menu BG + Color get darkMenuBG => const Color.fromARGB(255, 58, 58, 60); + + Color get darkestGrayBG => HexColor('#00000000'); + + /// Circular indicator paint on idle + Color get lighterGrayText => HexColor('#FFCBCBCB'); + + /// Circular indicator paint BG on idle + Color get lightGrayText => HexColor('#FF727272'); + + /// - Bottom Nav. Bar BG + Color get darkGrayBG => HexColor('#A6545458'); + + Color get darkerGrayBG => HexColor('#993B383E'); + + /// - Bottom Nav. Bar inactive text + Color get darkGrayText => HexColor('#999999'); + + /// - Bottom Nav. Bar active item/text + Color get primaryTextColor => HexColor('#D0BCFF'); + + /// - Remove Household Button BG + Color get secondaryButtonBG => HexColor('#EFB8C8'); + // Color get primaryButtonTextColor => HexColor("#381E72"); + Color get secondaryButtonTextColor => HexColor('#381E72'); + + /// Charger green + Color get chargerGreen => HexColor('#00FF1A'); + Color get buttonGreen => HexColor('#33C542'); + + ///Charger blue + Color get chargerBlue => HexColor('#02B0F0'); + + /// Charger orange + Color get chargerRed => HexColor('#ED5601'); + Color get chargerGray => HexColor('#FFFFFF'); + + /// Circular indicator + Color get circularIndicatorBG => HexColor('#002A25'); //#002A25//#007568//#BDFF00//FD0000 + + Color get vehicleBlueMask => HexColor('#CDE8E1'); + Color get vehicleRedMask => HexColor('#ED5601'); + Color get vehicleGreenMask => HexColor('#86FE16'); + + /// Gauge colors + + Color get batteryRed => HexColor('FFFD0000'); + Color get batteryOrange => HexColor('#FFBDFF00'); + // Color get batteryOrangishYellow => HexColor('#FF007568'); + Color get batteryGreen => HexColor('#00FF38'); + Color get batteryYellowishGreen => HexColor('#D9FD00'); + Color get darkGray => HexColor("#1C1C1E"); + Color get grayColor => const Color.fromRGBO(84, 84, 88, 0.65); + + Color get lightblueColor => HexColor("#D0BCFF"); /* primarybuttonBGcolor, hinttextcolor, actiontextbuttoncolor */ + Color get darkblueColor => HexColor("#381E72"); /*primarybuttontextcolor*/ + Color get whitecolor => HexColor("#FFFFFF"); /* TextColor*/ + Color get lightGrayColor => HexColor('#3A3A3C'); // Card background color + Color get darkelevatedColor => HexColor("#3A3A3C"); + + Color get lightredColor => HexColor("#EFB8C8"); + + Color get secondaryDark =>const Color.fromRGBO(235, 235, 245, 0.6); + Color get progressBarFilledColor => primaryTextColor; + Color get progressBarInactiveColor => HexColor('#ffbababa'); + Color get blueColor => HexColor("02B0F0"); //Manual schedule available hours color + Color get blackColor => HexColor("38383A"); // Not avilable hours color + + /// Reports Page + Color get reportBarBlue => HexColor('#02B0F0'); + Color get reportBarGray => HexColor('#EBEBF5'); + Color get reportByButtonBG => HexColor('#4A4458'); + Color get tableBorderColor => HexColor('#99EBEBF5'); + + ///Setup flow + Color get setUpText => HexColor("#CCC2DC"); + + ///close icon + Color get bgCloseIcon => const Color.fromRGBO(127, 127, 127, 0.5); + Color get closeIcon => const Color.fromRGBO(141, 135, 135, 1); + + // dialog color + Color get materialGrayBG => HexColor('#FF3B383E'); +} diff --git a/lib/src/constants/env_constants.dart b/lib/src/constants/env_constants.dart new file mode 100644 index 00000000..6cceb780 --- /dev/null +++ b/lib/src/constants/env_constants.dart @@ -0,0 +1,20 @@ +class EnvValues { + static String get productionEnv => 'prod'; + static String get preProductionEnv => 'preprod'; + static String get developmentEnv => 'dev'; + + static const String env = String.fromEnvironment('ENV'); + static const String username = String.fromEnvironment('USERNAME'); + static const String password = String.fromEnvironment('PASSWORD'); + + static const String apiEnv = String.fromEnvironment('API_ENV'); + static const String apiHost = String.fromEnvironment('API_HOST'); + static const String apiVersion = String.fromEnvironment('API_VERSION'); + static const String apiAccessToken = String.fromEnvironment('API_ACCESS_TOKEN'); + static const String apiRefreshToken = String.fromEnvironment('API_REFRESH_TOKEN'); +} + +class EnvKeys { + static String get apiAccessTokenKey => 'API_ACCESS_TOKEN'; + static String get apiRefreshTokenKey => 'API_REFRESH_TOKEN'; +} diff --git a/lib/src/constants/keys.dart b/lib/src/constants/keys.dart index ab319745..672b873a 100644 --- a/lib/src/constants/keys.dart +++ b/lib/src/constants/keys.dart @@ -8,4 +8,7 @@ class Keys { static const String logout = 'logout'; static const String alertDefault = 'alertDefault'; static const String alertCancel = 'alertCancel'; + + static const String invalidVIN = 'Received VIN is not valid'; + static const String negativeRange = 'Received vehicle range is negative'; } diff --git a/lib/src/constants/strings.dart b/lib/src/constants/strings.dart index ef092248..b5367216 100644 --- a/lib/src/constants/strings.dart +++ b/lib/src/constants/strings.dart @@ -5,8 +5,7 @@ class Strings { // Logout static const String logout = 'Logout'; - static const String logoutAreYouSure = - 'Are you sure that you want to logout?'; + static const String logoutAreYouSure = 'Are you sure that you want to logout?'; static const String logoutFailed = 'Logout failed'; // Sign In Page @@ -16,8 +15,13 @@ class Strings { static const String or = 'or'; static const String signInFailed = 'Sign in failed'; + static const String home = 'home'; + static const String account = 'account'; + static const String alerts = 'alerts'; + static const String schedule = 'schedule'; + static const String reports = 'reports'; // Home page - static const String homePage = 'Home Page'; + // static const String homePage = 'Home Page'; // Jobs page static const String jobs = 'Jobs'; @@ -26,6 +30,6 @@ class Strings { static const String entries = 'Entries'; // Account page - static const String account = 'Account'; - static const String accountPage = 'Account Page'; + // static const String account = 'Account'; + // static const String accountPage = 'Account Page'; } diff --git a/lib/src/constants/svg_loader.dart b/lib/src/constants/svg_loader.dart new file mode 100644 index 00000000..32dd1d17 --- /dev/null +++ b/lib/src/constants/svg_loader.dart @@ -0,0 +1,190 @@ +import 'package:flutter_starter_base_app/src/constants/colors.dart'; +import 'package:flutter_starter_base_app/src/constants/strings.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/widgets.dart'; +import 'package:flutter_svg/flutter_svg.dart' hide Svg; + +class SVGLoader { + final String _bottomAppBarAssetPath = 'assets/bottom_navigation_bar/'; + final String _vehicleIconPath = 'assets/vehicle_icon/'; + final String _smallChargerIconPath = 'assets/charger_icon/small/'; + final String _redsPath = 'assets/charger_icon/red/'; + final String _bluesPath = 'assets/charger_icon/blue/'; + final String _graysPath = 'assets/charger_icon/gray/'; + final String _greensPath = 'assets/charger_icon/green/'; + final String _detailIconPath = 'assets/detail_icon/'; + final String _chargerSetupIconPath = 'assets/setup_charger/'; + final String _dongleSetupIconPath = 'assets/setup_dongle/'; + + /// ----- Bottom Bar Icons ----- + Widget get accountIcon => SvgPicture.asset( + '${_bottomAppBarAssetPath}account.svg', + semanticsLabel: Strings.account, + colorFilter: ColorFilter.mode(CustomColors().primaryTextColor, BlendMode.srcIn), + ); + Widget get accountInactiveIcon => SvgPicture.asset( + '${_bottomAppBarAssetPath}account.svg', + semanticsLabel: Strings.account, + colorFilter: ColorFilter.mode(CustomColors().darkGrayText, BlendMode.srcIn), + ); + Widget get alertsIcon => SvgPicture.asset( + '${_bottomAppBarAssetPath}alerts.svg', + semanticsLabel: Strings.alerts, + colorFilter: ColorFilter.mode(CustomColors().primaryTextColor, BlendMode.srcIn), + ); + Widget get alertsInactiveIcon => SvgPicture.asset( + '${_bottomAppBarAssetPath}alerts.svg', + semanticsLabel: Strings.alerts, + colorFilter: ColorFilter.mode(CustomColors().darkGrayText, BlendMode.srcIn), + ); + Widget get homeIcon => SvgPicture.asset( + '${_bottomAppBarAssetPath}home.svg', + semanticsLabel: Strings.home, + colorFilter: ColorFilter.mode(CustomColors().primaryTextColor, BlendMode.srcIn), + ); + Widget get homeInactiveIcon => SvgPicture.asset( + '${_bottomAppBarAssetPath}home.svg', + semanticsLabel: Strings.home, + colorFilter: ColorFilter.mode(CustomColors().darkGrayText, BlendMode.srcIn), + ); + Widget get reportsIcon => SvgPicture.asset( + '${_bottomAppBarAssetPath}reports.svg', + semanticsLabel: Strings.reports, + colorFilter: ColorFilter.mode(CustomColors().primaryTextColor, BlendMode.srcIn), + ); + Widget get reportsInactiveIcon => SvgPicture.asset( + '${_bottomAppBarAssetPath}reports.svg', + semanticsLabel: Strings.reports, + colorFilter: ColorFilter.mode(CustomColors().darkGrayText, BlendMode.srcIn), + ); + Widget get scheduleIcon => SvgPicture.asset( + '${_bottomAppBarAssetPath}schedule.svg', + semanticsLabel: Strings.schedule, + colorFilter: ColorFilter.mode(CustomColors().primaryTextColor, BlendMode.srcIn), + ); + Widget get scheduleInactiveIcon => SvgPicture.asset( + '${_bottomAppBarAssetPath}schedule.svg', + semanticsLabel: Strings.schedule, + colorFilter: ColorFilter.mode(CustomColors().darkGrayText, BlendMode.srcIn), + ); + + SvgPicture getVehicleIcon(String iconName, {ColorFilter? colorFilter}) => + SvgPicture.asset('${_vehicleIconPath + iconName}.svg', + semanticsLabel: Strings.schedule, colorFilter: colorFilter); + + SvgPicture getSUVIcon({ColorFilter? colorFilter}) => + SvgPicture.asset('${_vehicleIconPath}suv.svg', semanticsLabel: Strings.schedule, colorFilter: colorFilter); + + /// ----- Vehicle Icons ----- + SvgPicture getMotoIcon({ColorFilter? colorFilter}) => + SvgPicture.asset('${_vehicleIconPath}motorcycle.svg', semanticsLabel: Strings.schedule, colorFilter: colorFilter); + SvgPicture getLorryIcon({ColorFilter? colorFilter}) => + SvgPicture.asset('${_vehicleIconPath}van.svg', semanticsLabel: Strings.schedule, colorFilter: colorFilter); + SvgPicture getSedanIcon({ColorFilter? colorFilter}) => + SvgPicture.asset('${_vehicleIconPath}sedan.svg', semanticsLabel: Strings.schedule, colorFilter: colorFilter); + SvgPicture getPickupIcon({ColorFilter? colorFilter}) => + SvgPicture.asset('${_vehicleIconPath}pickup.svg', semanticsLabel: Strings.schedule, colorFilter: colorFilter); + + /// - Small + Widget get smallRedChargerIcon => SvgPicture.asset('${_smallChargerIconPath}red.svg'); + Image get smallRedChargerGlow => Image.asset('${_smallChargerIconPath}red_glow.png', fit: BoxFit.cover); + Widget get smallIdleChargerIcon => SvgPicture.asset('${_smallChargerIconPath}idle.svg'); + + Widget get tinyRed => SvgPicture.asset('${_redsPath}tiny.svg'); + Image get tinyRedGlow => Image.asset('${_redsPath}tiny_glow.png', fit: BoxFit.cover); + Widget get smallRed => SvgPicture.asset('${_redsPath}small.svg'); + Image get smallRedGlow => Image.asset('${_redsPath}small_glow.png', fit: BoxFit.cover); + Widget get mediumRed => SvgPicture.asset('${_redsPath}medium.svg'); + Image get mediumRedGlow => Image.asset('${_redsPath}medium_glow.png', fit: BoxFit.cover); + Widget get largeRed => SvgPicture.asset('${_redsPath}large.svg'); + Image get largeRedGlow => Image.asset('${_redsPath}large_glow.png', fit: BoxFit.cover); + + Widget get tinyGray => SvgPicture.asset('${_graysPath}tiny.svg'); + Widget get smallGray => SvgPicture.asset('${_graysPath}small.svg'); + Widget get mediumGray => SvgPicture.asset('${_graysPath}medium.svg'); + Widget get largeGray => SvgPicture.asset('${_graysPath}large.svg'); + Image get emptyGlow => Image.asset('${_graysPath}empty.png'); + + Widget get tinyBlue => SvgPicture.asset('${_bluesPath}tiny.svg'); + Image get tinyBlueGlow => Image.asset('${_bluesPath}tiny_glow.png', fit: BoxFit.cover); + Widget get smallBlue => SvgPicture.asset('${_bluesPath}small.svg'); + Image get smallBlueGlow => Image.asset('${_bluesPath}small_glow.png', fit: BoxFit.cover); + Widget get mediumBlue => SvgPicture.asset('${_bluesPath}medium.svg'); + Image get mediumBlueGlow => Image.asset('${_bluesPath}medium_glow.png', fit: BoxFit.cover); + Widget get largeBlue => SvgPicture.asset('${_bluesPath}large.svg'); + Image get largeBlueGlow => Image.asset('${_bluesPath}large_glow.png', fit: BoxFit.cover); + + Widget get tinyGreen => SvgPicture.asset('${_greensPath}tiny.svg'); + Image get tinyGreenGlow => Image.asset('${_greensPath}tiny_glow.png', fit: BoxFit.cover); + Widget get smallGreen => SvgPicture.asset('${_greensPath}small.svg'); + Image get smallGreenGlow => Image.asset('${_greensPath}small_glow.png', fit: BoxFit.cover); + Widget get mediumGreen => SvgPicture.asset('${_greensPath}medium.svg'); + Image get mediumGreenGlow => Image.asset('${_greensPath}medium_glow.png', fit: BoxFit.cover); + Widget get largeGreen => SvgPicture.asset('${_greensPath}large.svg'); + Image get largeGreenGlow => Image.asset('${_greensPath}large_glow.png', fit: BoxFit.cover); + + /// ----- Detail Page Icons ----- + Widget get questionMark => SvgPicture.asset('${_detailIconPath}question_mark.svg'); + Widget get rightArrow => SvgPicture.asset('${_detailIconPath}right_arrow.svg'); + Widget get leftArrow => SvgPicture.asset('${_detailIconPath}left_arrow.svg'); + Widget get checkMark => SvgPicture.asset('${_detailIconPath}check_mark.svg'); + Widget get emptyMark => SvgPicture.asset('${_detailIconPath}empty_mark.svg'); + Widget get chargerWifi => SvgPicture.asset('${_detailIconPath}wifi_charger_icon.svg'); + Widget get wifi => SvgPicture.asset('${_detailIconPath}wifi_icon.svg'); + + /// ----- Misc ----- + Widget get hamburgerMenuIcon => SvgPicture.asset('assets/hamburger_menu_icon.svg'); + Widget get chargerRateIcon => SvgPicture.asset('assets/charge_rate_icon.svg'); + Widget get closeIcon => SvgPicture.asset('assets/close_button.svg', height: 40); + Widget get externalLinkIcon => SvgPicture.asset('assets/external_link.svg'); + + /// ----- Add Charger ----- + Widget get phoneBGImage => Image.asset('${_chargerSetupIconPath}phone_background.png'); + Widget get phoneImage => Image.asset('${_chargerSetupIconPath}phone.png'); + Widget get qrChargerIcon => Image.asset('${_chargerSetupIconPath}qr_charger.png'); + Widget get plugChargerIcon => SvgPicture.asset('${_chargerSetupIconPath}plug_charger.svg'); + Widget get bluetoothIcon => SvgPicture.asset('${_chargerSetupIconPath}bluetooth_icon.svg'); + Widget get connectionFailIcon => SvgPicture.asset('${_chargerSetupIconPath}connection_fail.svg'); + Widget get emptyCircleIcon => SvgPicture.asset('${_chargerSetupIconPath}empty_circle_large.svg'); + Widget get connectionSuccessIcon => SvgPicture.asset('${_chargerSetupIconPath}connection_success.svg'); + Widget get addChargerButtonIcon => SvgPicture.asset('assets/add_button_icon.svg'); + + /// ----- Add Dongle ----- + Widget get vehicleImage => Image.asset('${_dongleSetupIconPath}vehicle_icon.png'); + Widget get dongleImage => Image.asset('${_dongleSetupIconPath}dongle.png'); + Widget get dongleInstalleImage => Image.asset('${_dongleSetupIconPath}install_image.png'); + +// todo move + List> getChargerSizeMap(ColorPattern pattern) { + switch (pattern) { + case ColorPattern.red: + return [ + {tinyRed: emptyGlow}, + {smallRed: emptyGlow}, + {mediumRed: emptyGlow}, + {largeRed: emptyGlow} + ]; + case ColorPattern.green: + return [ + {tinyGreen: tinyGreenGlow}, + {smallGreen: smallGreenGlow}, + {mediumGreen: mediumGreenGlow}, + {largeGreen: largeGreenGlow}, + ]; + case ColorPattern.blue: + return [ + {tinyBlue: emptyGlow}, + {smallBlue: emptyGlow}, + {mediumBlue: emptyGlow}, + {largeBlue: emptyGlow}, + ]; + case ColorPattern.gray: + return [ + {tinyGray: emptyGlow}, + {smallGray: emptyGlow}, + {mediumGray: emptyGlow}, + {largeGray: emptyGlow}, + ]; + } + } +} diff --git a/lib/src/constants/theme_data.dart b/lib/src/constants/theme_data.dart new file mode 100644 index 00000000..2b5b954c --- /dev/null +++ b/lib/src/constants/theme_data.dart @@ -0,0 +1,98 @@ +import 'package:flutter_starter_base_app/src/constants/colors.dart'; +import 'package:flutter/material.dart'; + +const primaryColor = Colors.black; + +class DefaultTheme { + TextStyle defaultTextStyle(double fontSize) => + TextStyle(color: Colors.white, fontSize: fontSize, overflow: TextOverflow.ellipsis); + BoxDecoration getColoredBoxDecoration(Color color) => BoxDecoration( + border: Border.all(color: color, width: 2, style: BorderStyle.solid), + color: CustomColors().darkestGrayBG, + borderRadius: const BorderRadius.all(Radius.circular(25))); + BoxDecoration getColoredBoxDecorationSharper(Color color) => BoxDecoration( + border: Border.all(color: color, width: 2, style: BorderStyle.solid), + color: CustomColors().darkestGrayBG, + borderRadius: const BorderRadius.all(Radius.circular(10))); + ThemeData get themeData => ThemeData( + colorSchemeSeed: primaryColor, + unselectedWidgetColor: Colors.grey, + appBarTheme: AppBarTheme( + backgroundColor: primaryColor, + foregroundColor: Colors.white, + elevation: 2.0, + centerTitle: true, + iconTheme: IconThemeData(color: CustomColors().primaryTextColor)), + scaffoldBackgroundColor: const Color.fromARGB(255, 0, 0, 0), + popupMenuTheme: PopupMenuThemeData( + position: PopupMenuPosition.under, + color: CustomColors().darkMenuBG, + shape: const RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(10)))), + dividerTheme: const DividerThemeData(color: Color.fromARGB(51, 255, 255, 255)), + dividerColor: const Color.fromARGB(255, 255, 255, 255), + elevatedButtonTheme: ElevatedButtonThemeData( + style: ButtonStyle( + backgroundColor: WidgetStateProperty.all(primaryColor), + foregroundColor: WidgetStateProperty.all(Colors.white))), + bottomAppBarTheme: const BottomAppBarTheme(color: Colors.black), + outlinedButtonTheme: OutlinedButtonThemeData( + style: ButtonStyle( + backgroundColor: WidgetStateProperty.all(primaryColor), + foregroundColor: WidgetStateProperty.all(Colors.white))), + textButtonTheme: TextButtonThemeData( + style: ButtonStyle(foregroundColor: WidgetStateProperty.all(CustomColors().primaryTextColor)), + ), + floatingActionButtonTheme: const FloatingActionButtonThemeData(backgroundColor: primaryColor), + timePickerTheme: TimePickerThemeData( + helpTextStyle: TextStyle(color: CustomColors().lighterGrayText), + backgroundColor: const Color.fromRGBO(43, 41, 48, 1), + hourMinuteColor: WidgetStateColor.resolveWith( + (states) => states.contains(WidgetState.selected) + ? const Color.fromRGBO(79, 55, 139, 1) + : const Color.fromRGBO(54, 52, 59, 1), + ), + hourMinuteTextColor: WidgetStateColor.resolveWith( + (states) => states.contains(WidgetState.selected) + ? const Color.fromRGBO(234, 221, 255, 1) + : CustomColors().lighterGrayText, + ), + dialBackgroundColor: const Color.fromRGBO(54, 52, 59, 1), + dialTextColor: WidgetStateColor.resolveWith( + (states) => states.contains(WidgetState.selected) ? Colors.black : Colors.white, + ), + dialHandColor: CustomColors().primaryTextColor, + dayPeriodBorderSide: BorderSide( + color: CustomColors().lightGrayText, + ), + dayPeriodColor: WidgetStateColor.resolveWith( + (states) => + states.contains(WidgetState.selected) ? const Color.fromRGBO(99, 59, 72, 1) : Colors.transparent, + ), + dayPeriodTextColor: WidgetStateColor.resolveWith( + (states) => states.contains(WidgetState.selected) + ? const Color.fromRGBO(255, 216, 228, 1) + : CustomColors().lighterGrayText, + ), + entryModeIconColor: CustomColors().lightGrayText, + cancelButtonStyle: ButtonStyle(foregroundColor: WidgetStatePropertyAll(CustomColors().primaryTextColor)), + confirmButtonStyle: ButtonStyle(foregroundColor: WidgetStatePropertyAll(CustomColors().primaryTextColor)), + ), + dataTableTheme: DataTableThemeData( + headingRowColor: WidgetStatePropertyAll(CustomColors().darkerGrayBG), + decoration: BoxDecoration( + border: Border.symmetric(horizontal: BorderSide(color: CustomColors().tableBorderColor, width: .5))), + dataTextStyle: defaultTextStyle(11).copyWith(color: CustomColors().tableBorderColor), + columnSpacing: 0, + horizontalMargin: 0, + dataRowMinHeight: 4, + dataRowMaxHeight: 20, + dividerThickness: .5, + headingRowHeight: 24, + dataRowColor: WidgetStatePropertyAll(CustomColors().darkerGrayBG)), + dialogTheme: DialogTheme( + backgroundColor: CustomColors().materialGrayBG, + titleTextStyle: defaultTextStyle(24).copyWith(color: Colors.white), + contentTextStyle: defaultTextStyle(14).copyWith(color: CustomColors().lighterGrayText), + ), + ); +} diff --git a/lib/src/features/account/data/account_provider.dart b/lib/src/features/account/data/account_provider.dart new file mode 100644 index 00000000..98e76a8d --- /dev/null +++ b/lib/src/features/account/data/account_provider.dart @@ -0,0 +1,34 @@ +import 'package:flutter_starter_base_app/src/api/api.dart'; +import 'package:flutter_starter_base_app/src/api/api_facade.dart'; +import 'package:flutter_starter_base_app/src/root/domain/account.dart'; +import 'package:flutter_starter_base_app/src/root/domain/basic_api_response.dart'; +import 'package:flutter_starter_base_app/src/features/account/domain/create_account.dart'; +import 'package:flutter_starter_base_app/src/features/account/domain/eula.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:riverpod_annotation/riverpod_annotation.dart'; +part 'account_provider.g.dart'; + +@riverpod +Future fetchAccountDetails(FetchAccountDetailsRef ref) async => + await (await APIFacade().getApi()).getAccountDetails(); + +@Riverpod(dependencies: []) +Future fetchEULAStatus(FetchEULAStatusRef ref) async => await (await APIFacade().getApi()).acceptedEULA(); +@Riverpod(dependencies: []) +Future fetchEULA(FetchEULARef ref, {required String languageCode}) async => await API().getEULA(languageCode); + +@riverpod +Future submitEULA(SubmitEULARef ref, {required String agreementId}) async => await API().submitEULA(agreementId); + +/// tracks EULA ID +StateProvider eulaProvider = StateProvider((ref) => null); + +@riverpod +Future createAccount(CreateAccountRef ref, {required CreateAccountRequest createAccountRequest}) async { + return await API().createAccount(createAccountRequest: createAccountRequest); +} + +@riverpod +Future saveAccountDetails(SaveAccountDetailsRef ref,{required String phoneNumber, required String emailId}) async{ + return await API().saveAccountDetails(phoneNumber:phoneNumber,emailId:emailId); +} \ No newline at end of file diff --git a/lib/src/features/account/domain/create_account.dart b/lib/src/features/account/domain/create_account.dart new file mode 100644 index 00000000..eaa696ae --- /dev/null +++ b/lib/src/features/account/domain/create_account.dart @@ -0,0 +1,23 @@ + +import 'package:json_annotation/json_annotation.dart'; +part 'create_account.g.dart'; + +@JsonSerializable() +class CreateAccountRequest { + final String username; + final String email; + final String password; + final String phoneNumber; + final String languageCode; + CreateAccountRequest({ + required this.username, + required this.email, + required this.password, + required this.phoneNumber, + required this.languageCode, + }); + factory CreateAccountRequest.fromJson(Map json) => + _$CreateAccountRequestFromJson(json); + Map toJson() => _$CreateAccountRequestToJson(this); +} + \ No newline at end of file diff --git a/lib/src/features/account/domain/eula.dart b/lib/src/features/account/domain/eula.dart new file mode 100644 index 00000000..fb68cb27 --- /dev/null +++ b/lib/src/features/account/domain/eula.dart @@ -0,0 +1,11 @@ +import 'package:json_annotation/json_annotation.dart'; +part 'eula.g.dart'; + +@JsonSerializable() +class EULA { + final String agreementId; + final String text; + EULA({required this.text, required this.agreementId}); + factory EULA.fromJson(Map json) => _$EULAFromJson(json); + Map toJson() => _$EULAToJson(this); +} diff --git a/lib/src/features/account/presentation/add_account_page.dart b/lib/src/features/account/presentation/add_account_page.dart new file mode 100644 index 00000000..c66b844b --- /dev/null +++ b/lib/src/features/account/presentation/add_account_page.dart @@ -0,0 +1,47 @@ +import 'package:flutter_starter_base_app/src/constants/colors.dart'; +import 'package:flutter_starter_base_app/src/constants/theme_data.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:flutter_starter_base_app/src/localization/generated/locale_keys.g.dart'; +import 'package:easy_localization/easy_localization.dart'; + +class AddAccountPage extends ConsumerWidget { + const AddAccountPage({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + return Scaffold( + body: SingleChildScrollView( + child: Column(children: [ + Container( + color: primaryColor, + child: Column( + children: [ + SizedBox( + height: MediaQuery.of(context).size.height * 0.10, + ), + Image.asset("assets/images/house.png"), + Padding( + padding: + const EdgeInsets.only(left: 50.0, right: 50, top: 50, bottom: 10), + child: Text( + LocaleKeys.account_wizard_welcomeDesc.tr(), + textAlign: TextAlign.start, + maxLines: 3, + overflow: TextOverflow.ellipsis, + style: TextStyle( + color: CustomColors().whitecolor, + fontSize: 17, + fontWeight: FontWeight.w400), + ), + ), + SizedBox( + height: MediaQuery.of(context).size.height * 0.10, + ), + ], + ), + ) + ]), + )); + } +} diff --git a/lib/src/features/account/presentation/default_contact_info_page.dart b/lib/src/features/account/presentation/default_contact_info_page.dart new file mode 100644 index 00000000..b0ae39a3 --- /dev/null +++ b/lib/src/features/account/presentation/default_contact_info_page.dart @@ -0,0 +1,140 @@ +import 'package:flutter_starter_base_app/src/common_widgets/basic_page_importer.dart'; +import 'package:flutter_starter_base_app/src/common_widgets/custom_text_form_field.dart'; +import 'package:email_validator/email_validator.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; + +class DefaultContactInfoPage extends ConsumerStatefulWidget { + final GlobalKey? formKey; + final TextEditingController emailController; + final TextEditingController phoneNumberController; + const DefaultContactInfoPage( + {required this.formKey, required this.emailController, required this.phoneNumberController, super.key}); + @override + _DefaultContactInfoPageState createState() => _DefaultContactInfoPageState(); +} + +class _DefaultContactInfoPageState extends ConsumerState { + FocusNode _emailfocusNode = FocusNode(); + FocusNode _passwordfocusNode = FocusNode(); + @override + void initState() { + super.initState(); + _emailfocusNode = FocusNode(); + _passwordfocusNode = FocusNode(); + } + @override + void dispose() { + _emailfocusNode.dispose(); + _passwordfocusNode.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final TextEditingController emailActiveController = widget.emailController; + final TextEditingController phoneNumberActiveController = widget.phoneNumberController; + return Scaffold( + body: SingleChildScrollView( + child: Form( + key: widget.formKey, + child: Column( + children: [ + Padding( + padding: const EdgeInsets.only(top: 15, bottom: 15), + child: Text( + "Default Contact Information", + style: TextStyle(fontSize: 17, fontWeight: FontWeight.w600, color: CustomColors().whitecolor), + ), + ), + phoneNumberTextField(context, phoneNumberActiveController), + emailTextField(context, emailActiveController) + ], + ), + ), + ), + ); + } + + CustomTextFormField phoneNumberTextField(BuildContext context, TextEditingController controller) { + return CustomTextFormField( + autovalidateMode: AutovalidateMode.onUserInteraction, + controller: controller, + focusNode: _emailfocusNode, + textInputType: TextInputType.number, + hintText: "Enter Phone Number", + prefixIcon: Padding( + padding: const EdgeInsets.only(left: 15.0, right: 10), + child: Row( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + 'Phone Number', + style: TextStyle( + color: CustomColors().whitecolor, + fontWeight: FontWeight.w400, + fontSize: 17), + ), + SizedBox( + width: MediaQuery.of(context).size.width * 0.02, + ), + Icon( + Icons.help, + color: CustomColors().whitecolor, + ) + ], + ), + ), + inputFormatter: FilteringTextInputFormatter.digitsOnly, + validator: (value) { + if (value == null || value.isEmpty) { + return "Phone Number can't be empty"; + } + // if (value.length < 10) { + // return "Phone Number must be 10 characters"; + // } + // if (value.length > 10) { + // return "Phone Number can't be more than 10 characters"; + // } + return null; + }, + onSaved: (value) {}, + ); + } + + CustomTextFormField emailTextField(BuildContext context, TextEditingController controller) { + return CustomTextFormField( + autovalidateMode: AutovalidateMode.onUserInteraction, + controller: controller, + focusNode: _emailfocusNode, + textInputType: TextInputType.emailAddress, + hintText: "Enter Email", + prefixIcon: Padding( + padding: const EdgeInsets.only(left: 15.0, right: 10), + child: Row( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + 'Email', + style: TextStyle( + color: CustomColors().whitecolor, + fontWeight: FontWeight.w400, + fontSize: 17), + ), + SizedBox( + width: MediaQuery.of(context).size.width * 0.02, + ), + Icon( + Icons.help, + color: CustomColors().whitecolor, + ) + ], + ), + ), + validator: (value) => EmailValidator.validate(value ?? '') ? null : "Please enter a valid email", + ); + } +} diff --git a/lib/src/features/account/presentation/eula_transition.dart b/lib/src/features/account/presentation/eula_transition.dart new file mode 100644 index 00000000..68b2912d --- /dev/null +++ b/lib/src/features/account/presentation/eula_transition.dart @@ -0,0 +1,24 @@ +import 'package:flutter_starter_base_app/src/routing/routes.dart'; +import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:flutter_starter_base_app/src/constants/theme_data.dart'; +import 'package:flutter_starter_base_app/src/features/account/data/account_provider.dart'; +import 'package:flutter_starter_base_app/src/common_widgets/circular_loading_animation.dart'; + +class EULATransition extends ConsumerWidget { + const EULATransition({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + return ref.watch(submitEULAProvider(agreementId: ref.read(eulaProvider)?.agreementId ?? '')).when( + data: (bool isSuccesful) { + WidgetsBinding.instance.addPostFrameCallback((_) { + if (context.mounted) context.goNamed(isSuccesful ? AppRoute.home.name : AppRoute.splash.name); + }); + return Container(); + }, + error: (error, stackTrace) => Center(child: Text('Error: $error', style: DefaultTheme().defaultTextStyle(20))), + loading: () => const LoadingAnimation()); + } +} diff --git a/lib/src/features/account/presentation/eula_view.dart b/lib/src/features/account/presentation/eula_view.dart new file mode 100644 index 00000000..4be7d606 --- /dev/null +++ b/lib/src/features/account/presentation/eula_view.dart @@ -0,0 +1,75 @@ +import 'package:flutter_starter_base_app/src/common_widgets/action_text_button.dart'; +import 'package:flutter_starter_base_app/src/common_widgets/primary_button.dart'; +import 'package:flutter_starter_base_app/src/constants/colors.dart'; +import 'package:flutter_starter_base_app/src/constants/theme_data.dart'; +import 'package:flutter_starter_base_app/src/features/account/data/account_provider.dart'; +import 'package:flutter_starter_base_app/src/features/account/domain/eula.dart'; +import 'package:flutter_starter_base_app/src/routing/routes.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:go_router/go_router.dart'; + +class EulaView extends ConsumerWidget { + final bool isCancellable; + final Function(bool acceptEULA) onEULAAccepted; + + const EulaView({super.key, required this.onEULAAccepted, this.isCancellable = true}); + + @override + Widget build(BuildContext context, WidgetRef ref) => SafeArea( + child: Scaffold( + body: GestureDetector( + child: Container( + color: primaryColor, + child: Container( + margin: const EdgeInsets.all(2.0), + padding: const EdgeInsets.all(10.0), + child: Column(mainAxisSize: MainAxisSize.min, children: [ + Expanded( + child: Container( + padding: const EdgeInsets.all(6), + margin: const EdgeInsets.only(bottom: 10), + width: MediaQuery.of(context).size.width, + color: CustomColors().whitecolor, + child: SingleChildScrollView( + child: ref + .watch(fetchEULAProvider( + languageCode: PlatformDispatcher.instance.locale.toString())) + .when( + data: (EULA eula) { + WidgetsBinding.instance.addPostFrameCallback( + (_) => ref.read(eulaProvider.notifier).state = eula); + return RichText( + text: TextSpan( + text: 'EULA INFORMATION\n\n', + style: DefaultTheme() + .defaultTextStyle(18) + .copyWith(fontWeight: FontWeight.w600, color: primaryColor), + children: [ + TextSpan( + text: eula.text, + style: const TextStyle(fontSize: 16, color: primaryColor)) + ])); + }, + error: (error, stackTrace) => Text( + 'EULA INFORMATION\n\nCannot fetch EULA at this time. Please try again later.', + style: DefaultTheme().defaultTextStyle(18)), + loading: () => Container())))), + SizedBox( + + ///XXX: + height: MediaQuery.of(context).size.height * 0.13, + width: MediaQuery.of(context).size.width, + child: Column(mainAxisSize: MainAxisSize.min, children: [ + PrimaryButton( + text: "I have read and accept the EULA", + onPressed: () async { + onEULAAccepted(true); + if (!isCancellable) context.goNamed(AppRoute.eulaTransition.name); + }, + backgroundColor: CustomColors().lightblueColor), + if (isCancellable) ActionTextButton(text: "Cancel", onPressed: () => context.pop(false)) + ])) + ])))))); +} diff --git a/lib/src/features/account/presentation/username_password_page.dart b/lib/src/features/account/presentation/username_password_page.dart new file mode 100644 index 00000000..bdee55bf --- /dev/null +++ b/lib/src/features/account/presentation/username_password_page.dart @@ -0,0 +1,233 @@ +import 'package:flutter_starter_base_app/src/common_widgets/custom_text_form_field.dart'; +import 'package:flutter_starter_base_app/src/constants/colors.dart'; +import 'package:flutter_starter_base_app/src/localization/generated/locale_keys.g.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; + +class UsernameandPasswordPage extends ConsumerStatefulWidget { + final GlobalKey? formKey; + final TextEditingController usernameController; + final TextEditingController passwordController; + final TextEditingController confirmPasswordController; + UsernameandPasswordPage({ + Key? key, + this.formKey, + required this.usernameController, + required this.passwordController, + required this.confirmPasswordController, + }) : super(key: key); + + @override + _UsernameandPasswordPageState createState() => _UsernameandPasswordPageState(); +} + +class _UsernameandPasswordPageState extends ConsumerState { + FocusNode _usernamefocusNode = FocusNode(); + FocusNode _passwordfocusNode = FocusNode(); + FocusNode _confirmfocusNode = FocusNode(); + ValueNotifier obsecurePassword = ValueNotifier(true); + ValueNotifier obsecureConfirmPassword = ValueNotifier(true); + + @override + void initState() { + super.initState(); + _usernamefocusNode = FocusNode(); + _passwordfocusNode = FocusNode(); + _confirmfocusNode = FocusNode(); + } + + String? email; + + @override + void dispose() { + _usernamefocusNode.dispose(); + _passwordfocusNode.dispose(); + _confirmfocusNode.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + body: SingleChildScrollView( + child: Form( + key: widget.formKey, + child: Column( + children: [ + Padding( + padding: const EdgeInsets.only(top: 15, bottom: 15), + child: Text( + "Create Your Username And Password", + style: TextStyle(fontSize: 17, fontWeight: FontWeight.w600, color: CustomColors().whitecolor), + ), + ), + textfields(context), + + passwordfield(), + // Add some spacing between fields + confirmpasswordfield(), + ], + ), + ), + ), + ); + } + + ValueListenableBuilder confirmpasswordfield() { + return ValueListenableBuilder( + valueListenable: obsecureConfirmPassword, + builder: (BuildContext context, value, Widget? child) { + return CustomTextFormField( + controller: widget.confirmPasswordController, + focusNode: _confirmfocusNode, + textInputType: TextInputType.text, + autovalidateMode: AutovalidateMode.onUserInteraction, + prefixIcon: Padding( + padding: const EdgeInsets.only(left: 15.0, right: 10), + child: Row( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + LocaleKeys.account_confPassword.tr(), + style: TextStyle(color: CustomColors().whitecolor, fontWeight: FontWeight.w400, fontSize: 17), + ), + SizedBox( + width: MediaQuery.of(context).size.width * 0.02, + ), + Icon( + Icons.help, + color: CustomColors().whitecolor, + ), + ], + ), + ), + hintText: LocaleKeys.hint_password.tr(), + validator: (value) { + if (value == null || value.isEmpty) { + return 'Please enter confirm password'; + } + if (value != widget.passwordController.text) { + return 'Confirm Password does not match with Password'; + } + return null; + }, + // suffixIcon: IconButton( + // icon: Icon( + // obsecureConfirmPassword.value ? Icons.visibility : Icons.visibility_off, + // color: const Color.fromRGBO(208, 188, 255, 1), + // size: 20, + // ), + // onPressed: () { + // obsecureConfirmPassword.value = !obsecureConfirmPassword.value; + // }, + // ), + + maxLines: 1, + onPressed: () { + obsecureConfirmPassword.value = !obsecureConfirmPassword.value; + }, + obscureText: obsecureConfirmPassword.value, + ); + }, + ); + } + + ValueListenableBuilder passwordfield() { + return ValueListenableBuilder( + valueListenable: obsecurePassword, + builder: (BuildContext context, value, Widget? child) { + return CustomTextFormField( + controller: widget.passwordController, + focusNode: _passwordfocusNode, + textInputType: TextInputType.text, + autovalidateMode: AutovalidateMode.onUserInteraction, + prefixIcon: Padding( + padding: const EdgeInsets.only(left: 15.0, right: 10), + child: Row( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + LocaleKeys.common_password.tr(), + style: TextStyle(color: CustomColors().whitecolor, fontWeight: FontWeight.w400, fontSize: 17), + ), + SizedBox( + width: MediaQuery.of(context).size.width * 0.02, + ), + Icon( + Icons.help, + color: CustomColors().whitecolor, + ), + ], + ), + ), + hintText: LocaleKeys.hint_password.tr(), + validator: (value) { + if (value!.isEmpty) { + return 'Please enter a password'; + } + // // Password validation rules + // if (!RegExp(r'^(?=.*?[A-Z])(?=.*?[0-9])(?=.*?[!@#\$&*~]).{8,}$').hasMatch(value)) { + // return 'at least 8 characters, including one uppercase letter, one number, and one special character'; + // } + return null; + }, + maxLines: 1, + onPressed: () { + obsecurePassword.value = !obsecurePassword.value; + }, + obscureText: obsecurePassword.value, + ); + }, + ); + } + + CustomTextFormField textfields(BuildContext context) { + return CustomTextFormField( + autovalidateMode: AutovalidateMode.onUserInteraction, + controller: widget.usernameController, + focusNode: _usernamefocusNode, + textInputType: TextInputType.emailAddress, + hintText: LocaleKeys.hint_emailOrUsername.tr(), + prefixIcon: Padding( + padding: const EdgeInsets.only(left: 15.0, right: 10), + child: Row( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + LocaleKeys.common_username.tr(), + style: TextStyle( + // color: Colors.white, + color: CustomColors().whitecolor, + fontWeight: FontWeight.w400, + fontSize: 17), + ), + SizedBox( + width: MediaQuery.of(context).size.width * 0.02, + ), + Icon( + Icons.help, + color: CustomColors().whitecolor, + ) + ], + ), + ), + validator: (value) { + if (value == null || value.isEmpty) { + return "Username can't be empty"; + } + // if (value.length < 3) { + // return "Username must be at least 3 characters"; + // } + // if (value.length > 15) { + // return "Username can't be more than 15 characters"; + // } + return null; + }, + + ); + } +} diff --git a/lib/src/features/authentication/data/firebase_auth_repository.dart b/lib/src/features/authentication/data/firebase_auth_repository.dart deleted file mode 100644 index f5367d9c..00000000 --- a/lib/src/features/authentication/data/firebase_auth_repository.dart +++ /dev/null @@ -1,31 +0,0 @@ -import 'package:firebase_auth/firebase_auth.dart'; -import 'package:riverpod_annotation/riverpod_annotation.dart'; - -part 'firebase_auth_repository.g.dart'; - -class AuthRepository { - AuthRepository(this._auth); - final FirebaseAuth _auth; - - Stream authStateChanges() => _auth.authStateChanges(); - User? get currentUser => _auth.currentUser; - - Future signInAnonymously() { - return _auth.signInAnonymously(); - } -} - -@Riverpod(keepAlive: true) -FirebaseAuth firebaseAuth(FirebaseAuthRef ref) { - return FirebaseAuth.instance; -} - -@Riverpod(keepAlive: true) -AuthRepository authRepository(AuthRepositoryRef ref) { - return AuthRepository(ref.watch(firebaseAuthProvider)); -} - -@riverpod -Stream authStateChanges(AuthStateChangesRef ref) { - return ref.watch(authRepositoryProvider).authStateChanges(); -} diff --git a/lib/src/features/authentication/data/firebase_auth_repository.g.dart b/lib/src/features/authentication/data/firebase_auth_repository.g.dart deleted file mode 100644 index f657494e..00000000 --- a/lib/src/features/authentication/data/firebase_auth_repository.g.dart +++ /dev/null @@ -1,54 +0,0 @@ -// GENERATED CODE - DO NOT MODIFY BY HAND - -part of 'firebase_auth_repository.dart'; - -// ************************************************************************** -// RiverpodGenerator -// ************************************************************************** - -String _$firebaseAuthHash() => r'46c40b7c5cf8ab936c0daa96a6af106bd2ae5d51'; - -/// See also [firebaseAuth]. -@ProviderFor(firebaseAuth) -final firebaseAuthProvider = Provider.internal( - firebaseAuth, - name: r'firebaseAuthProvider', - debugGetCreateSourceHash: - const bool.fromEnvironment('dart.vm.product') ? null : _$firebaseAuthHash, - dependencies: null, - allTransitiveDependencies: null, -); - -typedef FirebaseAuthRef = ProviderRef; -String _$authRepositoryHash() => r'3871275ded2762a0e529629be71e890bfd3bd7ad'; - -/// See also [authRepository]. -@ProviderFor(authRepository) -final authRepositoryProvider = Provider.internal( - authRepository, - name: r'authRepositoryProvider', - debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') - ? null - : _$authRepositoryHash, - dependencies: null, - allTransitiveDependencies: null, -); - -typedef AuthRepositoryRef = ProviderRef; -String _$authStateChangesHash() => r'af0a0185c59bf3c1ad8a9e041075517b3a2dcc31'; - -/// See also [authStateChanges]. -@ProviderFor(authStateChanges) -final authStateChangesProvider = AutoDisposeStreamProvider.internal( - authStateChanges, - name: r'authStateChangesProvider', - debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') - ? null - : _$authStateChangesHash, - dependencies: null, - allTransitiveDependencies: null, -); - -typedef AuthStateChangesRef = AutoDisposeStreamProviderRef; -// ignore_for_file: type=lint -// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member diff --git a/lib/src/features/authentication/domain/app_user.dart b/lib/src/features/authentication/domain/app_user.dart deleted file mode 100644 index db572792..00000000 --- a/lib/src/features/authentication/domain/app_user.dart +++ /dev/null @@ -1,25 +0,0 @@ -/// Type defining a user ID from Firebase. -typedef UserID = String; - -/// Simple class representing the user UID and email. -class AppUser { - const AppUser({ - required this.uid, - required this.email, - }); - final String uid; - final String email; - - @override - bool operator ==(Object other) { - if (identical(this, other)) return true; - - return other is AppUser && other.uid == uid && other.email == email; - } - - @override - int get hashCode => uid.hashCode ^ email.hashCode; - - @override - String toString() => 'AppUser(uid: $uid, email: $email)'; -} diff --git a/lib/src/features/authentication/presentation/auth_providers.dart b/lib/src/features/authentication/presentation/auth_providers.dart deleted file mode 100644 index 273b8723..00000000 --- a/lib/src/features/authentication/presentation/auth_providers.dart +++ /dev/null @@ -1,14 +0,0 @@ -import 'package:firebase_auth/firebase_auth.dart' - hide EmailAuthProvider, AuthProvider; -import 'package:firebase_ui_auth/firebase_ui_auth.dart'; -import 'package:riverpod_annotation/riverpod_annotation.dart'; - -part 'auth_providers.g.dart'; - -@Riverpod(keepAlive: true) -List> authProviders( - AuthProvidersRef ref) { - return [ - EmailAuthProvider(), - ]; -} diff --git a/lib/src/features/authentication/presentation/auth_providers.g.dart b/lib/src/features/authentication/presentation/auth_providers.g.dart deleted file mode 100644 index 6f304119..00000000 --- a/lib/src/features/authentication/presentation/auth_providers.g.dart +++ /dev/null @@ -1,27 +0,0 @@ -// GENERATED CODE - DO NOT MODIFY BY HAND - -part of 'auth_providers.dart'; - -// ************************************************************************** -// RiverpodGenerator -// ************************************************************************** - -String _$authProvidersHash() => r'ae6cefe8190c6d4e0c24eed661e7889031bfabda'; - -/// See also [authProviders]. -@ProviderFor(authProviders) -final authProvidersProvider = - Provider>>.internal( - authProviders, - name: r'authProvidersProvider', - debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') - ? null - : _$authProvidersHash, - dependencies: null, - allTransitiveDependencies: null, -); - -typedef AuthProvidersRef - = ProviderRef>>; -// ignore_for_file: type=lint -// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member diff --git a/lib/src/features/authentication/presentation/custom_profile_screen.dart b/lib/src/features/authentication/presentation/custom_profile_screen.dart deleted file mode 100644 index 9f0d6b0f..00000000 --- a/lib/src/features/authentication/presentation/custom_profile_screen.dart +++ /dev/null @@ -1,19 +0,0 @@ -import 'package:firebase_ui_auth/firebase_ui_auth.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:starter_architecture_flutter_firebase/src/features/authentication/presentation/auth_providers.dart'; - -class CustomProfileScreen extends ConsumerWidget { - const CustomProfileScreen({super.key}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final authProviders = ref.watch(authProvidersProvider); - return ProfileScreen( - appBar: AppBar( - title: const Text('Profile'), - ), - providers: authProviders, - ); - } -} diff --git a/lib/src/features/authentication/presentation/custom_sign_in_screen.dart b/lib/src/features/authentication/presentation/custom_sign_in_screen.dart deleted file mode 100644 index 27642971..00000000 --- a/lib/src/features/authentication/presentation/custom_sign_in_screen.dart +++ /dev/null @@ -1,52 +0,0 @@ -import 'package:firebase_ui_auth/firebase_ui_auth.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:starter_architecture_flutter_firebase/src/constants/app_sizes.dart'; -import 'package:starter_architecture_flutter_firebase/src/features/authentication/data/firebase_auth_repository.dart'; - -import 'auth_providers.dart'; - -class CustomSignInScreen extends ConsumerWidget { - const CustomSignInScreen({super.key}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final authProviders = ref.watch(authProvidersProvider); - return Scaffold( - appBar: AppBar( - title: const Text('Sign in'), - ), - body: SignInScreen( - providers: authProviders, - footerBuilder: (context, action) => const SignInAnonymouslyFooter(), - ), - ); - } -} - -class SignInAnonymouslyFooter extends ConsumerWidget { - const SignInAnonymouslyFooter({super.key}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - return Column( - children: [ - gapH8, - const Row( - children: [ - Expanded(child: Divider()), - Padding( - padding: EdgeInsets.symmetric(horizontal: Sizes.p8), - child: Text('or'), - ), - Expanded(child: Divider()), - ], - ), - TextButton( - onPressed: () => ref.read(firebaseAuthProvider).signInAnonymously(), - child: const Text('Sign in anonymously'), - ), - ], - ); - } -} diff --git a/lib/src/features/entries/application/entries_service.dart b/lib/src/features/entries/application/entries_service.dart deleted file mode 100644 index 65d5824a..00000000 --- a/lib/src/features/entries/application/entries_service.dart +++ /dev/null @@ -1,100 +0,0 @@ -import 'package:riverpod_annotation/riverpod_annotation.dart'; -import 'package:rxdart/rxdart.dart'; -import 'package:starter_architecture_flutter_firebase/src/features/authentication/data/firebase_auth_repository.dart'; -import 'package:starter_architecture_flutter_firebase/src/features/authentication/domain/app_user.dart'; -import 'package:starter_architecture_flutter_firebase/src/features/entries/data/entries_repository.dart'; -import 'package:starter_architecture_flutter_firebase/src/features/entries/domain/daily_jobs_details.dart'; -import 'package:starter_architecture_flutter_firebase/src/features/entries/domain/entries_list_tile_model.dart'; -import 'package:starter_architecture_flutter_firebase/src/features/entries/domain/entry_job.dart'; -import 'package:starter_architecture_flutter_firebase/src/features/jobs/data/jobs_repository.dart'; -import 'package:starter_architecture_flutter_firebase/src/utils/format.dart'; -import 'package:starter_architecture_flutter_firebase/src/features/entries/domain/entry.dart'; -import 'package:starter_architecture_flutter_firebase/src/features/jobs/domain/job.dart'; - -part 'entries_service.g.dart'; - -// TODO: Clean up this code a bit more -class EntriesService { - EntriesService( - {required this.jobsRepository, required this.entriesRepository}); - final JobsRepository jobsRepository; - final EntriesRepository entriesRepository; - - /// combine List, List into List - Stream> _allEntriesStream(UserID uid) => - CombineLatestStream.combine2( - entriesRepository.watchEntries(uid: uid), - jobsRepository.watchJobs(uid: uid), - _entriesJobsCombiner, - ); - - static List _entriesJobsCombiner( - List entries, List jobs) { - return entries.map((entry) { - final job = jobs.firstWhere((job) => job.id == entry.jobId); - return EntryJob(entry, job); - }).toList(); - } - - /// Output stream - Stream> entriesTileModelStream(UserID uid) => - _allEntriesStream(uid).map(_createModels); - - static List _createModels(List allEntries) { - if (allEntries.isEmpty) { - return []; - } - final allDailyJobsDetails = DailyJobsDetails.all(allEntries); - - // total duration across all jobs - final totalDuration = allDailyJobsDetails - .map((dateJobsDuration) => dateJobsDuration.duration) - .reduce((value, element) => value + element); - - // total pay across all jobs - final totalPay = allDailyJobsDetails - .map((dateJobsDuration) => dateJobsDuration.pay) - .reduce((value, element) => value + element); - - return [ - EntriesListTileModel( - leadingText: 'All Entries', - middleText: Format.currency(totalPay), - trailingText: Format.hours(totalDuration), - ), - for (DailyJobsDetails dailyJobsDetails in allDailyJobsDetails) ...[ - EntriesListTileModel( - isHeader: true, - leadingText: Format.date(dailyJobsDetails.date), - middleText: Format.currency(dailyJobsDetails.pay), - trailingText: Format.hours(dailyJobsDetails.duration), - ), - for (JobDetails jobDuration in dailyJobsDetails.jobsDetails) - EntriesListTileModel( - leadingText: jobDuration.name, - middleText: Format.currency(jobDuration.pay), - trailingText: Format.hours(jobDuration.durationInHours), - ), - ] - ]; - } -} - -@riverpod -EntriesService entriesService(EntriesServiceRef ref) { - return EntriesService( - jobsRepository: ref.watch(jobsRepositoryProvider), - entriesRepository: ref.watch(entriesRepositoryProvider), - ); -} - -@riverpod -Stream> entriesTileModelStream( - EntriesTileModelStreamRef ref) { - final user = ref.watch(firebaseAuthProvider).currentUser; - if (user == null) { - throw AssertionError('User can\'t be null when fetching entries'); - } - final entriesService = ref.watch(entriesServiceProvider); - return entriesService.entriesTileModelStream(user.uid); -} diff --git a/lib/src/features/entries/application/entries_service.g.dart b/lib/src/features/entries/application/entries_service.g.dart deleted file mode 100644 index bf120c06..00000000 --- a/lib/src/features/entries/application/entries_service.g.dart +++ /dev/null @@ -1,43 +0,0 @@ -// GENERATED CODE - DO NOT MODIFY BY HAND - -part of 'entries_service.dart'; - -// ************************************************************************** -// RiverpodGenerator -// ************************************************************************** - -String _$entriesServiceHash() => r'ad6f017678723501d64b7d33ea05ce3553cc010b'; - -/// See also [entriesService]. -@ProviderFor(entriesService) -final entriesServiceProvider = AutoDisposeProvider.internal( - entriesService, - name: r'entriesServiceProvider', - debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') - ? null - : _$entriesServiceHash, - dependencies: null, - allTransitiveDependencies: null, -); - -typedef EntriesServiceRef = AutoDisposeProviderRef; -String _$entriesTileModelStreamHash() => - r'69af265d2969a10a62e0b9e7b679ce336445b91c'; - -/// See also [entriesTileModelStream]. -@ProviderFor(entriesTileModelStream) -final entriesTileModelStreamProvider = - AutoDisposeStreamProvider>.internal( - entriesTileModelStream, - name: r'entriesTileModelStreamProvider', - debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') - ? null - : _$entriesTileModelStreamHash, - dependencies: null, - allTransitiveDependencies: null, -); - -typedef EntriesTileModelStreamRef - = AutoDisposeStreamProviderRef>; -// ignore_for_file: type=lint -// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member diff --git a/lib/src/features/entries/data/entries_repository.dart b/lib/src/features/entries/data/entries_repository.dart deleted file mode 100644 index 3952cecc..00000000 --- a/lib/src/features/entries/data/entries_repository.dart +++ /dev/null @@ -1,74 +0,0 @@ -import 'package:cloud_firestore/cloud_firestore.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:starter_architecture_flutter_firebase/src/features/authentication/data/firebase_auth_repository.dart'; -import 'package:starter_architecture_flutter_firebase/src/features/authentication/domain/app_user.dart'; -import 'package:starter_architecture_flutter_firebase/src/features/entries/domain/entry.dart'; -import 'package:starter_architecture_flutter_firebase/src/features/jobs/domain/job.dart'; - -class EntriesRepository { - const EntriesRepository(this._firestore); - final FirebaseFirestore _firestore; - - static String entryPath(String uid, String entryId) => - 'users/$uid/entries/$entryId'; - static String entriesPath(String uid) => 'users/$uid/entries'; - - // create - Future addEntry({ - required UserID uid, - required JobID jobId, - required DateTime start, - required DateTime end, - required String comment, - }) => - _firestore.collection(entriesPath(uid)).add({ - 'jobId': jobId, - 'start': start.millisecondsSinceEpoch, - 'end': end.millisecondsSinceEpoch, - 'comment': comment, - }); - - // update - Future updateEntry({ - required UserID uid, - required Entry entry, - }) => - _firestore.doc(entryPath(uid, entry.id)).update(entry.toMap()); - - // delete - Future deleteEntry({required UserID uid, required EntryID entryId}) => - _firestore.doc(entryPath(uid, entryId)).delete(); - - // read - Stream> watchEntries({required UserID uid, JobID? jobId}) => - queryEntries(uid: uid, jobId: jobId) - .snapshots() - .map((snapshot) => snapshot.docs.map((doc) => doc.data()).toList()); - - Query queryEntries({required UserID uid, JobID? jobId}) { - Query query = - _firestore.collection(entriesPath(uid)).withConverter( - fromFirestore: (snapshot, _) => - Entry.fromMap(snapshot.data()!, snapshot.id), - toFirestore: (entry, _) => entry.toMap(), - ); - if (jobId != null) { - query = query.where('jobId', isEqualTo: jobId); - } - return query; - } -} - -final entriesRepositoryProvider = Provider((ref) { - return EntriesRepository(FirebaseFirestore.instance); -}); - -final jobEntriesQueryProvider = - Provider.autoDispose.family, JobID>((ref, jobId) { - final user = ref.watch(firebaseAuthProvider).currentUser; - if (user == null) { - throw AssertionError('User can\'t be null when fetching jobs'); - } - final repository = ref.watch(entriesRepositoryProvider); - return repository.queryEntries(uid: user.uid, jobId: jobId); -}); diff --git a/lib/src/features/entries/domain/daily_jobs_details.dart b/lib/src/features/entries/domain/daily_jobs_details.dart deleted file mode 100644 index 3cfaa4d5..00000000 --- a/lib/src/features/entries/domain/daily_jobs_details.dart +++ /dev/null @@ -1,76 +0,0 @@ -import 'package:starter_architecture_flutter_firebase/src/features/entries/domain/entry_job.dart'; - -/// Temporary model class to store the time tracked and pay for a job -class JobDetails { - JobDetails({ - required this.name, - required this.durationInHours, - required this.pay, - }); - final String name; - double durationInHours; - double pay; -} - -/// Groups together all jobs/entries on a given day -class DailyJobsDetails { - DailyJobsDetails({required this.date, required this.jobsDetails}); - final DateTime date; - final List jobsDetails; - - double get pay => jobsDetails - .map((jobDuration) => jobDuration.pay) - .reduce((value, element) => value + element); - - double get duration => jobsDetails - .map((jobDuration) => jobDuration.durationInHours) - .reduce((value, element) => value + element); - - /// splits all entries into separate groups by date - static Map> _entriesByDate(List entries) { - final Map> map = {}; - for (final entryJob in entries) { - final entryDayStart = DateTime(entryJob.entry.start.year, - entryJob.entry.start.month, entryJob.entry.start.day); - if (map[entryDayStart] == null) { - map[entryDayStart] = [entryJob]; - } else { - map[entryDayStart]!.add(entryJob); - } - } - return map; - } - - /// maps an unordered list of EntryJob into a list of DailyJobsDetails with date information - static List all(List entries) { - final byDate = _entriesByDate(entries); - final List list = []; - for (final pair in byDate.entries) { - final date = pair.key; - final entriesByDate = pair.value; - final byJob = _jobsDetails(entriesByDate); - list.add(DailyJobsDetails(date: date, jobsDetails: byJob)); - } - return list.toList(); - } - - /// groups entries by job - static List _jobsDetails(List entries) { - final Map jobDuration = {}; - for (final entryJob in entries) { - final entry = entryJob.entry; - final pay = entry.durationInHours * entryJob.job.ratePerHour; - if (jobDuration[entry.jobId] == null) { - jobDuration[entry.jobId] = JobDetails( - name: entryJob.job.name, - durationInHours: entry.durationInHours, - pay: pay, - ); - } else { - jobDuration[entry.jobId]!.pay += pay; - jobDuration[entry.jobId]!.durationInHours += entry.durationInHours; - } - } - return jobDuration.values.toList(); - } -} diff --git a/lib/src/features/entries/domain/entries_list_tile_model.dart b/lib/src/features/entries/domain/entries_list_tile_model.dart deleted file mode 100644 index 179afa27..00000000 --- a/lib/src/features/entries/domain/entries_list_tile_model.dart +++ /dev/null @@ -1,12 +0,0 @@ -class EntriesListTileModel { - const EntriesListTileModel({ - required this.leadingText, - required this.trailingText, - this.middleText, - this.isHeader = false, - }); - final String leadingText; - final String trailingText; - final String? middleText; - final bool isHeader; -} diff --git a/lib/src/features/entries/domain/entry.dart b/lib/src/features/entries/domain/entry.dart deleted file mode 100644 index 8bc3ee60..00000000 --- a/lib/src/features/entries/domain/entry.dart +++ /dev/null @@ -1,49 +0,0 @@ -import 'package:equatable/equatable.dart'; -import 'package:starter_architecture_flutter_firebase/src/features/jobs/domain/job.dart'; - -typedef EntryID = String; - -class Entry extends Equatable { - const Entry({ - required this.id, - required this.jobId, - required this.start, - required this.end, - required this.comment, - }); - final EntryID id; - final JobID jobId; - final DateTime start; - final DateTime end; - final String comment; - - @override - List get props => [id, jobId, start, end, comment]; - - @override - bool get stringify => true; - - double get durationInHours => - end.difference(start).inMinutes.toDouble() / 60.0; - - factory Entry.fromMap(Map value, EntryID id) { - final startMilliseconds = value['start'] as int; - final endMilliseconds = value['end'] as int; - return Entry( - id: id, - jobId: value['jobId'] as String, - start: DateTime.fromMillisecondsSinceEpoch(startMilliseconds), - end: DateTime.fromMillisecondsSinceEpoch(endMilliseconds), - comment: value['comment'] as String? ?? '', - ); - } - - Map toMap() { - return { - 'jobId': jobId, - 'start': start.millisecondsSinceEpoch, - 'end': end.millisecondsSinceEpoch, - 'comment': comment, - }; - } -} diff --git a/lib/src/features/entries/domain/entry_job.dart b/lib/src/features/entries/domain/entry_job.dart deleted file mode 100644 index 37a6fa0c..00000000 --- a/lib/src/features/entries/domain/entry_job.dart +++ /dev/null @@ -1,16 +0,0 @@ -import 'package:equatable/equatable.dart'; -import 'package:starter_architecture_flutter_firebase/src/features/entries/domain/entry.dart'; -import 'package:starter_architecture_flutter_firebase/src/features/jobs/domain/job.dart'; - -class EntryJob extends Equatable { - const EntryJob(this.entry, this.job); - - final Entry entry; - final Job job; - - @override - List get props => [entry, job]; - - @override - bool? get stringify => true; -} diff --git a/lib/src/features/entries/presentation/entries_screen.dart b/lib/src/features/entries/presentation/entries_screen.dart deleted file mode 100644 index d2fd023e..00000000 --- a/lib/src/features/entries/presentation/entries_screen.dart +++ /dev/null @@ -1,70 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:starter_architecture_flutter_firebase/src/constants/app_sizes.dart'; -import 'package:starter_architecture_flutter_firebase/src/constants/strings.dart'; -import 'package:starter_architecture_flutter_firebase/src/features/entries/domain/entries_list_tile_model.dart'; -import 'package:starter_architecture_flutter_firebase/src/features/entries/application/entries_service.dart'; -import 'package:starter_architecture_flutter_firebase/src/common_widgets/list_items_builder.dart'; - -class EntriesScreen extends ConsumerWidget { - const EntriesScreen({super.key}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - return Scaffold( - appBar: AppBar( - title: const Text(Strings.entries), - ), - body: Consumer( - builder: (context, ref, child) { - // * This data is combined from two streams, so it can't be returned - // * directly as a Query object from the repository. - // * As a result, we can't use FirestoreListView here. - final entriesTileModelStream = - ref.watch(entriesTileModelStreamProvider); - return ListItemsBuilder( - data: entriesTileModelStream, - itemBuilder: (context, model) => EntriesListTile(model: model), - ); - }, - ), - ); - } -} - -class EntriesListTile extends StatelessWidget { - const EntriesListTile({super.key, required this.model}); - final EntriesListTileModel model; - - @override - Widget build(BuildContext context) { - const fontSize = 16.0; - return Container( - color: model.isHeader ? Colors.indigo[100] : null, - padding: const EdgeInsets.symmetric( - vertical: Sizes.p8, - horizontal: Sizes.p16, - ), - child: Row( - children: [ - Text(model.leadingText, style: const TextStyle(fontSize: fontSize)), - Expanded(child: Container()), - if (model.middleText != null) - Text( - model.middleText!, - style: TextStyle(color: Colors.green[700], fontSize: fontSize), - textAlign: TextAlign.right, - ), - SizedBox( - width: 60.0, - child: Text( - model.trailingText, - style: const TextStyle(fontSize: fontSize), - textAlign: TextAlign.right, - ), - ), - ], - ), - ); - } -} diff --git a/lib/src/features/entries/presentation/entry_screen/entry_screen.dart b/lib/src/features/entries/presentation/entry_screen/entry_screen.dart deleted file mode 100644 index 0c98b5c7..00000000 --- a/lib/src/features/entries/presentation/entry_screen/entry_screen.dart +++ /dev/null @@ -1,157 +0,0 @@ -import 'dart:async'; - -import 'package:flutter/material.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:go_router/go_router.dart'; -import 'package:starter_architecture_flutter_firebase/src/common_widgets/date_time_picker.dart'; -import 'package:starter_architecture_flutter_firebase/src/common_widgets/responsive_center.dart'; -import 'package:starter_architecture_flutter_firebase/src/constants/app_sizes.dart'; -import 'package:starter_architecture_flutter_firebase/src/constants/breakpoints.dart'; -import 'package:starter_architecture_flutter_firebase/src/features/entries/domain/entry.dart'; -import 'package:starter_architecture_flutter_firebase/src/features/jobs/domain/job.dart'; -import 'package:starter_architecture_flutter_firebase/src/features/entries/presentation/entry_screen/entry_screen_controller.dart'; -import 'package:starter_architecture_flutter_firebase/src/utils/async_value_ui.dart'; -import 'package:starter_architecture_flutter_firebase/src/utils/format.dart'; - -class EntryScreen extends ConsumerStatefulWidget { - const EntryScreen({super.key, required this.jobId, this.entryId, this.entry}); - final JobID jobId; - final EntryID? entryId; - final Entry? entry; - - @override - ConsumerState createState() => _EntryPageState(); -} - -class _EntryPageState extends ConsumerState { - late DateTime _startDate; - late TimeOfDay _startTime; - late DateTime _endDate; - late TimeOfDay _endTime; - late String _comment; - - DateTime get start => DateTime(_startDate.year, _startDate.month, - _startDate.day, _startTime.hour, _startTime.minute); - DateTime get end => DateTime(_endDate.year, _endDate.month, _endDate.day, - _endTime.hour, _endTime.minute); - - @override - void initState() { - super.initState(); - final start = widget.entry?.start ?? DateTime.now(); - _startDate = DateTime(start.year, start.month, start.day); - _startTime = TimeOfDay.fromDateTime(start); - - final end = widget.entry?.end ?? DateTime.now(); - _endDate = DateTime(end.year, end.month, end.day); - _endTime = TimeOfDay.fromDateTime(end); - - _comment = widget.entry?.comment ?? ''; - } - - Future _setEntryAndDismiss() async { - final success = - await ref.read(entryScreenControllerProvider.notifier).submit( - entryId: widget.entryId, - jobId: widget.jobId, - start: start, - end: end, - comment: _comment, - ); - if (success && mounted) { - context.pop(); - } - } - - @override - Widget build(BuildContext context) { - ref.listen( - entryScreenControllerProvider, - (_, state) => state.showAlertDialogOnError(context), - ); - return Scaffold( - appBar: AppBar( - title: Text(widget.entry != null ? 'Edit Entry' : 'New Entry'), - actions: [ - TextButton( - child: Text( - widget.entry != null ? 'Update' : 'Create', - style: const TextStyle(fontSize: 18.0, color: Colors.white), - ), - onPressed: () => _setEntryAndDismiss(), - ), - ], - ), - body: SingleChildScrollView( - child: ResponsiveCenter( - maxContentWidth: Breakpoint.tablet, - padding: const EdgeInsets.all(Sizes.p16), - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - _buildStartDate(), - _buildEndDate(), - gapH8, - _buildDuration(), - gapH8, - _buildComment(), - ], - ), - ), - ), - ); - } - - Widget _buildStartDate() { - return DateTimePicker( - labelText: 'Start', - selectedDate: _startDate, - selectedTime: _startTime, - onSelectedDate: (date) => setState(() => _startDate = date), - onSelectedTime: (time) => setState(() => _startTime = time), - ); - } - - Widget _buildEndDate() { - return DateTimePicker( - labelText: 'End', - selectedDate: _endDate, - selectedTime: _endTime, - onSelectedDate: (date) => setState(() => _endDate = date), - onSelectedTime: (time) => setState(() => _endTime = time), - ); - } - - Widget _buildDuration() { - final durationInHours = end.difference(start).inMinutes.toDouble() / 60.0; - final durationFormatted = Format.hours(durationInHours); - return Row( - mainAxisAlignment: MainAxisAlignment.end, - children: [ - Text( - 'Duration: $durationFormatted', - style: const TextStyle(fontSize: 18.0, fontWeight: FontWeight.w500), - maxLines: 1, - overflow: TextOverflow.ellipsis, - ), - ], - ); - } - - Widget _buildComment() { - return TextField( - keyboardType: TextInputType.text, - maxLength: 50, - controller: TextEditingController(text: _comment), - decoration: const InputDecoration( - labelText: 'Comment', - labelStyle: TextStyle(fontSize: 18.0, fontWeight: FontWeight.w500), - ), - keyboardAppearance: Brightness.light, - style: const TextStyle(fontSize: 20.0, color: Colors.black), - maxLines: null, - onChanged: (comment) => _comment = comment, - ); - } -} diff --git a/lib/src/features/entries/presentation/entry_screen/entry_screen_controller.dart b/lib/src/features/entries/presentation/entry_screen/entry_screen_controller.dart deleted file mode 100644 index 4bf6ffaf..00000000 --- a/lib/src/features/entries/presentation/entry_screen/entry_screen_controller.dart +++ /dev/null @@ -1,52 +0,0 @@ -import 'dart:async'; - -import 'package:riverpod_annotation/riverpod_annotation.dart'; -import 'package:starter_architecture_flutter_firebase/src/features/authentication/data/firebase_auth_repository.dart'; -import 'package:starter_architecture_flutter_firebase/src/features/entries/data/entries_repository.dart'; -import 'package:starter_architecture_flutter_firebase/src/features/entries/domain/entry.dart'; -import 'package:starter_architecture_flutter_firebase/src/features/jobs/domain/job.dart'; - -part 'entry_screen_controller.g.dart'; - -@riverpod -class EntryScreenController extends _$EntryScreenController { - @override - FutureOr build() { - // ok to leave this empty if the return type is FutureOr - } - - Future submit({ - EntryID? entryId, - required JobID jobId, - required DateTime start, - required DateTime end, - required String comment, - }) async { - final currentUser = ref.read(authRepositoryProvider).currentUser; - if (currentUser == null) { - throw AssertionError('User can\'t be null'); - } - final repository = ref.read(entriesRepositoryProvider); - state = const AsyncLoading(); - if (entryId == null) { - state = await AsyncValue.guard(() => repository.addEntry( - uid: currentUser.uid, - jobId: jobId, - start: start, - end: end, - comment: comment, - )); - } else { - final entry = Entry( - id: entryId, - jobId: jobId, - start: start, - end: end, - comment: comment, - ); - state = await AsyncValue.guard( - () => repository.updateEntry(uid: currentUser.uid, entry: entry)); - } - return state.hasError == false; - } -} diff --git a/lib/src/features/entries/presentation/entry_screen/entry_screen_controller.g.dart b/lib/src/features/entries/presentation/entry_screen/entry_screen_controller.g.dart deleted file mode 100644 index 4eea11b5..00000000 --- a/lib/src/features/entries/presentation/entry_screen/entry_screen_controller.g.dart +++ /dev/null @@ -1,27 +0,0 @@ -// GENERATED CODE - DO NOT MODIFY BY HAND - -part of 'entry_screen_controller.dart'; - -// ************************************************************************** -// RiverpodGenerator -// ************************************************************************** - -String _$entryScreenControllerHash() => - r'75638e7eac6bacd498349a143fc5fc827171674a'; - -/// See also [EntryScreenController]. -@ProviderFor(EntryScreenController) -final entryScreenControllerProvider = - AutoDisposeAsyncNotifierProvider.internal( - EntryScreenController.new, - name: r'entryScreenControllerProvider', - debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') - ? null - : _$entryScreenControllerHash, - dependencies: null, - allTransitiveDependencies: null, -); - -typedef _$EntryScreenController = AutoDisposeAsyncNotifier; -// ignore_for_file: type=lint -// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member diff --git a/lib/src/features/jobs/data/jobs_repository.dart b/lib/src/features/jobs/data/jobs_repository.dart deleted file mode 100644 index 1347896b..00000000 --- a/lib/src/features/jobs/data/jobs_repository.dart +++ /dev/null @@ -1,103 +0,0 @@ -import 'dart:async'; - -import 'package:cloud_firestore/cloud_firestore.dart'; -import 'package:riverpod_annotation/riverpod_annotation.dart'; -import 'package:starter_architecture_flutter_firebase/src/features/authentication/data/firebase_auth_repository.dart'; -import 'package:starter_architecture_flutter_firebase/src/features/authentication/domain/app_user.dart'; -import 'package:starter_architecture_flutter_firebase/src/features/entries/data/entries_repository.dart'; -import 'package:starter_architecture_flutter_firebase/src/features/entries/domain/entry.dart'; -import 'package:starter_architecture_flutter_firebase/src/features/jobs/domain/job.dart'; - -part 'jobs_repository.g.dart'; - -class JobsRepository { - const JobsRepository(this._firestore); - final FirebaseFirestore _firestore; - - static String jobPath(String uid, String jobId) => 'users/$uid/jobs/$jobId'; - static String jobsPath(String uid) => 'users/$uid/jobs'; - static String entriesPath(String uid) => EntriesRepository.entriesPath(uid); - - // create - Future addJob( - {required UserID uid, - required String name, - required int ratePerHour}) => - _firestore.collection(jobsPath(uid)).add({ - 'name': name, - 'ratePerHour': ratePerHour, - }); - - // update - Future updateJob({required UserID uid, required Job job}) => - _firestore.doc(jobPath(uid, job.id)).update(job.toMap()); - - // delete - Future deleteJob({required UserID uid, required JobID jobId}) async { - // delete where entry.jobId == job.jobId - final entriesRef = _firestore.collection(entriesPath(uid)); - final entries = await entriesRef.get(); - for (final snapshot in entries.docs) { - final entry = Entry.fromMap(snapshot.data(), snapshot.id); - if (entry.jobId == jobId) { - await snapshot.reference.delete(); - } - } - // delete job - final jobRef = _firestore.doc(jobPath(uid, jobId)); - await jobRef.delete(); - } - - // read - Stream watchJob({required UserID uid, required JobID jobId}) => - _firestore - .doc(jobPath(uid, jobId)) - .withConverter( - fromFirestore: (snapshot, _) => - Job.fromMap(snapshot.data()!, snapshot.id), - toFirestore: (job, _) => job.toMap(), - ) - .snapshots() - .map((snapshot) => snapshot.data()!); - - Stream> watchJobs({required UserID uid}) => queryJobs(uid: uid) - .snapshots() - .map((snapshot) => snapshot.docs.map((doc) => doc.data()).toList()); - - Query queryJobs({required UserID uid}) => - _firestore.collection(jobsPath(uid)).withConverter( - fromFirestore: (snapshot, _) => - Job.fromMap(snapshot.data()!, snapshot.id), - toFirestore: (job, _) => job.toMap(), - ); - - Future> fetchJobs({required UserID uid}) async { - final jobs = await queryJobs(uid: uid).get(); - return jobs.docs.map((doc) => doc.data()).toList(); - } -} - -@Riverpod(keepAlive: true) -JobsRepository jobsRepository(JobsRepositoryRef ref) { - return JobsRepository(FirebaseFirestore.instance); -} - -@riverpod -Query jobsQuery(JobsQueryRef ref) { - final user = ref.watch(firebaseAuthProvider).currentUser; - if (user == null) { - throw AssertionError('User can\'t be null'); - } - final repository = ref.watch(jobsRepositoryProvider); - return repository.queryJobs(uid: user.uid); -} - -@riverpod -Stream jobStream(JobStreamRef ref, JobID jobId) { - final user = ref.watch(firebaseAuthProvider).currentUser; - if (user == null) { - throw AssertionError('User can\'t be null'); - } - final repository = ref.watch(jobsRepositoryProvider); - return repository.watchJob(uid: user.uid, jobId: jobId); -} diff --git a/lib/src/features/jobs/data/jobs_repository.g.dart b/lib/src/features/jobs/data/jobs_repository.g.dart deleted file mode 100644 index c1bfb42b..00000000 --- a/lib/src/features/jobs/data/jobs_repository.g.dart +++ /dev/null @@ -1,186 +0,0 @@ -// GENERATED CODE - DO NOT MODIFY BY HAND - -part of 'jobs_repository.dart'; - -// ************************************************************************** -// RiverpodGenerator -// ************************************************************************** - -String _$jobsRepositoryHash() => r'99834710b25b2229bf6bd85bb1e522bfb2b61d5b'; - -/// See also [jobsRepository]. -@ProviderFor(jobsRepository) -final jobsRepositoryProvider = Provider.internal( - jobsRepository, - name: r'jobsRepositoryProvider', - debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') - ? null - : _$jobsRepositoryHash, - dependencies: null, - allTransitiveDependencies: null, -); - -typedef JobsRepositoryRef = ProviderRef; -String _$jobsQueryHash() => r'46482866aecb8be7e41fd6bdb0e2d5a6a87fc350'; - -/// See also [jobsQuery]. -@ProviderFor(jobsQuery) -final jobsQueryProvider = AutoDisposeProvider>.internal( - jobsQuery, - name: r'jobsQueryProvider', - debugGetCreateSourceHash: - const bool.fromEnvironment('dart.vm.product') ? null : _$jobsQueryHash, - dependencies: null, - allTransitiveDependencies: null, -); - -typedef JobsQueryRef = AutoDisposeProviderRef>; -String _$jobStreamHash() => r'72fc86cf080cd4a6bdb2da9f13ff81efb312521e'; - -/// Copied from Dart SDK -class _SystemHash { - _SystemHash._(); - - static int combine(int hash, int value) { - // ignore: parameter_assignments - hash = 0x1fffffff & (hash + value); - // ignore: parameter_assignments - hash = 0x1fffffff & (hash + ((0x0007ffff & hash) << 10)); - return hash ^ (hash >> 6); - } - - static int finish(int hash) { - // ignore: parameter_assignments - hash = 0x1fffffff & (hash + ((0x03ffffff & hash) << 3)); - // ignore: parameter_assignments - hash = hash ^ (hash >> 11); - return 0x1fffffff & (hash + ((0x00003fff & hash) << 15)); - } -} - -/// See also [jobStream]. -@ProviderFor(jobStream) -const jobStreamProvider = JobStreamFamily(); - -/// See also [jobStream]. -class JobStreamFamily extends Family> { - /// See also [jobStream]. - const JobStreamFamily(); - - /// See also [jobStream]. - JobStreamProvider call( - String jobId, - ) { - return JobStreamProvider( - jobId, - ); - } - - @override - JobStreamProvider getProviderOverride( - covariant JobStreamProvider provider, - ) { - return call( - provider.jobId, - ); - } - - static const Iterable? _dependencies = null; - - @override - Iterable? get dependencies => _dependencies; - - static const Iterable? _allTransitiveDependencies = null; - - @override - Iterable? get allTransitiveDependencies => - _allTransitiveDependencies; - - @override - String? get name => r'jobStreamProvider'; -} - -/// See also [jobStream]. -class JobStreamProvider extends AutoDisposeStreamProvider { - /// See also [jobStream]. - JobStreamProvider( - String jobId, - ) : this._internal( - (ref) => jobStream( - ref as JobStreamRef, - jobId, - ), - from: jobStreamProvider, - name: r'jobStreamProvider', - debugGetCreateSourceHash: - const bool.fromEnvironment('dart.vm.product') - ? null - : _$jobStreamHash, - dependencies: JobStreamFamily._dependencies, - allTransitiveDependencies: JobStreamFamily._allTransitiveDependencies, - jobId: jobId, - ); - - JobStreamProvider._internal( - super._createNotifier, { - required super.name, - required super.dependencies, - required super.allTransitiveDependencies, - required super.debugGetCreateSourceHash, - required super.from, - required this.jobId, - }) : super.internal(); - - final String jobId; - - @override - Override overrideWith( - Stream Function(JobStreamRef provider) create, - ) { - return ProviderOverride( - origin: this, - override: JobStreamProvider._internal( - (ref) => create(ref as JobStreamRef), - from: from, - name: null, - dependencies: null, - allTransitiveDependencies: null, - debugGetCreateSourceHash: null, - jobId: jobId, - ), - ); - } - - @override - AutoDisposeStreamProviderElement createElement() { - return _JobStreamProviderElement(this); - } - - @override - bool operator ==(Object other) { - return other is JobStreamProvider && other.jobId == jobId; - } - - @override - int get hashCode { - var hash = _SystemHash.combine(0, runtimeType.hashCode); - hash = _SystemHash.combine(hash, jobId.hashCode); - - return _SystemHash.finish(hash); - } -} - -mixin JobStreamRef on AutoDisposeStreamProviderRef { - /// The parameter `jobId` of this provider. - String get jobId; -} - -class _JobStreamProviderElement extends AutoDisposeStreamProviderElement - with JobStreamRef { - _JobStreamProviderElement(super.provider); - - @override - String get jobId => (origin as JobStreamProvider).jobId; -} -// ignore_for_file: type=lint -// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member diff --git a/lib/src/features/jobs/domain/job.dart b/lib/src/features/jobs/domain/job.dart deleted file mode 100644 index 2aa3106b..00000000 --- a/lib/src/features/jobs/domain/job.dart +++ /dev/null @@ -1,35 +0,0 @@ -import 'package:equatable/equatable.dart'; -import 'package:flutter/foundation.dart'; - -typedef JobID = String; - -@immutable -class Job extends Equatable { - const Job({required this.id, required this.name, required this.ratePerHour}); - final JobID id; - final String name; - final int ratePerHour; - - @override - List get props => [name, ratePerHour]; - - @override - bool get stringify => true; - - factory Job.fromMap(Map data, String id) { - final name = data['name'] as String; - final ratePerHour = data['ratePerHour'] as int; - return Job( - id: id, - name: name, - ratePerHour: ratePerHour, - ); - } - - Map toMap() { - return { - 'name': name, - 'ratePerHour': ratePerHour, - }; - } -} diff --git a/lib/src/features/jobs/presentation/edit_job_screen/edit_job_screen.dart b/lib/src/features/jobs/presentation/edit_job_screen/edit_job_screen.dart deleted file mode 100644 index ccfaebee..00000000 --- a/lib/src/features/jobs/presentation/edit_job_screen/edit_job_screen.dart +++ /dev/null @@ -1,131 +0,0 @@ -import 'dart:async'; - -import 'package:flutter/material.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:go_router/go_router.dart'; -import 'package:starter_architecture_flutter_firebase/src/common_widgets/responsive_center.dart'; -import 'package:starter_architecture_flutter_firebase/src/constants/breakpoints.dart'; -import 'package:starter_architecture_flutter_firebase/src/features/jobs/domain/job.dart'; -import 'package:starter_architecture_flutter_firebase/src/features/jobs/presentation/edit_job_screen/edit_job_screen_controller.dart'; -import 'package:starter_architecture_flutter_firebase/src/utils/async_value_ui.dart'; - -class EditJobScreen extends ConsumerStatefulWidget { - const EditJobScreen({super.key, this.jobId, this.job}); - final JobID? jobId; - final Job? job; - - @override - ConsumerState createState() => _EditJobPageState(); -} - -class _EditJobPageState extends ConsumerState { - final _formKey = GlobalKey(); - - String? _name; - int? _ratePerHour; - - @override - void initState() { - super.initState(); - if (widget.job != null) { - _name = widget.job?.name; - _ratePerHour = widget.job?.ratePerHour; - } - } - - bool _validateAndSaveForm() { - final form = _formKey.currentState!; - if (form.validate()) { - form.save(); - return true; - } - return false; - } - - Future _submit() async { - if (_validateAndSaveForm()) { - final success = - await ref.read(editJobScreenControllerProvider.notifier).submit( - jobId: widget.jobId, - oldJob: widget.job, - name: _name ?? '', - ratePerHour: _ratePerHour ?? 0, - ); - if (success && mounted) { - context.pop(); - } - } - } - - @override - Widget build(BuildContext context) { - ref.listen( - editJobScreenControllerProvider, - (_, state) => state.showAlertDialogOnError(context), - ); - final state = ref.watch(editJobScreenControllerProvider); - return Scaffold( - appBar: AppBar( - title: Text(widget.job == null ? 'New Job' : 'Edit Job'), - actions: [ - TextButton( - onPressed: state.isLoading ? null : _submit, - child: const Text( - 'Save', - style: TextStyle(fontSize: 18, color: Colors.white), - ), - ), - ], - ), - body: _buildContents(), - ); - } - - Widget _buildContents() { - return SingleChildScrollView( - child: ResponsiveCenter( - maxContentWidth: Breakpoint.tablet, - padding: const EdgeInsets.all(16.0), - child: Card( - child: Padding( - padding: const EdgeInsets.all(16.0), - child: _buildForm(), - ), - ), - ), - ); - } - - Widget _buildForm() { - return Form( - key: _formKey, - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: _buildFormChildren(), - ), - ); - } - - List _buildFormChildren() { - return [ - TextFormField( - decoration: const InputDecoration(labelText: 'Job name'), - keyboardAppearance: Brightness.light, - initialValue: _name, - validator: (value) => - (value ?? '').isNotEmpty ? null : 'Name can\'t be empty', - onSaved: (value) => _name = value, - ), - TextFormField( - decoration: const InputDecoration(labelText: 'Rate per hour'), - keyboardAppearance: Brightness.light, - initialValue: _ratePerHour != null ? '$_ratePerHour' : null, - keyboardType: const TextInputType.numberWithOptions( - signed: false, - decimal: false, - ), - onSaved: (value) => _ratePerHour = int.tryParse(value ?? '') ?? 0, - ), - ]; - } -} diff --git a/lib/src/features/jobs/presentation/edit_job_screen/edit_job_screen_controller.dart b/lib/src/features/jobs/presentation/edit_job_screen/edit_job_screen_controller.dart deleted file mode 100644 index b8bb952c..00000000 --- a/lib/src/features/jobs/presentation/edit_job_screen/edit_job_screen_controller.dart +++ /dev/null @@ -1,58 +0,0 @@ -import 'dart:async'; - -import 'package:riverpod_annotation/riverpod_annotation.dart'; -import 'package:starter_architecture_flutter_firebase/src/features/authentication/data/firebase_auth_repository.dart'; -import 'package:starter_architecture_flutter_firebase/src/features/jobs/data/jobs_repository.dart'; -import 'package:starter_architecture_flutter_firebase/src/features/jobs/domain/job.dart'; -import 'package:starter_architecture_flutter_firebase/src/features/jobs/presentation/edit_job_screen/job_submit_exception.dart'; - -part 'edit_job_screen_controller.g.dart'; - -@riverpod -class EditJobScreenController extends _$EditJobScreenController { - @override - FutureOr build() { - // - } - - Future submit( - {JobID? jobId, - Job? oldJob, - required String name, - required int ratePerHour}) async { - final currentUser = ref.read(authRepositoryProvider).currentUser; - if (currentUser == null) { - throw AssertionError('User can\'t be null'); - } - // set loading state - state = const AsyncLoading().copyWithPrevious(state); - // check if name is already in use - final repository = ref.read(jobsRepositoryProvider); - final jobs = await repository.fetchJobs(uid: currentUser.uid); - final allLowerCaseNames = - jobs.map((job) => job.name.toLowerCase()).toList(); - // it's ok to use the same name as the old job - if (oldJob != null) { - allLowerCaseNames.remove(oldJob.name.toLowerCase()); - } - // check if name is already used - if (allLowerCaseNames.contains(name.toLowerCase())) { - state = AsyncError(JobSubmitException(), StackTrace.current); - return false; - } else { - // job previously existed - if (jobId != null) { - final job = Job(id: jobId, name: name, ratePerHour: ratePerHour); - state = await AsyncValue.guard( - () => repository.updateJob(uid: currentUser.uid, job: job), - ); - } else { - state = await AsyncValue.guard( - () => repository.addJob( - uid: currentUser.uid, name: name, ratePerHour: ratePerHour), - ); - } - return state.hasError == false; - } - } -} diff --git a/lib/src/features/jobs/presentation/edit_job_screen/edit_job_screen_controller.g.dart b/lib/src/features/jobs/presentation/edit_job_screen/edit_job_screen_controller.g.dart deleted file mode 100644 index 775f9d7c..00000000 --- a/lib/src/features/jobs/presentation/edit_job_screen/edit_job_screen_controller.g.dart +++ /dev/null @@ -1,27 +0,0 @@ -// GENERATED CODE - DO NOT MODIFY BY HAND - -part of 'edit_job_screen_controller.dart'; - -// ************************************************************************** -// RiverpodGenerator -// ************************************************************************** - -String _$editJobScreenControllerHash() => - r'e2985913f443860f6aa9d1b0aa462d4e5c25bed4'; - -/// See also [EditJobScreenController]. -@ProviderFor(EditJobScreenController) -final editJobScreenControllerProvider = - AutoDisposeAsyncNotifierProvider.internal( - EditJobScreenController.new, - name: r'editJobScreenControllerProvider', - debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') - ? null - : _$editJobScreenControllerHash, - dependencies: null, - allTransitiveDependencies: null, -); - -typedef _$EditJobScreenController = AutoDisposeAsyncNotifier; -// ignore_for_file: type=lint -// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member diff --git a/lib/src/features/jobs/presentation/edit_job_screen/job_submit_exception.dart b/lib/src/features/jobs/presentation/edit_job_screen/job_submit_exception.dart deleted file mode 100644 index e8f19329..00000000 --- a/lib/src/features/jobs/presentation/edit_job_screen/job_submit_exception.dart +++ /dev/null @@ -1,9 +0,0 @@ -class JobSubmitException { - String get title => 'Name already used'; - String get description => 'Please choose a different job name'; - - @override - String toString() { - return '$title. $description.'; - } -} diff --git a/lib/src/features/jobs/presentation/job_entries_screen/entry_list_item.dart b/lib/src/features/jobs/presentation/job_entries_screen/entry_list_item.dart deleted file mode 100644 index 767b9656..00000000 --- a/lib/src/features/jobs/presentation/job_entries_screen/entry_list_item.dart +++ /dev/null @@ -1,113 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:starter_architecture_flutter_firebase/src/constants/app_sizes.dart'; -import 'package:starter_architecture_flutter_firebase/src/utils/format.dart'; -import 'package:starter_architecture_flutter_firebase/src/features/entries/domain/entry.dart'; -import 'package:starter_architecture_flutter_firebase/src/features/jobs/domain/job.dart'; - -class EntryListItem extends StatelessWidget { - const EntryListItem({ - super.key, - required this.entry, - required this.job, - this.onTap, - }); - - final Entry entry; - final Job job; - final VoidCallback? onTap; - - @override - Widget build(BuildContext context) { - return InkWell( - onTap: onTap, - child: Container( - padding: const EdgeInsets.symmetric( - horizontal: Sizes.p16, - vertical: Sizes.p8, - ), - child: Row( - children: [ - Expanded( - child: _buildContents(context), - ), - const Icon(Icons.chevron_right, color: Colors.grey), - ], - ), - ), - ); - } - - Widget _buildContents(BuildContext context) { - final dayOfWeek = Format.dayOfWeek(entry.start); - final startDate = Format.date(entry.start); - final startTime = TimeOfDay.fromDateTime(entry.start).format(context); - final endTime = TimeOfDay.fromDateTime(entry.end).format(context); - final durationFormatted = Format.hours(entry.durationInHours); - - final pay = job.ratePerHour * entry.durationInHours; - final payFormatted = Format.currency(pay); - - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row(children: [ - Text(dayOfWeek, - style: const TextStyle(fontSize: 18.0, color: Colors.grey)), - gapW16, - Text(startDate, style: const TextStyle(fontSize: 18.0)), - if (job.ratePerHour > 0.0) ...[ - Expanded(child: Container()), - Text( - payFormatted, - style: TextStyle(fontSize: 16.0, color: Colors.green[700]), - ), - ], - ]), - Row(children: [ - Text('$startTime - $endTime', style: const TextStyle(fontSize: 16.0)), - Expanded(child: Container()), - Text(durationFormatted, style: const TextStyle(fontSize: 16.0)), - ]), - if (entry.comment.isNotEmpty) - Text( - entry.comment, - style: const TextStyle(fontSize: 12.0), - overflow: TextOverflow.ellipsis, - maxLines: 1, - ), - ], - ); - } -} - -class DismissibleEntryListItem extends StatelessWidget { - const DismissibleEntryListItem({ - super.key, - required this.dismissibleKey, - required this.entry, - required this.job, - this.onDismissed, - this.onTap, - }); - - final Key dismissibleKey; - final Entry entry; - final Job job; - final VoidCallback? onDismissed; - final VoidCallback? onTap; - - @override - Widget build(BuildContext context) { - return Dismissible( - background: Container(color: Colors.red), - key: dismissibleKey, - direction: DismissDirection.endToStart, - onDismissed: (direction) => onDismissed?.call(), - child: EntryListItem( - entry: entry, - job: job, - onTap: onTap, - ), - ); - } -} diff --git a/lib/src/features/jobs/presentation/job_entries_screen/job_entries_list.dart b/lib/src/features/jobs/presentation/job_entries_screen/job_entries_list.dart deleted file mode 100644 index 6b6d0ac7..00000000 --- a/lib/src/features/jobs/presentation/job_entries_screen/job_entries_list.dart +++ /dev/null @@ -1,44 +0,0 @@ -import 'package:firebase_ui_firestore/firebase_ui_firestore.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:go_router/go_router.dart'; -import 'package:starter_architecture_flutter_firebase/src/features/entries/data/entries_repository.dart'; -import 'package:starter_architecture_flutter_firebase/src/features/entries/domain/entry.dart'; -import 'package:starter_architecture_flutter_firebase/src/features/jobs/domain/job.dart'; -import 'package:starter_architecture_flutter_firebase/src/features/jobs/presentation/job_entries_screen/entry_list_item.dart'; -import 'package:starter_architecture_flutter_firebase/src/features/jobs/presentation/job_entries_screen/job_entries_list_controller.dart'; -import 'package:starter_architecture_flutter_firebase/src/routing/app_router.dart'; -import 'package:starter_architecture_flutter_firebase/src/utils/async_value_ui.dart'; - -class JobEntriesList extends ConsumerWidget { - const JobEntriesList({super.key, required this.job}); - final Job job; - - @override - Widget build(BuildContext context, WidgetRef ref) { - ref.listen( - jobsEntriesListControllerProvider, - (_, state) => state.showAlertDialogOnError(context), - ); - final jobEntriesQuery = ref.watch(jobEntriesQueryProvider(job.id)); - return FirestoreListView( - query: jobEntriesQuery, - itemBuilder: (context, doc) { - final entry = doc.data(); - return DismissibleEntryListItem( - dismissibleKey: Key('entry-${entry.id}'), - entry: entry, - job: job, - onDismissed: () => ref - .read(jobsEntriesListControllerProvider.notifier) - .deleteEntry(entry.id), - onTap: () => context.goNamed( - AppRoute.entry.name, - pathParameters: {'id': job.id, 'eid': entry.id}, - extra: entry, - ), - ); - }, - ); - } -} diff --git a/lib/src/features/jobs/presentation/job_entries_screen/job_entries_list_controller.dart b/lib/src/features/jobs/presentation/job_entries_screen/job_entries_list_controller.dart deleted file mode 100644 index f7f49367..00000000 --- a/lib/src/features/jobs/presentation/job_entries_screen/job_entries_list_controller.dart +++ /dev/null @@ -1,27 +0,0 @@ -import 'dart:async'; - -import 'package:riverpod_annotation/riverpod_annotation.dart'; -import 'package:starter_architecture_flutter_firebase/src/features/authentication/data/firebase_auth_repository.dart'; -import 'package:starter_architecture_flutter_firebase/src/features/entries/data/entries_repository.dart'; -import 'package:starter_architecture_flutter_firebase/src/features/entries/domain/entry.dart'; - -part 'job_entries_list_controller.g.dart'; - -@riverpod -class JobsEntriesListController extends _$JobsEntriesListController { - @override - FutureOr build() { - // ok to leave this empty if the return type is FutureOr - } - - Future deleteEntry(EntryID entryId) async { - final currentUser = ref.read(authRepositoryProvider).currentUser; - if (currentUser == null) { - throw AssertionError('User can\'t be null'); - } - final repository = ref.read(entriesRepositoryProvider); - state = const AsyncLoading(); - state = await AsyncValue.guard( - () => repository.deleteEntry(uid: currentUser.uid, entryId: entryId)); - } -} diff --git a/lib/src/features/jobs/presentation/job_entries_screen/job_entries_list_controller.g.dart b/lib/src/features/jobs/presentation/job_entries_screen/job_entries_list_controller.g.dart deleted file mode 100644 index 608608e5..00000000 --- a/lib/src/features/jobs/presentation/job_entries_screen/job_entries_list_controller.g.dart +++ /dev/null @@ -1,27 +0,0 @@ -// GENERATED CODE - DO NOT MODIFY BY HAND - -part of 'job_entries_list_controller.dart'; - -// ************************************************************************** -// RiverpodGenerator -// ************************************************************************** - -String _$jobsEntriesListControllerHash() => - r'f9a08b66a0c962d210a09aebb711d38acb354b1e'; - -/// See also [JobsEntriesListController]. -@ProviderFor(JobsEntriesListController) -final jobsEntriesListControllerProvider = - AutoDisposeAsyncNotifierProvider.internal( - JobsEntriesListController.new, - name: r'jobsEntriesListControllerProvider', - debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') - ? null - : _$jobsEntriesListControllerHash, - dependencies: null, - allTransitiveDependencies: null, -); - -typedef _$JobsEntriesListController = AutoDisposeAsyncNotifier; -// ignore_for_file: type=lint -// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member diff --git a/lib/src/features/jobs/presentation/job_entries_screen/job_entries_screen.dart b/lib/src/features/jobs/presentation/job_entries_screen/job_entries_screen.dart deleted file mode 100644 index ad256b80..00000000 --- a/lib/src/features/jobs/presentation/job_entries_screen/job_entries_screen.dart +++ /dev/null @@ -1,55 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:go_router/go_router.dart'; -import 'package:starter_architecture_flutter_firebase/src/common_widgets/async_value_widget.dart'; -import 'package:starter_architecture_flutter_firebase/src/features/jobs/data/jobs_repository.dart'; -import 'package:starter_architecture_flutter_firebase/src/features/jobs/domain/job.dart'; -import 'package:starter_architecture_flutter_firebase/src/features/jobs/presentation/job_entries_screen/job_entries_list.dart'; -import 'package:starter_architecture_flutter_firebase/src/routing/app_router.dart'; - -class JobEntriesScreen extends ConsumerWidget { - const JobEntriesScreen({super.key, required this.jobId}); - final JobID jobId; - - @override - Widget build(BuildContext context, WidgetRef ref) { - final jobAsync = ref.watch(jobStreamProvider(jobId)); - return ScaffoldAsyncValueWidget( - value: jobAsync, - data: (job) => JobEntriesPageContents(job: job), - ); - } -} - -class JobEntriesPageContents extends StatelessWidget { - const JobEntriesPageContents({super.key, required this.job}); - final Job job; - - @override - Widget build(BuildContext context) { - return Scaffold( - appBar: AppBar( - title: Text(job.name), - actions: [ - IconButton( - icon: const Icon(Icons.edit, color: Colors.white), - onPressed: () => context.goNamed( - AppRoute.editJob.name, - pathParameters: {'id': job.id}, - extra: job, - ), - ), - ], - ), - body: JobEntriesList(job: job), - floatingActionButton: FloatingActionButton( - child: const Icon(Icons.add, color: Colors.white), - onPressed: () => context.goNamed( - AppRoute.addEntry.name, - pathParameters: {'id': job.id}, - extra: job, - ), - ), - ); - } -} diff --git a/lib/src/features/jobs/presentation/jobs_screen/jobs_screen.dart b/lib/src/features/jobs/presentation/jobs_screen/jobs_screen.dart deleted file mode 100644 index 21d09ec9..00000000 --- a/lib/src/features/jobs/presentation/jobs_screen/jobs_screen.dart +++ /dev/null @@ -1,80 +0,0 @@ -import 'package:firebase_ui_firestore/firebase_ui_firestore.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:go_router/go_router.dart'; -import 'package:starter_architecture_flutter_firebase/src/constants/strings.dart'; -import 'package:starter_architecture_flutter_firebase/src/features/jobs/data/jobs_repository.dart'; -import 'package:starter_architecture_flutter_firebase/src/features/jobs/domain/job.dart'; -import 'package:starter_architecture_flutter_firebase/src/features/jobs/presentation/jobs_screen/jobs_screen_controller.dart'; -import 'package:starter_architecture_flutter_firebase/src/routing/app_router.dart'; -import 'package:starter_architecture_flutter_firebase/src/utils/async_value_ui.dart'; - -class JobsScreen extends StatelessWidget { - const JobsScreen({super.key}); - - @override - Widget build(BuildContext context) { - return Scaffold( - appBar: AppBar( - title: const Text(Strings.jobs), - actions: [ - IconButton( - icon: const Icon(Icons.add, color: Colors.white), - onPressed: () => context.goNamed(AppRoute.addJob.name), - ), - ], - ), - body: Consumer( - builder: (context, ref, child) { - ref.listen( - jobsScreenControllerProvider, - (_, state) => state.showAlertDialogOnError(context), - ); - final jobsQuery = ref.watch(jobsQueryProvider); - return FirestoreListView( - query: jobsQuery, - emptyBuilder: (context) => const Center(child: Text('No data')), - errorBuilder: (context, error, stackTrace) => Center( - child: Text(error.toString()), - ), - loadingBuilder: (context) => - const Center(child: CircularProgressIndicator()), - itemBuilder: (context, doc) { - final job = doc.data(); - return Dismissible( - key: Key('job-${job.id}'), - background: Container(color: Colors.red), - direction: DismissDirection.endToStart, - onDismissed: (direction) => ref - .read(jobsScreenControllerProvider.notifier) - .deleteJob(job), - child: JobListTile( - job: job, - onTap: () => context.goNamed( - AppRoute.job.name, - pathParameters: {'id': job.id}, - ), - ), - ); - }, - ); - }, - ), - ); - } -} - -class JobListTile extends StatelessWidget { - const JobListTile({super.key, required this.job, this.onTap}); - final Job job; - final VoidCallback? onTap; - - @override - Widget build(BuildContext context) { - return ListTile( - title: Text(job.name), - trailing: const Icon(Icons.chevron_right), - onTap: onTap, - ); - } -} diff --git a/lib/src/features/jobs/presentation/jobs_screen/jobs_screen_controller.dart b/lib/src/features/jobs/presentation/jobs_screen/jobs_screen_controller.dart deleted file mode 100644 index 260f844c..00000000 --- a/lib/src/features/jobs/presentation/jobs_screen/jobs_screen_controller.dart +++ /dev/null @@ -1,27 +0,0 @@ -import 'dart:async'; - -import 'package:riverpod_annotation/riverpod_annotation.dart'; -import 'package:starter_architecture_flutter_firebase/src/features/authentication/data/firebase_auth_repository.dart'; -import 'package:starter_architecture_flutter_firebase/src/features/jobs/data/jobs_repository.dart'; -import 'package:starter_architecture_flutter_firebase/src/features/jobs/domain/job.dart'; - -part 'jobs_screen_controller.g.dart'; - -@riverpod -class JobsScreenController extends _$JobsScreenController { - @override - FutureOr build() { - // ok to leave this empty if the return type is FutureOr - } - - Future deleteJob(Job job) async { - final currentUser = ref.read(authRepositoryProvider).currentUser; - if (currentUser == null) { - throw AssertionError('User can\'t be null'); - } - final repository = ref.read(jobsRepositoryProvider); - state = const AsyncLoading(); - state = await AsyncValue.guard( - () => repository.deleteJob(uid: currentUser.uid, jobId: job.id)); - } -} diff --git a/lib/src/features/jobs/presentation/jobs_screen/jobs_screen_controller.g.dart b/lib/src/features/jobs/presentation/jobs_screen/jobs_screen_controller.g.dart deleted file mode 100644 index ade570c8..00000000 --- a/lib/src/features/jobs/presentation/jobs_screen/jobs_screen_controller.g.dart +++ /dev/null @@ -1,27 +0,0 @@ -// GENERATED CODE - DO NOT MODIFY BY HAND - -part of 'jobs_screen_controller.dart'; - -// ************************************************************************** -// RiverpodGenerator -// ************************************************************************** - -String _$jobsScreenControllerHash() => - r'e3a40258404cf512fd12924d8f0a485f75d7d6fb'; - -/// See also [JobsScreenController]. -@ProviderFor(JobsScreenController) -final jobsScreenControllerProvider = - AutoDisposeAsyncNotifierProvider.internal( - JobsScreenController.new, - name: r'jobsScreenControllerProvider', - debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') - ? null - : _$jobsScreenControllerHash, - dependencies: null, - allTransitiveDependencies: null, -); - -typedef _$JobsScreenController = AutoDisposeAsyncNotifier; -// ignore_for_file: type=lint -// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member diff --git a/lib/src/features/login/data/providers.dart b/lib/src/features/login/data/providers.dart new file mode 100644 index 00000000..8d278a4d --- /dev/null +++ b/lib/src/features/login/data/providers.dart @@ -0,0 +1,23 @@ +import 'package:riverpod_annotation/riverpod_annotation.dart'; +import 'package:flutter_starter_base_app/src/root/domain/basic_api_response.dart'; +import 'package:flutter_starter_base_app/src/utils/authentication_handler.dart'; +import 'package:flutter_starter_base_app/src/api/api.dart'; +part 'providers.g.dart'; + +@riverpod +Future login(LoginRef ref, {required String username, required String password}) async => + await API().login(username: username, password: password); +@riverpod +Future logout(LogoutRef ref) async => await AuthenticationHandler().clearTokens(); +@riverpod +Future refreshToken(RefreshTokenRef ref) async => await API().refreshToken(); +@riverpod +Future forgotPassword(ForgotPasswordRef ref, {required String username}) async => + await API().forgotPassword(username: username); +@riverpod +Future resetPassword(ResetPasswordRef ref, + {required String username, required String otp, required String newPassword}) async => + await API().resetPassword(username: username, otp: otp, newPassword: newPassword); + +@riverpod +Future canAuthenticateUser(CanAuthenticateUserRef ref) async => AuthenticationHandler().canAuthenticateUser(); diff --git a/lib/src/features/login/domain/login_response.dart b/lib/src/features/login/domain/login_response.dart new file mode 100644 index 00000000..ddb8f9cb --- /dev/null +++ b/lib/src/features/login/domain/login_response.dart @@ -0,0 +1,13 @@ +import 'package:json_annotation/json_annotation.dart'; +part 'login_response.g.dart'; + +@JsonSerializable() +class LoginResponse { + final String accessToken; + final String refreshToken; + + LoginResponse({required this.accessToken, required this.refreshToken}); + + factory LoginResponse.fromJson(Map json) => _$LoginResponseFromJson(json); + Map toJson() => _$LoginResponseToJson(this); +} diff --git a/lib/src/features/login/presentation/check_eula.dart b/lib/src/features/login/presentation/check_eula.dart new file mode 100644 index 00000000..735213a9 --- /dev/null +++ b/lib/src/features/login/presentation/check_eula.dart @@ -0,0 +1,28 @@ +import 'package:flutter_starter_base_app/src/common_widgets/circular_loading_animation.dart'; +import 'package:flutter_starter_base_app/src/features/account/data/account_provider.dart'; +import 'package:flutter_starter_base_app/src/routing/routes.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:go_router/go_router.dart'; +import 'package:flutter/material.dart'; + +class CheckEULA extends ConsumerWidget { + const CheckEULA({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) => ref.watch(fetchEULAStatusProvider).when( + data: (bool acceptedEULA) { + WidgetsBinding.instance.addPostFrameCallback((_) { + if (context.mounted) { + acceptedEULA ? context.goNamed(AppRoute.home.name) : context.pushNamed(AppRoute.acceptEULA.name); + } + }); + return Container(); + }, + error: (error, stackTrace) { + WidgetsBinding.instance.addPostFrameCallback((_) { + if (context.mounted) context.goNamed(AppRoute.splash.name); + }); + return Container(); + }, + loading: () => const LoadingAnimation()); +} diff --git a/lib/src/features/login/presentation/forgot_password_page.dart b/lib/src/features/login/presentation/forgot_password_page.dart new file mode 100644 index 00000000..1db7f967 --- /dev/null +++ b/lib/src/features/login/presentation/forgot_password_page.dart @@ -0,0 +1,101 @@ +import 'package:flutter_starter_base_app/src/common_widgets/action_text_button.dart'; +import 'package:flutter_starter_base_app/src/common_widgets/app_bar.dart'; +import 'package:flutter_starter_base_app/src/features/login/data/providers.dart'; +import 'package:flutter_starter_base_app/src/localization/generated/locale_keys.g.dart'; +import 'package:flutter_starter_base_app/src/routing/routes.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:flutter_starter_base_app/src/constants/colors.dart'; +import 'package:flutter_starter_base_app/src/common_widgets/custom_text_form_field.dart'; +import 'package:flutter_starter_base_app/src/common_widgets/primary_button.dart'; +import 'package:go_router/go_router.dart'; + +class ForgotPasswordView extends ConsumerStatefulWidget { + const ForgotPasswordView({super.key}); + @override + ConsumerState createState() => _ForgotPasswordViewState(); +} + +class _ForgotPasswordViewState extends ConsumerState { + final _formKey = GlobalKey(); + final TextEditingController usernameController = TextEditingController(); + final FocusNode _usernameFocusNode = FocusNode(); + @override + void dispose() { + _usernameFocusNode.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) => Scaffold( + appBar: CustomAppBar( + titleWidget: Text(LocaleKeys.btn_forgotPassword.tr()), showBackButton: true, showHamburgerMenu: false), + body: Container( + color: CustomColors().darkGray, + child: Form( + key: _formKey, + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Column(mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ + Column(children: [ + Padding( + padding: const EdgeInsets.symmetric(horizontal: 17.5, vertical: 16), + child: Text(LocaleKeys.account_reset_forgotPassDesc.tr(), + style: TextStyle( + color: CustomColors().whitecolor, fontSize: 17, fontWeight: FontWeight.w400))), + _buildUsername(context) + ]), + Column(children: [ + _buildResetPassword(context), + ActionTextButton( + onPressed: () => context.canPop() ? context.pop() : null, text: LocaleKeys.btn_login.tr()) + ]) + ]))))); + + _buildResetPassword(BuildContext context) => PrimaryButton( + text: LocaleKeys.btn_resetPassword.tr(), + backgroundColor: CustomColors().lightblueColor, + onPressed: () async { + if (_formKey.currentState!.validate()) { + try { + await ref.read(forgotPasswordProvider(username: usernameController.text).future).then((response) { + final message = response.message; + if (context.mounted) { + ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(message))); + } + }); + if (context.mounted) context.pushNamed(AppRoute.resetPassword.name); + } catch (e) { + ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('$e'))); + } + } + }); + + _buildUsername(BuildContext context) => CustomTextFormField( + autovalidateMode: AutovalidateMode.onUserInteraction, + controller: usernameController, + focusNode: _usernameFocusNode, + textInputType: TextInputType.text, + hintText: 'Enter Username', + prefixIcon: Padding( + padding: const EdgeInsets.only(left: 15.0, right: 10), + child: Row(mainAxisSize: MainAxisSize.min, mainAxisAlignment: MainAxisAlignment.center, children: [ + Text(LocaleKeys.common_username.tr(), + style: TextStyle(color: CustomColors().whitecolor, fontWeight: FontWeight.w400, fontSize: 17)), + SizedBox(width: MediaQuery.of(context).size.width * 0.02), + Icon(Icons.help, color: CustomColors().lightblueColor, size: 15) + ])), + validator: (value) { + if (value == null || value.isEmpty) { + return "Username can't be empty"; + } + if (value.length < 3) { + return "Username must be at least 3 characters"; + } + if (value.length > 50) { + return "Username can't be more than 50 characters"; + } + return null; + }); +} diff --git a/lib/src/features/login/presentation/login_page.dart b/lib/src/features/login/presentation/login_page.dart new file mode 100644 index 00000000..e1464218 --- /dev/null +++ b/lib/src/features/login/presentation/login_page.dart @@ -0,0 +1,231 @@ +import 'package:flutter_starter_base_app/src/common_widgets/action_text_button.dart'; +import 'package:flutter_starter_base_app/src/common_widgets/app_bar.dart'; +import 'package:flutter_starter_base_app/src/common_widgets/custom_text_form_field.dart'; +import 'package:flutter_starter_base_app/src/common_widgets/primary_button.dart'; +import 'package:flutter_starter_base_app/src/constants/colors.dart'; +import 'package:flutter_starter_base_app/src/localization/generated/locale_keys.g.dart'; +import 'package:flutter_starter_base_app/src/routing/routes.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'dart:ui' as ui; +import 'package:go_router/go_router.dart'; + +///XXX: only for testing +const String username = String.fromEnvironment('USERNAME'); +const String password = String.fromEnvironment('PASSWORD'); + +class LoginPage extends ConsumerStatefulWidget { + const LoginPage({super.key}); + + @override + ConsumerState createState() => _LoginPageState(); +} + +class _LoginPageState extends ConsumerState { + final _formKey = GlobalKey(); + final TextEditingController usernameController = TextEditingController(text: username); + final TextEditingController passwordController = TextEditingController(text: password); + FocusNode _usernameFocusNode = FocusNode(); + FocusNode _passwordFocusNode = FocusNode(); + ValueNotifier obsecurePassword = ValueNotifier(true); + + @override + void initState() { + super.initState(); + _usernameFocusNode = FocusNode(); + _passwordFocusNode = FocusNode(); + } + + @override + void dispose() { + _usernameFocusNode.dispose(); + _passwordFocusNode.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) => Scaffold( + appBar: CustomAppBar( + titleWidget: Text(LocaleKeys.btn_login.tr()), + leading: _buildAppbarBack(context), + showHamburgerMenu: false, + ), + body: Container( + color: CustomColors().darkGray, + child: Form( + key: _formKey, + child: Padding( + padding: const EdgeInsets.all(16), + child: Column(mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ + Column( + mainAxisAlignment: MainAxisAlignment.start, + children: [_buildEmailField(context), _buildPasswordField()]), + Column(children: [ + _buildLogin(context), + ActionTextButton( + text: LocaleKeys.btn_forgotPassword.tr(), + onPressed: () => context.pushNamed(AppRoute.forgotPassword.name)) + ]) + ]))))); + + _buildAppbarBack(BuildContext context) { + return InkWell( + onTap: () => context.goNamed(AppRoute.splash.name), + child: Row(mainAxisAlignment: MainAxisAlignment.start, children: [ + const Icon(CupertinoIcons.back), + Text(LocaleKeys.common_back.tr(), + style: TextStyle( + color: CustomColors().primaryTextColor, + fontSize: 17, + )) + ])); + } + + _buildLogin(BuildContext context) => PrimaryButton( + text: LocaleKeys.btn_login.tr(), + backgroundColor: CustomColors().lightblueColor, + onPressed: () => _formKey.currentState!.validate() + ? context.pushNamed(AppRoute.loginPageTransition.name, + pathParameters: {'password': passwordController.text, 'username': usernameController.text}) + : null); + + _buildPasswordField() { + return ValueListenableBuilder( + valueListenable: obsecurePassword, + builder: (context, value, child) { + return TextFormField( + controller: passwordController, + focusNode: _passwordFocusNode, + keyboardType: TextInputType.text, + textAlign: TextAlign.right, + maxLines: 1, + style: TextStyle( + overflow: TextOverflow.ellipsis, + color: CustomColors().whitecolor, + ), + decoration: InputDecoration( + contentPadding: const EdgeInsets.all(12.0), + hintMaxLines: 1, + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(0.0), + borderSide: BorderSide( + color: CustomColors().grayColor, + )), + enabledBorder: OutlineInputBorder( + borderSide: BorderSide(color: CustomColors().grayColor, width: 0), + borderRadius: BorderRadius.circular(0.0), + ), + focusedBorder: OutlineInputBorder( + borderSide: BorderSide(color: CustomColors().grayColor, width: 0), + borderRadius: BorderRadius.circular(0.0), + ), + errorBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(0.0), + borderSide: BorderSide( + color: CustomColors().red, + width: 1, + ), + ), + errorStyle: TextStyle(color: CustomColors().red), + hintTextDirection: ui.TextDirection.rtl, + floatingLabelBehavior: FloatingLabelBehavior.never, + labelStyle: + TextStyle(color: CustomColors().lightblueColor, fontSize: 17, fontWeight: FontWeight.normal), + fillColor: CustomColors().grayColor, + filled: true, + hintText: LocaleKeys.hint_password.tr(), + hintStyle: TextStyle( + overflow: TextOverflow.ellipsis, + color: CustomColors().lightblueColor, + fontSize: 17, + fontWeight: FontWeight.normal), + prefixIcon: Padding( + padding: const EdgeInsets.only(left: 15.0, right: 10), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + LocaleKeys.common_password.tr(), + style: TextStyle( + color: CustomColors().whitecolor, + fontWeight: FontWeight.w400, + fontSize: 17, + ), + ), + SizedBox( + width: MediaQuery.of(context).size.width * 0.02, + ), + Icon( + Icons.help, + color: CustomColors().lightblueColor, + size: 15, + ), + ], + ), + ), + suffixIconConstraints: const BoxConstraints( + minWidth: 30, + minHeight: 30, + )), + obscureText: obsecurePassword.value, + validator: (value) { + if (value!.isEmpty) { + return 'Please enter a password'; + } + // if (!RegExp(r'^(?=.*?[A-Z])(?=.*?[0-9])(?=.*?[!@#\$&*~]).{8,}$').hasMatch(value)) { + // return 'At least 8 characters, including one uppercase letter, one number, and one special character'; + // } + return null; + }, + ); + }); + } + + _buildEmailField(BuildContext context) { + return CustomTextFormField( + controller: usernameController, + focusNode: _usernameFocusNode, + textInputType: TextInputType.emailAddress, + hintText: LocaleKeys.hint_emailOrUsername.tr(), + prefixIcon: Padding( + padding: const EdgeInsets.only(left: 15.0, right: 10), + child: Row( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + LocaleKeys.common_username.tr(), + style: TextStyle( + color: CustomColors().whitecolor, + fontWeight: FontWeight.w400, + fontSize: 17, + ), + ), + SizedBox( + width: MediaQuery.of(context).size.width * 0.02, + ), + Icon( + Icons.help, + color: CustomColors().lightblueColor, + size: 15, + ), + ], + ), + ), + validator: (value) { + if (value == null || value.isEmpty) { + return "Username can't be empty"; + } + if (value.length < 3) { + return "Username must be at least 3 characters"; + } + if (value.length > 25) { + return "Username can't be more than 15 characters"; + } + return null; + }, + ); + } +} diff --git a/lib/src/features/login/presentation/login_transition.dart b/lib/src/features/login/presentation/login_transition.dart new file mode 100644 index 00000000..8c788ec4 --- /dev/null +++ b/lib/src/features/login/presentation/login_transition.dart @@ -0,0 +1,31 @@ +import 'package:flutter_starter_base_app/src/features/login/data/providers.dart'; +import 'package:flutter_starter_base_app/src/routing/routes.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:flutter_starter_base_app/src/constants/colors.dart'; +import 'package:flutter_starter_base_app/src/common_widgets/circular_loading_animation.dart'; +import 'package:go_router/go_router.dart'; + +class LoginPageTransition extends ConsumerWidget { + final String username, password; + const LoginPageTransition({super.key, required this.password, required this.username}); + + @override + Widget build(BuildContext context, WidgetRef ref) => + ref.watch(loginProvider(username: username, password: password)).when( + loading: () => const LoadingAnimation(), + data: (_) { + WidgetsBinding.instance.addPostFrameCallback((_) async { + if (context.mounted) context.goNamed(AppRoute.home.name); + }); + return const LoadingAnimation(); + }, + error: (error, _) { + WidgetsBinding.instance.addPostFrameCallback((_) { + if (context.canPop()) context.pop(); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(backgroundColor: CustomColors().darkestGrayBG, content: Center(child: Text('$error')))); + }); + return Container(); + }); +} diff --git a/lib/src/features/login/presentation/logout_transition.dart b/lib/src/features/login/presentation/logout_transition.dart new file mode 100644 index 00000000..bd50662c --- /dev/null +++ b/lib/src/features/login/presentation/logout_transition.dart @@ -0,0 +1,29 @@ +import 'package:flutter_starter_base_app/src/features/login/data/providers.dart'; +import 'package:flutter_starter_base_app/src/routing/app_router.dart'; +import 'package:flutter_starter_base_app/src/routing/routes.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:flutter_starter_base_app/src/constants/colors.dart'; +import 'package:flutter_starter_base_app/src/common_widgets/circular_loading_animation.dart'; +import 'package:go_router/go_router.dart'; + +class LogoutPageTransition extends ConsumerWidget { + const LogoutPageTransition({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) => ref.watch(logoutProvider).when( + loading: () => const LoadingAnimation(), + data: (_) { + WidgetsBinding.instance.addPostFrameCallback((_) { + ref.read(currentMainRouteIndexProvider.notifier).state = 0; + if (context.mounted) context.goNamed(AppRoute.splash.name); + }); + + return Container(); + }, + error: (error, _) { + WidgetsBinding.instance.addPostFrameCallback((_) => ScaffoldMessenger.of(context).showSnackBar( + SnackBar(backgroundColor: CustomColors().darkestGrayBG, content: Center(child: Text('$error'))))); + return Container(); + }); +} diff --git a/lib/src/features/login/presentation/reset_password_page.dart b/lib/src/features/login/presentation/reset_password_page.dart new file mode 100644 index 00000000..1153676a --- /dev/null +++ b/lib/src/features/login/presentation/reset_password_page.dart @@ -0,0 +1,282 @@ +import 'package:flutter_starter_base_app/src/common_widgets/action_text_button.dart'; +import 'package:flutter_starter_base_app/src/common_widgets/app_bar.dart'; +import 'package:flutter_starter_base_app/src/common_widgets/custom_text_form_field.dart'; +import 'package:flutter_starter_base_app/src/common_widgets/primary_button.dart'; +import 'package:flutter_starter_base_app/src/constants/colors.dart'; +import 'package:flutter_starter_base_app/src/features/login/data/providers.dart'; +import 'package:flutter_starter_base_app/src/localization/generated/locale_keys.g.dart'; +import 'package:flutter_starter_base_app/src/routing/routes.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:go_router/go_router.dart'; + +class ResetPasswordView extends ConsumerStatefulWidget { + const ResetPasswordView({ + super.key, + }); + + @override + ConsumerState createState() => _ResetPasswordViewState(); +} + +class _ResetPasswordViewState extends ConsumerState { + final _formKey = GlobalKey(); + final TextEditingController usernameController = TextEditingController(); + final TextEditingController passwordController = TextEditingController(); + final TextEditingController confirmPasswordController = TextEditingController(); + final TextEditingController verificationCodeController = TextEditingController(); + FocusNode _usernameFocusNode = FocusNode(); + FocusNode _passwordFocusNode = FocusNode(); + FocusNode _confirmPasswordFocusNode = FocusNode(); + FocusNode _verificationCodeFocusNode = FocusNode(); + + final ValueNotifier _obscurePassword = ValueNotifier(true); + final ValueNotifier _obscureConfirmPassword = ValueNotifier(true); + + @override + void initState() { + super.initState(); + _usernameFocusNode = FocusNode(); + _passwordFocusNode = FocusNode(); + _confirmPasswordFocusNode = FocusNode(); + _verificationCodeFocusNode = FocusNode(); + } + + @override + void dispose() { + _usernameFocusNode.dispose(); + _passwordFocusNode.dispose(); + _confirmPasswordFocusNode.dispose(); + _verificationCodeFocusNode.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: CustomAppBar( + titleWidget: Text(LocaleKeys.btn_resetPassword.tr()), + showBackButton: true, + showHamburgerMenu: false, + ), + body: Container( + color: CustomColors().darkGray, + child: Column( + children: [ + Expanded( + child: SingleChildScrollView( + child: Form( + key: _formKey, + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + children: [ + Padding( + padding: const EdgeInsets.symmetric(horizontal: 17.5, vertical: 16), + child: Text( + LocaleKeys.account_reset_checkYourEmailDesc.tr(), + style: TextStyle( + color: CustomColors().whitecolor, + fontSize: 17, + fontWeight: FontWeight.w400, + ), + ), + ), + textField( + context, + controller: usernameController, + focusNode: _usernameFocusNode, + hintText: LocaleKeys.hint_enter.tr(), + label: LocaleKeys.common_username.tr(), + ), + passwordField( + context, + controller: passwordController, + focusNode: _passwordFocusNode, + hintText: LocaleKeys.hint_enter.tr(), + label: LocaleKeys.common_password.tr(), + obscureNotifier: _obscurePassword, + ), + passwordField( + context, + controller: confirmPasswordController, + focusNode: _confirmPasswordFocusNode, + hintText: LocaleKeys.hint_enter.tr(), + label: LocaleKeys.account_confPassword.tr(), + obscureNotifier: _obscureConfirmPassword, + isConfirm: true, + ), + textField( + context, + controller: verificationCodeController, + focusNode: _verificationCodeFocusNode, + hintText: LocaleKeys.hint_enter.tr(), + label: LocaleKeys.account_verification.tr(), + ), + ], + ), + ), + ), + ), + ), + Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + children: [ + PrimaryButton( + text: 'Update Password', + backgroundColor: CustomColors().lightblueColor, + onPressed: () async { + try { + if (_formKey.currentState!.validate()) { + await ref + .read(resetPasswordProvider( + username: usernameController.text, + newPassword: passwordController.text, + otp: verificationCodeController.text) + .future) + .then((response) { + final message = response.message; + if (context.mounted) { + ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(message))); + context.goNamed(AppRoute.passwordSuccess.name); + } + }); + } + } catch (e) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text('Failed to reset password: $e')), + ); + } + }, + ), + const SizedBox(height: 8), + ActionTextButton(onPressed: () => context.canPop() ? context.pop() : null, text: 'Cancel'), + ], + ), + ), + ], + ), + ), + ); + } + + CustomTextFormField textField( + BuildContext context, { + required TextEditingController controller, + required FocusNode focusNode, + required String hintText, + required String label, + }) { + return CustomTextFormField( + autovalidateMode: AutovalidateMode.onUserInteraction, + controller: controller, + focusNode: focusNode, + textInputType: TextInputType.text, + hintText: hintText, + prefixIcon: Padding( + padding: const EdgeInsets.only(left: 15.0, right: 10), + child: Row( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + label, + style: TextStyle( + color: CustomColors().whitecolor, + fontWeight: FontWeight.w400, + fontSize: 17, + ), + ), + SizedBox( + width: MediaQuery.of(context).size.width * 0.02, + ), + Icon( + Icons.help, + color: CustomColors().lightblueColor, + size: 15, + ), + ], + ), + ), + validator: (value) { + if (value == null || value.isEmpty) { + return "$label can't be empty"; + } + if (label == "Username") { + if (value.length < 3) { + return "Username must be at least 3 characters"; + } + if (value.length > 15) { + return "Username can't be more than 15 characters"; + } + } + return null; + }, + ); + } + + ValueListenableBuilder passwordField( + BuildContext context, { + required TextEditingController controller, + required FocusNode focusNode, + required String hintText, + required String label, + required ValueNotifier obscureNotifier, + bool isConfirm = false, + }) { + return ValueListenableBuilder( + valueListenable: obscureNotifier, + builder: (context, value, child) { + return CustomTextFormField( + controller: controller, + focusNode: focusNode, + textInputType: TextInputType.text, + autovalidateMode: AutovalidateMode.onUserInteraction, + prefixIcon: Padding( + padding: const EdgeInsets.only(left: 15.0, right: 10), + child: Row( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + label, + style: TextStyle( + color: CustomColors().whitecolor, + fontWeight: FontWeight.w400, + fontSize: 17, + ), + ), + SizedBox( + width: MediaQuery.of(context).size.width * 0.02, + ), + Icon( + Icons.help, + color: CustomColors().lightblueColor, + size: 15, + ), + ], + ), + ), + hintText: hintText, + validator: (value) { + if (value == null || value.isEmpty) { + return '$label is required'; + } + if (!isConfirm) { + if (!RegExp(r'^(?=.*?[A-Z])(?=.*?[0-9])(?=.*?[!@#\$&*~]).{8,}$').hasMatch(value)) { + return 'At least 8 characters, including one uppercase letter, one number, and one special character'; + } + } else { + if (value != passwordController.text) { + return 'Passwords do not match'; + } + } + return null; + }, + ); + }, + ); + } +} diff --git a/lib/src/features/login/presentation/reset_password_success_page.dart b/lib/src/features/login/presentation/reset_password_success_page.dart new file mode 100644 index 00000000..7916a894 --- /dev/null +++ b/lib/src/features/login/presentation/reset_password_success_page.dart @@ -0,0 +1,39 @@ +import 'package:flutter_starter_base_app/src/common_widgets/app_bar.dart'; +import 'package:flutter_starter_base_app/src/common_widgets/primary_button.dart'; +import 'package:flutter_starter_base_app/src/constants/colors.dart'; +import 'package:flutter_starter_base_app/src/routing/routes.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:go_router/go_router.dart'; + +class PasswordUpdateSuccessView extends ConsumerStatefulWidget { + const PasswordUpdateSuccessView({super.key}); + + @override + ConsumerState createState() => _PasswordUpdateSuccessViewState(); +} + +class _PasswordUpdateSuccessViewState extends ConsumerState { + @override + Widget build(BuildContext context) => Scaffold( + appBar: const CustomAppBar( + titleWidget: Text('Reset Password Success'), showBackButton: false, showHamburgerMenu: false), + body: Container( + color: CustomColors().darkGray, + child: Column(children: [ + Expanded( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 17.5, vertical: 16), + child: Text('Your password was updated. Please log in with your new password.', + style: + TextStyle(color: CustomColors().whitecolor, fontSize: 17, fontWeight: FontWeight.w400)))), + Padding( + padding: const EdgeInsets.all(16.0), + child: Column(children: [ + PrimaryButton( + text: 'Go To Log In Page', + backgroundColor: CustomColors().lightblueColor, + onPressed: () => context.mounted ? context.goNamed(AppRoute.signIn.name) : null) + ])) + ]))); +} diff --git a/lib/src/features/login/presentation/startup.dart b/lib/src/features/login/presentation/startup.dart new file mode 100644 index 00000000..84e220e8 --- /dev/null +++ b/lib/src/features/login/presentation/startup.dart @@ -0,0 +1,26 @@ +import 'package:flutter_starter_base_app/src/common_widgets/circular_loading_animation.dart'; +import 'package:flutter_starter_base_app/src/features/login/data/providers.dart'; +import 'package:flutter_starter_base_app/src/routing/routes.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:go_router/go_router.dart'; +import 'package:flutter/material.dart'; + +class Startup extends ConsumerWidget { + const Startup({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) => ref.watch(canAuthenticateUserProvider).when( + data: (bool isAuthenticated) { + WidgetsBinding.instance.addPostFrameCallback((_) async { + if (context.mounted) context.pushNamed(isAuthenticated ? AppRoute.checkEULA.name : AppRoute.splash.name); + }); + return const SizedBox(); + }, + error: (error, stackTrace) { + WidgetsBinding.instance.addPostFrameCallback((_) { + if (context.mounted) context.goNamed(AppRoute.splash.name); + }); + return const SizedBox(); + }, + loading: () => const LoadingAnimation()); +} diff --git a/lib/src/features/onboarding/data/onboarding_repository.g.dart b/lib/src/features/onboarding/data/onboarding_repository.g.dart deleted file mode 100644 index 9aadad45..00000000 --- a/lib/src/features/onboarding/data/onboarding_repository.g.dart +++ /dev/null @@ -1,27 +0,0 @@ -// GENERATED CODE - DO NOT MODIFY BY HAND - -part of 'onboarding_repository.dart'; - -// ************************************************************************** -// RiverpodGenerator -// ************************************************************************** - -String _$onboardingRepositoryHash() => - r'c622db9cad2e44cd70e29693d5653c6b22f36b56'; - -/// See also [onboardingRepository]. -@ProviderFor(onboardingRepository) -final onboardingRepositoryProvider = - FutureProvider.internal( - onboardingRepository, - name: r'onboardingRepositoryProvider', - debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') - ? null - : _$onboardingRepositoryHash, - dependencies: null, - allTransitiveDependencies: null, -); - -typedef OnboardingRepositoryRef = FutureProviderRef; -// ignore_for_file: type=lint -// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member diff --git a/lib/src/features/onboarding/presentation/onboarding_controller.dart b/lib/src/features/onboarding/presentation/onboarding_controller.dart index edfb004a..17ca3c7c 100644 --- a/lib/src/features/onboarding/presentation/onboarding_controller.dart +++ b/lib/src/features/onboarding/presentation/onboarding_controller.dart @@ -1,7 +1,7 @@ import 'dart:async'; import 'package:riverpod_annotation/riverpod_annotation.dart'; -import 'package:starter_architecture_flutter_firebase/src/features/onboarding/data/onboarding_repository.dart'; +import 'package:flutter_starter_base_app/src/features/onboarding/data/onboarding_repository.dart'; part 'onboarding_controller.g.dart'; diff --git a/lib/src/features/onboarding/presentation/onboarding_controller.g.dart b/lib/src/features/onboarding/presentation/onboarding_controller.g.dart deleted file mode 100644 index e412ba81..00000000 --- a/lib/src/features/onboarding/presentation/onboarding_controller.g.dart +++ /dev/null @@ -1,27 +0,0 @@ -// GENERATED CODE - DO NOT MODIFY BY HAND - -part of 'onboarding_controller.dart'; - -// ************************************************************************** -// RiverpodGenerator -// ************************************************************************** - -String _$onboardingControllerHash() => - r'232966a6326a75bb5f5166c8b76bbbb15087adaf'; - -/// See also [OnboardingController]. -@ProviderFor(OnboardingController) -final onboardingControllerProvider = - AutoDisposeAsyncNotifierProvider.internal( - OnboardingController.new, - name: r'onboardingControllerProvider', - debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') - ? null - : _$onboardingControllerHash, - dependencies: null, - allTransitiveDependencies: null, -); - -typedef _$OnboardingController = AutoDisposeAsyncNotifier; -// ignore_for_file: type=lint -// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member diff --git a/lib/src/features/onboarding/presentation/onboarding_screen.dart b/lib/src/features/onboarding/presentation/onboarding_screen.dart index 91da7000..ee251046 100644 --- a/lib/src/features/onboarding/presentation/onboarding_screen.dart +++ b/lib/src/features/onboarding/presentation/onboarding_screen.dart @@ -1,13 +1,13 @@ +import 'package:flutter_starter_base_app/src/features/onboarding/presentation/onboarding_controller.dart'; +import 'package:flutter_starter_base_app/src/routing/routes.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:go_router/go_router.dart'; -import 'package:starter_architecture_flutter_firebase/src/common_widgets/primary_button.dart'; -import 'package:starter_architecture_flutter_firebase/src/common_widgets/responsive_center.dart'; -import 'package:starter_architecture_flutter_firebase/src/constants/app_sizes.dart'; -import 'package:starter_architecture_flutter_firebase/src/features/onboarding/presentation/onboarding_controller.dart'; -import 'package:starter_architecture_flutter_firebase/src/localization/string_hardcoded.dart'; -import 'package:starter_architecture_flutter_firebase/src/routing/app_router.dart'; +import 'package:flutter_starter_base_app/src/common_widgets/primary_button.dart'; +import 'package:flutter_starter_base_app/src/common_widgets/responsive_center.dart'; +import 'package:flutter_starter_base_app/src/constants/app_sizes.dart'; +import 'package:flutter_starter_base_app/src/localization/string_hardcoded.dart'; class OnboardingScreen extends ConsumerWidget { const OnboardingScreen({super.key}); @@ -42,9 +42,7 @@ class OnboardingScreen extends ConsumerWidget { onPressed: state.isLoading ? null : () async { - await ref - .read(onboardingControllerProvider.notifier) - .completeOnboarding(); + await ref.read(onboardingControllerProvider.notifier).completeOnboarding(); if (context.mounted) { // go to sign in page after completing onboarding context.goNamed(AppRoute.signIn.name); diff --git a/lib/src/features/onboarding/presentation/splash.dart b/lib/src/features/onboarding/presentation/splash.dart new file mode 100644 index 00000000..4ede12d0 --- /dev/null +++ b/lib/src/features/onboarding/presentation/splash.dart @@ -0,0 +1,49 @@ +import 'package:flutter_starter_base_app/src/common_widgets/primary_button.dart'; +import 'package:flutter_starter_base_app/src/constants/colors.dart'; +import 'package:flutter_starter_base_app/src/localization/generated/locale_keys.g.dart'; +import 'package:flutter_starter_base_app/src/routing/routes.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; + +class SplashView extends StatelessWidget { + const SplashView({super.key}); + + @override + Widget build(BuildContext context) => Scaffold( + body: Container( + decoration: + const BoxDecoration(image: DecorationImage(image: AssetImage('assets/splash.jpg'), fit: BoxFit.cover)), + child: Stack(children: [ + Align( + alignment: Alignment.bottomCenter, + child: Container( + height: 250, + width: double.infinity, + color: CustomColors().whitecolor, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Column(mainAxisAlignment: MainAxisAlignment.end, children: [ + Image.asset('assets/logo.png'), + const SizedBox(height: 20), + PrimaryButton( + text: LocaleKeys.btn_getStarted.tr(), + backgroundColor: CustomColors().lightblueColor, + onPressed: () => context.goNamed(AppRoute.setupSecreen.name)), + const SizedBox(height: 10), + Row(mainAxisAlignment: MainAxisAlignment.center, children: [ + Text('${LocaleKeys.btn_haveAccount.tr()} ', + style: TextStyle( + fontSize: 14, color: CustomColors().lightblueColor, fontWeight: FontWeight.bold)), + TextButton( + onPressed: () => context.pushNamed(AppRoute.signIn.name), + child: Text(LocaleKeys.btn_login.tr(), + style: TextStyle( + fontSize: 14, + color: CustomColors().lightblueColor, + fontWeight: FontWeight.bold))) + ]), + const SizedBox(height: 10) + ])))) + ]))); +} diff --git a/lib/src/features/report/data/report_providers.dart b/lib/src/features/report/data/report_providers.dart new file mode 100644 index 00000000..510816e6 --- /dev/null +++ b/lib/src/features/report/data/report_providers.dart @@ -0,0 +1,21 @@ +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:flutter_starter_base_app/src/features/report/domain/report_data.dart'; +import 'package:flutter_starter_base_app/src/root/domain/item.dart'; +import 'package:riverpod_annotation/riverpod_annotation.dart'; +import 'package:flutter_starter_base_app/src/api/api_facade.dart'; +import 'package:flutter_starter_base_app/src/features/report/domain/report_by.dart'; +import 'package:flutter_starter_base_app/src/features/report/domain/report_frame.dart'; + +part 'report_providers.g.dart'; + +@riverpod +Future> reportData(ReportDataRef ref, {required String timeWindow}) async { + return await (await APIFacade().getApi()).getReportData(timeWindow); +} + + +StateProvider triggerPage = StateProvider((ref) => null); +StateProvider reportBy = StateProvider((ref) => ReportBy.cost); +StateProvider currentTimeWindow = StateProvider((ref) => TimeWindow.values.first); +ReportDataProvider? newReportList = ReportDataProvider(timeWindow: TimeWindow.values.first.timeWindowEum.name); +StateProvider> currentlySelectedVehicleList = StateProvider>((ref) => []); diff --git a/lib/src/features/report/domain/report_by.dart b/lib/src/features/report/domain/report_by.dart new file mode 100644 index 00000000..d519eb40 --- /dev/null +++ b/lib/src/features/report/domain/report_by.dart @@ -0,0 +1 @@ +enum ReportBy { power, cost } diff --git a/lib/src/features/report/domain/report_data.dart b/lib/src/features/report/domain/report_data.dart new file mode 100644 index 00000000..e7383f55 --- /dev/null +++ b/lib/src/features/report/domain/report_data.dart @@ -0,0 +1,24 @@ +import 'package:equatable/equatable.dart'; +import 'package:flutter_starter_base_app/src/root/domain/item.dart'; +import 'package:json_annotation/json_annotation.dart'; +part 'report_data.g.dart'; + +@JsonSerializable(explicitToJson: true) +class ReportData implements Item { + final String label; + final String value; + + final double x; + final double y; + + ReportData({ + required this.label, + required this.value, + required this.x, + required this.y + }); + + factory ReportData.fromJson(Map json) => _$ReportDataFromJson(json); + Map toJson() => _$ReportDataToJson(this); + +} \ No newline at end of file diff --git a/lib/src/features/report/domain/report_frame.dart b/lib/src/features/report/domain/report_frame.dart new file mode 100644 index 00000000..b373b9e1 --- /dev/null +++ b/lib/src/features/report/domain/report_frame.dart @@ -0,0 +1,30 @@ +import 'package:flutter_starter_base_app/src/root/domain/item.dart'; +import 'package:flutter_starter_base_app/src/root/domain/label_value.dart'; +import 'package:json_annotation/json_annotation.dart'; +import 'package:flutter_starter_base_app/src/utils/string_extension.dart'; +part 'report_frame.g.dart'; + +enum TimeWindowEnum { past31Days, thisWeek, thisMonth, thisYear } + +@JsonSerializable(explicitToJson: true) +class TimeWindow extends LabelValuePair with Item { + final TimeWindowEnum timeWindowEum; + const TimeWindow({required super.value, required super.label, required this.timeWindowEum}); + String get name => value.toString().camelCaseToTitleCase(); + + @override + String get displayText => name; + String get localizationKey => "reports.tw.${timeWindowEum.name}"; + LabelValuePair get toLabelValuePair => LabelValuePair(label: name, value: value); + factory TimeWindow.fromLabelValuePair(LabelValuePair labelValuePair) => TimeWindow( + value: labelValuePair.value, + label: labelValuePair.label, + timeWindowEum: TimeWindowEnum.values.firstWhere((e) => labelValuePair.value == e.name)); + factory TimeWindow.fromJson(String json) => + TimeWindow(value: json, label: json, timeWindowEum: $enumDecode(_$TimeWindowEnumEnumMap, json)); + + @override + Map toJson() => _$TimeWindowToJson(this); + static List get values => + TimeWindowEnum.values.map((e) => TimeWindow(value: e.name, label: e.name, timeWindowEum: e)).toList(); +} diff --git a/lib/src/features/report/presentation/bar_background.dart b/lib/src/features/report/presentation/bar_background.dart new file mode 100644 index 00000000..dc08c428 --- /dev/null +++ b/lib/src/features/report/presentation/bar_background.dart @@ -0,0 +1,19 @@ +import 'package:flutter_starter_base_app/src/common_widgets/basic_page_importer.dart'; +import 'package:flutter/widgets.dart'; + +class BarBackgroud extends StatelessWidget { + const BarBackgroud({super.key}); + + @override + Widget build(BuildContext context) => Row( + mainAxisAlignment: MainAxisAlignment.spaceAround, + children: List.generate( + 5, + (_) => Expanded( + child: Container( + decoration: BoxDecoration( + border: Border( + left: BorderSide(color: CustomColors().lightGrayText), + bottom: BorderSide(color: CustomColors().lightGrayText), + right: BorderSide(color: CustomColors().lightGrayText))))))); +} diff --git a/lib/src/features/report/presentation/bar_view.dart b/lib/src/features/report/presentation/bar_view.dart new file mode 100644 index 00000000..a706c920 --- /dev/null +++ b/lib/src/features/report/presentation/bar_view.dart @@ -0,0 +1,85 @@ +import 'package:flutter_starter_base_app/src/features/report/domain/report_data.dart'; +import 'package:flutter_starter_base_app/src/routing/routes.dart'; +import 'package:flutter/gestures.dart'; +import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter_starter_base_app/src/utils/bar_view_calculator.dart'; +import 'package:flutter_starter_base_app/src/features/report/domain/report_by.dart'; +import 'package:flutter_starter_base_app/src/common_widgets/basic_page_importer.dart'; +import 'package:flutter_starter_base_app/src/localization/generated/locale_keys.g.dart'; +import 'package:flutter_starter_base_app/src/features/report/data/report_providers.dart'; +import 'package:flutter_starter_base_app/src/features/report/presentation/bar_background.dart'; + +//todo handle multiple unknown vehicles? +class BarView extends ConsumerWidget { + final List reportData; + + const BarView({super.key, required this.reportData}); + @override + Widget build(BuildContext context, WidgetRef ref) { + ref.watch(triggerPage); + bool reportByCost = ref.read(reportBy) == ReportBy.cost; + + final barViewCalculator = BarViewCalculator( + values: reportData + .map((ReportData reportData) => reportData.x + reportData.y) + .toList()); + return Padding( + padding: const EdgeInsets.all(10), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.only(left: 45, right: 15, bottom: 20), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: barViewCalculator + .calculateMarkings() + .map((String step) => + Text(step, style: TextStyle(color: CustomColors().lighterGrayText, fontSize: 14))) + .toList())) + ] + + reportData.map((ReportData reportData) { + double homeTotal = reportData.x; + double publicTotal = reportData.y; + double total = homeTotal + publicTotal; + double flexTot = barViewCalculator.calculateFlexValue(total); + double flexHome = total <= 0 ? 0 : flexTot * homeTotal / total; + double flexPublic = total <= 0 ? 0 : flexTot * publicTotal / total; + return Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ + Tooltip( + decoration: + BoxDecoration(color: CustomColors().whitecolor, borderRadius: BorderRadius.circular(40)), + triggerMode: TooltipTriggerMode.tap, + richMessage: + TextSpan( + text: LocaleKeys.reports_viewTrips.tr(), + style: const TextStyle(color: Colors.black), + recognizer: TapGestureRecognizer() + ..onTap = () => context.pushNamed(AppRoute.reportTable.name, extra: reportData.label) + ), + child: SizedBox( + height: 27, + child: Stack(children: [ + const BarBackgroud(), + Row(children: [ + if (flexHome > 0) + Flexible( + flex: (flexHome * 100).toInt(), + child: Container(color: CustomColors().reportBarBlue)), + if (flexPublic > 0) + Flexible( + flex: (flexPublic * 100).toInt(), + child: Container(color: CustomColors().reportBarGray)), + Spacer(flex: (((1 - flexTot)) * 100).toInt()) + ]) + ]))), + const SizedBox(height: 10), + Text(reportData.label, style: DefaultTheme().defaultTextStyle(15)), + const SizedBox(height: 20) + ]); + }).toList())); + } +} diff --git a/lib/src/features/report/presentation/bar_view_legend.dart b/lib/src/features/report/presentation/bar_view_legend.dart new file mode 100644 index 00000000..e3bccc42 --- /dev/null +++ b/lib/src/features/report/presentation/bar_view_legend.dart @@ -0,0 +1,21 @@ +import 'package:flutter_starter_base_app/src/localization/generated/locale_keys.g.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_starter_base_app/src/common_widgets/basic_page_importer.dart'; +import 'package:flutter_starter_base_app/src/features/report/presentation/report_subtitle.dart'; + +class BarViewLegend extends StatelessWidget { + const BarViewLegend({super.key}); + + @override + Widget build(BuildContext context) => Row(children: [ + Row(children: [ + Container(height: 10, width: 10, color: CustomColors().reportBarBlue), + ReportSubTitle(text:LocaleKeys.reports_bar_legendHome.tr()) + ]), + Row(children: [ + Container(height: 10, width: 10, color: CustomColors().reportBarGray), + ReportSubTitle(text: LocaleKeys.reports_bar_legendpub.tr()) + ]) + ]); +} diff --git a/lib/src/features/report/presentation/elliptic_button.dart b/lib/src/features/report/presentation/elliptic_button.dart new file mode 100644 index 00000000..e20048b3 --- /dev/null +++ b/lib/src/features/report/presentation/elliptic_button.dart @@ -0,0 +1,45 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:flutter_starter_base_app/src/features/report/domain/report_by.dart'; +import 'package:flutter_starter_base_app/src/common_widgets/basic_page_importer.dart'; +import 'package:flutter_starter_base_app/src/features/report/data/report_providers.dart'; + +class EllipticButton extends ConsumerWidget { + final ReportBy setReportByTo; + final String text; + final bool mirror; + const EllipticButton({super.key, required this.setReportByTo, required this.text, this.mirror = false}); + @override + Widget build(BuildContext context, WidgetRef ref) => Transform.flip( + flipX: mirror, + child: InkWell( + onTap: () { + ref.read(reportBy.notifier).state = setReportByTo; + WidgetsBinding.instance.addPostFrameCallback( + (_) => ref.read(triggerPage.notifier).state = ref.read(currentlySelectedVehicleList)); + }, + child: Container( + height: 30, + width: 60, + decoration: BoxDecoration( + color: ref.watch(reportBy) == setReportByTo ? CustomColors().reportByButtonBG : Colors.transparent, + border: Border.all(color: CustomColors().lightGrayText), + borderRadius: const BorderRadius.only( + topLeft: Radius.elliptical(100, 70), bottomLeft: Radius.elliptical(100, 70))), + child: Transform.flip( + flipX: mirror, + child: Row( + crossAxisAlignment: CrossAxisAlignment.center, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + ref.watch(reportBy) == setReportByTo + ? Row(children: [ + Icon(Icons.check, color: CustomColors().whitecolor, size: 15), + const SizedBox(width: 5) + ]) + : Container(), + Text(text, + textAlign: TextAlign.center, + style: TextStyle(color: CustomColors().whitecolor, fontSize: 11)) + ]))))); +} diff --git a/lib/src/features/report/presentation/report_bar_view.dart b/lib/src/features/report/presentation/report_bar_view.dart new file mode 100644 index 00000000..9cd20a64 --- /dev/null +++ b/lib/src/features/report/presentation/report_bar_view.dart @@ -0,0 +1,27 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:flutter_starter_base_app/src/common_widgets/basic_page_importer.dart'; +import 'package:flutter_starter_base_app/src/features/report/data/report_providers.dart'; +import 'package:flutter_starter_base_app/src/features/report/domain/report_data.dart'; +import 'package:flutter_starter_base_app/src/features/report/presentation/bar_view.dart'; +import 'package:flutter_starter_base_app/src/common_widgets/circular_loading_animation.dart'; + +class ReportBarView extends ConsumerWidget { + final ReportDataProvider? reportDataProvider; + const ReportBarView({super.key, required this.reportDataProvider}); + @override + Widget build(BuildContext context, WidgetRef ref) => reportDataProvider == null + ? Padding( + padding: const EdgeInsets.all(20), + child: Text('Select Vehicles to start', style: DefaultTheme().defaultTextStyle(17))) + : ref.watch(reportDataProvider!).when( + loading: () => const LoadingAnimation(), + data: (reportData) => reportData.isEmpty + ? Container() + : BarView(reportData: reportData), + error: (error, stackTrace) { + WidgetsBinding.instance.addPostFrameCallback((_) => ScaffoldMessenger.of(context) + .showSnackBar(SnackBar(content: Center(child: Text('${(error as Exception)}'))))); + return Container(); + }); +} diff --git a/lib/src/features/report/presentation/report_subtitle.dart b/lib/src/features/report/presentation/report_subtitle.dart new file mode 100644 index 00000000..12f73369 --- /dev/null +++ b/lib/src/features/report/presentation/report_subtitle.dart @@ -0,0 +1,11 @@ +import 'package:flutter_starter_base_app/src/common_widgets/basic_page_importer.dart'; +import 'package:flutter/material.dart'; + +class ReportSubTitle extends StatelessWidget { + final String text; + const ReportSubTitle({super.key, required this.text}); + @override + Widget build(BuildContext context) => Padding( + padding: const EdgeInsets.all(10), + child: Row(children: [Text(text, style: TextStyle(color: CustomColors().lighterGrayText, fontSize: 11))])); +} diff --git a/lib/src/features/report/presentation/report_table_page.dart b/lib/src/features/report/presentation/report_table_page.dart new file mode 100644 index 00000000..ea16305b --- /dev/null +++ b/lib/src/features/report/presentation/report_table_page.dart @@ -0,0 +1,46 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_starter_base_app/src/common_widgets/app_bar.dart'; +import 'package:flutter_starter_base_app/src/common_widgets/basic_page_importer.dart'; +import 'package:flutter_starter_base_app/src/common_widgets/circular_loading_animation.dart'; +import 'package:flutter_starter_base_app/src/features/report/data/report_providers.dart'; +import 'package:flutter_starter_base_app/src/features/report/domain/report_data.dart'; +import 'package:flutter_starter_base_app/src/features/report/presentation/vehicle_report_table.dart'; +import 'package:go_router/go_router.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; + +class ReportTablePage extends ConsumerWidget { + const ReportTablePage({super.key}); + @override + Widget build(BuildContext context, WidgetRef ref) => SafeArea( + child: Scaffold( + appBar: + CustomAppBar(showBackButton: true, titleWidget: Text('Trips', style: DefaultTheme().defaultTextStyle(20))), + body: ref + .watch(ReportDataProvider(timeWindow: ref.watch(currentTimeWindow.notifier).state.timeWindowEum.name)) + .when( + loading: () => const LoadingAnimation(), + error: (error, stackTrace) { + if (context.canPop()) context.pop(); + WidgetsBinding.instance.addPostFrameCallback((_) => + ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Center(child: Text('$error'))))); + return Container(); + }, + data: (List reportData) => Container( + color: CustomColors().darkGray, + child: SafeArea( + child: Scrollbar( + thickness: 0, + trackVisibility: false, + thumbVisibility: false, + child: SingleChildScrollView( + child: Column(crossAxisAlignment: CrossAxisAlignment.stretch, children: [ + Padding( + padding: const EdgeInsets.only(bottom: 15, top: 15), + child: Center( + child: Text(ref.read(currentTimeWindow).displayText, + style: DefaultTheme().defaultTextStyle(20)))), + SizedBox( + height: MediaQuery.of(context).size.height, + child: ReportTable(reportData: reportData)) + ])))))))); +} diff --git a/lib/src/features/report/presentation/reports_page.dart b/lib/src/features/report/presentation/reports_page.dart new file mode 100644 index 00000000..d5e898c0 --- /dev/null +++ b/lib/src/features/report/presentation/reports_page.dart @@ -0,0 +1,82 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter_starter_base_app/src/common_widgets/subsection_title.dart'; +import 'package:flutter_starter_base_app/src/features/report/domain/report_by.dart'; +import 'package:flutter_starter_base_app/src/common_widgets/basic_page_importer.dart'; +import 'package:flutter_starter_base_app/src/features/report/domain/report_frame.dart'; +import 'package:flutter_starter_base_app/src/localization/generated/locale_keys.g.dart'; +import 'package:flutter_starter_base_app/src/features/report/data/report_providers.dart'; +import 'package:flutter_starter_base_app/src/features/report/presentation/report_subtitle.dart'; +import 'package:flutter_starter_base_app/src/features/report/presentation/bar_view_legend.dart'; +import 'package:flutter_starter_base_app/src/features/report/presentation/elliptic_button.dart'; +import 'package:flutter_starter_base_app/src/features/report/presentation/report_bar_view.dart'; +import 'package:flutter_starter_base_app/src/features/report/presentation/time_window_select.dart'; + +class ReportsPage extends ConsumerWidget { + const ReportsPage({super.key}); + + ///todo change trigger future provider by vehicle list => already returns all the vehicles + @override + Widget build(BuildContext context, WidgetRef ref) { + ref.watch(reportBy); + ref.watch(triggerPage); + ref.watch(currentTimeWindow.notifier).stream.listen( + onDone: () => newReportList = null, + (TimeWindow? event) => (ref.read(currentlySelectedVehicleList.notifier).state.isEmpty || event == null) + ? null + : newReportList = ReportDataProvider(timeWindow: event.timeWindowEum.name)); + ref.watch(currentlySelectedVehicleList.notifier).stream.listen(onDone: () => newReportList = null, (_) { + TimeWindow? currentTimeWindowLocal = ref.read(currentTimeWindow); + ref.read(currentlySelectedVehicleList.notifier).state.isEmpty || currentTimeWindowLocal == null + ? newReportList = null + : newReportList = ReportDataProvider(timeWindow: currentTimeWindowLocal.timeWindowEum.name); + }); + return SafeArea( + child: Scaffold( + appBar: CustomAppBar( + titleWidget: Text(LocaleKeys.common_reports.tr(), + style: DefaultTheme().defaultTextStyle(20).copyWith(fontWeight: FontWeight.w500))), + body: CustomScrollView(slivers: [ + SliverFillRemaining( + hasScrollBody: false, + child: Container( + color: CustomColors().darkGray, + child: Column(children: [ + Flexible( + child: Center( + child: Text(LocaleKeys.reports_vehicleEnergyReport.tr(), + style: DefaultTheme().defaultTextStyle(20)))), + Flexible( + flex: 9, + child: Column(children: [ + Padding( + padding: const EdgeInsets.only(left: 10, right: 10), + child: Container( + color: CustomColors().lightGrayColor, + child: Column(children: [ + TimeWindowSingleSelect( + text: LocaleKeys.reports_tw_title.tr(), + description: 'description', + value: ref.watch(currentTimeWindow.notifier).state.displayText), + const Padding( + padding: EdgeInsets.only(left: 10, right: 10), child: PopupMenuDivider()), + Column(children: [ReportBarView(reportDataProvider: newReportList)]), + ReportSubTitle(text: LocaleKeys.reports_info_barSelection.tr()), + const Row(children: [ + Row(children: [ + SizedBox(width: 10), + EllipticButton(text: '\$', setReportByTo: ReportBy.cost), + EllipticButton(text: 'KW', mirror: true, setReportByTo: ReportBy.power), + ]), + Spacer(), + BarViewLegend() + ]), + const SizedBox(height: 10) + ])) + ), + ])) + ]))) + ]))); + } +} diff --git a/lib/src/features/report/presentation/time_window_select.dart b/lib/src/features/report/presentation/time_window_select.dart new file mode 100644 index 00000000..4a6413cd --- /dev/null +++ b/lib/src/features/report/presentation/time_window_select.dart @@ -0,0 +1,42 @@ +import 'package:flutter_starter_base_app/src/root/domain/label_value.dart'; +import 'package:flutter_starter_base_app/src/routing/routes.dart'; +import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter_starter_base_app/src/constants/colors.dart'; +import 'package:flutter_starter_base_app/src/constants/svg_loader.dart'; +import 'package:flutter_starter_base_app/src/constants/theme_data.dart'; +import 'package:flutter_starter_base_app/src/features/report/domain/report_frame.dart'; +import 'package:flutter_starter_base_app/src/features/report/data/report_providers.dart'; + +class TimeWindowSingleSelect extends ConsumerWidget { + final String text; + final String value; + final String description; + const TimeWindowSingleSelect({super.key, required this.text, required this.value, required this.description}); + @override + Widget build(BuildContext context, WidgetRef ref) => Container( + padding: const EdgeInsets.only(top: 7, bottom: 7, left: 17, right: 17), + child: Row(children: [ + Text(text, style: DefaultTheme().defaultTextStyle(18)), + Tooltip( + message: description, + child: Container(padding: const EdgeInsets.only(left: 10), child: SVGLoader().questionMark)), + const Spacer(), + Text( ref.watch(currentTimeWindow).localizationKey.tr(), + style: DefaultTheme().defaultTextStyle(17).copyWith(color: CustomColors().darkGrayText)), + InkWell( + child: InkWell( + child: Container(padding: const EdgeInsets.only(left: 10), child: SVGLoader().rightArrow), + onTap: () async { + LabelValuePair? labelValuePairOrNull = await context.pushNamed(AppRoute.reportTimeWindowSelect.name, + extra: [text, CustomColors().darkGray, TimeWindow.values, ref.read(currentTimeWindow)]); + if (labelValuePairOrNull != null) { + ref.watch(currentTimeWindow.notifier).state = TimeWindow.fromLabelValuePair(labelValuePairOrNull); + WidgetsBinding.instance.addPostFrameCallback( + (_) => ref.read(triggerPage.notifier).state = ref.read(currentTimeWindow)); + } + })) + ])); +} diff --git a/lib/src/features/report/presentation/vehicle_report_table.dart b/lib/src/features/report/presentation/vehicle_report_table.dart new file mode 100644 index 00000000..ea59f28c --- /dev/null +++ b/lib/src/features/report/presentation/vehicle_report_table.dart @@ -0,0 +1,34 @@ +import 'package:flutter/material.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter_starter_base_app/src/features/report/domain/report_data.dart'; +import 'package:flutter_starter_base_app/src/utils/format.dart'; +import 'package:flutter_starter_base_app/src/common_widgets/basic_page_importer.dart'; +import 'package:flutter_starter_base_app/src/localization/generated/locale_keys.g.dart'; + + +class ReportTable extends StatelessWidget { + final List reportData; + const ReportTable({super.key, required this.reportData}); + + @override + Widget build(BuildContext context) => DataTable( + border: TableBorder.symmetric(inside: BorderSide(color: CustomColors().whitecolor, width: .5)), + columns: [ + "X",// LocaleKeys.common_vehicle.tr(), + "Label",// LocaleKeys.common_start.tr(), + "Value"// LocaleKeys.common_time.tr(), + ] + .map((header) => DataColumn( + label: Expanded( + child: Row(mainAxisAlignment: MainAxisAlignment.center, children: [ + Text(header, textAlign: TextAlign.center, style: DefaultTheme().defaultTextStyle(13)) + ])))) + .toList(), + rows: reportData + .map((ReportData reportRow) => DataRow(cells: [ + DataCell(Center(child: Text("Row"))), + DataCell(Center(child: Text(reportRow.label))), + DataCell(Center(child: Text(reportRow.value))) + ])) + .toList()); +} diff --git a/lib/src/localization/asset_handler.dart b/lib/src/localization/asset_handler.dart new file mode 100644 index 00000000..3e572573 --- /dev/null +++ b/lib/src/localization/asset_handler.dart @@ -0,0 +1,89 @@ +import 'dart:convert'; +import 'dart:io'; +import 'package:flutter_starter_base_app/src/api/mock_api.dart'; +import 'package:flutter_starter_base_app/src/localization/localization_service.dart'; +import 'package:flutter_starter_base_app/src/utils/translation_storage.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter/material.dart'; + +class AssetHandler extends AssetLoader { + static final Map> _cachedData = {}; + static final Set _apiCalledLocales = {}; + + //TODO: Handle multiple calls due to use of fallback translation property + @override + Future> load(String path, Locale locale) async { + final localeKey = _getLocaleString(locale); + + try { + // Check if domain for this locale is already cached in memory + if (_cachedData.containsKey(localeKey)) { + return _cachedData[localeKey]!; + } + + // If API call hasn't been made for this locale, try API + if (!_apiCalledLocales.contains(localeKey)) { + final apiData = await _loadFromApi(locale); + if (apiData != null) { + _cachedData[localeKey] = apiData; + _apiCalledLocales.add(localeKey); + return _cachedData[localeKey]!; + } + } + + final translationStorage = TranslationStorage(locale: locale); + final localFilePath = await translationStorage.localizedFilePath; + final localFile = File('$localFilePath/$localeKey.json'); + + // Try to load from local file + if (await localFile.exists()) { + final localData = await translationStorage.readTranslationFile(); + _cachedData[localeKey] = json.decode(localData); + return _cachedData[localeKey]!; + } + + // Fallback to asset + _cachedData[localeKey] = await _loadFromAsset(path, locale); + return _cachedData[localeKey]!; + } catch (e) { + debugPrint(e.toString()); + // Fallback to fallback language asset + final fallbackLocale = LocalizationService.getFallbackLocale(); + + return await _loadFromAsset(path, fallbackLocale); + } + } + + Future?> _loadFromApi(Locale locale) async { + final localeKey = _getLocaleString(locale); + try { + final data = await APIMock().getInformationText(localeKey); + if (data != null) { + // Save the domain to local storage + final translationStorage = TranslationStorage(locale: locale); + await translationStorage.writeTranslationFile(json.encode(data)); + return data; + } + return null; + } catch (e) { + debugPrint('Error loading from API: $e'); + return null; + } + } + + Future> _loadFromAsset(String path, Locale locale) async { + try { + final assetPath = '$path/${_getLocaleString(locale)}.json'; + final data = await rootBundle.loadString(assetPath); + return json.decode(data); + } catch (e) { + debugPrint(e.toString()); + throw Exception('Failed to load asset file'); + } + } + + String _getLocaleString(Locale locale) { + return '${locale.languageCode}-${locale.countryCode}'; + } +} diff --git a/lib/src/localization/localization_service.dart b/lib/src/localization/localization_service.dart new file mode 100644 index 00000000..aa60a51f --- /dev/null +++ b/lib/src/localization/localization_service.dart @@ -0,0 +1,57 @@ +import 'dart:ui'; +import 'package:flutter/material.dart'; +import 'package:easy_localization/easy_localization.dart'; + +class LocalizationService { + static const List _supportedLanguages = [Locale('en', 'US'), Locale('es', 'MX'), Locale('fr', 'CA')]; + static const Locale _fallbackLanguage = Locale('en', 'US'); + + static Locale getDeviceLocale() { + final platformDispatcher = PlatformDispatcher.instance; + final deviceLocales = platformDispatcher.locales; + debugPrint("Raw device locales: $deviceLocales"); + + // Try to find a supported locale + for (var deviceLocale in deviceLocales) { + // Check for exact match + if (_supportedLanguages.contains(deviceLocale)) { + debugPrint("Using device locale: $deviceLocale"); + return deviceLocale; + } + + // Check for language match + final matchingLocale = + _supportedLanguages.where((locale) => locale.languageCode == deviceLocale.languageCode).firstOrNull; + + if (matchingLocale != null) { + debugPrint("Using matched locale: $matchingLocale"); + return matchingLocale; + } + } + + // If no match found, use fallback + debugPrint("Using fallback locale: $_fallbackLanguage"); + return _fallbackLanguage; + } + + static Future setLocaleFromDevice(BuildContext context) async { + final deviceLocale = getDeviceLocale(); + await setLocale(context, deviceLocale); + } + + static setLocale(BuildContext context, Locale locale) async { + await context.setLocale(locale); + } + + static Locale getFallbackLocale() { + return _fallbackLanguage; + } + + static Locale getCurrentLocale(BuildContext context) { + return context.locale; + } + + static List getSupportedLocales() { + return _supportedLanguages; + } +} diff --git a/lib/src/root/data/providers.dart b/lib/src/root/data/providers.dart new file mode 100644 index 00000000..02ebc15a --- /dev/null +++ b/lib/src/root/data/providers.dart @@ -0,0 +1,12 @@ +import 'package:flutter_starter_base_app/src/root/domain/contact.dart'; +import 'package:riverpod_annotation/riverpod_annotation.dart'; +import 'package:flutter_starter_base_app/src/api/api.dart'; +part 'providers.g.dart'; + +@riverpod +Future> getData(GetDataRef ref) async { + return await API().getData(); +} + + + diff --git a/lib/src/root/domain/account.dart b/lib/src/root/domain/account.dart new file mode 100644 index 00000000..931dfad1 --- /dev/null +++ b/lib/src/root/domain/account.dart @@ -0,0 +1,30 @@ +import 'package:json_annotation/json_annotation.dart'; +part 'account.g.dart'; + +@JsonSerializable(explicitToJson: true) +class AccountDetails { + String phoneNumber; + String emailId; + + AccountDetails({required this.emailId, required this.phoneNumber}); + factory AccountDetails.fromJson(Map json) => _$AccountDetailsFromJson(json); + Map toJson() => _$AccountDetailsToJson(this); +} + +@JsonSerializable() +class AccountVehicle { + final String vin; + final String vehicleName; + final String year; + final String make; + final String model; + AccountVehicle({ + required this.make, + required this.model, + required this.vehicleName, + required this.vin, + required this.year, + }); + factory AccountVehicle.fromJson(Map json) => _$AccountVehicleFromJson(json); + Map toJson() => _$AccountVehicleToJson(this); +} diff --git a/lib/src/root/domain/basic_api_response.dart b/lib/src/root/domain/basic_api_response.dart new file mode 100644 index 00000000..14ec0bc3 --- /dev/null +++ b/lib/src/root/domain/basic_api_response.dart @@ -0,0 +1,16 @@ +import 'package:json_annotation/json_annotation.dart'; +part 'basic_api_response.g.dart'; + +@JsonSerializable() +class APIResponse { + + factory APIResponse.failed() => APIResponse(message: "Failed", status: "500"); + factory APIResponse.success() => APIResponse(message: "Success", status: "200"); + + final String message; + final dynamic status; + APIResponse({required this.message, required this.status}); + factory APIResponse.fromJson(Map json) => _$APIResponseFromJson(json); + Map toJson() => _$APIResponseToJson(this); + +} diff --git a/lib/src/root/domain/contact.dart b/lib/src/root/domain/contact.dart new file mode 100644 index 00000000..f66fb541 --- /dev/null +++ b/lib/src/root/domain/contact.dart @@ -0,0 +1,34 @@ +import 'package:json_annotation/json_annotation.dart'; +part 'contact.g.dart'; + +@JsonSerializable() +class Contact { + final String id; + final String firstname; + final String lastname; + final String email; + final String phoneNumber; + final String address; + final String city; + final String state; + final String zipcode; + final String company; + + Contact({ + required this.id, + required this.firstname, + required this.lastname, + required this.email, + required this.phoneNumber, + required this.address, + required this.city, + required this.state, + required this.zipcode, + required this.company + }); + + factory Contact.fromJson(Map json) => + _$ContactFromJson(json); + Map toJson() => _$ContactToJson(this); +} + \ No newline at end of file diff --git a/lib/src/root/domain/country_data.dart b/lib/src/root/domain/country_data.dart new file mode 100644 index 00000000..8856960c --- /dev/null +++ b/lib/src/root/domain/country_data.dart @@ -0,0 +1,38 @@ +import 'package:flutter_starter_base_app/src/root/domain/item.dart'; +import 'package:json_annotation/json_annotation.dart'; +part 'country_data.g.dart'; + +@JsonSerializable() +class Country with Item { + final String name; + final String code; + final String telephoneCode; + Country({ + required this.code, + required this.name, + required this.telephoneCode, + }); + + factory Country.fromJson(Map json) => _$CountryFromJson(json); + Map toJson() => _$CountryToJson(this); + + @override + String get label => name; + @override + String get value => code; +} + +@JsonSerializable() +class State with Item { + final String name; + final String code; + State({required this.code, required this.name}); + + factory State.fromJson(Map json) => _$StateFromJson(json); + Map toJson() => _$StateToJson(this); + + @override + String get label => name; + @override + String get value => code; +} diff --git a/lib/src/root/domain/item.dart b/lib/src/root/domain/item.dart new file mode 100644 index 00000000..85c56b39 --- /dev/null +++ b/lib/src/root/domain/item.dart @@ -0,0 +1,4 @@ +mixin Item { + String get label; + T get value; +} diff --git a/lib/src/root/domain/label_value.dart b/lib/src/root/domain/label_value.dart new file mode 100644 index 00000000..78c49bf5 --- /dev/null +++ b/lib/src/root/domain/label_value.dart @@ -0,0 +1,22 @@ +import 'package:equatable/equatable.dart'; +import 'package:flutter_starter_base_app/src/root/domain/item.dart'; +import 'package:json_annotation/json_annotation.dart'; +part 'label_value.g.dart'; + +@JsonSerializable(explicitToJson: true) +class LabelValuePair extends Equatable implements Item { + final String label; + final dynamic value; + const LabelValuePair({required this.label, required this.value}); + factory LabelValuePair.fromJson(Map json) => _$LabelValuePairFromJson(json); + Map toJson() => _$LabelValuePairToJson(this); + + @override + List get props => [value]; + + @override + bool get stringify => true; + + String get displayText => label; +} + diff --git a/lib/src/root/domain/user.dart b/lib/src/root/domain/user.dart new file mode 100644 index 00000000..c0feca77 --- /dev/null +++ b/lib/src/root/domain/user.dart @@ -0,0 +1,7 @@ +class User { + final String emailId; + //with country code + final String phone; + String? dongleId; + User({required this.emailId, required this.phone, this.dongleId}); +} diff --git a/lib/src/root/domain/wifi_details.dart b/lib/src/root/domain/wifi_details.dart new file mode 100644 index 00000000..0cb31481 --- /dev/null +++ b/lib/src/root/domain/wifi_details.dart @@ -0,0 +1,6 @@ +class WifiDetails { + final String ssid; + final String mac; + final String ip; + WifiDetails({required this.ssid, required this.mac, required this.ip}); +} diff --git a/lib/src/root/presentation/accounts_page.dart b/lib/src/root/presentation/accounts_page.dart new file mode 100644 index 00000000..4600db7d --- /dev/null +++ b/lib/src/root/presentation/accounts_page.dart @@ -0,0 +1,368 @@ +import 'package:flutter_starter_base_app/src/common_widgets/basic_page_importer.dart'; +import 'package:flutter_starter_base_app/src/common_widgets/circular_loading_animation.dart'; +import 'package:flutter_starter_base_app/src/common_widgets/primary_button.dart'; +import 'package:flutter_starter_base_app/src/common_widgets/subsection_title.dart'; +import 'package:flutter_starter_base_app/src/features/account/data/account_provider.dart'; +import 'package:flutter_starter_base_app/src/localization/generated/locale_keys.g.dart'; +import 'package:flutter_starter_base_app/src/routing/routes.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:email_validator/email_validator.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'dart:ui' as ui; +import 'package:go_router/go_router.dart'; + +class AccountDetailsPage extends ConsumerStatefulWidget { + const AccountDetailsPage({super.key}); + + @override + ConsumerState createState() => _AccountDetailsPageState(); +} + +class _AccountDetailsPageState extends ConsumerState { + final phoneNumberController = TextEditingController(); + final emailController = TextEditingController(); + final _formKey = GlobalKey(); + bool isEditingPhone = false; + bool isEditingEmail = false; + + Future _saveAccountDetails({required String phoneNumber, required String emailId}) async { + try { + final future = ref.read(saveAccountDetailsProvider(emailId: emailId, phoneNumber: phoneNumber).future); + + future.then((response) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Center(child: Text(response.message)), + ), + ); + }).catchError((error) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Center(child: Text('Error: $error')), + ), + ); + }); + } catch (e) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + backgroundColor: CustomColors().darkestGrayBG, + content: Center(child: Text('Failed to save account details: $e')), + ), + ); + } + } + + @override + Widget build(BuildContext context) { + final accountDetailsAsync = ref.watch(fetchAccountDetailsProvider); + + return Scaffold( + appBar: CustomAppBar( + titleWidget: Text(LocaleKeys.common_account.tr(), + style: DefaultTheme().defaultTextStyle(20).copyWith(fontWeight: FontWeight.w500))), + body: accountDetailsAsync.when( + loading: () => const Scaffold( + body: Directionality( + textDirection: ui.TextDirection.ltr, + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [Text('Loading Account Details'), LoadingAnimation()]))), + error: (error, stack) { + return Center(child: Text('Error: $error')); + }, + data: (accountDetails) { + phoneNumberController.text = accountDetails.phoneNumber; + emailController.text = accountDetails.emailId; + return Form( + key: _formKey, + child: Column( + children: [ + Expanded( + child: ListView( + padding: const EdgeInsets.all(16), + children: [ + SubSectionTitle(text: LocaleKeys.account_wizard_contactInformationDesc.tr().toUpperCase()), + GestureDetector( + onTap: () { + setState(() { + isEditingPhone = !isEditingPhone; + }); + }, + child: Container( + height: 44, + decoration: BoxDecoration( + color: CustomColors().grayColor, + border: Border( + bottom: BorderSide( + color: CustomColors().grayColor, + width: 1.0, + ), + ), + ), + child: Padding( + padding: const EdgeInsets.only(left: 16.0, right: 16), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + LocaleKeys.account_phoneNumber.tr(), + style: TextStyle( + color: CustomColors().whitecolor, + fontSize: 17, + ), + ), + if (isEditingPhone) + Expanded( + child: TextFormField( + controller: phoneNumberController, + textAlign: TextAlign.right, + style: TextStyle( + color: CustomColors().secondaryDark, + fontSize: 17, + ), + decoration: InputDecoration( + hintText: LocaleKeys.hint_phoneNumber.tr(), + hintStyle: TextStyle( + color: CustomColors().secondaryDark, + ), + border: InputBorder.none, + ), + keyboardType: TextInputType.phone, + onChanged: (value) { + setState(() { + accountDetails.phoneNumber = value; + }); + }, + onEditingComplete: () { + if (_formKey.currentState!.validate()) { + _saveAccountDetails( + phoneNumber: accountDetails.phoneNumber, + emailId: accountDetails.emailId, + ); + setState(() { + isEditingPhone = false; + }); + } + }, + validator: (value) { + if (value == null || value.isEmpty) { + return 'Please enter a phone number'; + } + final phoneRegExp = RegExp(r'^\d{10}$'); + if (!phoneRegExp.hasMatch(value)) { + return 'Please enter a valid 10-digit phone number'; + } + return null; + }, + ), + ) + else + Expanded( + child: Text( + accountDetails.phoneNumber, + maxLines: 1, + textAlign: TextAlign.right, + style: TextStyle( + color: CustomColors().secondaryDark, + fontSize: 17, + ), + ), + ), + ], + ), + ), + ), + ), + GestureDetector( + onTap: () { + setState(() { + isEditingEmail = !isEditingEmail; + }); + }, + child: Container( + height: 44, + decoration: BoxDecoration( + color: CustomColors().grayColor, + border: Border( + bottom: BorderSide( + color: CustomColors().grayColor, + width: 1.0, + ), + ), + ), + child: Padding( + padding: const EdgeInsets.only(left: 16.0, right: 16), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + LocaleKeys.account_email.tr(), + style: TextStyle( + color: CustomColors().whitecolor, + fontSize: 17, + ), + ), + Padding( + padding: const EdgeInsets.only(left: 10.0), + child: Icon( + Icons.help_rounded, + color: CustomColors().lightblueColor, + size: 15, + ), + ), + // Spacer(), + const SizedBox( + width: 20, + ), + if (isEditingEmail) + Expanded( + child: TextFormField( + controller: emailController, + textAlign: TextAlign.right, + style: TextStyle( + color: CustomColors().secondaryDark, + fontSize: 17, + ), + decoration: InputDecoration( + hintText: 'Enter email', + hintStyle: TextStyle( + color: CustomColors().secondaryDark, + ), + border: InputBorder.none, + ), + keyboardType: TextInputType.emailAddress, + onChanged: (value) { + setState(() { + accountDetails.emailId = value; + }); + }, + onEditingComplete: () { + if (_formKey.currentState!.validate()) { + _saveAccountDetails( + phoneNumber: accountDetails.phoneNumber, + emailId: accountDetails.emailId); + setState(() { + isEditingEmail = false; + }); + } + }, + validator: (value) => + EmailValidator.validate(value ?? '') ? null : "Please enter a valid email", + ), + ) + else + Expanded( + child: Text( + accountDetails.emailId, + textAlign: TextAlign.right, + overflow: TextOverflow.ellipsis, + style: TextStyle( + color: CustomColors().secondaryDark, + fontSize: 17, + ), + ), + ), + ], + ), + ), + ), + ), + InkWell( + onTap: () async { + final result = await context.push( + '/${AppRoute.addHousehold.name}', + ); + + if (result == true) { + ref.invalidate(fetchAccountDetailsProvider); + ref.read(fetchAccountDetailsProvider); + debugPrint("refresh = "); + } + }, + child: Container( + height: 44, + decoration: BoxDecoration(color: CustomColors().grayColor), + child: Padding( + padding: const EdgeInsets.only(left: 15.0), + child: Row( + children: [ + Icon( + Icons.add_circle, + color: CustomColors().lightblueColor, + ), + Padding( + padding: const EdgeInsets.only(left: 10.0), + child: Text( + LocaleKeys.btn_addHousehold.tr(), + style: TextStyle( + color: CustomColors().lightblueColor, + fontSize: 17, + ), + ), + ), + ], + ), + ), + ), + ), + InkWell( + onTap: () async { + final result = await context.push( + '/${AppRoute.addVehicle.name}', + ); + + if (result == true) { + ref.invalidate(fetchAccountDetailsProvider); + ref.read(fetchAccountDetailsProvider); + debugPrint("refresh = "); + } + }, + child: Container( + height: 44, + decoration: BoxDecoration(color: CustomColors().grayColor), + child: Padding( + padding: const EdgeInsets.only(left: 15.0), + child: Row( + children: [ + Icon( + Icons.add_circle, + color: CustomColors().lightblueColor, + ), + Padding( + padding: const EdgeInsets.only(left: 10.0), + child: Text( + LocaleKeys.vehicle_wizard_addVehicleTitle.tr(), + style: TextStyle( + color: CustomColors().lightblueColor, + fontSize: 17, + ), + ), + ), + ], + ), + ), + ), + ), + ], + ), + ), + Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + children: [ + PrimaryButton( + onPressed: () => context.goNamed(AppRoute.logoutPageTransition.name), + backgroundColor: CustomColors().lightblueColor, + text: LocaleKeys.btn_logout.tr(), + ), + ], + ), + ), + ], + )); + }, + ), + ); + } +} diff --git a/lib/src/root/presentation/contact_view.dart b/lib/src/root/presentation/contact_view.dart new file mode 100644 index 00000000..dae8c3b2 --- /dev/null +++ b/lib/src/root/presentation/contact_view.dart @@ -0,0 +1,29 @@ +import 'package:flutter/material.dart'; +import 'package:carousel_slider/carousel_slider.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:flutter_starter_base_app/src/common_widgets/basic_page_importer.dart'; +import 'package:flutter_starter_base_app/src/root/data/providers.dart'; +import 'package:flutter_starter_base_app/src/root/domain/contact.dart'; + +final listItemSelectorProvider = StateProvider((ref) => 0); + +class ContactView extends ConsumerWidget { + final Contact contact; + + const ContactView({super.key, required this.contact}); + + @override + Widget build(BuildContext context, WidgetRef ref) => + Center(child: + Column( + children: [ + Text("First Name:" + contact.firstname + " Lastname: " + + contact.lastname), + Text("Address:"), + Text(contact.address), + Text(contact.city + ", " + contact.state + " " + contact.zipcode) + ], + ) + ); + +} diff --git a/lib/src/root/presentation/home_page.dart b/lib/src/root/presentation/home_page.dart new file mode 100644 index 00000000..35788bcd --- /dev/null +++ b/lib/src/root/presentation/home_page.dart @@ -0,0 +1,66 @@ +import 'package:flutter/material.dart'; +import 'package:carousel_slider/carousel_slider.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:flutter_starter_base_app/src/common_widgets/basic_page_importer.dart'; +import 'package:flutter_starter_base_app/src/root/data/providers.dart'; +import 'package:flutter_starter_base_app/src/root/domain/contact.dart'; +import 'package:flutter_starter_base_app/src/root/presentation/contact_view.dart'; + +final listItemSelectorProvider = StateProvider((ref) => 0); + +class HomePage extends ConsumerWidget { + const HomePage({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) => RefreshIndicator( + color: CustomColors().primaryTextColor, + backgroundColor: CustomColors().darkGrayBG, + onRefresh: () async => ref.refresh(getDataProvider.future), + child: ref.watch(getDataProvider).when( + error: (Object error, StackTrace stackTrace) { + WidgetsBinding.instance.addPostFrameCallback( + (_) => ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Center(child: Text('$error'))))); + return const Scaffold( + appBar: CustomAppBar(titleWidget: Text('Cannot Load')), + ); + }, + loading: () { + return const Scaffold(body: Directionality(textDirection: TextDirection.ltr, child: SizedBox())); + }, + data: (List contact) { + return Scaffold( + appBar: CustomAppBar( + titleWidget: Text("Contacts", + style: DefaultTheme().defaultTextStyle(20).copyWith(fontWeight: FontWeight.w500)), + automaticallyImplyLeading: false), + body: CustomScrollView(primary: true, scrollDirection: Axis.vertical, slivers: [ + SliverFillViewport( + delegate: SliverChildBuilderDelegate( + childCount: 1, + (context, index) => Column(children: [ + Flexible( + child: CarouselSlider( + items: contact.map((e) => ContactView(contact: e)).toList(), + options: CarouselOptions( + initialPage: ref.watch(listItemSelectorProvider), + viewportFraction: 1, + enableInfiniteScroll: false, + height: MediaQuery.of(context).size.height))), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: contact + .asMap() + .entries + .map((entry) => Container( + width: 7.0, + height: 7.0, + margin: const EdgeInsets.symmetric(vertical: 6.0, horizontal: 4.0), + decoration: BoxDecoration( + shape: BoxShape.circle, + color: Colors.white.withOpacity( + ref.watch(listItemSelectorProvider) == entry.key ? 0.9 : 0.4)))) + .toList()) + ]))), + ])); + })); +} diff --git a/lib/src/root/presentation/setup_screen.dart b/lib/src/root/presentation/setup_screen.dart new file mode 100644 index 00000000..e70c0e6b --- /dev/null +++ b/lib/src/root/presentation/setup_screen.dart @@ -0,0 +1,113 @@ +import 'package:flutter_starter_base_app/src/common_widgets/action_text_button.dart'; +import 'package:flutter_starter_base_app/src/common_widgets/basic_page_importer.dart'; +import 'package:flutter_starter_base_app/src/localization/generated/locale_keys.g.dart'; +import 'package:flutter_starter_base_app/src/routing/routes.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; +import 'package:shared_preferences/shared_preferences.dart'; + +class SetupScreen extends StatefulWidget { + const SetupScreen({super.key}); + + @override + SetupScreenState createState() => SetupScreenState(); +} + +class SetupScreenState extends State { + int _completedStep = -1; + + @override + void initState() { + super.initState(); + _loadCompletedStep(); + } + + Future _loadCompletedStep() async { + _completedStep = (await SharedPreferences.getInstance()).getInt('completedStep') ?? -1; + setState(() {}); + } + + Future _saveCompletedStep(int step) async => + (await SharedPreferences.getInstance()).setInt('completedStep', step); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: CustomAppBar( + titleWidget: Text(LocaleKeys.oob_setup.tr()), + showHamburgerMenu: false, + ), + body: Padding( + padding: const EdgeInsets.symmetric(vertical: 16), + child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Text(LocaleKeys.oob_setupGuideline.tr(), + style: TextStyle(fontSize: 18, color: CustomColors().whitecolor), textAlign: TextAlign.center)), + const SizedBox(height: 20), + _buildSetupBox(0, LocaleKeys.account_wizard_title.tr(), LocaleKeys.oob_accountWizDesc.tr()), + const SizedBox(height: 10), + _buildSetupBox(1, LocaleKeys.charger_wizard_addChargerTitle.tr(), LocaleKeys.oob_chargerWizDesc.tr()), + const SizedBox(height: 10), + _buildSetupBox(2, LocaleKeys.vehicle_wizard_addVehicleTitle.tr(), LocaleKeys.oob_vehicleWizDesc.tr()), + const Spacer(), + Center( + child: ActionTextButton( + text: LocaleKeys.common_cancel.tr(), onPressed: () => context.goNamed(AppRoute.splash.name))) + ]))); + } + + Widget _buildSetupBox(int index, String title, String subtitle) { + bool isCompleted = index <= _completedStep; + bool isNextStep = index == _completedStep + 1; + Color boxColor; + Color textColor; + IconData icon; + + if (isCompleted) { + boxColor = CustomColors().reportByButtonBG; + textColor = CustomColors().setUpText; + icon = Icons.check; + } else if (isNextStep) { + boxColor = CustomColors().primaryTextColor; + textColor = CustomColors().secondaryButtonTextColor; + icon = Icons.arrow_forward; + } else { + boxColor = CustomColors().lightGrayColor; + textColor = CustomColors().secondaryDark; + icon = Icons.arrow_forward; + } + + return GestureDetector( + onTap: () async { + dynamic result; + switch (index) { + case 0: + result = await context.pushNamed(AppRoute.addAccount.name); + case 1: + result = await context.pushNamed(AppRoute.addCharger.name); + case 2: + result = await context.pushNamed(AppRoute.addVehicle.name); + } + + if (result == true) { + setState(() => _completedStep = index); + _saveCompletedStep(index); + if (index == 2 && mounted) context.pushReplacementNamed(AppRoute.setupComplete.name); + } + }, + child: Container( + padding: const EdgeInsets.all(16.0), + decoration: BoxDecoration(color: boxColor), + child: Row(children: [ + Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ + Text(title, style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold, color: textColor)), + const SizedBox(height: 4), + Text(subtitle, style: TextStyle(fontSize: 14, color: textColor)) + ]), + const Spacer(), + Icon(icon, color: textColor, size: 30) + ]))); + } +} diff --git a/lib/src/root/presentation/setup_success_page.dart b/lib/src/root/presentation/setup_success_page.dart new file mode 100644 index 00000000..1c98dec7 --- /dev/null +++ b/lib/src/root/presentation/setup_success_page.dart @@ -0,0 +1,38 @@ +import 'package:flutter_starter_base_app/src/common_widgets/basic_page_importer.dart'; +import 'package:flutter_starter_base_app/src/common_widgets/primary_button.dart'; +import 'package:flutter_starter_base_app/src/routing/routes.dart'; +import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; + +class SetupCompletePage extends StatelessWidget { + const SetupCompletePage({super.key}); + + @override + Widget build(BuildContext context) => Scaffold( + body: Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + const SizedBox(height: 18), + Text('Setup', + style: TextStyle(fontSize: 34, fontWeight: FontWeight.w400, color: CustomColors().whitecolor), + textAlign: TextAlign.center), + const SizedBox(height: 16), + Text('Congratulations!', + style: TextStyle(fontSize: 28, fontWeight: FontWeight.w400, color: CustomColors().whitecolor), + textAlign: TextAlign.center), + const SizedBox(height: 16), + Image.asset('assets/group.png'), + const SizedBox(height: 16), + Text('Your initial setup is complete.', + style: TextStyle(fontSize: 20, fontWeight: FontWeight.w400, color: CustomColors().whitecolor), + textAlign: TextAlign.center), + const Spacer(), + PrimaryButton( + text: 'Go To App Home', + backgroundColor: CustomColors().lightblueColor, + onPressed: () => context.goNamed(AppRoute.home.name)) + ]))); +} diff --git a/lib/src/routing/app_router.dart b/lib/src/routing/app_router.dart index b9e29995..35173a76 100644 --- a/lib/src/routing/app_router.dart +++ b/lib/src/routing/app_router.dart @@ -1,230 +1,125 @@ +import 'package:flutter_starter_base_app/src/common_widgets/basic_page_importer.dart'; +import 'package:flutter_starter_base_app/src/common_widgets/country_select/presentation/country_select.dart'; +import 'package:flutter_starter_base_app/src/common_widgets/state_select/presentation/state_select.dart'; +import 'package:flutter_starter_base_app/src/features/account/presentation/eula_transition.dart'; +import 'package:flutter_starter_base_app/src/features/account/presentation/eula_view.dart'; +import 'package:flutter_starter_base_app/src/features/login/presentation/check_eula.dart'; +import 'package:flutter_starter_base_app/src/features/login/presentation/forgot_password_page.dart'; +import 'package:flutter_starter_base_app/src/features/login/presentation/login_page.dart'; +import 'package:flutter_starter_base_app/src/features/login/presentation/login_transition.dart'; +import 'package:flutter_starter_base_app/src/features/login/presentation/logout_transition.dart'; +import 'package:flutter_starter_base_app/src/features/login/presentation/reset_password_page.dart'; +import 'package:flutter_starter_base_app/src/features/login/presentation/reset_password_success_page.dart'; +import 'package:flutter_starter_base_app/src/features/login/presentation/startup.dart'; +import 'package:flutter_starter_base_app/src/features/onboarding/presentation/splash.dart'; +import 'package:flutter_starter_base_app/src/features/report/presentation/report_table_page.dart'; +import 'package:flutter_starter_base_app/src/features/report/presentation/reports_page.dart'; +import 'package:flutter_starter_base_app/src/root/domain/item.dart'; +import 'package:flutter_starter_base_app/src/root/presentation/accounts_page.dart'; +import 'package:flutter_starter_base_app/src/root/presentation/setup_screen.dart'; +import 'package:flutter_starter_base_app/src/root/presentation/setup_success_page.dart'; +import 'package:flutter_starter_base_app/src/routing/routes.dart'; import 'package:flutter/material.dart'; -import 'package:riverpod_annotation/riverpod_annotation.dart'; -import 'package:starter_architecture_flutter_firebase/src/routing/app_startup.dart'; -import 'package:starter_architecture_flutter_firebase/src/features/authentication/data/firebase_auth_repository.dart'; -import 'package:starter_architecture_flutter_firebase/src/features/authentication/presentation/custom_profile_screen.dart'; -import 'package:starter_architecture_flutter_firebase/src/features/authentication/presentation/custom_sign_in_screen.dart'; -import 'package:starter_architecture_flutter_firebase/src/features/entries/presentation/entries_screen.dart'; -import 'package:starter_architecture_flutter_firebase/src/features/entries/domain/entry.dart'; -import 'package:starter_architecture_flutter_firebase/src/features/jobs/domain/job.dart'; -import 'package:starter_architecture_flutter_firebase/src/features/entries/presentation/entry_screen/entry_screen.dart'; -import 'package:starter_architecture_flutter_firebase/src/features/jobs/presentation/job_entries_screen/job_entries_screen.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:go_router/go_router.dart'; -import 'package:starter_architecture_flutter_firebase/src/features/jobs/presentation/edit_job_screen/edit_job_screen.dart'; -import 'package:starter_architecture_flutter_firebase/src/features/jobs/presentation/jobs_screen/jobs_screen.dart'; -import 'package:starter_architecture_flutter_firebase/src/features/onboarding/data/onboarding_repository.dart'; -import 'package:starter_architecture_flutter_firebase/src/features/onboarding/presentation/onboarding_screen.dart'; -import 'package:starter_architecture_flutter_firebase/src/routing/go_router_refresh_stream.dart'; -import 'package:starter_architecture_flutter_firebase/src/routing/not_found_screen.dart'; -import 'package:starter_architecture_flutter_firebase/src/routing/scaffold_with_nested_navigation.dart'; - +import 'package:riverpod_annotation/riverpod_annotation.dart'; part 'app_router.g.dart'; -// private navigators -final _rootNavigatorKey = GlobalKey(); -final _jobsNavigatorKey = GlobalKey(debugLabel: 'jobs'); -final _entriesNavigatorKey = GlobalKey(debugLabel: 'entries'); -final _accountNavigatorKey = GlobalKey(debugLabel: 'account'); +final currentMainRouteIndexProvider = StateProvider((ref) => 0); +final _rootNavigatorKey = GlobalKey(debugLabel: 'root'); +final _shellNavigatorKey = GlobalKey(debugLabel: 'nav'); -enum AppRoute { - onboarding, - signIn, - jobs, - job, - addJob, - editJob, - entry, - addEntry, - editEntry, - entries, - profile, -} +List get bottomNavRoutes => [AppRoute.home, AppRoute.account, AppRoute.reports]; -@riverpod -GoRouter goRouter(GoRouterRef ref) { - // rebuild GoRouter when app startup state changes - final appStartupState = ref.watch(appStartupProvider); - final authRepository = ref.watch(authRepositoryProvider); - return GoRouter( - initialLocation: '/signIn', - navigatorKey: _rootNavigatorKey, - debugLogDiagnostics: true, - redirect: (context, state) { - // If the app is still initializing, show the /startup route - if (appStartupState.isLoading || appStartupState.hasError) { - return '/startup'; - } - final onboardingRepository = - ref.read(onboardingRepositoryProvider).requireValue; - final didCompleteOnboarding = onboardingRepository.isOnboardingComplete(); - final path = state.uri.path; - if (!didCompleteOnboarding) { - // Always check state.subloc before returning a non-null route - // https://github.com/flutter/packages/blob/main/packages/go_router/example/lib/redirection.dart#L78 - if (path != '/onboarding') { - return '/onboarding'; - } - return null; - } - final isLoggedIn = authRepository.currentUser != null; - if (isLoggedIn) { - if (path.startsWith('/startup') || - path.startsWith('/onboarding') || - path.startsWith('/signIn')) { - return '/jobs'; - } - } else { - if (path.startsWith('/startup') || - path.startsWith('/onboarding') || - path.startsWith('/jobs') || - path.startsWith('/entries') || - path.startsWith('/account')) { - return '/signIn'; - } - } - return null; +/// todo meta objects can be serialized +final routes = [ + GoRoute(name: AppRoute.start.name, path: '/${AppRoute.start.name}', builder: (context, state) => const Startup()), + GoRoute( + name: AppRoute.splash.name, path: '/${AppRoute.splash.name}', builder: (context, state) => const SplashView()), + GoRoute(name: AppRoute.signIn.name, path: '/${AppRoute.signIn.name}', builder: (context, state) => const LoginPage()), + GoRoute( + name: AppRoute.checkEULA.name, + path: '/${AppRoute.checkEULA.name}', + builder: (context, state) => const CheckEULA()), + GoRoute( + name: AppRoute.resetPassword.name, + path: '/${AppRoute.resetPassword.name}', + builder: (context, state) => const ResetPasswordView()), + GoRoute( + name: AppRoute.passwordSuccess.name, + path: '/${AppRoute.passwordSuccess.name}', + builder: (context, state) => const PasswordUpdateSuccessView()), + GoRoute( + name: AppRoute.setupSecreen.name, + path: '/${AppRoute.setupSecreen.name}', + builder: (context, state) => const SetupScreen()), + GoRoute( + name: AppRoute.logoutPageTransition.name, + path: '/${AppRoute.logoutPageTransition.name}', + builder: (context, state) => const LogoutPageTransition()), + GoRoute( + name: AppRoute.loginPageTransition.name, + path: '/${AppRoute.loginPageTransition.name}/:username/:password', + builder: (context, state) => LoginPageTransition( + username: state.pathParameters['username'] ?? '', password: state.pathParameters['password'] ?? '')), + GoRoute( + name: AppRoute.acceptEULA.name, + path: '/${AppRoute.acceptEULA.name}', + builder: (context, state) => EulaView(isCancellable: false, onEULAAccepted: (bool acceptEULA) {})), + GoRoute( + name: AppRoute.eulaTransition.name, + path: '/${AppRoute.eulaTransition.name}', + builder: (context, state) => const EULATransition()), + GoRoute( + name: AppRoute.setupComplete.name, + path: '/${AppRoute.setupComplete.name}', + builder: (context, state) => const SetupCompletePage()), + GoRoute( + name: AppRoute.forgotPassword.name, + path: '/${AppRoute.forgotPassword.name}', + builder: (context, state) => const ForgotPasswordView()), + GoRoute( + name: AppRoute.stateChooser.name, + path: "/${AppRoute.stateChooser.name}", + parentNavigatorKey: _rootNavigatorKey, + builder: (context, state) { + final extras = state.extra as Map; + return StateSelect( + countryName: extras['countryName'] as String, + initialSelection: extras['initialSelection'] as Item?); }, - refreshListenable: GoRouterRefreshStream(authRepository.authStateChanges()), - routes: [ - GoRoute( - path: '/startup', - pageBuilder: (context, state) => NoTransitionPage( - child: AppStartupWidget( - // * This is just a placeholder - // * The loaded route will be managed by GoRouter on state change - onLoaded: (_) => const SizedBox.shrink(), - ), - ), - ), - GoRoute( - path: '/onboarding', - name: AppRoute.onboarding.name, - pageBuilder: (context, state) => const NoTransitionPage( - child: OnboardingScreen(), - ), - ), - GoRoute( - path: '/signIn', - name: AppRoute.signIn.name, - pageBuilder: (context, state) => const NoTransitionPage( - child: CustomSignInScreen(), - ), - ), - // Stateful navigation based on: - // https://github.com/flutter/packages/blob/main/packages/go_router/example/lib/stateful_shell_route.dart - StatefulShellRoute.indexedStack( - pageBuilder: (context, state, navigationShell) => NoTransitionPage( - child: ScaffoldWithNestedNavigation(navigationShell: navigationShell), - ), - branches: [ - StatefulShellBranch( - navigatorKey: _jobsNavigatorKey, - routes: [ - GoRoute( - path: '/jobs', - name: AppRoute.jobs.name, - pageBuilder: (context, state) => const NoTransitionPage( - child: JobsScreen(), - ), - routes: [ - GoRoute( - path: 'add', - name: AppRoute.addJob.name, - parentNavigatorKey: _rootNavigatorKey, - pageBuilder: (context, state) { - return const MaterialPage( - fullscreenDialog: true, - child: EditJobScreen(), - ); - }, - ), - GoRoute( - path: ':id', - name: AppRoute.job.name, - pageBuilder: (context, state) { - final id = state.pathParameters['id']!; - return MaterialPage( - child: JobEntriesScreen(jobId: id), - ); - }, - routes: [ - GoRoute( - path: 'entries/add', - name: AppRoute.addEntry.name, - parentNavigatorKey: _rootNavigatorKey, - pageBuilder: (context, state) { - final jobId = state.pathParameters['id']!; - return MaterialPage( - fullscreenDialog: true, - child: EntryScreen( - jobId: jobId, - ), - ); - }, - ), - GoRoute( - path: 'entries/:eid', - name: AppRoute.entry.name, - pageBuilder: (context, state) { - final jobId = state.pathParameters['id']!; - final entryId = state.pathParameters['eid']!; - final entry = state.extra as Entry?; - return MaterialPage( - child: EntryScreen( - jobId: jobId, - entryId: entryId, - entry: entry, - ), - ); - }, - ), - GoRoute( - path: 'edit', - name: AppRoute.editJob.name, - pageBuilder: (context, state) { - final jobId = state.pathParameters['id']; - final job = state.extra as Job?; - return MaterialPage( - fullscreenDialog: true, - child: EditJobScreen(jobId: jobId, job: job), - ); - }, - ), - ], - ), - ], - ), - ], - ), - StatefulShellBranch( - navigatorKey: _entriesNavigatorKey, + ), + GoRoute( + name: AppRoute.countryChooser.name, + path: "/${AppRoute.countryChooser.name}", + parentNavigatorKey: _rootNavigatorKey, + builder: (context, state) { + final extras = state.extra as Map?; + return CountrySelect(initialSelection: extras?['initialSelection'] as Item?); + }, + ), + ShellRoute( + navigatorKey: _shellNavigatorKey, + builder: (context, state, child) => CustomNavigationBar(child: child), + routes: [ + GoRoute( + name: AppRoute.account.name, + path: '/${AppRoute.account.name}', + builder: (context, state) => const AccountDetailsPage(), routes: [ - GoRoute( - path: '/entries', - name: AppRoute.entries.name, - pageBuilder: (context, state) => const NoTransitionPage( - child: EntriesScreen(), - ), - ), - ], - ), - StatefulShellBranch( - navigatorKey: _accountNavigatorKey, + ]), + GoRoute( + name: AppRoute.reports.name, + path: '/${AppRoute.reports.name}', + builder: (context, state) => const ReportsPage(), routes: [ GoRoute( - path: '/account', - name: AppRoute.profile.name, - pageBuilder: (context, state) => const NoTransitionPage( - child: CustomProfileScreen(), - ), - ), - ], - ), - ], - ), - ], - errorPageBuilder: (context, state) => const NoTransitionPage( - child: NotFoundScreen(), - ), - ); -} + name: AppRoute.reportTable.name, + path: AppRoute.reportTable.name, + builder: (context, state) => const ReportTablePage()), + ]) + ]), +]; + +@riverpod +GoRouter goRouter(GoRouterRef ref) => GoRouter(debugLogDiagnostics: true, navigatorKey: _rootNavigatorKey, initialLocation: '/start', routes: routes); diff --git a/lib/src/routing/app_router.g.dart b/lib/src/routing/app_router.g.dart deleted file mode 100644 index 75aa9be6..00000000 --- a/lib/src/routing/app_router.g.dart +++ /dev/null @@ -1,24 +0,0 @@ -// GENERATED CODE - DO NOT MODIFY BY HAND - -part of 'app_router.dart'; - -// ************************************************************************** -// RiverpodGenerator -// ************************************************************************** - -String _$goRouterHash() => r'3e0521496632ca0a69f0f6658ba74bff70cd6629'; - -/// See also [goRouter]. -@ProviderFor(goRouter) -final goRouterProvider = AutoDisposeProvider.internal( - goRouter, - name: r'goRouterProvider', - debugGetCreateSourceHash: - const bool.fromEnvironment('dart.vm.product') ? null : _$goRouterHash, - dependencies: null, - allTransitiveDependencies: null, -); - -typedef GoRouterRef = AutoDisposeProviderRef; -// ignore_for_file: type=lint -// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member diff --git a/lib/src/routing/app_startup.dart b/lib/src/routing/app_startup.dart index 11cd4721..ed1af1ea 100644 --- a/lib/src/routing/app_startup.dart +++ b/lib/src/routing/app_startup.dart @@ -1,8 +1,9 @@ +import 'package:flutter_starter_base_app/src/common_widgets/circular_loading_animation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; -import 'package:starter_architecture_flutter_firebase/src/constants/app_sizes.dart'; -import 'package:starter_architecture_flutter_firebase/src/features/onboarding/data/onboarding_repository.dart'; +import 'package:flutter_starter_base_app/src/constants/app_sizes.dart'; +import 'package:flutter_starter_base_app/src/features/onboarding/data/onboarding_repository.dart'; part 'app_startup.g.dart'; @@ -41,18 +42,17 @@ class AppStartupLoadingWidget extends StatelessWidget { @override Widget build(BuildContext context) { - return Scaffold( - appBar: AppBar(), - body: const Center( - child: CircularProgressIndicator(), + return const Scaffold( + // appBar: AppBar(), + body: Center( + child: LoadingAnimation(), ), ); } } class AppStartupErrorWidget extends StatelessWidget { - const AppStartupErrorWidget( - {super.key, required this.message, required this.onRetry}); + const AppStartupErrorWidget({super.key, required this.message, required this.onRetry}); final String message; final VoidCallback onRetry; diff --git a/lib/src/routing/app_startup.g.dart b/lib/src/routing/app_startup.g.dart deleted file mode 100644 index b2f72cbe..00000000 --- a/lib/src/routing/app_startup.g.dart +++ /dev/null @@ -1,24 +0,0 @@ -// GENERATED CODE - DO NOT MODIFY BY HAND - -part of 'app_startup.dart'; - -// ************************************************************************** -// RiverpodGenerator -// ************************************************************************** - -String _$appStartupHash() => r'3bf56eca0f1cd6b6760eb62ce3ffdd5136af96b2'; - -/// See also [appStartup]. -@ProviderFor(appStartup) -final appStartupProvider = FutureProvider.internal( - appStartup, - name: r'appStartupProvider', - debugGetCreateSourceHash: - const bool.fromEnvironment('dart.vm.product') ? null : _$appStartupHash, - dependencies: null, - allTransitiveDependencies: null, -); - -typedef AppStartupRef = FutureProviderRef; -// ignore_for_file: type=lint -// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member diff --git a/lib/src/routing/go_router_refresh_stream.dart b/lib/src/routing/go_router_refresh_stream.dart deleted file mode 100644 index 8c9c433b..00000000 --- a/lib/src/routing/go_router_refresh_stream.dart +++ /dev/null @@ -1,21 +0,0 @@ -import 'dart:async'; - -import 'package:flutter/foundation.dart'; - -/// This class was imported from the migration guide for GoRouter 5.0 -class GoRouterRefreshStream extends ChangeNotifier { - GoRouterRefreshStream(Stream stream) { - notifyListeners(); - _subscription = stream.asBroadcastStream().listen( - (dynamic _) => notifyListeners(), - ); - } - - late final StreamSubscription _subscription; - - @override - void dispose() { - _subscription.cancel(); - super.dispose(); - } -} diff --git a/lib/src/routing/not_found_screen.dart b/lib/src/routing/not_found_screen.dart deleted file mode 100644 index f7fe8684..00000000 --- a/lib/src/routing/not_found_screen.dart +++ /dev/null @@ -1,17 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:starter_architecture_flutter_firebase/src/common_widgets/empty_placeholder_widget.dart'; - -/// Simple not found screen used for 404 errors (page not found on web) -class NotFoundScreen extends StatelessWidget { - const NotFoundScreen({super.key}); - - @override - Widget build(BuildContext context) { - return Scaffold( - appBar: AppBar(), - body: const EmptyPlaceholderWidget( - message: '404 - Page not found!', - ), - ); - } -} diff --git a/lib/src/routing/routes.dart b/lib/src/routing/routes.dart new file mode 100644 index 00000000..4b7c87af --- /dev/null +++ b/lib/src/routing/routes.dart @@ -0,0 +1,46 @@ +enum AppRoute { + start, + onboarding, + signIn, + profile, + account, + home, + reports, + schedule, + scheduleSetup, + alerts, + chargerAlerts, + vehicleAlerts, + vinPage, + dtcPage, + splash, + chargerDetails, + addCharger, + addVehicle, + addHousehold, + addAccount, + acceptEULA, + checkEULA, + eulaTransition, + vehicleDetails, + householdDetails, + setupSecreen, + setupComplete, + loginPageTransition, + logoutPageTransition, + reportTable, + reportVehicleSelect, + reportTimeWindowSelect, + breakPointSelect, + forgotPassword, + resetPassword, + passwordSuccess, + vehicleIconSelector, + manualSchedulePage, + stateChooser, + countryChooser, + pricingMethodChooser, + utilityChooser, + rateProgramChooser, + rateChooser, +} diff --git a/lib/src/routing/scaffold_with_nested_navigation.dart b/lib/src/routing/scaffold_with_nested_navigation.dart deleted file mode 100644 index af5fb66b..00000000 --- a/lib/src/routing/scaffold_with_nested_navigation.dart +++ /dev/null @@ -1,134 +0,0 @@ -// ignore_for_file: public_member_api_docs, sort_constructors_first -import 'package:flutter/material.dart'; -import 'package:go_router/go_router.dart'; - -import 'package:starter_architecture_flutter_firebase/src/localization/string_hardcoded.dart'; - -// Stateful navigation based on: -// https://github.com/flutter/packages/blob/main/packages/go_router/example/lib/stateful_shell_route.dart -class ScaffoldWithNestedNavigation extends StatelessWidget { - const ScaffoldWithNestedNavigation({ - Key? key, - required this.navigationShell, - }) : super(key: key ?? const ValueKey('ScaffoldWithNestedNavigation')); - final StatefulNavigationShell navigationShell; - - void _goBranch(int index) { - navigationShell.goBranch( - index, - // A common pattern when using bottom navigation bars is to support - // navigating to the initial location when tapping the item that is - // already active. This example demonstrates how to support this behavior, - // using the initialLocation parameter of goBranch. - initialLocation: index == navigationShell.currentIndex, - ); - } - - @override - Widget build(BuildContext context) { - final size = MediaQuery.sizeOf(context); - if (size.width < 450) { - return ScaffoldWithNavigationBar( - body: navigationShell, - currentIndex: navigationShell.currentIndex, - onDestinationSelected: _goBranch, - ); - } else { - return ScaffoldWithNavigationRail( - body: navigationShell, - currentIndex: navigationShell.currentIndex, - onDestinationSelected: _goBranch, - ); - } - } -} - -class ScaffoldWithNavigationBar extends StatelessWidget { - const ScaffoldWithNavigationBar({ - super.key, - required this.body, - required this.currentIndex, - required this.onDestinationSelected, - }); - final Widget body; - final int currentIndex; - final ValueChanged onDestinationSelected; - - @override - Widget build(BuildContext context) { - return Scaffold( - body: body, - bottomNavigationBar: NavigationBar( - selectedIndex: currentIndex, - destinations: [ - // products - NavigationDestination( - icon: const Icon(Icons.work_outline), - selectedIcon: const Icon(Icons.work), - label: 'Jobs'.hardcoded, - ), - NavigationDestination( - icon: const Icon(Icons.view_headline_outlined), - selectedIcon: const Icon(Icons.view_headline), - label: 'Entries'.hardcoded, - ), - NavigationDestination( - icon: const Icon(Icons.person_outline), - selectedIcon: const Icon(Icons.person), - label: 'Account'.hardcoded, - ), - ], - onDestinationSelected: onDestinationSelected, - ), - ); - } -} - -class ScaffoldWithNavigationRail extends StatelessWidget { - const ScaffoldWithNavigationRail({ - super.key, - required this.body, - required this.currentIndex, - required this.onDestinationSelected, - }); - final Widget body; - final int currentIndex; - final ValueChanged onDestinationSelected; - - @override - Widget build(BuildContext context) { - return Scaffold( - body: Row( - children: [ - NavigationRail( - selectedIndex: currentIndex, - onDestinationSelected: onDestinationSelected, - labelType: NavigationRailLabelType.all, - destinations: [ - NavigationRailDestination( - icon: const Icon(Icons.work_outline), - selectedIcon: const Icon(Icons.work), - label: Text('Jobs'.hardcoded), - ), - NavigationRailDestination( - icon: const Icon(Icons.view_headline_outlined), - selectedIcon: const Icon(Icons.view_headline), - label: Text('Entries'.hardcoded), - ), - NavigationRailDestination( - icon: const Icon(Icons.person_outline), - selectedIcon: const Icon(Icons.person), - label: Text('Account'.hardcoded), - ), - ], - ), - const VerticalDivider(thickness: 1, width: 1), - // This is the main content. - Expanded( - child: body, - ), - ], - ), - ); - } -} diff --git a/lib/src/utils/alert_dialogs.dart b/lib/src/utils/alert_dialogs.dart index 6c26926c..7c6b3482 100644 --- a/lib/src/utils/alert_dialogs.dart +++ b/lib/src/utils/alert_dialogs.dart @@ -2,11 +2,14 @@ library alert_dialogs; import 'dart:io'; +import 'package:flutter_starter_base_app/src/constants/theme_data.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; -import 'package:firebase_core/firebase_core.dart'; import 'package:flutter/services.dart'; +import 'package:go_router/go_router.dart'; part 'show_alert_dialog.dart'; part 'show_exception_alert_dialog.dart'; +part 'package:flutter_starter_base_app/src/common_widgets/confirmation_dialog.dart'; +part 'package:flutter_starter_base_app/src/common_widgets/info_text_dialog.dart'; diff --git a/lib/src/utils/async_value_ui.dart b/lib/src/utils/async_value_ui.dart index 4fc1f997..1e2011a2 100644 --- a/lib/src/utils/async_value_ui.dart +++ b/lib/src/utils/async_value_ui.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:starter_architecture_flutter_firebase/src/localization/string_hardcoded.dart'; -import 'package:starter_architecture_flutter_firebase/src/utils/alert_dialogs.dart'; +import 'package:flutter_starter_base_app/src/localization/string_hardcoded.dart'; +import 'package:flutter_starter_base_app/src/utils/alert_dialogs.dart'; extension AsyncValueUI on AsyncValue { void showAlertDialogOnError(BuildContext context) { diff --git a/lib/src/utils/authentication_handler.dart b/lib/src/utils/authentication_handler.dart new file mode 100644 index 00000000..1d08be82 --- /dev/null +++ b/lib/src/utils/authentication_handler.dart @@ -0,0 +1,31 @@ +import 'package:jwt_decode/jwt_decode.dart'; +import 'package:flutter_starter_base_app/src/api/api.dart'; +import 'package:flutter_secure_storage/flutter_secure_storage.dart'; + +class AuthenticationHandler { + final _usernameKey = 'username'; + final _accessTokenKey = 'access_token'; + final _refreshTokenKey = 'refresh_token'; + final _storage = const FlutterSecureStorage(); + Future clearTokens() async => await _storage.deleteAll(); + Future getUsername() async => await _storage.read(key: _usernameKey); + Future getAccessToken() async => await _storage.read(key: _accessTokenKey); + Future getRefreshToken() async => await _storage.read(key: _refreshTokenKey); + Future saveUsername(String username) async => await _storage.write(key: _usernameKey, value: username); + Future saveTokens(String accessToken, String refreshToken) async => await _storage + .write(key: _accessTokenKey, value: accessToken) + .then((_) async => await _storage.write(key: _refreshTokenKey, value: refreshToken)); + Future isTokenExpired() async { + String? tokenOrNull = await getAccessToken(); + if (tokenOrNull == null || tokenOrNull.isEmpty) return true; + DateTime? expiryDateOrNull = Jwt.getExpiryDate(tokenOrNull); + if (expiryDateOrNull == null) return true; + return DateTime.now().toUtc().isAfter(expiryDateOrNull); + } + + Future canAuthenticateUser() async => await isTokenExpired() + ? (await getUsername()) == null || (await getRefreshToken() == null) + ? false + : await API().refreshToken() + : true; +} diff --git a/lib/src/utils/bar_view_calculator.dart b/lib/src/utils/bar_view_calculator.dart new file mode 100644 index 00000000..204636fc --- /dev/null +++ b/lib/src/utils/bar_view_calculator.dart @@ -0,0 +1,21 @@ +class BarViewCalculator { + late double maximum; + late double minimum; + final List values; + + BarViewCalculator({required this.values}) { + minimum = (values.reduce((curr, next) => curr < next ? curr : next)) * .19; + maximum = (values.reduce((curr, next) => curr > next ? curr : next)) * 1.2; + } + List calculateMarkings() => maximum == minimum + ? List.generate(5, (_) => '') + : List.generate(6, (i) => minimum + i * (maximum - minimum) / 5) + .map((e) => e.toStringAsFixed(2)) + .skip(1) + .toList(); + double calculateFlexValue(double x) => maximum == 0 + ? maximum + : maximum == minimum + ? x / maximum + : (x - minimum) / (maximum - minimum); +} diff --git a/lib/src/utils/error_handler.dart b/lib/src/utils/error_handler.dart new file mode 100644 index 00000000..d2f97ce7 --- /dev/null +++ b/lib/src/utils/error_handler.dart @@ -0,0 +1,28 @@ +import 'dart:ui'; +import 'package:flutter/material.dart'; +import 'package:flutter_starter_base_app/src/localization/string_hardcoded.dart'; + +// * Register error handlers. For more info, see: +// * https://docs.flutter.dev/testing/errors +void registerErrorHandlers() { + // * Show some error UI if any uncaught exception happens + FlutterError.onError = (FlutterErrorDetails details) { + FlutterError.presentError(details); + debugPrint(details.toString()); + }; + // * Handle errors from the underlying platform/OS + PlatformDispatcher.instance.onError = (Object error, StackTrace stack) { + debugPrint(error.toString()); + return true; + }; + // * Show some error UI when any widget in the app fails to build + ErrorWidget.builder = (FlutterErrorDetails details) { + return Scaffold( + appBar: AppBar( + backgroundColor: Colors.red, + title: Text('An error occurred'.hardcoded), + ), + body: Center(child: Text(details.toString())), + ); + }; +} diff --git a/lib/src/utils/format.dart b/lib/src/utils/format.dart index ff3371af..1ce200e4 100644 --- a/lib/src/utils/format.dart +++ b/lib/src/utils/format.dart @@ -1,4 +1,5 @@ -import 'package:intl/intl.dart'; +import 'dart:ui'; +import 'package:easy_localization/easy_localization.dart'; class Format { static String hours(double hours) { @@ -12,6 +13,9 @@ class Format { return DateFormat.yMMMd().format(date); } + static String formatForTable(DateTime datetime) => + '${DateFormat.Md(PlatformDispatcher.instance.locale.toStringWithSeparator()).format(datetime)} ${DateFormat.jm(PlatformDispatcher.instance.locale.toStringWithSeparator()).format(datetime)}'; + static String dayOfWeek(DateTime date) { return DateFormat.E().format(date); } @@ -23,4 +27,6 @@ class Format { } return ''; } + + static String formatForSchedule(String time) => DateFormat("h a").format(DateFormat("HH:mm:ss").parse(time)); } diff --git a/lib/src/utils/show_alert_dialog.dart b/lib/src/utils/show_alert_dialog.dart index f60d571d..25eb962a 100644 --- a/lib/src/utils/show_alert_dialog.dart +++ b/lib/src/utils/show_alert_dialog.dart @@ -15,14 +15,8 @@ Future showAlertDialog({ content: content != null ? Text(content) : null, actions: [ if (cancelActionText != null) - TextButton( - child: Text(cancelActionText), - onPressed: () => Navigator.of(context).pop(false), - ), - TextButton( - child: Text(defaultActionText), - onPressed: () => Navigator.of(context).pop(true), - ), + TextButton(child: Text(cancelActionText), onPressed: () => context.canPop() ? context.pop(false) : null), + TextButton(child: Text(defaultActionText), onPressed: () => context.canPop() ? context.pop(true) : null), ], ), ); @@ -35,13 +29,9 @@ Future showAlertDialog({ actions: [ if (cancelActionText != null) CupertinoDialogAction( - child: Text(cancelActionText), - onPressed: () => Navigator.of(context).pop(false), - ), + child: Text(cancelActionText), onPressed: () => context.canPop() ? context.pop(false) : null), CupertinoDialogAction( - child: Text(defaultActionText), - onPressed: () => Navigator.of(context).pop(true), - ), + child: Text(defaultActionText), onPressed: () => context.canPop() ? context.pop(true) : null) ], ), ); diff --git a/lib/src/utils/show_exception_alert_dialog.dart b/lib/src/utils/show_exception_alert_dialog.dart index 515010f4..ecddac28 100644 --- a/lib/src/utils/show_exception_alert_dialog.dart +++ b/lib/src/utils/show_exception_alert_dialog.dart @@ -13,9 +13,6 @@ Future showExceptionAlertDialog({ ); String _message(dynamic exception) { - if (exception is FirebaseException) { - return exception.message ?? exception.toString(); - } if (exception is PlatformException) { return exception.message ?? exception.toString(); } diff --git a/lib/src/utils/string_extension.dart b/lib/src/utils/string_extension.dart new file mode 100644 index 00000000..c7182b87 --- /dev/null +++ b/lib/src/utils/string_extension.dart @@ -0,0 +1,22 @@ +extension StringExtension on String { + String capitalize() { + return "${this[0].toUpperCase()}${substring(1).toLowerCase()}"; + } + + String camelCaseToTitleCase() { + if (isEmpty) return ''; + String titleCase = this[0].toUpperCase(); + for (int i = 1; i < length; i++) { + if (this[i].toUpperCase() == this[i] && !this[i].startsWith(RegExp(r'[0-9]'))) { + titleCase += ' ${this[i]}'; + } else { + if (this[i].startsWith(RegExp(r'[0-9]')) && i - 1 >= 0 && !this[i - 1].startsWith(RegExp(r'[0-9]'))) { + titleCase += ' ${this[i]}'; + } else { + titleCase += this[i]; + } + } + } + return titleCase; + } +} diff --git a/lib/src/utils/translation_storage.dart b/lib/src/utils/translation_storage.dart new file mode 100644 index 00000000..43bab36a --- /dev/null +++ b/lib/src/utils/translation_storage.dart @@ -0,0 +1,32 @@ +import 'dart:io'; +import 'package:flutter/material.dart'; +import 'package:path_provider/path_provider.dart'; + +class TranslationStorage { + final Locale locale; + static const int _cacheExpirationHours = 24; + TranslationStorage({required this.locale}); + Future get localizedFilePath async => (await getApplicationDocumentsDirectory()).path; + Future get _localizedFile async => File('${await localizedFilePath}/${locale.languageCode}-${locale.countryCode}.json'); + + Future readTranslationFile() async { + try { + return await (await _localizedFile).readAsString(); + } catch (e) { + //todo + } + throw Exception('Failed to read localized values'); + } + + Future writeTranslationFile(String translationJson) async { + try { + return (await _localizedFile).writeAsString(translationJson); + } catch (e) { + // todo + } + throw Exception('Failed to write localized values'); + } + + Future isFileOutdated(File file) async => + !await file.exists() || DateTime.now().difference((await file.stat()).modified).inHours >= _cacheExpirationHours; +} diff --git a/lib/src/utils/vin_check.dart b/lib/src/utils/vin_check.dart new file mode 100644 index 00000000..59af4b42 --- /dev/null +++ b/lib/src/utils/vin_check.dart @@ -0,0 +1,57 @@ +/// todo check +bool isValidVIN(String vin) { + vin = vin.replaceAll(RegExp(r'\s'), '').toUpperCase(); + if (vin.length != 17) return false; + + // Define weights for VIN characters + final Map weights = { + 'A': 1, + 'B': 2, + 'C': 3, + 'D': 4, + 'E': 5, + 'F': 6, + 'G': 7, + 'H': 8, + 'J': 1, + 'K': 2, + 'L': 3, + 'M': 4, + 'N': 5, + 'P': 7, + 'R': 9, + 'S': 2, + 'T': 3, + 'U': 4, + 'V': 5, + 'W': 6, + 'X': 7, + 'Y': 8, + 'Z': 9, + '1': 1, + '2': 2, + '3': 3, + '4': 4, + '5': 5, + '6': 6, + '7': 7, + '8': 8, + '9': 9, + '0': 0, + }; + + // Define the positions of the weights in the VIN + final List positions = [8, 7, 6, 5, 4, 3, 2, 10, 0, 9, 8, 7, 6, 5, 4, 3, 2]; + + int sum = 0; + for (int i = 0; i < 17; i++) { + String char = vin[i]; + if (!weights.containsKey(char)) return false; + sum += ((weights[char] ?? 0) * positions[i]); + } + + // Check digit + int checkDigit = sum % 11; + if (checkDigit == 10) return vin[8] == 'X'; + return checkDigit == int.parse(vin[8]); +} diff --git a/macos/.gitignore b/macos/.gitignore deleted file mode 100644 index 746adbb6..00000000 --- a/macos/.gitignore +++ /dev/null @@ -1,7 +0,0 @@ -# Flutter-related -**/Flutter/ephemeral/ -**/Pods/ - -# Xcode-related -**/dgph -**/xcuserdata/ diff --git a/macos/Flutter/Flutter-Debug.xcconfig b/macos/Flutter/Flutter-Debug.xcconfig deleted file mode 100644 index 4b81f9b2..00000000 --- a/macos/Flutter/Flutter-Debug.xcconfig +++ /dev/null @@ -1,2 +0,0 @@ -#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" -#include "ephemeral/Flutter-Generated.xcconfig" diff --git a/macos/Flutter/Flutter-Release.xcconfig b/macos/Flutter/Flutter-Release.xcconfig deleted file mode 100644 index 5caa9d15..00000000 --- a/macos/Flutter/Flutter-Release.xcconfig +++ /dev/null @@ -1,2 +0,0 @@ -#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" -#include "ephemeral/Flutter-Generated.xcconfig" diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift deleted file mode 100644 index 18ff6ea9..00000000 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ /dev/null @@ -1,20 +0,0 @@ -// -// Generated file. Do not edit. -// - -import FlutterMacOS -import Foundation - -import cloud_firestore -import desktop_webview_auth -import firebase_auth -import firebase_core -import shared_preferences_foundation - -func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { - FLTFirebaseFirestorePlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseFirestorePlugin")) - DesktopWebviewAuthPlugin.register(with: registry.registrar(forPlugin: "DesktopWebviewAuthPlugin")) - FLTFirebaseAuthPlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseAuthPlugin")) - FLTFirebaseCorePlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseCorePlugin")) - SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin")) -} diff --git a/macos/Podfile b/macos/Podfile deleted file mode 100644 index bf4565c5..00000000 --- a/macos/Podfile +++ /dev/null @@ -1,64 +0,0 @@ -platform :osx, '10.14' - -# CocoaPods analytics sends network stats synchronously affecting flutter build latency. -ENV['COCOAPODS_DISABLE_STATS'] = 'true' - -project 'Runner', { - 'Debug' => :debug, - 'Profile' => :release, - 'Release' => :release, -} - -def flutter_root - generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'ephemeral', 'Flutter-Generated.xcconfig'), __FILE__) - unless File.exist?(generated_xcode_build_settings_path) - raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure \"flutter pub get\" is executed first" - end - - File.foreach(generated_xcode_build_settings_path) do |line| - matches = line.match(/FLUTTER_ROOT\=(.*)/) - return matches[1].strip if matches - end - raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Flutter-Generated.xcconfig, then run \"flutter pub get\"" -end - -require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) - -flutter_macos_podfile_setup - -target 'Runner' do - pod 'FirebaseFirestore', :git => 'https://github.com/invertase/firestore-ios-sdk-frameworks.git', :tag => '10.18.0' - use_frameworks! - use_modular_headers! - - flutter_install_all_macos_pods File.dirname(File.realpath(__FILE__)) - target 'RunnerTests' do - inherit! :search_paths - end -end - -post_install do |installer| - # Ensure pods also use the minimum deployment target set above - # https://stackoverflow.com/a/64385584/436422 - puts 'Determining pod project minimum deployment target' - - pods_project = installer.pods_project - deployment_target_key = 'MACOSX_DEPLOYMENT_TARGET' - deployment_targets = pods_project.build_configurations.map{ |config| config.build_settings[deployment_target_key] } - minimum_deployment_target = deployment_targets.min_by{ |version| Gem::Version.new(version) } - - puts 'Minimal deployment target is ' + minimum_deployment_target - puts 'Setting each pod deployment target to ' + minimum_deployment_target - - installer.pods_project.targets.each do |target| - flutter_additional_macos_build_settings(target) - target.build_configurations.each do |config| - config.build_settings[deployment_target_key] = minimum_deployment_target - # https://stackoverflow.com/a/77142190 - xcconfig_path = config.base_configuration_reference.real_path - xcconfig = File.read(xcconfig_path) - xcconfig_mod = xcconfig.gsub(/DT_TOOLCHAIN_DIR/, "TOOLCHAIN_DIR") - File.open(xcconfig_path, "w") { |file| file << xcconfig_mod } - end - end -end \ No newline at end of file diff --git a/macos/Podfile.lock b/macos/Podfile.lock deleted file mode 100644 index 5cb8829f..00000000 --- a/macos/Podfile.lock +++ /dev/null @@ -1,138 +0,0 @@ -PODS: - - cloud_firestore (4.14.0): - - Firebase/CoreOnly (~> 10.18.0) - - Firebase/Firestore (~> 10.18.0) - - firebase_core - - FlutterMacOS - - nanopb (< 2.30910.0, >= 2.30908.0) - - desktop_webview_auth (0.0.1): - - FlutterMacOS - - Firebase/Auth (10.18.0): - - Firebase/CoreOnly - - FirebaseAuth (~> 10.18.0) - - Firebase/CoreOnly (10.18.0): - - FirebaseCore (= 10.18.0) - - Firebase/Firestore (10.18.0): - - Firebase/CoreOnly - - FirebaseFirestore (~> 10.18.0) - - firebase_auth (4.16.0): - - Firebase/Auth (~> 10.18.0) - - Firebase/CoreOnly (~> 10.18.0) - - firebase_core - - FlutterMacOS - - firebase_core (2.24.2): - - Firebase/CoreOnly (~> 10.18.0) - - FlutterMacOS - - FirebaseAppCheckInterop (10.19.0) - - FirebaseAuth (10.18.0): - - FirebaseAppCheckInterop (~> 10.17) - - FirebaseCore (~> 10.0) - - GoogleUtilities/AppDelegateSwizzler (~> 7.8) - - GoogleUtilities/Environment (~> 7.8) - - GTMSessionFetcher/Core (< 4.0, >= 2.1) - - RecaptchaInterop (~> 100.0) - - FirebaseCore (10.18.0): - - FirebaseCoreInternal (~> 10.0) - - GoogleUtilities/Environment (~> 7.12) - - GoogleUtilities/Logger (~> 7.12) - - FirebaseCoreInternal (10.19.0): - - "GoogleUtilities/NSData+zlib (~> 7.8)" - - FirebaseFirestore (10.18.0): - - FirebaseFirestore/AutodetectLeveldb (= 10.18.0) - - FirebaseFirestore/AutodetectLeveldb (10.18.0): - - FirebaseFirestore/Base - - FirebaseFirestore/WithLeveldb - - FirebaseFirestore/Base (10.18.0) - - FirebaseFirestore/WithLeveldb (10.18.0): - - FirebaseFirestore/Base - - FlutterMacOS (1.0.0) - - GoogleUtilities/AppDelegateSwizzler (7.12.0): - - GoogleUtilities/Environment - - GoogleUtilities/Logger - - GoogleUtilities/Network - - GoogleUtilities/Environment (7.12.0): - - PromisesObjC (< 3.0, >= 1.2) - - GoogleUtilities/Logger (7.12.0): - - GoogleUtilities/Environment - - GoogleUtilities/Network (7.12.0): - - GoogleUtilities/Logger - - "GoogleUtilities/NSData+zlib" - - GoogleUtilities/Reachability - - "GoogleUtilities/NSData+zlib (7.12.0)" - - GoogleUtilities/Reachability (7.12.0): - - GoogleUtilities/Logger - - GTMSessionFetcher/Core (3.2.0) - - nanopb (2.30909.1): - - nanopb/decode (= 2.30909.1) - - nanopb/encode (= 2.30909.1) - - nanopb/decode (2.30909.1) - - nanopb/encode (2.30909.1) - - PromisesObjC (2.3.1) - - shared_preferences_foundation (0.0.1): - - Flutter - - FlutterMacOS - -DEPENDENCIES: - - cloud_firestore (from `Flutter/ephemeral/.symlinks/plugins/cloud_firestore/macos`) - - desktop_webview_auth (from `Flutter/ephemeral/.symlinks/plugins/desktop_webview_auth/macos`) - - firebase_auth (from `Flutter/ephemeral/.symlinks/plugins/firebase_auth/macos`) - - firebase_core (from `Flutter/ephemeral/.symlinks/plugins/firebase_core/macos`) - - FirebaseFirestore (from `https://github.com/invertase/firestore-ios-sdk-frameworks.git`, tag `10.18.0`) - - FlutterMacOS (from `Flutter/ephemeral`) - - shared_preferences_foundation (from `Flutter/ephemeral/.symlinks/plugins/shared_preferences_foundation/darwin`) - -SPEC REPOS: - trunk: - - Firebase - - FirebaseAppCheckInterop - - FirebaseAuth - - FirebaseCore - - FirebaseCoreInternal - - GoogleUtilities - - GTMSessionFetcher - - nanopb - - PromisesObjC - -EXTERNAL SOURCES: - cloud_firestore: - :path: Flutter/ephemeral/.symlinks/plugins/cloud_firestore/macos - desktop_webview_auth: - :path: Flutter/ephemeral/.symlinks/plugins/desktop_webview_auth/macos - firebase_auth: - :path: Flutter/ephemeral/.symlinks/plugins/firebase_auth/macos - firebase_core: - :path: Flutter/ephemeral/.symlinks/plugins/firebase_core/macos - FirebaseFirestore: - :git: https://github.com/invertase/firestore-ios-sdk-frameworks.git - :tag: 10.18.0 - FlutterMacOS: - :path: Flutter/ephemeral - shared_preferences_foundation: - :path: Flutter/ephemeral/.symlinks/plugins/shared_preferences_foundation/darwin - -CHECKOUT OPTIONS: - FirebaseFirestore: - :git: https://github.com/invertase/firestore-ios-sdk-frameworks.git - :tag: 10.18.0 - -SPEC CHECKSUMS: - cloud_firestore: c24b3cd9294b9e8a94fe8d03b34c5b3339f40204 - desktop_webview_auth: 9bba53a29c9bc6a4ff621fd0ebfbb18856c4dada - Firebase: 414ad272f8d02dfbf12662a9d43f4bba9bec2a06 - firebase_auth: 1d5713117a5a961e90b0bfd7766a7ec89c5a709f - firebase_core: a74ee8b3ab5f91ae6b73f4913eaca996c24458b6 - FirebaseAppCheckInterop: 37884781f3e16a1ba47e7ec80a1e805f987788e3 - FirebaseAuth: 12314b438fa76048540c8fb86d6cfc9e08595176 - FirebaseCore: 2322423314d92f946219c8791674d2f3345b598f - FirebaseCoreInternal: b444828ea7cfd594fca83046b95db98a2be4f290 - FirebaseFirestore: 584e3f563142f63d20e9ec9c505370d674d44eba - FlutterMacOS: 8f6f14fa908a6fb3fba0cd85dbd81ec4b251fb24 - GoogleUtilities: 0759d1a57ebb953965c2dfe0ba4c82e95ccc2e34 - GTMSessionFetcher: 41b9ef0b4c08a6db4b7eb51a21ae5183ec99a2c8 - nanopb: d4d75c12cd1316f4a64e3c6963f879ecd4b5e0d5 - PromisesObjC: c50d2056b5253dadbd6c2bea79b0674bd5a52fa4 - shared_preferences_foundation: 5b919d13b803cadd15ed2dc053125c68730e5126 - -PODFILE CHECKSUM: ed25aa82c3d6dc904e5983ebb48866441998f773 - -COCOAPODS: 1.14.2 diff --git a/macos/Runner.xcodeproj/project.pbxproj b/macos/Runner.xcodeproj/project.pbxproj deleted file mode 100644 index 08af7ab6..00000000 --- a/macos/Runner.xcodeproj/project.pbxproj +++ /dev/null @@ -1,819 +0,0 @@ -// !$*UTF8*$! -{ - archiveVersion = 1; - classes = { - }; - objectVersion = 54; - objects = { - -/* Begin PBXAggregateTarget section */ - 33CC111A2044C6BA0003C045 /* Flutter Assemble */ = { - isa = PBXAggregateTarget; - buildConfigurationList = 33CC111B2044C6BA0003C045 /* Build configuration list for PBXAggregateTarget "Flutter Assemble" */; - buildPhases = ( - 33CC111E2044C6BF0003C045 /* ShellScript */, - ); - dependencies = ( - ); - name = "Flutter Assemble"; - productName = FLX; - }; -/* End PBXAggregateTarget section */ - -/* Begin PBXBuildFile section */ - 10B2BDCC19F5F8A769C75A69 /* Pods_RunnerTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 956870BF59AE6EB524719E6F /* Pods_RunnerTests.framework */; }; - 331C80D8294CF71000263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C80D7294CF71000263BE5 /* RunnerTests.swift */; }; - 335BBD1B22A9A15E00E9071D /* GeneratedPluginRegistrant.swift in Sources */ = {isa = PBXBuildFile; fileRef = 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */; }; - 33CC10F12044A3C60003C045 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC10F02044A3C60003C045 /* AppDelegate.swift */; }; - 33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F22044A3C60003C045 /* Assets.xcassets */; }; - 33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F42044A3C60003C045 /* MainMenu.xib */; }; - 33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */; }; - 37AAE182D15FE04DBAC96973 /* GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = EBC24CA94494A14C4790F8A7 /* GoogleService-Info.plist */; }; - 38A1088FCCD61196A9C6821F /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = B2FB2F95AEC928FDCDDC1F43 /* Pods_Runner.framework */; }; -/* End PBXBuildFile section */ - -/* Begin PBXContainerItemProxy section */ - 331C80D9294CF71000263BE5 /* PBXContainerItemProxy */ = { - isa = PBXContainerItemProxy; - containerPortal = 33CC10E52044A3C60003C045 /* Project object */; - proxyType = 1; - remoteGlobalIDString = 33CC10EC2044A3C60003C045; - remoteInfo = Runner; - }; - 33CC111F2044C79F0003C045 /* PBXContainerItemProxy */ = { - isa = PBXContainerItemProxy; - containerPortal = 33CC10E52044A3C60003C045 /* Project object */; - proxyType = 1; - remoteGlobalIDString = 33CC111A2044C6BA0003C045; - remoteInfo = FLX; - }; -/* End PBXContainerItemProxy section */ - -/* Begin PBXCopyFilesBuildPhase section */ - 33CC110E2044A8840003C045 /* Bundle Framework */ = { - isa = PBXCopyFilesBuildPhase; - buildActionMask = 2147483647; - dstPath = ""; - dstSubfolderSpec = 10; - files = ( - ); - name = "Bundle Framework"; - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXCopyFilesBuildPhase section */ - -/* Begin PBXFileReference section */ - 331C80D5294CF71000263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; - 331C80D7294CF71000263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = ""; }; - 333000ED22D3DE5D00554162 /* Warnings.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Warnings.xcconfig; sourceTree = ""; }; - 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GeneratedPluginRegistrant.swift; sourceTree = ""; }; - 33CC10ED2044A3C60003C045 /* starter_architecture_flutter_firebase.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = starter_architecture_flutter_firebase.app; sourceTree = BUILT_PRODUCTS_DIR; }; - 33CC10F02044A3C60003C045 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; - 33CC10F22044A3C60003C045 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Assets.xcassets; path = Runner/Assets.xcassets; sourceTree = ""; }; - 33CC10F52044A3C60003C045 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/MainMenu.xib; sourceTree = ""; }; - 33CC10F72044A3C60003C045 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; name = Info.plist; path = Runner/Info.plist; sourceTree = ""; }; - 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainFlutterWindow.swift; sourceTree = ""; }; - 33CEB47222A05771004F2AC0 /* Flutter-Debug.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Flutter-Debug.xcconfig"; sourceTree = ""; }; - 33CEB47422A05771004F2AC0 /* Flutter-Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Flutter-Release.xcconfig"; sourceTree = ""; }; - 33CEB47722A0578A004F2AC0 /* Flutter-Generated.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = "Flutter-Generated.xcconfig"; path = "ephemeral/Flutter-Generated.xcconfig"; sourceTree = ""; }; - 33E51913231747F40026EE4D /* DebugProfile.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = DebugProfile.entitlements; sourceTree = ""; }; - 33E51914231749380026EE4D /* Release.entitlements */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.entitlements; path = Release.entitlements; sourceTree = ""; }; - 33E5194F232828860026EE4D /* AppInfo.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = AppInfo.xcconfig; sourceTree = ""; }; - 4C2B59A97789E53D8DF5C8A0 /* Pods-RunnerTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.debug.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.debug.xcconfig"; sourceTree = ""; }; - 69CA1056711F241B75F50D1D /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; - 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Release.xcconfig; sourceTree = ""; }; - 7F84554EA89430C57B119361 /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = ""; }; - 956870BF59AE6EB524719E6F /* Pods_RunnerTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RunnerTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; - 956F465479A0D8247B273A1C /* Pods-RunnerTests.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.profile.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.profile.xcconfig"; sourceTree = ""; }; - 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Debug.xcconfig; sourceTree = ""; }; - B2FB2F95AEC928FDCDDC1F43 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; - BB5BDC00C993F90A2D1ED899 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; - EBC24CA94494A14C4790F8A7 /* GoogleService-Info.plist */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.xml; name = "GoogleService-Info.plist"; path = "Runner/GoogleService-Info.plist"; sourceTree = ""; }; - F5D850A714A1FDE3B89655C2 /* Pods-RunnerTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.release.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.release.xcconfig"; sourceTree = ""; }; -/* End PBXFileReference section */ - -/* Begin PBXFrameworksBuildPhase section */ - 331C80D2294CF70F00263BE5 /* Frameworks */ = { - isa = PBXFrameworksBuildPhase; - buildActionMask = 2147483647; - files = ( - 10B2BDCC19F5F8A769C75A69 /* Pods_RunnerTests.framework in Frameworks */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; - 33CC10EA2044A3C60003C045 /* Frameworks */ = { - isa = PBXFrameworksBuildPhase; - buildActionMask = 2147483647; - files = ( - 38A1088FCCD61196A9C6821F /* Pods_Runner.framework in Frameworks */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXFrameworksBuildPhase section */ - -/* Begin PBXGroup section */ - 331C80D6294CF71000263BE5 /* RunnerTests */ = { - isa = PBXGroup; - children = ( - 331C80D7294CF71000263BE5 /* RunnerTests.swift */, - ); - path = RunnerTests; - sourceTree = ""; - }; - 33BA886A226E78AF003329D5 /* Configs */ = { - isa = PBXGroup; - children = ( - 33E5194F232828860026EE4D /* AppInfo.xcconfig */, - 9740EEB21CF90195004384FC /* Debug.xcconfig */, - 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, - 333000ED22D3DE5D00554162 /* Warnings.xcconfig */, - ); - path = Configs; - sourceTree = ""; - }; - 33CC10E42044A3C60003C045 = { - isa = PBXGroup; - children = ( - 33FAB671232836740065AC1E /* Runner */, - 33CEB47122A05771004F2AC0 /* Flutter */, - 331C80D6294CF71000263BE5 /* RunnerTests */, - 33CC10EE2044A3C60003C045 /* Products */, - D73912EC22F37F3D000D13A0 /* Frameworks */, - EBC24CA94494A14C4790F8A7 /* GoogleService-Info.plist */, - 582EA25906B0232CA7A84F0B /* Pods */, - ); - sourceTree = ""; - }; - 33CC10EE2044A3C60003C045 /* Products */ = { - isa = PBXGroup; - children = ( - 33CC10ED2044A3C60003C045 /* starter_architecture_flutter_firebase.app */, - 331C80D5294CF71000263BE5 /* RunnerTests.xctest */, - ); - name = Products; - sourceTree = ""; - }; - 33CC11242044D66E0003C045 /* Resources */ = { - isa = PBXGroup; - children = ( - 33CC10F22044A3C60003C045 /* Assets.xcassets */, - 33CC10F42044A3C60003C045 /* MainMenu.xib */, - 33CC10F72044A3C60003C045 /* Info.plist */, - ); - name = Resources; - path = ..; - sourceTree = ""; - }; - 33CEB47122A05771004F2AC0 /* Flutter */ = { - isa = PBXGroup; - children = ( - 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */, - 33CEB47222A05771004F2AC0 /* Flutter-Debug.xcconfig */, - 33CEB47422A05771004F2AC0 /* Flutter-Release.xcconfig */, - 33CEB47722A0578A004F2AC0 /* Flutter-Generated.xcconfig */, - ); - path = Flutter; - sourceTree = ""; - }; - 33FAB671232836740065AC1E /* Runner */ = { - isa = PBXGroup; - children = ( - 33CC10F02044A3C60003C045 /* AppDelegate.swift */, - 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */, - 33E51913231747F40026EE4D /* DebugProfile.entitlements */, - 33E51914231749380026EE4D /* Release.entitlements */, - 33CC11242044D66E0003C045 /* Resources */, - 33BA886A226E78AF003329D5 /* Configs */, - ); - path = Runner; - sourceTree = ""; - }; - 582EA25906B0232CA7A84F0B /* Pods */ = { - isa = PBXGroup; - children = ( - 69CA1056711F241B75F50D1D /* Pods-Runner.debug.xcconfig */, - BB5BDC00C993F90A2D1ED899 /* Pods-Runner.release.xcconfig */, - 7F84554EA89430C57B119361 /* Pods-Runner.profile.xcconfig */, - 4C2B59A97789E53D8DF5C8A0 /* Pods-RunnerTests.debug.xcconfig */, - F5D850A714A1FDE3B89655C2 /* Pods-RunnerTests.release.xcconfig */, - 956F465479A0D8247B273A1C /* Pods-RunnerTests.profile.xcconfig */, - ); - name = Pods; - path = Pods; - sourceTree = ""; - }; - D73912EC22F37F3D000D13A0 /* Frameworks */ = { - isa = PBXGroup; - children = ( - B2FB2F95AEC928FDCDDC1F43 /* Pods_Runner.framework */, - 956870BF59AE6EB524719E6F /* Pods_RunnerTests.framework */, - ); - name = Frameworks; - sourceTree = ""; - }; -/* End PBXGroup section */ - -/* Begin PBXNativeTarget section */ - 331C80D4294CF70F00263BE5 /* RunnerTests */ = { - isa = PBXNativeTarget; - buildConfigurationList = 331C80DE294CF71000263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */; - buildPhases = ( - B533C9439FDEA908B8868EF9 /* [CP] Check Pods Manifest.lock */, - 331C80D1294CF70F00263BE5 /* Sources */, - 331C80D2294CF70F00263BE5 /* Frameworks */, - 331C80D3294CF70F00263BE5 /* Resources */, - ); - buildRules = ( - ); - dependencies = ( - 331C80DA294CF71000263BE5 /* PBXTargetDependency */, - ); - name = RunnerTests; - productName = RunnerTests; - productReference = 331C80D5294CF71000263BE5 /* RunnerTests.xctest */; - productType = "com.apple.product-type.bundle.unit-test"; - }; - 33CC10EC2044A3C60003C045 /* Runner */ = { - isa = PBXNativeTarget; - buildConfigurationList = 33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget "Runner" */; - buildPhases = ( - 91BFEA4BD51282DBBB9B7C63 /* [CP] Check Pods Manifest.lock */, - 33CC10E92044A3C60003C045 /* Sources */, - 33CC10EA2044A3C60003C045 /* Frameworks */, - 33CC10EB2044A3C60003C045 /* Resources */, - 33CC110E2044A8840003C045 /* Bundle Framework */, - 3399D490228B24CF009A79C7 /* ShellScript */, - A4E333A9A67E77DE18241AE2 /* [CP] Embed Pods Frameworks */, - 6E15F62152ED82F836F21AEA /* [CP] Copy Pods Resources */, - ); - buildRules = ( - ); - dependencies = ( - 33CC11202044C79F0003C045 /* PBXTargetDependency */, - ); - name = Runner; - productName = Runner; - productReference = 33CC10ED2044A3C60003C045 /* starter_architecture_flutter_firebase.app */; - productType = "com.apple.product-type.application"; - }; -/* End PBXNativeTarget section */ - -/* Begin PBXProject section */ - 33CC10E52044A3C60003C045 /* Project object */ = { - isa = PBXProject; - attributes = { - LastSwiftUpdateCheck = 0920; - LastUpgradeCheck = 1430; - ORGANIZATIONNAME = ""; - TargetAttributes = { - 331C80D4294CF70F00263BE5 = { - CreatedOnToolsVersion = 14.0; - TestTargetID = 33CC10EC2044A3C60003C045; - }; - 33CC10EC2044A3C60003C045 = { - CreatedOnToolsVersion = 9.2; - LastSwiftMigration = 1100; - ProvisioningStyle = Automatic; - SystemCapabilities = { - com.apple.Sandbox = { - enabled = 1; - }; - }; - }; - 33CC111A2044C6BA0003C045 = { - CreatedOnToolsVersion = 9.2; - ProvisioningStyle = Manual; - }; - }; - }; - buildConfigurationList = 33CC10E82044A3C60003C045 /* Build configuration list for PBXProject "Runner" */; - compatibilityVersion = "Xcode 9.3"; - developmentRegion = en; - hasScannedForEncodings = 0; - knownRegions = ( - en, - Base, - ); - mainGroup = 33CC10E42044A3C60003C045; - productRefGroup = 33CC10EE2044A3C60003C045 /* Products */; - projectDirPath = ""; - projectRoot = ""; - targets = ( - 33CC10EC2044A3C60003C045 /* Runner */, - 331C80D4294CF70F00263BE5 /* RunnerTests */, - 33CC111A2044C6BA0003C045 /* Flutter Assemble */, - ); - }; -/* End PBXProject section */ - -/* Begin PBXResourcesBuildPhase section */ - 331C80D3294CF70F00263BE5 /* Resources */ = { - isa = PBXResourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - runOnlyForDeploymentPostprocessing = 0; - }; - 33CC10EB2044A3C60003C045 /* Resources */ = { - isa = PBXResourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - 33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */, - 33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */, - 37AAE182D15FE04DBAC96973 /* GoogleService-Info.plist in Resources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXResourcesBuildPhase section */ - -/* Begin PBXShellScriptBuildPhase section */ - 3399D490228B24CF009A79C7 /* ShellScript */ = { - isa = PBXShellScriptBuildPhase; - alwaysOutOfDate = 1; - buildActionMask = 2147483647; - files = ( - ); - inputFileListPaths = ( - ); - inputPaths = ( - ); - outputFileListPaths = ( - ); - outputPaths = ( - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "echo \"$PRODUCT_NAME.app\" > \"$PROJECT_DIR\"/Flutter/ephemeral/.app_filename && \"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh embed\n"; - }; - 33CC111E2044C6BF0003C045 /* ShellScript */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputFileListPaths = ( - Flutter/ephemeral/FlutterInputs.xcfilelist, - ); - inputPaths = ( - Flutter/ephemeral/tripwire, - ); - outputFileListPaths = ( - Flutter/ephemeral/FlutterOutputs.xcfilelist, - ); - outputPaths = ( - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "\"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh && touch Flutter/ephemeral/tripwire"; - }; - 6E15F62152ED82F836F21AEA /* [CP] Copy Pods Resources */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputFileListPaths = ( - "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-input-files.xcfilelist", - ); - name = "[CP] Copy Pods Resources"; - outputFileListPaths = ( - "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-output-files.xcfilelist", - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh\"\n"; - showEnvVarsInLog = 0; - }; - 91BFEA4BD51282DBBB9B7C63 /* [CP] Check Pods Manifest.lock */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputFileListPaths = ( - ); - inputPaths = ( - "${PODS_PODFILE_DIR_PATH}/Podfile.lock", - "${PODS_ROOT}/Manifest.lock", - ); - name = "[CP] Check Pods Manifest.lock"; - outputFileListPaths = ( - ); - outputPaths = ( - "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt", - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; - showEnvVarsInLog = 0; - }; - A4E333A9A67E77DE18241AE2 /* [CP] Embed Pods Frameworks */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputFileListPaths = ( - "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist", - ); - name = "[CP] Embed Pods Frameworks"; - outputFileListPaths = ( - "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist", - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; - showEnvVarsInLog = 0; - }; - B533C9439FDEA908B8868EF9 /* [CP] Check Pods Manifest.lock */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputFileListPaths = ( - ); - inputPaths = ( - "${PODS_PODFILE_DIR_PATH}/Podfile.lock", - "${PODS_ROOT}/Manifest.lock", - ); - name = "[CP] Check Pods Manifest.lock"; - outputFileListPaths = ( - ); - outputPaths = ( - "$(DERIVED_FILE_DIR)/Pods-RunnerTests-checkManifestLockResult.txt", - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; - showEnvVarsInLog = 0; - }; -/* End PBXShellScriptBuildPhase section */ - -/* Begin PBXSourcesBuildPhase section */ - 331C80D1294CF70F00263BE5 /* Sources */ = { - isa = PBXSourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - 331C80D8294CF71000263BE5 /* RunnerTests.swift in Sources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; - 33CC10E92044A3C60003C045 /* Sources */ = { - isa = PBXSourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - 33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */, - 33CC10F12044A3C60003C045 /* AppDelegate.swift in Sources */, - 335BBD1B22A9A15E00E9071D /* GeneratedPluginRegistrant.swift in Sources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXSourcesBuildPhase section */ - -/* Begin PBXTargetDependency section */ - 331C80DA294CF71000263BE5 /* PBXTargetDependency */ = { - isa = PBXTargetDependency; - target = 33CC10EC2044A3C60003C045 /* Runner */; - targetProxy = 331C80D9294CF71000263BE5 /* PBXContainerItemProxy */; - }; - 33CC11202044C79F0003C045 /* PBXTargetDependency */ = { - isa = PBXTargetDependency; - target = 33CC111A2044C6BA0003C045 /* Flutter Assemble */; - targetProxy = 33CC111F2044C79F0003C045 /* PBXContainerItemProxy */; - }; -/* End PBXTargetDependency section */ - -/* Begin PBXVariantGroup section */ - 33CC10F42044A3C60003C045 /* MainMenu.xib */ = { - isa = PBXVariantGroup; - children = ( - 33CC10F52044A3C60003C045 /* Base */, - ); - name = MainMenu.xib; - path = Runner; - sourceTree = ""; - }; -/* End PBXVariantGroup section */ - -/* Begin XCBuildConfiguration section */ - 331C80DB294CF71000263BE5 /* Debug */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 4C2B59A97789E53D8DF5C8A0 /* Pods-RunnerTests.debug.xcconfig */; - buildSettings = { - BUNDLE_LOADER = "$(TEST_HOST)"; - CURRENT_PROJECT_VERSION = 1; - GENERATE_INFOPLIST_FILE = YES; - MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = com.example.starterArchitectureFlutterFirebase.RunnerTests; - PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_VERSION = 5.0; - TEST_HOST = "$(BUILT_PRODUCTS_DIR)/starter_architecture_flutter_firebase.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/starter_architecture_flutter_firebase"; - }; - name = Debug; - }; - 331C80DC294CF71000263BE5 /* Release */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = F5D850A714A1FDE3B89655C2 /* Pods-RunnerTests.release.xcconfig */; - buildSettings = { - BUNDLE_LOADER = "$(TEST_HOST)"; - CURRENT_PROJECT_VERSION = 1; - GENERATE_INFOPLIST_FILE = YES; - MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = com.example.starterArchitectureFlutterFirebase.RunnerTests; - PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_VERSION = 5.0; - TEST_HOST = "$(BUILT_PRODUCTS_DIR)/starter_architecture_flutter_firebase.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/starter_architecture_flutter_firebase"; - }; - name = Release; - }; - 331C80DD294CF71000263BE5 /* Profile */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 956F465479A0D8247B273A1C /* Pods-RunnerTests.profile.xcconfig */; - buildSettings = { - BUNDLE_LOADER = "$(TEST_HOST)"; - CURRENT_PROJECT_VERSION = 1; - GENERATE_INFOPLIST_FILE = YES; - MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = com.example.starterArchitectureFlutterFirebase.RunnerTests; - PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_VERSION = 5.0; - TEST_HOST = "$(BUILT_PRODUCTS_DIR)/starter_architecture_flutter_firebase.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/starter_architecture_flutter_firebase"; - }; - name = Profile; - }; - 338D0CE9231458BD00FA5F75 /* Profile */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; - buildSettings = { - ALWAYS_SEARCH_USER_PATHS = NO; - CLANG_ANALYZER_NONNULL = YES; - CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; - CLANG_CXX_LIBRARY = "libc++"; - CLANG_ENABLE_MODULES = YES; - CLANG_ENABLE_OBJC_ARC = YES; - CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; - CLANG_WARN_BOOL_CONVERSION = YES; - CLANG_WARN_CONSTANT_CONVERSION = YES; - CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; - CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; - CLANG_WARN_DOCUMENTATION_COMMENTS = YES; - CLANG_WARN_EMPTY_BODY = YES; - CLANG_WARN_ENUM_CONVERSION = YES; - CLANG_WARN_INFINITE_RECURSION = YES; - CLANG_WARN_INT_CONVERSION = YES; - CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; - CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; - CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; - CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; - CLANG_WARN_SUSPICIOUS_MOVE = YES; - CODE_SIGN_IDENTITY = "-"; - COPY_PHASE_STRIP = NO; - DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; - ENABLE_NS_ASSERTIONS = NO; - ENABLE_STRICT_OBJC_MSGSEND = YES; - GCC_C_LANGUAGE_STANDARD = gnu11; - GCC_NO_COMMON_BLOCKS = YES; - GCC_WARN_64_TO_32_BIT_CONVERSION = YES; - GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; - GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; - GCC_WARN_UNUSED_FUNCTION = YES; - GCC_WARN_UNUSED_VARIABLE = YES; - MACOSX_DEPLOYMENT_TARGET = 10.14; - MTL_ENABLE_DEBUG_INFO = NO; - SDKROOT = macosx; - SWIFT_COMPILATION_MODE = wholemodule; - SWIFT_OPTIMIZATION_LEVEL = "-O"; - }; - name = Profile; - }; - 338D0CEA231458BD00FA5F75 /* Profile */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */; - buildSettings = { - ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; - CLANG_ENABLE_MODULES = YES; - CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements; - CODE_SIGN_IDENTITY = "Apple Development"; - CODE_SIGN_STYLE = Automatic; - COMBINE_HIDPI_IMAGES = YES; - DEVELOPMENT_TEAM = M54ZVB688G; - INFOPLIST_FILE = Runner/Info.plist; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/../Frameworks", - ); - PROVISIONING_PROFILE_SPECIFIER = ""; - SWIFT_VERSION = 5.0; - }; - name = Profile; - }; - 338D0CEB231458BD00FA5F75 /* Profile */ = { - isa = XCBuildConfiguration; - buildSettings = { - CODE_SIGN_STYLE = Manual; - PRODUCT_NAME = "$(TARGET_NAME)"; - }; - name = Profile; - }; - 33CC10F92044A3C60003C045 /* Debug */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; - buildSettings = { - ALWAYS_SEARCH_USER_PATHS = NO; - CLANG_ANALYZER_NONNULL = YES; - CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; - CLANG_CXX_LIBRARY = "libc++"; - CLANG_ENABLE_MODULES = YES; - CLANG_ENABLE_OBJC_ARC = YES; - CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; - CLANG_WARN_BOOL_CONVERSION = YES; - CLANG_WARN_CONSTANT_CONVERSION = YES; - CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; - CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; - CLANG_WARN_DOCUMENTATION_COMMENTS = YES; - CLANG_WARN_EMPTY_BODY = YES; - CLANG_WARN_ENUM_CONVERSION = YES; - CLANG_WARN_INFINITE_RECURSION = YES; - CLANG_WARN_INT_CONVERSION = YES; - CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; - CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; - CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; - CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; - CLANG_WARN_SUSPICIOUS_MOVE = YES; - CODE_SIGN_IDENTITY = "-"; - COPY_PHASE_STRIP = NO; - DEBUG_INFORMATION_FORMAT = dwarf; - ENABLE_STRICT_OBJC_MSGSEND = YES; - ENABLE_TESTABILITY = YES; - GCC_C_LANGUAGE_STANDARD = gnu11; - GCC_DYNAMIC_NO_PIC = NO; - GCC_NO_COMMON_BLOCKS = YES; - GCC_OPTIMIZATION_LEVEL = 0; - GCC_PREPROCESSOR_DEFINITIONS = ( - "DEBUG=1", - "$(inherited)", - ); - GCC_WARN_64_TO_32_BIT_CONVERSION = YES; - GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; - GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; - GCC_WARN_UNUSED_FUNCTION = YES; - GCC_WARN_UNUSED_VARIABLE = YES; - MACOSX_DEPLOYMENT_TARGET = 10.14; - MTL_ENABLE_DEBUG_INFO = YES; - ONLY_ACTIVE_ARCH = YES; - SDKROOT = macosx; - SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; - SWIFT_OPTIMIZATION_LEVEL = "-Onone"; - }; - name = Debug; - }; - 33CC10FA2044A3C60003C045 /* Release */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; - buildSettings = { - ALWAYS_SEARCH_USER_PATHS = NO; - CLANG_ANALYZER_NONNULL = YES; - CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; - CLANG_CXX_LIBRARY = "libc++"; - CLANG_ENABLE_MODULES = YES; - CLANG_ENABLE_OBJC_ARC = YES; - CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; - CLANG_WARN_BOOL_CONVERSION = YES; - CLANG_WARN_CONSTANT_CONVERSION = YES; - CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; - CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; - CLANG_WARN_DOCUMENTATION_COMMENTS = YES; - CLANG_WARN_EMPTY_BODY = YES; - CLANG_WARN_ENUM_CONVERSION = YES; - CLANG_WARN_INFINITE_RECURSION = YES; - CLANG_WARN_INT_CONVERSION = YES; - CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; - CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; - CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; - CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; - CLANG_WARN_SUSPICIOUS_MOVE = YES; - CODE_SIGN_IDENTITY = "-"; - COPY_PHASE_STRIP = NO; - DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; - ENABLE_NS_ASSERTIONS = NO; - ENABLE_STRICT_OBJC_MSGSEND = YES; - GCC_C_LANGUAGE_STANDARD = gnu11; - GCC_NO_COMMON_BLOCKS = YES; - GCC_WARN_64_TO_32_BIT_CONVERSION = YES; - GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; - GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; - GCC_WARN_UNUSED_FUNCTION = YES; - GCC_WARN_UNUSED_VARIABLE = YES; - MACOSX_DEPLOYMENT_TARGET = 10.14; - MTL_ENABLE_DEBUG_INFO = NO; - SDKROOT = macosx; - SWIFT_COMPILATION_MODE = wholemodule; - SWIFT_OPTIMIZATION_LEVEL = "-O"; - }; - name = Release; - }; - 33CC10FC2044A3C60003C045 /* Debug */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */; - buildSettings = { - ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; - CLANG_ENABLE_MODULES = YES; - CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements; - CODE_SIGN_IDENTITY = "Apple Development"; - CODE_SIGN_STYLE = Automatic; - COMBINE_HIDPI_IMAGES = YES; - DEVELOPMENT_TEAM = M54ZVB688G; - INFOPLIST_FILE = Runner/Info.plist; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/../Frameworks", - ); - PROVISIONING_PROFILE_SPECIFIER = ""; - SWIFT_OPTIMIZATION_LEVEL = "-Onone"; - SWIFT_VERSION = 5.0; - }; - name = Debug; - }; - 33CC10FD2044A3C60003C045 /* Release */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */; - buildSettings = { - ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; - CLANG_ENABLE_MODULES = YES; - CODE_SIGN_ENTITLEMENTS = Runner/Release.entitlements; - CODE_SIGN_IDENTITY = "Apple Development"; - CODE_SIGN_STYLE = Automatic; - COMBINE_HIDPI_IMAGES = YES; - DEVELOPMENT_TEAM = M54ZVB688G; - INFOPLIST_FILE = Runner/Info.plist; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/../Frameworks", - ); - PROVISIONING_PROFILE_SPECIFIER = ""; - SWIFT_VERSION = 5.0; - }; - name = Release; - }; - 33CC111C2044C6BA0003C045 /* Debug */ = { - isa = XCBuildConfiguration; - buildSettings = { - CODE_SIGN_STYLE = Manual; - PRODUCT_NAME = "$(TARGET_NAME)"; - }; - name = Debug; - }; - 33CC111D2044C6BA0003C045 /* Release */ = { - isa = XCBuildConfiguration; - buildSettings = { - CODE_SIGN_STYLE = Automatic; - PRODUCT_NAME = "$(TARGET_NAME)"; - }; - name = Release; - }; -/* End XCBuildConfiguration section */ - -/* Begin XCConfigurationList section */ - 331C80DE294CF71000263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - 331C80DB294CF71000263BE5 /* Debug */, - 331C80DC294CF71000263BE5 /* Release */, - 331C80DD294CF71000263BE5 /* Profile */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; - 33CC10E82044A3C60003C045 /* Build configuration list for PBXProject "Runner" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - 33CC10F92044A3C60003C045 /* Debug */, - 33CC10FA2044A3C60003C045 /* Release */, - 338D0CE9231458BD00FA5F75 /* Profile */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; - 33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget "Runner" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - 33CC10FC2044A3C60003C045 /* Debug */, - 33CC10FD2044A3C60003C045 /* Release */, - 338D0CEA231458BD00FA5F75 /* Profile */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; - 33CC111B2044C6BA0003C045 /* Build configuration list for PBXAggregateTarget "Flutter Assemble" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - 33CC111C2044C6BA0003C045 /* Debug */, - 33CC111D2044C6BA0003C045 /* Release */, - 338D0CEB231458BD00FA5F75 /* Profile */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; -/* End XCConfigurationList section */ - }; - rootObject = 33CC10E52044A3C60003C045 /* Project object */; -} diff --git a/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist deleted file mode 100644 index 18d98100..00000000 --- a/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist +++ /dev/null @@ -1,8 +0,0 @@ - - - - - IDEDidComputeMac32BitWarning - - - diff --git a/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme deleted file mode 100644 index bba88c4c..00000000 --- a/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ /dev/null @@ -1,98 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/macos/Runner.xcworkspace/contents.xcworkspacedata b/macos/Runner.xcworkspace/contents.xcworkspacedata deleted file mode 100644 index 21a3cc14..00000000 --- a/macos/Runner.xcworkspace/contents.xcworkspacedata +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - diff --git a/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist deleted file mode 100644 index 18d98100..00000000 --- a/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist +++ /dev/null @@ -1,8 +0,0 @@ - - - - - IDEDidComputeMac32BitWarning - - - diff --git a/macos/Runner/AppDelegate.swift b/macos/Runner/AppDelegate.swift deleted file mode 100644 index d53ef643..00000000 --- a/macos/Runner/AppDelegate.swift +++ /dev/null @@ -1,9 +0,0 @@ -import Cocoa -import FlutterMacOS - -@NSApplicationMain -class AppDelegate: FlutterAppDelegate { - override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool { - return true - } -} diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json deleted file mode 100644 index a2ec33f1..00000000 --- a/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json +++ /dev/null @@ -1,68 +0,0 @@ -{ - "images" : [ - { - "size" : "16x16", - "idiom" : "mac", - "filename" : "app_icon_16.png", - "scale" : "1x" - }, - { - "size" : "16x16", - "idiom" : "mac", - "filename" : "app_icon_32.png", - "scale" : "2x" - }, - { - "size" : "32x32", - "idiom" : "mac", - "filename" : "app_icon_32.png", - "scale" : "1x" - }, - { - "size" : "32x32", - "idiom" : "mac", - "filename" : "app_icon_64.png", - "scale" : "2x" - }, - { - "size" : "128x128", - "idiom" : "mac", - "filename" : "app_icon_128.png", - "scale" : "1x" - }, - { - "size" : "128x128", - "idiom" : "mac", - "filename" : "app_icon_256.png", - "scale" : "2x" - }, - { - "size" : "256x256", - "idiom" : "mac", - "filename" : "app_icon_256.png", - "scale" : "1x" - }, - { - "size" : "256x256", - "idiom" : "mac", - "filename" : "app_icon_512.png", - "scale" : "2x" - }, - { - "size" : "512x512", - "idiom" : "mac", - "filename" : "app_icon_512.png", - "scale" : "1x" - }, - { - "size" : "512x512", - "idiom" : "mac", - "filename" : "app_icon_1024.png", - "scale" : "2x" - } - ], - "info" : { - "version" : 1, - "author" : "xcode" - } -} diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png deleted file mode 100644 index 82b6f9d9..00000000 Binary files a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png and /dev/null differ diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png deleted file mode 100644 index 13b35eba..00000000 Binary files a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png and /dev/null differ diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png deleted file mode 100644 index 0a3f5fa4..00000000 Binary files a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png and /dev/null differ diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png deleted file mode 100644 index bdb57226..00000000 Binary files a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png and /dev/null differ diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png deleted file mode 100644 index f083318e..00000000 Binary files a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png and /dev/null differ diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png deleted file mode 100644 index 326c0e72..00000000 Binary files a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png and /dev/null differ diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png deleted file mode 100644 index 2f1632cf..00000000 Binary files a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png and /dev/null differ diff --git a/macos/Runner/Base.lproj/MainMenu.xib b/macos/Runner/Base.lproj/MainMenu.xib deleted file mode 100644 index 80e867a4..00000000 --- a/macos/Runner/Base.lproj/MainMenu.xib +++ /dev/null @@ -1,343 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/macos/Runner/Configs/AppInfo.xcconfig b/macos/Runner/Configs/AppInfo.xcconfig deleted file mode 100644 index e8f175dd..00000000 --- a/macos/Runner/Configs/AppInfo.xcconfig +++ /dev/null @@ -1,14 +0,0 @@ -// Application-level settings for the Runner target. -// -// This may be replaced with something auto-generated from metadata (e.g., pubspec.yaml) in the -// future. If not, the values below would default to using the project name when this becomes a -// 'flutter create' template. - -// The application's name. By default this is also the title of the Flutter window. -PRODUCT_NAME = starter_architecture_flutter_firebase - -// The application's bundle identifier -PRODUCT_BUNDLE_IDENTIFIER = com.example.starterArchitectureFlutterFirebase - -// The copyright displayed in application information -PRODUCT_COPYRIGHT = Copyright © 2023 com.example. All rights reserved. diff --git a/macos/Runner/Configs/Debug.xcconfig b/macos/Runner/Configs/Debug.xcconfig deleted file mode 100644 index 36b0fd94..00000000 --- a/macos/Runner/Configs/Debug.xcconfig +++ /dev/null @@ -1,2 +0,0 @@ -#include "../../Flutter/Flutter-Debug.xcconfig" -#include "Warnings.xcconfig" diff --git a/macos/Runner/Configs/Release.xcconfig b/macos/Runner/Configs/Release.xcconfig deleted file mode 100644 index dff4f495..00000000 --- a/macos/Runner/Configs/Release.xcconfig +++ /dev/null @@ -1,2 +0,0 @@ -#include "../../Flutter/Flutter-Release.xcconfig" -#include "Warnings.xcconfig" diff --git a/macos/Runner/Configs/Warnings.xcconfig b/macos/Runner/Configs/Warnings.xcconfig deleted file mode 100644 index 42bcbf47..00000000 --- a/macos/Runner/Configs/Warnings.xcconfig +++ /dev/null @@ -1,13 +0,0 @@ -WARNING_CFLAGS = -Wall -Wconditional-uninitialized -Wnullable-to-nonnull-conversion -Wmissing-method-return-type -Woverlength-strings -GCC_WARN_UNDECLARED_SELECTOR = YES -CLANG_UNDEFINED_BEHAVIOR_SANITIZER_NULLABILITY = YES -CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE -CLANG_WARN__DUPLICATE_METHOD_MATCH = YES -CLANG_WARN_PRAGMA_PACK = YES -CLANG_WARN_STRICT_PROTOTYPES = YES -CLANG_WARN_COMMA = YES -GCC_WARN_STRICT_SELECTOR_MATCH = YES -CLANG_WARN_OBJC_REPEATED_USE_OF_WEAK = YES -CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES -GCC_WARN_SHADOW = YES -CLANG_WARN_UNREACHABLE_CODE = YES diff --git a/macos/Runner/DebugProfile.entitlements b/macos/Runner/DebugProfile.entitlements deleted file mode 100644 index 1fbcb4ea..00000000 --- a/macos/Runner/DebugProfile.entitlements +++ /dev/null @@ -1,16 +0,0 @@ - - - - - com.apple.security.app-sandbox - - com.apple.security.cs.allow-jit - - com.apple.security.network.client - - com.apple.security.network.server - - keychain-access-groups - - - diff --git a/macos/Runner/Info.plist b/macos/Runner/Info.plist deleted file mode 100644 index 4789daa6..00000000 --- a/macos/Runner/Info.plist +++ /dev/null @@ -1,32 +0,0 @@ - - - - - CFBundleDevelopmentRegion - $(DEVELOPMENT_LANGUAGE) - CFBundleExecutable - $(EXECUTABLE_NAME) - CFBundleIconFile - - CFBundleIdentifier - $(PRODUCT_BUNDLE_IDENTIFIER) - CFBundleInfoDictionaryVersion - 6.0 - CFBundleName - $(PRODUCT_NAME) - CFBundlePackageType - APPL - CFBundleShortVersionString - $(FLUTTER_BUILD_NAME) - CFBundleVersion - $(FLUTTER_BUILD_NUMBER) - LSMinimumSystemVersion - $(MACOSX_DEPLOYMENT_TARGET) - NSHumanReadableCopyright - $(PRODUCT_COPYRIGHT) - NSMainNibFile - MainMenu - NSPrincipalClass - NSApplication - - diff --git a/macos/Runner/MainFlutterWindow.swift b/macos/Runner/MainFlutterWindow.swift deleted file mode 100644 index 3cc05eb2..00000000 --- a/macos/Runner/MainFlutterWindow.swift +++ /dev/null @@ -1,15 +0,0 @@ -import Cocoa -import FlutterMacOS - -class MainFlutterWindow: NSWindow { - override func awakeFromNib() { - let flutterViewController = FlutterViewController() - let windowFrame = self.frame - self.contentViewController = flutterViewController - self.setFrame(windowFrame, display: true) - - RegisterGeneratedPlugins(registry: flutterViewController) - - super.awakeFromNib() - } -} diff --git a/macos/Runner/Release.entitlements b/macos/Runner/Release.entitlements deleted file mode 100644 index 225aa48b..00000000 --- a/macos/Runner/Release.entitlements +++ /dev/null @@ -1,12 +0,0 @@ - - - - - com.apple.security.app-sandbox - - com.apple.security.network.client - - keychain-access-groups - - - diff --git a/macos/RunnerTests/RunnerTests.swift b/macos/RunnerTests/RunnerTests.swift deleted file mode 100644 index 5418c9f5..00000000 --- a/macos/RunnerTests/RunnerTests.swift +++ /dev/null @@ -1,12 +0,0 @@ -import FlutterMacOS -import Cocoa -import XCTest - -class RunnerTests: XCTestCase { - - func testExample() { - // If you add code to the Runner application, consider adding tests here. - // See https://developer.apple.com/documentation/xctest for more information about using XCTest. - } - -} diff --git a/mock/contacts.json b/mock/contacts.json new file mode 100644 index 00000000..9cc27c98 --- /dev/null +++ b/mock/contacts.json @@ -0,0 +1,242 @@ +[ + { + "id": "c190d3ed-dfaa-4de2-bb97-5bd8a13bfc0b", + "firstname": "FirstName1", + "lastname": "LastName1", + "email": "contact1@example.com", + "phoneNumber": "555-00001", + "address": "100 Example St", + "city": "CityName", + "state": "StateName", + "zipcode": "10000", + "company": "Company1" + }, + { + "id": "8f4bf447-1452-4121-87d8-96756f331dc7", + "firstname": "FirstName2", + "lastname": "LastName2", + "email": "contact2@example.com", + "phoneNumber": "555-00011", + "address": "101 Example St", + "city": "CityName", + "state": "StateName", + "zipcode": "10001", + "company": "Company2" + }, + { + "id": "fc5cfde5-6463-4cb2-aaad-a3a26d6f31b2", + "firstname": "FirstName3", + "lastname": "LastName3", + "email": "contact3@example.com", + "phoneNumber": "555-00021", + "address": "102 Example St", + "city": "CityName", + "state": "StateName", + "zipcode": "10002", + "company": "Company3" + }, + { + "id": "e32a9f82-57d5-46a6-81f3-32f09e1ad357", + "firstname": "FirstName4", + "lastname": "LastName4", + "email": "contact4@example.com", + "phoneNumber": "555-00031", + "address": "103 Example St", + "city": "CityName", + "state": "StateName", + "zipcode": "10003", + "company": "Company4" + }, + { + "id": "d911c9a1-79e3-4a3a-afec-f45bd8882c90", + "firstname": "FirstName5", + "lastname": "LastName5", + "email": "contact5@example.com", + "phoneNumber": "555-00041", + "address": "104 Example St", + "city": "CityName", + "state": "StateName", + "zipcode": "10004", + "company": "Company5" + }, + { + "id": "aac3420e-d9f3-44ed-95b9-4793652749d8", + "firstname": "FirstName6", + "lastname": "LastName6", + "email": "contact6@example.com", + "phoneNumber": "555-00051", + "address": "105 Example St", + "city": "CityName", + "state": "StateName", + "zipcode": "10005", + "company": "Company6" + }, + { + "id": "f58f76bd-081f-4cc0-97e2-1559d82d47ca", + "firstname": "FirstName7", + "lastname": "LastName7", + "email": "contact7@example.com", + "phoneNumber": "555-00061", + "address": "106 Example St", + "city": "CityName", + "state": "StateName", + "zipcode": "10006", + "company": "Company7" + }, + { + "id": "41e99135-68e1-4ffd-bf6e-2b5e089e4bec", + "firstname": "FirstName8", + "lastname": "LastName8", + "email": "contact8@example.com", + "phoneNumber": "555-00071", + "address": "107 Example St", + "city": "CityName", + "state": "StateName", + "zipcode": "10007", + "company": "Company8" + }, + { + "id": "303651da-7e92-45e4-a2b0-404a0488041a", + "firstname": "FirstName9", + "lastname": "LastName9", + "email": "contact9@example.com", + "phoneNumber": "555-00081", + "address": "108 Example St", + "city": "CityName", + "state": "StateName", + "zipcode": "10008", + "company": "Company9" + }, + { + "id": "1598b82f-3da1-4510-90b5-2d7fa5c4906e", + "firstname": "FirstName10", + "lastname": "LastName10", + "email": "contact10@example.com", + "phoneNumber": "555-00091", + "address": "109 Example St", + "city": "CityName", + "state": "StateName", + "zipcode": "10009", + "company": "Company10" + }, + { + "id": "9649e19c-333d-4bb4-81eb-b273e49739af", + "firstname": "FirstName11", + "lastname": "LastName11", + "email": "contact11@example.com", + "phoneNumber": "555-00101", + "address": "110 Example St", + "city": "CityName", + "state": "StateName", + "zipcode": "10010", + "company": "Company11" + }, + { + "id": "4071dbe4-5bc4-493e-ba0c-71464b3c6f68", + "firstname": "FirstName12", + "lastname": "LastName12", + "email": "contact12@example.com", + "phoneNumber": "555-00111", + "address": "111 Example St", + "city": "CityName", + "state": "StateName", + "zipcode": "10011", + "company": "Company12" + }, + { + "id": "d09496bd-cba4-4a0a-9eb2-83e88a91e796", + "firstname": "FirstName13", + "lastname": "LastName13", + "email": "contact13@example.com", + "phoneNumber": "555-00121", + "address": "112 Example St", + "city": "CityName", + "state": "StateName", + "zipcode": "10012", + "company": "Company13" + }, + { + "id": "3515cfb9-4f74-4009-8282-e8325146834d", + "firstname": "FirstName14", + "lastname": "LastName14", + "email": "contact14@example.com", + "phoneNumber": "555-00131", + "address": "113 Example St", + "city": "CityName", + "state": "StateName", + "zipcode": "10013", + "company": "Company14" + }, + { + "id": "5f2a57dc-85f7-4d1e-a63a-553a48ca606f", + "firstname": "FirstName15", + "lastname": "LastName15", + "email": "contact15@example.com", + "phoneNumber": "555-00141", + "address": "114 Example St", + "city": "CityName", + "state": "StateName", + "zipcode": "10014", + "company": "Company15" + }, + { + "id": "a4d3352f-468a-4027-9b49-47febf5564d0", + "firstname": "FirstName16", + "lastname": "LastName16", + "email": "contact16@example.com", + "phoneNumber": "555-00151", + "address": "115 Example St", + "city": "CityName", + "state": "StateName", + "zipcode": "10015", + "company": "Company16" + }, + { + "id": "046065c7-3726-4491-93b9-e781cd9e74a3", + "firstname": "FirstName17", + "lastname": "LastName17", + "email": "contact17@example.com", + "phoneNumber": "555-00161", + "address": "116 Example St", + "city": "CityName", + "state": "StateName", + "zipcode": "10016", + "company": "Company17" + }, + { + "id": "e4f28337-e46d-40e4-b435-14a617c65e49", + "firstname": "FirstName18", + "lastname": "LastName18", + "email": "contact18@example.com", + "phoneNumber": "555-00171", + "address": "117 Example St", + "city": "CityName", + "state": "StateName", + "zipcode": "10017", + "company": "Company18" + }, + { + "id": "f7912b03-f082-453e-905a-1d2415af443a", + "firstname": "FirstName19", + "lastname": "LastName19", + "email": "contact19@example.com", + "phoneNumber": "555-00181", + "address": "118 Example St", + "city": "CityName", + "state": "StateName", + "zipcode": "10018", + "company": "Company19" + }, + { + "id": "03db1b40-7a62-412f-b25a-8d4882a07c2b", + "firstname": "FirstName20", + "lastname": "LastName20", + "email": "contact20@example.com", + "phoneNumber": "555-00191", + "address": "119 Example St", + "city": "CityName", + "state": "StateName", + "zipcode": "10019", + "company": "Company20" + } +] \ No newline at end of file diff --git a/mock/countries.json b/mock/countries.json new file mode 100644 index 00000000..8f9a34ac --- /dev/null +++ b/mock/countries.json @@ -0,0 +1,48 @@ +{ + "status": "success", + "message": "Countries retrieved successfully!", + "data": { + "countries": [ + { + "name": "United States", + "code": "US", + "telephoneCode": "+1" + }, + { + "name": "Canada", + "code": "CA", + "telephoneCode": "+1" + }, + { + "name": "United Kingdom", + "code": "UK", + "telephoneCode": "+44" + }, + { + "name": "Germany", + "code": "DE", + "telephoneCode": "+49" + }, + { + "name": "France", + "code": "FR", + "telephoneCode": "+33" + }, + { + "name": "Australia", + "code": "AU", + "telephoneCode": "+61" + }, + { + "name": "Japan", + "code": "JP", + "telephoneCode": "+81" + }, + { + "name": "China", + "code": "CN", + "telephoneCode": "+86" + } + ] + } +} \ No newline at end of file diff --git a/mock/en-US.json b/mock/en-US.json new file mode 100644 index 00000000..e0b59d79 --- /dev/null +++ b/mock/en-US.json @@ -0,0 +1,459 @@ +{ + "status": "success", + "message": "Loaclization values retrieved successfully", + "data": { + "common": { + "name": "Name", + "home": "Home", + "schedule": "Schedule", + "account": "Account", + "reports": "Reports", + "alerts": "Alerts", + "details": "Details", + "start": "Start", + "set": "Set", + "select": "Select", + "next": "Next", + "finish": "Finish", + "cancel": "Cancel", + "back": "Back", + "time": "Time", + "distance": "Distance", + "duration": "Duration", + "cost": "Cost", + "used": "Used", + "remove": "Remove", + "centerQR": "Center QR Code in camera view", + "username": "Username", + "password": "Password", + "vehicle": "Vehicle", + "charger": "Charger", + "add": "Add", + "selected":"Selected" , + "unknonwn": "Unknown" + }, + "btn": { + "ok": "OK", + "getStarted": "Get Started", + "exitSetup": "Exit Setup", + "haveAccount": "Already have an account?", + "tryAgain": "Try Again", + "login": "Log In", + "loggingIn": "Logging In...", + "logout": "Logout", + "forgotPassword": "Forgot Password", + "resetPassword": "Reset Password", + "updatePassword": "Update Password", + "acceptEula": "I have read and accept the EULA", + "tryAnotherSsid": "Try Another SSID", + "openCamera": "Open Camera", + "addVehicleWDongle": "Add Vehicle with Dongle", + "removeVehicle": "Remove Vehicle", + "removeDongle": "Remove Dongle", + "removeCharger": "Remove Charger", + "removeHousehold":"Remove Household", + "changeIcon": "Change Icon", + "addHousehold": "Add Household", + "support": "Support", + "accessaries": "Accessories", + "appHome": "Go To App Home", + "scanQRCode": "Scan QR Code", + "orScanQRCode": "Or Scan QR Code", + "tryAnotherWayToConnect": "Try Another Way To Connect", + "confirm": "Confirm", + "goBack": "Go Back", + "selectDiffNetwork": "Select A Different Network", + "loginPage": "Go to Log In Page" + }, + "hint": { + "choose": "Choose", + "enter": "Enter", + "vehicleName": "Enter Vehicle Name", + "nameYourCharger": "Name Your Charger", + "customRates":"Set Custom Rates", + "optional": "Optional", + "phoneNumber": "Enter Phone Number", + "email": "Enter Email", + "emailOrUsername": "Enter Email or Username", + "password": "Enter Password", + "address": "Enter Address", + "address2": "Address 2 (optional)", + "city": "Enter City", + "zipCode": "Enter Zip Code" + }, + "validation": { + "textfield": { + "username": "Enter username", + "validUsername": "Enter a valid username", + "password": "Enter a password", + "validPassword": "Enter a valid password", + "confPassword": "Entered password doesn't match", + "verificationCode": "Enter verification code", + "validVerificationCode": "Enter a valid verification code", + "chargerName": "Enter a name for your charger", + "validChargerName": "Enter a valid name for your charger", + "vehicleName": "Enter vehicle name", + "validVehicleName": "Enter a valid name for your vehicle", + "householdName": "Enter a valid household name", + "streetAddress": "Enter a valid street address", + "city": "Enter a valid city", + "zipCode": "Enter a valid zip code", + "email": "Enter your email", + "validEmail": "Enter a valid email ID", + "phone": "Enter your phone number", + "validPhone": "Enter a valid phone number", + "lowMilesThreshold": "Enter a valid low miles threshold", + "arrivalAlertTime": "Enter valid time in seconds", + "minimumMilesThreshold": "Enter valid Minimum miles Threshold", + "maxChargeCost": "Enter a maximum charge cost", + "validMaxChargeCost": "Enter a valid maximum charge cost" + }, + "field": { + "breakerSize": "Select a Breaker Size", + "maxChargeCurrent": "Select a Max charge Current", + "reportingUnits": "Select reporting units", + "state": "Select a valid state", + "country": "Select a valid country", + "method": "Select a valid Pricing Method", + "utility": "Select a valid Utility", + "rateProgram": "Select a valid Rate Program", + "rate": "Enter valid rates", + "alertTime": "Please enter a valid alert time" + } + }, + "status": { + "charging": "Charging", + "idle": "Idle", + "pluggedIn": "Plugged in", + "finishedCharging": "Finished Charging", + "scheduled": "Scheduled", + "chargingError": "Charging Error" + }, + + "signalStrength": { + "strong": "Strong", + "moderate": "Moderate", + "weak": "Weak" + }, + + "connectionStatus": { + "connected": "Connected", + "disconnected": "Disconnected" + + }, + + "oob": { + "setup": "Setup", + "setupGuideline": "Follow the guided steps, below, to set up and start using your new charger.", + "accountWizDesc": "Set up address, contact and utility pricing", + "chargerWizDesc": "Install and configure your charger", + "vehicleWizDesc": "Connect dongle and configure your vehicle", + "setupComplete": "Your initial setup is complete", + "congratulations": "Congratulations" + }, + "dialog": { + "exitSetup": "Are you sure you want to exit setup?", + "resourceRemovalDesc": "This {resource} will be removed from your account.", + "removalConfirmation": "{resource} removed from your account" + }, + "charger": { + "wizard": { + "addChargerTitle": "Add Charger", + "pluginYourCharger": "Plug In Your Charger", + "configureCharger": "Configure Charger", + "pluginYourChargerDesc": "Once your charger is plugged in and the red, wifi light is on, you are ready to add it to your account.", + "connectYourCharger": "Connect Your Charger", + "bluetoothConnectionDesc": "Stand close to your charger and use the number on the side to select it from this list of available Bluetooth devices: ", + "bluetoothDevices": "Bluetooth Devices", + "connectYourChargerDesc": "Use your camera to scan the QR code on the size of your charger.", + "wifiSetup": "Set Up Charger WiFi", + "wifiSelectionDesc": "Select a WiFi connection for your charger", + "wifiNetworks": "Networks", + "wifiPasswordTitle": "WiFi Password for {name}", + "confirmConnection": "Confirm Charger Connection", + "confirmConnectionDesc": "The charger WiFi LED will blink Blue when connected to the Internet.", + "connecting": "Charger connecting to WiFi...", + "connected": "Charger Connected", + "failed": "Could not connect charger!" + }, + "startCharging": "Start Charging", + "stopCharging": "Stop Charging", + "scheduledStart": "Scheduled Start", + "scheduleCharging": "Schedule Charging", + "chargeRate": "Charge Rate", + "estChargeTime": "Est. Charge Time", + "estChargeCost": "Est. Charge Cost", + "completeBy": "Complete By", + "info": "Information", + "status": "Status", + "connectedVehicle": "Connected Vehicle", + "breakerSize": "Breaker Size", + "maxChargeCurrent": "Max Charge Current", + "modelNumber": "Model Number", + "serialNumber": "Serial Number", + "softwareVersion": "Software Version", + "panelLedLighting": "Panel LED Lighting", + "wifiSec": "WiFi Settings", + "wifi": { + "ssid": "SSID", + "mac": "MAC", + "connectionStatus": "Connection Status", + "signalStrength": "Signal Strength", + "rssi": "RSSI", + "ipAddress": "IP Address", + "lastContact": "Last Contact" + }, + "authSec": "Authorization", + "auth": { + "none": "None", + "always": "Ask every time", + "devicePresent": "When authorized device present" + }, + "specialOpsSec": "Special Operations", + "manageAlerts": "Manage Device Alerts", + "reboot": "Reboot Charger", + "factoryReset": "Restore Factory Defaults", + "resetFault": "Reset Charger Fault Error" + }, + "schedule": { + "options": "Schedule Options", + "custom": "Custom Schedule", + "onAllDay": "On All Day", + "scheduledCharging": "Scheduled Charging", + "smart": "Smart Schedule", + "monday": "Monday", + "tuesday": "Tuesday", + "wednesday": "Wednesday", + "thursday": "Thursday", + "friday": "Friday", + "saturday": "Saturday", + "sunday": "Sunday", + "selectHours": "Select the hours when charging is available", + "timeSelection": { + "ampm": "Charging will be available from {morningSession} and {eveningSession}. Please note all hours are in the charger's time zone.", + "am": "Charging will be available from {morningSession}. Please note all hours are in the charger's time zone.", + "pm": "Charging will be available from {eveningSession}. Please note all hours are in the charger's time zone.", + "none": "No charging hours available. Please configure your schedule." + } + }, + + "account": { + "phoneNumber": "Phone Number", + "email": "Email", + "wizard": { + "title": "Create Account", + "welcomeDesc": "Create your account with your home address and select your utility information.", + "createCredentialsDesc": "Create Your Username and Password", + "contactInformationDesc": "Default Contact Information" + }, + "reset": { + "resetPassSuccess":"Reset Password Success", + "forgotPassDesc": "Enter the username for your Danlaw account", + "checkYourEmailDesc": "Please check your email for a message from us with a code to reset your password.", + "resetPassSuccessDesc": "Your password was updated. Please log in with your new password" + }, + "confPassword": "Confirm Password", + "verification": "Verification Code" + }, + "household": { + "title": "Household", + "wizard": { + "title": "Create Household", + "nameYourHouseholdDesc": "Name your Household", + "householdAddressDesc": "Create Your Charger Household Address", + "useMyLocation": "Use my current location", + "pricingMethodDesc": "Select Charge Pricing Method", + "utilityCompanyDesc": "Select Utility Company", + "rateProgramDesc": "Select Rate Program", + "customRatesDesc": "Select Custom Rates", + "customRates": { + "subTitle": "Enter the rates per kWh for these standard rate times", + "weekdayDaytime": "Weekday Daytime Rate", + "weekdayNighttime": "Weekday Nighttime Rate", + "weekendRate": "Weekend Rate" + } + }, + "address": "Address", + "electricityPricing": "Electricity Pricing", + "chargers": "Chargers", + "streetAddress1": "Street Address 1", + "streetAddress2": "Street Address 2", + "city": "City", + "state": "State", + "zipCode": "Zip Code", + "country": "Country", + "pricingMethod": { + "short": "Method", + "long": "Pricing Method", + "chargerEstCost": "Charger Estimates Costs", + "utilityRates": "Utility company-based", + "manual": "Manual" + }, + "utility": "Utility", + "rateProgram": "Rate Program", + "rate": "Rate" + }, + "vehicle": { + "title": "Vehicle", + "wizard": { + "addVehicleTitle": "Add Vehicle", + "addDongle": "Add Dongle", + "connectDongle": "Connect Your Dongle", + "dongleDesc": "Locate the dongle you want to assign to your account", + "vehicleIconDesc": "Choose An Icon For Your Vehicle", + "scanDongleQR": "Scan Dongle QR Code", + "scanDongleQRDesc": "Use your camera to scan the QR code on the dongle.", + "installDongle": "Install Dongle", + "installDongleInVehicle": "Install dongle in your vehicle", + "installDongleInfo": "Once the dongle is plugged into your vehicle the app will confirm your connection.", + "noDongleFound": "No Dongle Found", + "installDongleLEDConf": "Make sure the LED on the dongle lights up. If not, please turn your vehicle on.", + "verifyingComms": "Verifying Communications", + "primaryUserInfo": "Enter Primary User Information", + "contactPhone": "Contact Phone", + "contactEmail": "Contact Email" + }, + "nameVehicle": "Name Your Vehicle", + "lastVehicleInfo": "Last Vehicle Attached Information", + "status": "Status", + "vin": "Vin", + "year": "Year", + "make": "Make", + "model": "Model", + "battery": "Battery", + "stateOfCharge": "State of Charge", + "batteryLife": "Battery Life", + "estMilesAvailable": "Estimated Miles Available", + "failureCodes": "Failure Codes", + "diagnosticsCodes": "Diagnostic Trouble Codes", + "dongleInfo": "Dongle Information", + "dongleId": "Dongle Id", + "lastConnected": "Last Connected", + "alerts": "Alerts", + "manageAlerts": "Manage Vehicle Alerts", + "ass": { + "title": "Active Safety System", + "abs": "Anti-lock Braking System (ABS)", + "esc": "Electronic Stability Control (ESC)", + "tc": "Traction Control", + "tpms": "Tire Pressure Monitory System Type (TPMS)", + "note": "Active Safety System Note" + }, + "eng": { + "title": "Engine", + "numCylinders": "Engine Number of Cylinders", + "displacementCC": "Displacement (CC)", + "displacementCI": "Displacement (CI)", + "displacementL": "Displacement (L)", + "engineModel": "Engine Model" + }, + "body": { + "title": "Exterior/Body", + "bodyClass": "Body Class", + "doors": "Doors", + "windows": "Windows" + } + }, + "reports": { + "vehicleEnergyReport": "Vehicle Energy Report", + "tw": { + "title": "Time Window", + "past31": "Past 31 Days", + "week": "This Week", + "month": "This Month", + "year": "This year" + }, + "info": { + "barSelection": "Select a bar segment to see more details" + }, + "viewTrips": "View Trips", + "trips": "Trips", + "viewEvents": "View Charge Events", + "bar": { + "legendHome": "Home", + "legendpub": "Public", + "unknownVehicles": "Unknown Vehicles" + }, + "settings": "Settings", + "vehiclesInReport": "Vehicles In Report" + }, + "alerts": { + "charger": "Charger Alerts", + "vehicle": "Vehicle Alerts", + "options": "Alert Options", + "usePush": "Use Push Alerts", + "useText": "Send Text Alerts", + "useEmail": "Send Email Alerts", + "recipientPhone": "Recipient Phone", + "recipientEmail": "Recipient Email", + "pluginReminder": "Plug-in Reminder", + "chargingStatus": "Charging Status", + "nightly": "Nightly Alert", + "nightlyLowMiles": "Nightly Low Miles Alert", + "time": "Alert Time", + "lowMilesThreshold": "Low Miles Threshold", + "arrivalTime": "Arrival Alert Time (Seconds)", + "fullyCharged": "Vehicle Fully Charged Alert", + "chargingInterrupted": "Charging Interrupted Alert", + "minimumMilesMet": "Minimum Miles Met", + "minimumMilesThreshold": "Minimum Miles Threshold", + "highCost": "High Cost Alert", + "maxChargeCost": "Maximum Charge Cost" + }, + + "error": { + "noInfo": "No information provided" + }, + + "apiResponse": { + "success": { + "general": "Request successful.", + "fetch": "{resource} retrieved successfully.", + "fetchLoc": "Localized data retrieved successfully.", + "create": "{resource} created successfully.", + "update": "{resource} updated successfully.", + "delete": "{resource} deleted successfully.", + "remove": "{resource} removed successfully", + "auth": { + "login": "Logged in successfully.", + "logout": "Logged out successfully.", + "passwordReset": { + "codeSent": "Reset code sent successfully. Please check your email.", + "passwordUpdated": "Password reset successfully." + } + }, + "eula": { + "fetched": "EULA loaded successfully.", + "submitted": "EULA acceptance submitted successfully." + }, + "qrScanned": "QR code for {resource} scanned successfully.", + "bluetoothConnected": "Connected to {resource} via Bluetooth successfully.", + "operationCompleted": "{operation} on {resource} completed successfully." + }, + "error": { + "general": "An error occurred. Please try again later.", + "fetch": "Failed to retrieve {resource}. Please try again later.", + "fetchLoc": "Failed to retrieve Localized data. Defaulting to English.", + "create": "Failed to create {resource}. Please check your input and try again.", + "update": "Failed to update {resource}. Please check your input and try again.", + "delete": "Failed to delete {resource}. Please try again later.", + "missingParameter": "Oops! {field} is missing. Please provide all necessary information and try again.", + "invalidQR": "Invalid QR code for {resource}. Please try again.", + "limitReached": "You've reached the maximum limit of {limit} for {resource}.", + "unauthorized": "Unauthorized. Please log in again.", + "loginFailed": "Login failed. Please check your credentials and try again.", + "accessDenied": "Access forbidden. You don't have permission to perform this action.", + "notFound": "{Resource} not found. Please check and try again.", + "timeout": "Request timed out. Please try again later.", + "invalidData": "Unable to process the request. Please check your input and try again.", + "serverError": "Server error. Please try again later.", + "serviceUnavailable": "Service unavailable. Please try again later.", + "duplicateEntry": "A {resource} with this information already exists.", + "dependencyIssue": "Unable to perform this action due to dependencies on {resource}." + }, + "networkError": { + "general": "Unable to connect. Please check your internet connection." + } + } +} +} \ No newline at end of file diff --git a/mock/fr-CA.json b/mock/fr-CA.json new file mode 100644 index 00000000..cad4bd63 --- /dev/null +++ b/mock/fr-CA.json @@ -0,0 +1,452 @@ +{ + "status": "success", + "message": "Loaclization values retrieved successfully", + "data": { + "common": { + "name": "Nom", + "home": "Accueil", + "schedule": "Horaire", + "account": "Compte", + "reports": "Rapports", + "alerts": "Alertes", + "details": "Détails", + "start": "Démarrer", + "set": "Définir", + "select": "Sélectionner", + "next": "Suivant", + "finish": "Terminer", + "cancel": "Annuler", + "back": "Retour", + "time": "Temps", + "distance": "Distance", + "duration": "Durée", + "cost": "Coût", + "used": "Utilisé", + "remove": "Supprimer", + "centerQR": "Centrez le code QR dans la vue de la caméra", + "username": "Nom d'utilisateur", + "password": "Mot de passe", + "vehicle": "Véhicule", + "charger": "Chargeur", + "add": "Ajouter", + "selected": "Sélectionné", + "unknonwn": "Inconnu" + }, + "btn": { + "ok": "OK", + "getStarted": "Commencer", + "exitSetup": "Quitter la configuration", + "haveAccount": "Vous avez déjà un compte ?", + "tryAgain": "Réessayer", + "login": "Se connecter", + "loggingIn": "Connexion en cours...", + "logout": "Déconnexion", + "forgotPassword": "Mot de passe oublié", + "resetPassword": "Réinitialiser le mot de passe", + "updatePassword": "Mettre à jour le mot de passe", + "acceptEula": "J'ai lu et j'accepte les conditions d'utilisation", + "tryAnotherSsid": "Essayer un autre SSID", + "openCamera": "Ouvrir la caméra", + "addVehicleWDongle": "Ajouter un véhicule avec un dongle", + "removeVehicle": "Supprimer le véhicule", + "removeDongle": "Supprimer le dongle", + "removeCharger": "Supprimer le chargeur", + "removeHousehold": "Supprimer le foyer", + "changeIcon": "Changer l'icône", + "addHousehold": "Ajouter un foyer", + "support": "Support", + "accessaries": "Accessoires", + "appHome": "Aller à l'accueil de l'application", + "scanQRCode": "Scanner le code QR", + "orScanQRCode": "Ou scanner le code QR", + "tryAnotherWayToConnect": "Essayer une autre méthode de connexion", + "confirm": "Confirmer", + "goBack": "Retour", + "selectDiffNetwork": "Sélectionner un réseau différent", + "loginPage": "Aller à la page de connexion" + }, + "hint": { + "choose": "Choisir", + "enter": "Entrer", + "vehicleName": "Entrez le nom du véhicule", + "nameYourCharger": "Nommez votre chargeur", + "customRates": "Définir des tarifs personnalisés", + "optional": "Optionnel", + "phoneNumber": "Entrez le numéro de téléphone", + "email": "Entrez l'adresse e-mail", + "emailOrUsername": "Entrez l'e-mail ou le nom d'utilisateur", + "password": "Entrez le mot de passe", + "address": "Entrez l'adresse", + "address2": "Adresse 2 (optionnel)", + "city": "Entrez la ville", + "zipCode": "Entrez le code postal" + }, + "validation": { + "textfield": { + "username": "Entrez le nom d'utilisateur", + "validUsername": "Entrez un nom d'utilisateur valide", + "password": "Entrez un mot de passe", + "validPassword": "Entrez un mot de passe valide", + "confPassword": "Le mot de passe entré ne correspond pas", + "verificationCode": "Entrez le code de vérification", + "validVerificationCode": "Entrez un code de vérification valide", + "chargerName": "Entrez un nom pour votre chargeur", + "validChargerName": "Entrez un nom valide pour votre chargeur", + "vehicleName": "Entrez le nom du véhicule", + "validVehicleName": "Entrez un nom valide pour votre véhicule", + "householdName": "Entrez un nom de foyer valide", + "streetAddress": "Entrez une adresse valide", + "city": "Entrez une ville valide", + "zipCode": "Entrez un code postal valide", + "email": "Entrez votre adresse e-mail", + "validEmail": "Entrez une adresse e-mail valide", + "phone": "Entrez votre numéro de téléphone", + "validPhone": "Entrez un numéro de téléphone valide", + "lowMilesThreshold": "Entrez un seuil de kilométrage bas valide", + "arrivalAlertTime": "Entrez un temps valide en secondes", + "minimumMilesThreshold": "Entrez un seuil de kilométrage minimum valide", + "maxChargeCost": "Entrez un coût de charge maximum", + "validMaxChargeCost": "Entrez un coût de charge maximum valide" + }, + "field": { + "breakerSize": "Sélectionnez une taille de disjoncteur", + "maxChargeCurrent": "Sélectionnez un courant de charge maximum", + "reportingUnits": "Sélectionnez les unités de rapport", + "state": "Sélectionnez un état valide", + "country": "Sélectionnez un pays valide", + "method": "Sélectionnez une méthode de tarification valide", + "utility": "Sélectionnez un fournisseur d'énergie valide", + "rateProgram": "Sélectionnez un programme tarifaire valide", + "rate": "Entrez des tarifs valides", + "alertTime": "Veuillez entrer une heure d'alerte valide" + } + }, + "status": { + "charging": "En charge", + "idle": "Inactif", + "pluggedIn": "Branché", + "finishedCharging": "Charge terminée", + "scheduled": "Programmé", + "chargingError": "Erreur de charge" + }, + "signalStrength": { + "strong": "Fort", + "moderate": "Modéré", + "weak": "Faible" + }, + "connectionStatus": { + "connected": "Connecté", + "disconnected": "Déconnecté" + }, + "oob": { + "setup": "Configuration", + "setupGuideline": "Suivez les étapes guidées ci-dessous pour configurer et commencer à utiliser votre nouveau chargeur.", + "accountWizDesc": "Configurez l'adresse, les contacts et les tarifs d'électricité", + "chargerWizDesc": "Installez et configurez votre chargeur", + "vehicleWizDesc": "Connectez le dongle et configurez votre véhicule", + "setupComplete": "Votre configuration initiale est terminée", + "congratulations": "Félicitations" + }, + "dialog": { + "exitSetup": "Êtes-vous sûr de vouloir quitter la configuration ?", + "resourceRemovalDesc": "Ce {resource} sera supprimé de votre compte.", + "removalConfirmation": "{resource} supprimé de votre compte" + }, + "charger": { + "wizard": { + "addChargerTitle": "Ajouter un chargeur", + "pluginYourCharger": "Branchez votre chargeur", + "configureCharger": "Configurer le chargeur", + "pluginYourChargerDesc": "Une fois votre chargeur branché et le voyant Wi-Fi rouge allumé, vous êtes prêt à l'ajouter à votre compte.", + "connectYourCharger": "Connectez votre chargeur", + "bluetoothConnectionDesc": "Tenez-vous près de votre chargeur et utilisez le numéro sur le côté pour le sélectionner dans cette liste d'appareils Bluetooth disponibles :", + "bluetoothDevices": "Appareils Bluetooth", + "connectYourChargerDesc": "Utilisez votre caméra pour scanner le code QR sur le côté de votre chargeur.", + "wifiSetup": "Configurer le Wi-Fi du chargeur", + "wifiSelectionDesc": "Sélectionnez une connexion Wi-Fi pour votre chargeur", + "wifiNetworks": "Réseaux", + "wifiPasswordTitle": "Mot de passe Wi-Fi pour {name}", + "confirmConnection": "Confirmer la connexion du chargeur", + "confirmConnectionDesc": "Le voyant Wi-Fi du chargeur clignotera en bleu lorsqu'il sera connecté à Internet.", + "connecting": "Chargeur en cours de connexion au Wi-Fi...", + "connected": "Chargeur connecté", + "failed": "Impossible de connecter le chargeur !" + }, + "startCharging": "Commencer la charge", + "stopCharging": "Arrêter la charge", + "scheduledStart": "Démarrage programmé", + "scheduleCharging": "Programmer la charge", + "chargeRate": "Taux de charge", + "estChargeTime": "Temps de charge estimé", + "estChargeCost": "Coût de charge estimé", + "completeBy": "Terminé avant", + "info": "Informations", + "status": "Statut", + "connectedVehicle": "Véhicule connecté", + "breakerSize": "Taille du disjoncteur", + "maxChargeCurrent": "Courant de charge maximum", + "modelNumber": "Numéro de modèle", + "serialNumber": "Numéro de série", + "softwareVersion": "Version du logiciel", + "panelLedLighting": "Éclairage LED du panneau", + "wifiSec": "Paramètres Wi-Fi", + "wifi": { + "ssid": "SSID", + "mac": "MAC", + "connectionStatus": "Statut de connexion", + "signalStrength": "Force du signal", + "rssi": "RSSI", + "ipAddress": "Adresse IP", + "lastContact": "Dernier contact" + }, + "authSec": "Autorisation", + "auth": { + "none": "Aucune", + "always": "Demander à chaque fois", + "devicePresent": "Quand un appareil autorisé est présent" + }, + "specialOpsSec": "Opérations spéciales", + "manageAlerts": "Gérer les alertes de l'appareil", + "reboot": "Redémarrer le chargeur", + "factoryReset": "Restaurer les paramètres d'usine", + "resetFault": "Réinitialiser l'erreur de défaut du chargeur" + }, + "schedule": { + "options": "Options de programmation", + "custom": "Programmation personnalisée", + "onAllDay": "Activé toute la journée", + "scheduledCharging": "Charge programmée", + "smart": "Programmation intelligente", + "monday": "Lundi", + "tuesday": "Mardi", + "wednesday": "Mercredi", + "thursday": "Jeudi", + "friday": "Vendredi", + "saturday": "Samedi", + "sunday": "Dimanche", + "selectHours": "Sélectionnez les heures où la charge est disponible", + "timeSelection": { + "ampm": "La charge sera disponible de {morningSession} et {eveningSession}. Veuillez noter que toutes les heures sont dans le fuseau horaire du chargeur.", + "am": "La charge sera disponible de {morningSession}. Veuillez noter que toutes les heures sont dans le fuseau horaire du chargeur.", + "pm": "La charge sera disponible de {eveningSession}. Veuillez noter que toutes les heures sont dans le fuseau horaire du chargeur.", + "none": "Aucune heure de charge disponible. Veuillez configurer votre programmation." + } + }, + "account": { + "phoneNumber": "Numéro de téléphone", + "email": "E-mail", + "wizard": { + "title": "Créer un compte", + "welcomeDesc": "Créez votre compte avec votre adresse domicile et sélectionnez vos informations de fournisseur d'énergie.", + "createCredentialsDesc": "Créez votre nom d'utilisateur et mot de passe", + "contactInformationDesc": "Informations de contact par défaut" + }, + "reset": { + "resetPassSuccess": "Réinitialisation du mot de passe réussie", + "forgotPassDesc": "Entrez le nom d'utilisateur de votre compte Danlaw", + "checkYourEmailDesc": "Veuillez vérifier votre e-mail pour un message de notre part contenant un code pour réinitialiser votre mot de passe.", + "resetPassSuccessDesc": "Votre mot de passe a été mis à jour. Veuillez vous connecter avec votre nouveau mot de passe" + }, + "confPassword": "Confirmer le mot de passe", + "verification": "Code de vérification" + }, + "household": { + "title": "Foyer", + "wizard": { + "title": "Créer un foyer", + "nameYourHouseholdDesc": "Nommez votre foyer", + "householdAddressDesc": "Créez l'adresse de votre foyer pour le chargeur", + "useMyLocation": "Utiliser ma position actuelle", + "pricingMethodDesc": "Sélectionnez la méthode de tarification de la charge", + "utilityCompanyDesc": "Sélectionnez le fournisseur d'énergie", + "rateProgramDesc": "Sélectionnez le programme tarifaire", + "customRatesDesc": "Sélectionnez les tarifs personnalisés", + "customRates": { + "subTitle": "Entrez les tarifs par kWh pour ces périodes tarifaires standard", + "weekdayDaytime": "Tarif en semaine (journée)", + "weekdayNighttime": "Tarif en semaine (nuit)", + "weekendRate": "Tarif du week-end" + } + }, + "address": "Adresse", + "electricityPricing": "Tarification de l'électricité", + "chargers": "Chargeurs", + "streetAddress1": "Adresse 1", + "streetAddress2": "Adresse 2", + "city": "Ville", + "state": "État", + "zipCode": "Code postal", + "country": "Pays", + "pricingMethod": { + "short": "Méthode", + "long": "Méthode de tarification", + "chargerEstCost": "Estimation des coûts par le chargeur", + "utilityRates": "Basée sur le fournisseur d'énergie", + "manual": "Manuelle" + }, + "utility": "Fournisseur d'énergie", + "rateProgram": "Programme tarifaire", + "rate": "Tarif" + }, + "vehicle": { + "title": "Véhicule", + "wizard": { + "addVehicleTitle": "Ajouter un véhicule", + "addDongle": "Ajouter un dongle", + "connectDongle": "Connecter votre dongle", + "dongleDesc": "Localisez le dongle que vous souhaitez attribuer à votre compte", + "vehicleIconDesc": "Choisissez une icône pour votre véhicule", + "scanDongleQR": "Scanner le code QR du dongle", + "scanDongleQRDesc": "Utilisez votre caméra pour scanner le code QR sur le dongle.", + "installDongle": "Installer le dongle", + "installDongleInVehicle": "Installez le dongle dans votre véhicule", + "installDongleInfo": "Une fois le dongle branché dans votre véhicule, l'application confirmera votre connexion.", + "noDongleFound": "Aucun dongle trouvé", + "installDongleLEDConf": "Assurez-vous que la LED du dongle s'allume. Si ce n'est pas le cas, veuillez allumer votre véhicule.", + "verifyingComms": "Vérification des communications", + "primaryUserInfo": "Entrez les informations de l'utilisateur principal", + "contactPhone": "Téléphone de contact", + "contactEmail": "E-mail de contact" + }, + "nameVehicle": "Nommez votre véhicule", + "lastVehicleInfo": "Dernières informations du véhicule connecté", + "status": "Statut", + "vin": "NIV", + "year": "Année", + "make": "Marque", + "model": "Modèle", + "battery": "Batterie", + "stateOfCharge": "État de charge", + "batteryLife": "Durée de vie de la batterie", + "estMilesAvailable": "Kilométrage estimé disponible", + "failureCodes": "Codes de défaillance", + "diagnosticsCodes": "Codes de diagnostic", + "dongleInfo": "Informations du dongle", + "dongleId": "ID du dongle", + "lastConnected": "Dernière connexion", + "alerts": "Alertes", + "manageAlerts": "Gérer les alertes du véhicule", + "ass": { + "title": "Système de sécurité active", + "abs": "Système antiblocage des roues (ABS)", + "esc": "Contrôle électronique de stabilité (ESC)", + "tc": "Contrôle de traction", + "tpms": "Type de système de surveillance de la pression des pneus (TPMS)", + "note": "Note sur le système de sécurité active" + }, + "eng": { + "title": "Moteur", + "numCylinders": "Nombre de cylindres du moteur", + "displacementCC": "Cylindrée (CC)", + "displacementCI": "Cylindrée (CI)", + "displacementL": "Cylindrée (L)", + "engineModel": "Modèle du moteur" + }, + "body": { + "title": "Extérieur/Carrosserie", + "bodyClass": "Classe de carrosserie", + "doors": "Portes", + "windows": "Fenêtres" + } + }, + "reports": { + "vehicleEnergyReport": "Rapport énergétique du véhicule", + "tw": { + "title": "Période", + "past31": "31 derniers jours", + "week": "Cette semaine", + "month": "Ce mois-ci", + "year": "Cette année" + }, + "info": { + "barSelection": "Sélectionnez un segment de barre pour voir plus de détails" + }, + "viewTrips": "Voir les trajets", + "trips": "Trajets", + "viewEvents": "Voir les événements de charge", + "bar": { + "legendHome": "Domicile", + "legendpub": "Public", + "unknownVehicles": "Véhicules inconnus" + }, + "settings": "Paramètres", + "vehiclesInReport": "Véhicules dans le rapport" + }, + "alerts": { + "charger": "Alertes du chargeur", + "vehicle": "Alertes du véhicule", + "options": "Options d'alerte", + "usePush": "Utiliser les notifications push", + "useText": "Envoyer des alertes par SMS", + "useEmail": "Envoyer des alertes par e-mail", + "recipientPhone": "Téléphone du destinataire", + "recipientEmail": "E-mail du destinataire", + "pluginReminder": "Rappel de branchement", + "chargingStatus": "Statut de charge", + "nightly": "Alerte nocturne", + "nightlyLowMiles": "Alerte nocturne de kilométrage bas", + "time": "Heure de l'alerte", + "lowMilesThreshold": "Seuil de kilométrage bas", + "arrivalTime": "Temps d'alerte d'arrivée (secondes)", + "fullyCharged": "Alerte de véhicule complètement chargé", + "chargingInterrupted": "Alerte de charge interrompue", + "minimumMilesMet": "Kilométrage minimum atteint", + "minimumMilesThreshold": "Seuil de kilométrage minimum", + "highCost": "Alerte de coût élevé", + "maxChargeCost": "Coût de charge maximum" + }, + "error": { + "noInfo": "Aucune information fournie" + }, + "apiResponse": { + "success": { + "general": "Requête réussie.", + "fetch": "{resource} récupéré avec succès.", + "fetchLoc": "Données localisées récupérées avec succès.", + "create": "{resource} créé avec succès.", + "update": "{resource} mis à jour avec succès.", + "delete": "{resource} supprimé avec succès.", + "remove": "{resource} retiré avec succès", + "auth": { + "login": "Connexion réussie.", + "logout": "Déconnexion réussie.", + "passwordReset": { + "codeSent": "Code de réinitialisation envoyé avec succès. Veuillez vérifier votre e-mail.", + "passwordUpdated": "Mot de passe réinitialisé avec succès." + } + }, + "eula": { + "fetched": "CLUF chargé avec succès.", + "submitted": "Acceptation du CLUF soumise avec succès." + }, + "qrScanned": "Code QR pour {resource} scanné avec succès.", + "bluetoothConnected": "Connecté à {resource} via Bluetooth avec succès.", + "operationCompleted": "{operation} sur {resource} terminée avec succès." + }, + "error": { + "general": "Une erreur s'est produite. Veuillez réessayer plus tard.", + "fetch": "Échec de la récupération de {resource}. Veuillez réessayer plus tard.", + "fetchLoc": "Échec de la récupération des données localisées. Utilisation de l'anglais par défaut.", + "create": "Échec de la création de {resource}. Veuillez vérifier vos entrées et réessayer.", + "update": "Échec de la mise à jour de {resource}. Veuillez vérifier vos entrées et réessayer.", + "delete": "Échec de la suppression de {resource}. Veuillez réessayer plus tard.", + "missingParameter": "Oups ! {field} est manquant. Veuillez fournir toutes les informations nécessaires et réessayer.", + "invalidQR": "Code QR invalide pour {resource}. Veuillez réessayer.", + "limitReached": "Vous avez atteint la limite maximale de {limit} pour {resource}.", + "unauthorized": "Non autorisé. Veuillez vous reconnecter.", + "loginFailed": "Échec de la connexion. Veuillez vérifier vos identifiants et réessayer.", + "accessDenied": "Accès interdit. Vous n'avez pas la permission d'effectuer cette action.", + "notFound": "{Resource} non trouvé. Veuillez vérifier et réessayer.", + "timeout": "Délai d'attente dépassé. Veuillez réessayer plus tard.", + "invalidData": "Impossible de traiter la requête. Veuillez vérifier vos entrées et réessayer.", + "serverError": "Erreur serveur. Veuillez réessayer plus tard.", + "serviceUnavailable": "Service indisponible. Veuillez réessayer plus tard.", + "duplicateEntry": "Un {resource} avec ces informations existe déjà.", + "dependencyIssue": "Impossible d'effectuer cette action en raison de dépendances sur {resource}." + }, + "networkError": { + "general": "Impossible de se connecter. Veuillez vérifier votre connexion internet." + } + } +} +} \ No newline at end of file diff --git a/mock/states.json b/mock/states.json new file mode 100644 index 00000000..1f9094eb --- /dev/null +++ b/mock/states.json @@ -0,0 +1,28 @@ +{ + "status": "success", + "message": "States retrieved successfully!", + "data": { + "states": [ + { + "name": "Michigan", + "code": "MI" + }, + { + "name": "California", + "code": "CA" + }, + { + "name": "New York", + "code": "NY" + }, + { + "name": "Texas", + "code": "TX" + }, + { + "name": "Florida", + "code": "FL" + } + ] + } +} \ No newline at end of file diff --git a/package-cleanup.sh b/package-cleanup.sh new file mode 100755 index 00000000..cee414ea --- /dev/null +++ b/package-cleanup.sh @@ -0,0 +1,36 @@ +#!/bin/bash + +# Check if an old package name was provided +if [ "$#" -ne 1 ]; then + echo "Usage: $0 " + exit 1 +fi + +# Assign command line argument to variable for the old package name +old_package_name="$1" + +# Derive the new package name from the directory the script is located in +script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +new_package_name=$(basename "$script_dir") + +# Echo the old and new package names +echo "Old package name: $old_package_name" +echo "New package name: $new_package_name" + +# Update Flutter package names in pubspec.yaml +echo "Updating Flutter package names in pubspec.yaml files..." +find "$script_dir" -type f -name "pubspec.yaml" -exec sh -c ' + sed -i.bak "s/^name:.*/name: $1/" "$2" && echo "Updated $2" && rm "$2.bak" +' _ "$new_package_name" {} \; + +# Update Flutter package imports in Dart files within the /lib directory +echo "Updating Flutter package imports in Dart files..." +lib_path="${script_dir}/lib" +if [ -d "$lib_path" ]; then + find "$lib_path" -type f -name "*.dart" -exec sh -c ' + sed -i.bak "s/package:$2\//package:$1\//g" "$3" && echo "Updated $3" && rm "$3.bak" + ' _ "$new_package_name" "$old_package_name" {} \; +fi + +# Inform user of success +echo "The files have been cleaned to use packages matching $new_package_name" diff --git a/pubspec.lock b/pubspec.lock index d0f6e571..8fd911b6 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -9,14 +9,6 @@ packages: url: "https://pub.dev" source: hosted version: "67.0.0" - _flutterfire_internals: - dependency: transitive - description: - name: _flutterfire_internals - sha256: "554f148e71e9e016d9c04d4af6b103ca3f74a1ceed7d7307b70a0f41e991eb77" - url: "https://pub.dev" - source: hosted - version: "1.3.26" analyzer: dependency: transitive description: @@ -33,14 +25,30 @@ packages: url: "https://pub.dev" source: hosted version: "0.11.3" + ansicolor: + dependency: transitive + description: + name: ansicolor + sha256: "8bf17a8ff6ea17499e40a2d2542c2f481cd7615760c6d34065cb22bfd22e6880" + url: "https://pub.dev" + source: hosted + version: "2.0.2" + archive: + dependency: transitive + description: + name: archive + sha256: cb6a278ef2dbb298455e1a713bda08524a175630ec643a242c399c932a0a1f7d + url: "https://pub.dev" + source: hosted + version: "3.6.1" args: dependency: transitive description: name: args - sha256: eef6c46b622e0494a36c5a12d10d77fb4e855501a91c1b9ef9339326e58f0596 + sha256: "7cf60b9f0cc88203c5a190b4cd62a99feea42759a7fa695010eb5de1c0b2252a" url: "https://pub.dev" source: hosted - version: "2.4.2" + version: "2.5.0" async: dependency: transitive description: @@ -93,10 +101,10 @@ packages: dependency: "direct dev" description: name: build_runner - sha256: "581bacf68f89ec8792f5e5a0b2c4decd1c948e97ce659dc783688c8a88fbec21" + sha256: "3ac61a79bfb6f6cc11f693591063a7f19a7af628dc52f141743edac5c16e8c22" url: "https://pub.dev" source: hosted - version: "2.4.8" + version: "2.4.9" build_runner_core: dependency: transitive description: @@ -117,10 +125,18 @@ packages: dependency: transitive description: name: built_value - sha256: fedde275e0a6b798c3296963c5cd224e3e1b55d0e478d5b7e65e6b540f363a0e + sha256: c7913a9737ee4007efedaffc968c049fd0f3d0e49109e778edc10de9426005cb + url: "https://pub.dev" + source: hosted + version: "8.9.2" + carousel_slider: + dependency: "direct main" + description: + name: carousel_slider + sha256: "9c695cc963bf1d04a47bd6021f68befce8970bcd61d24938e1fb0918cf5d9c42" url: "https://pub.dev" source: hosted - version: "8.9.1" + version: "4.2.1" characters: dependency: transitive description: @@ -161,30 +177,6 @@ packages: url: "https://pub.dev" source: hosted version: "1.1.1" - cloud_firestore: - dependency: "direct main" - description: - name: cloud_firestore - sha256: "621afb8a3752732f865ea2d25e2f5cd01ed16ee5691f5a33958876d0a1cae846" - url: "https://pub.dev" - source: hosted - version: "4.15.9" - cloud_firestore_platform_interface: - dependency: transitive - description: - name: cloud_firestore_platform_interface - sha256: d200d82314548a11a02056ca96861d216ff814a1efe7bcc6595d404202f5212c - url: "https://pub.dev" - source: hosted - version: "6.1.10" - cloud_firestore_web: - dependency: transitive - description: - name: cloud_firestore_web - sha256: "3b3046c23eccc1064328ab010cff4df470b6b1cfd648b82e08a84f5fb210bcb9" - url: "https://pub.dev" - source: hosted - version: "3.10.9" code_builder: dependency: transitive description: @@ -209,6 +201,22 @@ packages: url: "https://pub.dev" source: hosted version: "3.1.1" + country_code: + dependency: "direct main" + description: + name: country_code + sha256: f69ccd5163b1ca43011be9632e33ebe7ffac65e49ce2afcd3e3e5228af5d91fc + url: "https://pub.dev" + source: hosted + version: "1.0.0" + cross_file: + dependency: transitive + description: + name: cross_file + sha256: "55d7b444feb71301ef6b8838dbc1ae02e63dd48c8773f3810ff53bb1e2945b32" + url: "https://pub.dev" + source: hosted + version: "0.3.4+1" crypto: dependency: transitive description: @@ -217,14 +225,22 @@ packages: url: "https://pub.dev" source: hosted version: "3.0.3" + csslib: + dependency: transitive + description: + name: csslib + sha256: "706b5707578e0c1b4b7550f64078f0a0f19dec3f50a178ffae7006b0a9ca58fb" + url: "https://pub.dev" + source: hosted + version: "1.0.0" cupertino_icons: dependency: "direct main" description: name: cupertino_icons - sha256: d57953e10f9f8327ce64a508a355f0b1ec902193f66288e8cb5070e7c47eeb2d + sha256: ba631d1c7f7bef6b729a622b7b752645a2d076dba9976925b8f25725a30e1ee6 url: "https://pub.dev" source: hosted - version: "1.0.6" + version: "1.0.8" custom_lint: dependency: "direct dev" description: @@ -257,16 +273,32 @@ packages: url: "https://pub.dev" source: hosted version: "2.3.6" - desktop_webview_auth: + dio: + dependency: "direct main" + description: + name: dio + sha256: "11e40df547d418cc0c4900a9318b26304e665da6fa4755399a9ff9efd09034b5" + url: "https://pub.dev" + source: hosted + version: "5.4.3+1" + easy_localization: + dependency: "direct main" + description: + name: easy_localization + sha256: fa59bcdbbb911a764aa6acf96bbb6fa7a5cf8234354fc45ec1a43a0349ef0201 + url: "https://pub.dev" + source: hosted + version: "3.0.7" + easy_logger: dependency: transitive description: - name: desktop_webview_auth - sha256: c4dce73346a7be7243c90ac3b1a68586d9f0e2c2710e81e07d758e80a6ebd920 + name: easy_logger + sha256: c764a6e024846f33405a2342caf91c62e357c24b02c04dbc712ef232bf30ffb7 url: "https://pub.dev" source: hosted - version: "0.0.15" + version: "0.0.2" email_validator: - dependency: transitive + dependency: "direct main" description: name: email_validator sha256: e9a90f27ab2b915a27d7f9c2a7ddda5dd752d6942616ee83529b686fc086221b @@ -305,137 +337,64 @@ packages: url: "https://pub.dev" source: hosted version: "7.0.0" - firebase_auth: - dependency: "direct main" - description: - name: firebase_auth - sha256: "3fd6475e60d518c021e70e7d4262db7dac327adf496e71048545da2e0b9ca510" - url: "https://pub.dev" - source: hosted - version: "4.17.9" - firebase_auth_platform_interface: + fixnum: dependency: transitive description: - name: firebase_auth_platform_interface - sha256: d44458576804f246a126fe797547330d2f7bd62069ce12479b583082340a2e4d + name: fixnum + sha256: "25517a4deb0c03aa0f32fd12db525856438902d9c16536311e76cdc57b31d7d1" url: "https://pub.dev" source: hosted - version: "7.1.9" - firebase_auth_web: - dependency: "direct overridden" - description: - path: "packages/firebase_auth/firebase_auth_web" - ref: master - resolved-ref: "04280a31310dbbe51a8e619f031f5190d02e695d" - url: "https://github.com/firebase/flutterfire" - source: git - version: "5.10.0" - firebase_core: + version: "1.1.0" + flutter: dependency: "direct main" - description: - name: firebase_core - sha256: "67bf0d5fd78f12f51c6b54a72f6141314136a1a90e98b1b7c45e7fac883254ed" - url: "https://pub.dev" - source: hosted - version: "2.27.1" - firebase_core_platform_interface: - dependency: transitive - description: - name: firebase_core_platform_interface - sha256: c437ae5d17e6b5cc7981cf6fd458a5db4d12979905f9aafd1fea930428a9fe63 - url: "https://pub.dev" - source: hosted - version: "5.0.0" - firebase_core_web: - dependency: transitive - description: - name: firebase_core_web - sha256: "5377eaac3b9fe8aaf22638d87f92b62784f23572e132dfc029195e84d6cb37de" - url: "https://pub.dev" - source: hosted - version: "2.12.0" - firebase_dynamic_links: - dependency: transitive - description: - name: firebase_dynamic_links - sha256: "6f8180d1641ba274e241a690cd12d1163fdec75d807aa9daa37c27489e7ad3f7" - url: "https://pub.dev" - source: hosted - version: "5.4.18" - firebase_dynamic_links_platform_interface: - dependency: transitive - description: - name: firebase_dynamic_links_platform_interface - sha256: "8b6c1827488fa1af56e9a4834ee9e356c510e35d89d1839761064eabad951cbb" - url: "https://pub.dev" - source: hosted - version: "0.2.6+26" - firebase_ui_auth: + description: flutter + source: sdk + version: "0.0.0" + flutter_blue_plus: dependency: "direct main" description: - name: firebase_ui_auth - sha256: d138c7a2c53e39fc6b4d5c1bea7a6e4d75a7683ff9dbde1865682de517966a4d + name: flutter_blue_plus + sha256: c762a694c2f67b1f492ef19ead2a30ed3254650bafd852cb8933823d13d7c89f url: "https://pub.dev" source: hosted - version: "1.13.1" - firebase_ui_firestore: + version: "1.32.7" + flutter_i18n: dependency: "direct main" description: - name: firebase_ui_firestore - sha256: "49ef3e157828161cde9aaf1441a7098b6f5d7d360eafe5a588e6350f7f32caf9" - url: "https://pub.dev" - source: hosted - version: "1.6.2" - firebase_ui_localizations: - dependency: transitive - description: - name: firebase_ui_localizations - sha256: "816501d26bb9e2a58b5d5f80351d82a50bd2f5c8d4aeb22504c53f91b6c57259" - url: "https://pub.dev" - source: hosted - version: "1.10.2" - firebase_ui_oauth: - dependency: transitive - description: - name: firebase_ui_oauth - sha256: "00003a7ff0b75b081ed9815bad732343e3702d6237f90f1b63b623ba5e8d1410" - url: "https://pub.dev" - source: hosted - version: "1.5.1" - firebase_ui_shared: - dependency: transitive - description: - name: firebase_ui_shared - sha256: f1d07c130a39104d32fba1dab274b7bcb13be2bf4e652624a4ccabb58f9781f1 + name: flutter_i18n + sha256: "0e88d479c11c4d3d46b2610fb7e94a13ac34fbf5d3adc265afb1645361ec5062" url: "https://pub.dev" source: hosted - version: "1.4.1" - fixnum: - dependency: transitive + version: "0.36.0" + flutter_launcher_icons: + dependency: "direct dev" description: - name: fixnum - sha256: "25517a4deb0c03aa0f32fd12db525856438902d9c16536311e76cdc57b31d7d1" + name: flutter_launcher_icons + sha256: "526faf84284b86a4cb36d20a5e45147747b7563d921373d4ee0559c54fcdbcea" url: "https://pub.dev" source: hosted - version: "1.1.0" - flutter: - dependency: "direct main" - description: flutter - source: sdk - version: "0.0.0" + version: "0.13.1" flutter_lints: dependency: "direct dev" description: name: flutter_lints - sha256: e2a421b7e59244faef694ba7b30562e489c2b489866e505074eb005cd7060db7 + sha256: "9e8c3858111da373efc5aa341de011d9bd23e2c5c5e0c62bccf32438e192d7b1" url: "https://pub.dev" source: hosted - version: "3.0.1" + version: "3.0.2" flutter_localizations: - dependency: transitive + dependency: "direct main" description: flutter source: sdk version: "0.0.0" + flutter_native_splash: + dependency: "direct main" + description: + name: flutter_native_splash + sha256: edf39bcf4d74aca1eb2c1e43c3e445fd9f494013df7f0da752fefe72020eedc0 + url: "https://pub.dev" + source: hosted + version: "2.4.0" flutter_riverpod: dependency: "direct main" description: @@ -444,6 +403,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.5.1" + flutter_secure_storage: + dependency: "direct main" + description: + name: flutter_secure_storage + sha256: "9f3dd2ac3b6875b0fde5b04734789c3ef35ba3965c18e99dd564a7a2f8056df6" + url: "https://pub.dev" + source: hosted + version: "4.2.1" flutter_svg: dependency: "direct main" description: @@ -452,6 +419,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.0.10+1" + flutter_svg_provider: + dependency: "direct main" + description: + name: flutter_svg_provider + sha256: cda47ab350671ba51ae4605d48f4c82fa5a2c399d22ebda367c1b407234c5048 + url: "https://pub.dev" + source: hosted + version: "1.0.7" flutter_test: dependency: "direct dev" description: flutter @@ -474,10 +449,90 @@ packages: dependency: transitive description: name: frontend_server_client - sha256: "408e3ca148b31c20282ad6f37ebfa6f4bdc8fede5b74bc2f08d9d92b55db3612" + sha256: f64a0333a82f30b0cca061bc3d143813a486dc086b574bfb233b7c1372427694 + url: "https://pub.dev" + source: hosted + version: "4.0.0" + geocoding: + dependency: "direct main" + description: + name: geocoding + sha256: d580c801cba9386b4fac5047c4c785a4e19554f46be42f4f5e5b7deacd088a66 + url: "https://pub.dev" + source: hosted + version: "3.0.0" + geocoding_android: + dependency: transitive + description: + name: geocoding_android + sha256: "1b13eca79b11c497c434678fed109c2be020b158cec7512c848c102bc7232603" + url: "https://pub.dev" + source: hosted + version: "3.3.1" + geocoding_ios: + dependency: transitive + description: + name: geocoding_ios + sha256: "94ddba60387501bd1c11e18dca7c5a9e8c645d6e3da9c38b9762434941870c24" + url: "https://pub.dev" + source: hosted + version: "3.0.1" + geocoding_platform_interface: + dependency: transitive + description: + name: geocoding_platform_interface + sha256: "8c2c8226e5c276594c2e18bfe88b19110ed770aeb7c1ab50ede570be8b92229b" url: "https://pub.dev" source: hosted version: "3.2.0" + geolocator: + dependency: "direct main" + description: + name: geolocator + sha256: "149876cc5207a0f5daf4fdd3bfcf0a0f27258b3fe95108fa084f527ad0568f1b" + url: "https://pub.dev" + source: hosted + version: "12.0.0" + geolocator_android: + dependency: transitive + description: + name: geolocator_android + sha256: "00c7177a95823dd3ee35ef42fd8666cd27d219ae14cea472ac76a21dff43000b" + url: "https://pub.dev" + source: hosted + version: "4.6.0" + geolocator_apple: + dependency: transitive + description: + name: geolocator_apple + sha256: bc2aca02423ad429cb0556121f56e60360a2b7d694c8570301d06ea0c00732fd + url: "https://pub.dev" + source: hosted + version: "2.3.7" + geolocator_platform_interface: + dependency: transitive + description: + name: geolocator_platform_interface + sha256: "386ce3d9cce47838355000070b1d0b13efb5bc430f8ecda7e9238c8409ace012" + url: "https://pub.dev" + source: hosted + version: "4.2.4" + geolocator_web: + dependency: transitive + description: + name: geolocator_web + sha256: "7a22f400d831f924a89d931ba126a10e6b8b437f31e6b9311320435f3e1571bd" + url: "https://pub.dev" + source: hosted + version: "4.0.0" + geolocator_windows: + dependency: transitive + description: + name: geolocator_windows + sha256: "53da08937d07c24b0d9952eb57a3b474e29aae2abf9dd717f7e1230995f13f0e" + url: "https://pub.dev" + source: hosted + version: "0.2.3" glob: dependency: transitive description: @@ -490,10 +545,18 @@ packages: dependency: "direct main" description: name: go_router - sha256: c247a4f76071c3b97bb5ae8912968870d5565644801c5e09f3bc961b4d874895 + sha256: "39dd52168d6c59984454183148dc3a5776960c61083adfc708cc79a7b3ce1ba8" url: "https://pub.dev" source: hosted - version: "12.1.1" + version: "14.2.1" + google_fonts: + dependency: "direct main" + description: + name: google_fonts + sha256: b1ac0fe2832c9cc95e5e88b57d627c5e68c223b9657f4b96e1487aa9098c7b82 + url: "https://pub.dev" + source: hosted + version: "6.2.1" graphs: dependency: transitive description: @@ -502,6 +565,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.3.1" + hexcolor: + dependency: "direct main" + description: + name: hexcolor + sha256: c07f4bbb9095df87eeca87e7c69e8c3d60f70c66102d7b8d61c4af0453add3f6 + url: "https://pub.dev" + source: hosted + version: "3.0.1" hotreloader: dependency: transitive description: @@ -510,6 +581,14 @@ packages: url: "https://pub.dev" source: hosted version: "4.2.0" + html: + dependency: transitive + description: + name: html + sha256: "3a7812d5bcd2894edf53dfaf8cd640876cf6cef50a8f238745c8b8120ea74d3a" + url: "https://pub.dev" + source: hosted + version: "0.15.4" http: dependency: transitive description: @@ -534,14 +613,22 @@ packages: url: "https://pub.dev" source: hosted version: "4.0.2" + image: + dependency: transitive + description: + name: image + sha256: "2237616a36c0d69aef7549ab439b833fb7f9fb9fc861af2cc9ac3eedddd69ca8" + url: "https://pub.dev" + source: hosted + version: "4.2.0" intl: dependency: "direct main" description: name: intl - sha256: "3bc132a9dbce73a7e4a21a17d06e1878839ffbf975568bc875c60537824b0c4d" + sha256: d6f56758b7d3014a48af9701c085700aac781a92a87a62b1333b46d8879661cf url: "https://pub.dev" source: hosted - version: "0.18.1" + version: "0.19.0" io: dependency: transitive description: @@ -554,42 +641,58 @@ packages: dependency: transitive description: name: js - sha256: f2c445dce49627136094980615a031419f7f3eb393237e4ecd97ac15dea343f3 + sha256: c1b2e9b5ea78c45e1a0788d29606ba27dc5f71f019f32ca5140f61ef071838cf url: "https://pub.dev" source: hosted - version: "0.6.7" + version: "0.7.1" json_annotation: - dependency: transitive + dependency: "direct main" description: name: json_annotation - sha256: b10a7b2ff83d83c777edba3c6a0f97045ddadd56c944e1a23a3fdf43a1bf4467 + sha256: "1ce844379ca14835a50d2f019a3099f419082cfdd231cd86a142af94dd5c6bb1" + url: "https://pub.dev" + source: hosted + version: "4.9.0" + json_serializable: + dependency: "direct dev" + description: + name: json_serializable + sha256: ea1432d167339ea9b5bb153f0571d0039607a873d6e04e0117af043f14a1fd4b url: "https://pub.dev" source: hosted - version: "4.8.1" + version: "6.8.0" + jwt_decode: + dependency: "direct main" + description: + name: jwt_decode + sha256: d2e9f68c052b2225130977429d30f187aa1981d789c76ad104a32243cfdebfbb + url: "https://pub.dev" + source: hosted + version: "0.3.1" leak_tracker: dependency: transitive description: name: leak_tracker - sha256: "78eb209deea09858f5269f5a5b02be4049535f568c07b275096836f01ea323fa" + sha256: "7f0df31977cb2c0b88585095d168e689669a2cc9b97c309665e3386f3e9d341a" url: "https://pub.dev" source: hosted - version: "10.0.0" + version: "10.0.4" leak_tracker_flutter_testing: dependency: transitive description: name: leak_tracker_flutter_testing - sha256: b46c5e37c19120a8a01918cfaf293547f47269f7cb4b0058f21531c2465d6ef0 + sha256: "06e98f569d004c1315b991ded39924b21af84cf14cc94791b8aea337d25b57f8" url: "https://pub.dev" source: hosted - version: "2.0.1" + version: "3.0.3" leak_tracker_testing: dependency: transitive description: name: leak_tracker_testing - sha256: a597f72a664dbd293f3bfc51f9ba69816f84dcd403cdac7066cb3f6003f3ab47 + sha256: "6ba465d5d76e67ddf503e1161d1f4a6bc42306f9d66ca1e8f079a47290fb06d3" url: "https://pub.dev" source: hosted - version: "2.0.1" + version: "3.0.1" lints: dependency: transitive description: @@ -626,10 +729,10 @@ packages: dependency: transitive description: name: meta - sha256: d584fa6707a52763a52446f02cc621b077888fb63b93bbcb1143a7be5a0c0c04 + sha256: "7687075e408b093f36e6bbf6c91878cc0d4cd10f409506f7bc996f68220b9136" url: "https://pub.dev" source: hosted - version: "1.11.0" + version: "1.12.0" mime: dependency: transitive description: @@ -638,6 +741,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.5" + mobile_scanner: + dependency: "direct main" + description: + name: mobile_scanner + sha256: b8c0e9afcfd52534f85ec666f3d52156f560b5e6c25b1e3d4fe2087763607926 + url: "https://pub.dev" + source: hosted + version: "5.1.1" mocktail: dependency: "direct dev" description: @@ -670,6 +781,30 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.1" + path_provider: + dependency: "direct main" + description: + name: path_provider + sha256: c9e7d3a4cd1410877472158bee69963a4579f78b68c65a2b7d40d1a7a88bb161 + url: "https://pub.dev" + source: hosted + version: "2.1.3" + path_provider_android: + dependency: transitive + description: + name: path_provider_android + sha256: a248d8146ee5983446bf03ed5ea8f6533129a12b11f12057ad1b4a67a2b3b41d + url: "https://pub.dev" + source: hosted + version: "2.2.4" + path_provider_foundation: + dependency: transitive + description: + name: path_provider_foundation + sha256: f234384a3fdd67f989b4d54a5d73ca2a6c422fa55ae694381ae0f4375cd1ea16 + url: "https://pub.dev" + source: hosted + version: "2.4.0" path_provider_linux: dependency: transitive description: @@ -738,10 +873,10 @@ packages: dependency: transitive description: name: pubspec_parse - sha256: c63b2876e58e194e4b0828fcb080ad0e06d051cb607a6be51a9e084f47cb9367 + sha256: c799b721d79eb6ee6fa56f00c04b472dcd44a30d258fac2174a6ec57302678f8 url: "https://pub.dev" source: hosted - version: "1.2.3" + version: "1.3.0" random_string: dependency: "direct dev" description: @@ -750,6 +885,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.3.1" + reflectable: + dependency: "direct main" + description: + name: reflectable + sha256: cdc1a278a2e9769abafaf9ba54ce1fd3432b2a38360e14b87ea6344f715340de + url: "https://pub.dev" + source: hosted + version: "4.0.6" riverpod: dependency: transitive description: @@ -798,30 +941,46 @@ packages: url: "https://pub.dev" source: hosted version: "0.27.7" + share_plus: + dependency: "direct main" + description: + name: share_plus + sha256: ef3489a969683c4f3d0239010cc8b7a2a46543a8d139e111c06c558875083544 + url: "https://pub.dev" + source: hosted + version: "9.0.0" + share_plus_platform_interface: + dependency: transitive + description: + name: share_plus_platform_interface + sha256: "0f9e4418835d1b2c3ae78fdb918251959106cefdbc4dd43526e182f80e82f6d4" + url: "https://pub.dev" + source: hosted + version: "4.0.0" shared_preferences: dependency: "direct main" description: name: shared_preferences - sha256: "81429e4481e1ccfb51ede496e916348668fd0921627779233bd24cc3ff6abd02" + sha256: d3bbe5553a986e83980916ded2f0b435ef2e1893dfaa29d5a7a790d0eca12180 url: "https://pub.dev" source: hosted - version: "2.2.2" + version: "2.2.3" shared_preferences_android: dependency: transitive description: name: shared_preferences_android - sha256: "8568a389334b6e83415b6aae55378e158fbc2314e074983362d20c562780fb06" + sha256: "1ee8bf911094a1b592de7ab29add6f826a7331fb854273d55918693d5364a1f2" url: "https://pub.dev" source: hosted - version: "2.2.1" + version: "2.2.2" shared_preferences_foundation: dependency: transitive description: name: shared_preferences_foundation - sha256: "7708d83064f38060c7b39db12aefe449cb8cdc031d6062280087bc4cdb988f5c" + sha256: "0a8a893bf4fd1152f93fec03a415d11c27c74454d96e2318a7ac38dd18683ab7" url: "https://pub.dev" source: hosted - version: "2.3.5" + version: "2.4.0" shared_preferences_linux: dependency: transitive description: @@ -870,6 +1029,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.4" + simple_circular_progress_bar: + dependency: "direct main" + description: + name: simple_circular_progress_bar + sha256: e661ca942fbc617298e975b41fde19003d995de73ca6c2a1526c54d52f07151b + url: "https://pub.dev" + source: hosted + version: "1.0.2" sky_engine: dependency: transitive description: flutter @@ -883,6 +1050,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.5.0" + source_helper: + dependency: transitive + description: + name: source_helper + sha256: "6adebc0006c37dd63fe05bca0a929b99f06402fc95aa35bf36d67f5c06de01fd" + url: "https://pub.dev" + source: hosted + version: "1.3.4" source_span: dependency: transitive description: @@ -951,10 +1126,10 @@ packages: dependency: transitive description: name: test_api - sha256: "5c2f730018264d276c20e4f1503fd1308dfbbae39ec8ee63c5236311ac06954b" + sha256: "9955ae474176f7ac8ee4e989dadfb411a58c30415bcfb648fa04b2b8a03afa7f" url: "https://pub.dev" source: hosted - version: "0.6.1" + version: "0.7.0" timing: dependency: transitive description: @@ -963,6 +1138,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.1" + toml: + dependency: transitive + description: + name: toml + sha256: "9968de24e45b632bf1a654fe1ac7b6fe5261c349243df83fd262397799c45a2d" + url: "https://pub.dev" + source: hosted + version: "0.15.0" typed_data: dependency: transitive description: @@ -971,14 +1154,94 @@ packages: url: "https://pub.dev" source: hosted version: "1.3.2" - uuid: + universal_io: + dependency: transitive + description: + name: universal_io + sha256: "1722b2dcc462b4b2f3ee7d188dad008b6eb4c40bbd03a3de451d82c78bba9aad" + url: "https://pub.dev" + source: hosted + version: "2.2.2" + url_launcher: + dependency: "direct main" + description: + name: url_launcher + sha256: "21b704ce5fa560ea9f3b525b43601c678728ba46725bab9b01187b4831377ed3" + url: "https://pub.dev" + source: hosted + version: "6.3.0" + url_launcher_android: + dependency: transitive + description: + name: url_launcher_android + sha256: ceb2625f0c24ade6ef6778d1de0b2e44f2db71fded235eb52295247feba8c5cf + url: "https://pub.dev" + source: hosted + version: "6.3.3" + url_launcher_ios: + dependency: transitive + description: + name: url_launcher_ios + sha256: "7068716403343f6ba4969b4173cbf3b84fc768042124bc2c011e5d782b24fe89" + url: "https://pub.dev" + source: hosted + version: "6.3.0" + url_launcher_linux: + dependency: transitive + description: + name: url_launcher_linux + sha256: ab360eb661f8879369acac07b6bb3ff09d9471155357da8443fd5d3cf7363811 + url: "https://pub.dev" + source: hosted + version: "3.1.1" + url_launcher_macos: + dependency: transitive + description: + name: url_launcher_macos + sha256: "9a1a42d5d2d95400c795b2914c36fdcb525870c752569438e4ebb09a2b5d90de" + url: "https://pub.dev" + source: hosted + version: "3.2.0" + url_launcher_platform_interface: + dependency: transitive + description: + name: url_launcher_platform_interface + sha256: "552f8a1e663569be95a8190206a38187b531910283c3e982193e4f2733f01029" + url: "https://pub.dev" + source: hosted + version: "2.3.2" + url_launcher_web: dependency: transitive + description: + name: url_launcher_web + sha256: "8d9e750d8c9338601e709cd0885f95825086bd8b642547f26bda435aade95d8a" + url: "https://pub.dev" + source: hosted + version: "2.3.1" + url_launcher_windows: + dependency: transitive + description: + name: url_launcher_windows + sha256: ecf9725510600aa2bb6d7ddabe16357691b6d2805f66216a97d1b881e21beff7 + url: "https://pub.dev" + source: hosted + version: "3.1.1" + us_states: + dependency: "direct main" + description: + name: us_states + sha256: "1abc927838eb3db1a7e3f8904400da7b62cd6cd6b21844b2b3f3dbbd1f299475" + url: "https://pub.dev" + source: hosted + version: "1.2.0" + uuid: + dependency: "direct dev" description: name: uuid - sha256: cd210a09f7c18cbe5a02511718e0334de6559871052c90a90c0cca46a4aa81c8 + sha256: "814e9e88f21a176ae1359149021870e87f7cddaf633ab678a5d2b0bff7fd1ba8" url: "https://pub.dev" source: hosted - version: "4.3.3" + version: "4.4.0" vector_graphics: dependency: transitive description: @@ -1015,10 +1278,10 @@ packages: dependency: transitive description: name: vm_service - sha256: b3d56ff4341b8f182b96aceb2fa20e3dcb336b9f867bc0eafc0de10f1048e957 + sha256: "3923c89304b715fb1eb6423f017651664a03bf5f4b29983627c4da791f74a4ec" url: "https://pub.dev" source: hosted - version: "13.0.0" + version: "14.2.1" watcher: dependency: transitive description: @@ -1028,29 +1291,45 @@ packages: source: hosted version: "1.1.0" web: - dependency: "direct overridden" + dependency: transitive description: name: web - sha256: "4188706108906f002b3a293509234588823c8c979dc83304e229ff400c996b05" + sha256: "97da13628db363c635202ad97068d47c5b8aa555808e7a9411963c533b449b27" url: "https://pub.dev" source: hosted - version: "0.4.2" + version: "0.5.1" web_socket_channel: dependency: transitive description: name: web_socket_channel - sha256: "1d8e795e2a8b3730c41b8a98a2dff2e0fb57ae6f0764a1c46ec5915387d257b2" + sha256: "58c6666b342a38816b2e7e50ed0f1e261959630becd4c879c4f26bfa14aa5a42" + url: "https://pub.dev" + source: hosted + version: "2.4.5" + wifi_scan: + dependency: "direct main" + description: + name: wifi_scan + sha256: cd0b98a611a3206c1bd9e600b9dff3aca27dcc924aa025548b00713e93a27299 url: "https://pub.dev" source: hosted - version: "2.4.4" + version: "0.4.1" win32: dependency: transitive description: name: win32 - sha256: "8cb58b45c47dcb42ab3651533626161d6b67a2921917d8d429791f76972b3480" + sha256: "0eaf06e3446824099858367950a813472af675116bf63f008a4c2a75ae13e9cb" + url: "https://pub.dev" + source: hosted + version: "5.5.0" + world_zipcode_validator: + dependency: "direct main" + description: + name: world_zipcode_validator + sha256: "3b28c56326499531851391d8c1c4fea72fa355b0ad00cdb2bd4146dcaecf2d6d" url: "https://pub.dev" source: hosted - version: "5.3.0" + version: "0.0.2" xdg_directories: dependency: transitive description: @@ -1067,6 +1346,14 @@ packages: url: "https://pub.dev" source: hosted version: "6.5.0" + xml2json: + dependency: transitive + description: + name: xml2json + sha256: "52b7c8d350fdce09545b058982c26689ee89f1eb188cc9910d585665bfe27bc0" + url: "https://pub.dev" + source: hosted + version: "6.2.3" yaml: dependency: transitive description: @@ -1076,5 +1363,5 @@ packages: source: hosted version: "3.1.2" sdks: - dart: ">=3.3.0 <4.0.0" - flutter: ">=3.19.0" + dart: ">=3.4.0 <4.0.0" + flutter: ">=3.22.0" diff --git a/pubspec.yaml b/pubspec.yaml index e1c88619..c81cc3af 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,30 +1,51 @@ -name: starter_architecture_flutter_firebase -description: A new Flutter project. +name: flutter_starter_base_app +description: Danlaw EV Charger -publish_to: 'none' -version: 2.1.0 +publish_to: "none" +version: 1.0.0+7 environment: sdk: ">=3.1.0 <4.0.0" dependencies: - cloud_firestore: ^4.14.0 cupertino_icons: ^1.0.6 equatable: ^2.0.5 - firebase_auth: ^4.16.0 - firebase_core: ^2.24.2 - firebase_ui_auth: ^1.12.0 - firebase_ui_firestore: ^1.6.1 flutter: sdk: flutter + flutter_localizations: + sdk: flutter flutter_riverpod: ^2.4.6 flutter_svg: ^2.0.9 - go_router: 12.1.1 - intl: ^0.18.1 + go_router: ^14.2.1 + intl: ^0.19.0 rxdart: ^0.27.7 shared_preferences: ^2.2.2 - # the annotation package containing @riverpod riverpod_annotation: 2.3.1 + dio: ^5.4.3+1 + json_annotation: ^4.9.0 + world_zipcode_validator: ^0.0.2 + us_states: ^1.2.0 + country_code: ^1.0.0 + hexcolor: ^3.0.1 + carousel_slider: ^4.2.1 + simple_circular_progress_bar: ^1.0.2 + flutter_svg_provider: ^1.0.7 + reflectable: ^4.0.6 + google_fonts: ^6.2.1 + share_plus: ^9.0.0 + mobile_scanner: ^5.1.1 + flutter_blue_plus: ^1.32.7 + wifi_scan: ^0.4.1 + email_validator: ^2.1.17 + flutter_native_splash: ^2.4.0 + path_provider: ^2.1.3 + flutter_i18n: ^0.36.0 + easy_localization: ^3.0.7 + url_launcher: ^6.3.0 + jwt_decode: ^0.3.1 + flutter_secure_storage: ^4.2.1 + geocoding: ^3.0.0 + geolocator: ^12.0.0 dev_dependencies: flutter_test: @@ -32,24 +53,39 @@ dev_dependencies: mocktail: ^1.0.1 random_string: ^2.3.1 flutter_lints: ^3.0.1 - # a tool for running code generators build_runner: ^2.4.8 - # the code generator riverpod_generator: 2.3.6 - # riverpod_lint makes it easier to work with Riverpod riverpod_lint: 2.3.4 - # import custom_lint too as riverpod_lint depends on it custom_lint: 0.5.6 + json_serializable: ^6.7.1 + uuid: ^4.4.0 + flutter_launcher_icons: ^0.13.1 -dependency_overrides: - web: ^0.4.2 - firebase_auth_web: - git: - url: https://github.com/firebase/flutterfire - ref: master - path: packages/firebase_auth/firebase_auth_web +flutter_launcher_icons: + android: "launcher_icon" + ios: true + remove_alpha_ios: true + image_path: "assets/launch_icon.ico" flutter: + generate: true uses-material-design: true assets: - assets/time-tracking.svg + - mock/ + - assets/bottom_navigation_bar/ + - assets/detail_icon/ + - assets/vehicle_icon/ + - assets/charger_icon/ + - assets/charger_icon/large/ + - assets/charger_icon/medium/ + - assets/charger_icon/small/ + - assets/charger_icon/gray/ + - assets/charger_icon/green/ + - assets/charger_icon/blue/ + - assets/charger_icon/red/ + - assets/ + - assets/images/ + - assets/setup_charger/ + - assets/setup_dongle/ + - assets/locale/ diff --git a/test/common_widgets_test/action_text_button_test.dart b/test/common_widgets_test/action_text_button_test.dart new file mode 100644 index 00000000..7ab960db --- /dev/null +++ b/test/common_widgets_test/action_text_button_test.dart @@ -0,0 +1,38 @@ +import 'package:danlaw_charger/src/common_widgets/action_text_button.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + group("action text button", () { + testWidgets("display the text correctly", (WidgetTester tester) async { + const text = "click here"; + + await tester.pumpWidget(MaterialApp( + home: Scaffold( + body: ActionTextButton( + text: text, + onPressed: () {}, + ), + ), + )); + expect(find.text(text), findsOneWidget); + }); + + testWidgets("onPressed is Triggered", (WidgetTester tester) async { + bool onpressed = false; + await tester.pumpWidget(MaterialApp( + home: Scaffold( + body: ActionTextButton( + text: "Click here", + onPressed: () { + onpressed = true; + }, + ), + ))); + + await tester.tap(find.byType(TextButton)); + await tester.pump(); + expect(onpressed, isTrue); + }); + }); +} diff --git a/test/common_widgets_test/custom_stepper_test.dart b/test/common_widgets_test/custom_stepper_test.dart new file mode 100644 index 00000000..1c0b0a89 --- /dev/null +++ b/test/common_widgets_test/custom_stepper_test.dart @@ -0,0 +1,107 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:danlaw_charger/src/constants/colors.dart'; +import 'package:danlaw_charger/src/common_widgets/custom_stepper.dart'; + +void main() { + group('CustomStepper Widget Tests', () { + testWidgets('renders the correct number of steps', + (WidgetTester tester) async { + const totalSteps = 5; + const curStep = 3; + + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: CustomStepper( + curStep: curStep, + totalSteps: totalSteps, + stepCompleteColor: Color(0xffD0BCFF), + inactiveColor: Color(0xffbababa), + currentStepColor: CustomColors().whitecolor, + lineWidth: 2.0, + ), + ), + ), + ); + + // Verify the number of step circles + expect(find.byType(GestureDetector), findsNWidgets(totalSteps)); + }); + + // testWidgets('renders correct colors for steps', + // (WidgetTester tester) async { + // const totalSteps = 5; + // const curStep = 3; + + // await tester.pumpWidget( + // MaterialApp( + // home: Scaffold( + // body: CustomStepper( + // curStep: curStep, + // totalSteps: totalSteps, + // stepCompleteColor: Color(0xffD0BCFF), + // inactiveColor: Color(0xffbababa), + // currentStepColor: CustomColors().whitecolor, + // lineWidth: 2.0, + // ), + // ), + // ), + // ); + + // final containers = tester.widgetList(find.byType(Container)); + + // // Verify the colors of the step circles + // BoxDecoration? decoration1 = + // containers.elementAt(0).decoration as BoxDecoration?; + // BoxDecoration? decoration2 = + // containers.elementAt(1).decoration as BoxDecoration?; + // BoxDecoration? decoration3 = + // containers.elementAt(2).decoration as BoxDecoration?; + // BoxDecoration? decoration4 = + // containers.elementAt(3).decoration as BoxDecoration?; + // BoxDecoration? decoration5 = + // containers.elementAt(4).decoration as BoxDecoration?; + + // expect( + // decoration1?.color, + // Color(0xffbababa), + // ); // Step 1 (inactive) + // expect( + // decoration2?.color, + // Color(0xffbababa), + // ); // Step 2 (inactive) + // expect(decoration3?.color, CustomColors().whitecolor); // Step 3 (current) + // expect(decoration4?.color, Color(0xffD0BCFF)); // Step 4 (complete) + // expect(decoration5?.color, Color(0xffD0BCFF)); // Step 5 (complete) + // }); + + // testWidgets('tapping on a step calls jumpToPage', + // (WidgetTester tester) async { + // const totalSteps = 5; + // const curStep = 3; + // final pageController = PageController(); + + // await tester.pumpWidget( + // MaterialApp( + // home: Scaffold( + // body: CustomStepper( + // curStep: curStep, + // totalSteps: totalSteps, + // stepCompleteColor: Color(0xffD0BCFF), + // inactiveColor: Color(0xffbababa), + // currentStepColor: CustomColors().whitecolor, + // lineWidth: 2.0, + // pageController: pageController, + // ), + // ), + // ), + // ); + + // await tester.tap(find.byType(GestureDetector).at(1)); + // await tester.pumpAndSettle(); + + // expect(pageController.page, 1); + // }); + }); +} diff --git a/test/common_widgets_test/custom_textformfield_test.dart b/test/common_widgets_test/custom_textformfield_test.dart new file mode 100644 index 00000000..3859f9dc --- /dev/null +++ b/test/common_widgets_test/custom_textformfield_test.dart @@ -0,0 +1,120 @@ +import 'package:danlaw_charger/src/common_widgets/custom_text_form_field.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +// Adjust this import according to your project structure + +void main() { + group('CustomTextFormField Tests', () { + testWidgets('displays hint text correctly', (WidgetTester tester) async { + final hintText = 'Enter text'; + + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: CustomTextFormField( + controller: TextEditingController(), + textInputType: TextInputType.text, + hintText: hintText, + ), + ), + ), + ); + + expect(find.text(hintText), findsOneWidget); + }); + + testWidgets('displays prefix icon correctly', (WidgetTester tester) async { + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: CustomTextFormField( + controller: TextEditingController(), + textInputType: TextInputType.text, + hintText: 'Enter text', + prefixIcon: Icon(Icons.location_on), + ), + ), + ), + ); + + expect(find.byIcon(Icons.location_on), findsOneWidget); + }); + + testWidgets('onTap callback is triggered', (WidgetTester tester) async { + bool tapped = false; + + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: CustomTextFormField( + controller: TextEditingController(), + textInputType: TextInputType.text, + hintText: 'Enter text', + onTap: () { + tapped = true; + }, + ), + ), + ), + ); + + await tester.tap(find.byType(TextFormField)); + await tester.pump(); + + expect(tapped, isTrue); + }); + + testWidgets('validator displays error text', (WidgetTester tester) async { + final formKey = GlobalKey(); + + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: Form( + key: formKey, + child: CustomTextFormField( + controller: TextEditingController(), + textInputType: TextInputType.text, + hintText: 'Enter text', + validator: (value) { + if (value == null || value.isEmpty) { + return 'Error text'; + } + return null; + }, + ), + ), + ), + ), + ); + + formKey.currentState?.validate(); + await tester.pump(); + + expect(find.text('Error text'), findsOneWidget); + }); + + testWidgets('readonly field does not accept input', + (WidgetTester tester) async { + final controller = TextEditingController(text: 'Initial text'); + + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: CustomTextFormField( + controller: controller, + textInputType: TextInputType.text, + hintText: 'Enter text', + readOnly: true, + ), + ), + ), + ); + + await tester.enterText(find.byType(TextFormField), 'New text'); + await tester.pump(); + + expect(controller.text, 'Initial text'); + }); + }); +} diff --git a/test/common_widgets_test/primary_button_test.dart b/test/common_widgets_test/primary_button_test.dart new file mode 100644 index 00000000..211d80a4 --- /dev/null +++ b/test/common_widgets_test/primary_button_test.dart @@ -0,0 +1,50 @@ +import 'package:danlaw_charger/src/common_widgets/primary_button.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + group("Primary Button ", () { + testWidgets("on Pressed is triggered", (WidgetTester tester) async { + bool onpressed = false; + await tester.pumpWidget(MaterialApp( + home: Scaffold( + body: PrimaryButton( + onPressed: () { + onpressed = true; + }, + text: "Click Me", + )), + )); + + await tester.tap(find.byType(ElevatedButton)); + await tester.pump(); + expect(onpressed, isTrue); + }); + testWidgets("display the text correctly", (WidgetTester tester) async { + const buttontext = "Click Me"; + await tester.pumpWidget(MaterialApp( + home: Scaffold( + body: PrimaryButton(text: buttontext, onPressed: () {})))); + expect(find.text(buttontext), findsOneWidget); + }); + + testWidgets("applies background color correctly", + (WidgetTester tester) async { + const color = Colors.red; + await tester.pumpWidget(MaterialApp( + home: Scaffold( + body: PrimaryButton( + text: 'Click Me', + onPressed: () {}, + backgroundColor: color, + ), + ))); + final elevatedButton = + tester.widget(find.byType(ElevatedButton)); + final backgroundColor = + elevatedButton.style?.backgroundColor?.resolve({}); + + expect(backgroundColor, color); + }); + }); +} diff --git a/test/src/features/jobs/domain/job_test.dart b/test/src/features/jobs/domain/job_test.dart deleted file mode 100644 index 530bde65..00000000 --- a/test/src/features/jobs/domain/job_test.dart +++ /dev/null @@ -1,48 +0,0 @@ -import 'package:flutter_test/flutter_test.dart'; -import 'package:starter_architecture_flutter_firebase/src/features/jobs/domain/job.dart'; - -void main() { - group('fromMap', () { - test('job with all properties', () { - final job = Job.fromMap(const { - 'name': 'Blogging', - 'ratePerHour': 10, - }, 'abc'); - expect(job, const Job(name: 'Blogging', ratePerHour: 10, id: 'abc')); - }); - - test('missing name', () { - // * If the 'name' is missing, this error will be emitted: - // * _CastError: - // * We can detect it by expecting that the test throws a TypeError - expect( - () => Job.fromMap(const { - 'ratePerHour': 10, - }, 'abc'), - throwsA(isInstanceOf())); - }); - }); - - group('toMap', () { - test('valid name, ratePerHour', () { - const job = Job(name: 'Blogging', ratePerHour: 10, id: 'abc'); - expect(job.toMap(), { - 'name': 'Blogging', - 'ratePerHour': 10, - }); - }); - }); - - group('equality', () { - test('different properties, equality returns false', () { - const job1 = Job(name: 'Blogging', ratePerHour: 10, id: 'abc'); - const job2 = Job(name: 'Blogging', ratePerHour: 5, id: 'abc'); - expect(job1 == job2, false); - }); - test('same properties, equality returns true', () { - const job1 = Job(name: 'Blogging', ratePerHour: 10, id: 'abc'); - const job2 = Job(name: 'Blogging', ratePerHour: 10, id: 'abc'); - expect(job1 == job2, true); - }); - }); -} diff --git a/test/src/mocks.dart b/test/src/mocks.dart deleted file mode 100644 index 447e4db2..00000000 --- a/test/src/mocks.dart +++ /dev/null @@ -1,18 +0,0 @@ -import 'package:firebase_auth/firebase_auth.dart'; -import 'package:mocktail/mocktail.dart'; -import 'package:starter_architecture_flutter_firebase/src/features/authentication/data/firebase_auth_repository.dart'; -import 'package:starter_architecture_flutter_firebase/src/features/onboarding/data/onboarding_repository.dart'; - -class MockAuthRepository extends Mock implements AuthRepository {} - -class MockFirebaseAuth extends Mock implements FirebaseAuth {} - -class MockUserCredential extends Mock implements UserCredential {} - -class MockUser extends Mock implements User {} - -class MockOnboardingRepository extends Mock implements OnboardingRepository {} - -class Listener extends Mock { - void call(T? previous, T? next); -} diff --git a/test/unit_test/dio_interceptor_test.dart b/test/unit_test/dio_interceptor_test.dart new file mode 100644 index 00000000..b5ddfbec --- /dev/null +++ b/test/unit_test/dio_interceptor_test.dart @@ -0,0 +1,148 @@ +import 'package:danlaw_charger/src/constants/keys.dart'; +import 'package:danlaw_charger/src/features/household/domain/pricing_method.dart'; +import 'package:danlaw_charger/src/features/vehicle/domain/vehicle.dart'; +import 'package:danlaw_charger/src/utils/vin_check.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:danlaw_charger/src/api/mock_api.dart'; +import 'package:world_zipcode_validator/world_zipcode_validator.dart'; +import 'package:us_states/us_states.dart'; +import 'package:country_code/country_code.dart'; +import 'package:uuid/uuid.dart'; + +void main() { + setUpAll(() => WidgetsFlutterBinding.ensureInitialized()); + + bool isValidPhoneNumber(String? value) => + RegExp(r'(^[\+]?[(]?[0-9]{3}[)]?[-\s\.]?[0-9]{3}[-\s\.]?[0-9]{4,6}$)').hasMatch(value ?? ''); + bool isValidEmail(String? value) => RegExp(r'[^@ \t\r\n]+@[^@ \t\r\n]+\.[^@ \t\r\n]+ ').hasMatch(value ?? ''); + DateTime convertUnixSeconds(String seconds) => DateTime.fromMillisecondsSinceEpoch(int.parse(seconds) * 1000); + + test('get_household_list', () async { + final response = await APIMock().getHouseholds(); + if (response.length > 1) { + expect(WorldZipcodeValidator.isValid('us', response[0].homeAddress.zipCode), true); + expect(USStates.getAllAbbreviations().contains(response[0].homeAddress.state), true); + expect(PricingMethodType.values.map((e) => e.name).toList().contains(response[0].chargePricingMethod!.type.name), + true); + } + if (response.length > 2) { + expect(WorldZipcodeValidator.isValid('us', response[1].homeAddress.zipCode), true); + expect(USStates.getAllAbbreviations().contains(response[1].homeAddress.state), true); + expect(PricingMethodType.values.map((e) => e.name).toList().contains(response[1].chargePricingMethod!.type.name), + true); + } + }); + test('get_countries', () async { + final response = await APIMock().getCountries(); + if (response.length > 1) { + expect(CountryCode.values.map((e) => e.alpha2).toList().contains(response[0].code), true); + } + if (response.length > 2) { + expect(CountryCode.values.map((e) => e.alpha2).toList().contains(response[1].code), true); + } + }); + test('account_details', () async { + final response = await APIMock().getAccountDetails(); + if ((response.households?.length ?? 0) > 1) { + expect(Uuid.isValidUUID(fromString: response.households![0].householdId), true); + expect(isValidPhoneNumber(response.phoneNumber), true); + expect(isValidEmail(response.emailId), true); + } + if ((response.vehicles?.length ?? 0) > 1) { + expect(isValidVIN(response.vehicles![0].vin), true, reason: Keys.invalidVIN); + } + if ((response.vehicles?.length ?? 0) > 2) { + expect(isValidVIN(response.vehicles![1].vin), true, reason: Keys.invalidVIN); + } + if ((response.vehicles?.length ?? 0) > 3) { + expect(isValidVIN(response.vehicles![2].vin), true, reason: Keys.invalidVIN); + } + if ((response.households?.length ?? 0) > 2) { + expect(Uuid.isValidUUID(fromString: response.households![1].householdId), true); + } + if ((response.households?.length ?? 0) > 3) { + expect(Uuid.isValidUUID(fromString: response.households![2].householdId), true); + } + }); + + /// max 6 vehicles + test('vehicles_for_household', () async { + final response = await APIMock().getVehiclesForHousehold(); + if (response.length > 1) { + expect(int.parse(response[0].range) >= 0, true); + expect(isValidVIN(response[0].vin), true, reason: Keys.invalidVIN); + expect((int.parse(response[0].battery)) >= 0, true, reason: Keys.negativeRange); + expect(VehicleHealth.values.map((e) => e.name).toList().contains(response[0].vehicleHealth.name), true); + expect(VehicleRangeStatus.values.map((e) => e.name).toList().contains(response[0].vehicleStatus.name), true); + } + if (response.length > 2) { + expect(int.parse(response[1].range) >= 0, true); + expect(isValidVIN(response[1].vin), true, reason: Keys.invalidVIN); + expect((int.parse(response[1].battery)) >= 0, true, reason: Keys.negativeRange); + expect(VehicleHealth.values.map((e) => e.name).toList().contains(response[1].vehicleHealth.name), true); + expect(VehicleRangeStatus.values.map((e) => e.name).toList().contains(response[1].vehicleStatus.name), true); + } + if (response.length > 3) { + expect(int.parse(response[0].range) >= 0, true); + expect(isValidVIN(response[2].vin), true, reason: Keys.invalidVIN); + expect((int.parse(response[2].battery)) >= 0, true, reason: Keys.negativeRange); + expect(VehicleHealth.values.map((e) => e.name).toList().contains(response[2].vehicleHealth.name), true); + expect(VehicleRangeStatus.values.map((e) => e.name).toList().contains(response[2].vehicleStatus.name), true); + } + if (response.length > 4) { + expect(int.parse(response[3].range) >= 0, true); + expect(isValidVIN(response[3].vin), true, reason: Keys.invalidVIN); + expect((int.parse(response[3].battery)) >= 0, true, reason: Keys.negativeRange); + expect(VehicleHealth.values.map((e) => e.name).toList().contains(response[3].vehicleHealth.name), true); + expect(VehicleRangeStatus.values.map((e) => e.name).toList().contains(response[3].vehicleStatus.name), true); + } + if (response.length > 5) { + expect(int.parse(response[0].range) >= 0, true); + expect(isValidVIN(response[4].vin), true, reason: Keys.invalidVIN); + expect((int.parse(response[4].battery)) >= 0, true, reason: Keys.negativeRange); + expect(VehicleHealth.values.map((e) => e.name).toList().contains(response[4].vehicleHealth.name), true); + expect(VehicleRangeStatus.values.map((e) => e.name).toList().contains(response[4].vehicleStatus.name), true); + } + if (response.length > 6) { + expect(int.parse(response[0].range) >= 0, true); + expect(isValidVIN(response[5].vin), true, reason: Keys.invalidVIN); + expect((int.parse(response[5].battery)) >= 0, true, reason: Keys.negativeRange); + expect(VehicleHealth.values.map((e) => e.name).toList().contains(response[5].vehicleHealth.name), true); + expect(VehicleRangeStatus.values.map((e) => e.name).toList().contains(response[5].vehicleStatus.name), true); + } + }); + + /// max 3 chargers + test('chargers_for_household', () async { + final response = await APIMock().getChargerForHousehold(); + if (response.length > 1) { + expect((response[0].chargerDetails?.chargerRate?.value ?? 0) > 0, true); + expect( + DateTime.now() + .difference(convertUnixSeconds(response[0].chargerDetails?.estimatedCompleteBy ?? '0')) + .isNegative, + false); + } + if (response.length > 2) { + if (response[1].chargerDetails?.chargerRate?.value != null) { + expect(response[1].chargerDetails!.chargerRate!.value > 0, true); + } + expect( + DateTime.now() + .difference(convertUnixSeconds(response[1].chargerDetails?.estimatedCompleteBy ?? '0')) + .isNegative, + false); + } + if (response.length > 3) { + if (response[2].chargerDetails?.chargerRate?.value != null) { + expect(response[2].chargerDetails!.chargerRate!.value > 0, true); + } + expect( + DateTime.now() + .difference(convertUnixSeconds(response[2].chargerDetails?.estimatedCompleteBy ?? '0')) + .isNegative, + false); + } + }); +} diff --git a/test/widget_test.dart b/test/widget_test.dart new file mode 100644 index 00000000..c70d5cbc --- /dev/null +++ b/test/widget_test.dart @@ -0,0 +1,27 @@ +// This is a basic Flutter widget test. +// +// To perform an interaction with a widget in your test, use the WidgetTester +// utility in the flutter_test package. For example, you can send tap and scroll +// gestures. You can also use WidgetTester to find child widgets in the widget +// tree, read text, and verify that the values of widget properties are correct. + +import 'package:flutter_test/flutter_test.dart'; + +void main() { + testWidgets('Counter increments smoke test', (WidgetTester tester) async { + // Build our app and trigger a frame. + // await tester.pumpWidget(const HomePage()); + + // Verify that our counter starts at 0. + // expect(find.text('0'), findsOneWidget); + // expect(find.text('1'), findsNothing); + + // Tap the '+' icon and trigger a frame. + // await tester.tap(find.byIcon(Icons.add)); + // await tester.pump(); + + // Verify that our counter has incremented. + // expect(find.text('0'), findsNothing); + // expect(find.text('1'), findsOneWidget); + }); +} diff --git a/web/favicon.png b/web/favicon.png deleted file mode 100644 index 8aaa46ac..00000000 Binary files a/web/favicon.png and /dev/null differ diff --git a/web/icons/Icon-192.png b/web/icons/Icon-192.png deleted file mode 100644 index b749bfef..00000000 Binary files a/web/icons/Icon-192.png and /dev/null differ diff --git a/web/icons/Icon-512.png b/web/icons/Icon-512.png deleted file mode 100644 index 88cfd48d..00000000 Binary files a/web/icons/Icon-512.png and /dev/null differ diff --git a/web/icons/Icon-maskable-192.png b/web/icons/Icon-maskable-192.png deleted file mode 100644 index eb9b4d76..00000000 Binary files a/web/icons/Icon-maskable-192.png and /dev/null differ diff --git a/web/icons/Icon-maskable-512.png b/web/icons/Icon-maskable-512.png deleted file mode 100644 index d69c5669..00000000 Binary files a/web/icons/Icon-maskable-512.png and /dev/null differ diff --git a/web/index.html b/web/index.html deleted file mode 100644 index be2be3bd..00000000 --- a/web/index.html +++ /dev/null @@ -1,58 +0,0 @@ - - - - - - - - - - - - - - - - - - - - starter_architecture_flutter_firebase - - - - - - - - - - diff --git a/web/manifest.json b/web/manifest.json deleted file mode 100644 index c7f1a4ce..00000000 --- a/web/manifest.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "starter_architecture_flutter_firebase", - "short_name": "starter_architecture_flutter_firebase", - "start_url": ".", - "display": "standalone", - "background_color": "#0175C2", - "theme_color": "#0175C2", - "description": "A new Flutter project.", - "orientation": "portrait-primary", - "prefer_related_applications": false, - "icons": [ - { - "src": "icons/Icon-192.png", - "sizes": "192x192", - "type": "image/png" - }, - { - "src": "icons/Icon-512.png", - "sizes": "512x512", - "type": "image/png" - }, - { - "src": "icons/Icon-maskable-192.png", - "sizes": "192x192", - "type": "image/png", - "purpose": "maskable" - }, - { - "src": "icons/Icon-maskable-512.png", - "sizes": "512x512", - "type": "image/png", - "purpose": "maskable" - } - ] -}