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