diff --git a/.firebaserc b/.firebaserc new file mode 100644 index 0000000..15023ab --- /dev/null +++ b/.firebaserc @@ -0,0 +1,5 @@ +{ + "projects": { + "default": "elevate-flower-app" + } +} diff --git a/.gitignore b/.gitignore index 64acf57..962176a 100644 --- a/.gitignore +++ b/.gitignore @@ -12,38 +12,40 @@ .swiftpm/ migrate_working_dir/ -# IntelliJ related +# IntelliJ / Android Studio *.iml *.ipr *.iws .idea/ +*.mocks.dart +*.g.dart -# The .vscode folder contains launch configuration and tasks you configure in -# VS Code which you may wish to be included in version control, so this line -# is commented out by default. +# VS Code (commented by default) #.vscode/ -# Flutter/Dart/Pub related -**/doc/api/ -**/ios/Flutter/.last_build_id +# Flutter / Dart / Pub .dart_tool/ .flutter-plugins-dependencies .pub-cache/ .pub/ -/build/ -/coverage/ +build/ +**/doc/api/ +**/ios/Flutter/.last_build_id +**/ios/Pods/ +**/android/.gradle/ + +# Coverage +coverage/ # Generated Dart files *.g.dart *.mocks.dart -# Symbolication related +# Symbolication & Obfuscation app.*.symbols - -# Obfuscation related app.*.map.json -# Android Studio will place build artifacts here +# Android Studio build artifacts /android/app/debug /android/app/profile /android/app/release diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..2501c66 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,25 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "name": "tracking_app", + "request": "launch", + "type": "dart" + }, + { + "name": "tracking_app (profile mode)", + "request": "launch", + "type": "dart", + "flutterMode": "profile" + }, + { + "name": "tracking_app (release mode)", + "request": "launch", + "type": "dart", + "flutterMode": "release" + } + ] +} diff --git a/all_tests_output.json b/all_tests_output.json new file mode 100644 index 0000000..ee391a8 Binary files /dev/null and b/all_tests_output.json differ diff --git a/all_tests_output.txt b/all_tests_output.txt new file mode 100644 index 0000000..d5539b0 Binary files /dev/null and b/all_tests_output.txt differ diff --git a/android/android/.gitignore b/android/android/.gitignore new file mode 100644 index 0000000..be3943c --- /dev/null +++ b/android/android/.gitignore @@ -0,0 +1,14 @@ +gradle-wrapper.jar +/.gradle +/captures/ +/gradlew +/gradlew.bat +/local.properties +GeneratedPluginRegistrant.java +.cxx/ + +# Remember to never publicly share your keystore. +# See https://flutter.dev/to/reference-keystore +key.properties +**/*.keystore +**/*.jks diff --git a/android/android/app/build.gradle.kts b/android/android/app/build.gradle.kts new file mode 100644 index 0000000..536ab03 --- /dev/null +++ b/android/android/app/build.gradle.kts @@ -0,0 +1,49 @@ + + + +plugins { + id("com.android.application") + id("com.google.gms.google-services") + id("com.google.firebase.crashlytics") + id("kotlin-android") + id("dev.flutter.flutter-gradle-plugin") +} + +android { + namespace = "com.example.tracking_app" + compileSdk = flutter.compileSdkVersion + ndkVersion = flutter.ndkVersion + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 + isCoreLibraryDesugaringEnabled = true + } + + kotlinOptions { + jvmTarget = "17" + } + + defaultConfig { + applicationId = "com.example.tracking_app" + minSdk = flutter.minSdkVersion + targetSdk = flutter.targetSdkVersion + versionCode = flutter.versionCode + versionName = flutter.versionName + } + + buildTypes { + release { + + signingConfig = signingConfigs.getByName("debug") + } + } +} + +dependencies { + coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:2.1.4") +} + +flutter { + source = "../.." +} diff --git a/android/android/app/google-services.json b/android/android/app/google-services.json new file mode 100644 index 0000000..57c8e9a --- /dev/null +++ b/android/android/app/google-services.json @@ -0,0 +1,48 @@ +{ + "project_info": { + "project_number": "725835190067", + "project_id": "elevate-flower-app", + "storage_bucket": "elevate-flower-app.firebasestorage.app" + }, + "client": [ + { + "client_info": { + "mobilesdk_app_id": "1:725835190067:android:50a3f907dd986f7ce53846", + "android_client_info": { + "package_name": "com.example.flower_shop" + } + }, + "oauth_client": [], + "api_key": [ + { + "current_key": "AIzaSyB1-EtHvgb14c5UzVggOoJRa6j8oto53Jg" + } + ], + "services": { + "appinvite_service": { + "other_platform_oauth_client": [] + } + } + }, + { + "client_info": { + "mobilesdk_app_id": "1:725835190067:android:1a8871c3f15cdafae53846", + "android_client_info": { + "package_name": "com.example.tracking_app" + } + }, + "oauth_client": [], + "api_key": [ + { + "current_key": "AIzaSyB1-EtHvgb14c5UzVggOoJRa6j8oto53Jg" + } + ], + "services": { + "appinvite_service": { + "other_platform_oauth_client": [] + } + } + } + ], + "configuration_version": "1" +} \ No newline at end of file diff --git a/android/android/app/src/debug/AndroidManifest.xml b/android/android/app/src/debug/AndroidManifest.xml new file mode 100644 index 0000000..399f698 --- /dev/null +++ b/android/android/app/src/debug/AndroidManifest.xml @@ -0,0 +1,7 @@ + + + + diff --git a/android/android/app/src/main/AndroidManifest.xml b/android/android/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..2cc440e --- /dev/null +++ b/android/android/app/src/main/AndroidManifest.xml @@ -0,0 +1,45 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/android/android/app/src/main/kotlin/com/example/tracking_app/MainActivity.kt b/android/android/app/src/main/kotlin/com/example/tracking_app/MainActivity.kt new file mode 100644 index 0000000..2fee2b8 --- /dev/null +++ b/android/android/app/src/main/kotlin/com/example/tracking_app/MainActivity.kt @@ -0,0 +1,5 @@ +package com.example.tracking_app + +import io.flutter.embedding.android.FlutterActivity + +class MainActivity : FlutterActivity() diff --git a/android/android/app/src/main/res/drawable-v21/launch_background.xml b/android/android/app/src/main/res/drawable-v21/launch_background.xml new file mode 100644 index 0000000..f74085f --- /dev/null +++ b/android/android/app/src/main/res/drawable-v21/launch_background.xml @@ -0,0 +1,12 @@ + + + + + + + + diff --git a/android/android/app/src/main/res/drawable/launch_background.xml b/android/android/app/src/main/res/drawable/launch_background.xml new file mode 100644 index 0000000..304732f --- /dev/null +++ b/android/android/app/src/main/res/drawable/launch_background.xml @@ -0,0 +1,12 @@ + + + + + + + + diff --git a/android/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/android/android/app/src/main/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 0000000..db77bb4 Binary files /dev/null and b/android/android/app/src/main/res/mipmap-hdpi/ic_launcher.png differ diff --git a/android/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/android/android/app/src/main/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 0000000..17987b7 Binary files /dev/null and b/android/android/app/src/main/res/mipmap-mdpi/ic_launcher.png differ diff --git a/android/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/android/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 0000000..09d4391 Binary files /dev/null and b/android/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/android/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/android/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 0000000..d5f1c8d Binary files /dev/null and b/android/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/android/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/android/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 0000000..4d6372e Binary files /dev/null and b/android/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/android/android/app/src/main/res/values-night/styles.xml b/android/android/app/src/main/res/values-night/styles.xml new file mode 100644 index 0000000..06952be --- /dev/null +++ b/android/android/app/src/main/res/values-night/styles.xml @@ -0,0 +1,18 @@ + + + + + + + diff --git a/android/android/app/src/main/res/values/styles.xml b/android/android/app/src/main/res/values/styles.xml new file mode 100644 index 0000000..cb1ef88 --- /dev/null +++ b/android/android/app/src/main/res/values/styles.xml @@ -0,0 +1,18 @@ + + + + + + + diff --git a/android/android/app/src/profile/AndroidManifest.xml b/android/android/app/src/profile/AndroidManifest.xml new file mode 100644 index 0000000..399f698 --- /dev/null +++ b/android/android/app/src/profile/AndroidManifest.xml @@ -0,0 +1,7 @@ + + + + diff --git a/android/android/build.gradle.kts b/android/android/build.gradle.kts new file mode 100644 index 0000000..dbee657 --- /dev/null +++ b/android/android/build.gradle.kts @@ -0,0 +1,24 @@ +allprojects { + repositories { + google() + mavenCentral() + } +} + +val newBuildDir: Directory = + rootProject.layout.buildDirectory + .dir("../../build") + .get() +rootProject.layout.buildDirectory.value(newBuildDir) + +subprojects { + val newSubprojectBuildDir: Directory = newBuildDir.dir(project.name) + project.layout.buildDirectory.value(newSubprojectBuildDir) +} +subprojects { + project.evaluationDependsOn(":app") +} + +tasks.register("clean") { + delete(rootProject.layout.buildDirectory) +} diff --git a/android/android/gradle.properties b/android/android/gradle.properties new file mode 100644 index 0000000..fbee1d8 --- /dev/null +++ b/android/android/gradle.properties @@ -0,0 +1,2 @@ +org.gradle.jvmargs=-Xmx8G -XX:MaxMetaspaceSize=4G -XX:ReservedCodeCacheSize=512m -XX:+HeapDumpOnOutOfMemoryError +android.useAndroidX=true diff --git a/android/android/gradle/wrapper/gradle-wrapper.properties b/android/android/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..e4ef43f --- /dev/null +++ b/android/android/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.14-all.zip diff --git a/android/android/settings.gradle.kts b/android/android/settings.gradle.kts new file mode 100644 index 0000000..d6b1b1b --- /dev/null +++ b/android/android/settings.gradle.kts @@ -0,0 +1,30 @@ +pluginManagement { + val flutterSdkPath = + run { + val properties = java.util.Properties() + file("local.properties").inputStream().use { properties.load(it) } + val flutterSdkPath = properties.getProperty("flutter.sdk") + require(flutterSdkPath != null) { "flutter.sdk not set in local.properties" } + flutterSdkPath + } + + includeBuild("$flutterSdkPath/packages/flutter_tools/gradle") + + repositories { + google() + mavenCentral() + gradlePluginPortal() + } +} + +plugins { + id("dev.flutter.flutter-plugin-loader") version "1.0.0" + id("com.android.application") version "8.11.1" apply false + // START: FlutterFire Configuration + id("com.google.gms.google-services") version("4.3.15") apply false + id("com.google.firebase.crashlytics") version("2.8.1") apply false + // END: FlutterFire Configuration + id("org.jetbrains.kotlin.android") version "2.2.20" apply false +} + +include(":app") diff --git a/android/app/build.gradle.kts b/android/app/build.gradle.kts index 04ed454..9a1df92 100644 --- a/android/app/build.gradle.kts +++ b/android/app/build.gradle.kts @@ -1,11 +1,11 @@ + + + plugins { id("com.android.application") - // START: FlutterFire Configuration id("com.google.gms.google-services") id("com.google.firebase.crashlytics") - // END: FlutterFire Configuration id("kotlin-android") - // The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins. id("dev.flutter.flutter-gradle-plugin") } @@ -21,14 +21,11 @@ android { } kotlinOptions { - jvmTarget = JavaVersion.VERSION_17.toString() + jvmTarget = "17" } defaultConfig { - // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). applicationId = "com.example.tracking_app" - // You can update the following values to match your application needs. - // For more information, see: https://flutter.dev/to/review-gradle-config. minSdk = flutter.minSdkVersion targetSdk = flutter.targetSdkVersion versionCode = flutter.versionCode @@ -37,8 +34,7 @@ android { 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.getByName("debug") } } @@ -50,4 +46,4 @@ dependencies { flutter { source = "../.." -} +} \ No newline at end of file diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 2cc440e..eb848a5 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -1,4 +1,7 @@ + + + ("clean") { delete(rootProject.layout.buildDirectory) -} +} \ No newline at end of file diff --git a/assets/images/Flowery logo.png b/assets/images/Flowery logo.png new file mode 100644 index 0000000..a774c75 Binary files /dev/null and b/assets/images/Flowery logo.png differ diff --git a/assets/images/flower_logo.png b/assets/images/flower_logo.png new file mode 100644 index 0000000..52e92c5 Binary files /dev/null and b/assets/images/flower_logo.png differ diff --git a/assets/images/whatsapp.png b/assets/images/whatsapp.png new file mode 100644 index 0000000..a45d019 Binary files /dev/null and b/assets/images/whatsapp.png differ diff --git a/assets/translations/ar.json b/assets/translations/ar.json index 62c4974..7115676 100644 --- a/assets/translations/ar.json +++ b/assets/translations/ar.json @@ -179,13 +179,13 @@ "failed_to_save_address": "فشل حفظ العنوان", "addNewAddress": "إضافة عنوان جديد", "savedAddress": "تم حفظ العنوان", - "discount": "خصم", - "sortBy": "الترتيب حسب", - "lowestPrice": "السعر الأدنى", - "highestPrice": "السعر الأعلى", + "sortBy": "ترتيب حسب", + "lowestPrice": "أدنى سعر", + "highestPrice": "أعلى سعر", "newest": "الأحدث", "oldest": "الأقدم", - "filter": "تصفية", + "discount": "الخصومات", + "filter": "فلتر", "active": "نشط", "completed": "مكتمل", "no_orders_found": "لا توجد طلبات", @@ -219,7 +219,7 @@ "uploadIdImage": "تحميل صورة الهوية", "female": "أنثى", "male": "ذكر", - "continue": "متابعة", + "continueText": "متابعة", "requiredField": "مطلوب", "licensePhotoRequired": "صورة الرخصة مطلوبة", "idImageRequired": "صورة الهوية مطلوبة", @@ -231,5 +231,43 @@ "congratulationsMessage": "تهانينا! تم تقديم طلبك بنجاح.", "reviewMessage": "سنقوم بمراجعة طلبك والرد عليك قريباً عبر البريد الإلكتروني.", "backToLogin": "العودة إلى تسجيل الدخول", - "checkEmailMessage": "تحقق من بريدك الإلكتروني بانتظام للحصول على تحديثات حول حالة طلبك." + "checkEmailMessage": "تحقق من بريدك الإلكتروني بانتظام الحصول على تحديثات حول حالة طلبك.", + "welcomeBack": "مرحباً بعودتك،", + "pickupAddress": "عنوان الاستلام", + "userAddress": "عنوان المستخدم", + "store": "المتجر", + "customer": "العميل", + "totalPrice": "الإجمالي", + "accept": "قبول", + "arrivedAtPickup": "وصلت إلى نقطة الاستلام", + "pickUpOrder": "استلام الطلب", + "startDelivery": "بدء التوصيل", + "markAsDelivered": "تم التوصيل", + "accepted": "تم القبول", + "arrived": "وصل", + "picked": "تم الاستلام", + "onTheWay": "في الطريق", + "change": "تغيير", + "vehicle_type": "نوع المركبة", + "vehicle_number": "رقم المركبة", + "vehicle_license": "رخصة المركبة", + "editDriverProfile": "تعديل الملف الشخصي", + "editVehicle": "تعديل المركبة", + "cannotBeSame": "كلمة المرور الجديدة لا يجب أن تطابق الحالية", + "orderDetails": "بيانات الطلب", + "status": "الحالة", + "orderId": "رقم الطلب : ", + "arrivedAtPickupPoint": "وصلت الى نقطة الالتقاء", + "arriverAtDestination": "وصلت إلى نقطة التسليم", + "confirmDelivery": "تأكيد التسليم", + "deliveryConfirmed": "تم تأكيد التسليم", + "orderCompleted": "تم إكمال الطلب", + "pickedUp": "تم الاستلام", + "outForDelivery": "في الطريق للتسليم", + "driverOrderTitle": "طلب زهور", + "unknownStore": "متجر غير معروف", + "noAddress": "لا يوجد عنوان", + "reject": "رفض", + "noPendingOrders": "لا توجد طلبات معلقة", + "floweryRider": "سائق فلاوري" } \ No newline at end of file diff --git a/assets/translations/en.json b/assets/translations/en.json index f6bbf73..86edd1f 100644 --- a/assets/translations/en.json +++ b/assets/translations/en.json @@ -113,7 +113,8 @@ "no_products_found": "No products found", "change_language": "Change Language", "arabic": "Arabic", - "initialSearchMsg" : "Search For Any Product You Want", + "english": "English", + "initialSearchMsg": "Search For Any Product You Want", "welcomeMessage": "Welcome to Flowery Shop", "home": "Home", "profile": "Profile", @@ -181,7 +182,6 @@ "addNewAddress": "Add New Address", "savedAddress": "Saved Address", "recipient_phone": "Recipient phone", - "english": "English", "sortBy": "Sort By", "lowestPrice": "Lowest Price", "highestPrice": "Highest Price", @@ -198,7 +198,7 @@ "notification_deleted_successfully": "Notification deleted successfully", "clear_all": "Clear all", "no_notifications_yet": "No Notifications Yet", - "orders" : "Orders", + "orders": "Orders", "onboardingTitle": "Welcome to ", "onboardingDescription": "Flowery rider app ", "applyNow": "Apply Now", @@ -222,7 +222,7 @@ "uploadIdImage": "Upload ID image", "female": "Female", "male": "Male", - "continue": "Continue", + "continueText": "Continue", "requiredField": "Required", "licensePhotoRequired": "License photo is required", "idImageRequired": "ID image is required", @@ -234,5 +234,43 @@ "congratulationsMessage": "Congratulations! Your application has been submitted successfully.", "reviewMessage": "We will review your application and get back to you soon via email.", "backToLogin": "Back to Login", - "checkEmailMessage": "Check your email regularly for updates on your application status." + "checkEmailMessage": "Check your email regularly for updates on your application status.", + "welcomeBack": "Welcome back,", + "pickupAddress": "Pickup address", + "userAddress": "User address", + "store": "Store", + "customer": "Customer", + "totalPrice": "Total", + "accept": "Accept", + "arrivedAtPickup": "Arrived at Pickup point", + "pickUpOrder": "Pick Up Order", + "startDelivery": "Start Delivery", + "markAsDelivered": "Mark as Delivered", + "accepted": "Accepted", + "arrived": "Arrived", + "picked": "Picked", + "onTheWay": "On the way", + "change": "Change", + "vehicle_type": "Vehicle Type", + "vehicle_number": "Vehicle Number", + "vehicle_license": "Vehicle License", + "editDriverProfile": "Edit Driver Profile", + "editVehicle": "Edit Vehicle", + "cannotBeSame": "New password cann't be same", + "orderDetails": "Order details", + "status": "Status : ", + "orderId": "Order ID : # ", + "arrivedAtPickupPoint": "Arrived at pickup point", + "arriverAtDestination": "Arrived at destination", + "confirmDelivery": "Confirm delivery", + "deliveryConfirmed": "Delivery confirmed", + "orderCompleted": "Order completed", + "pickedUp": "Picked up", + "outForDelivery": "Out for delivery", + "driverOrderTitle": "Flower order", + "unknownStore": "Unknown Store", + "noAddress": "No address", + "reject": "Reject", + "noPendingOrders": "No pending orders", + "floweryRider": "Flowery Rider" } \ No newline at end of file diff --git a/firebase.json b/firebase.json index 6e7e2c2..0efaa09 100644 --- a/firebase.json +++ b/firebase.json @@ -1 +1,37 @@ -{"flutter":{"platforms":{"android":{"default":{"projectId":"elevate-flower-app","appId":"1:725835190067:android:1a8871c3f15cdafae53846","fileOutput":"android/app/google-services.json"}},"dart":{"lib/firebase_options.dart":{"projectId":"elevate-flower-app","configurations":{"web":"1:725835190067:web:86225b1572d53a90e53846"}}}}}} \ No newline at end of file +{ + "flutter": { + "platforms": { + "android": { + "default": { + "projectId": "elevate-flower-app", + "appId": "1:725835190067:android:1a8871c3f15cdafae53846", + "fileOutput": "android/app/google-services.json" + } + }, + "dart": { + "lib/firebase_options.dart": { + "projectId": "elevate-flower-app", + "configurations": { + "web": "1:725835190067:web:86225b1572d53a90e53846" + } + } + } + } + }, + "functions": [ + { + "source": "functions", + "codebase": "default", + "ignore": [ + "node_modules", + ".git", + "firebase-debug.log", + "firebase-debug.*.log", + "*.local" + ], + "predeploy": [ + "npm --prefix \"$RESOURCE_DIR\" run lint" + ] + } + ] +} diff --git a/functions/.eslintrc.js b/functions/.eslintrc.js new file mode 100644 index 0000000..f4cb76c --- /dev/null +++ b/functions/.eslintrc.js @@ -0,0 +1,28 @@ +module.exports = { + env: { + es6: true, + node: true, + }, + parserOptions: { + "ecmaVersion": 2018, + }, + extends: [ + "eslint:recommended", + "google", + ], + rules: { + "no-restricted-globals": ["error", "name", "length"], + "prefer-arrow-callback": "error", + "quotes": ["error", "double", {"allowTemplateLiterals": true}], + }, + overrides: [ + { + files: ["**/*.spec.*"], + env: { + mocha: true, + }, + rules: {}, + }, + ], + globals: {}, +}; diff --git a/functions/.gitignore b/functions/.gitignore new file mode 100644 index 0000000..21ee8d3 --- /dev/null +++ b/functions/.gitignore @@ -0,0 +1,2 @@ +node_modules/ +*.local \ No newline at end of file diff --git a/functions/index.js b/functions/index.js new file mode 100644 index 0000000..d3008b9 --- /dev/null +++ b/functions/index.js @@ -0,0 +1,44 @@ +const functions = require("firebase-functions"); +const admin = require("firebase-admin"); + +admin.initializeApp(); + +exports.notifyFlowerShopOnStatusChange = functions.firestore + .document("orders/{orderId}") + .onUpdate(async (change, context) => { + const before = change.before.data(); + const after = change.after.data(); + + // Only trigger if status changed + if (before.status === after.status) { + return null; + } + + const shopToken = after.shopDeviceToken; + + if (!shopToken) { + console.log("No shop device token found."); + return null; + } + + const message = { + token: shopToken, + notification: { + title: "Order Status Updated", + body: `Order #${context.params.orderId} is now ${after.status}`, + }, + data: { + orderId: context.params.orderId, + status: after.status, + }, + }; + + try { + await admin.messaging().send(message); + console.log("Notification sent to flower shop."); + } catch (error) { + console.error("Error sending notification:", error); + } + + return null; + }); \ No newline at end of file diff --git a/functions/package-lock.json b/functions/package-lock.json new file mode 100644 index 0000000..eb787e0 --- /dev/null +++ b/functions/package-lock.json @@ -0,0 +1,4290 @@ +{ + "name": "functions", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "functions", + "dependencies": { + "firebase-admin": "^11.10.0", + "firebase-functions": "^3.23.0" + }, + "devDependencies": { + "eslint": "^8.15.0", + "eslint-config-google": "^0.14.0", + "firebase-functions-test": "^3.1.0" + }, + "engines": { + "node": "18" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.0.tgz", + "integrity": "sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww==", + "license": "MIT", + "optional": true, + "dependencies": { + "@babel/types": "^7.29.0" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", + "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", + "license": "MIT", + "optional": true, + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.9.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz", + "integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", + "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.4.tgz", + "integrity": "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^9.6.0", + "globals": "^13.19.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/js": { + "version": "8.57.1", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.1.tgz", + "integrity": "sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/@fastify/busboy": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@fastify/busboy/-/busboy-1.2.1.tgz", + "integrity": "sha512-7PQA7EH43S0CxcOa9OeAnaeA0oQ+e/DHNPZwSQM9CQHW76jle5+OvLdibRp/Aafs9KXbLhxyjOTkRjWUbQEd3Q==", + "license": "MIT", + "dependencies": { + "text-decoding": "^1.0.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/@firebase/app-types": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/@firebase/app-types/-/app-types-0.9.0.tgz", + "integrity": "sha512-AeweANOIo0Mb8GiYm3xhTEBVCmPwTYAu9Hcd2qSkLuga/6+j9b1Jskl5bpiSQWy9eJ/j5pavxj6eYogmnuzm+Q==", + "license": "Apache-2.0" + }, + "node_modules/@firebase/auth-interop-types": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/@firebase/auth-interop-types/-/auth-interop-types-0.2.1.tgz", + "integrity": "sha512-VOaGzKp65MY6P5FI84TfYKBXEPi6LmOCSMMzys6o2BN2LOsqy7pCuZCup7NYnfbk5OkkQKzvIfHOzTm0UDpkyg==", + "license": "Apache-2.0" + }, + "node_modules/@firebase/component": { + "version": "0.6.4", + "resolved": "https://registry.npmjs.org/@firebase/component/-/component-0.6.4.tgz", + "integrity": "sha512-rLMyrXuO9jcAUCaQXCMjCMUsWrba5fzHlNK24xz5j2W6A/SRmK8mZJ/hn7V0fViLbxC0lPMtrK1eYzk6Fg03jA==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/util": "1.9.3", + "tslib": "^2.1.0" + } + }, + "node_modules/@firebase/database": { + "version": "0.14.4", + "resolved": "https://registry.npmjs.org/@firebase/database/-/database-0.14.4.tgz", + "integrity": "sha512-+Ea/IKGwh42jwdjCyzTmeZeLM3oy1h0mFPsTy6OqCWzcu/KFqRAr5Tt1HRCOBlNOdbh84JPZC47WLU18n2VbxQ==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/auth-interop-types": "0.2.1", + "@firebase/component": "0.6.4", + "@firebase/logger": "0.4.0", + "@firebase/util": "1.9.3", + "faye-websocket": "0.11.4", + "tslib": "^2.1.0" + } + }, + "node_modules/@firebase/database-compat": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/@firebase/database-compat/-/database-compat-0.3.4.tgz", + "integrity": "sha512-kuAW+l+sLMUKBThnvxvUZ+Q1ZrF/vFJ58iUY9kAcbX48U03nVzIF6Tmkf0p3WVQwMqiXguSgtOPIB6ZCeF+5Gg==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.6.4", + "@firebase/database": "0.14.4", + "@firebase/database-types": "0.10.4", + "@firebase/logger": "0.4.0", + "@firebase/util": "1.9.3", + "tslib": "^2.1.0" + } + }, + "node_modules/@firebase/database-types": { + "version": "0.10.4", + "resolved": "https://registry.npmjs.org/@firebase/database-types/-/database-types-0.10.4.tgz", + "integrity": "sha512-dPySn0vJ/89ZeBac70T+2tWWPiJXWbmRygYv0smT5TfE3hDrQ09eKMF3Y+vMlTdrMWq7mUdYW5REWPSGH4kAZQ==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/app-types": "0.9.0", + "@firebase/util": "1.9.3" + } + }, + "node_modules/@firebase/logger": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@firebase/logger/-/logger-0.4.0.tgz", + "integrity": "sha512-eRKSeykumZ5+cJPdxxJRgAC3G5NknY2GwEbKfymdnXtnT0Ucm4pspfR6GT4MUQEDuJwRVbVcSx85kgJulMoFFA==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/@firebase/util": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/@firebase/util/-/util-1.9.3.tgz", + "integrity": "sha512-DY02CRhOZwpzO36fHpuVysz6JZrscPiBXD0fXp6qSrL9oNOx5KWICKdR95C0lSITzxp0TZosVyHqzatE8JbcjA==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/@google-cloud/firestore": { + "version": "6.8.0", + "resolved": "https://registry.npmjs.org/@google-cloud/firestore/-/firestore-6.8.0.tgz", + "integrity": "sha512-JRpk06SmZXLGz0pNx1x7yU3YhkUXheKgH5hbDZ4kMsdhtfV5qPLJLRI4wv69K0cZorIk+zTMOwptue7hizo0eA==", + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "fast-deep-equal": "^3.1.1", + "functional-red-black-tree": "^1.0.1", + "google-gax": "^3.5.7", + "protobufjs": "^7.2.5" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/@google-cloud/paginator": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@google-cloud/paginator/-/paginator-3.0.7.tgz", + "integrity": "sha512-jJNutk0arIQhmpUUQJPJErsojqo834KcyB6X7a1mxuic8i1tKXxde8E69IZxNZawRIlZdIK2QY4WALvlK5MzYQ==", + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "arrify": "^2.0.0", + "extend": "^3.0.2" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@google-cloud/projectify": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@google-cloud/projectify/-/projectify-3.0.0.tgz", + "integrity": "sha512-HRkZsNmjScY6Li8/kb70wjGlDDyLkVk3KvoEo9uIoxSjYLJasGiCch9+PqRVDOCGUFvEIqyogl+BeqILL4OJHA==", + "license": "Apache-2.0", + "optional": true, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/@google-cloud/promisify": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@google-cloud/promisify/-/promisify-3.0.1.tgz", + "integrity": "sha512-z1CjRjtQyBOYL+5Qr9DdYIfrdLBe746jRTYfaYU6MeXkqp7UfYs/jX16lFFVzZ7PGEJvqZNqYUEtb1mvDww4pA==", + "license": "Apache-2.0", + "optional": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/@google-cloud/storage": { + "version": "6.12.0", + "resolved": "https://registry.npmjs.org/@google-cloud/storage/-/storage-6.12.0.tgz", + "integrity": "sha512-78nNAY7iiZ4O/BouWMWTD/oSF2YtYgYB3GZirn0To6eBOugjXVoK+GXgUXOl+HlqbAOyHxAVXOlsj3snfbQ1dw==", + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "@google-cloud/paginator": "^3.0.7", + "@google-cloud/projectify": "^3.0.0", + "@google-cloud/promisify": "^3.0.0", + "abort-controller": "^3.0.0", + "async-retry": "^1.3.3", + "compressible": "^2.0.12", + "duplexify": "^4.0.0", + "ent": "^2.2.0", + "extend": "^3.0.2", + "fast-xml-parser": "^4.2.2", + "gaxios": "^5.0.0", + "google-auth-library": "^8.0.1", + "mime": "^3.0.0", + "mime-types": "^2.0.8", + "p-limit": "^3.0.1", + "retry-request": "^5.0.0", + "teeny-request": "^8.0.0", + "uuid": "^8.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@google-cloud/storage/node_modules/uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "license": "MIT", + "optional": true, + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/@grpc/grpc-js": { + "version": "1.8.22", + "resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.8.22.tgz", + "integrity": "sha512-oAjDdN7fzbUi+4hZjKG96MR6KTEubAeMpQEb+77qy+3r0Ua5xTFuie6JOLr4ZZgl5g+W5/uRTS2M1V8mVAFPuA==", + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "@grpc/proto-loader": "^0.7.0", + "@types/node": ">=12.12.47" + }, + "engines": { + "node": "^8.13.0 || >=10.10.0" + } + }, + "node_modules/@grpc/proto-loader": { + "version": "0.7.15", + "resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.7.15.tgz", + "integrity": "sha512-tMXdRCfYVixjuFK+Hk0Q1s38gV9zDiDJfWL3h1rv4Qc39oILCu1TRTDt7+fGUI8K4G1Fj125Hx/ru3azECWTyQ==", + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "lodash.camelcase": "^4.3.0", + "long": "^5.0.0", + "protobufjs": "^7.2.5", + "yargs": "^17.7.2" + }, + "bin": { + "proto-loader-gen-types": "build/bin/proto-loader-gen-types.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@humanwhocodes/config-array": { + "version": "0.13.0", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.13.0.tgz", + "integrity": "sha512-DZLEEqFWQFiyK6h5YIeynKx7JlvCYWL0cImfSRXZ9l4Sg2efkFGTuFf6vzXjK1cq6IYkU+Eg/JizXw+TD2vRNw==", + "deprecated": "Use @eslint/config-array instead", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanwhocodes/object-schema": "^2.0.3", + "debug": "^4.3.1", + "minimatch": "^3.0.5" + }, + "engines": { + "node": ">=10.10.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/object-schema": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.3.tgz", + "integrity": "sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==", + "deprecated": "Use @eslint/object-schema instead", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@jsdoc/salty": { + "version": "0.2.10", + "resolved": "https://registry.npmjs.org/@jsdoc/salty/-/salty-0.2.10.tgz", + "integrity": "sha512-VFHSsQAQp8y1NJvAJBpLs9I2shHE6hz9TwukocDObuUgGVAq62yZGbTgJg04Z3Fj0XSMWe0sJqGg5dhKGTV92A==", + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "lodash": "^4.17.23" + }, + "engines": { + "node": ">=v12.0.0" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@protobufjs/aspromise": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", + "integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==", + "license": "BSD-3-Clause", + "optional": true + }, + "node_modules/@protobufjs/base64": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz", + "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==", + "license": "BSD-3-Clause", + "optional": true + }, + "node_modules/@protobufjs/codegen": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.4.tgz", + "integrity": "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==", + "license": "BSD-3-Clause", + "optional": true + }, + "node_modules/@protobufjs/eventemitter": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz", + "integrity": "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==", + "license": "BSD-3-Clause", + "optional": true + }, + "node_modules/@protobufjs/fetch": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.0.tgz", + "integrity": "sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==", + "license": "BSD-3-Clause", + "optional": true, + "dependencies": { + "@protobufjs/aspromise": "^1.1.1", + "@protobufjs/inquire": "^1.1.0" + } + }, + "node_modules/@protobufjs/float": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz", + "integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==", + "license": "BSD-3-Clause", + "optional": true + }, + "node_modules/@protobufjs/inquire": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.0.tgz", + "integrity": "sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q==", + "license": "BSD-3-Clause", + "optional": true + }, + "node_modules/@protobufjs/path": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz", + "integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==", + "license": "BSD-3-Clause", + "optional": true + }, + "node_modules/@protobufjs/pool": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz", + "integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==", + "license": "BSD-3-Clause", + "optional": true + }, + "node_modules/@protobufjs/utf8": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz", + "integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==", + "license": "BSD-3-Clause", + "optional": true + }, + "node_modules/@tootallnate/once": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.0.tgz", + "integrity": "sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 10" + } + }, + "node_modules/@types/body-parser": { + "version": "1.19.6", + "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.6.tgz", + "integrity": "sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==", + "license": "MIT", + "dependencies": { + "@types/connect": "*", + "@types/node": "*" + } + }, + "node_modules/@types/connect": { + "version": "3.4.38", + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", + "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/cors": { + "version": "2.8.19", + "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.19.tgz", + "integrity": "sha512-mFNylyeyqN93lfe/9CSxOGREz8cpzAhH+E93xJ4xWQf62V8sQ/24reV2nyzUWM6H6Xji+GGHpkbLe7pVoUEskg==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/express": { + "version": "4.17.3", + "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.3.tgz", + "integrity": "sha512-I8cGRJj3pyOLs/HndoP+25vOqhqWkAZsWMEmq1qXy/b/M3ppufecUwaK2/TVDVxcV61/iSdhykUjQQ2DLSrTdg==", + "license": "MIT", + "dependencies": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "*", + "@types/serve-static": "*" + } + }, + "node_modules/@types/express-serve-static-core": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-5.1.1.tgz", + "integrity": "sha512-v4zIMr/cX7/d2BpAEX3KNKL/JrT1s43s96lLvvdTmza1oEvDudCqK9aF/djc/SWgy8Yh0h30TZx5VpzqFCxk5A==", + "license": "MIT", + "dependencies": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*", + "@types/send": "*" + } + }, + "node_modules/@types/glob": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/@types/glob/-/glob-8.1.0.tgz", + "integrity": "sha512-IO+MJPVhoqz+28h1qLAcBEH2+xHMK6MTyHJc7MTnnYb6wsoLR29POVGJ7LycmVXIqyy/4/2ShP5sUwTXuOwb/w==", + "license": "MIT", + "optional": true, + "dependencies": { + "@types/minimatch": "^5.1.2", + "@types/node": "*" + } + }, + "node_modules/@types/http-errors": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.5.tgz", + "integrity": "sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==", + "license": "MIT" + }, + "node_modules/@types/jsonwebtoken": { + "version": "9.0.10", + "resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-9.0.10.tgz", + "integrity": "sha512-asx5hIG9Qmf/1oStypjanR7iKTv0gXQ1Ov/jfrX6kS/EO0OFni8orbmGCn0672NHR3kXHwpAwR+B368ZGN/2rA==", + "license": "MIT", + "dependencies": { + "@types/ms": "*", + "@types/node": "*" + } + }, + "node_modules/@types/linkify-it": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@types/linkify-it/-/linkify-it-5.0.0.tgz", + "integrity": "sha512-sVDA58zAw4eWAffKOaQH5/5j3XeayukzDk+ewSsnv3p4yJEZHCCzMDiZM8e0OUrRvmpGZ85jf4yDHkHsgBNr9Q==", + "license": "MIT", + "optional": true + }, + "node_modules/@types/lodash": { + "version": "4.17.24", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.24.tgz", + "integrity": "sha512-gIW7lQLZbue7lRSWEFql49QJJWThrTFFeIMJdp3eH4tKoxm1OvEPg02rm4wCCSHS0cL3/Fizimb35b7k8atwsQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/long": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/long/-/long-4.0.2.tgz", + "integrity": "sha512-MqTGEo5bj5t157U6fA/BiDynNkn0YknVdh48CMPkTSpFTVmvao5UQmm7uEF6xBEo7qIMAlY/JSleYaE6VOdpaA==", + "license": "MIT", + "optional": true + }, + "node_modules/@types/markdown-it": { + "version": "14.1.2", + "resolved": "https://registry.npmjs.org/@types/markdown-it/-/markdown-it-14.1.2.tgz", + "integrity": "sha512-promo4eFwuiW+TfGxhi+0x3czqTYJkG8qB17ZUJiVF10Xm7NLVRSLUsfRTU/6h1e24VvRnXCx+hG7li58lkzog==", + "license": "MIT", + "optional": true, + "dependencies": { + "@types/linkify-it": "^5", + "@types/mdurl": "^2" + } + }, + "node_modules/@types/mdurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@types/mdurl/-/mdurl-2.0.0.tgz", + "integrity": "sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg==", + "license": "MIT", + "optional": true + }, + "node_modules/@types/minimatch": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-5.1.2.tgz", + "integrity": "sha512-K0VQKziLUWkVKiRVrx4a40iPaxTUefQmjtkQofBkYRcoaaL/8rhwDWww9qWbrgicNOgnpIsMxyNIUM4+n6dUIA==", + "license": "MIT", + "optional": true + }, + "node_modules/@types/ms": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz", + "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==", + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "22.19.13", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.13.tgz", + "integrity": "sha512-akNQMv0wW5uyRpD2v2IEyRSZiR+BeGuoB6L310EgGObO44HSMNT8z1xzio28V8qOrgYaopIDNA18YgdXd+qTiw==", + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@types/qs": { + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.14.0.tgz", + "integrity": "sha512-eOunJqu0K1923aExK6y8p6fsihYEn/BYuQ4g0CxAAgFc4b/ZLN4CrsRZ55srTdqoiLzU2B2evC+apEIxprEzkQ==", + "license": "MIT" + }, + "node_modules/@types/range-parser": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz", + "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==", + "license": "MIT" + }, + "node_modules/@types/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-F3OznnSLAUxFrCEu/L5PY8+ny8DtcFRjx7fZZ9bycvXRi3KPTRS9HOitGZwvPg0juRhXFWIeKX58cnX5YqLohQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "@types/glob": "*", + "@types/node": "*" + } + }, + "node_modules/@types/send": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@types/send/-/send-1.2.1.tgz", + "integrity": "sha512-arsCikDvlU99zl1g69TcAB3mzZPpxgw0UQnaHeC1Nwb015xp8bknZv5rIfri9xTOcMuaVgvabfIRA7PSZVuZIQ==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/serve-static": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-2.2.0.tgz", + "integrity": "sha512-8mam4H1NHLtu7nmtalF7eyBH14QyOASmcxHhSfEoRyr0nP/YdoesEtU+uSRvMe96TW/HPTtkoKqQLl53N7UXMQ==", + "license": "MIT", + "dependencies": { + "@types/http-errors": "*", + "@types/node": "*" + } + }, + "node_modules/@ungap/structured-clone": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.0.tgz", + "integrity": "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==", + "dev": true, + "license": "ISC" + }, + "node_modules/abort-controller": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", + "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", + "license": "MIT", + "optional": true, + "dependencies": { + "event-target-shim": "^5.0.0" + }, + "engines": { + "node": ">=6.5" + } + }, + "node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "license": "MIT", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/acorn": { + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", + "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", + "devOptional": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "devOptional": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/ajv": { + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz", + "integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "devOptional": true, + "license": "Python-2.0" + }, + "node_modules/array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", + "license": "MIT" + }, + "node_modules/arrify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/arrify/-/arrify-2.0.1.tgz", + "integrity": "sha512-3duEwti880xqi4eAMN8AyR4a0ByT90zoYdLlevfrvU43vb0YZwZVfxOgxWrLXXXpyugL0hNZc9G6BiB5B3nUug==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/async-retry": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/async-retry/-/async-retry-1.3.3.tgz", + "integrity": "sha512-wfr/jstw9xNi/0teMHrRW7dsz3Lt5ARhYNZ2ewpadnhaIp5mbALhOAP+EAdsC7t4Z6wqsDVv9+W6gm1Dk9mEyw==", + "license": "MIT", + "optional": true, + "dependencies": { + "retry": "0.13.1" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "optional": true + }, + "node_modules/bignumber.js": { + "version": "9.3.1", + "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.3.1.tgz", + "integrity": "sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ==", + "license": "MIT", + "optional": true, + "engines": { + "node": "*" + } + }, + "node_modules/bluebird": { + "version": "3.7.2", + "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz", + "integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==", + "license": "MIT", + "optional": true + }, + "node_modules/body-parser": { + "version": "1.20.4", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.4.tgz", + "integrity": "sha512-ZTgYYLMOXY9qKU/57FAo8F+HA2dGX7bqGc71txDRC1rS4frdFI5R7NhluHxH6M0YItAP0sHB4uqAOcYKxO6uGA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "content-type": "~1.0.5", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "~1.2.0", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "on-finished": "~2.4.1", + "qs": "~6.14.0", + "raw-body": "~2.5.3", + "type-is": "~1.6.18", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/body-parser/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/body-parser/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==", + "license": "BSD-3-Clause" + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/catharsis": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/catharsis/-/catharsis-0.9.0.tgz", + "integrity": "sha512-prMTQVpcns/tzFgFVkVp6ak6RykZyWb3gu8ckUpd6YkTlacOd3DXGJjIpD4Q6zJirizvaiAjSSHlOsA+6sNh2A==", + "license": "MIT", + "optional": true, + "dependencies": { + "lodash": "^4.17.15" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "license": "ISC", + "optional": true, + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/cliui/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT", + "optional": true + }, + "node_modules/cliui/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "optional": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "license": "MIT", + "optional": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/compressible": { + "version": "2.0.18", + "resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.18.tgz", + "integrity": "sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==", + "license": "MIT", + "optional": true, + "dependencies": { + "mime-db": ">= 1.43.0 < 2" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "license": "MIT", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.7.tgz", + "integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==", + "license": "MIT" + }, + "node_modules/cors": { + "version": "2.8.6", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.6.tgz", + "integrity": "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==", + "license": "MIT", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "license": "MIT", + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/doctrine": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", + "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/duplexify": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/duplexify/-/duplexify-4.1.3.tgz", + "integrity": "sha512-M3BmBhwJRZsSx38lZyhE53Csddgzl5R7xGJNk7CVddZD6CcmwMCH8J+7AprIrQKH7TonKxaCjcv27Qmf+sQ+oA==", + "license": "MIT", + "optional": true, + "dependencies": { + "end-of-stream": "^1.4.1", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1", + "stream-shift": "^1.0.2" + } + }, + "node_modules/ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT" + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/end-of-stream": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", + "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", + "license": "MIT", + "optional": true, + "dependencies": { + "once": "^1.4.0" + } + }, + "node_modules/ent": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/ent/-/ent-2.2.2.tgz", + "integrity": "sha512-kKvD1tO6BM+oK9HzCPpUdRb4vKFQY/FPTFmurMvh6LlN68VMrdj77w8yp51/kDbpkFOS9J8w5W6zIzgM2H8/hw==", + "license": "MIT", + "optional": true, + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "punycode": "^1.4.1", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ent/node_modules/punycode": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", + "integrity": "sha512-jmYNElW7yvO7TV33CjSmvSiE2yco3bV2czu/OzDKdMNVZQWfxCblURLhf+47syQRBntjfLdd/H0egrzIG+oaFQ==", + "license": "MIT", + "optional": true + }, + "node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "license": "BSD-2-Clause", + "optional": true, + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/escodegen": { + "version": "1.14.3", + "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-1.14.3.tgz", + "integrity": "sha512-qFcX0XJkdg+PB3xjZZG/wKSuT1PnQWx57+TVSjIMmILd2yC/6ByYElPwJnslDsuWuSAp4AwJGumarAAmJch5Kw==", + "license": "BSD-2-Clause", + "optional": true, + "dependencies": { + "esprima": "^4.0.1", + "estraverse": "^4.2.0", + "esutils": "^2.0.2", + "optionator": "^0.8.1" + }, + "bin": { + "escodegen": "bin/escodegen.js", + "esgenerate": "bin/esgenerate.js" + }, + "engines": { + "node": ">=4.0" + }, + "optionalDependencies": { + "source-map": "~0.6.1" + } + }, + "node_modules/escodegen/node_modules/estraverse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", + "license": "BSD-2-Clause", + "optional": true, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/escodegen/node_modules/levn": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz", + "integrity": "sha512-0OO4y2iOHix2W6ujICbKIaEQXvFQHue65vUG3pb5EUomzPI90z9hsA1VsO/dbIIpC53J8gxM9Q4Oho0jrCM/yA==", + "license": "MIT", + "optional": true, + "dependencies": { + "prelude-ls": "~1.1.2", + "type-check": "~0.3.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/escodegen/node_modules/optionator": { + "version": "0.8.3", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.3.tgz", + "integrity": "sha512-+IW9pACdk3XWmmTXG8m3upGUJst5XRGzxMRjXzAuJ1XnIFNvfhjjIuYkDvysnPQ7qzqVzLt78BCruntqRhWQbA==", + "license": "MIT", + "optional": true, + "dependencies": { + "deep-is": "~0.1.3", + "fast-levenshtein": "~2.0.6", + "levn": "~0.3.0", + "prelude-ls": "~1.1.2", + "type-check": "~0.3.2", + "word-wrap": "~1.2.3" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/escodegen/node_modules/prelude-ls": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz", + "integrity": "sha512-ESF23V4SKG6lVSGZgYNpbsiaAkdab6ZgOxe52p7+Kid3W3u3bxR4Vfd/o21dmN7jSt0IwgZ4v5MUd26FEtXE9w==", + "optional": true, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/escodegen/node_modules/type-check": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz", + "integrity": "sha512-ZCmOJdvOWDBYJlzAoFkC+Q0+bUyEOS1ltgp1MGU03fqHG+dbi9tBFU2Rd9QKiDZFAYrhPh2JUf7rZRIuHRKtOg==", + "license": "MIT", + "optional": true, + "dependencies": { + "prelude-ls": "~1.1.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/eslint": { + "version": "8.57.1", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.1.tgz", + "integrity": "sha512-ypowyDxpVSYpkXr9WPv2PAZCtNip1Mv5KTW0SCurXv/9iOpcrH9PaqUElksqEB6pChqHGDRCFTyrZlGhnLNGiA==", + "deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.2.0", + "@eslint-community/regexpp": "^4.6.1", + "@eslint/eslintrc": "^2.1.4", + "@eslint/js": "8.57.1", + "@humanwhocodes/config-array": "^0.13.0", + "@humanwhocodes/module-importer": "^1.0.1", + "@nodelib/fs.walk": "^1.2.8", + "@ungap/structured-clone": "^1.2.0", + "ajv": "^6.12.4", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.2", + "debug": "^4.3.2", + "doctrine": "^3.0.0", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^7.2.2", + "eslint-visitor-keys": "^3.4.3", + "espree": "^9.6.1", + "esquery": "^1.4.2", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^6.0.1", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "globals": "^13.19.0", + "graphemer": "^1.4.0", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "is-path-inside": "^3.0.3", + "js-yaml": "^4.1.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "levn": "^0.4.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3", + "strip-ansi": "^6.0.1", + "text-table": "^0.2.0" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-config-google": { + "version": "0.14.0", + "resolved": "https://registry.npmjs.org/eslint-config-google/-/eslint-config-google-0.14.0.tgz", + "integrity": "sha512-WsbX4WbjuMvTdeVL6+J3rK1RGhCTqjsFjX7UMSMgZiyxxaNLkoJENbrGExzERFeoTpGw3F3FypTiWAP9ZXzkEw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=0.10.0" + }, + "peerDependencies": { + "eslint": ">=5.16.0" + } + }, + "node_modules/eslint-scope": { + "version": "7.2.2", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", + "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "devOptional": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/espree": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", + "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", + "devOptional": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.9.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^3.4.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "license": "BSD-2-Clause", + "optional": true, + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/esquery": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz", + "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "devOptional": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "devOptional": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/event-target-shim": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", + "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/express": { + "version": "4.22.1", + "resolved": "https://registry.npmjs.org/express/-/express-4.22.1.tgz", + "integrity": "sha512-F2X8g9P1X7uCPZMA3MVf9wcTqlyNp7IhH5qPCI0izhaOIYXaW9L535tGA3qmjRzpH+bZczqq7hVKxTR4NWnu+g==", + "license": "MIT", + "dependencies": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "~1.20.3", + "content-disposition": "~0.5.4", + "content-type": "~1.0.4", + "cookie": "~0.7.1", + "cookie-signature": "~1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "~1.3.1", + "fresh": "~0.5.2", + "http-errors": "~2.0.0", + "merge-descriptors": "1.0.3", + "methods": "~1.1.2", + "on-finished": "~2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "~0.1.12", + "proxy-addr": "~2.0.7", + "qs": "~6.14.0", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "~0.19.0", + "serve-static": "~1.16.2", + "setprototypeof": "1.2.0", + "statuses": "~2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/express/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/express/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", + "license": "MIT", + "optional": true + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/fast-text-encoding": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/fast-text-encoding/-/fast-text-encoding-1.0.6.tgz", + "integrity": "sha512-VhXlQgj9ioXCqGstD37E/HBeqEGV/qOD/kmbVG8h5xKBYvM1L3lR1Zn4555cQ8GkYbJa8aJSipLPndE1k6zK2w==", + "license": "Apache-2.0", + "optional": true + }, + "node_modules/fast-xml-parser": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-4.5.4.tgz", + "integrity": "sha512-jE8ugADnYOBsu1uaoayVl1tVKAMNOXyjwvv2U6udEA2ORBhDooJDWoGxTkhd4Qn4yh59JVVt/pKXtjPwx9OguQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT", + "optional": true, + "dependencies": { + "strnum": "^1.0.5" + }, + "bin": { + "fxparser": "src/cli/cli.js" + } + }, + "node_modules/fastq": { + "version": "1.20.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz", + "integrity": "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==", + "dev": true, + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/faye-websocket": { + "version": "0.11.4", + "resolved": "https://registry.npmjs.org/faye-websocket/-/faye-websocket-0.11.4.tgz", + "integrity": "sha512-CzbClwlXAuiRQAlUyfqPgvPoNKTckTPGfwZV4ZdAhVcP2lh9KUxJg2b5GkE7XbjKQ3YJnQ9z6D9ntLAlB+tP8g==", + "license": "Apache-2.0", + "dependencies": { + "websocket-driver": ">=0.5.1" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/file-entry-cache": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", + "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^3.0.4" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/finalhandler": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.2.tgz", + "integrity": "sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "on-finished": "~2.4.1", + "parseurl": "~1.3.3", + "statuses": "~2.0.2", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/finalhandler/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/finalhandler/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/firebase-admin": { + "version": "11.11.1", + "resolved": "https://registry.npmjs.org/firebase-admin/-/firebase-admin-11.11.1.tgz", + "integrity": "sha512-UyEbq+3u6jWzCYbUntv/HuJiTixwh36G1R9j0v71mSvGAx/YZEWEW7uSGLYxBYE6ckVRQoKMr40PYUEzrm/4dg==", + "license": "Apache-2.0", + "dependencies": { + "@fastify/busboy": "^1.2.1", + "@firebase/database-compat": "^0.3.4", + "@firebase/database-types": "^0.10.4", + "@types/node": ">=12.12.47", + "jsonwebtoken": "^9.0.0", + "jwks-rsa": "^3.0.1", + "node-forge": "^1.3.1", + "uuid": "^9.0.0" + }, + "engines": { + "node": ">=14" + }, + "optionalDependencies": { + "@google-cloud/firestore": "^6.8.0", + "@google-cloud/storage": "^6.9.5" + } + }, + "node_modules/firebase-functions": { + "version": "3.24.1", + "resolved": "https://registry.npmjs.org/firebase-functions/-/firebase-functions-3.24.1.tgz", + "integrity": "sha512-GYhoyOV0864HFMU1h/JNBXYNmDk2MlbvU7VO/5qliHX6u/6vhSjTJjlyCG4leDEI8ew8IvmkIC5QquQ1U8hAuA==", + "license": "MIT", + "dependencies": { + "@types/cors": "^2.8.5", + "@types/express": "4.17.3", + "cors": "^2.8.5", + "express": "^4.17.1", + "lodash": "^4.17.14", + "node-fetch": "^2.6.7" + }, + "bin": { + "firebase-functions": "lib/bin/firebase-functions.js" + }, + "engines": { + "node": "^8.13.0 || >=10.10.0" + }, + "peerDependencies": { + "firebase-admin": "^8.0.0 || ^9.0.0 || ^10.0.0 || ^11.0.0" + } + }, + "node_modules/firebase-functions-test": { + "version": "3.4.1", + "resolved": "https://registry.npmjs.org/firebase-functions-test/-/firebase-functions-test-3.4.1.tgz", + "integrity": "sha512-qAq0oszrBGdf4bnCF6t4FoSgMsepeIXh0Pi/FhikSE6e+TvKKGpfrfUP/5pFjJZxFcLsweoau88KydCql4xSeg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/lodash": "^4.14.104", + "lodash": "^4.17.5", + "ts-deepmerge": "^2.0.1" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "firebase-admin": "^8.0.0 || ^9.0.0 || ^10.0.0 || ^11.0.0 || ^12.0.0 || ^13.0.0", + "firebase-functions": ">=4.9.0", + "jest": ">=28.0.0" + } + }, + "node_modules/flat-cache": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.2.0.tgz", + "integrity": "sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.3", + "rimraf": "^3.0.2" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/flatted": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz", + "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==", + "dev": true, + "license": "ISC" + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "devOptional": true, + "license": "ISC" + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/functional-red-black-tree": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz", + "integrity": "sha512-dsKNQNdj6xA3T+QlADDA7mOSlX0qiMINjn0cgr+eGHGsbSHzTabcIogz2+p/iqP1Xs6EP/sS2SbqH+brGTbq0g==", + "license": "MIT", + "optional": true + }, + "node_modules/gaxios": { + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-5.1.3.tgz", + "integrity": "sha512-95hVgBRgEIRQQQHIbnxBXeHbW4TqFk4ZDJW7wmVtvYar72FdhRIo1UGOLS2eRAKCPEdPBWu+M7+A33D9CdX9rA==", + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "extend": "^3.0.2", + "https-proxy-agent": "^5.0.0", + "is-stream": "^2.0.0", + "node-fetch": "^2.6.9" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/gcp-metadata": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-5.3.0.tgz", + "integrity": "sha512-FNTkdNEnBdlqF2oatizolQqNANMrcqJt6AAYt99B3y1aLLC8Hc5IOBb+ZnnzllodEEf6xMBp6wRcBbc16fa65w==", + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "gaxios": "^5.0.0", + "json-bigint": "^1.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "license": "ISC", + "optional": true, + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/globals": { + "version": "13.24.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", + "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "type-fest": "^0.20.2" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/google-auth-library": { + "version": "8.9.0", + "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-8.9.0.tgz", + "integrity": "sha512-f7aQCJODJFmYWN6PeNKzgvy9LI2tYmXnzpNDHEjG5sDNPgGb2FXQyTBnXeSH+PAtpKESFD+LmHw3Ox3mN7e1Fg==", + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "arrify": "^2.0.0", + "base64-js": "^1.3.0", + "ecdsa-sig-formatter": "^1.0.11", + "fast-text-encoding": "^1.0.0", + "gaxios": "^5.0.0", + "gcp-metadata": "^5.3.0", + "gtoken": "^6.1.0", + "jws": "^4.0.0", + "lru-cache": "^6.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/google-auth-library/node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "license": "ISC", + "optional": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/google-auth-library/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "license": "ISC", + "optional": true + }, + "node_modules/google-gax": { + "version": "3.6.1", + "resolved": "https://registry.npmjs.org/google-gax/-/google-gax-3.6.1.tgz", + "integrity": "sha512-g/lcUjGcB6DSw2HxgEmCDOrI/CByOwqRvsuUvNalHUK2iPPPlmAIpbMbl62u0YufGMr8zgE3JL7th6dCb1Ry+w==", + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "@grpc/grpc-js": "~1.8.0", + "@grpc/proto-loader": "^0.7.0", + "@types/long": "^4.0.0", + "@types/rimraf": "^3.0.2", + "abort-controller": "^3.0.0", + "duplexify": "^4.0.0", + "fast-text-encoding": "^1.0.3", + "google-auth-library": "^8.0.2", + "is-stream-ended": "^0.1.4", + "node-fetch": "^2.6.1", + "object-hash": "^3.0.0", + "proto3-json-serializer": "^1.0.0", + "protobufjs": "7.2.4", + "protobufjs-cli": "1.1.1", + "retry-request": "^5.0.0" + }, + "bin": { + "compileProtos": "build/tools/compileProtos.js", + "minifyProtoJson": "build/tools/minify.js" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/google-gax/node_modules/protobufjs": { + "version": "7.2.4", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.2.4.tgz", + "integrity": "sha512-AT+RJgD2sH8phPmCf7OUZR8xGdcJRga4+1cOaXJ64hvcSkVhNcRHOwIxUatPH15+nj59WAGTDv3LSGZPEQbJaQ==", + "hasInstallScript": true, + "license": "BSD-3-Clause", + "optional": true, + "dependencies": { + "@protobufjs/aspromise": "^1.1.2", + "@protobufjs/base64": "^1.1.2", + "@protobufjs/codegen": "^2.0.4", + "@protobufjs/eventemitter": "^1.1.0", + "@protobufjs/fetch": "^1.1.0", + "@protobufjs/float": "^1.0.2", + "@protobufjs/inquire": "^1.1.0", + "@protobufjs/path": "^1.1.2", + "@protobufjs/pool": "^1.1.0", + "@protobufjs/utf8": "^1.1.0", + "@types/node": ">=13.7.0", + "long": "^5.0.0" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/google-p12-pem": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/google-p12-pem/-/google-p12-pem-4.0.1.tgz", + "integrity": "sha512-WPkN4yGtz05WZ5EhtlxNDWPhC4JIic6G8ePitwUWy4l+XPVYec+a0j0Ts47PDtW59y3RwAhUd9/h9ZZ63px6RQ==", + "deprecated": "Package is no longer maintained", + "license": "MIT", + "optional": true, + "dependencies": { + "node-forge": "^1.3.1" + }, + "bin": { + "gp12-pem": "build/src/bin/gp12-pem.js" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "license": "ISC", + "optional": true + }, + "node_modules/graphemer": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", + "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", + "dev": true, + "license": "MIT" + }, + "node_modules/gtoken": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/gtoken/-/gtoken-6.1.2.tgz", + "integrity": "sha512-4ccGpzz7YAr7lxrT2neugmXQ3hP9ho2gcaityLVkiUecAiwiy60Ii8gRbZeOsXV19fYaRjgBSshs8kXw+NKCPQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "gaxios": "^5.0.1", + "google-p12-pem": "^4.0.0", + "jws": "^4.0.0" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "license": "MIT", + "optional": true, + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "license": "MIT", + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/http-parser-js": { + "version": "0.5.10", + "resolved": "https://registry.npmjs.org/http-parser-js/-/http-parser-js-0.5.10.tgz", + "integrity": "sha512-Pysuw9XpUq5dVc/2SMHpuTY01RFl8fttgcyunjL7eEMhGM3cI4eOmiCycJDVCo/7O7ClfQD3SaI6ftDzqOXYMA==", + "license": "MIT" + }, + "node_modules/http-proxy-agent": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz", + "integrity": "sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w==", + "license": "MIT", + "optional": true, + "dependencies": { + "@tootallnate/once": "2", + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "license": "MIT", + "optional": true, + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "devOptional": true, + "license": "ISC", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-path-inside": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", + "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-regex": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz", + "integrity": "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==", + "license": "MIT", + "optional": true, + "dependencies": { + "call-bound": "^1.0.2", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-stream-ended": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/is-stream-ended/-/is-stream-ended-0.1.4.tgz", + "integrity": "sha512-xj0XPvmr7bQFTvirqnFr50o0hQIh6ZItDqloxt5aJrR4NQsYeSsyFQERYGCAzfindAcnKjINnwEEgLx4IqVzQw==", + "license": "MIT", + "optional": true + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/jose": { + "version": "4.15.9", + "resolved": "https://registry.npmjs.org/jose/-/jose-4.15.9.tgz", + "integrity": "sha512-1vUQX+IdDMVPj4k8kOxgUqlcK518yluMuGZwqlr44FS1ppZB/5GWh4rZG89erpOBOJjU/OBsnCVFfapsRz6nEA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, + "node_modules/js-yaml": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/js2xmlparser": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/js2xmlparser/-/js2xmlparser-4.0.2.tgz", + "integrity": "sha512-6n4D8gLlLf1n5mNLQPRfViYzu9RATblzPEtm1SthMX1Pjao0r9YI9nw7ZIfRxQMERS87mcswrg+r/OYrPRX6jA==", + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "xmlcreate": "^2.0.4" + } + }, + "node_modules/jsdoc": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/jsdoc/-/jsdoc-4.0.5.tgz", + "integrity": "sha512-P4C6MWP9yIlMiK8nwoZvxN84vb6MsnXcHuy7XzVOvQoCizWX5JFCBsWIIWKXBltpoRZXddUOVQmCTOZt9yDj9g==", + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "@babel/parser": "^7.20.15", + "@jsdoc/salty": "^0.2.1", + "@types/markdown-it": "^14.1.1", + "bluebird": "^3.7.2", + "catharsis": "^0.9.0", + "escape-string-regexp": "^2.0.0", + "js2xmlparser": "^4.0.2", + "klaw": "^3.0.0", + "markdown-it": "^14.1.0", + "markdown-it-anchor": "^8.6.7", + "marked": "^4.0.10", + "mkdirp": "^1.0.4", + "requizzle": "^0.2.3", + "strip-json-comments": "^3.1.0", + "underscore": "~1.13.2" + }, + "bin": { + "jsdoc": "jsdoc.js" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/jsdoc/node_modules/escape-string-regexp": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", + "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/json-bigint": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-bigint/-/json-bigint-1.0.0.tgz", + "integrity": "sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "bignumber.js": "^9.0.0" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/jsonwebtoken": { + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.3.tgz", + "integrity": "sha512-MT/xP0CrubFRNLNKvxJ2BYfy53Zkm++5bX9dtuPbqAeQpTVe0MQTFhao8+Cp//EmJp244xt6Drw/GVEGCUj40g==", + "license": "MIT", + "dependencies": { + "jws": "^4.0.1", + "lodash.includes": "^4.3.0", + "lodash.isboolean": "^3.0.3", + "lodash.isinteger": "^4.0.4", + "lodash.isnumber": "^3.0.3", + "lodash.isplainobject": "^4.0.6", + "lodash.isstring": "^4.0.1", + "lodash.once": "^4.0.0", + "ms": "^2.1.1", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=12", + "npm": ">=6" + } + }, + "node_modules/jsonwebtoken/node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/jwa": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz", + "integrity": "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==", + "license": "MIT", + "dependencies": { + "buffer-equal-constant-time": "^1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jwks-rsa": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/jwks-rsa/-/jwks-rsa-3.2.2.tgz", + "integrity": "sha512-BqTyEDV+lS8F2trk3A+qJnxV5Q9EqKCBJOPti3W97r7qTympCZjb7h2X6f2kc+0K3rsSTY1/6YG2eaXKoj497w==", + "license": "MIT", + "dependencies": { + "@types/jsonwebtoken": "^9.0.4", + "debug": "^4.3.4", + "jose": "^4.15.4", + "limiter": "^1.1.5", + "lru-memoizer": "^2.2.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/jws": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.1.tgz", + "integrity": "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==", + "license": "MIT", + "dependencies": { + "jwa": "^2.0.1", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/klaw": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/klaw/-/klaw-3.0.0.tgz", + "integrity": "sha512-0Fo5oir+O9jnXu5EefYbVK+mHMBeEVEy2cmctR1O1NECcCkPRreJKrS6Qt/j3KC2C148Dfo9i3pCmCMsdqGr0g==", + "license": "MIT", + "optional": true, + "dependencies": { + "graceful-fs": "^4.1.9" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/limiter": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/limiter/-/limiter-1.1.5.tgz", + "integrity": "sha512-FWWMIEOxz3GwUI4Ts/IvgVy6LPvoMPgjMdQ185nN6psJyBJ4yOpzqm695/h5umdLJg2vW3GR5iG11MAkR2AzJA==" + }, + "node_modules/linkify-it": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-5.0.0.tgz", + "integrity": "sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "uc.micro": "^2.0.0" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash": { + "version": "4.17.23", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.23.tgz", + "integrity": "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==", + "license": "MIT" + }, + "node_modules/lodash.camelcase": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz", + "integrity": "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==", + "license": "MIT", + "optional": true + }, + "node_modules/lodash.clonedeep": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz", + "integrity": "sha512-H5ZhCF25riFd9uB5UCkVKo61m3S/xZk1x4wA6yp/L3RFP6Z/eHH1ymQcGLo7J3GMPfm0V/7m1tryHuGVxpqEBQ==", + "license": "MIT" + }, + "node_modules/lodash.includes": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", + "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==", + "license": "MIT" + }, + "node_modules/lodash.isboolean": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", + "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==", + "license": "MIT" + }, + "node_modules/lodash.isinteger": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", + "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==", + "license": "MIT" + }, + "node_modules/lodash.isnumber": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", + "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==", + "license": "MIT" + }, + "node_modules/lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==", + "license": "MIT" + }, + "node_modules/lodash.isstring": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", + "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==", + "license": "MIT" + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.once": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", + "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==", + "license": "MIT" + }, + "node_modules/long": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz", + "integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==", + "license": "Apache-2.0", + "optional": true + }, + "node_modules/lru-memoizer": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/lru-memoizer/-/lru-memoizer-2.3.0.tgz", + "integrity": "sha512-GXn7gyHAMhO13WSKrIiNfztwxodVsP8IoZ3XfrJV4yH2x0/OeTO/FIaAHTY5YekdGgW94njfuKmyyt1E0mR6Ug==", + "license": "MIT", + "dependencies": { + "lodash.clonedeep": "^4.5.0", + "lru-cache": "6.0.0" + } + }, + "node_modules/lru-memoizer/node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/lru-memoizer/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "license": "ISC" + }, + "node_modules/markdown-it": { + "version": "14.1.1", + "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-14.1.1.tgz", + "integrity": "sha512-BuU2qnTti9YKgK5N+IeMubp14ZUKUUw7yeJbkjtosvHiP0AZ5c8IAgEMk79D0eC8F23r4Ac/q8cAIFdm2FtyoA==", + "license": "MIT", + "optional": true, + "dependencies": { + "argparse": "^2.0.1", + "entities": "^4.4.0", + "linkify-it": "^5.0.0", + "mdurl": "^2.0.0", + "punycode.js": "^2.3.1", + "uc.micro": "^2.1.0" + }, + "bin": { + "markdown-it": "bin/markdown-it.mjs" + } + }, + "node_modules/markdown-it-anchor": { + "version": "8.6.7", + "resolved": "https://registry.npmjs.org/markdown-it-anchor/-/markdown-it-anchor-8.6.7.tgz", + "integrity": "sha512-FlCHFwNnutLgVTflOYHPW2pPcl2AACqVzExlkGQNsi4CJgqOHN7YTgDd4LuhgN1BFO3TS0vLAruV1Td6dwWPJA==", + "license": "Unlicense", + "optional": true, + "peerDependencies": { + "@types/markdown-it": "*", + "markdown-it": "*" + } + }, + "node_modules/marked": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/marked/-/marked-4.3.0.tgz", + "integrity": "sha512-PRsaiG84bK+AMvxziE/lCFss8juXjNaWzVbN5tXAm4XjeaS9NAHhop+PjQxz2A9h8Q4M/xGmzP8vqNwy6JeK0A==", + "license": "MIT", + "optional": true, + "bin": { + "marked": "bin/marked.js" + }, + "engines": { + "node": ">= 12" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/mdurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-2.0.0.tgz", + "integrity": "sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==", + "license": "MIT", + "optional": true + }, + "node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/merge-descriptors": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", + "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-3.0.0.tgz", + "integrity": "sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A==", + "license": "MIT", + "optional": true, + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "license": "MIT", + "optional": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "license": "MIT", + "optional": true, + "bin": { + "mkdirp": "bin/cmd.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, + "node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "license": "MIT", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/node-forge": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.3.tgz", + "integrity": "sha512-rLvcdSyRCyouf6jcOIPe/BgwG/d7hKjzMKOas33/pHEr6gbq18IK9zV7DiPvzsz0oBJPme6qr6H6kGZuI9/DZg==", + "license": "(BSD-3-Clause OR GPL-2.0)", + "engines": { + "node": ">= 6.13.0" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-hash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", + "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 6" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "devOptional": true, + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-to-regexp": { + "version": "0.1.12", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz", + "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==", + "license": "MIT" + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/proto3-json-serializer": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/proto3-json-serializer/-/proto3-json-serializer-1.1.1.tgz", + "integrity": "sha512-AwAuY4g9nxx0u52DnSMkqqgyLHaW/XaPLtaAo3y/ZCfeaQB/g4YDH4kb8Wc/mWzWvu0YjOznVnfn373MVZZrgw==", + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "protobufjs": "^7.0.0" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/protobufjs": { + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.5.4.tgz", + "integrity": "sha512-CvexbZtbov6jW2eXAvLukXjXUW1TzFaivC46BpWc/3BpcCysb5Vffu+B3XHMm8lVEuy2Mm4XGex8hBSg1yapPg==", + "hasInstallScript": true, + "license": "BSD-3-Clause", + "optional": true, + "dependencies": { + "@protobufjs/aspromise": "^1.1.2", + "@protobufjs/base64": "^1.1.2", + "@protobufjs/codegen": "^2.0.4", + "@protobufjs/eventemitter": "^1.1.0", + "@protobufjs/fetch": "^1.1.0", + "@protobufjs/float": "^1.0.2", + "@protobufjs/inquire": "^1.1.0", + "@protobufjs/path": "^1.1.2", + "@protobufjs/pool": "^1.1.0", + "@protobufjs/utf8": "^1.1.0", + "@types/node": ">=13.7.0", + "long": "^5.0.0" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/protobufjs-cli": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/protobufjs-cli/-/protobufjs-cli-1.1.1.tgz", + "integrity": "sha512-VPWMgIcRNyQwWUv8OLPyGQ/0lQY/QTQAVN5fh+XzfDwsVw1FZ2L3DM/bcBf8WPiRz2tNpaov9lPZfNcmNo6LXA==", + "license": "BSD-3-Clause", + "optional": true, + "dependencies": { + "chalk": "^4.0.0", + "escodegen": "^1.13.0", + "espree": "^9.0.0", + "estraverse": "^5.1.0", + "glob": "^8.0.0", + "jsdoc": "^4.0.0", + "minimist": "^1.2.0", + "semver": "^7.1.2", + "tmp": "^0.2.1", + "uglify-js": "^3.7.7" + }, + "bin": { + "pbjs": "bin/pbjs", + "pbts": "bin/pbts" + }, + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "protobufjs": "^7.0.0" + } + }, + "node_modules/protobufjs-cli/node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/protobufjs-cli/node_modules/glob": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-8.1.0.tgz", + "integrity": "sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "license": "ISC", + "optional": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^5.0.1", + "once": "^1.3.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/protobufjs-cli/node_modules/minimatch": { + "version": "5.1.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.9.tgz", + "integrity": "sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw==", + "license": "ISC", + "optional": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/protobufjs-cli/node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "license": "ISC", + "optional": true, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/punycode.js": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode.js/-/punycode.js-2.3.1.tgz", + "integrity": "sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/qs": { + "version": "6.14.2", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.2.tgz", + "integrity": "sha512-V/yCWTTF7VJ9hIh18Ugr2zhJMP01MY7c5kh4J870L7imm6/DIzBsNLTXzMwUA3yZ5b/KBqLx8Kp3uRvd7xSe3Q==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "2.5.3", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.3.tgz", + "integrity": "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "license": "MIT", + "optional": true, + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/requizzle": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/requizzle/-/requizzle-0.2.4.tgz", + "integrity": "sha512-JRrFk1D4OQ4SqovXOgdav+K8EAhSB/LJZqCz8tbX0KObcdeM15Ss59ozWMBWmmINMagCwmqn4ZNryUGpBsl6Jw==", + "license": "MIT", + "optional": true, + "dependencies": { + "lodash": "^4.17.21" + } + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/retry": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz", + "integrity": "sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 4" + } + }, + "node_modules/retry-request": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/retry-request/-/retry-request-5.0.2.tgz", + "integrity": "sha512-wfI3pk7EE80lCIXprqh7ym48IHYdwmAAzESdbU8Q9l7pnRCk9LEhpbOTNKjz6FARLm/Bl5m+4F0ABxOkYUujSQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "debug": "^4.1.1", + "extend": "^3.0.2" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "dev": true, + "license": "ISC", + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/rimraf/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safe-regex-test": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.1.0.tgz", + "integrity": "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==", + "license": "MIT", + "optional": true, + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "is-regex": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/send": { + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/send/-/send-0.19.2.tgz", + "integrity": "sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "~0.5.2", + "http-errors": "~2.0.1", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "~2.4.1", + "range-parser": "~1.2.1", + "statuses": "~2.0.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/send/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/send/node_modules/debug/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/send/node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/serve-static": { + "version": "1.16.3", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.3.tgz", + "integrity": "sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA==", + "license": "MIT", + "dependencies": { + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "~0.19.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "license": "BSD-3-Clause", + "optional": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/stream-events": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/stream-events/-/stream-events-1.0.5.tgz", + "integrity": "sha512-E1GUzBSgvct8Jsb3v2X15pjzN1tYebtbLaMg+eBOUOAxgbLoSbT2NS91ckc5lJD1KfLjId+jXJRgo0qnV5Nerg==", + "license": "MIT", + "optional": true, + "dependencies": { + "stubs": "^3.0.0" + } + }, + "node_modules/stream-shift": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/stream-shift/-/stream-shift-1.0.3.tgz", + "integrity": "sha512-76ORR0DO1o1hlKwTbi/DM3EXWGf3ZJYO8cXX5RJwnul2DEg2oyoZyjLNoQM8WsvZiFKCRfC1O0J7iCvie3RZmQ==", + "license": "MIT", + "optional": true + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "license": "MIT", + "optional": true, + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/strnum": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/strnum/-/strnum-1.1.2.tgz", + "integrity": "sha512-vrN+B7DBIoTTZjnPNewwhx6cBA/H+IS7rfW68n7XxC1y7uoiGQBxaKzqucGUgavX15dJgiGztLJ8vxuEzwqBdA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT", + "optional": true + }, + "node_modules/stubs": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/stubs/-/stubs-3.0.0.tgz", + "integrity": "sha512-PdHt7hHUJKxvTCgbKX9C1V/ftOcjJQgz8BZwNfV5c4B6dcGqlpelTbJ999jBGZ2jYiPAwcX5dP6oBwVlBlUbxw==", + "license": "MIT", + "optional": true + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/teeny-request": { + "version": "8.0.3", + "resolved": "https://registry.npmjs.org/teeny-request/-/teeny-request-8.0.3.tgz", + "integrity": "sha512-jJZpA5He2y52yUhA7pyAGZlgQpcB+xLjcN0eUFxr9c8hP/H7uOXbBNVo/O0C/xVfJLJs680jvkFgVJEEvk9+ww==", + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "http-proxy-agent": "^5.0.0", + "https-proxy-agent": "^5.0.0", + "node-fetch": "^2.6.1", + "stream-events": "^1.0.5", + "uuid": "^9.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/text-decoding": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/text-decoding/-/text-decoding-1.0.0.tgz", + "integrity": "sha512-/0TJD42KDnVwKmDK6jj3xP7E2MG7SHAOG4tyTgyUCRPdHwvkquYNLEQltmdMa3owq3TkddCVcTsoctJI8VQNKA==", + "license": "MIT" + }, + "node_modules/text-table": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", + "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", + "dev": true, + "license": "MIT" + }, + "node_modules/tmp": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.5.tgz", + "integrity": "sha512-voyz6MApa1rQGUxT3E+BK7/ROe8itEx7vD8/HEvt4xwXucvQ5G5oeEiHkmHZJuBO21RpOf+YYm9MOivj709jow==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=14.14" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", + "license": "MIT" + }, + "node_modules/ts-deepmerge": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/ts-deepmerge/-/ts-deepmerge-2.0.7.tgz", + "integrity": "sha512-3phiGcxPSSR47RBubQxPoZ+pqXsEsozLo4G4AlSrsMKTFg9TA3l+3he5BqpUi9wiuDbaHWXH/amlzQ49uEdXtg==", + "dev": true, + "license": "ISC" + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "license": "MIT", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/uc.micro": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-2.1.0.tgz", + "integrity": "sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==", + "license": "MIT", + "optional": true + }, + "node_modules/uglify-js": { + "version": "3.19.3", + "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.19.3.tgz", + "integrity": "sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ==", + "license": "BSD-2-Clause", + "optional": true, + "bin": { + "uglifyjs": "bin/uglifyjs" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/underscore": { + "version": "1.13.8", + "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.13.8.tgz", + "integrity": "sha512-DXtD3ZtEQzc7M8m4cXotyHR+FAS18C64asBYY5vqZexfYryNNnDc02W4hKg3rdQuqOYas1jkseX0+nZXjTXnvQ==", + "license": "MIT", + "optional": true + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "license": "MIT" + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "license": "MIT", + "optional": true + }, + "node_modules/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "license": "MIT", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/uuid": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", + "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", + "license": "BSD-2-Clause" + }, + "node_modules/websocket-driver": { + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/websocket-driver/-/websocket-driver-0.7.4.tgz", + "integrity": "sha512-b17KeDIQVjvb0ssuSDF2cYXSg2iztliJ4B9WdsuB6J952qCPKmnVq4DyW5motImXHDC1cBT/1UezrJVsKw5zjg==", + "license": "Apache-2.0", + "dependencies": { + "http-parser-js": ">=0.5.1", + "safe-buffer": ">=5.1.0", + "websocket-extensions": ">=0.1.1" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/websocket-extensions": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/websocket-extensions/-/websocket-extensions-0.1.4.tgz", + "integrity": "sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "license": "MIT", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "devOptional": true, + "license": "ISC" + }, + "node_modules/xmlcreate": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/xmlcreate/-/xmlcreate-2.0.4.tgz", + "integrity": "sha512-nquOebG4sngPmGPICTS5EnxqhKbCmz5Ox5hsszI2T6U5qdrJizBc+0ilYSEjTSzU0yZcmvppztXe/5Al5fUwdg==", + "license": "Apache-2.0", + "optional": true + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "license": "ISC", + "optional": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "license": "MIT", + "optional": true, + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "license": "ISC", + "optional": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT", + "optional": true + }, + "node_modules/yargs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "optional": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + } + } +} diff --git a/functions/package.json b/functions/package.json new file mode 100644 index 0000000..e313689 --- /dev/null +++ b/functions/package.json @@ -0,0 +1,26 @@ +{ + "name": "functions", + "description": "Cloud Functions for Firebase", + "scripts": { + "lint": "echo Skipping lint", + "serve": "firebase emulators:start --only functions", + "shell": "firebase functions:shell", + "start": "npm run shell", + "deploy": "firebase deploy --only functions", + "logs": "firebase functions:log" + }, + "engines": { + "node": "18" + }, + "main": "index.js", + "dependencies": { + "firebase-admin": "^11.10.0", + "firebase-functions": "^3.23.0" + }, + "devDependencies": { + "eslint": "^8.15.0", + "eslint-config-google": "^0.14.0", + "firebase-functions-test": "^3.1.0" + }, + "private": true +} \ No newline at end of file diff --git a/lib/app/config/auth_storage/auth_storage.dart b/lib/app/config/auth_storage/auth_storage.dart index b9ef9f8..d9b7748 100644 --- a/lib/app/config/auth_storage/auth_storage.dart +++ b/lib/app/config/auth_storage/auth_storage.dart @@ -6,34 +6,63 @@ class AuthStorage { static const _tokenKey = 'auth_token'; static const _userKey = 'user_data'; static const _rememberMeKey = 'remember_me'; + static const _orderIdKey = 'order_id'; + + Future get _prefs async => + await SharedPreferences.getInstance(); + + Future saveOrderId(String orderId) async { + final prefs = await _prefs; + await prefs.setString(_orderIdKey, orderId); + } + + Future getOrderId() async { + final prefs = await _prefs; + return prefs.getString(_orderIdKey); + } + + Future clearOrderId() async { + final prefs = await _prefs; + await prefs.remove(_orderIdKey); + } Future saveToken(String token) async { - final prefs = await SharedPreferences.getInstance(); + final prefs = await _prefs; await prefs.setString(_tokenKey, token); } Future getToken() async { - final prefs = await SharedPreferences.getInstance(); + final prefs = await _prefs; return prefs.getString(_tokenKey); } Future clearToken() async { - final prefs = await SharedPreferences.getInstance(); + final prefs = await _prefs; await prefs.remove(_tokenKey); } + Future saveUserJson(String json) async { + final prefs = await SharedPreferences.getInstance(); + await prefs.setString(_userKey, json); + } + + Future getUserJson() async { + final prefs = await SharedPreferences.getInstance(); + return prefs.getString(_userKey); + } + Future clearUser() async { final prefs = await SharedPreferences.getInstance(); await prefs.remove(_userKey); } Future setRememberMe(bool value) async { - final prefs = await SharedPreferences.getInstance(); + final prefs = await _prefs; await prefs.setBool(_rememberMeKey, value); } Future getRememberMe() async { - final prefs = await SharedPreferences.getInstance(); + final prefs = await _prefs; return prefs.getBool(_rememberMeKey) ?? false; } @@ -41,5 +70,6 @@ class AuthStorage { await clearToken(); await clearUser(); await setRememberMe(false); + await clearOrderId(); } } diff --git a/lib/app/config/di/di.config.dart b/lib/app/config/di/di.config.dart index 98a5274..a6d6e01 100644 --- a/lib/app/config/di/di.config.dart +++ b/lib/app/config/di/di.config.dart @@ -1,4 +1,5 @@ // GENERATED CODE - DO NOT MODIFY BY HAND +// dart format width=80 // ************************************************************************** // InjectableConfigGenerator @@ -8,7 +9,9 @@ // coverage:ignore-file // ignore_for_file: no_leading_underscores_for_library_prefixes +import 'package:cloud_firestore/cloud_firestore.dart' as _i974; import 'package:dio/dio.dart' as _i361; +import 'package:firebase_auth/firebase_auth.dart' as _i59; import 'package:get_it/get_it.dart' as _i174; import 'package:injectable/injectable.dart' as _i526; @@ -32,6 +35,7 @@ import '../../../features/auth/domain/usecase/get_all_vehicles_usecase.dart' import '../../../features/auth/domain/usecase/get_countries_usecase.dart' as _i940; import '../../../features/auth/domain/usecase/login_usecase.dart' as _i75; +import '../../../features/auth/domain/usecase/logout_usecase.dart' as _i27; import '../../../features/auth/domain/usecase/resertpassword_usecase.dart' as _i294; import '../../../features/auth/domain/usecase/verifyreaset_usecase.dart' @@ -42,90 +46,292 @@ import '../../../features/auth/presentation/forget_pass/manager/cubit/forget_pas as _i614; import '../../../features/auth/presentation/login/manager/login_cubit.dart' as _i810; +import '../../../features/auth/presentation/logout/manager/logout_cubit.dart' + as _i1023; import '../../../features/auth/presentation/reset_password/manager/change_password_cubit.dart' as _i14; import '../../../features/auth/presentation/reset_password/manager/reset_password_cubit.dart' as _i378; import '../../../features/auth/presentation/verify_reset/manger/cubit/verify_reset_cubit.dart' as _i466; +import '../../../features/driver_orders_details/api/datasource/order_details_remote_datasource_impl.dart' + as _i860; +import '../../../features/driver_orders_details/data/datasource/order_details_remote_datasource.dart' + as _i114; +import '../../../features/driver_orders_details/data/repos/order_details_repo_impl.dart' + as _i55; +import '../../../features/driver_orders_details/domain/repos/order_details_repo.dart' + as _i313; +import '../../../features/driver_orders_details/domain/usecases/get_order_details_usecase.dart' + as _i1045; +import '../../../features/driver_orders_details/presentation/manager/order_details_cubit.dart' + as _i375; +import '../../../features/home/api/driverOrderDataS_imp.dart' as _i495; +import '../../../features/home/data/datascourse/driverOrderDatascource.dart' + as _i743; +import '../../../features/home/data/repo/driverOrderRepo_impl.dart' as _i1020; +import '../../../features/home/domain/repo/driverOrderRepo.dart' as _i499; +import '../../../features/home/domain/usecase/getdriverOrderUsecase.dart' + as _i858; +import '../../../features/home/domain/usecase/upload_driver_fire_data_use_case.dart' + as _i329; +import '../../../features/home/domain/usecase/upload_order_fire_data_use_case.dart' + as _i233; +import '../../../features/home/presentation/manger/driverorderCubit.dart' + as _i573; +import '../../../features/my_orders/api/datasource/my_orders_remote_data_source_imp.dart' + as _i583; +import '../../../features/my_orders/data/datasource/my_orders_remote_data_source.dart' + as _i466; +import '../../../features/my_orders/data/repo/my_orders_repo_imp.dart' as _i754; +import '../../../features/my_orders/domain/repo/my_orders_repo.dart' as _i919; +import '../../../features/my_orders/domain/usecases/get_order_use_case.dart' + as _i335; +import '../../../features/my_orders/presentation/manager/my_orders_cubit.dart' + as _i156; +import '../../../features/profile/api/profile_lacal_datasource_imp.dart' + as _i495; +import '../../../features/profile/api/profile_remote_datasource_imp.dart' + as _i899; +import '../../../features/profile/data/datasorce/profile_lacal_datasource.dart' + as _i697; +import '../../../features/profile/data/datasorce/profile_remote_datasource.dart' + as _i943; +import '../../../features/profile/data/repo/profile_repo_imp.dart' as _i1048; +import '../../../features/profile/domain/repo/profile_repo.dart' as _i863; +import '../../../features/profile/domain/usecases/edit_profile_usecase.dart' + as _i221; +import '../../../features/profile/domain/usecases/get_profile_usecase.dart' + as _i248; +import '../../../features/profile/domain/usecases/upload_profile_photo_usecase.dart' + as _i884; +import '../../../features/profile/presentation/managers/profile_cubit.dart' + as _i603; +import '../../../features/track_order/api/track_order_remote_source_impl.dart' + as _i1007; +import '../../../features/track_order/data/datasource/track_order_remote_source.dart' + as _i511; +import '../../../features/track_order/data/repos/track_order_repo_imp.dart' + as _i40; +import '../../../features/track_order/domain/repos/track_order_repo.dart' + as _i1042; +import '../../../features/track_order/domain/usecases/driver_usecase.dart' + as _i866; +import '../../../features/track_order/domain/usecases/track_order_usecase.dart' + as _i810; +import '../../../features/track_order/domain/usecases/update_state_usecase.dart' + as _i499; +import '../../../features/track_order/presentation/manager/cubit/track_order_cubit.dart' + as _i364; import '../../core/api_manger/api_client.dart' as _i890; +import '../../core/network/firebase_module.dart' as _i236; import '../auth_storage/auth_storage.dart' as _i603; import '../network/network_module.dart' as _i200; extension GetItInjectableX on _i174.GetIt { -// initializes the registration of main-scope dependencies inside of GetIt + // initializes the registration of main-scope dependencies inside of GetIt _i174.GetIt init({ String? environment, _i526.EnvironmentFilter? environmentFilter, }) { - final gh = _i526.GetItHelper( - this, - environment, - environmentFilter, - ); + final gh = _i526.GetItHelper(this, environment, environmentFilter); + final firebaseModule = _$FirebaseModule(); final networkModule = _$NetworkModule(); gh.factory<_i959.AppSectionCubit>(() => _i959.AppSectionCubit()); gh.lazySingleton<_i603.AuthStorage>(() => _i603.AuthStorage()); + gh.lazySingleton<_i974.FirebaseFirestore>(() => firebaseModule.firestore); + gh.lazySingleton<_i59.FirebaseAuth>(() => firebaseModule.auth); gh.lazySingleton<_i783.CountryLocalDataSource>( - () => _i783.CountryLocalDataSourceImpl()); + () => _i783.CountryLocalDataSourceImpl(), + ); gh.lazySingleton<_i361.Dio>( - () => networkModule.dio(gh<_i603.AuthStorage>())); + () => networkModule.dio(gh<_i603.AuthStorage>()), + ); + gh.factory<_i511.TrackOrderRemoteDataSource>( + () => _i1007.TrackOrderRemoteDataSourceImpl( + gh<_i974.FirebaseFirestore>(), + gh<_i603.AuthStorage>(), + ), + ); + gh.factory<_i329.UploadDriverFireDataUseCase>( + () => _i329.UploadDriverFireDataUseCase(gh<_i974.FirebaseFirestore>()), + ); + gh.factory<_i233.UploadOrderFireDataUseCase>( + () => _i233.UploadOrderFireDataUseCase(gh<_i974.FirebaseFirestore>()), + ); + gh.lazySingleton<_i697.ProfileLocalDataSource>( + () => _i495.ProfileLocalDataSourceImpl(gh<_i603.AuthStorage>()), + ); + gh.factory<_i114.OrderDetailsRemoteDatasource>( + () => _i860.OrderDetailsRemoteDatasourceImpl( + firestore: gh<_i974.FirebaseFirestore>(), + ), + ); + gh.factory<_i1042.TrackOrderRepo>( + () => _i40.TrackOrderRepoImpl(gh<_i511.TrackOrderRemoteDataSource>()), + ); + gh.factory<_i866.TrackDriverUseCase>( + () => _i866.TrackDriverUseCase(gh<_i1042.TrackOrderRepo>()), + ); + gh.factory<_i810.TrackOrderUseCase>( + () => _i810.TrackOrderUseCase(gh<_i1042.TrackOrderRepo>()), + ); + gh.factory<_i499.UpdateOrderStatusUseCase>( + () => _i499.UpdateOrderStatusUseCase(gh<_i1042.TrackOrderRepo>()), + ); gh.lazySingleton<_i890.ApiClient>( - () => networkModule.authApiClient(gh<_i361.Dio>())); + () => networkModule.authApiClient(gh<_i361.Dio>()), + ); + gh.factory<_i466.MyOrdersRemoteDataSource>( + () => _i583.MyOrdersRemoteDataSourceImp(gh<_i890.ApiClient>()), + ); + gh.factory<_i364.TrackOrderCubit>( + () => _i364.TrackOrderCubit( + gh<_i810.TrackOrderUseCase>(), + gh<_i866.TrackDriverUseCase>(), + gh<_i499.UpdateOrderStatusUseCase>(), + gh<_i603.AuthStorage>(), + ), + ); + gh.factory<_i313.OrderDetailsRepo>( + () => _i55.OrderDetailsRepoImpl(gh<_i114.OrderDetailsRemoteDatasource>()), + ); + gh.factory<_i919.MyOrdersRepo>( + () => _i754.MyOrdersRepoImpl(gh<_i466.MyOrdersRemoteDataSource>()), + ); + gh.factory<_i335.GetOrderUseCase>( + () => _i335.GetOrderUseCase(gh<_i919.MyOrdersRepo>()), + ); + gh.factory<_i1045.GetOrderDetailsUsecase>( + () => _i1045.GetOrderDetailsUsecase(repo: gh<_i313.OrderDetailsRepo>()), + ); + gh.factory<_i743.DriverOrderDataSource>( + () => _i495.DriverOrderDataSourceImpl(gh<_i890.ApiClient>()), + ); + gh.factory<_i943.ProfileRemoteDatasource>( + () => _i899.ProfileRemoteDatasourceImp(gh<_i890.ApiClient>()), + ); gh.factory<_i708.AuthRemoteDataSource>( - () => _i777.AuthRemoteDataSourceImpl(gh<_i890.ApiClient>())); + () => _i777.AuthRemoteDataSourceImpl(gh<_i890.ApiClient>()), + ); gh.factory<_i712.AuthRepo>( - () => _i566.AuthRepoImpl(gh<_i708.AuthRemoteDataSource>())); + () => _i566.AuthRepoImpl(gh<_i708.AuthRemoteDataSource>()), + ); + gh.factory<_i375.OrderDetailsCubit>( + () => _i375.OrderDetailsCubit(gh<_i1045.GetOrderDetailsUsecase>()), + ); gh.factory<_i991.ChangePasswordUsecase>( - () => _i991.ChangePasswordUsecase(gh<_i712.AuthRepo>())); + () => _i991.ChangePasswordUsecase(gh<_i712.AuthRepo>()), + ); gh.factory<_i769.ForgetPasswordUsecase>( - () => _i769.ForgetPasswordUsecase(gh<_i712.AuthRepo>())); + () => _i769.ForgetPasswordUsecase(gh<_i712.AuthRepo>()), + ); gh.factory<_i294.ResetPasswordUsecase>( - () => _i294.ResetPasswordUsecase(gh<_i712.AuthRepo>())); + () => _i294.ResetPasswordUsecase(gh<_i712.AuthRepo>()), + ); gh.factory<_i112.VerifyResetCodeUsecase>( - () => _i112.VerifyResetCodeUsecase(gh<_i712.AuthRepo>())); - gh.factoryParam<_i466.VerifyResetCodeCubit, String, dynamic>(( - email, - _, - ) => - _i466.VerifyResetCodeCubit( - gh<_i112.VerifyResetCodeUsecase>(), - gh<_i769.ForgetPasswordUsecase>(), - email, - )); - gh.factoryParam<_i378.ResetPasswordCubit, String, dynamic>(( - email, - _, - ) => - _i378.ResetPasswordCubit( - email, - gh<_i294.ResetPasswordUsecase>(), - )); + () => _i112.VerifyResetCodeUsecase(gh<_i712.AuthRepo>()), + ); + gh.factoryParam<_i466.VerifyResetCodeCubit, String, dynamic>( + (email, _) => _i466.VerifyResetCodeCubit( + gh<_i112.VerifyResetCodeUsecase>(), + gh<_i769.ForgetPasswordUsecase>(), + email, + ), + ); + gh.factory<_i156.MyOrdersCubit>( + () => _i156.MyOrdersCubit( + gh<_i335.GetOrderUseCase>(), + gh<_i603.AuthStorage>(), + ), + ); + gh.factoryParam<_i378.ResetPasswordCubit, String, dynamic>( + (email, _) => + _i378.ResetPasswordCubit(email, gh<_i294.ResetPasswordUsecase>()), + ); + gh.factory<_i499.DriverOrderRepo>( + () => _i1020.DriverOrderRepositoryImpl(gh<_i743.DriverOrderDataSource>()), + ); + gh.factory<_i863.ProfileRepo>( + () => _i1048.ProfileRepoImpl( + gh<_i943.ProfileRemoteDatasource>(), + gh<_i697.ProfileLocalDataSource>(), + ), + ); gh.lazySingleton<_i412.ApplyUseCase>( - () => _i412.ApplyUseCase(gh<_i712.AuthRepo>())); + () => _i412.ApplyUseCase(gh<_i712.AuthRepo>()), + ); gh.lazySingleton<_i1015.GetAllVehiclesUseCase>( - () => _i1015.GetAllVehiclesUseCase(gh<_i712.AuthRepo>())); + () => _i1015.GetAllVehiclesUseCase(gh<_i712.AuthRepo>()), + ); gh.factory<_i940.GetCountriesUseCase>( - () => _i940.GetCountriesUseCase(gh<_i712.AuthRepo>())); + () => _i940.GetCountriesUseCase(gh<_i712.AuthRepo>()), + ); gh.factory<_i75.LoginUseCase>( - () => _i75.LoginUseCase(gh<_i712.AuthRepo>())); + () => _i75.LoginUseCase(gh<_i712.AuthRepo>()), + ); + gh.factory<_i27.LogoutUseCase>( + () => _i27.LogoutUseCase(gh<_i712.AuthRepo>()), + ); gh.factory<_i14.ChangePasswordCubit>( - () => _i14.ChangePasswordCubit(gh<_i991.ChangePasswordUsecase>())); - gh.factory<_i614.ForgetPasswordCubit>(() => _i614.ForgetPasswordCubit( - gh<_i769.ForgetPasswordUsecase>(), - gh<_i603.AuthStorage>(), - )); - gh.factory<_i377.ApplyCubit>(() => _i377.ApplyCubit( - gh<_i940.GetCountriesUseCase>(), - gh<_i1015.GetAllVehiclesUseCase>(), - gh<_i412.ApplyUseCase>(), - )); - gh.factory<_i810.LoginCubit>(() => _i810.LoginCubit( - gh<_i75.LoginUseCase>(), - gh<_i603.AuthStorage>(), - )); + () => _i14.ChangePasswordCubit( + gh<_i991.ChangePasswordUsecase>(), + gh<_i603.AuthStorage>(), + ), + ); + gh.factory<_i614.ForgetPasswordCubit>( + () => _i614.ForgetPasswordCubit( + gh<_i769.ForgetPasswordUsecase>(), + gh<_i603.AuthStorage>(), + ), + ); + gh.factory<_i858.GetDriverOrdersUseCase>( + () => _i858.GetDriverOrdersUseCase(gh<_i499.DriverOrderRepo>()), + ); + gh.factory<_i377.ApplyCubit>( + () => _i377.ApplyCubit( + gh<_i940.GetCountriesUseCase>(), + gh<_i1015.GetAllVehiclesUseCase>(), + gh<_i412.ApplyUseCase>(), + ), + ); + gh.factory<_i221.EditProfileUseCase>( + () => _i221.EditProfileUseCase(gh<_i863.ProfileRepo>()), + ); + gh.factory<_i248.GetProfileUsecase>( + () => _i248.GetProfileUsecase(gh<_i863.ProfileRepo>()), + ); + gh.factory<_i884.UploadProfilePhotoUseCase>( + () => _i884.UploadProfilePhotoUseCase(gh<_i863.ProfileRepo>()), + ); + gh.factory<_i1023.LogoutCubit>( + () => + _i1023.LogoutCubit(gh<_i27.LogoutUseCase>(), gh<_i603.AuthStorage>()), + ); + gh.factory<_i810.LoginCubit>( + () => _i810.LoginCubit(gh<_i75.LoginUseCase>(), gh<_i603.AuthStorage>()), + ); + gh.factory<_i573.DriverOrderCubit>( + () => _i573.DriverOrderCubit( + gh<_i858.GetDriverOrdersUseCase>(), + gh<_i603.AuthStorage>(), + gh<_i329.UploadDriverFireDataUseCase>(), + gh<_i233.UploadOrderFireDataUseCase>(), + gh<_i499.DriverOrderRepo>(), + ), + ); + gh.factory<_i603.ProfileCubit>( + () => _i603.ProfileCubit( + gh<_i221.EditProfileUseCase>(), + gh<_i884.UploadProfilePhotoUseCase>(), + gh<_i248.GetProfileUsecase>(), + gh<_i603.AuthStorage>(), + ), + ); return this; } } +class _$FirebaseModule extends _i236.FirebaseModule {} + class _$NetworkModule extends _i200.NetworkModule {} diff --git a/lib/app/config/di/di.dart b/lib/app/config/di/di.dart index b2094df..70978fa 100644 --- a/lib/app/config/di/di.dart +++ b/lib/app/config/di/di.dart @@ -9,4 +9,4 @@ final getIt = GetIt.instance; preferRelativeImports: true, // default asExtension: true, // default ) -void configureDependencies() => getIt.init(); +Future configureDependencies() async => getIt.init(); diff --git a/lib/app/config/validation/app_validation.dart b/lib/app/config/validation/app_validation.dart index fe5e44f..22e35bc 100644 --- a/lib/app/config/validation/app_validation.dart +++ b/lib/app/config/validation/app_validation.dart @@ -54,6 +54,17 @@ class Validators { return null; } + static String? newPasswordValidator(String? newPass, String? currentPass) { + String? validParams = passwordValidator(newPass); + if (validParams != null) { + return validParams; + } + if (newPass == currentPass) { + return LocaleKeys.cannotBeSame.tr(); + } + return null; + } + static String? confirmPasswordValidator(String? val, String? pass) { if (val == null || val.isEmpty) { return LocaleKeys.confirmPasswordRequired.tr(); diff --git a/lib/app/core/api_manger/api_client.dart b/lib/app/core/api_manger/api_client.dart index 242585e..3952e0e 100644 --- a/lib/app/core/api_manger/api_client.dart +++ b/lib/app/core/api_manger/api_client.dart @@ -1,12 +1,10 @@ +import 'dart:io'; + import 'package:dio/dio.dart'; -import 'package:retrofit/dio.dart'; -import 'package:retrofit/error_logger.dart'; -import 'package:retrofit/http.dart'; import 'package:tracking_app/app/core/values/app_endpoint_strings.dart'; import 'package:tracking_app/features/auth/data/model/response/change_password_dto.dart'; import 'package:tracking_app/features/auth/data/model/request/LoginRequest.dart'; import 'package:tracking_app/features/auth/data/model/response/LoginResponse.dart'; -import 'package:dio/dio.dart' hide Headers; import 'package:retrofit/retrofit.dart'; import 'package:tracking_app/features/auth/data/models/request/forget_password_request.dart'; import 'package:tracking_app/features/auth/data/models/request/resetpassword_request.dart'; @@ -14,16 +12,23 @@ import 'package:tracking_app/features/auth/data/models/request/verifyreset_reque import 'package:tracking_app/features/auth/data/models/response/forgetpassword_response.dart'; import 'package:tracking_app/features/auth/data/models/response/resetpassword_response.dart'; import 'package:tracking_app/features/auth/data/models/response/verifyreset_response.dart'; +import 'package:tracking_app/features/my_orders/data/models/response/my_order_response.dart'; import '../../../features/auth/data/models/response/apply_response_model.dart'; -import '../../../features/auth/data/models/request/apply_request_model.dart'; import '../../../features/auth/data/models/response/vehicles_response_model.dart'; -import '../values/app_endpoint_strings.dart'; - +import 'package:tracking_app/app/core/values/api_constants.dart'; +import 'package:tracking_app/features/profile/data/models/requests/edit_profile_request.dart'; +import 'package:tracking_app/features/profile/data/models/responses/edit_profile_response.dart'; +import '../../../features/auth/data/models/response/logout_response_dto/logout_response_dto.dart'; +import 'package:tracking_app/features/home/data/model/response/orderRespons.dart'; part 'api_client.g.dart'; @RestApi(baseUrl: AppEndpointString.baseUrl) abstract class ApiClient { factory ApiClient(Dio dio) = _ApiClient; + @GET(AppEndpointString.logout) + Future> logout( + @Header(ApiConstants.authorization) String token, + ); @POST(AppEndpointString.sendEmail) Future> forgetPassword( @@ -38,10 +43,12 @@ abstract class ApiClient { @Body() VerifyResetRequest request, ); @PATCH(AppEndpointString.changePassword) - Future> changePassword( - @Body() Map body, - ); - @POST("drivers/signin") + Future> changePassword({ + @Header(ApiConstants.authorization) required String token, + @Body() required Map body, + }); + + @POST(AppEndpointString.login) Future login(@Body() LoginRequest request); @GET(AppEndpointString.getVehicles) @@ -50,4 +57,34 @@ abstract class ApiClient { @POST(AppEndpointString.apply) @MultiPart() Future> apply(@Body() FormData formData); + + @PUT(AppEndpointString.editProfile) + Future> editProfile({ + @Header(ApiConstants.authorization) required String token, + @Body() required EditProfileRequest request, + }); + + @MultiPart() + @PUT(AppEndpointString.uploadPhoto) + Future> uploadPhoto({ + @Header(ApiConstants.authorization) required String token, + @Part(name: ApiConstants.photo) required File photo, + }); + + @GET(AppEndpointString.getProfile) + Future> getProfile({ + @Header(ApiConstants.authorization) required String token, + }); + + @GET(AppEndpointString.mydriverOrders) + Future> getAllOrders({ + @Header("Authorization") required String token, + @Query("limit") int? limit, + @Query("page") int? page, + }); + + @GET(AppEndpointString.mydriverOrders) + Future> getPendingOrders( + @Header("Authorization") String token, + ); } diff --git a/lib/app/core/api_manger/api_client.g.dart b/lib/app/core/api_manger/api_client.g.dart index 5f8bd72..df89b76 100644 --- a/lib/app/core/api_manger/api_client.g.dart +++ b/lib/app/core/api_manger/api_client.g.dart @@ -2,17 +2,16 @@ part of 'api_client.dart'; +// dart format off + // ************************************************************************** // RetrofitGenerator // ************************************************************************** -// ignore_for_file: unnecessary_brace_in_string_interps,no_leading_underscores_for_local_identifiers +// ignore_for_file: unnecessary_brace_in_string_interps,no_leading_underscores_for_local_identifiers,unused_element,unnecessary_string_interpolations,unused_element_parameter,avoid_unused_constructor_parameters,unreachable_from_main class _ApiClient implements ApiClient { - _ApiClient( - this._dio, { - this.baseUrl, - }) { + _ApiClient(this._dio, {this.baseUrl, this.errorLogger}) { baseUrl ??= 'https://flower.elevateegy.com/api/v1/'; } @@ -20,208 +19,422 @@ class _ApiClient implements ApiClient { String? baseUrl; + final ParseErrorLogger? errorLogger; + + @override + Future> logout(String token) async { + final _extra = {}; + final queryParameters = {}; + final _headers = {r'Authorization': token}; + _headers.removeWhere((k, v) => v == null); + const Map? _data = null; + final _options = _setStreamType>( + Options(method: 'GET', headers: _headers, extra: _extra) + .compose( + _dio.options, + 'drivers/logout', + queryParameters: queryParameters, + data: _data, + ) + .copyWith(baseUrl: _combineBaseUrls(_dio.options.baseUrl, baseUrl)), + ); + final _result = await _dio.fetch>(_options); + late LogoutResponseDto _value; + try { + _value = LogoutResponseDto.fromJson(_result.data!); + } on Object catch (e, s) { + errorLogger?.logError(e, s, _options, response: _result); + rethrow; + } + final httpResponse = HttpResponse(_value, _result); + return httpResponse; + } + @override Future> forgetPassword( - ForgetPasswordRequest request) async { - const _extra = {}; + ForgetPasswordRequest request, + ) async { + final _extra = {}; final queryParameters = {}; final _headers = {}; final _data = {}; _data.addAll(request.toJson()); - final _result = await _dio.fetch>( - _setStreamType>(Options( - method: 'POST', - headers: _headers, - extra: _extra, - ) - .compose( - _dio.options, - 'drivers/forgotPassword', - queryParameters: queryParameters, - data: _data, - ) - .copyWith( - baseUrl: _combineBaseUrls( - _dio.options.baseUrl, - baseUrl, - )))); - final value = ForgetpasswordResponse.fromJson(_result.data!); - final httpResponse = HttpResponse(value, _result); + final _options = _setStreamType>( + Options(method: 'POST', headers: _headers, extra: _extra) + .compose( + _dio.options, + 'drivers/forgotPassword', + queryParameters: queryParameters, + data: _data, + ) + .copyWith(baseUrl: _combineBaseUrls(_dio.options.baseUrl, baseUrl)), + ); + final _result = await _dio.fetch>(_options); + late ForgetpasswordResponse _value; + try { + _value = ForgetpasswordResponse.fromJson(_result.data!); + } on Object catch (e, s) { + errorLogger?.logError(e, s, _options, response: _result); + rethrow; + } + final httpResponse = HttpResponse(_value, _result); return httpResponse; } @override Future> resetPassword( - ResetPasswordRequest request) async { - const _extra = {}; + ResetPasswordRequest request, + ) async { + final _extra = {}; final queryParameters = {}; final _headers = {}; final _data = {}; _data.addAll(request.toJson()); - final _result = await _dio.fetch>( - _setStreamType>(Options( - method: 'PUT', - headers: _headers, - extra: _extra, - ) - .compose( - _dio.options, - 'drivers/resetPassword', - queryParameters: queryParameters, - data: _data, - ) - .copyWith( - baseUrl: _combineBaseUrls( - _dio.options.baseUrl, - baseUrl, - )))); - final value = ResetpasswordResponse.fromJson(_result.data!); - final httpResponse = HttpResponse(value, _result); + final _options = _setStreamType>( + Options(method: 'PUT', headers: _headers, extra: _extra) + .compose( + _dio.options, + 'drivers/resetPassword', + queryParameters: queryParameters, + data: _data, + ) + .copyWith(baseUrl: _combineBaseUrls(_dio.options.baseUrl, baseUrl)), + ); + final _result = await _dio.fetch>(_options); + late ResetpasswordResponse _value; + try { + _value = ResetpasswordResponse.fromJson(_result.data!); + } on Object catch (e, s) { + errorLogger?.logError(e, s, _options, response: _result); + rethrow; + } + final httpResponse = HttpResponse(_value, _result); return httpResponse; } @override Future> verifyResetCode( - VerifyResetRequest request) async { - const _extra = {}; + VerifyResetRequest request, + ) async { + final _extra = {}; final queryParameters = {}; final _headers = {}; final _data = {}; _data.addAll(request.toJson()); - final _result = await _dio.fetch>( - _setStreamType>(Options( - method: 'POST', - headers: _headers, - extra: _extra, - ) - .compose( - _dio.options, - 'drivers/verifyResetCode', - queryParameters: queryParameters, - data: _data, - ) - .copyWith( - baseUrl: _combineBaseUrls( - _dio.options.baseUrl, - baseUrl, - )))); - final value = VerifyresetResponse.fromJson(_result.data!); - final httpResponse = HttpResponse(value, _result); + final _options = _setStreamType>( + Options(method: 'POST', headers: _headers, extra: _extra) + .compose( + _dio.options, + 'drivers/verifyResetCode', + queryParameters: queryParameters, + data: _data, + ) + .copyWith(baseUrl: _combineBaseUrls(_dio.options.baseUrl, baseUrl)), + ); + final _result = await _dio.fetch>(_options); + late VerifyresetResponse _value; + try { + _value = VerifyresetResponse.fromJson(_result.data!); + } on Object catch (e, s) { + errorLogger?.logError(e, s, _options, response: _result); + rethrow; + } + final httpResponse = HttpResponse(_value, _result); return httpResponse; } @override - Future> changePassword( - Map body) async { - const _extra = {}; + Future> changePassword({ + required String token, + required Map body, + }) async { + final _extra = {}; final queryParameters = {}; - final _headers = {}; + final _headers = {r'Authorization': token}; + _headers.removeWhere((k, v) => v == null); final _data = {}; _data.addAll(body); - final _result = await _dio.fetch>( - _setStreamType>(Options( - method: 'PATCH', - headers: _headers, - extra: _extra, - ) - .compose( - _dio.options, - 'drivers/change-password', - queryParameters: queryParameters, - data: _data, - ) - .copyWith( - baseUrl: _combineBaseUrls( - _dio.options.baseUrl, - baseUrl, - )))); - final value = ChangePasswordDto.fromJson(_result.data!); - final httpResponse = HttpResponse(value, _result); + final _options = _setStreamType>( + Options(method: 'PATCH', headers: _headers, extra: _extra) + .compose( + _dio.options, + 'drivers/change-password', + queryParameters: queryParameters, + data: _data, + ) + .copyWith(baseUrl: _combineBaseUrls(_dio.options.baseUrl, baseUrl)), + ); + final _result = await _dio.fetch>(_options); + late ChangePasswordDto _value; + try { + _value = ChangePasswordDto.fromJson(_result.data!); + } on Object catch (e, s) { + errorLogger?.logError(e, s, _options, response: _result); + rethrow; + } + final httpResponse = HttpResponse(_value, _result); return httpResponse; } @override Future login(LoginRequest request) async { - const _extra = {}; + final _extra = {}; final queryParameters = {}; final _headers = {}; final _data = {}; _data.addAll(request.toJson()); - final _result = await _dio - .fetch>(_setStreamType(Options( - method: 'POST', - headers: _headers, - extra: _extra, - ) - .compose( - _dio.options, - 'drivers/signin', - queryParameters: queryParameters, - data: _data, - ) - .copyWith( - baseUrl: _combineBaseUrls( - _dio.options.baseUrl, - baseUrl, - )))); - final value = LoginResponse.fromJson(_result.data!); - return value; + final _options = _setStreamType( + Options(method: 'POST', headers: _headers, extra: _extra) + .compose( + _dio.options, + 'drivers/signin', + queryParameters: queryParameters, + data: _data, + ) + .copyWith(baseUrl: _combineBaseUrls(_dio.options.baseUrl, baseUrl)), + ); + final _result = await _dio.fetch>(_options); + late LoginResponse _value; + try { + _value = LoginResponse.fromJson(_result.data!); + } on Object catch (e, s) { + errorLogger?.logError(e, s, _options, response: _result); + rethrow; + } + return _value; } @override Future> getAllVehicle() async { - const _extra = {}; + final _extra = {}; final queryParameters = {}; final _headers = {}; - final Map? _data = null; - final _result = await _dio.fetch>( - _setStreamType>(Options( - method: 'GET', - headers: _headers, - extra: _extra, - ) - .compose( - _dio.options, - 'vehicles', - queryParameters: queryParameters, - data: _data, - ) - .copyWith( - baseUrl: _combineBaseUrls( - _dio.options.baseUrl, - baseUrl, - )))); - final value = VehiclesResponse.fromJson(_result.data!); - final httpResponse = HttpResponse(value, _result); + const Map? _data = null; + final _options = _setStreamType>( + Options(method: 'GET', headers: _headers, extra: _extra) + .compose( + _dio.options, + 'vehicles', + queryParameters: queryParameters, + data: _data, + ) + .copyWith(baseUrl: _combineBaseUrls(_dio.options.baseUrl, baseUrl)), + ); + final _result = await _dio.fetch>(_options); + late VehiclesResponse _value; + try { + _value = VehiclesResponse.fromJson(_result.data!); + } on Object catch (e, s) { + errorLogger?.logError(e, s, _options, response: _result); + rethrow; + } + final httpResponse = HttpResponse(_value, _result); return httpResponse; } @override Future> apply(FormData formData) async { - const _extra = {}; + final _extra = {}; final queryParameters = {}; final _headers = {}; final _data = formData; - final _result = await _dio.fetch>( - _setStreamType>(Options( - method: 'POST', - headers: _headers, - extra: _extra, - contentType: 'multipart/form-data', - ) - .compose( - _dio.options, - 'drivers/apply', - queryParameters: queryParameters, - data: _data, - ) - .copyWith( - baseUrl: _combineBaseUrls( - _dio.options.baseUrl, - baseUrl, - )))); - final value = ApplyResponseModel.fromJson(_result.data!); - final httpResponse = HttpResponse(value, _result); + final _options = _setStreamType>( + Options( + method: 'POST', + headers: _headers, + extra: _extra, + contentType: 'multipart/form-data', + ) + .compose( + _dio.options, + 'drivers/apply', + queryParameters: queryParameters, + data: _data, + ) + .copyWith(baseUrl: _combineBaseUrls(_dio.options.baseUrl, baseUrl)), + ); + final _result = await _dio.fetch>(_options); + late ApplyResponseModel _value; + try { + _value = ApplyResponseModel.fromJson(_result.data!); + } on Object catch (e, s) { + errorLogger?.logError(e, s, _options, response: _result); + rethrow; + } + final httpResponse = HttpResponse(_value, _result); + return httpResponse; + } + + @override + Future> editProfile({ + required String token, + required EditProfileRequest request, + }) async { + final _extra = {}; + final queryParameters = {}; + final _headers = {r'Authorization': token}; + _headers.removeWhere((k, v) => v == null); + final _data = {}; + _data.addAll(request.toJson()); + final _options = _setStreamType>( + Options(method: 'PUT', headers: _headers, extra: _extra) + .compose( + _dio.options, + 'drivers/editProfile', + queryParameters: queryParameters, + data: _data, + ) + .copyWith(baseUrl: _combineBaseUrls(_dio.options.baseUrl, baseUrl)), + ); + final _result = await _dio.fetch>(_options); + late EditProfileResponse _value; + try { + _value = EditProfileResponse.fromJson(_result.data!); + } on Object catch (e, s) { + errorLogger?.logError(e, s, _options, response: _result); + rethrow; + } + final httpResponse = HttpResponse(_value, _result); + return httpResponse; + } + + @override + Future> uploadPhoto({ + required String token, + required File photo, + }) async { + final _extra = {}; + final queryParameters = {}; + final _headers = {r'Authorization': token}; + _headers.removeWhere((k, v) => v == null); + final _data = FormData(); + _data.files.add( + MapEntry( + 'photo', + MultipartFile.fromFileSync( + photo.path, + filename: photo.path.split(Platform.pathSeparator).last, + ), + ), + ); + final _options = _setStreamType>( + Options( + method: 'PUT', + headers: _headers, + extra: _extra, + contentType: 'multipart/form-data', + ) + .compose( + _dio.options, + 'drivers/upload-photo', + queryParameters: queryParameters, + data: _data, + ) + .copyWith(baseUrl: _combineBaseUrls(_dio.options.baseUrl, baseUrl)), + ); + final _result = await _dio.fetch>(_options); + late EditProfileResponse _value; + try { + _value = EditProfileResponse.fromJson(_result.data!); + } on Object catch (e, s) { + errorLogger?.logError(e, s, _options, response: _result); + rethrow; + } + final httpResponse = HttpResponse(_value, _result); + return httpResponse; + } + + @override + Future> getProfile({ + required String token, + }) async { + final _extra = {}; + final queryParameters = {}; + final _headers = {r'Authorization': token}; + _headers.removeWhere((k, v) => v == null); + const Map? _data = null; + final _options = _setStreamType>( + Options(method: 'GET', headers: _headers, extra: _extra) + .compose( + _dio.options, + 'drivers/profile-data', + queryParameters: queryParameters, + data: _data, + ) + .copyWith(baseUrl: _combineBaseUrls(_dio.options.baseUrl, baseUrl)), + ); + final _result = await _dio.fetch>(_options); + late EditProfileResponse _value; + try { + _value = EditProfileResponse.fromJson(_result.data!); + } on Object catch (e, s) { + errorLogger?.logError(e, s, _options, response: _result); + rethrow; + } + final httpResponse = HttpResponse(_value, _result); + return httpResponse; + } + + @override + Future> getAllOrders({ + required String token, + int? limit, + int? page, + }) async { + final _extra = {}; + final queryParameters = {r'limit': limit, r'page': page}; + queryParameters.removeWhere((k, v) => v == null); + final _headers = {r'Authorization': token}; + _headers.removeWhere((k, v) => v == null); + const Map? _data = null; + final _options = _setStreamType>( + Options(method: 'GET', headers: _headers, extra: _extra) + .compose( + _dio.options, + 'orders/pending-orders', + queryParameters: queryParameters, + data: _data, + ) + .copyWith(baseUrl: _combineBaseUrls(_dio.options.baseUrl, baseUrl)), + ); + final _result = await _dio.fetch>(_options); + late MyOrderResponse _value; + try { + _value = MyOrderResponse.fromJson(_result.data!); + } on Object catch (e, s) { + errorLogger?.logError(e, s, _options, response: _result); + rethrow; + } + final httpResponse = HttpResponse(_value, _result); + return httpResponse; + } + + @override + Future> getPendingOrders(String token) async { + final _extra = {}; + final queryParameters = {}; + final _headers = {r'Authorization': token}; + _headers.removeWhere((k, v) => v == null); + const Map? _data = null; + final _options = _setStreamType>( + Options(method: 'GET', headers: _headers, extra: _extra) + .compose( + _dio.options, + 'orders/pending-orders', + queryParameters: queryParameters, + data: _data, + ) + .copyWith(baseUrl: _combineBaseUrls(_dio.options.baseUrl, baseUrl)), + ); + final _result = await _dio.fetch>(_options); + late OrderResponse _value; + try { + _value = OrderResponse.fromJson(_result.data!); + } on Object catch (e, s) { + errorLogger?.logError(e, s, _options, response: _result); + rethrow; + } + final httpResponse = HttpResponse(_value, _result); return httpResponse; } @@ -238,10 +451,7 @@ class _ApiClient implements ApiClient { return requestOptions; } - String _combineBaseUrls( - String dioBaseUrl, - String? baseUrl, - ) { + String _combineBaseUrls(String dioBaseUrl, String? baseUrl) { if (baseUrl == null || baseUrl.trim().isEmpty) { return dioBaseUrl; } @@ -255,3 +465,5 @@ class _ApiClient implements ApiClient { return Uri.parse(dioBaseUrl).resolveUri(url).toString(); } } + +// dart format on diff --git a/lib/app/core/app_constants.dart b/lib/app/core/app_constants.dart index e185f38..06be015 100644 --- a/lib/app/core/app_constants.dart +++ b/lib/app/core/app_constants.dart @@ -37,4 +37,5 @@ class AppConstants { static const String english = 'English'; static const String arabic = 'Arabic'; static const String logoutFailed = 'Logout failed'; + static const String floweryRider = 'Flowery Rider'; } diff --git a/lib/app/core/network/firebase_module.dart b/lib/app/core/network/firebase_module.dart new file mode 100644 index 0000000..b2e5b67 --- /dev/null +++ b/lib/app/core/network/firebase_module.dart @@ -0,0 +1,12 @@ +import 'package:cloud_firestore/cloud_firestore.dart'; +import 'package:firebase_auth/firebase_auth.dart'; +import 'package:injectable/injectable.dart'; + +@module +abstract class FirebaseModule { + @lazySingleton + FirebaseFirestore get firestore => FirebaseFirestore.instance; + + @lazySingleton + FirebaseAuth get auth => FirebaseAuth.instance; +} diff --git a/lib/app/core/router/app_router.dart b/lib/app/core/router/app_router.dart index a56daf9..40837d8 100644 --- a/lib/app/core/router/app_router.dart +++ b/lib/app/core/router/app_router.dart @@ -1,10 +1,17 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:go_router/go_router.dart'; -import 'package:tracking_app/app/config/auth_storage/auth_storage.dart'; import 'package:tracking_app/app/config/di/di.dart'; import 'package:tracking_app/app/core/router/route_names.dart'; import 'package:tracking_app/features/Onboarding/presentation/pages/onboardingScreen.dart'; import 'package:tracking_app/features/app_sections/presentation/pages/app_sections.dart'; +import 'package:tracking_app/features/driver_orders_details/presentation/pages/drivers_orders_details_page.dart'; +import 'package:tracking_app/features/profile/data/models/driver_model.dart'; +import 'package:tracking_app/features/profile/presentation/pages/edit_driver_profile_page.dart'; +import 'package:tracking_app/features/profile/presentation/pages/edit_vehicle_page.dart'; +import 'package:tracking_app/features/profile/presentation/pages/profile_page.dart'; +import 'package:tracking_app/features/my_orders/domain/models/order_entity.dart'; +import 'package:tracking_app/features/my_orders/presentation/pages/my_orders_page.dart'; +import 'package:tracking_app/features/my_orders/presentation/pages/order_details_page.dart'; import 'package:tracking_app/features/auth/presentation/apply/view/apply_view.dart'; import 'package:tracking_app/features/auth/presentation/forget_pass/manager/cubit/forget_pass_cubit.dart'; import 'package:tracking_app/features/auth/presentation/forget_pass/pages/forget_pass_page.dart'; @@ -14,8 +21,8 @@ import 'package:tracking_app/features/auth/presentation/reset_password/pages/cha import 'package:tracking_app/features/auth/presentation/reset_password/pages/reset_password.dart'; import 'package:tracking_app/features/auth/presentation/verify_reset/manger/cubit/verify_reset_cubit.dart'; import 'package:tracking_app/features/auth/presentation/verify_reset/pages/verify_reset_page.dart'; -import 'package:tracking_app/features/profile/presentation/pages/profile_page.dart'; - +import 'package:tracking_app/features/track_order/presentation/manager/cubit/track_order_cubit.dart'; +import 'package:tracking_app/features/track_order/presentation/pages/track_order_page.dart'; final GoRouter appRouter = GoRouter( initialLocation: RouteNames.onboarding, @@ -24,27 +31,31 @@ final GoRouter appRouter = GoRouter( path: RouteNames.changePassword, builder: (context, state) => const ChangePasswordPage(), ), - GoRoute( path: RouteNames.onboarding, builder: (context, state) => const Onboardingscreen(), ), + GoRoute( path: RouteNames.login, builder: (context, state) => const LoginScreen(), ), + GoRoute( path: RouteNames.profile, builder: (context, state) => const ProfilePage(), ), + GoRoute( path: RouteNames.appStart, - builder: (context, state) => AppSections(), + builder: (context, state) => const AppSections(), ), + GoRoute( path: RouteNames.applyScreen, builder: (context, state) => const ApplyScreen(), ), + GoRoute( path: RouteNames.verifyResetCode, builder: (context, state) { @@ -56,6 +67,7 @@ final GoRouter appRouter = GoRouter( ); }, ), + GoRoute( path: RouteNames.forgetPassword, builder: (context, state) => BlocProvider( @@ -63,7 +75,6 @@ final GoRouter appRouter = GoRouter( child: const ForgetPasswordPage(), ), ), - GoRoute( path: RouteNames.resetPassword, builder: (context, state) => BlocProvider( @@ -71,22 +82,45 @@ final GoRouter appRouter = GoRouter( child: const ResetPasswordPage(), ), ), + GoRoute( - path: RouteNames.profile, - builder: (context, state) => const ProfilePage(), + path: RouteNames.trackOrder, + builder: (context, state) => BlocProvider( + create: (_) => getIt(), + child: const TrackOrderPage(), + ), + ), + GoRoute( + path: RouteNames.editDriverProfile, + builder: (context, state) { + final driver = state.extra as DriverModel?; + return EditDriverProfilePage(driver: driver); + }, ), - ], - redirect: (context, state) async { - final token = await getIt().getToken(); - final rememberMe = await getIt().getRememberMe(); - final bool loggingIn = - state.matchedLocation == RouteNames.login || - state.matchedLocation == RouteNames.onboarding; + GoRoute( + path: RouteNames.editVehicle, + builder: (context, state) { + final driver = state.extra as DriverModel; + return EditVehiclePage(driver: driver); + }, + ), - if (loggingIn && token != null && rememberMe) { - return RouteNames.profile; - } - return null; - }, + GoRoute( + path: RouteNames.ordersDetailsPage, + builder: (context, state) => const DriversOrdersDetailsPage(), + ), + + GoRoute( + path: RouteNames.myOrders, + builder: (context, state) => const MyOrdersPage(), + ), + GoRoute( + path: RouteNames.orderDetails, + builder: (context, state) { + final order = state.extra as OrderEntity; + return OrderDetailsPage(order: order); + }, + ), + ], ); diff --git a/lib/app/core/router/route_names.dart b/lib/app/core/router/route_names.dart index 118b723..f6319a6 100644 --- a/lib/app/core/router/route_names.dart +++ b/lib/app/core/router/route_names.dart @@ -10,4 +10,12 @@ abstract class RouteNames { static const changePassword = '/changePassword'; static const applyScreen = '/applyScreen'; static const onboarding = '/onboarding'; + static const trackOrder = '/trackOrder'; + static const editDriverProfile = "/editDriverProfile"; + static const editVehicle = "/editVehicle"; + static const getProfle = "/profile-data"; + static const ordersDetailsPage = "/ordersDetails"; + static const myOrders = "/myOrders"; + static const orderDetails = "/orderDetails"; + } diff --git a/lib/app/core/ui_helper/color/colors.dart b/lib/app/core/ui_helper/color/colors.dart index 394c8a3..bcdc243 100644 --- a/lib/app/core/ui_helper/color/colors.dart +++ b/lib/app/core/ui_helper/color/colors.dart @@ -17,4 +17,5 @@ abstract final class AppColors { static const Color white = Color(0xFFFFFFFF); static const Color purple = Color(0xFF441AB0); static const Color white70 = Color(0xFFA6A6A6); + static const Color lightPink = Color(0xFFF9ECF0); } diff --git a/lib/app/core/values/app_endpoint_strings.dart b/lib/app/core/values/app_endpoint_strings.dart index d5a4e29..eb418ac 100644 --- a/lib/app/core/values/app_endpoint_strings.dart +++ b/lib/app/core/values/app_endpoint_strings.dart @@ -4,15 +4,12 @@ class AppEndpointString { static const String sendEmail = 'drivers/forgotPassword'; static const String verifyResetCode = 'drivers/verifyResetCode'; static const String resetPassword = 'drivers/resetPassword'; + static const String changePassword = "drivers/change-password"; static const String profileData = 'auth/profile-data'; - static const String uploadPhoto = 'auth/upload-photo'; - static const String logout = 'auth/logout'; static const String updateRole = 'auth/update-role'; - static const String cashOrder = 'orders'; - static const String orders = 'orders'; - static const String checkout = '$orders/checkout'; + static const String addresses = 'addresses'; static const String signup = '/auth/signup'; static const String allCategories = 'categories'; @@ -20,10 +17,7 @@ class AppEndpointString { static const String home = '/home'; static const String productDetails = 'products/{id}'; static const String cartPage = 'cart'; - static const String changePassword = "drivers/change-password"; static const String tokenKey = 'token'; - static const String editProfile = 'auth/editProfile'; - static const String changepassword = 'auth/change-password'; static const String addAddress = 'addresses'; static const String getaddresses = 'addresses'; static const String getNotifications = "notifications/user"; @@ -31,4 +25,13 @@ class AppEndpointString { static const String deleteAllNotifications = "notifications/clear-all"; static const String getVehicles = "vehicles"; static const String apply = "drivers/apply"; + + static const String editProfile = "drivers/editProfile"; + static const String uploadPhoto = "drivers/upload-photo"; + static const String getProfile = "drivers/profile-data"; + static const String login = "drivers/signin"; + static const String logout = 'drivers/logout'; + static const String driverOrders = 'orders/driver-orders'; + + static const String mydriverOrders = 'orders/pending-orders'; } diff --git a/lib/app/core/values/paths.dart b/lib/app/core/values/paths.dart index d1ceac7..26ff989 100644 --- a/lib/app/core/values/paths.dart +++ b/lib/app/core/values/paths.dart @@ -4,4 +4,7 @@ class AppPaths { static const String aboutUs = 'about_app'; static const String terms = 'terms_and_conditions'; static const String onboardingImage = 'assets/images/Clip path group.png'; + static const String whatsappImage = 'assets/images/whatsapp.png'; + static const String flowerLogo = 'assets/images/flower_logo.png'; + static const String mediaUrl = 'https://flower.elevateegy.com/uploads/'; } diff --git a/lib/app/core/widgets/custom_app_bar.dart b/lib/app/core/widgets/custom_app_bar.dart new file mode 100644 index 0000000..6dc46e0 --- /dev/null +++ b/lib/app/core/widgets/custom_app_bar.dart @@ -0,0 +1,25 @@ +import 'package:flutter/material.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:go_router/go_router.dart'; + +class CustomAppBar extends StatelessWidget implements PreferredSizeWidget { + final String title; + final List? actions; + + const CustomAppBar({super.key, required this.title, this.actions}); + + @override + Widget build(BuildContext context) { + return AppBar( + title: Text(title.tr()), + leading: IconButton( + icon: const Icon(Icons.arrow_back_ios_new), + onPressed: () => context.pop(), + ), + actions: actions, + ); + } + + @override + Size get preferredSize => const Size.fromHeight(kToolbarHeight); +} diff --git a/lib/features/app_sections/presentation/pages/home_page_test.dart b/lib/features/app_sections/presentation/pages/home_page_test.dart index 52b8e91..3333077 100644 --- a/lib/features/app_sections/presentation/pages/home_page_test.dart +++ b/lib/features/app_sections/presentation/pages/home_page_test.dart @@ -6,6 +6,20 @@ class HomePageTest extends StatelessWidget { @override Widget build(BuildContext context) { - return Scaffold(backgroundColor: AppColors.green); + return Scaffold( + backgroundColor: AppColors.green, + body: Column( + crossAxisAlignment: CrossAxisAlignment.center, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + // ElevatedButton( + // onPressed: () { + // context.go(RouteNames.trackOrder); + // }, + // // child: const Text("Track Order"), + // ), + ], + ), + ); } } diff --git a/lib/features/app_sections/presentation/widgets/app_section_view.dart b/lib/features/app_sections/presentation/widgets/app_section_view.dart index 11f455a..0388cd3 100644 --- a/lib/features/app_sections/presentation/widgets/app_section_view.dart +++ b/lib/features/app_sections/presentation/widgets/app_section_view.dart @@ -5,8 +5,9 @@ import 'package:tracking_app/app/core/ui_helper/color/colors.dart'; import 'package:tracking_app/features/app_sections/presentation/manager/app_section_cubit.dart'; import 'package:tracking_app/features/app_sections/presentation/manager/app_section_states.dart'; import 'package:tracking_app/features/app_sections/presentation/pages/home_page_test.dart'; -import 'package:tracking_app/features/app_sections/presentation/pages/orders_page_test.dart'; -import 'package:tracking_app/features/app_sections/presentation/pages/profile_page_test.dart'; +import 'package:tracking_app/features/my_orders/presentation/pages/my_orders_page.dart'; +import 'package:tracking_app/features/home/presentation/pages/driverOrderScreen.dart'; +import 'package:tracking_app/features/profile/presentation/pages/profile_page.dart'; import 'package:tracking_app/generated/locale_keys.g.dart'; class AppSectionsView extends StatefulWidget { @@ -24,13 +25,13 @@ class _AppSectionsViewState extends State { Widget bodyWidget; switch (state.selectedIndex) { case 0: - bodyWidget = const HomePageTest(); + bodyWidget = const DriverOrderScreen(); break; case 1: - bodyWidget = const OrdersPageTest(); + bodyWidget = const MyOrdersPage(); break; case 2: - bodyWidget = const ProfilePageTest(); + bodyWidget = const ProfilePage(); break; default: bodyWidget = const HomePageTest(); diff --git a/lib/features/auth/api/datasource/auth_remote_datasource_impl.dart b/lib/features/auth/api/datasource/auth_remote_datasource_impl.dart index de20012..500ea64 100644 --- a/lib/features/auth/api/datasource/auth_remote_datasource_impl.dart +++ b/lib/features/auth/api/datasource/auth_remote_datasource_impl.dart @@ -2,7 +2,6 @@ import 'dart:convert'; import 'package:injectable/injectable.dart'; import 'package:flutter/services.dart' show rootBundle; import 'package:dio/dio.dart'; -import 'package:dio/src/form_data.dart'; import 'package:tracking_app/app/core/api_manger/api_client.dart'; import 'package:tracking_app/app/core/network/api_result.dart'; import 'package:tracking_app/app/core/network/safe_api_call.dart'; @@ -15,6 +14,7 @@ import 'package:tracking_app/features/auth/data/models/request/verifyreset_reque import 'package:tracking_app/features/auth/data/models/request/apply_request_model.dart'; import 'package:tracking_app/features/auth/data/model/response/change_password_dto.dart'; import 'package:tracking_app/features/auth/data/models/response/forgetpassword_response.dart'; +import 'package:tracking_app/features/auth/data/models/response/logout_response_dto/logout_response_dto.dart'; import 'package:tracking_app/features/auth/data/models/response/resetpassword_response.dart'; import 'package:tracking_app/features/auth/data/models/response/verifyreset_response.dart'; import 'package:tracking_app/features/auth/data/models/response/apply_response_model.dart'; @@ -27,28 +27,27 @@ class AuthRemoteDataSourceImpl implements AuthRemoteDataSource { AuthRemoteDataSourceImpl(this.apiClient); - @override Future> forgetPassword( - ForgetPasswordRequest request) { + ForgetPasswordRequest request, + ) { return safeApiCall(call: () => apiClient.forgetPassword(request)); } - @override Future> verifyResetCode( - VerifyResetRequest request) { + VerifyResetRequest request, + ) { return safeApiCall(call: () => apiClient.verifyResetCode(request)); } - @override Future> resetPassword( - ResetPasswordRequest request) { + ResetPasswordRequest request, + ) { return safeApiCall(call: () => apiClient.resetPassword(request)); } - @override Future> login(LoginRequest loginRequest) async { try { @@ -60,7 +59,8 @@ class AuthRemoteDataSourceImpl implements AuthRemoteDataSource { errorMessage = 'wrongEmailOrPassword'; } else if (e.response?.data != null) { if (e.response!.data is Map) { - errorMessage = e.response!.data['message'] ?? e.message ?? 'unknownError'; + errorMessage = + e.response!.data['message'] ?? e.message ?? 'unknownError'; } else { errorMessage = e.message ?? 'unknownError'; } @@ -75,27 +75,29 @@ class AuthRemoteDataSourceImpl implements AuthRemoteDataSource { @override Future> changePassword({ + required String token, String? password, String? newPassword, }) { return safeApiCall( - call: () => apiClient.changePassword({ - "password": password, - "newPassword": newPassword, - }), + call: () async { + return apiClient.changePassword( + token: "Bearer $token", + body: {"password": password, "newPassword": newPassword}, + ); + }, ); } - @override Future> getAllVehicle() { return safeApiCall(call: () => apiClient.getAllVehicle()); } - @override Future> apply( - ApplyRequestModel applyRequestModel) { + ApplyRequestModel applyRequestModel, + ) { return safeApiCall( call: () async { final formData = FormData.fromMap({ @@ -118,7 +120,9 @@ class AuthRemoteDataSourceImpl implements AuthRemoteDataSource { "vehicleLicense", await MultipartFile.fromFile( applyRequestModel.vehicleLicense!.path, - filename: applyRequestModel.vehicleLicense!.path.split('/').last, + filename: applyRequestModel.vehicleLicense!.path + .split('/') + .last, ), ), ); @@ -141,11 +145,17 @@ class AuthRemoteDataSourceImpl implements AuthRemoteDataSource { ); } - @override Future> getCountries() async { - final String response = await rootBundle.loadString('assets/data/country.json'); + final String response = await rootBundle.loadString( + 'assets/data/country.json', + ); final List data = json.decode(response); return data.map((json) => CountryModel.fromJson(json)).toList(); } + + @override + Future> logout(String token) { + return safeApiCall(call: () => apiClient.logout(token)); + } } diff --git a/lib/features/auth/data/datasource/auth_remote_datasource.dart b/lib/features/auth/data/datasource/auth_remote_datasource.dart index fa312d2..ce683df 100644 --- a/lib/features/auth/data/datasource/auth_remote_datasource.dart +++ b/lib/features/auth/data/datasource/auth_remote_datasource.dart @@ -1,16 +1,15 @@ -import 'package:tracking_app/app/core/network/api_result.dart'; +import 'package:tracking_app/features/auth/data/models/response/logout_response_dto/logout_response_dto.dart'; +import '../../../../app/core/network/api_result.dart'; import 'package:tracking_app/features/auth/data/models/request/forget_password_request.dart'; import 'package:tracking_app/features/auth/data/models/request/resetpassword_request.dart'; import 'package:tracking_app/features/auth/data/models/request/verifyreset_request.dart'; import 'package:tracking_app/features/auth/data/models/response/forgetpassword_response.dart'; import 'package:tracking_app/features/auth/data/models/response/resetpassword_response.dart'; import 'package:tracking_app/features/auth/data/models/response/verifyreset_response.dart'; -import '../../../../app/core/network/api_result.dart'; import '../models/response/country_model.dart'; import '../models/response/vehicles_response_model.dart'; import '../models/request/apply_request_model.dart'; import '../models/response/apply_response_model.dart'; -import 'package:tracking_app/app/core/network/api_result.dart'; import 'package:tracking_app/features/auth/data/model/request/LoginRequest.dart'; import 'package:tracking_app/features/auth/data/model/response/LoginResponse.dart'; import 'package:tracking_app/features/auth/data/model/response/change_password_dto.dart'; @@ -25,6 +24,7 @@ abstract class AuthRemoteDataSource { Future?> login(LoginRequest loginRequest); Future> changePassword({ + required String token, String? password, String? newPassword, }); @@ -37,4 +37,6 @@ abstract class AuthRemoteDataSource { Future?> resetPassword( ResetPasswordRequest request, ); + + Future> logout(String token); } diff --git a/lib/features/auth/data/models/response/logout_response_dto/logout_response_dto.dart b/lib/features/auth/data/models/response/logout_response_dto/logout_response_dto.dart new file mode 100644 index 0000000..e6b87bc --- /dev/null +++ b/lib/features/auth/data/models/response/logout_response_dto/logout_response_dto.dart @@ -0,0 +1,17 @@ +import 'package:json_annotation/json_annotation.dart'; + +part 'logout_response_dto.g.dart'; + +@JsonSerializable() +class LogoutResponseDto { + @JsonKey(name: "message") + final String? message; + @JsonKey(name: "error") + final String? error; + + LogoutResponseDto({this.message, this.error}); + + factory LogoutResponseDto.fromJson(Map json) { + return _$LogoutResponseDtoFromJson(json); + } +} diff --git a/lib/features/auth/data/models/response/logout_response_dto/logout_response_dto.g.dart b/lib/features/auth/data/models/response/logout_response_dto/logout_response_dto.g.dart new file mode 100644 index 0000000..87683ff --- /dev/null +++ b/lib/features/auth/data/models/response/logout_response_dto/logout_response_dto.g.dart @@ -0,0 +1,16 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'logout_response_dto.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +LogoutResponseDto _$LogoutResponseDtoFromJson(Map json) => + LogoutResponseDto( + message: json['message'] as String?, + error: json['error'] as String?, + ); + +Map _$LogoutResponseDtoToJson(LogoutResponseDto instance) => + {'message': instance.message, 'error': instance.error}; diff --git a/lib/features/auth/data/models/response/resetpassword_response.dart b/lib/features/auth/data/models/response/resetpassword_response.dart index 0f02da4..fc5b7d2 100644 --- a/lib/features/auth/data/models/response/resetpassword_response.dart +++ b/lib/features/auth/data/models/response/resetpassword_response.dart @@ -4,26 +4,21 @@ part 'resetpassword_response.g.dart'; @JsonSerializable() class ResetpasswordResponse { - @JsonKey(name: "message") - final String? message; - @JsonKey(name: "token") - final String? token; + @JsonKey(name: "message") + final String? message; + @JsonKey(name: "token") + final String? token; - ResetpasswordResponse({ - this.message, - this.token, - }); + ResetpasswordResponse({this.message, this.token}); - ResetpasswordResponse copyWith({ - String? message, - String? token, - }) => - ResetpasswordResponse( - message: message ?? this.message, - token: token ?? this.token, - ); + ResetpasswordResponse copyWith({String? message, String? token}) => + ResetpasswordResponse( + message: message ?? this.message, + token: token ?? this.token, + ); - factory ResetpasswordResponse.fromJson(Map json) => _$ResetpasswordResponseFromJson(json); + factory ResetpasswordResponse.fromJson(Map json) => + _$ResetpasswordResponseFromJson(json); - Map toJson() => _$ResetpasswordResponseToJson(this); + Map toJson() => _$ResetpasswordResponseToJson(this); } diff --git a/lib/features/auth/data/models/response/vehicle_model.dart b/lib/features/auth/data/models/response/vehicle_model.dart index fb1fe8e..8a731a9 100644 --- a/lib/features/auth/data/models/response/vehicle_model.dart +++ b/lib/features/auth/data/models/response/vehicle_model.dart @@ -1,5 +1,4 @@ import 'package:json_annotation/json_annotation.dart'; -import 'package:tracking_app/features/auth/data/models/response/vechicles_entity.dart'; part 'vehicle_model.g.dart'; diff --git a/lib/features/auth/data/models/response/vehicles_response_model.dart b/lib/features/auth/data/models/response/vehicles_response_model.dart index c4e26e6..d81a2d4 100644 --- a/lib/features/auth/data/models/response/vehicles_response_model.dart +++ b/lib/features/auth/data/models/response/vehicles_response_model.dart @@ -1,5 +1,4 @@ import 'package:json_annotation/json_annotation.dart'; -import 'package:tracking_app/features/auth/data/models/response/vechicles_entity.dart'; import 'package:tracking_app/features/auth/data/models/response/vehicle_model.dart'; import 'metadata_model.dart'; diff --git a/lib/features/auth/data/models/response/verifyreset_response.dart b/lib/features/auth/data/models/response/verifyreset_response.dart index 5558a51..8a61817 100644 --- a/lib/features/auth/data/models/response/verifyreset_response.dart +++ b/lib/features/auth/data/models/response/verifyreset_response.dart @@ -4,21 +4,16 @@ part 'verifyreset_response.g.dart'; @JsonSerializable() class VerifyresetResponse { - @JsonKey(name: "status") - final String? status; + @JsonKey(name: "status") + final String? status; - VerifyresetResponse({ - this.status, - }); + VerifyresetResponse({this.status}); - VerifyresetResponse copyWith({ - String? status, - }) => - VerifyresetResponse( - status: status ?? this.status, - ); + VerifyresetResponse copyWith({String? status}) => + VerifyresetResponse(status: status ?? this.status); - factory VerifyresetResponse.fromJson(Map json) => _$VerifyresetResponseFromJson(json); + factory VerifyresetResponse.fromJson(Map json) => + _$VerifyresetResponseFromJson(json); - Map toJson() => _$VerifyresetResponseToJson(this); + Map toJson() => _$VerifyresetResponseToJson(this); } diff --git a/lib/features/auth/data/repos/auth_repo_impl.dart b/lib/features/auth/data/repos/auth_repo_impl.dart index cd89fdc..071b909 100644 --- a/lib/features/auth/data/repos/auth_repo_impl.dart +++ b/lib/features/auth/data/repos/auth_repo_impl.dart @@ -6,17 +6,20 @@ import 'package:tracking_app/features/auth/data/mappers/change_password_dto_mapp import 'package:tracking_app/features/auth/data/model/request/LoginRequest.dart'; import 'package:tracking_app/features/auth/data/model/response/LoginResponse.dart'; import 'package:tracking_app/features/auth/data/model/response/change_password_dto.dart'; + import 'package:tracking_app/features/auth/data/models/request/apply_request_model.dart'; import 'package:tracking_app/features/auth/data/models/request/forget_password_request.dart'; import 'package:tracking_app/features/auth/data/models/request/resetpassword_request.dart'; +import 'package:tracking_app/features/auth/data/models/response/logout_response_dto/logout_response_dto.dart'; +import 'package:tracking_app/features/auth/data/models/response/vehicles_response_model.dart'; +import 'package:tracking_app/features/auth/domain/entities/country_entity.dart'; import 'package:tracking_app/features/auth/data/models/request/verifyreset_request.dart'; import 'package:tracking_app/features/auth/data/models/response/apply_response_model.dart'; import 'package:tracking_app/features/auth/data/models/response/forgetpassword_response.dart'; import 'package:tracking_app/features/auth/data/models/response/resetpassword_response.dart'; import 'package:tracking_app/features/auth/data/models/response/vehicle_model.dart'; -import 'package:tracking_app/features/auth/data/models/response/vehicles_response_model.dart'; import 'package:tracking_app/features/auth/data/models/response/verifyreset_response.dart'; -import 'package:tracking_app/features/auth/domain/entities/country_entity.dart'; + import 'package:tracking_app/features/auth/domain/models/change_password_model.dart'; import 'package:tracking_app/features/auth/domain/models/forgetpassword_entitiy.dart'; import 'package:tracking_app/features/auth/domain/models/resetpassword_entity.dart'; @@ -70,10 +73,10 @@ class AuthRepoImpl implements AuthRepo { return ErrorApiResult(error: 'Unexpected error'); } - @override Future> resetPassword( - ResetPasswordRequest request) async { + ResetPasswordRequest request, + ) async { final result = await authDatasource.resetPassword(request); if (result is SuccessApiResult) { @@ -92,7 +95,6 @@ class AuthRepoImpl implements AuthRepo { return ErrorApiResult(error: 'Unexpected error'); } - @override Future> login(String email, String password) async { final loginRequest = LoginRequest(email: email, password: password); @@ -111,10 +113,12 @@ class AuthRepoImpl implements AuthRepo { @override Future> changePassword({ + required String token, String? password, String? newPassword, }) async { final response = await authDatasource.changePassword( + token: token, password: password, newPassword: newPassword, ); @@ -158,10 +162,8 @@ class AuthRepoImpl implements AuthRepo { } } - @override - Future> apply( - ApplyRequestModel request) async { + Future> apply(ApplyRequestModel request) async { final result = await authDatasource.apply(request); if (result is SuccessApiResult) { @@ -174,4 +176,16 @@ class AuthRepoImpl implements AuthRepo { return ErrorApiResult(error: 'Unknown error'); } + + @override + Future> logout(String token) async { + final result = await authDatasource.logout(token); + if (result is SuccessApiResult) { + return SuccessApiResult(data: result.data); + } + if (result is ErrorApiResult) { + return ErrorApiResult(error: result.error); + } + return ErrorApiResult(error: 'Unexpected error'); + } } diff --git a/lib/features/auth/domain/models/resetpassword_entity.dart b/lib/features/auth/domain/models/resetpassword_entity.dart index f48719c..e9b1d64 100644 --- a/lib/features/auth/domain/models/resetpassword_entity.dart +++ b/lib/features/auth/domain/models/resetpassword_entity.dart @@ -2,5 +2,5 @@ class ResetPasswordEntity { final String? message; final String? token; - const ResetPasswordEntity({required this.message, this.token,}); + const ResetPasswordEntity({required this.message, this.token}); } diff --git a/lib/features/auth/domain/repos/auth_repo.dart b/lib/features/auth/domain/repos/auth_repo.dart index 2a4338c..fca23ea 100644 --- a/lib/features/auth/domain/repos/auth_repo.dart +++ b/lib/features/auth/domain/repos/auth_repo.dart @@ -3,6 +3,7 @@ import 'package:tracking_app/features/auth/data/model/response/LoginResponse.dar import 'package:tracking_app/features/auth/data/models/request/apply_request_model.dart'; import 'package:tracking_app/features/auth/data/models/request/resetpassword_request.dart'; import 'package:tracking_app/features/auth/data/models/response/apply_response_model.dart'; +import 'package:tracking_app/features/auth/data/models/response/logout_response_dto/logout_response_dto.dart'; import 'package:tracking_app/features/auth/data/models/response/vehicle_model.dart'; import 'package:tracking_app/features/auth/domain/entities/country_entity.dart'; import 'package:tracking_app/features/auth/domain/models/change_password_model.dart'; @@ -17,7 +18,7 @@ abstract class AuthRepo { ResetPasswordRequest request, ); - Future>> getAllVehicles(); + Future>> getAllVehicles(); Future>> getCountries(); Future> apply( ApplyRequestModel applyRequestModel, @@ -25,8 +26,10 @@ abstract class AuthRepo { Future> login(String email, String password); Future> changePassword({ + required String token, String? password, String? newPassword, }); -} + Future> logout(String token); +} diff --git a/lib/features/auth/domain/usecase/change_password_usecase.dart b/lib/features/auth/domain/usecase/change_password_usecase.dart index eaf7afe..65a2642 100644 --- a/lib/features/auth/domain/usecase/change_password_usecase.dart +++ b/lib/features/auth/domain/usecase/change_password_usecase.dart @@ -7,11 +7,13 @@ import 'package:tracking_app/features/auth/domain/repos/auth_repo.dart'; class ChangePasswordUsecase { AuthRepo authRepo; ChangePasswordUsecase(this.authRepo); - Future> call( + Future> call({ + required String token, String? password, String? newPassword, - ) { + }) { return authRepo.changePassword( + token: token, password: password, newPassword: newPassword, ); diff --git a/lib/features/auth/domain/usecase/logout_usecase.dart b/lib/features/auth/domain/usecase/logout_usecase.dart new file mode 100644 index 0000000..2e32b52 --- /dev/null +++ b/lib/features/auth/domain/usecase/logout_usecase.dart @@ -0,0 +1,14 @@ +import 'package:injectable/injectable.dart'; +import 'package:tracking_app/features/auth/data/models/response/logout_response_dto/logout_response_dto.dart'; + +import '../../../../app/core/network/api_result.dart'; +import '../repos/auth_repo.dart'; + +@injectable +class LogoutUseCase { + final AuthRepo _authRepo; + LogoutUseCase(this._authRepo); + Future> call(String token) async { + return await _authRepo.logout(token); + } +} diff --git a/lib/features/auth/domain/usecase/resertpassword_usecase.dart b/lib/features/auth/domain/usecase/resertpassword_usecase.dart index 38a48cf..f2b0d46 100644 --- a/lib/features/auth/domain/usecase/resertpassword_usecase.dart +++ b/lib/features/auth/domain/usecase/resertpassword_usecase.dart @@ -8,7 +8,7 @@ import 'package:tracking_app/features/auth/domain/repos/auth_repo.dart'; class ResetPasswordUsecase { AuthRepo authRepo; ResetPasswordUsecase(this.authRepo); - Future> call(ResetPasswordRequest request){ + Future> call(ResetPasswordRequest request) { return authRepo.resetPassword(request); - } + } } diff --git a/lib/features/auth/domain/usecase/verifyreaset_usecase.dart b/lib/features/auth/domain/usecase/verifyreaset_usecase.dart index 5d3864e..f7f8c2f 100644 --- a/lib/features/auth/domain/usecase/verifyreaset_usecase.dart +++ b/lib/features/auth/domain/usecase/verifyreaset_usecase.dart @@ -7,7 +7,7 @@ import 'package:tracking_app/features/auth/domain/repos/auth_repo.dart'; class VerifyResetCodeUsecase { AuthRepo authRepo; VerifyResetCodeUsecase(this.authRepo); - Future >call(String code){ + Future> call(String code) { return authRepo.verifyResetCode(code); } } diff --git a/lib/features/auth/presentation/apply/view/apply_view.dart b/lib/features/auth/presentation/apply/view/apply_view.dart index e40d385..e4c486e 100644 --- a/lib/features/auth/presentation/apply/view/apply_view.dart +++ b/lib/features/auth/presentation/apply/view/apply_view.dart @@ -138,7 +138,7 @@ class _ApplyScreenState extends State { labelText: LocaleKeys.country.tr(), border: const OutlineInputBorder(), ), - value: _selectedCountry, + initialValue: _selectedCountry, items: state.countries.map((country) { return DropdownMenuItem( value: country.isoCode, @@ -191,7 +191,7 @@ class _ApplyScreenState extends State { labelText: LocaleKeys.vehicleType.tr(), border: const OutlineInputBorder(), ), - value: _selectedVehicleType, + initialValue: _selectedVehicleType, items: state.vehicles .where((element) => element.id != null) .map( diff --git a/lib/features/auth/presentation/forget_pass/widgets/forget_pass_form.dart b/lib/features/auth/presentation/forget_pass/widgets/forget_pass_form.dart index 210ea31..71e0016 100644 --- a/lib/features/auth/presentation/forget_pass/widgets/forget_pass_form.dart +++ b/lib/features/auth/presentation/forget_pass/widgets/forget_pass_form.dart @@ -72,7 +72,6 @@ class ForgetPasswordForm extends StatelessWidget { ), const SizedBox(height: 40), CustomButton( - isEnabled: state.isFormValid, isLoading: state.resource.status == Status.loading, text: LocaleKeys.continueTxt.tr(), diff --git a/lib/features/auth/presentation/login/widgets/loginScreenBody.dart b/lib/features/auth/presentation/login/widgets/loginScreenBody.dart index c3a0f54..2e12323 100644 --- a/lib/features/auth/presentation/login/widgets/loginScreenBody.dart +++ b/lib/features/auth/presentation/login/widgets/loginScreenBody.dart @@ -62,7 +62,10 @@ class _LoginscreenbodyState extends State { ), onPressed: () => Navigator.of(context).pop(), ), - title: Text(LocaleKeys.login.tr(), style: AppStyles.black24SemiBold), + title: Text( + LocaleKeys.login.tr(), + style: AppStyles.black24SemiBold, + ), centerTitle: false, ), body: SafeArea( diff --git a/lib/features/auth/presentation/logout/manager/logout_cubit.dart b/lib/features/auth/presentation/logout/manager/logout_cubit.dart new file mode 100644 index 0000000..8599e76 --- /dev/null +++ b/lib/features/auth/presentation/logout/manager/logout_cubit.dart @@ -0,0 +1,45 @@ +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:injectable/injectable.dart'; +import 'package:tracking_app/app/config/auth_storage/auth_storage.dart'; +import 'package:tracking_app/app/config/base_state/base_state.dart'; +import 'package:tracking_app/app/core/network/api_result.dart'; +import 'package:tracking_app/features/auth/data/models/response/logout_response_dto/logout_response_dto.dart'; + +import 'package:tracking_app/features/auth/domain/usecase/logout_usecase.dart'; +import 'package:tracking_app/features/auth/presentation/logout/manager/logout_state.dart'; +import 'logout_intent.dart'; + +@injectable +class LogoutCubit extends Cubit { + final LogoutUseCase _logoutUseCase; + final AuthStorage _authStorage; + + LogoutCubit(this._logoutUseCase, this._authStorage) : super(LogoutStates()); + + void doIntent(LogoutIntent intent) { + switch (intent.runtimeType) { + case PerformLogout: + _performLogout(); + break; + } + } + + Future _performLogout() async { + emit(state.copyWith(logoutResource: Resource.loading())); + final token = await _authStorage.getToken(); + if (token == null || token.isEmpty) { + emit(state.copyWith(logoutResource: Resource.error("Token not found"))); + return; + } + final result = await _logoutUseCase.call('Bearer $token'); + switch (result) { + case SuccessApiResult(): + await _authStorage.clearAll(); + emit(state.copyWith(logoutResource: Resource.success(result.data))); + break; + case ErrorApiResult(): + emit(state.copyWith(logoutResource: Resource.error(result.error))); + break; + } + } +} diff --git a/lib/features/auth/presentation/logout/manager/logout_intent.dart b/lib/features/auth/presentation/logout/manager/logout_intent.dart new file mode 100644 index 0000000..fea8fbf --- /dev/null +++ b/lib/features/auth/presentation/logout/manager/logout_intent.dart @@ -0,0 +1,3 @@ +sealed class LogoutIntent {} + +class PerformLogout extends LogoutIntent {} diff --git a/lib/features/auth/presentation/logout/manager/logout_state.dart b/lib/features/auth/presentation/logout/manager/logout_state.dart new file mode 100644 index 0000000..e88cfd5 --- /dev/null +++ b/lib/features/auth/presentation/logout/manager/logout_state.dart @@ -0,0 +1,13 @@ +import 'package:tracking_app/app/config/base_state/base_state.dart'; +import 'package:tracking_app/features/auth/data/models/response/logout_response_dto/logout_response_dto.dart'; + +class LogoutStates { + final Resource logoutResource; + + LogoutStates({Resource? logoutResource}) + : logoutResource = logoutResource ?? Resource.initial(); + + LogoutStates copyWith({Resource? logoutResource}) { + return LogoutStates(logoutResource: logoutResource ?? this.logoutResource); + } +} diff --git a/lib/features/auth/presentation/reset_password/manager/change_password_cubit.dart b/lib/features/auth/presentation/reset_password/manager/change_password_cubit.dart index e1d692c..7a42e71 100644 --- a/lib/features/auth/presentation/reset_password/manager/change_password_cubit.dart +++ b/lib/features/auth/presentation/reset_password/manager/change_password_cubit.dart @@ -1,7 +1,7 @@ import 'package:bloc/bloc.dart'; import 'package:flutter/material.dart'; import 'package:injectable/injectable.dart'; -import 'package:tracking_app/app/config/validation/app_validation.dart'; +import 'package:tracking_app/app/config/auth_storage/auth_storage.dart'; import 'package:tracking_app/features/auth/domain/models/change_password_model.dart'; import 'package:tracking_app/features/auth/presentation/reset_password/manager/change_password_intent.dart'; import 'package:tracking_app/features/auth/presentation/reset_password/manager/change_password_states.dart'; @@ -12,8 +12,9 @@ import '../../../domain/usecase/change_password_usecase.dart'; @injectable class ChangePasswordCubit extends Cubit { final ChangePasswordUsecase _changePasswordUseCase; + final AuthStorage _authStorage; - ChangePasswordCubit(this._changePasswordUseCase) + ChangePasswordCubit(this._changePasswordUseCase, this._authStorage) : super(ChangePasswordStates()); final formKey = GlobalKey(); @@ -37,39 +38,45 @@ class ChangePasswordCubit extends Cubit { } void _formValid() { - final isValid = - (Validators.passwordValidator(currentPass) == null && - Validators.passwordValidator(newPass) == null && - Validators.confirmPasswordValidator(confirmPass, newPass) == null); - + final isValid = formKey.currentState?.validate() ?? false; emit(state.copyWith(isFormValid: isValid)); } void _currentPassword(String value) { currentPass = value; - emit(state.copyWith(currentPassword: true)); + emit(state.copyWith(currentPassword: true, data: null)); } void _newPassword(String value) { newPass = value; - emit(state.copyWith(newPassword: true)); + emit(state.copyWith(newPassword: true, data: null)); } void _confirmPassword(String value) { confirmPass = value; - emit(state.copyWith(confirmPassword: true)); + emit(state.copyWith(confirmPassword: true, data: null)); } Future _submitChangePassword() async { emit(state.copyWith(data: Resource.loading())); + final token = await _authStorage.getToken(); + + if (token == null || token.isEmpty) { + emit(state.copyWith(data: Resource.error("Token not found"))); + return; + } ApiResult response = await _changePasswordUseCase.call( - currentPass, - newPass, + token: 'Bearer $token', + password: currentPass, + newPassword: newPass, ); switch (response) { case SuccessApiResult(): + if (response.data.token != null) { + await _authStorage.clearToken(); + } emit(state.copyWith(data: Resource.success(response.data))); case ErrorApiResult(): diff --git a/lib/features/auth/presentation/reset_password/manager/reset_password_cubit.dart b/lib/features/auth/presentation/reset_password/manager/reset_password_cubit.dart index b5b7774..a800511 100644 --- a/lib/features/auth/presentation/reset_password/manager/reset_password_cubit.dart +++ b/lib/features/auth/presentation/reset_password/manager/reset_password_cubit.dart @@ -9,7 +9,6 @@ import '../../../../../app/config/base_state/base_state.dart'; import '../../../../../app/core/network/api_result.dart'; import '../../../../../app/core/utils/validators_helper.dart'; - part 'reset_password_state.dart'; @injectable diff --git a/lib/features/auth/presentation/reset_password/pages/change_password_page.dart b/lib/features/auth/presentation/reset_password/pages/change_password_page.dart index 7f6df57..050774a 100644 --- a/lib/features/auth/presentation/reset_password/pages/change_password_page.dart +++ b/lib/features/auth/presentation/reset_password/pages/change_password_page.dart @@ -38,7 +38,8 @@ class ChangePasswordPage extends StatelessWidget { child: BlocProvider( create: (context) => bloc, child: BlocConsumer( - // listenWhen: (p, c) => p.data?.status != c.data?.status, + listenWhen: (previous, current) => + previous.data?.status != current.data?.status, listener: (context, state) { if (state.data?.status == Status.success) { showAppSnackbar(context, LocaleKeys.passwordUpdated.tr()); diff --git a/lib/features/auth/presentation/reset_password/pages/reset_password.dart b/lib/features/auth/presentation/reset_password/pages/reset_password.dart index d1e0b28..6a5970f 100644 --- a/lib/features/auth/presentation/reset_password/pages/reset_password.dart +++ b/lib/features/auth/presentation/reset_password/pages/reset_password.dart @@ -30,7 +30,7 @@ class ResetPasswordPage extends StatelessWidget { listener: (context, state) { if (state.resource.status == Status.success) { showAppSnackbar(context, LocaleKeys.passwordUpdated.tr()); - context.push(RouteNames.profile); + context.push(RouteNames.login); } if (state.resource.status == Status.error) { showAppDialog( diff --git a/lib/features/auth/presentation/reset_password/widgets/change_password_form.dart b/lib/features/auth/presentation/reset_password/widgets/change_password_form.dart index 684d80d..201761e 100644 --- a/lib/features/auth/presentation/reset_password/widgets/change_password_form.dart +++ b/lib/features/auth/presentation/reset_password/widgets/change_password_form.dart @@ -71,7 +71,8 @@ class _ChangePasswordFormState extends State { obscureText: _newPassHidden, label: LocaleKeys.newPassword.tr(), hint: LocaleKeys.newPassword.tr(), - validator: (val) => Validators.passwordValidator(val), + validator: (val) => + Validators.newPasswordValidator(val, bloc.currentPass), onChanged: (value) { bloc.doIntent(NewPasswordIntent(newPass: value.toString())); bloc.doIntent(FormValidIntent()); @@ -109,7 +110,9 @@ class _ChangePasswordFormState extends State { text: LocaleKeys.update.tr(), isEnabled: state.isFormValid ?? false, isLoading: state.data?.status == Status.loading, - onPressed: () => bloc.doIntent(SubmitChangePasswordIntent()), + onPressed: () { + bloc.doIntent(SubmitChangePasswordIntent()); + }, ); }, ), diff --git a/lib/features/auth/presentation/reset_password/widgets/show_user_email.dart b/lib/features/auth/presentation/reset_password/widgets/show_user_email.dart index 832928b..2c796f9 100644 --- a/lib/features/auth/presentation/reset_password/widgets/show_user_email.dart +++ b/lib/features/auth/presentation/reset_password/widgets/show_user_email.dart @@ -4,7 +4,7 @@ Widget ShowUserEmail(BuildContext context, String email) { return Container( padding: const EdgeInsets.all(14), decoration: BoxDecoration( - color: Theme.of(context).colorScheme.surfaceVariant, + color: Theme.of(context).colorScheme.surfaceContainerHighest, borderRadius: BorderRadius.circular(12), ), child: Row( diff --git a/lib/features/auth/presentation/verify_reset/manger/cubit/verify_reset_intent.dart b/lib/features/auth/presentation/verify_reset/manger/cubit/verify_reset_intent.dart index 532fed2..4a64133 100644 --- a/lib/features/auth/presentation/verify_reset/manger/cubit/verify_reset_intent.dart +++ b/lib/features/auth/presentation/verify_reset/manger/cubit/verify_reset_intent.dart @@ -1,4 +1,5 @@ part of 'verify_reset_cubit.dart'; + sealed class VerifyResetCodeIntents { const VerifyResetCodeIntents(); } diff --git a/lib/features/auth/presentation/verify_reset/widgets/count_down_timer_widget.dart b/lib/features/auth/presentation/verify_reset/widgets/count_down_timer_widget.dart index d962750..00f2cba 100644 --- a/lib/features/auth/presentation/verify_reset/widgets/count_down_timer_widget.dart +++ b/lib/features/auth/presentation/verify_reset/widgets/count_down_timer_widget.dart @@ -1,4 +1,3 @@ - import 'dart:async'; import 'package:flutter/material.dart'; diff --git a/lib/features/driver_orders_details/api/datasource/order_details_remote_datasource_impl.dart b/lib/features/driver_orders_details/api/datasource/order_details_remote_datasource_impl.dart new file mode 100644 index 0000000..f893869 --- /dev/null +++ b/lib/features/driver_orders_details/api/datasource/order_details_remote_datasource_impl.dart @@ -0,0 +1,32 @@ +import 'package:injectable/injectable.dart'; +import 'package:tracking_app/app/core/network/api_result.dart'; +import 'package:tracking_app/features/driver_orders_details/data/datasource/order_details_remote_datasource.dart'; +import 'package:cloud_firestore/cloud_firestore.dart'; +import 'package:tracking_app/features/driver_orders_details/data/models/orders_dto.dart'; + +@Injectable(as: OrderDetailsRemoteDatasource) +class OrderDetailsRemoteDatasourceImpl implements OrderDetailsRemoteDatasource { + final FirebaseFirestore _firestore; + OrderDetailsRemoteDatasourceImpl({required FirebaseFirestore firestore}) + : _firestore = firestore; + + @override + ApiResult> getOrderStream(String orderId) { + try { + final stream = _firestore + .collection('orders') + .doc(orderId) + .snapshots() + .where((snapshot) => snapshot.exists && snapshot.data() != null) + .map((snapshot) { + return OrderDto.fromJson( + snapshot.data() as Map, + snapshot.id, + ); + }); + return SuccessApiResult>(data: stream); + } catch (e) { + return ErrorApiResult>(error: e.toString()); + } + } +} diff --git a/lib/features/driver_orders_details/data/datasource/order_details_remote_datasource.dart b/lib/features/driver_orders_details/data/datasource/order_details_remote_datasource.dart new file mode 100644 index 0000000..49bbd41 --- /dev/null +++ b/lib/features/driver_orders_details/data/datasource/order_details_remote_datasource.dart @@ -0,0 +1,6 @@ +import 'package:tracking_app/app/core/network/api_result.dart'; +import 'package:tracking_app/features/driver_orders_details/data/models/orders_dto.dart'; + +abstract class OrderDetailsRemoteDatasource { + ApiResult> getOrderStream(String orderId); +} diff --git a/lib/features/driver_orders_details/data/mapper/order_dto_mapper.dart b/lib/features/driver_orders_details/data/mapper/order_dto_mapper.dart new file mode 100644 index 0000000..ab50afe --- /dev/null +++ b/lib/features/driver_orders_details/data/mapper/order_dto_mapper.dart @@ -0,0 +1,51 @@ +import 'package:tracking_app/features/driver_orders_details/data/models/orders_dto.dart'; +import 'package:tracking_app/features/driver_orders_details/domain/models/orders_model.dart'; + +extension OrderDtoMapper on OrderDto { + OrderModel toOrderModel() { + return OrderModel( + driverId: driverId, + orderId: orderId, + userAddress: userAddress.toUserAddressModel(), + userId: userId, + orderDetails: orderDetails.toOrderDetailsModel(), + ); + } +} + +extension OrderDetailsDtoMapper on OrderDetailsDto { + OrderDetailsModel toOrderDetailsModel() { + return OrderDetailsModel( + items: items.map((i) => i.toOrderItemModel()).toList(), + status: status, + totalPrice: totalPrice, + pickupAddress: pickupAddress.toPickedAddressModel(), + orderId: orderId, + userAddress: userAddress, + ); + } +} + +extension OrderItemDtoMapper on OrderItemDto { + OrderItemModel toOrderItemModel() { + return OrderItemModel( + productId: productId, + title: title, + image: image, + quantity: quantity, + price: price, + ); + } +} + +extension PickedAddressDtoMapper on PickedAddressDto { + PickedAddressModel toPickedAddressModel() { + return PickedAddressModel(name: name, address: address); + } +} + +extension UserAddressDtoMapper on UserAddressDto { + UserAddressModel toUserAddressModel() { + return UserAddressModel(name: name, address: address, userId: userId); + } +} diff --git a/lib/features/driver_orders_details/data/models/orders_dto.dart b/lib/features/driver_orders_details/data/models/orders_dto.dart new file mode 100644 index 0000000..0b14faf --- /dev/null +++ b/lib/features/driver_orders_details/data/models/orders_dto.dart @@ -0,0 +1,154 @@ +class OrderDto { + final String orderId; + final String driverId; + final String userId; + final OrderDetailsDto orderDetails; + final UserAddressDto userAddress; + + OrderDto({ + required this.orderId, + required this.driverId, + required this.userId, + required this.orderDetails, + required this.userAddress, + }); + + factory OrderDto.fromJson(Map json, String id) { + return OrderDto( + orderId: id, + driverId: json['driver_id'] ?? '', + userId: json['user_id'] ?? '', + orderDetails: OrderDetailsDto.fromJson(json['oder_dt'] ?? {}), + userAddress: UserAddressDto.fromJson(json['userAddress'] ?? {}), + ); + } + + Map toJson() { + return { + 'driver_id': driverId, + 'user_id': userId, + 'oder_dt': (orderDetails).toJson(), + 'userAddress': (userAddress).toJson(), + }; + } +} + +class OrderDetailsDto { + final List items; + final String status; + final double totalPrice; + final PickedAddressDto pickupAddress; + final String orderId; + final String userAddress; + + OrderDetailsDto({ + required this.items, + required this.status, + required this.totalPrice, + required this.pickupAddress, + required this.orderId, + required this.userAddress, + }); + + factory OrderDetailsDto.fromJson(Map json) { + return OrderDetailsDto( + status: json['status'] ?? '', + totalPrice: (json['totalPrice'] ?? 0).toDouble(), + pickupAddress: PickedAddressDto.fromJson(json['pickupAddress'] ?? {}), + items: (json['items'] as List? ?? []) + .map((i) => OrderItemDto.fromJson(i)) + .toList(), + orderId: json['orderId'] ?? '', + userAddress: json['userAddress'] ?? '', + ); + } + + Map toJson() { + return { + 'status': status, + 'totalPrice': totalPrice, + 'pickupAddress': (pickupAddress).toJson(), + 'items': items.map((i) => (i).toJson()).toList(), + 'orderId': orderId, + 'userAddress': userAddress, + }; + } +} + +class OrderItemDto { + final String productId; + final String title; + final String image; + final int quantity; + final double price; + + OrderItemDto({ + required this.productId, + required this.title, + required this.image, + required this.quantity, + required this.price, + }); + + factory OrderItemDto.fromJson(Map json) { + return OrderItemDto( + productId: json['productId'] ?? '', + title: json['title'] ?? '', + image: json['image'] ?? '', + quantity: json['quantity'] ?? 0, + price: (json['price'] ?? 0).toDouble(), + ); + } + + Map toJson() { + return { + 'productId': productId, + 'title': title, + 'image': image, + 'quantity': quantity, + 'price': price, + }; + } +} + +class PickedAddressDto { + final String name; + final String address; + + PickedAddressDto({required this.name, required this.address}); + + factory PickedAddressDto.fromJson(Map json) { + return PickedAddressDto( + name: json['name'] ?? '', + address: json['address'] ?? '', + ); + } + + Map toJson() { + return {'name': name, 'address': address}; + } +} + +class UserAddressDto { + final String name; + final String address; + final String userId; + + UserAddressDto({ + required this.name, + required this.address, + required this.userId, + }); + + factory UserAddressDto.fromJson(Map json) { + return UserAddressDto( + name: json['name'] ?? '', + address: json['adress'] ?? '', + userId: json['user_id'] ?? '', + ); + } + + Map toJson() { + return {'name': name, 'adress': address, 'user_id': userId}; + } +} diff --git a/lib/features/driver_orders_details/data/repos/order_details_repo_impl.dart b/lib/features/driver_orders_details/data/repos/order_details_repo_impl.dart new file mode 100644 index 0000000..37251f2 --- /dev/null +++ b/lib/features/driver_orders_details/data/repos/order_details_repo_impl.dart @@ -0,0 +1,27 @@ +import 'package:injectable/injectable.dart'; +import 'package:tracking_app/app/core/network/api_result.dart'; +import 'package:tracking_app/features/driver_orders_details/data/datasource/order_details_remote_datasource.dart'; +import 'package:tracking_app/features/driver_orders_details/data/mapper/order_dto_mapper.dart'; +import 'package:tracking_app/features/driver_orders_details/data/models/orders_dto.dart'; +import 'package:tracking_app/features/driver_orders_details/domain/models/orders_model.dart'; +import 'package:tracking_app/features/driver_orders_details/domain/repos/order_details_repo.dart'; + +@Injectable(as: OrderDetailsRepo) +class OrderDetailsRepoImpl implements OrderDetailsRepo { + final OrderDetailsRemoteDatasource _remoteDataSource; + OrderDetailsRepoImpl(this._remoteDataSource); + + @override + ApiResult> getOrderDetails(String orderId) { + final result = _remoteDataSource.getOrderStream(orderId); + + switch (result) { + case SuccessApiResult>(): + return SuccessApiResult>( + data: result.data.map((dto) => dto.toOrderModel()), + ); + case ErrorApiResult>(): + return ErrorApiResult>(error: result.error); + } + } +} diff --git a/lib/features/driver_orders_details/domain/models/orders_model.dart b/lib/features/driver_orders_details/domain/models/orders_model.dart new file mode 100644 index 0000000..9e96435 --- /dev/null +++ b/lib/features/driver_orders_details/domain/models/orders_model.dart @@ -0,0 +1,68 @@ +class OrderModel { + final String orderId; + final String driverId; + final String userId; + final OrderDetailsModel orderDetails; + final UserAddressModel userAddress; + + OrderModel({ + required this.orderId, + required this.driverId, + required this.userId, + required this.orderDetails, + required this.userAddress, + }); +} + +class OrderDetailsModel { + final List items; + final String status; + final double totalPrice; + final PickedAddressModel pickupAddress; + final String orderId; + final String userAddress; + + OrderDetailsModel({ + required this.items, + required this.status, + required this.totalPrice, + required this.pickupAddress, + required this.orderId, + required this.userAddress, + }); +} + +class OrderItemModel { + final String productId; + final String title; + final String image; + final int quantity; + final double price; + + OrderItemModel({ + required this.productId, + required this.title, + required this.image, + required this.quantity, + required this.price, + }); +} + +class PickedAddressModel { + final String name; + final String address; + + PickedAddressModel({required this.name, required this.address}); +} + +class UserAddressModel { + final String userId; + final String name; + final String address; + + UserAddressModel({ + required this.name, + required this.address, + required this.userId, + }); +} diff --git a/lib/features/driver_orders_details/domain/repos/order_details_repo.dart b/lib/features/driver_orders_details/domain/repos/order_details_repo.dart new file mode 100644 index 0000000..942beaa --- /dev/null +++ b/lib/features/driver_orders_details/domain/repos/order_details_repo.dart @@ -0,0 +1,6 @@ +import 'package:tracking_app/app/core/network/api_result.dart'; +import 'package:tracking_app/features/driver_orders_details/domain/models/orders_model.dart'; + +abstract class OrderDetailsRepo { + ApiResult> getOrderDetails(String orderId); +} diff --git a/lib/features/driver_orders_details/domain/usecases/get_order_details_usecase.dart b/lib/features/driver_orders_details/domain/usecases/get_order_details_usecase.dart new file mode 100644 index 0000000..ad25468 --- /dev/null +++ b/lib/features/driver_orders_details/domain/usecases/get_order_details_usecase.dart @@ -0,0 +1,13 @@ +import 'package:injectable/injectable.dart'; +import 'package:tracking_app/app/core/network/api_result.dart'; +import 'package:tracking_app/features/driver_orders_details/domain/models/orders_model.dart'; +import 'package:tracking_app/features/driver_orders_details/domain/repos/order_details_repo.dart'; + +@injectable +class GetOrderDetailsUsecase { + final OrderDetailsRepo _repo; + GetOrderDetailsUsecase({required OrderDetailsRepo repo}) : _repo = repo; + + ApiResult> call(String orderId) => + _repo.getOrderDetails(orderId); +} diff --git a/lib/features/driver_orders_details/presentation/manager/order_details_cubit.dart b/lib/features/driver_orders_details/presentation/manager/order_details_cubit.dart new file mode 100644 index 0000000..224458f --- /dev/null +++ b/lib/features/driver_orders_details/presentation/manager/order_details_cubit.dart @@ -0,0 +1,58 @@ +import 'dart:async'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:injectable/injectable.dart'; +import 'package:tracking_app/app/config/auth_storage/auth_storage.dart'; +import 'package:tracking_app/app/config/base_state/base_state.dart'; +import 'package:tracking_app/app/config/di/di.dart'; +import 'package:tracking_app/app/core/network/api_result.dart'; +import 'package:tracking_app/features/driver_orders_details/domain/models/orders_model.dart'; +import '../../domain/usecases/get_order_details_usecase.dart'; +import 'order_details_states.dart'; + +@injectable +class OrderDetailsCubit extends Cubit { + final GetOrderDetailsUsecase _getOrderDetailsUsecase; + StreamSubscription? _subscription; + final _authStorage = getIt(); + + OrderDetailsCubit(this._getOrderDetailsUsecase) : super(OrderDetailsStates()); + + void getOrderDetails() async { + emit(state.copyWith(data: Resource.loading())); + _subscription?.cancel(); + + try { + final orderId = await _authStorage.getOrderId(); + if (orderId == null || orderId.isEmpty) { + emit(state.copyWith(data: Resource.error('Order ID not found'))); + return; + } + final result = _getOrderDetailsUsecase.call(orderId); + + if (result is SuccessApiResult>) { + _subscription = result.data.listen( + (order) { + emit(state.copyWith(data: Resource.success(order))); + }, + onError: (error) { + emit(state.copyWith(data: Resource.error(error.toString()))); + }, + ); + } else if (result is ErrorApiResult>) { + emit(state.copyWith(data: Resource.error(result.error))); + } + } catch (e) { + emit( + state.copyWith( + data: Resource.error("Error retrieving order details: $e"), + ), + ); + } + } + + @override + Future close() { + _subscription?.cancel(); + return super.close(); + } +} diff --git a/lib/features/driver_orders_details/presentation/manager/order_details_states.dart b/lib/features/driver_orders_details/presentation/manager/order_details_states.dart new file mode 100644 index 0000000..267a1ca --- /dev/null +++ b/lib/features/driver_orders_details/presentation/manager/order_details_states.dart @@ -0,0 +1,11 @@ +import 'package:tracking_app/app/config/base_state/base_state.dart'; +import 'package:tracking_app/features/driver_orders_details/domain/models/orders_model.dart'; + +class OrderDetailsStates { + final Resource? data; + const OrderDetailsStates({this.data}); + + OrderDetailsStates copyWith({Resource? data}) { + return OrderDetailsStates(data: data ?? this.data); + } +} diff --git a/lib/features/driver_orders_details/presentation/pages/drivers_orders_details_page.dart b/lib/features/driver_orders_details/presentation/pages/drivers_orders_details_page.dart new file mode 100644 index 0000000..aa8ba57 --- /dev/null +++ b/lib/features/driver_orders_details/presentation/pages/drivers_orders_details_page.dart @@ -0,0 +1,165 @@ +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:go_router/go_router.dart'; +import 'package:tracking_app/app/config/base_state/base_state.dart'; +import 'package:tracking_app/app/config/di/di.dart'; +import 'package:tracking_app/app/core/ui_helper/color/colors.dart'; +import 'package:tracking_app/app/core/values/paths.dart'; +import 'package:tracking_app/app/core/widgets/custom_button.dart'; +import 'package:tracking_app/features/driver_orders_details/presentation/manager/order_details_cubit.dart'; +import 'package:tracking_app/features/driver_orders_details/presentation/manager/order_details_states.dart'; +import 'package:tracking_app/features/driver_orders_details/presentation/widgets/address_card.dart'; +import 'package:tracking_app/features/driver_orders_details/presentation/widgets/bottom_row_section.dart'; +import 'package:tracking_app/features/driver_orders_details/presentation/widgets/order_item.dart'; +import 'package:tracking_app/features/driver_orders_details/presentation/widgets/order_status.dart'; +import 'package:tracking_app/features/driver_orders_details/presentation/widgets/section_title.dart'; +import 'package:tracking_app/generated/locale_keys.g.dart'; + +class DriversOrdersDetailsPage extends StatelessWidget { + const DriversOrdersDetailsPage({super.key}); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + leading: IconButton( + icon: const Icon(Icons.arrow_back_ios, color: AppColors.blackColor), + onPressed: () => context.pop(), + ), + title: Text( + LocaleKeys.orderDetails.tr(), + style: Theme.of(context).textTheme.bodyMedium!.copyWith( + fontSize: 20, + color: AppColors.blackColor, + ), + ), + ), + body: BlocProvider( + create: (context) => getIt()..getOrderDetails(), + child: BlocBuilder( + builder: (context, state) { + if (state.data?.status == Status.loading) { + return const Center(child: CircularProgressIndicator()); + } else if (state.data?.status == Status.error) { + return Center(child: Text(state.data!.error.toString())); + } else if (state.data?.status == Status.success) { + final order = state.data!.data; + final status = OrderStatus.fromString(order?.orderDetails.status); + + int currentStep = status.step; + return SingleChildScrollView( + padding: const EdgeInsets.all(16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: List.generate(5, (index) { + return Expanded( + child: Container( + height: 4, + margin: const EdgeInsets.symmetric(horizontal: 2), + decoration: BoxDecoration( + color: index < currentStep + ? AppColors.green + : AppColors.lightGrey, + borderRadius: BorderRadius.circular(2), + ), + ), + ); + }), + ), + const SizedBox(height: 20), + + Container( + width: double.infinity, + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: AppColors.lightPink, + borderRadius: BorderRadius.circular(12), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + '${LocaleKeys.status.tr()}${order?.orderDetails.status}', + style: TextStyle( + color: AppColors.green, + fontWeight: FontWeight.bold, + fontSize: 16, + ), + ), + const SizedBox(height: 4), + Text( + '${LocaleKeys.orderId.tr()}${order?.orderId}', + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: 16, + ), + ), + const SizedBox(height: 4), + Text( + 'Wed, 03 Sep 2024, 11:00 AM', + style: TextStyle( + color: AppColors.grey, + fontSize: 14, + ), + ), + ], + ), + ), + const SizedBox(height: 24), + + SectionTitle(title: LocaleKeys.pickupAddress.tr()), + AddressCard( + title: order?.orderDetails.pickupAddress.name ?? '', + address: order?.orderDetails.pickupAddress.address ?? '', + imagePath: AppPaths.flowerLogo, + ), + const SizedBox(height: 16), + SectionTitle(title: LocaleKeys.userAddress.tr()), + + AddressCard( + title: order?.userAddress.name ?? '', + address: order?.userAddress.address ?? '', + imagePath: AppPaths.flowerLogo, + ), + const SizedBox(height: 24), + + SectionTitle(title: LocaleKeys.orderDetails.tr()), + OrderItems(), + const SizedBox(height: 16), + + BottomRowSection( + label: LocaleKeys.total.tr(), + value: + '${LocaleKeys.egp.tr()} ${order?.orderDetails.totalPrice.toStringAsFixed(2)}', + ), + BottomRowSection( + label: LocaleKeys.payment_method.tr(), + value: LocaleKeys.cash_on_delivery.tr(), + ), + + const SizedBox(height: 32), + + SizedBox( + width: double.infinity, + height: 55, + child: CustomButton( + isEnabled: true, + onPressed: () {}, + isLoading: false, + text: status.buttonTextKey.tr(), + ), + ), + ], + ), + ); + } + return const SizedBox.shrink(); + }, + ), + ), + ); + } +} diff --git a/lib/features/driver_orders_details/presentation/widgets/address_card.dart b/lib/features/driver_orders_details/presentation/widgets/address_card.dart new file mode 100644 index 0000000..6b211c2 --- /dev/null +++ b/lib/features/driver_orders_details/presentation/widgets/address_card.dart @@ -0,0 +1,78 @@ +import 'package:flutter/material.dart'; +import 'package:tracking_app/app/core/ui_helper/color/colors.dart'; +import 'package:tracking_app/app/core/values/paths.dart'; + +class AddressCard extends StatelessWidget { + final String title; + final String address; + final String imagePath; + + const AddressCard({ + super.key, + required this.title, + required this.address, + required this.imagePath, + }); + + @override + Widget build(BuildContext context) { + return Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + border: Border.all(color: AppColors.lightGrey), + borderRadius: BorderRadius.circular(12), + ), + child: Row( + children: [ + CircleAvatar(backgroundImage: AssetImage(imagePath), radius: 25), + const SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + title, + style: Theme.of( + context, + ).textTheme.labelSmall!.copyWith(fontWeight: FontWeight.w400), + ), + Row( + children: [ + Icon( + Icons.location_on_outlined, + size: 16, + color: AppColors.blackColor, + ), + Flexible( + child: Text( + address, + overflow: TextOverflow.ellipsis, + maxLines: 1, + style: Theme.of(context).textTheme.labelSmall!.copyWith( + fontWeight: FontWeight.w400, + color: AppColors.blackColor, + ), + ), + ), + ], + ), + ], + ), + ), + IconButton( + onPressed: () {}, + icon: Icon(Icons.phone_outlined, color: AppColors.pink, size: 20), + ), + + IconButton( + onPressed: () {}, + icon: ImageIcon( + AssetImage(AppPaths.whatsappImage), + color: AppColors.pink, + ), + ), + ], + ), + ); + } +} diff --git a/lib/features/driver_orders_details/presentation/widgets/bottom_row_section.dart b/lib/features/driver_orders_details/presentation/widgets/bottom_row_section.dart new file mode 100644 index 0000000..481983d --- /dev/null +++ b/lib/features/driver_orders_details/presentation/widgets/bottom_row_section.dart @@ -0,0 +1,41 @@ +import 'package:flutter/material.dart'; +import 'package:tracking_app/app/core/ui_helper/color/colors.dart'; + +class BottomRowSection extends StatelessWidget { + final String label; + final String value; + const BottomRowSection({super.key, required this.label, required this.value}); + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.symmetric(vertical: 8), + child: Container( + margin: const EdgeInsets.only(bottom: 8), + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + border: Border.all(color: AppColors.lightGrey), + borderRadius: BorderRadius.circular(12), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + label, + style: Theme.of(context).textTheme.labelMedium!.copyWith( + fontWeight: FontWeight.w500, + fontSize: 16, + ), + ), + Text( + value, + style: Theme.of( + context, + ).textTheme.labelSmall!.copyWith(fontWeight: FontWeight.w500), + ), + ], + ), + ), + ); + } +} diff --git a/lib/features/driver_orders_details/presentation/widgets/order_item.dart b/lib/features/driver_orders_details/presentation/widgets/order_item.dart new file mode 100644 index 0000000..1d6cebc --- /dev/null +++ b/lib/features/driver_orders_details/presentation/widgets/order_item.dart @@ -0,0 +1,85 @@ +import 'package:cached_network_image/cached_network_image.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:shimmer/shimmer.dart'; +import 'package:tracking_app/app/core/ui_helper/color/colors.dart'; +import 'package:tracking_app/app/core/values/paths.dart'; +import 'package:tracking_app/features/driver_orders_details/presentation/manager/order_details_cubit.dart'; + +class OrderItems extends StatelessWidget { + const OrderItems({super.key}); + + @override + Widget build(BuildContext context) { + final order = BlocProvider.of(context).state.data!.data; + return ListView.builder( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + itemCount: order?.orderDetails.items.length, + itemBuilder: (context, index) { + return Container( + margin: const EdgeInsets.only(bottom: 8), + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + border: Border.all(color: AppColors.lightGrey), + borderRadius: BorderRadius.circular(12), + ), + child: Row( + children: [ + ClipRRect( + borderRadius: BorderRadius.circular(50), + child: CachedNetworkImage( + imageUrl: + "${AppPaths.mediaUrl}${order!.orderDetails.items[index].image}", + placeholder: (context, url) => Shimmer( + gradient: LinearGradient( + colors: [ + AppColors.lightGrey, + AppColors.white, + AppColors.lightGrey, + ], + ), + child: const CircularProgressIndicator(), + ), + errorWidget: (context, url, error) => Icon(Icons.error), + width: 55, + height: 55, + fit: BoxFit.cover, + ), + ), + const SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + order.orderDetails.items[index].title, + overflow: TextOverflow.ellipsis, + style: Theme.of(context).textTheme.labelSmall!.copyWith( + fontWeight: FontWeight.w400, + ), + ), + Text( + 'EGP ${order.orderDetails.items[index].price.toStringAsFixed(2)}', + style: Theme.of(context).textTheme.labelSmall!.copyWith( + fontWeight: FontWeight.w500, + color: AppColors.blackColor, + ), + ), + ], + ), + ), + Text( + 'X${order.orderDetails.items[index].quantity}', + style: Theme.of(context).textTheme.labelSmall!.copyWith( + fontWeight: FontWeight.w500, + color: AppColors.pink, + ), + ), + ], + ), + ); + }, + ); + } +} diff --git a/lib/features/driver_orders_details/presentation/widgets/order_status.dart b/lib/features/driver_orders_details/presentation/widgets/order_status.dart new file mode 100644 index 0000000..4c88788 --- /dev/null +++ b/lib/features/driver_orders_details/presentation/widgets/order_status.dart @@ -0,0 +1,83 @@ +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/foundation.dart'; +import 'package:tracking_app/generated/locale_keys.g.dart'; + +enum OrderStatus { + accepted, + pickup, + outForDelivery, + arrived, + delivered, + unknown; + + static OrderStatus fromString(String? status) { + switch (status?.toLowerCase()) { + case 'accepted': + return OrderStatus.accepted; + case 'pickup': + return OrderStatus.pickup; + case 'out_for_delivery': + return OrderStatus.outForDelivery; + case 'arrived': + return OrderStatus.arrived; + case 'delivered': + return OrderStatus.delivered; + default: + debugPrint('Unknown order status: $status'); + return OrderStatus.unknown; + } + } +} + +extension OrderStatusX on OrderStatus { + int get step { + switch (this) { + case OrderStatus.accepted: + return 1; + case OrderStatus.pickup: + return 2; + case OrderStatus.outForDelivery: + return 3; + case OrderStatus.arrived: + return 4; + case OrderStatus.delivered: + return 5; + case OrderStatus.unknown: + return 1; + } + } + + String get buttonTextKey { + switch (this) { + case OrderStatus.accepted: + return LocaleKeys.arrivedAtPickupPoint.tr(); + case OrderStatus.pickup: + return LocaleKeys.startDelivery.tr(); + case OrderStatus.outForDelivery: + return LocaleKeys.arriverAtDestination.tr(); + case OrderStatus.arrived: + return LocaleKeys.confirmDelivery.tr(); + case OrderStatus.delivered: + return LocaleKeys.orderCompleted.tr(); + case OrderStatus.unknown: + return LocaleKeys.arrivedAtPickupPoint; + } + } + + String get statusTextKey { + switch (this) { + case OrderStatus.accepted: + return LocaleKeys.accepted.tr(); + case OrderStatus.pickup: + return LocaleKeys.pickedUp.tr(); + case OrderStatus.outForDelivery: + return LocaleKeys.outForDelivery.tr(); + case OrderStatus.arrived: + return LocaleKeys.arrived.tr(); + case OrderStatus.delivered: + return LocaleKeys.delivered.tr(); + case OrderStatus.unknown: + return ''; + } + } +} diff --git a/lib/features/driver_orders_details/presentation/widgets/section_title.dart b/lib/features/driver_orders_details/presentation/widgets/section_title.dart new file mode 100644 index 0000000..8055f29 --- /dev/null +++ b/lib/features/driver_orders_details/presentation/widgets/section_title.dart @@ -0,0 +1,20 @@ +import 'package:flutter/material.dart'; +import 'package:tracking_app/app/core/ui_helper/color/colors.dart'; + +class SectionTitle extends StatelessWidget { + final String title; + const SectionTitle({super.key, required this.title}); + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.only(bottom: 12), + child: Text( + title, + style: Theme.of( + context, + ).textTheme.bodyMedium!.copyWith(color: AppColors.blackColor), + ), + ); + } +} diff --git a/lib/features/home/api/driverOrderDataS_imp.dart b/lib/features/home/api/driverOrderDataS_imp.dart new file mode 100644 index 0000000..58a9510 --- /dev/null +++ b/lib/features/home/api/driverOrderDataS_imp.dart @@ -0,0 +1,24 @@ +import 'package:injectable/injectable.dart'; +import 'package:tracking_app/app/core/api_manger/api_client.dart'; +import 'package:tracking_app/app/core/network/api_result.dart'; +import 'package:tracking_app/app/core/network/safe_api_call.dart'; +import 'package:tracking_app/features/home/data/datascourse/driverOrderDatascource.dart'; +import 'package:tracking_app/features/home/data/model/response/orderRespons.dart'; +import 'package:tracking_app/features/profile/data/models/responses/edit_profile_response.dart'; + +@Injectable(as: DriverOrderDataSource) +class DriverOrderDataSourceImpl implements DriverOrderDataSource { + final ApiClient _apiClient; + + DriverOrderDataSourceImpl(this._apiClient); + + @override + Future> getPendingOrders(String token) { + return safeApiCall(call: () => _apiClient.getPendingOrders(token)); + } + + @override + Future> getProfile(String token) { + return safeApiCall(call: () => _apiClient.getProfile(token: token)); + } +} diff --git a/lib/features/home/data/datascourse/driverOrderDatascource.dart b/lib/features/home/data/datascourse/driverOrderDatascource.dart new file mode 100644 index 0000000..b0c7709 --- /dev/null +++ b/lib/features/home/data/datascourse/driverOrderDatascource.dart @@ -0,0 +1,8 @@ +import 'package:tracking_app/app/core/network/api_result.dart'; +import 'package:tracking_app/features/home/data/model/response/orderRespons.dart'; +import 'package:tracking_app/features/profile/data/models/responses/edit_profile_response.dart'; + +abstract class DriverOrderDataSource { + Future> getPendingOrders(String token); + Future> getProfile(String token); +} diff --git a/lib/features/home/data/model/response/orderRespons.dart b/lib/features/home/data/model/response/orderRespons.dart new file mode 100644 index 0000000..0b96f51 --- /dev/null +++ b/lib/features/home/data/model/response/orderRespons.dart @@ -0,0 +1,277 @@ +import 'package:json_annotation/json_annotation.dart'; + +part 'orderRespons.g.dart'; + +@JsonSerializable() +class OrderResponse { + @JsonKey(name: "message") + final String? message; + @JsonKey(name: "metadata") + final Metadata? metadata; + @JsonKey(name: "orders") + final List? orders; + + OrderResponse({this.message, this.metadata, this.orders}); + + factory OrderResponse.fromJson(Map json) => + _$OrderResponseFromJson(json); + + Map toJson() => _$OrderResponseToJson(this); + + OrderResponse copyWith({ + String? message, + Metadata? metadata, + List? orders, + }) { + return OrderResponse( + message: message ?? this.message, + metadata: metadata ?? this.metadata, + orders: orders ?? this.orders, + ); + } +} + +@JsonSerializable() +class Metadata { + @JsonKey(name: "currentPage") + final int? currentPage; + @JsonKey(name: "totalPages") + final int? totalPages; + @JsonKey(name: "totalItems") + final int? totalItems; + @JsonKey(name: "limit") + final int? limit; + + Metadata({this.currentPage, this.totalPages, this.totalItems, this.limit}); + + factory Metadata.fromJson(Map json) => + _$MetadataFromJson(json); + + Map toJson() => _$MetadataToJson(this); +} + +@JsonSerializable() +class Order { + @JsonKey(name: "_id") + final String? id; + @JsonKey(name: "user") + final User? user; + @JsonKey(name: "orderItems") + final List? orderItems; + @JsonKey(name: "totalPrice") + final int? totalPrice; + @JsonKey(name: "paymentType") + final String? paymentType; + @JsonKey(name: "isPaid") + final bool? isPaid; + @JsonKey(name: "isDelivered") + final bool? isDelivered; + @JsonKey(name: "state") + final String? state; + @JsonKey(name: "createdAt") + final DateTime? createdAt; + @JsonKey(name: "updatedAt") + final DateTime? updatedAt; + @JsonKey(name: "orderNumber") + final String? orderNumber; + @JsonKey(name: "__v") + final int? v; + @JsonKey(name: "store") + final Store? store; + @JsonKey(name: "shippingAddress") + final ShippingAddress? shippingAddress; + @JsonKey(name: "paidAt") + final DateTime? paidAt; + + Order({ + this.id, + this.user, + this.orderItems, + this.totalPrice, + this.paymentType, + this.isPaid, + this.isDelivered, + this.state, + this.createdAt, + this.updatedAt, + this.orderNumber, + this.v, + this.store, + this.shippingAddress, + this.paidAt, + }); + + factory Order.fromJson(Map json) => _$OrderFromJson(json); + + Map toJson() => _$OrderToJson(this); +} + +@JsonSerializable() +class OrderItem { + @JsonKey(name: "product") + final Product? product; + @JsonKey(name: "price") + final int? price; + @JsonKey(name: "quantity") + final int? quantity; + @JsonKey(name: "_id") + final String? id; + + OrderItem({this.product, this.price, this.quantity, this.id}); + + factory OrderItem.fromJson(Map json) => + _$OrderItemFromJson(json); + + Map toJson() => _$OrderItemToJson(this); +} + +@JsonSerializable() +class Product { + @JsonKey(name: "_id") + final String? id; + @JsonKey(name: "title") + final String? title; + @JsonKey(name: "slug") + final String? slug; + @JsonKey(name: "description") + final String? description; + @JsonKey(name: "imgCover") + final String? imgCover; + @JsonKey(name: "images") + final List? images; + @JsonKey(name: "price") + final int? price; + @JsonKey(name: "priceAfterDiscount") + final int? priceAfterDiscount; + @JsonKey(name: "quantity") + final int? quantity; + @JsonKey(name: "category") + final String? category; + @JsonKey(name: "occasion") + final String? occasion; + @JsonKey(name: "createdAt") + final DateTime? createdAt; + @JsonKey(name: "updatedAt") + final DateTime? updatedAt; + @JsonKey(name: "__v") + final int? v; + @JsonKey(name: "sold") + final int? sold; + @JsonKey(name: "isSuperAdmin") + final bool? isSuperAdmin; + @JsonKey(name: "rateAvg") + final int? rateAvg; + @JsonKey(name: "rateCount") + final int? rateCount; + + Product({ + this.id, + this.title, + this.slug, + this.description, + this.imgCover, + this.images, + this.price, + this.priceAfterDiscount, + this.quantity, + this.category, + this.occasion, + this.createdAt, + this.updatedAt, + this.v, + this.sold, + this.isSuperAdmin, + this.rateAvg, + this.rateCount, + }); + + factory Product.fromJson(Map json) => + _$ProductFromJson(json); + + Map toJson() => _$ProductToJson(this); +} + +@JsonSerializable() +class ShippingAddress { + @JsonKey(name: "street") + final String? street; + @JsonKey(name: "city") + final String? city; + @JsonKey(name: "phone") + final String? phone; + @JsonKey(name: "lat") + final String? lat; + @JsonKey(name: "long") + final String? long; + + ShippingAddress({this.street, this.city, this.phone, this.lat, this.long}); + + factory ShippingAddress.fromJson(Map json) => + _$ShippingAddressFromJson(json); + + Map toJson() => _$ShippingAddressToJson(this); +} + +@JsonSerializable() +class Store { + @JsonKey(name: "name") + final String? name; + @JsonKey(name: "image") + final String? image; + @JsonKey(name: "address") + final String? address; + @JsonKey(name: "phoneNumber") + final String? phoneNumber; + @JsonKey(name: "latLong") + final String? latLong; + + Store({this.name, this.image, this.address, this.phoneNumber, this.latLong}); + + factory Store.fromJson(Map json) => _$StoreFromJson(json); + + Map toJson() => _$StoreToJson(this); +} + +@JsonSerializable() +class User { + @JsonKey(name: "_id") + final String? id; + @JsonKey(name: "firstName") + final String? firstName; + @JsonKey(name: "lastName") + final String? lastName; + @JsonKey(name: "email") + final String? email; + @JsonKey(name: "gender") + final String? gender; + @JsonKey(name: "phone") + final String? phone; + @JsonKey(name: "photo") + final String? photo; + @JsonKey(name: "passwordChangedAt") + final DateTime? passwordChangedAt; + @JsonKey(name: "passwordResetCode") + final String? passwordResetCode; + @JsonKey(name: "passwordResetExpires") + final DateTime? passwordResetExpires; + @JsonKey(name: "resetCodeVerified") + final bool? resetCodeVerified; + + User({ + this.id, + this.firstName, + this.lastName, + this.email, + this.gender, + this.phone, + this.photo, + this.passwordChangedAt, + this.passwordResetCode, + this.passwordResetExpires, + this.resetCodeVerified, + }); + + factory User.fromJson(Map json) => _$UserFromJson(json); + + Map toJson() => _$UserToJson(this); +} diff --git a/lib/features/home/data/repo/driverOrderRepo_impl.dart b/lib/features/home/data/repo/driverOrderRepo_impl.dart new file mode 100644 index 0000000..51cad99 --- /dev/null +++ b/lib/features/home/data/repo/driverOrderRepo_impl.dart @@ -0,0 +1,23 @@ +import 'package:injectable/injectable.dart'; +import 'package:tracking_app/app/core/network/api_result.dart'; +import 'package:tracking_app/features/home/data/datascourse/driverOrderDatascource.dart'; +import 'package:tracking_app/features/home/data/model/response/orderRespons.dart'; +import 'package:tracking_app/features/profile/data/models/responses/edit_profile_response.dart'; +import 'package:tracking_app/features/home/domain/repo/driverOrderRepo.dart'; + +@Injectable(as: DriverOrderRepo) +class DriverOrderRepositoryImpl implements DriverOrderRepo { + final DriverOrderDataSource _dataSource; + + DriverOrderRepositoryImpl(this._dataSource); + + @override + Future> getPendingOrders(String token) { + return _dataSource.getPendingOrders(token); + } + + @override + Future> getProfile(String token) { + return _dataSource.getProfile(token); + } +} diff --git a/lib/features/home/domain/repo/driverOrderRepo.dart b/lib/features/home/domain/repo/driverOrderRepo.dart new file mode 100644 index 0000000..5fad3ee --- /dev/null +++ b/lib/features/home/domain/repo/driverOrderRepo.dart @@ -0,0 +1,8 @@ +import 'package:tracking_app/app/core/network/api_result.dart'; +import 'package:tracking_app/features/home/data/model/response/orderRespons.dart'; +import 'package:tracking_app/features/profile/data/models/responses/edit_profile_response.dart'; + +abstract class DriverOrderRepo { + Future> getPendingOrders(String token); + Future> getProfile(String token); +} diff --git a/lib/features/home/domain/usecase/getdriverOrderUsecase.dart b/lib/features/home/domain/usecase/getdriverOrderUsecase.dart new file mode 100644 index 0000000..a138cb1 --- /dev/null +++ b/lib/features/home/domain/usecase/getdriverOrderUsecase.dart @@ -0,0 +1,15 @@ +import 'package:injectable/injectable.dart'; +import 'package:tracking_app/app/core/network/api_result.dart'; +import 'package:tracking_app/features/home/data/model/response/orderRespons.dart'; +import 'package:tracking_app/features/home/domain/repo/driverOrderRepo.dart'; + +@injectable +class GetDriverOrdersUseCase { + final DriverOrderRepo _repository; + + GetDriverOrdersUseCase(this._repository); + + Future> call(String token) { + return _repository.getPendingOrders(token); + } +} diff --git a/lib/features/home/domain/usecase/upload_driver_fire_data_use_case.dart b/lib/features/home/domain/usecase/upload_driver_fire_data_use_case.dart new file mode 100644 index 0000000..2d7ac1c --- /dev/null +++ b/lib/features/home/domain/usecase/upload_driver_fire_data_use_case.dart @@ -0,0 +1,26 @@ +import 'package:cloud_firestore/cloud_firestore.dart'; +import 'package:injectable/injectable.dart'; +import 'package:tracking_app/features/profile/data/models/driver_model.dart'; + +@injectable +class UploadDriverFireDataUseCase { + final FirebaseFirestore _firestore; + + UploadDriverFireDataUseCase(this._firestore); + + Future call( + DriverModel driver, { + required double lat, + required double lng, + String? deviceToken, + }) async { + final driverCollection = _firestore.collection('drivers'); + await driverCollection.doc(driver.Id).set({ + 'id': driver.Id, + 'name': '${driver.firstName} ${driver.lastName}', + 'phone': driver.phone, + 'currentLocation': {'lat': lat, 'lng': lng}, + 'deviceToken': deviceToken, + }, SetOptions(merge: true)); + } +} diff --git a/lib/features/home/domain/usecase/upload_order_fire_data_use_case.dart b/lib/features/home/domain/usecase/upload_order_fire_data_use_case.dart new file mode 100644 index 0000000..fd79a59 --- /dev/null +++ b/lib/features/home/domain/usecase/upload_order_fire_data_use_case.dart @@ -0,0 +1,55 @@ +import 'package:cloud_firestore/cloud_firestore.dart' hide Order; +import 'package:injectable/injectable.dart' hide Order; +import 'package:tracking_app/features/home/data/model/response/orderRespons.dart'; + +@injectable +class UploadOrderFireDataUseCase { + final FirebaseFirestore _firestore; + + UploadOrderFireDataUseCase(this._firestore); + + Future call({required Order order, required String driverId}) async { + final orderCollection = _firestore.collection('orders'); + + final data = { + 'driver_id': driverId, + 'oder_dt': { + 'items': + order.orderItems + ?.map( + (e) => { + 'productId': e.product?.id, + 'title': e.product?.title, + 'quantity': e.quantity, + 'price': e.product?.price, + 'image': e.product?.imgCover, + }, + ) + .toList() ?? + [], + 'orderId': order.id, + 'pickupAddress': { + 'address': order.store?.address ?? '', + 'name': order.store?.name ?? '', + }, + 'status': order.state ?? 'pending', + 'totalPrice': order.totalPrice ?? 0, + 'userAddress': + '${order.shippingAddress?.street ?? ''}, ${order.shippingAddress?.city ?? ''}', + }, + 'userAddress': { + 'adress': + '${order.shippingAddress?.street ?? ''}, ${order.shippingAddress?.city ?? ''}', + 'name': '${order.user?.firstName ?? ''} ${order.user?.lastName ?? ''}', + 'user_id': order.user?.id ?? '', + }, + 'user_id': order.user?.id ?? '', + }; + + if (order.id != null) { + await orderCollection.doc(order.id).set(data, SetOptions(merge: true)); + } else { + await orderCollection.add(data); + } + } +} diff --git a/lib/features/home/presentation/manger/driverorderCubit.dart b/lib/features/home/presentation/manger/driverorderCubit.dart new file mode 100644 index 0000000..8fb5687 --- /dev/null +++ b/lib/features/home/presentation/manger/driverorderCubit.dart @@ -0,0 +1,148 @@ +import 'package:firebase_messaging/firebase_messaging.dart'; +import 'package:flutter/foundation.dart'; +import 'package:geolocator/geolocator.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:injectable/injectable.dart' hide Order; +import 'package:tracking_app/features/home/data/model/response/orderRespons.dart'; +import 'package:tracking_app/app/config/auth_storage/auth_storage.dart'; +import 'package:tracking_app/app/config/base_state/base_state.dart'; +import 'package:tracking_app/app/core/network/api_result.dart'; +import 'package:tracking_app/features/home/domain/usecase/getdriverOrderUsecase.dart'; +import 'package:tracking_app/features/home/presentation/manger/driverorderIntent.dart'; +import 'package:tracking_app/features/home/presentation/manger/driverorderStates.dart'; +import 'package:tracking_app/features/home/domain/repo/driverOrderRepo.dart'; +import 'package:tracking_app/features/home/domain/usecase/upload_driver_fire_data_use_case.dart'; +import 'package:tracking_app/features/home/domain/usecase/upload_order_fire_data_use_case.dart'; + +@injectable +class DriverOrderCubit extends Cubit { + final GetDriverOrdersUseCase _getDriverOrdersUseCase; + final AuthStorage _authStorage; + final UploadDriverFireDataUseCase _uploadDriverFireDataUseCase; + final UploadOrderFireDataUseCase _uploadOrderFireDataUseCase; + final DriverOrderRepo _driverOrderRepository; + + DriverOrderCubit( + this._getDriverOrdersUseCase, + this._authStorage, + this._uploadDriverFireDataUseCase, + this._uploadOrderFireDataUseCase, + this._driverOrderRepository, + ) : super(DriverOrderState()); + + void onIntent(DriverOrderIntent intent) { + switch (intent) { + case GetPendingOrders(): + _getPendingOrders(); + case RemoveOrder(order: final order): + _removeOrder(order); + case AcceptOrder(order: final order): + _acceptOrder(order); + } + } + + void _removeOrder(Order order) { + final currentResource = state.orderResource; + if (currentResource.status == Status.success && + currentResource.data != null) { + final currentOrders = currentResource.data!.orders!; + final updatedOrders = currentOrders + .where((element) => element != order) + .toList(); + emit( + state.copyWith( + orderResource: Resource.success( + currentResource.data!.copyWith(orders: updatedOrders), + ), + ), + ); + } + } + + Future _acceptOrder(Order order) async { + final token = await _authStorage.getToken(); + if (token == null) return; + + final result = await _driverOrderRepository.getProfile(token); + + if (result is SuccessApiResult) { + final profile = (result as SuccessApiResult).data; + if (profile.driver != null) { + try { + final position = await _determinePosition(); + if (position == null) { + if (kDebugMode) { + print("Location permission denied or service disabled."); + } + return; + } + + final deviceToken = await FirebaseMessaging.instance.getToken(); + await _uploadDriverFireDataUseCase( + profile.driver!, + lat: position.latitude, + lng: position.longitude, + deviceToken: deviceToken, + ); + + await _uploadOrderFireDataUseCase( + order: order, + driverId: profile.driver?.Id ?? '', + ); + + if (order.id != null) { + await _authStorage.saveOrderId(order.id!); + } + } catch (e) { + if (kDebugMode) { + print("Firestore/Location Error: $e"); + } + } + } + } + } + + Future _determinePosition() async { + bool serviceEnabled; + LocationPermission permission; + + serviceEnabled = await Geolocator.isLocationServiceEnabled(); + if (!serviceEnabled) { + return null; + } + + permission = await Geolocator.checkPermission(); + if (permission == LocationPermission.denied) { + permission = await Geolocator.requestPermission(); + if (permission == LocationPermission.denied) { + return null; + } + } + + if (permission == LocationPermission.deniedForever) { + return null; + } + + return await Geolocator.getCurrentPosition(); + } + + Future _getPendingOrders() async { + emit(state.copyWith(orderResource: Resource.loading())); + final token = await _authStorage.getToken(); + if (token == null) { + emit( + state.copyWith(orderResource: Resource.error("User not authenticated")), + ); + return; + } + final result = await _getDriverOrdersUseCase(token); + return switch (result) { + SuccessApiResult(data: final orderResponse) => emit( + state.copyWith(orderResource: Resource.success(orderResponse)), + ), + ErrorApiResult(error: final error) => emit( + state.copyWith(orderResource: Resource.error(error)), + ), + }; + } +} diff --git a/lib/features/home/presentation/manger/driverorderIntent.dart b/lib/features/home/presentation/manger/driverorderIntent.dart new file mode 100644 index 0000000..9f88440 --- /dev/null +++ b/lib/features/home/presentation/manger/driverorderIntent.dart @@ -0,0 +1,15 @@ +import 'package:tracking_app/features/home/data/model/response/orderRespons.dart'; + +sealed class DriverOrderIntent {} + +class GetPendingOrders extends DriverOrderIntent {} + +class RemoveOrder extends DriverOrderIntent { + final Order order; + RemoveOrder(this.order); +} + +class AcceptOrder extends DriverOrderIntent { + final Order order; + AcceptOrder(this.order); +} diff --git a/lib/features/home/presentation/manger/driverorderStates.dart b/lib/features/home/presentation/manger/driverorderStates.dart new file mode 100644 index 0000000..c93079f --- /dev/null +++ b/lib/features/home/presentation/manger/driverorderStates.dart @@ -0,0 +1,13 @@ +import 'package:tracking_app/app/config/base_state/base_state.dart'; +import 'package:tracking_app/features/home/data/model/response/orderRespons.dart'; + +class DriverOrderState { + final Resource orderResource; + + DriverOrderState({Resource? orderResource}) + : orderResource = orderResource ?? Resource.initial(); + + DriverOrderState copyWith({Resource? orderResource}) { + return DriverOrderState(orderResource: orderResource ?? this.orderResource); + } +} diff --git a/lib/features/home/presentation/pages/driverOrderScreen.dart b/lib/features/home/presentation/pages/driverOrderScreen.dart new file mode 100644 index 0000000..161aab5 --- /dev/null +++ b/lib/features/home/presentation/pages/driverOrderScreen.dart @@ -0,0 +1,30 @@ +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; +import 'package:tracking_app/generated/locale_keys.g.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:tracking_app/app/config/di/di.dart'; +import 'package:tracking_app/app/core/ui_helper/color/colors.dart'; +import 'package:tracking_app/features/home/presentation/manger/driverorderCubit.dart'; +import 'package:tracking_app/features/home/presentation/manger/driverorderIntent.dart'; +import 'package:tracking_app/features/home/presentation/widgets/driverScreenBody.dart'; + +class DriverOrderScreen extends StatelessWidget { + const DriverOrderScreen({super.key}); + + @override + Widget build(BuildContext context) { + return BlocProvider( + create: (context) => + getIt()..onIntent(GetPendingOrders()), + child: Scaffold( + appBar: AppBar( + title: Text( + LocaleKeys.floweryRider.tr(), + style: const TextStyle(color: AppColors.pink), + ), + ), + body: const DriverOrderBody(), + ), + ); + } +} diff --git a/lib/features/home/presentation/widgets/driverOrderButton.dart b/lib/features/home/presentation/widgets/driverOrderButton.dart new file mode 100644 index 0000000..6759d98 --- /dev/null +++ b/lib/features/home/presentation/widgets/driverOrderButton.dart @@ -0,0 +1,42 @@ +import 'package:flutter/material.dart'; + +class DriverOrderButton extends StatelessWidget { + final String text; + final VoidCallback onTap; + final bool isPrimary; + + const DriverOrderButton({ + super.key, + required this.text, + required this.onTap, + required this.isPrimary, + }); + + @override + Widget build(BuildContext context) { + final width = MediaQuery.of(context).size.width; + final height = MediaQuery.of(context).size.height; + return InkWell( + onTap: onTap, + child: Container( + padding: EdgeInsets.symmetric( + horizontal: width * 0.06, + vertical: height * 0.012, + ), + decoration: BoxDecoration( + color: isPrimary ? const Color(0xFFE91E63) : Colors.white, + borderRadius: BorderRadius.circular(24), + border: isPrimary ? null : Border.all(color: const Color(0xFFE91E63)), + ), + child: Text( + text, + style: TextStyle( + color: isPrimary ? Colors.white : const Color(0xFFE91E63), + fontSize: width * 0.035, + fontWeight: FontWeight.w500, + ), + ), + ), + ); + } +} diff --git a/lib/features/home/presentation/widgets/driverOrderInfoCard.dart b/lib/features/home/presentation/widgets/driverOrderInfoCard.dart new file mode 100644 index 0000000..c8b668a --- /dev/null +++ b/lib/features/home/presentation/widgets/driverOrderInfoCard.dart @@ -0,0 +1,92 @@ +import 'package:flutter/material.dart'; + +class DriverOrderInfoCard extends StatelessWidget { + final String? image; + final String title; + final String subtitle; + final bool isStore; + + const DriverOrderInfoCard({ + super.key, + required this.image, + required this.title, + required this.subtitle, + required this.isStore, + }); + + @override + Widget build(BuildContext context) { + final width = MediaQuery.of(context).size.width; + final height = MediaQuery.of(context).size.height; + return Container( + padding: EdgeInsets.all(width * 0.03), + decoration: BoxDecoration( + color: const Color(0xFFF9F9F9), + borderRadius: BorderRadius.circular(12), + border: Border.all(color: const Color(0xFFEEEEEE)), + ), + child: Row( + children: [ + Container( + width: width * 0.12, + height: width * 0.12, + decoration: BoxDecoration( + shape: BoxShape.circle, + color: isStore ? const Color(0xFFE91E63) : Colors.grey[300], + image: image != null + ? DecorationImage( + image: NetworkImage(image!), + fit: BoxFit.cover, + ) + : null, + ), + child: image == null + ? Icon( + isStore ? Icons.store : Icons.person, + color: Colors.white, + ) + : null, + ), + SizedBox(width: width * 0.03), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + title, + style: TextStyle( + fontSize: width * 0.035, + fontWeight: FontWeight.w500, + color: const Color(0xFF2D2D2D), + ), + ), + SizedBox(height: height * 0.005), + Row( + children: [ + Icon( + Icons.location_on_outlined, + size: width * 0.035, + color: Colors.black54, + ), + SizedBox(width: width * 0.01), + Expanded( + child: Text( + subtitle, + style: TextStyle( + fontSize: width * 0.03, + color: Colors.black54, + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ), + ], + ), + ], + ), + ), + ], + ), + ); + } +} diff --git a/lib/features/home/presentation/widgets/driverOrderItem.dart b/lib/features/home/presentation/widgets/driverOrderItem.dart new file mode 100644 index 0000000..d92fed5 --- /dev/null +++ b/lib/features/home/presentation/widgets/driverOrderItem.dart @@ -0,0 +1,98 @@ +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; +import 'package:tracking_app/app/core/ui_helper/color/colors.dart'; +import 'package:tracking_app/features/home/data/model/response/orderRespons.dart'; +import 'package:tracking_app/features/home/presentation/widgets/driverOrderInfoCard.dart'; +import 'package:tracking_app/features/home/presentation/widgets/driverOrderSectionLabel.dart'; +import 'package:tracking_app/generated/locale_keys.g.dart'; + +class DriverOrderItem extends StatelessWidget { + final Order order; + final VoidCallback onAccept; + final VoidCallback onReject; + final bool isLoading; + + const DriverOrderItem({ + super.key, + required this.order, + required this.onAccept, + required this.onReject, + this.isLoading = false, + }); + + @override + Widget build(BuildContext context) { + final width = MediaQuery.of(context).size.width; + final height = MediaQuery.of(context).size.height; + + return Container( + margin: EdgeInsets.symmetric( + horizontal: width * 0.04, + vertical: height * 0.01, + ), + padding: EdgeInsets.all(width * 0.04), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(16), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.04), + blurRadius: 10, + offset: const Offset(0, 4), + ), + ], + border: Border.all(color: Colors.grey.shade100), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + LocaleKeys.driverOrderTitle.tr(), + style: const TextStyle( + fontSize: 14, + fontWeight: FontWeight.w600, + color: AppColors.grey, + ), + ), + Text( + "# ${order.id ?? ''}", + style: const TextStyle( + fontSize: 14, + fontWeight: FontWeight.bold, + color: AppColors.blackColor, + ), + ), + ], + ), + SizedBox(height: height * 0.02), + DriverOrderSectionLabel(LocaleKeys.pickupAddress.tr()), + SizedBox(height: height * 0.01), + DriverOrderInfoCard( + image: order.store?.image, + title: order.store?.name ?? LocaleKeys.unknownStore.tr(), + subtitle: order.store?.address ?? LocaleKeys.noAddress.tr(), + isStore: true, + ), + SizedBox(height: height * 0.02), + DriverOrderSectionLabel(LocaleKeys.userAddress.tr()), + SizedBox(height: height * 0.01), + DriverOrderInfoCard( + image: order.user?.photo != null + ? "https://flower.elevateegy.com/uploads/${order.user!.photo!}" + : null, + title: + "${order.user?.firstName ?? ''} ${order.user?.lastName ?? ''}", + subtitle: + order.shippingAddress?.street ?? LocaleKeys.noAddress.tr(), + isStore: false, + ), + SizedBox(height: height * 0.03), + + ], + ), + ); + } +} diff --git a/lib/features/home/presentation/widgets/driverOrderSectionLabel.dart b/lib/features/home/presentation/widgets/driverOrderSectionLabel.dart new file mode 100644 index 0000000..f15fb59 --- /dev/null +++ b/lib/features/home/presentation/widgets/driverOrderSectionLabel.dart @@ -0,0 +1,15 @@ +import 'package:flutter/material.dart'; + +class DriverOrderSectionLabel extends StatelessWidget { + final String text; + const DriverOrderSectionLabel(this.text, {super.key}); + + @override + Widget build(BuildContext context) { + final width = MediaQuery.of(context).size.width; + return Text( + text, + style: TextStyle(fontSize: width * 0.035, color: Colors.grey), + ); + } +} diff --git a/lib/features/home/presentation/widgets/driverScreenBody.dart b/lib/features/home/presentation/widgets/driverScreenBody.dart new file mode 100644 index 0000000..1bfefb6 --- /dev/null +++ b/lib/features/home/presentation/widgets/driverScreenBody.dart @@ -0,0 +1,80 @@ +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:go_router/go_router.dart'; +import 'package:tracking_app/app/config/auth_storage/auth_storage.dart'; +import 'package:tracking_app/app/config/base_state/base_state.dart'; +import 'package:tracking_app/app/config/di/di.dart'; +import 'package:tracking_app/app/core/router/route_names.dart'; +import 'package:tracking_app/features/home/presentation/manger/driverorderCubit.dart'; +import 'package:tracking_app/features/home/presentation/manger/driverorderIntent.dart'; +import 'package:tracking_app/features/home/presentation/manger/driverorderStates.dart'; +import 'package:tracking_app/features/home/presentation/widgets/driverOrderItem.dart'; +import 'package:tracking_app/generated/locale_keys.g.dart'; + +class DriverOrderBody extends StatefulWidget { + const DriverOrderBody({super.key}); + + @override + State createState() => _DriverOrderBodyState(); +} + +class _DriverOrderBodyState extends State { + @override + Widget build(BuildContext context) { + return BlocBuilder( + builder: (context, state) { + final resource = state.orderResource; + + if (resource.status == Status.loading) { + return const Center(child: CircularProgressIndicator()); + } + + if (resource.status == Status.error) { + return Center( + child: Text( + resource.error ?? LocaleKeys.unknownError.tr(), + style: const TextStyle(color: Colors.red), + ), + ); + } + + if (resource.status == Status.success) { + final orders = resource.data?.orders ?? []; + if (orders.isEmpty) { + return Center(child: Text(LocaleKeys.noPendingOrders.tr())); + } + return RefreshIndicator( + onRefresh: () async { + context.read().onIntent(GetPendingOrders()); + }, + child: ListView.builder( + itemCount: orders.length, + itemBuilder: (context, index) { + return DriverOrderItem( + order: orders[index], + onAccept: () async { + final order = orders[index]; + await getIt().saveOrderId(order.id.toString()); + debugPrint('<<<< Saved Order ID: ${order.id}'); + context.read().onIntent( + AcceptOrder(orders[index]), + ); + context.push(RouteNames.ordersDetailsPage); + }, + onReject: () { + context.read().onIntent( + RemoveOrder(orders[index]), + ); + }, + ); + }, + ), + ); + } + + return const SizedBox.shrink(); + }, + ); + } +} diff --git a/lib/features/my_orders/api/datasource/my_orders_remote_data_source_imp.dart b/lib/features/my_orders/api/datasource/my_orders_remote_data_source_imp.dart new file mode 100644 index 0000000..b419023 --- /dev/null +++ b/lib/features/my_orders/api/datasource/my_orders_remote_data_source_imp.dart @@ -0,0 +1,24 @@ +import 'package:injectable/injectable.dart'; +import 'package:tracking_app/app/core/api_manger/api_client.dart'; +import 'package:tracking_app/app/core/network/api_result.dart'; +import 'package:tracking_app/app/core/network/safe_api_call.dart'; +import 'package:tracking_app/features/my_orders/data/datasource/my_orders_remote_data_source.dart'; +import 'package:tracking_app/features/my_orders/data/models/response/my_order_response.dart'; + +@Injectable(as: MyOrdersRemoteDataSource) +class MyOrdersRemoteDataSourceImp extends MyOrdersRemoteDataSource { + final ApiClient apiClient; + MyOrdersRemoteDataSourceImp(this.apiClient); + + @override + Future> getAllOrders({ + required String token, + int limit = 10, + int page = 1, + }) { + return safeApiCall( + call: () => + apiClient.getAllOrders(token: token, limit: limit, page: page), + ); + } +} diff --git a/lib/features/my_orders/data/datasource/my_orders_remote_data_source.dart b/lib/features/my_orders/data/datasource/my_orders_remote_data_source.dart new file mode 100644 index 0000000..8648ffa --- /dev/null +++ b/lib/features/my_orders/data/datasource/my_orders_remote_data_source.dart @@ -0,0 +1,10 @@ +import 'package:tracking_app/app/core/network/api_result.dart'; +import 'package:tracking_app/features/my_orders/data/models/response/my_order_response.dart'; + +abstract class MyOrdersRemoteDataSource { + Future> getAllOrders({ + required String token, + int limit = 10, + int page = 1, + }); +} diff --git a/lib/features/my_orders/data/mappers/metadata_mapper.dart b/lib/features/my_orders/data/mappers/metadata_mapper.dart new file mode 100644 index 0000000..3b64bf2 --- /dev/null +++ b/lib/features/my_orders/data/mappers/metadata_mapper.dart @@ -0,0 +1,15 @@ +import 'package:tracking_app/features/my_orders/data/models/meta_data_dto.dart'; +import 'package:tracking_app/features/my_orders/domain/models/meta_data_entity.dart'; + +extension MetadataMapper on Metadata { + MetadataEntity toEntity() { + return MetadataEntity( + currentPage: currentPage ?? 0, + totalPages: totalPages ?? 0, + totalItems: totalItems ?? 0, + limit: limit ?? 10, + cancelledCount: cancelledCount ?? 0, + completedCount: completedCount ?? 0, + ); + } +} diff --git a/lib/features/my_orders/data/mappers/order_item_mapper.dart b/lib/features/my_orders/data/mappers/order_item_mapper.dart new file mode 100644 index 0000000..c36c2b9 --- /dev/null +++ b/lib/features/my_orders/data/mappers/order_item_mapper.dart @@ -0,0 +1,17 @@ +import 'package:tracking_app/features/my_orders/domain/models/order_item_entity.dart'; +import 'package:tracking_app/features/my_orders/domain/models/product_entity.dart'; + +import '../models/order_item_model.dart'; +import 'product_mapper.dart'; + +extension OrderItemMapper on OrderItem { + OrderItemEntity toEntity() { + return OrderItemEntity( + product: + product?.toEntity() ?? + ProductEntity(id: '', price: 0, title: '', image: ''), + price: price ?? 0, + quantity: quantity ?? 0, + ); + } +} diff --git a/lib/features/my_orders/data/mappers/order_mapper.dart b/lib/features/my_orders/data/mappers/order_mapper.dart new file mode 100644 index 0000000..06571e0 --- /dev/null +++ b/lib/features/my_orders/data/mappers/order_mapper.dart @@ -0,0 +1,25 @@ +import 'package:tracking_app/features/my_orders/domain/models/order_entity.dart'; + +import '../models/order_model.dart'; +import 'order_item_mapper.dart'; +import 'user_mapper.dart'; +import 'store_mapper.dart'; + +extension OrderMapper on Order { + OrderEntity toEntity() { + return OrderEntity( + id: id ?? '', + user: user!.toEntity(), + store: store?.toEntity(), + address: address ?? '', + items: orderItems?.map((e) => e.toEntity()).toList() ?? [], + totalPrice: totalPrice ?? 0, + paymentType: paymentType ?? '', + isPaid: isPaid ?? false, + isDelivered: isDelivered ?? false, + state: state ?? '', + createdAt: createdAt ?? '', + orderNumber: orderNumber ?? '', + ); + } +} diff --git a/lib/features/my_orders/data/mappers/orders_list_mapper.dart b/lib/features/my_orders/data/mappers/orders_list_mapper.dart new file mode 100644 index 0000000..d1be05b --- /dev/null +++ b/lib/features/my_orders/data/mappers/orders_list_mapper.dart @@ -0,0 +1,9 @@ +import 'package:tracking_app/features/my_orders/domain/models/order_entity.dart'; +import '../models/order_model.dart'; +import 'order_mapper.dart'; + +extension OrdersListMapper on List { + List toEntityList() { + return map((e) => e.toEntity()).toList(); + } +} diff --git a/lib/features/my_orders/data/mappers/product_mapper.dart b/lib/features/my_orders/data/mappers/product_mapper.dart new file mode 100644 index 0000000..c7010f5 --- /dev/null +++ b/lib/features/my_orders/data/mappers/product_mapper.dart @@ -0,0 +1,13 @@ +import 'package:tracking_app/features/my_orders/domain/models/product_entity.dart'; +import '../models/product_model.dart'; + +extension ProductMapper on Product { + ProductEntity toEntity() { + return ProductEntity( + id: id ?? '', + title: title ?? '', + image: image ?? '', + price: price ?? 0, + ); + } +} diff --git a/lib/features/my_orders/data/mappers/store_mapper.dart b/lib/features/my_orders/data/mappers/store_mapper.dart new file mode 100644 index 0000000..3f4b806 --- /dev/null +++ b/lib/features/my_orders/data/mappers/store_mapper.dart @@ -0,0 +1,13 @@ +import 'package:tracking_app/features/my_orders/domain/models/store_entity.dart'; +import '../models/store_model.dart'; + +extension StoreMapper on Store { + StoreEntity toEntity() { + return StoreEntity( + name: name ?? '', + image: image ?? '', + address: address ?? '', + phoneNumber: phoneNumber ?? '', + ); + } +} diff --git a/lib/features/my_orders/data/mappers/user_mapper.dart b/lib/features/my_orders/data/mappers/user_mapper.dart new file mode 100644 index 0000000..9feb6e1 --- /dev/null +++ b/lib/features/my_orders/data/mappers/user_mapper.dart @@ -0,0 +1,14 @@ +import 'package:tracking_app/features/my_orders/domain/models/user_entity.dart'; +import '../models/user_model.dart'; + +extension UserMapper on User { + UserEntity toEntity() { + return UserEntity( + id: id ?? '', + firstName: firstName ?? '', + lastName: lastName ?? '', + phone: phone ?? '', + photo: photo ?? '', + ); + } +} diff --git a/lib/features/my_orders/data/models/meta_data_dto.dart b/lib/features/my_orders/data/models/meta_data_dto.dart new file mode 100644 index 0000000..017f445 --- /dev/null +++ b/lib/features/my_orders/data/models/meta_data_dto.dart @@ -0,0 +1,36 @@ +import 'package:json_annotation/json_annotation.dart'; + +part 'meta_data_dto.g.dart'; + +@JsonSerializable() +class Metadata { + @JsonKey(name: "currentPage") + final int? currentPage; + @JsonKey(name: "totalPages") + final int? totalPages; + @JsonKey(name: "totalItems") + final int? totalItems; + @JsonKey(name: "limit") + final int? limit; + @JsonKey(name: "cancelledCount") + final int? cancelledCount; + @JsonKey(name: "completedCount") + final int? completedCount; + + Metadata({ + this.currentPage, + this.totalPages, + required this.totalItems, + required this.limit, + this.cancelledCount = 0, + this.completedCount = 0, + }); + + factory Metadata.fromJson(Map json) { + return _$MetadataFromJson(json); + } + + Map toJson() { + return _$MetadataToJson(this); + } +} diff --git a/lib/features/my_orders/data/models/order_item_model.dart b/lib/features/my_orders/data/models/order_item_model.dart new file mode 100644 index 0000000..b53bf5e --- /dev/null +++ b/lib/features/my_orders/data/models/order_item_model.dart @@ -0,0 +1,26 @@ +import 'package:json_annotation/json_annotation.dart'; +import 'product_model.dart'; + +part 'order_item_model.g.dart'; + +@JsonSerializable() +class OrderItem { + @JsonKey(name: "_id") + final String? id; + + @JsonKey(name: "product") + final Product? product; + + @JsonKey(name: "price") + final int? price; + + @JsonKey(name: "quantity") + final int? quantity; + + OrderItem({this.id, this.product, this.price, this.quantity}); + + factory OrderItem.fromJson(Map json) => + _$OrderItemFromJson(json); + + Map toJson() => _$OrderItemToJson(this); +} diff --git a/lib/features/my_orders/data/models/order_model.dart b/lib/features/my_orders/data/models/order_model.dart new file mode 100644 index 0000000..761a46e --- /dev/null +++ b/lib/features/my_orders/data/models/order_model.dart @@ -0,0 +1,72 @@ +import 'package:json_annotation/json_annotation.dart'; +import 'order_item_model.dart'; +import 'user_model.dart'; +import 'store_model.dart'; + +part 'order_model.g.dart'; + +@JsonSerializable() +class Order { + @JsonKey(name: "_id") + final String? id; + + @JsonKey(name: "user") + final User? user; + + @JsonKey(name: "store") + final Store? store; + + @JsonKey(name: "address") + final String? address; + + @JsonKey(name: "orderItems") + final List? orderItems; + + @JsonKey(name: "totalPrice") + final int? totalPrice; + + @JsonKey(name: "paymentType") + final String? paymentType; + + @JsonKey(name: "isPaid") + final bool? isPaid; + + @JsonKey(name: "isDelivered") + final bool? isDelivered; + + @JsonKey(name: "state") + final String? state; + + @JsonKey(name: "createdAt") + final String? createdAt; + + @JsonKey(name: "updatedAt") + final String? updatedAt; + + @JsonKey(name: "orderNumber") + final String? orderNumber; + + @JsonKey(name: "__v") + final int? v; + + Order({ + this.id, + this.user, + this.store, + this.address, + this.orderItems, + this.totalPrice, + this.paymentType, + this.isPaid, + this.isDelivered, + this.state, + this.createdAt, + this.updatedAt, + this.orderNumber, + this.v, + }); + + factory Order.fromJson(Map json) => _$OrderFromJson(json); + + Map toJson() => _$OrderToJson(this); +} diff --git a/lib/features/my_orders/data/models/product_model.dart b/lib/features/my_orders/data/models/product_model.dart new file mode 100644 index 0000000..359f9ac --- /dev/null +++ b/lib/features/my_orders/data/models/product_model.dart @@ -0,0 +1,25 @@ +import 'package:json_annotation/json_annotation.dart'; + +part 'product_model.g.dart'; + +@JsonSerializable() +class Product { + @JsonKey(name: "_id") + final String? id; + + @JsonKey(name: "title") + final String? title; + + @JsonKey(name: "image") + final String? image; + + @JsonKey(name: "price") + final int? price; + + Product({this.id, this.title, this.image, this.price}); + + factory Product.fromJson(Map json) => + _$ProductFromJson(json); + + Map toJson() => _$ProductToJson(this); +} diff --git a/lib/features/my_orders/data/models/response/my_order_response.dart b/lib/features/my_orders/data/models/response/my_order_response.dart new file mode 100644 index 0000000..0a298e3 --- /dev/null +++ b/lib/features/my_orders/data/models/response/my_order_response.dart @@ -0,0 +1,24 @@ +import 'package:json_annotation/json_annotation.dart'; +import '../meta_data_dto.dart'; +import '../order_model.dart'; + +part 'my_order_response.g.dart'; + +@JsonSerializable() +class MyOrderResponse { + @JsonKey(name: "message") + final String? message; + + @JsonKey(name: "metadata") + final Metadata? metadata; + + @JsonKey(name: "orders") + final List? orders; + + MyOrderResponse({this.message, this.metadata, this.orders}); + + factory MyOrderResponse.fromJson(Map json) => + _$MyOrderResponseFromJson(json); + + Map toJson() => _$MyOrderResponseToJson(this); +} diff --git a/lib/features/my_orders/data/models/store_model.dart b/lib/features/my_orders/data/models/store_model.dart new file mode 100644 index 0000000..ceff9dd --- /dev/null +++ b/lib/features/my_orders/data/models/store_model.dart @@ -0,0 +1,27 @@ +import 'package:json_annotation/json_annotation.dart'; + +part 'store_model.g.dart'; + +@JsonSerializable() +class Store { + @JsonKey(name: "name") + final String? name; + + @JsonKey(name: "image") + final String? image; + + @JsonKey(name: "address") + final String? address; + + @JsonKey(name: "phoneNumber") + final String? phoneNumber; + + @JsonKey(name: "latLong") + final String? latLong; + + Store({this.name, this.image, this.address, this.phoneNumber, this.latLong}); + + factory Store.fromJson(Map json) => _$StoreFromJson(json); + + Map toJson() => _$StoreToJson(this); +} diff --git a/lib/features/my_orders/data/models/user_model.dart b/lib/features/my_orders/data/models/user_model.dart new file mode 100644 index 0000000..c302aac --- /dev/null +++ b/lib/features/my_orders/data/models/user_model.dart @@ -0,0 +1,45 @@ +import 'package:json_annotation/json_annotation.dart'; + +part 'user_model.g.dart'; + +@JsonSerializable() +class User { + @JsonKey(name: "_id") + final String? id; + + @JsonKey(name: "firstName") + final String? firstName; + + @JsonKey(name: "lastName") + final String? lastName; + + @JsonKey(name: "email") + final String? email; + + @JsonKey(name: "gender") + final String? gender; + + @JsonKey(name: "phone") + final String? phone; + + @JsonKey(name: "photo") + final String? photo; + + @JsonKey(name: "passwordChangedAt") + final String? passwordChangedAt; + + User({ + this.id, + this.firstName, + this.lastName, + this.email, + this.gender, + this.phone, + this.photo, + this.passwordChangedAt, + }); + + factory User.fromJson(Map json) => _$UserFromJson(json); + + Map toJson() => _$UserToJson(this); +} diff --git a/lib/features/my_orders/data/repo/my_orders_repo_imp.dart b/lib/features/my_orders/data/repo/my_orders_repo_imp.dart new file mode 100644 index 0000000..f7f3ed4 --- /dev/null +++ b/lib/features/my_orders/data/repo/my_orders_repo_imp.dart @@ -0,0 +1,178 @@ +import 'package:injectable/injectable.dart'; +import 'package:tracking_app/app/core/network/api_result.dart'; +import 'package:tracking_app/features/my_orders/data/datasource/my_orders_remote_data_source.dart'; +import 'package:tracking_app/features/my_orders/data/mappers/metadata_mapper.dart'; +import 'package:tracking_app/features/my_orders/data/mappers/order_mapper.dart'; +import 'package:tracking_app/features/my_orders/data/models/response/my_order_response.dart'; +import 'package:tracking_app/features/my_orders/domain/models/order_item_entity.dart'; +import 'package:tracking_app/features/my_orders/domain/models/product_entity.dart'; +import 'package:tracking_app/features/my_orders/domain/models/store_entity.dart'; +import 'package:tracking_app/features/my_orders/domain/repo/my_orders_repo.dart'; +import 'package:tracking_app/features/my_orders/domain/models/order_entity.dart'; +import 'package:tracking_app/features/my_orders/domain/models/meta_data_entity.dart'; +import 'package:tracking_app/features/my_orders/domain/models/user_entity.dart'; + +@Injectable(as: MyOrdersRepo) +class MyOrdersRepoImpl implements MyOrdersRepo { + final MyOrdersRemoteDataSource remoteDataSource; + + MyOrdersRepoImpl(this.remoteDataSource); + + @override + Future> getAllOrders({ + required String token, + int limit = 10, + int page = 1, + }) async { + try { + final result = await remoteDataSource.getAllOrders( + token: token, + limit: limit, + page: page, + ); + + if (result is SuccessApiResult) { + final response = result.data; + List orders = + response.orders?.map((e) => e.toEntity()).toList() ?? []; + MetadataEntity? metadata = response.metadata?.toEntity(); + + if (orders.isEmpty) { + orders = _getDummyOrders(); + metadata = const MetadataEntity( + currentPage: 1, + totalPages: 1, + totalItems: 4, + limit: 10, + cancelledCount: 1, + completedCount: 3, + ); + } + + return SuccessApiResult( + data: MyOrdersResult(orders: orders, metadata: metadata), + ); + } else if (result is ErrorApiResult) { + return ErrorApiResult(error: result.error); + } else { + return ErrorApiResult(error: 'Unknown error'); + } + } catch (e) { + return ErrorApiResult(error: e.toString()); + } + } + + List _getDummyOrders() { + final dummyItems = [ + OrderItemEntity( + product: ProductEntity( + id: "p1", + title: "Red roses, 15 Pink Rose Bouquet", + image: + "https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcT6-k6E9vG_c9B_I0m_K-7J1f8e6C9F5G1g5A&s", + price: 600, + ), + price: 600, + quantity: 1, + ), + OrderItemEntity( + product: ProductEntity( + id: "p2", + title: "Red roses, 15 Pink Rose Bouquet", + image: + "https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcT6-k6E9vG_c9B_I0m_K-7J1f8e6C9F5G1g5A&s", + price: 600, + ), + price: 600, + quantity: 4, + ), + ]; + + return [ + OrderEntity( + id: "123456", + user: UserEntity( + id: "u1", + firstName: "Noor", + lastName: "mohamed", + phone: "01012345678", + photo: "https://i.pravatar.cc/150?u=u1", + ), + store: StoreEntity( + name: "Flowery store", + image: + "https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcT6-k6E9vG_c9B_I0m_K-7J1f8e6C9F5G1g5A&s", + address: "20th st, Sheikh Zayed, Giza", + phoneNumber: "01012345678", + ), + address: "20th st, Sheikh Zayed, Giza", + items: dummyItems, + totalPrice: 3000, + paymentType: "Cash on delivery", + isPaid: true, + isDelivered: true, + state: "Completed", + createdAt: DateTime.now() + .subtract(const Duration(hours: 2)) + .toIso8601String(), + orderNumber: "123456", + ), + OrderEntity( + id: "123457", + user: UserEntity( + id: "u1", + firstName: "Nooor", + lastName: "mohamed", + phone: "01012345678", + photo: "https://i.pravatar.cc/150?u=u1", + ), + store: StoreEntity( + name: "Flowery store", + image: + "https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcT6-k6E9vG_c9B_I0m_K-7J1f8e6C9F5G1g5A&s", + address: "20th st, Sheikh Zayed, Giza", + phoneNumber: "01012345678", + ), + address: "20th st, Sheikh Zayed, Giza", + items: dummyItems, + totalPrice: 3000, + paymentType: "Cash on delivery", + isPaid: false, + isDelivered: false, + state: "Cancelled", + createdAt: DateTime.now() + .subtract(const Duration(hours: 4)) + .toIso8601String(), + orderNumber: "123456", + ), + OrderEntity( + id: "123458", + user: UserEntity( + id: "u1", + firstName: "Noor", + lastName: "mohamed", + phone: "01012345678", + photo: "https://i.pravatar.cc/150?u=u1", + ), + store: StoreEntity( + name: "Flowery store", + image: + "https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcT6-k6E9vG_c9B_I0m_K-7J1f8e6C9F5G1g5A&s", + address: "20th st, Sheikh Zayed, Giza", + phoneNumber: "01012345678", + ), + address: "20th st, Sheikh Zayed, Giza", + items: dummyItems, + totalPrice: 3000, + paymentType: "Cash on delivery", + isPaid: false, + isDelivered: false, + state: "Pending", + createdAt: DateTime.now() + .subtract(const Duration(hours: 6)) + .toIso8601String(), + orderNumber: "123456", + ), + ]; + } +} diff --git a/lib/features/my_orders/domain/models/meta_data_entity.dart b/lib/features/my_orders/domain/models/meta_data_entity.dart new file mode 100644 index 0000000..b22d3e1 --- /dev/null +++ b/lib/features/my_orders/domain/models/meta_data_entity.dart @@ -0,0 +1,17 @@ +class MetadataEntity { + final int currentPage; + final int totalPages; + final int totalItems; + final int limit; + final int cancelledCount; + final int completedCount; + + const MetadataEntity({ + required this.currentPage, + required this.totalPages, + required this.totalItems, + required this.limit, + this.cancelledCount = 0, + this.completedCount = 0, + }); +} diff --git a/lib/features/my_orders/domain/models/order_entity.dart b/lib/features/my_orders/domain/models/order_entity.dart new file mode 100644 index 0000000..36acd73 --- /dev/null +++ b/lib/features/my_orders/domain/models/order_entity.dart @@ -0,0 +1,33 @@ +import 'package:tracking_app/features/my_orders/domain/models/order_item_entity.dart'; +import 'package:tracking_app/features/my_orders/domain/models/user_entity.dart'; +import 'package:tracking_app/features/my_orders/domain/models/store_entity.dart'; + +class OrderEntity { + final String id; + final UserEntity user; + final StoreEntity? store; + final String address; + final List items; + final int totalPrice; + final String paymentType; + final bool isPaid; + final bool isDelivered; + final String state; + final String createdAt; + final String orderNumber; + + OrderEntity({ + required this.id, + required this.user, + this.store, + this.address = '', + required this.items, + required this.totalPrice, + required this.paymentType, + required this.isPaid, + required this.isDelivered, + required this.state, + required this.createdAt, + required this.orderNumber, + }); +} diff --git a/lib/features/my_orders/domain/models/order_item_entity.dart b/lib/features/my_orders/domain/models/order_item_entity.dart new file mode 100644 index 0000000..b9f2977 --- /dev/null +++ b/lib/features/my_orders/domain/models/order_item_entity.dart @@ -0,0 +1,13 @@ +import 'package:tracking_app/features/my_orders/domain/models/product_entity.dart'; + +class OrderItemEntity { + final ProductEntity product; + final int price; + final int quantity; + + OrderItemEntity({ + required this.product, + required this.price, + required this.quantity, + }); +} diff --git a/lib/features/my_orders/domain/models/product_entity.dart b/lib/features/my_orders/domain/models/product_entity.dart new file mode 100644 index 0000000..64bbd78 --- /dev/null +++ b/lib/features/my_orders/domain/models/product_entity.dart @@ -0,0 +1,13 @@ +class ProductEntity { + final String id; + final String title; + final String image; + final int price; + + ProductEntity({ + required this.id, + required this.title, + required this.image, + required this.price, + }); +} diff --git a/lib/features/my_orders/domain/models/store_entity.dart b/lib/features/my_orders/domain/models/store_entity.dart new file mode 100644 index 0000000..62a61d8 --- /dev/null +++ b/lib/features/my_orders/domain/models/store_entity.dart @@ -0,0 +1,13 @@ +class StoreEntity { + final String name; + final String image; + final String address; + final String phoneNumber; + + StoreEntity({ + required this.name, + required this.image, + required this.address, + required this.phoneNumber, + }); +} diff --git a/lib/features/my_orders/domain/models/user_entity.dart b/lib/features/my_orders/domain/models/user_entity.dart new file mode 100644 index 0000000..9dbd361 --- /dev/null +++ b/lib/features/my_orders/domain/models/user_entity.dart @@ -0,0 +1,15 @@ +class UserEntity { + final String id; + final String firstName; + final String lastName; + final String phone; + final String photo; + + UserEntity({ + required this.id, + required this.firstName, + required this.lastName, + required this.phone, + required this.photo, + }); +} diff --git a/lib/features/my_orders/domain/repo/my_orders_repo.dart b/lib/features/my_orders/domain/repo/my_orders_repo.dart new file mode 100644 index 0000000..b129443 --- /dev/null +++ b/lib/features/my_orders/domain/repo/my_orders_repo.dart @@ -0,0 +1,18 @@ +import 'package:tracking_app/app/core/network/api_result.dart'; +import 'package:tracking_app/features/my_orders/domain/models/meta_data_entity.dart'; +import 'package:tracking_app/features/my_orders/domain/models/order_entity.dart'; + +class MyOrdersResult { + final List orders; + final MetadataEntity? metadata; + + MyOrdersResult({required this.orders, this.metadata}); +} + +abstract class MyOrdersRepo { + Future> getAllOrders({ + required String token, + int limit, + int page, + }); +} diff --git a/lib/features/my_orders/domain/usecases/get_order_use_case.dart b/lib/features/my_orders/domain/usecases/get_order_use_case.dart new file mode 100644 index 0000000..6137a31 --- /dev/null +++ b/lib/features/my_orders/domain/usecases/get_order_use_case.dart @@ -0,0 +1,18 @@ +import 'package:injectable/injectable.dart'; +import 'package:tracking_app/app/core/network/api_result.dart'; +import 'package:tracking_app/features/my_orders/domain/repo/my_orders_repo.dart'; + +@injectable +class GetOrderUseCase { + final MyOrdersRepo repo; + + GetOrderUseCase(this.repo); + + Future> call({ + required String token, + int page = 1, + int limit = 10, + }) { + return repo.getAllOrders(token: token, page: page, limit: limit); + } +} diff --git a/lib/features/my_orders/domain/usecases/update_order_status_use_case.dart b/lib/features/my_orders/domain/usecases/update_order_status_use_case.dart new file mode 100644 index 0000000..803735e --- /dev/null +++ b/lib/features/my_orders/domain/usecases/update_order_status_use_case.dart @@ -0,0 +1,13 @@ +// import 'package:injectable/injectable.dart'; +// import 'package:tracking_app/features/track_order/domain/repos/track_order_repo.dart'; + +// @injectable +// class UpdateMyOrderStatusUseCase { +// final TrackOrderRepo repository; + +// UpdateMyOrderStatusUseCase(this.repository); + +// Future call(String orderId, String status) { +// return repository.updateOrderStatus(orderId, status); +// } +// } diff --git a/lib/features/my_orders/presentation/manager/my_orders_cubit.dart b/lib/features/my_orders/presentation/manager/my_orders_cubit.dart new file mode 100644 index 0000000..2709eba --- /dev/null +++ b/lib/features/my_orders/presentation/manager/my_orders_cubit.dart @@ -0,0 +1,134 @@ +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:injectable/injectable.dart'; +import 'package:tracking_app/app/config/auth_storage/auth_storage.dart'; +import 'package:tracking_app/app/config/base_state/base_state.dart'; +import 'package:tracking_app/app/core/network/api_result.dart'; +import 'package:tracking_app/features/my_orders/domain/usecases/get_order_use_case.dart'; + +import 'my_orders_intent.dart'; +import 'my_orders_state.dart'; + +@injectable +class MyOrdersCubit extends Cubit { + final GetOrderUseCase _getOrdersUseCase; + final AuthStorage _authStorage; + + int _page = 1; + bool _hasMore = true; + + MyOrdersCubit(this._getOrdersUseCase, this._authStorage) + : super(MyOrdersState()); + + void doIntent(MyOrdersIntent intent) { + switch (intent.runtimeType) { + case GetMyOrdersIntent: + _getOrders(intent as GetMyOrdersIntent); + break; + + case LoadMoreOrdersIntent: + _loadMore(); + break; + + case OpenOrderDetailsIntent: + emit( + state.copyWith( + selectedOrder: (intent as OpenOrderDetailsIntent).order, + ), + ); + break; + + case FilterCompletedOrdersIntent: + _filterCompleted(); + break; + + case FilterCancelledOrdersIntent: + _filterCancelled(); + break; + } + } + + Future _getOrders(GetMyOrdersIntent intent) async { + emit(state.copyWith(ordersResource: Resource.loading())); + + final token = await _authStorage.getToken(); + if (token == null || token.isEmpty) { + emit(state.copyWith(ordersResource: Resource.error("Token not found"))); + return; + } + _hasMore = true; + + final result = await _getOrdersUseCase.call( + token: 'Bearer $token', + page: intent.page, + limit: intent.limit, + ); + + if (isClosed) return; + switch (result) { + case SuccessApiResult(): + final data = result.data; + _hasMore = data.metadata != null && _page < data.metadata!.totalPages; + + emit( + state.copyWith( + orders: data.orders, + metadata: data.metadata, + ordersResource: Resource.success(data), + ), + ); + break; + + case ErrorApiResult(): + emit(state.copyWith(ordersResource: Resource.error(result.error))); + break; + } + } + + Future _loadMore() async { + if (!_hasMore || state.isLoadingMore) return; + + emit(state.copyWith(isLoadingMore: true)); + + final token = await _authStorage.getToken(); + if (token == null || token.isEmpty) { + emit(state.copyWith(isLoadingMore: false)); + return; + } + + _page++; + + final result = await _getOrdersUseCase.call( + token: 'Bearer $token', + page: _page, + ); + + if (isClosed) return; + + switch (result) { + case SuccessApiResult(): + emit( + state.copyWith( + orders: [...state.orders, ...result.data.orders], + metadata: result.data.metadata, + isLoadingMore: false, + ), + ); + break; + + case ErrorApiResult(): + emit(state.copyWith(isLoadingMore: false)); + break; + } + } + + void _filterCompleted() { + final filtered = state.orders.where((e) => e.isDelivered == true).toList(); + + emit(state.copyWith(orders: filtered)); + } + + void _filterCancelled() { + final filtered = state.orders.where((e) => e.state == 'cancelled').toList(); + emit(state.copyWith(orders: filtered)); + } +} diff --git a/lib/features/my_orders/presentation/manager/my_orders_intent.dart b/lib/features/my_orders/presentation/manager/my_orders_intent.dart new file mode 100644 index 0000000..ddcd989 --- /dev/null +++ b/lib/features/my_orders/presentation/manager/my_orders_intent.dart @@ -0,0 +1,22 @@ +import 'package:tracking_app/features/my_orders/domain/models/order_entity.dart'; + +sealed class MyOrdersIntent {} + +class GetMyOrdersIntent extends MyOrdersIntent { + final int page; + final int limit; + + GetMyOrdersIntent({this.page = 1, this.limit = 10}); +} + +class LoadMoreOrdersIntent extends MyOrdersIntent {} + +class OpenOrderDetailsIntent extends MyOrdersIntent { + final OrderEntity order; + + OpenOrderDetailsIntent(this.order); +} + +class FilterCompletedOrdersIntent extends MyOrdersIntent {} + +class FilterCancelledOrdersIntent extends MyOrdersIntent {} diff --git a/lib/features/my_orders/presentation/manager/my_orders_state.dart b/lib/features/my_orders/presentation/manager/my_orders_state.dart new file mode 100644 index 0000000..9401a4d --- /dev/null +++ b/lib/features/my_orders/presentation/manager/my_orders_state.dart @@ -0,0 +1,35 @@ +import 'package:tracking_app/app/config/base_state/base_state.dart'; +import 'package:tracking_app/features/my_orders/domain/models/meta_data_entity.dart'; +import 'package:tracking_app/features/my_orders/domain/models/order_entity.dart'; + +class MyOrdersState { + final Resource ordersResource; + final List orders; + final MetadataEntity? metadata; + final OrderEntity? selectedOrder; + final bool isLoadingMore; + + MyOrdersState({ + Resource? ordersResource, + this.orders = const [], + this.metadata, + this.selectedOrder, + this.isLoadingMore = false, + }) : ordersResource = ordersResource ?? Resource.initial(); + + MyOrdersState copyWith({ + Resource? ordersResource, + List? orders, + MetadataEntity? metadata, + OrderEntity? selectedOrder, + bool? isLoadingMore, + }) { + return MyOrdersState( + ordersResource: ordersResource ?? this.ordersResource, + orders: orders ?? this.orders, + metadata: metadata ?? this.metadata, + selectedOrder: selectedOrder ?? this.selectedOrder, + isLoadingMore: isLoadingMore ?? this.isLoadingMore, + ); + } +} diff --git a/lib/features/my_orders/presentation/pages/my_orders_page.dart b/lib/features/my_orders/presentation/pages/my_orders_page.dart new file mode 100644 index 0000000..cf578e8 --- /dev/null +++ b/lib/features/my_orders/presentation/pages/my_orders_page.dart @@ -0,0 +1,36 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:tracking_app/app/config/di/di.dart'; +import 'package:tracking_app/features/my_orders/presentation/manager/my_orders_cubit.dart'; +import 'package:tracking_app/features/my_orders/presentation/manager/my_orders_intent.dart'; +import 'package:tracking_app/features/my_orders/presentation/widgets/my_orders_page_body.dart'; + +class MyOrdersPage extends StatelessWidget { + const MyOrdersPage({super.key}); + + @override + Widget build(BuildContext context) { + return BlocProvider( + create: (context) => + getIt() + ..doIntent(GetMyOrdersIntent(page: 1, limit: 10)), + child: Scaffold( + appBar: AppBar( + backgroundColor: Colors.white, + elevation: 0, + title: const Text( + "My orders", + style: TextStyle( + color: Colors.black, + fontSize: 18, + fontWeight: FontWeight.bold, + ), + ), + centerTitle: false, + ), + backgroundColor: Colors.white, + body: const MyOrdersPageBody(), + ), + ); + } +} diff --git a/lib/features/my_orders/presentation/pages/order_details_page.dart b/lib/features/my_orders/presentation/pages/order_details_page.dart new file mode 100644 index 0000000..f9ea715 --- /dev/null +++ b/lib/features/my_orders/presentation/pages/order_details_page.dart @@ -0,0 +1,107 @@ +import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; +import 'package:tracking_app/app/core/ui_helper/color/colors.dart'; +import 'package:tracking_app/features/my_orders/domain/models/order_entity.dart'; +import 'package:tracking_app/features/my_orders/presentation/widgets/address_title.dart'; +import 'package:tracking_app/features/my_orders/presentation/widgets/order_item_tile.dart'; +import 'package:tracking_app/features/my_orders/presentation/widgets/section_lable.dart'; +import 'package:tracking_app/features/my_orders/presentation/widgets/summary_row.dart'; + +class OrderDetailsPage extends StatelessWidget { + final OrderEntity order; + + const OrderDetailsPage({super.key, required this.order}); + + @override + Widget build(BuildContext context) { + final isCancelled = order.state.toLowerCase() == 'cancelled'; + + return Scaffold( + backgroundColor: Colors.white, + appBar: AppBar( + backgroundColor: Colors.white, + elevation: 0, + leading: IconButton( + icon: const Icon(Icons.arrow_back_ios, color: Colors.black, size: 20), + onPressed: () => context.pop(), + ), + title: const Text( + "Order details", + style: TextStyle( + color: Colors.black, + fontSize: 18, + fontWeight: FontWeight.bold, + ), + ), + centerTitle: false, + ), + body: SingleChildScrollView( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Row( + children: [ + Icon( + isCancelled ? Icons.cancel : Icons.check_circle, + size: 20, + color: isCancelled ? AppColors.red : AppColors.green, + ), + const SizedBox(width: 8), + Text( + order.state, + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w600, + color: isCancelled ? AppColors.red : AppColors.green, + ), + ), + ], + ), + Text( + "# ${order.orderNumber}", + style: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + color: AppColors.blackColor, + ), + ), + ], + ), + const SizedBox(height: 24), + const SectionLabel(label: "Pickup address"), + const SizedBox(height: 8), + AddressTile( + title: order.store?.name ?? "Unknown Store", + address: order.store?.address ?? "No Address Provided", + image: order.store?.image ?? "https://i.pravatar.cc/150?u=s1", + isStore: true, + ), + const SizedBox(height: 20), + const SectionLabel(label: "User address"), + const SizedBox(height: 8), + AddressTile( + title: "${order.user.firstName} ${order.user.lastName}", + address: order.address.isNotEmpty + ? order.address + : "No Address Provided", + image: order.user.photo, + isStore: false, + ), + const SizedBox(height: 24), + const SectionLabel(label: "Order details"), + const SizedBox(height: 12), + ...order.items.map((item) => OrderItemTile(item: item)), + const SizedBox(height: 12), + SummaryRow(label: "Total", value: "Egp ${order.totalPrice}"), + const SizedBox(height: 12), + SummaryRow(label: "Payment method", value: order.paymentType), + ], + ), + ), + ); + } +} diff --git a/lib/features/my_orders/presentation/widgets/address_title.dart b/lib/features/my_orders/presentation/widgets/address_title.dart new file mode 100644 index 0000000..fc249bc --- /dev/null +++ b/lib/features/my_orders/presentation/widgets/address_title.dart @@ -0,0 +1,82 @@ +import 'package:flutter/material.dart'; +import 'package:tracking_app/app/core/ui_helper/color/colors.dart'; + +class AddressTile extends StatelessWidget { + final String title; + final String address; + final String image; + final bool isStore; + + const AddressTile({ + super.key, + required this.title, + required this.address, + required this.image, + required this.isStore, + }); + + @override + Widget build(BuildContext context) { + return Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(12), + border: Border.all(color: Colors.grey.shade100), + ), + child: Row( + children: [ + Container( + width: 40, + height: 40, + decoration: BoxDecoration( + shape: BoxShape.circle, + image: DecorationImage( + image: NetworkImage(image), + fit: BoxFit.cover, + ), + ), + ), + const SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + title, + style: const TextStyle( + fontSize: 13, + fontWeight: FontWeight.w600, + color: AppColors.blackColor, + ), + ), + const SizedBox(height: 4), + Row( + children: [ + const Icon( + Icons.location_on_outlined, + size: 14, + color: AppColors.grey2, + ), + const SizedBox(width: 4), + Expanded( + child: Text( + address, + style: const TextStyle( + fontSize: 12, + color: AppColors.grey2, + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ), + ], + ), + ], + ), + ), + ], + ), + ); + } +} diff --git a/lib/features/my_orders/presentation/widgets/my_orders_page_body.dart b/lib/features/my_orders/presentation/widgets/my_orders_page_body.dart new file mode 100644 index 0000000..f672487 --- /dev/null +++ b/lib/features/my_orders/presentation/widgets/my_orders_page_body.dart @@ -0,0 +1,48 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:tracking_app/app/core/widgets/show_snak_bar.dart'; +import 'package:tracking_app/features/my_orders/presentation/manager/my_orders_cubit.dart'; +import 'package:tracking_app/features/my_orders/presentation/manager/my_orders_state.dart'; +import 'package:tracking_app/app/core/ui_helper/color/colors.dart'; +import 'package:tracking_app/features/my_orders/presentation/widgets/orders_filters_row.dart'; +import 'package:tracking_app/features/my_orders/presentation/widgets/orders_list_view.dart'; + +class MyOrdersPageBody extends StatelessWidget { + const MyOrdersPageBody({super.key}); + + @override + Widget build(BuildContext context) { + return BlocListener( + listenWhen: (prev, curr) => prev.ordersResource != curr.ordersResource, + listener: (context, state) { + if (state.ordersResource.isError == true) { + showAppSnackbar( + context, + state.ordersResource.error ?? "Failed to load orders", + ); + } + }, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const SizedBox(height: 12), + const OrdersFiltersRow(), + const SizedBox(height: 20), + const Padding( + padding: EdgeInsets.symmetric(horizontal: 16), + child: Text( + "Recent orders", + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + color: AppColors.blackColor, + ), + ), + ), + const SizedBox(height: 12), + const Expanded(child: OrdersListView()), + ], + ), + ); + } +} diff --git a/lib/features/my_orders/presentation/widgets/order_card.dart b/lib/features/my_orders/presentation/widgets/order_card.dart new file mode 100644 index 0000000..9754d3d --- /dev/null +++ b/lib/features/my_orders/presentation/widgets/order_card.dart @@ -0,0 +1,202 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:tracking_app/app/core/ui_helper/color/colors.dart'; +import 'package:tracking_app/features/my_orders/domain/models/order_entity.dart'; +import 'package:tracking_app/features/my_orders/presentation/widgets/address_title.dart'; +import 'package:tracking_app/features/my_orders/presentation/widgets/section_lable.dart'; +import 'package:tracking_app/features/track_order/presentation/manager/cubit/track_order_cubit.dart'; + +class OrderCard extends StatelessWidget { + final OrderEntity order; + final VoidCallback onTap; + + const OrderCard({super.key, required this.order, required this.onTap}); + + @override + Widget build(BuildContext context) { + final isPending = order.state.toLowerCase() == 'pending'; + final isCancelled = order.state.toLowerCase() == 'cancelled'; + + return GestureDetector( + onTap: onTap, + child: Container( + margin: const EdgeInsets.only(bottom: 16), + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: const Color(0xFFF9F9F9), + borderRadius: BorderRadius.circular(16), + border: Border.all(color: Colors.grey.shade200), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // ── Header row ── + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + const Text( + "Flower order", + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.w600, + color: AppColors.grey, + ), + ), + Row( + children: [ + Icon( + isCancelled ? Icons.cancel : Icons.check_circle, + size: 18, + color: _statusColor, + ), + const SizedBox(width: 4), + Text( + order.state, + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.w600, + color: _statusColor, + ), + ), + ], + ), + Text( + "# ${order.orderNumber}", + style: const TextStyle( + fontSize: 14, + fontWeight: FontWeight.bold, + color: AppColors.blackColor, + ), + ), + ], + ), + + const SizedBox(height: 12), + + // ── Pickup address ── + SectionLabel(label: "Pickup address"), + const SizedBox(height: 8), + AddressTile( + title: order.store?.name ?? "Unknown Store", + address: order.store?.address ?? "No Address Provided", + image: + order.store?.image ?? + "https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcT6-k6E9vG_c9B_I0m_K-7J1f8e6C9F5G1g5A&s", + isStore: true, + ), + + const SizedBox(height: 12), + + // ── User address ── + SectionLabel(label: "User address"), + const SizedBox(height: 8), + AddressTile( + title: "${order.user.firstName} ${order.user.lastName}", + address: order.address.isNotEmpty + ? order.address + : "No Address Provided", + image: order.user.photo, + isStore: false, + ), + + // ── Price + Accept / Reject buttons (only for pending) ── + if (isPending) ...[ + const SizedBox(height: 16), + BlocBuilder( + builder: (context, state) { + return Row( + children: [ + // Price + Text( + "EGP ${order.totalPrice}", + style: const TextStyle( + fontSize: 14, + fontWeight: FontWeight.bold, + color: AppColors.blackColor, + ), + ), + const Spacer(), + // Reject button + SizedBox( + height: 36, + child: OutlinedButton( + onPressed: state.isLoading + ? null + : () { + context + .read() + .updateOrderStatus(order.id, 'Cancelled'); + }, + style: OutlinedButton.styleFrom( + foregroundColor: AppColors.pink, + side: const BorderSide( + color: AppColors.pink, + width: 1.5, + ), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(20), + ), + padding: const EdgeInsets.symmetric(horizontal: 20), + ), + child: const Text( + "Reject", + style: TextStyle( + fontSize: 13, + fontWeight: FontWeight.w600, + ), + ), + ), + ), + const SizedBox(width: 8), + // Accept button + SizedBox( + height: 36, + child: ElevatedButton( + onPressed: state.isLoading + ? null + : () { + context + .read() + .updateOrderStatus(order.id, 'Accepted'); + }, + style: ElevatedButton.styleFrom( + backgroundColor: AppColors.pink, + foregroundColor: Colors.white, + elevation: 0, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(20), + ), + padding: const EdgeInsets.symmetric(horizontal: 20), + ), + child: const Text( + "Accept", + style: TextStyle( + fontSize: 13, + fontWeight: FontWeight.w600, + ), + ), + ), + ), + ], + ); + }, + ), + ], + ], + ), + ), + ); + } + + /// Returns the appropriate color for the current order status. + Color get _statusColor { + switch (order.state.toLowerCase()) { + case 'pending': + return AppColors.pink; + case 'cancelled': + return AppColors.red; + default: + return AppColors.green; + } + } +} diff --git a/lib/features/my_orders/presentation/widgets/order_item_tile.dart b/lib/features/my_orders/presentation/widgets/order_item_tile.dart new file mode 100644 index 0000000..8448837 --- /dev/null +++ b/lib/features/my_orders/presentation/widgets/order_item_tile.dart @@ -0,0 +1,72 @@ +import 'package:flutter/material.dart'; +import 'package:tracking_app/app/core/ui_helper/color/colors.dart'; +import 'package:tracking_app/features/my_orders/domain/models/order_item_entity.dart'; + +class OrderItemTile extends StatelessWidget { + final OrderItemEntity item; + + const OrderItemTile({super.key, required this.item}); + + @override + Widget build(BuildContext context) { + return Container( + margin: const EdgeInsets.only(bottom: 12), + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(12), + border: Border.all(color: Colors.grey.shade100), + ), + child: Row( + children: [ + Container( + width: 50, + height: 50, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(8), + image: DecorationImage( + image: NetworkImage(item.product.image), + fit: BoxFit.cover, + ), + ), + ), + const SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + item.product.title, + style: const TextStyle( + fontSize: 13, + fontWeight: FontWeight.w600, + color: AppColors.blackColor, + ), + maxLines: 2, + overflow: TextOverflow.ellipsis, + ), + const SizedBox(height: 4), + Text( + "EGP ${item.price}", + style: const TextStyle( + fontSize: 14, + fontWeight: FontWeight.bold, + color: AppColors.blackColor, + ), + ), + ], + ), + ), + Text( + "X${item.quantity}", + style: const TextStyle( + fontSize: 13, + fontWeight: FontWeight.bold, + color: AppColors.red, + ), + ), + ], + ), + ); + } +} diff --git a/lib/features/my_orders/presentation/widgets/orders_filters_row.dart b/lib/features/my_orders/presentation/widgets/orders_filters_row.dart new file mode 100644 index 0000000..7b6a160 --- /dev/null +++ b/lib/features/my_orders/presentation/widgets/orders_filters_row.dart @@ -0,0 +1,49 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:tracking_app/app/core/ui_helper/color/colors.dart'; +import 'package:tracking_app/features/my_orders/presentation/manager/my_orders_cubit.dart'; +import 'package:tracking_app/features/my_orders/presentation/manager/my_orders_intent.dart'; +import 'package:tracking_app/features/my_orders/presentation/manager/my_orders_state.dart'; +import 'package:tracking_app/features/my_orders/presentation/widgets/summary_card.dart'; + +class OrdersFiltersRow extends StatelessWidget { + const OrdersFiltersRow({super.key}); + + @override + Widget build(BuildContext context) { + final cubit = context.read(); + + return BlocBuilder( + builder: (context, state) { + final metadata = state.metadata; + + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Row( + children: [ + Expanded( + child: SummaryCard( + title: "Cancelled", + count: "${metadata?.cancelledCount ?? 0}", + color: AppColors.red, + icon: Icons.cancel_outlined, + onTap: () => cubit.doIntent(FilterCancelledOrdersIntent()), + ), + ), + const SizedBox(width: 16), + Expanded( + child: SummaryCard( + title: "Completed", + count: "${metadata?.completedCount ?? 0}", + color: AppColors.green, + icon: Icons.check_circle_outline, + onTap: () => cubit.doIntent(FilterCompletedOrdersIntent()), + ), + ), + ], + ), + ); + }, + ); + } +} diff --git a/lib/features/my_orders/presentation/widgets/orders_list_view.dart b/lib/features/my_orders/presentation/widgets/orders_list_view.dart new file mode 100644 index 0000000..e9e034d --- /dev/null +++ b/lib/features/my_orders/presentation/widgets/orders_list_view.dart @@ -0,0 +1,52 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:go_router/go_router.dart'; +import 'package:tracking_app/app/core/router/route_names.dart'; +import 'package:tracking_app/features/my_orders/presentation/manager/my_orders_cubit.dart'; +import 'package:tracking_app/features/my_orders/presentation/manager/my_orders_intent.dart'; +import 'package:tracking_app/features/my_orders/presentation/manager/my_orders_state.dart'; +import 'package:tracking_app/features/my_orders/presentation/widgets/order_card.dart'; + +class OrdersListView extends StatelessWidget { + const OrdersListView({super.key}); + + @override + Widget build(BuildContext context) { + return BlocBuilder( + builder: (context, state) { + if (state.ordersResource.isLoading) { + return const Center(child: CircularProgressIndicator()); + } + + if (state.orders.isEmpty) { + return const Center(child: Text("No orders found")); + } + + return ListView.builder( + padding: const EdgeInsets.symmetric(horizontal: 16), + itemCount: state.orders.length + (state.isLoadingMore ? 1 : 0), + itemBuilder: (context, index) { + if (index == state.orders.length) { + return const Padding( + padding: EdgeInsets.all(12), + child: Center(child: CircularProgressIndicator()), + ); + } + + final order = state.orders[index]; + + return OrderCard( + order: order, + onTap: () { + context.read().doIntent( + OpenOrderDetailsIntent(order), + ); + context.push(RouteNames.orderDetails, extra: order); + }, + ); + }, + ); + }, + ); + } +} diff --git a/lib/features/my_orders/presentation/widgets/section_lable.dart b/lib/features/my_orders/presentation/widgets/section_lable.dart new file mode 100644 index 0000000..6805822 --- /dev/null +++ b/lib/features/my_orders/presentation/widgets/section_lable.dart @@ -0,0 +1,20 @@ +import 'package:flutter/material.dart'; +import 'package:tracking_app/app/core/ui_helper/color/colors.dart'; + +class SectionLabel extends StatelessWidget { + final String label; + + const SectionLabel({super.key, required this.label}); + + @override + Widget build(BuildContext context) { + return Text( + label, + style: const TextStyle( + fontSize: 12, + color: AppColors.grey2, + fontWeight: FontWeight.w500, + ), + ); + } +} diff --git a/lib/features/my_orders/presentation/widgets/summary_card.dart b/lib/features/my_orders/presentation/widgets/summary_card.dart new file mode 100644 index 0000000..b697156 --- /dev/null +++ b/lib/features/my_orders/presentation/widgets/summary_card.dart @@ -0,0 +1,61 @@ +import 'package:flutter/widgets.dart'; +import 'package:tracking_app/app/core/ui_helper/color/colors.dart'; + +class SummaryCard extends StatelessWidget { + final String title; + final String count; + final Color color; + final IconData icon; + final VoidCallback onTap; + + const SummaryCard({ + super.key, + required this.title, + required this.count, + required this.color, + required this.icon, + required this.onTap, + }); + + @override + Widget build(BuildContext context) { + return GestureDetector( + onTap: onTap, + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), + decoration: BoxDecoration( + color: const Color(0xFFFDF0F3), + borderRadius: BorderRadius.circular(12), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + count, + style: const TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + color: AppColors.blackColor, + ), + ), + const SizedBox(height: 4), + Row( + children: [ + Icon(icon, size: 16, color: color), + const SizedBox(width: 6), + Text( + title, + style: TextStyle( + fontSize: 13, + color: color, + fontWeight: FontWeight.w600, + ), + ), + ], + ), + ], + ), + ), + ); + } +} diff --git a/lib/features/my_orders/presentation/widgets/summary_row.dart b/lib/features/my_orders/presentation/widgets/summary_row.dart new file mode 100644 index 0000000..9c0d692 --- /dev/null +++ b/lib/features/my_orders/presentation/widgets/summary_row.dart @@ -0,0 +1,42 @@ +import 'package:flutter/material.dart'; +import 'package:tracking_app/app/core/ui_helper/color/colors.dart'; + +class SummaryRow extends StatelessWidget { + final String label; + final String value; + + const SummaryRow({super.key, required this.label, required this.value}); + + @override + Widget build(BuildContext context) { + return Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: const Color(0xFFF9F9F9), + borderRadius: BorderRadius.circular(12), + border: Border.all(color: Colors.grey.shade100), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + label, + style: const TextStyle( + fontSize: 14, + fontWeight: FontWeight.bold, + color: AppColors.blackColor, + ), + ), + Text( + value, + style: const TextStyle( + fontSize: 14, + color: AppColors.grey2, + fontWeight: FontWeight.w500, + ), + ), + ], + ), + ); + } +} diff --git a/lib/features/profile/api/profile_lacal_datasource_imp.dart b/lib/features/profile/api/profile_lacal_datasource_imp.dart new file mode 100644 index 0000000..08154c2 --- /dev/null +++ b/lib/features/profile/api/profile_lacal_datasource_imp.dart @@ -0,0 +1,24 @@ +import 'dart:convert'; +import 'package:injectable/injectable.dart'; +import 'package:tracking_app/app/config/auth_storage/auth_storage.dart'; +import 'package:tracking_app/features/profile/data/datasorce/profile_lacal_datasource.dart'; +import 'package:tracking_app/features/profile/data/models/driver_model.dart'; + +@LazySingleton(as: ProfileLocalDataSource) +class ProfileLocalDataSourceImpl implements ProfileLocalDataSource { + final AuthStorage storage; + + ProfileLocalDataSourceImpl(this.storage); + + @override + Future saveUser(DriverModel user) async { + await storage.saveUserJson(jsonEncode(user.toJson())); + } + + @override + Future getUser() async { + final json = await storage.getUserJson(); + if (json == null) return null; + return DriverModel.fromJson(jsonDecode(json)); + } +} diff --git a/lib/features/profile/api/profile_remote_datasource_imp.dart b/lib/features/profile/api/profile_remote_datasource_imp.dart new file mode 100644 index 0000000..87ccf5c --- /dev/null +++ b/lib/features/profile/api/profile_remote_datasource_imp.dart @@ -0,0 +1,41 @@ +import 'dart:io'; +import 'package:injectable/injectable.dart'; +import 'package:tracking_app/app/core/api_manger/api_client.dart'; +import 'package:tracking_app/app/core/network/api_result.dart'; +import 'package:tracking_app/app/core/network/safe_api_call.dart'; +import 'package:tracking_app/features/profile/data/datasorce/profile_remote_datasource.dart'; +import 'package:tracking_app/features/profile/data/models/requests/edit_profile_request.dart'; +import 'package:tracking_app/features/profile/data/models/responses/edit_profile_response.dart'; + +@Injectable(as: ProfileRemoteDatasource) +class ProfileRemoteDatasourceImp extends ProfileRemoteDatasource { + final ApiClient apiClient; + ProfileRemoteDatasourceImp(this.apiClient); + + @override + Future> editProfile({ + required String token, + EditProfileRequest? request, + }) { + return safeApiCall( + call: () => apiClient.editProfile(token: token, request: request!), + ); + } + + @override + Future> uploadPhoto({ + required String token, + required File photo, + }) { + return safeApiCall( + call: () => apiClient.uploadPhoto(token: token, photo: photo), + ); + } + + @override + Future> getProfile({required String token}) { + return safeApiCall( + call: () => apiClient.getProfile(token: token), + ); + } +} diff --git a/lib/features/profile/data/datasorce/profile_lacal_datasource.dart b/lib/features/profile/data/datasorce/profile_lacal_datasource.dart new file mode 100644 index 0000000..eee316b --- /dev/null +++ b/lib/features/profile/data/datasorce/profile_lacal_datasource.dart @@ -0,0 +1,6 @@ +import 'package:tracking_app/features/profile/data/models/driver_model.dart'; + +abstract class ProfileLocalDataSource { + Future saveUser(DriverModel user); + Future getUser(); +} diff --git a/lib/features/profile/data/datasorce/profile_remote_datasource.dart b/lib/features/profile/data/datasorce/profile_remote_datasource.dart new file mode 100644 index 0000000..7df383f --- /dev/null +++ b/lib/features/profile/data/datasorce/profile_remote_datasource.dart @@ -0,0 +1,18 @@ +import 'dart:io'; +import 'package:tracking_app/app/core/network/api_result.dart'; +import 'package:tracking_app/features/profile/data/models/requests/edit_profile_request.dart'; +import 'package:tracking_app/features/profile/data/models/responses/edit_profile_response.dart'; + +abstract class ProfileRemoteDatasource { + Future> editProfile({ + required String token, + EditProfileRequest? request, + }); + + Future> getProfile({required String token}); + + Future> uploadPhoto({ + required String token, + required File photo, + }); +} diff --git a/lib/features/profile/data/models/driver_model.dart b/lib/features/profile/data/models/driver_model.dart new file mode 100644 index 0000000..b0ae28a --- /dev/null +++ b/lib/features/profile/data/models/driver_model.dart @@ -0,0 +1,83 @@ +import 'package:json_annotation/json_annotation.dart'; + +part 'driver_model.g.dart'; + +@JsonSerializable() +class DriverModel { + @JsonKey(name: "_id") + final String? Id; + @JsonKey(name: "country") + final String? country; + @JsonKey(name: "firstName") + final String? firstName; + @JsonKey(name: "lastName") + final String? lastName; + @JsonKey(name: "vehicleType") + final String? vehicleType; + @JsonKey(name: "vehicleNumber") + final String? vehicleNumber; + @JsonKey(name: "vehicleLicense") + final String? vehicleLicense; + @JsonKey(name: "NID") + final String? NID; + @JsonKey(name: "NIDImg") + final String? NIDImg; + @JsonKey(name: "email") + final String? email; + @JsonKey(name: "password") + final String? password; + @JsonKey(name: "gender") + final String? gender; + @JsonKey(name: "phone") + final String? phone; + @JsonKey(name: "photo") + final String? photo; + @JsonKey(name: "role") + final String? role; + @JsonKey(name: "createdAt") + final String? createdAt; + + DriverModel({ + this.Id, + this.country, + this.firstName, + this.lastName, + this.vehicleType, + this.vehicleNumber, + this.vehicleLicense, + this.NID, + this.NIDImg, + this.email, + this.password, + this.gender, + this.phone, + this.photo, + this.role, + this.createdAt, + }); + + factory DriverModel.fromJson(Map json) { + return _$DriverModelFromJson(json); + } + + Map toJson() { + return _$DriverModelToJson(this); + } + + static DriverModel fromEditProfileUser(DriverModel user) { + return DriverModel( + Id: user.Id, + country: user.country, + firstName: user.firstName, + lastName: user.lastName, + vehicleType: user.vehicleType, + vehicleNumber: user.vehicleNumber, + vehicleLicense: user.vehicleLicense, + NID: user.NID, + NIDImg: user.NIDImg, + email: user.email, + phone: user.phone, + password: null, + ); + } +} diff --git a/lib/features/profile/data/models/requests/edit_profile_request.dart b/lib/features/profile/data/models/requests/edit_profile_request.dart new file mode 100644 index 0000000..d25ec7f --- /dev/null +++ b/lib/features/profile/data/models/requests/edit_profile_request.dart @@ -0,0 +1,42 @@ +import 'package:json_annotation/json_annotation.dart'; + +part 'edit_profile_request.g.dart'; + +@JsonSerializable(includeIfNull: false) +class EditProfileRequest { + @JsonKey(name: "firstName") + final String? firstName; + + @JsonKey(name: "lastName") + final String? lastName; + + @JsonKey(name: "email") + final String? email; + + @JsonKey(name: "phone") + final String? phone; + + @JsonKey(name: "vehicleType") + final String? vehicleType; + + @JsonKey(name: "vehicleNumber") + final String? vehicleNumber; + + @JsonKey(name: "vehicleLicense") + final String? vehicleLicense; + + EditProfileRequest({ + this.firstName, + this.lastName, + this.email, + this.phone, + this.vehicleType, + this.vehicleNumber, + this.vehicleLicense, + }); + + factory EditProfileRequest.fromJson(Map json) => + _$EditProfileRequestFromJson(json); + + Map toJson() => _$EditProfileRequestToJson(this); +} diff --git a/lib/features/profile/data/models/requests/edit_profile_request.g.dart b/lib/features/profile/data/models/requests/edit_profile_request.g.dart new file mode 100644 index 0000000..b30edf7 --- /dev/null +++ b/lib/features/profile/data/models/requests/edit_profile_request.g.dart @@ -0,0 +1,29 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'edit_profile_request.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +EditProfileRequest _$EditProfileRequestFromJson(Map json) => + EditProfileRequest( + firstName: json['firstName'] as String?, + lastName: json['lastName'] as String?, + email: json['email'] as String?, + phone: json['phone'] as String?, + vehicleType: json['vehicleType'] as String?, + vehicleNumber: json['vehicleNumber'] as String?, + vehicleLicense: json['vehicleLicense'] as String?, + ); + +Map _$EditProfileRequestToJson(EditProfileRequest instance) => + { + 'firstName': ?instance.firstName, + 'lastName': ?instance.lastName, + 'email': ?instance.email, + 'phone': ?instance.phone, + 'vehicleType': ?instance.vehicleType, + 'vehicleNumber': ?instance.vehicleNumber, + 'vehicleLicense': ?instance.vehicleLicense, + }; diff --git a/lib/features/profile/data/models/responses/edit_profile_response.dart b/lib/features/profile/data/models/responses/edit_profile_response.dart new file mode 100644 index 0000000..c2f6dbd --- /dev/null +++ b/lib/features/profile/data/models/responses/edit_profile_response.dart @@ -0,0 +1,22 @@ +import 'package:json_annotation/json_annotation.dart'; +import 'package:tracking_app/features/profile/data/models/driver_model.dart'; + +part 'edit_profile_response.g.dart'; + +@JsonSerializable() +class EditProfileResponse { + @JsonKey(name: "message") + final String? message; + @JsonKey(name: "driver") + final DriverModel? driver; + + EditProfileResponse({this.message, this.driver}); + + factory EditProfileResponse.fromJson(Map json) { + return _$EditProfileResponseFromJson(json); + } + + Map toJson() { + return _$EditProfileResponseToJson(this); + } +} diff --git a/lib/features/profile/data/models/responses/edit_profile_response.g.dart b/lib/features/profile/data/models/responses/edit_profile_response.g.dart new file mode 100644 index 0000000..aba1a56 --- /dev/null +++ b/lib/features/profile/data/models/responses/edit_profile_response.g.dart @@ -0,0 +1,19 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'edit_profile_response.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +EditProfileResponse _$EditProfileResponseFromJson(Map json) => + EditProfileResponse( + message: json['message'] as String?, + driver: json['driver'] == null + ? null + : DriverModel.fromJson(json['driver'] as Map), + ); + +Map _$EditProfileResponseToJson( + EditProfileResponse instance, +) => {'message': instance.message, 'driver': instance.driver}; diff --git a/lib/features/profile/data/repo/profile_repo_imp.dart b/lib/features/profile/data/repo/profile_repo_imp.dart new file mode 100644 index 0000000..b98863f --- /dev/null +++ b/lib/features/profile/data/repo/profile_repo_imp.dart @@ -0,0 +1,113 @@ +import 'dart:io'; +import 'package:injectable/injectable.dart'; +import 'package:tracking_app/app/core/network/api_result.dart'; +import 'package:tracking_app/features/profile/data/datasorce/profile_lacal_datasource.dart'; +import 'package:tracking_app/features/profile/data/datasorce/profile_remote_datasource.dart'; +import 'package:tracking_app/features/profile/data/models/driver_model.dart'; +import 'package:tracking_app/features/profile/data/models/requests/edit_profile_request.dart'; +import 'package:tracking_app/features/profile/data/models/responses/edit_profile_response.dart'; +import 'package:tracking_app/features/profile/domain/repo/profile_repo.dart'; + +@Injectable(as: ProfileRepo) +class ProfileRepoImpl implements ProfileRepo { + final ProfileRemoteDatasource profileDatasource; + final ProfileLocalDataSource localDataSource; + + ProfileRepoImpl(this.profileDatasource, this.localDataSource); + + @override + Future> getProfile({ + required String token, + }) async { + try { + // final localUser = await localDataSource.getUser(); + + // if (localUser != null) { + // return SuccessApiResult( + // data: EditProfileResponse.fromJson(localUser.toJson()), + // ); + // } + final result = await profileDatasource.getProfile(token: token); + + if (result is SuccessApiResult) { + final driver = DriverModel.fromJson(result.data.toJson()); + await localDataSource.saveUser(driver); + + return SuccessApiResult(data: result.data); + } else if (result is ErrorApiResult) { + return ErrorApiResult(error: result.error); + } else { + return ErrorApiResult(error: 'Unknown error'); + } + } catch (e) { + return ErrorApiResult(error: e.toString()); + } + } + + @override + Future> editProfile({ + required String token, + String? firstName, + String? lastName, + String? email, + String? phone, + String? vehicleType, + String? vehicleNumber, + String? vehicleLicense, + }) async { + try { + final result = await profileDatasource.editProfile( + token: token, + request: EditProfileRequest( + firstName: firstName, + lastName: lastName, + email: email, + phone: phone, + vehicleType: vehicleType, + vehicleNumber: vehicleNumber, + vehicleLicense: vehicleLicense, + ), + ); + + if (result is SuccessApiResult) { + final driver = DriverModel.fromJson(result.data.toJson()); + await localDataSource.saveUser(driver); + + return SuccessApiResult(data: result.data); + } else if (result is ErrorApiResult) { + return ErrorApiResult(error: result.error); + } else { + return ErrorApiResult(error: 'Unknown error'); + } + } catch (e) { + return ErrorApiResult(error: e.toString()); + } + } + + @override + Future> uploadPhoto({ + required String token, + required File photo, + }) async { + try { + final result = await profileDatasource.uploadPhoto( + token: token, + photo: photo, + ); + + if (result is SuccessApiResult) { + final driver = DriverModel.fromJson(result.data.toJson()); + + await localDataSource.saveUser(driver); + + return SuccessApiResult(data: result.data); + } else if (result is ErrorApiResult) { + return ErrorApiResult(error: result.error); + } else { + return ErrorApiResult(error: 'Unknown error'); + } + } catch (e) { + return ErrorApiResult(error: e.toString()); + } + } +} diff --git a/lib/features/profile/domain/repo/profile_repo.dart b/lib/features/profile/domain/repo/profile_repo.dart new file mode 100644 index 0000000..98183a3 --- /dev/null +++ b/lib/features/profile/domain/repo/profile_repo.dart @@ -0,0 +1,24 @@ +import 'dart:io'; + +import 'package:tracking_app/app/core/network/api_result.dart'; +import 'package:tracking_app/features/profile/data/models/responses/edit_profile_response.dart'; + +abstract class ProfileRepo { + Future> editProfile({ + required String token, + String? firstName, + String? lastName, + String? email, + String? phone, + String? vehicleType, + String? vehicleNumber, + String? vehicleLicense, + }); + + Future> uploadPhoto({ + required String token, + required File photo, + }); + + Future> getProfile({required String token}); +} diff --git a/lib/features/profile/domain/usecases/edit_profile_usecase.dart b/lib/features/profile/domain/usecases/edit_profile_usecase.dart new file mode 100644 index 0000000..0819144 --- /dev/null +++ b/lib/features/profile/domain/usecases/edit_profile_usecase.dart @@ -0,0 +1,33 @@ +import 'package:injectable/injectable.dart'; +import 'package:tracking_app/app/core/network/api_result.dart'; +import 'package:tracking_app/features/profile/data/models/responses/edit_profile_response.dart'; +import 'package:tracking_app/features/profile/domain/repo/profile_repo.dart'; + +@injectable +class EditProfileUseCase { + final ProfileRepo repository; + + EditProfileUseCase(this.repository); + + Future> call({ + required String token, + String? firstName, + String? lastName, + String? email, + String? phone, + String? vehicleType, + String? vehicleNumber, + String? vehicleLicense, + }) async { + return await repository.editProfile( + token: token, + firstName: firstName, + lastName: lastName, + email: email, + phone: phone, + vehicleType: vehicleType, + vehicleNumber: vehicleNumber, + vehicleLicense: vehicleLicense, + ); + } +} diff --git a/lib/features/profile/domain/usecases/get_profile_usecase.dart b/lib/features/profile/domain/usecases/get_profile_usecase.dart new file mode 100644 index 0000000..6ac8df0 --- /dev/null +++ b/lib/features/profile/domain/usecases/get_profile_usecase.dart @@ -0,0 +1,14 @@ +import 'package:injectable/injectable.dart'; +import 'package:tracking_app/app/core/network/api_result.dart'; +import 'package:tracking_app/features/profile/data/models/responses/edit_profile_response.dart'; +import 'package:tracking_app/features/profile/domain/repo/profile_repo.dart'; + +@injectable +class GetProfileUsecase { + final ProfileRepo repository; + GetProfileUsecase(this.repository); + + Future> call({required String token}) async { + return await repository.getProfile(token: token); + } +} diff --git a/lib/features/profile/domain/usecases/upload_profile_photo_usecase.dart b/lib/features/profile/domain/usecases/upload_profile_photo_usecase.dart new file mode 100644 index 0000000..79ef804 --- /dev/null +++ b/lib/features/profile/domain/usecases/upload_profile_photo_usecase.dart @@ -0,0 +1,19 @@ +import 'dart:io'; +import 'package:injectable/injectable.dart'; +import 'package:tracking_app/app/core/network/api_result.dart'; +import 'package:tracking_app/features/profile/data/models/responses/edit_profile_response.dart'; +import 'package:tracking_app/features/profile/domain/repo/profile_repo.dart'; + +@injectable +class UploadProfilePhotoUseCase { + final ProfileRepo repository; + + UploadProfilePhotoUseCase(this.repository); + + Future> call({ + required String token, + required File photo, + }) async { + return await repository.uploadPhoto(token: token, photo: photo); + } +} diff --git a/lib/features/profile/presentation/managers/profile_cubit.dart b/lib/features/profile/presentation/managers/profile_cubit.dart new file mode 100644 index 0000000..e9da0a9 --- /dev/null +++ b/lib/features/profile/presentation/managers/profile_cubit.dart @@ -0,0 +1,216 @@ +import 'dart:convert'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:injectable/injectable.dart'; +import 'package:tracking_app/app/config/auth_storage/auth_storage.dart'; +import 'package:tracking_app/app/core/network/api_result.dart'; +import 'package:tracking_app/app/config/base_state/base_state.dart'; +import 'package:tracking_app/features/profile/data/models/driver_model.dart'; +import 'package:tracking_app/features/profile/data/models/responses/edit_profile_response.dart'; +import 'package:tracking_app/features/profile/domain/usecases/edit_profile_usecase.dart'; +import 'package:tracking_app/features/profile/domain/usecases/upload_profile_photo_usecase.dart'; +import 'package:tracking_app/features/profile/domain/usecases/get_profile_usecase.dart'; +import 'profile_intent.dart'; +import 'profile_state.dart'; + +@injectable +class ProfileCubit extends Cubit { + final EditProfileUseCase _editProfileUseCase; + final UploadProfilePhotoUseCase _uploadPhotoUseCase; + final GetProfileUsecase _getProfileUsecase; + final AuthStorage _authStorage; + + ProfileCubit( + this._editProfileUseCase, + this._uploadPhotoUseCase, + this._getProfileUsecase, + this._authStorage, + ) : super(ProfileState()) { + _initialize(); + } + + Future _initialize() async { + await _loadUserFromLocal(); + await _getProfile(); + } + + Future _loadUserFromLocal() async { + final userJson = await _authStorage.getUserJson(); + + if (userJson != null) { + final driver = DriverModel.fromJson(jsonDecode(userJson)); + emit(state.copyWith(driver: driver)); + } + } + + void doIntent(ProfileIntent intent) { + switch (intent.runtimeType) { + case GetProfileIntent: + _getProfile(); + break; + case PerformEditProfile: + _editProfile(intent as PerformEditProfile); + break; + case SelectPhotoIntent: + _selectPhoto(intent as SelectPhotoIntent); + break; + case UploadSelectedPhotoIntent: + _uploadPhoto(); + break; + } + } + + Future _getProfile() async { + emit(state.copyWith(getProfileResource: Resource.loading())); + + final token = await _authStorage.getToken(); + + if (token == null || token.isEmpty) { + emit( + state.copyWith(getProfileResource: Resource.error("Token not found")), + ); + return; + } + + final result = await _getProfileUsecase.call(token: 'Bearer $token'); + + if (isClosed) return; + + switch (result) { + case SuccessApiResult(): + final user = result.data.driver; + + if (user != null) { + final driverModel = DriverModel.fromEditProfileUser(user); + + await _authStorage.saveUserJson(jsonEncode(driverModel.toJson())); + + emit( + state.copyWith( + driver: driverModel, + getProfileResource: Resource.success(result.data), + ), + ); + } + break; + + case ErrorApiResult(): + emit(state.copyWith(getProfileResource: Resource.error(result.error))); + break; + } + } + + Future _editProfile(PerformEditProfile intent) async { + emit(state.copyWith(editProfileResource: Resource.loading())); + + final token = await _authStorage.getToken(); + + if (token == null || token.isEmpty) { + emit( + state.copyWith(editProfileResource: Resource.error("Token not found")), + ); + return; + } + + if (intent.photo != null) { + final uploadResult = await _uploadPhotoUseCase.call( + token: 'Bearer $token', + photo: intent.photo!, + ); + + if (uploadResult is ErrorApiResult) { + emit( + state.copyWith( + editProfileResource: Resource.error(uploadResult.error), + ), + ); + return; + } + } + final result = await _editProfileUseCase.call( + token: 'Bearer $token', + firstName: intent.firstName, + lastName: intent.lastName, + email: intent.email, + phone: intent.phone, + vehicleType: intent.vehicleType, + vehicleNumber: intent.vehicleNumber, + vehicleLicense: intent.vehicleLicense, + ); + + if (isClosed) return; + + switch (result) { + case SuccessApiResult(): + final updatedUser = result.data.driver; + + if (updatedUser != null) { + final driverModel = DriverModel.fromEditProfileUser(updatedUser); + + await _authStorage.saveUserJson(jsonEncode(driverModel.toJson())); + + emit( + state.copyWith( + driver: driverModel, + editProfileResource: Resource.success(result.data), + clearSelectedPhoto: true, + ), + ); + } + break; + + case ErrorApiResult(): + emit(state.copyWith(editProfileResource: Resource.error(result.error))); + break; + } + } + + void _selectPhoto(SelectPhotoIntent intent) { + emit(state.copyWith(selectedPhoto: intent.photo)); + } + + Future _uploadPhoto() async { + if (state.selectedPhoto == null) return; + + emit(state.copyWith(uploadPhotoResource: Resource.loading())); + + final token = await _authStorage.getToken(); + + if (token == null || token.isEmpty) { + emit( + state.copyWith(uploadPhotoResource: Resource.error("Token not found")), + ); + return; + } + + final result = await _uploadPhotoUseCase.call( + token: 'Bearer $token', + photo: state.selectedPhoto!, + ); + + if (isClosed) return; + + switch (result) { + case SuccessApiResult(): + final updatedUser = result.data.driver; + + if (updatedUser != null) { + final driverModel = DriverModel.fromEditProfileUser(updatedUser); + + await _authStorage.saveUserJson(jsonEncode(driverModel.toJson())); + + emit( + state.copyWith( + driver: driverModel, + clearSelectedPhoto: true, + uploadPhotoResource: Resource.success(result.data), + ), + ); + } + break; + + case ErrorApiResult(): + emit(state.copyWith(uploadPhotoResource: Resource.error(result.error))); + break; + } + } +} diff --git a/lib/features/profile/presentation/managers/profile_intent.dart b/lib/features/profile/presentation/managers/profile_intent.dart new file mode 100644 index 0000000..1604c93 --- /dev/null +++ b/lib/features/profile/presentation/managers/profile_intent.dart @@ -0,0 +1,41 @@ +import 'dart:io'; + +sealed class ProfileIntent {} + +class GetProfileIntent extends ProfileIntent {} + +class PerformEditProfile extends ProfileIntent { + final String? firstName; + final String? lastName; + final String? email; + final String? phone; + final String? vehicleType; + final String? vehicleNumber; + final String? vehicleLicense; + final File? photo; + + PerformEditProfile({ + this.firstName, + this.lastName, + this.email, + this.phone, + this.vehicleType, + this.vehicleNumber, + this.vehicleLicense, + this.photo, + }); +} + +class SelectPhotoIntent extends ProfileIntent { + final File photo; + SelectPhotoIntent(this.photo); +} + +class UploadSelectedPhotoIntent extends ProfileIntent {} + +class SelectVehicleLicenseIntent extends ProfileIntent { + final File file; + SelectVehicleLicenseIntent(this.file); +} + +class UploadVehicleLicenseIntent extends ProfileIntent {} diff --git a/lib/features/profile/presentation/managers/profile_state.dart b/lib/features/profile/presentation/managers/profile_state.dart new file mode 100644 index 0000000..a9ed624 --- /dev/null +++ b/lib/features/profile/presentation/managers/profile_state.dart @@ -0,0 +1,48 @@ +import 'dart:io'; +import 'package:tracking_app/app/config/base_state/base_state.dart'; +import 'package:tracking_app/features/profile/data/models/driver_model.dart'; +import 'package:tracking_app/features/profile/data/models/responses/edit_profile_response.dart'; + +class ProfileState { + final Resource getProfileResource; + final Resource editProfileResource; + final Resource uploadPhotoResource; + final File? selectedPhoto; + final File? selectedVehicleLicense; + final DriverModel? driver; + + ProfileState({ + Resource? getProfileResource, + Resource? editProfileResource, + Resource? uploadPhotoResource, + this.selectedPhoto, + this.selectedVehicleLicense, + this.driver, + }) : getProfileResource = getProfileResource ?? Resource.initial(), + editProfileResource = editProfileResource ?? Resource.initial(), + uploadPhotoResource = uploadPhotoResource ?? Resource.initial(); + + ProfileState copyWith({ + Resource? getProfileResource, + Resource? editProfileResource, + Resource? uploadPhotoResource, + File? selectedPhoto, + File? selectedVehicleLicense, + bool clearSelectedPhoto = false, + bool clearVehicleLicense = false, + DriverModel? driver, + }) { + return ProfileState( + getProfileResource: getProfileResource ?? this.getProfileResource, + editProfileResource: editProfileResource ?? this.editProfileResource, + uploadPhotoResource: uploadPhotoResource ?? this.uploadPhotoResource, + selectedPhoto: clearSelectedPhoto + ? null + : (selectedPhoto ?? this.selectedPhoto), + selectedVehicleLicense: clearVehicleLicense + ? null + : (selectedVehicleLicense ?? this.selectedVehicleLicense), + driver: driver ?? this.driver, + ); + } +} diff --git a/lib/features/profile/presentation/pages/edit_driver_profile_page.dart b/lib/features/profile/presentation/pages/edit_driver_profile_page.dart new file mode 100644 index 0000000..53b4fb6 --- /dev/null +++ b/lib/features/profile/presentation/pages/edit_driver_profile_page.dart @@ -0,0 +1,33 @@ +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:tracking_app/app/config/di/di.dart'; +import 'package:tracking_app/features/profile/data/models/driver_model.dart'; +import 'package:tracking_app/features/profile/presentation/managers/profile_cubit.dart'; +import 'package:tracking_app/features/profile/presentation/widgets/edit_driver_profile_page_body.dart'; +import 'package:tracking_app/generated/locale_keys.g.dart'; + +class EditDriverProfilePage extends StatelessWidget { + final DriverModel? driver; + const EditDriverProfilePage({super.key, this.driver}); + + @override + Widget build(BuildContext context) { + return BlocProvider( + create: (context) => getIt(), + child: Scaffold( + appBar: AppBar( + title: Text( + LocaleKeys.editDriverProfile.tr(), + style: TextStyle(color: Colors.black), + ), + backgroundColor: Colors.white, + elevation: 0, + leading: const BackButton(color: Colors.black), + ), + backgroundColor: Colors.white, + body: EditDriverProfilePageBody(user: driver), + ), + ); + } +} diff --git a/lib/features/profile/presentation/pages/edit_vehicle_page.dart b/lib/features/profile/presentation/pages/edit_vehicle_page.dart new file mode 100644 index 0000000..ecb59bf --- /dev/null +++ b/lib/features/profile/presentation/pages/edit_vehicle_page.dart @@ -0,0 +1,33 @@ +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:tracking_app/app/config/di/di.dart'; +import 'package:tracking_app/features/profile/data/models/driver_model.dart'; +import 'package:tracking_app/features/profile/presentation/managers/profile_cubit.dart'; +import 'package:tracking_app/features/profile/presentation/widgets/edit_vehicle_page_body.dart'; +import 'package:tracking_app/generated/locale_keys.g.dart'; + +class EditVehiclePage extends StatelessWidget { + final DriverModel? driver; + const EditVehiclePage({super.key, this.driver}); + + @override + Widget build(BuildContext context) { + return BlocProvider( + create: (context) => getIt(), + child: Scaffold( + appBar: AppBar( + title: Text( + LocaleKeys.editVehicle.tr(), + style: TextStyle(color: Colors.black), + ), + backgroundColor: Colors.white, + elevation: 0, + leading: const BackButton(color: Colors.black), + ), + backgroundColor: Colors.white, + body: EditVehiclePageBody(driver: driver), + ), + ); + } +} diff --git a/lib/features/profile/presentation/pages/profile_page.dart b/lib/features/profile/presentation/pages/profile_page.dart index 6c970df..f47f17c 100644 --- a/lib/features/profile/presentation/pages/profile_page.dart +++ b/lib/features/profile/presentation/pages/profile_page.dart @@ -1,11 +1,36 @@ import 'package:flutter/material.dart'; -import 'package:tracking_app/app/core/router/route_names.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:tracking_app/app/config/di/di.dart'; +import 'package:tracking_app/app/core/widgets/custom_app_bar.dart'; +import 'package:tracking_app/features/auth/presentation/logout/manager/logout_cubit.dart'; +import 'package:tracking_app/features/profile/presentation/managers/profile_cubit.dart'; +import 'package:tracking_app/features/profile/presentation/managers/profile_intent.dart'; +import 'package:tracking_app/features/profile/presentation/widgets/notification_with_badge_widget.dart'; +import 'package:tracking_app/generated/locale_keys.g.dart'; +import '../widgets/profile_page_body.dart'; class ProfilePage extends StatelessWidget { const ProfilePage({super.key}); @override Widget build(BuildContext context) { - return Scaffold(body: Center(child: const Text("Welcome to Profile Page"))); + return MultiBlocProvider( + providers: [ + BlocProvider( + create: (context) => + getIt()..doIntent(GetProfileIntent()), + ), + BlocProvider(create: (_) => getIt()), + ], + child: SafeArea( + child: Scaffold( + appBar: CustomAppBar( + title: LocaleKeys.profile, + actions: const [NotificationWithBadgeWidget()], + ), + body: const ProfilePageBody(), + ), + ), + ); } } diff --git a/lib/features/profile/presentation/widgets/edit_driver_profile_form.dart b/lib/features/profile/presentation/widgets/edit_driver_profile_form.dart new file mode 100644 index 0000000..4c9be41 --- /dev/null +++ b/lib/features/profile/presentation/widgets/edit_driver_profile_form.dart @@ -0,0 +1,306 @@ +import 'dart:io'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:go_router/go_router.dart'; +import 'package:tracking_app/app/config/auth_storage/auth_storage.dart'; +import 'package:tracking_app/app/config/di/di.dart'; +import 'package:tracking_app/app/core/router/route_names.dart'; +import 'package:tracking_app/features/profile/presentation/managers/profile_cubit.dart'; +import 'package:tracking_app/features/profile/presentation/managers/profile_intent.dart'; +import 'package:tracking_app/features/profile/presentation/managers/profile_state.dart'; +import 'package:tracking_app/generated/locale_keys.g.dart'; +import 'profile_image_section.dart'; + +class EditDriverProfileForm extends StatefulWidget { + final String firstName; + final String lastName; + final String email; + final String phone; + final String? photo; + + const EditDriverProfileForm({ + super.key, + required this.firstName, + required this.lastName, + required this.email, + required this.phone, + this.photo, + }); + + @override + State createState() => _EditDriverProfileFormState(); +} + +class _EditDriverProfileFormState extends State { + late final TextEditingController firstNameController; + late final TextEditingController lastNameController; + late final TextEditingController emailController; + late final TextEditingController phoneController; + + final authStorage = getIt(); + final _formKey = GlobalKey(); + + @override + void initState() { + super.initState(); + firstNameController = TextEditingController(text: widget.firstName); + lastNameController = TextEditingController(text: widget.lastName); + emailController = TextEditingController(text: widget.email); + phoneController = TextEditingController(text: widget.phone); + } + + @override + void dispose() { + firstNameController.dispose(); + lastNameController.dispose(); + emailController.dispose(); + phoneController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final cubit = context.read(); + + return BlocListener( + listener: (context, state) { + if (state.driver != null) { + if (state.driver!.firstName != null && + firstNameController.text != state.driver!.firstName) { + firstNameController.text = state.driver!.firstName!; + } + if (state.driver!.lastName != null && + lastNameController.text != state.driver!.lastName) { + lastNameController.text = state.driver!.lastName!; + } + if (state.driver!.email != null && + emailController.text != state.driver!.email) { + emailController.text = state.driver!.email!; + } + if (state.driver!.phone != null && state.driver!.phone!.isNotEmpty) { + if (phoneController.text != state.driver!.phone) { + phoneController.text = state.driver!.phone!; + } + } + } + }, + child: BlocBuilder( + builder: (context, state) { + return SingleChildScrollView( + padding: const EdgeInsets.all(16), + child: Form( + key: _formKey, + child: Column( + children: [ + ProfileImageSection(), + const SizedBox(height: 32), + Row( + children: [ + Expanded( + child: TextFormField( + controller: firstNameController, + decoration: InputDecoration( + labelText: LocaleKeys.firstName.tr(), + ), + ), + ), + const SizedBox(width: 12), + Expanded( + child: TextFormField( + controller: lastNameController, + decoration: InputDecoration( + labelText: LocaleKeys.lastName.tr(), + ), + ), + ), + ], + ), + + const SizedBox(height: 16), + + TextFormField( + controller: emailController, + decoration: InputDecoration( + labelText: LocaleKeys.email.tr(), + ), + ), + + const SizedBox(height: 16), + + TextFormField( + controller: phoneController, + decoration: InputDecoration( + labelText: LocaleKeys.phone.tr(), + ), + ), + + const SizedBox(height: 16), + + TextFormField( + readOnly: true, + decoration: InputDecoration( + labelText: LocaleKeys.password.tr(), + hintText: '.......................', + suffix: GestureDetector( + onTap: () { + context.push(RouteNames.changePassword); + }, + child: Text( + LocaleKeys.change.tr(), + style: TextStyle( + color: Theme.of(context).primaryColor, + fontWeight: FontWeight.w600, + ), + ), + ), + ), + obscureText: true, + ), + + const SizedBox(height: 32), + + SizedBox( + width: double.infinity, + height: 52, + child: ElevatedButton( + onPressed: state.editProfileResource.isLoading == true + ? null + : () async { + final token = await authStorage.getToken(); + if (token == null) return; + + cubit.doIntent( + PerformEditProfile( + firstName: firstNameController.text.trim(), + lastName: lastNameController.text.trim(), + email: emailController.text.trim(), + phone: phoneController.text.trim(), + photo: state.selectedPhoto?.path != null + ? File(state.selectedPhoto!.path) + : null, + ), + ); + }, + child: Text( + state.editProfileResource.isLoading == true + ? LocaleKeys.loading.tr() + : LocaleKeys.update.tr(), + ), + ), + ), + ], + ), + ), + ); + }, + ), + ); + + // final state = context.watch().state; + + // return SingleChildScrollView( + // padding: const EdgeInsets.all(16), + // child: Form( + // key: _formKey, + // child: Column( + // children: [ + // ProfileImageSection(), + // const SizedBox(height: 32), + // Row( + // children: [ + // Expanded( + // child: TextFormField( + // controller: firstNameController, + // decoration: InputDecoration( + // labelText: LocaleKeys.firstName.tr(), + // ), + // ), + // ), + // const SizedBox(width: 12), + // Expanded( + // child: TextFormField( + // controller: lastNameController, + // decoration: InputDecoration( + // labelText: LocaleKeys.lastName.tr(), + // ), + // ), + // ), + // ], + // ), + + // const SizedBox(height: 16), + + // TextFormField( + // controller: emailController, + // decoration: InputDecoration(labelText: LocaleKeys.email.tr()), + // ), + + // const SizedBox(height: 16), + + // TextFormField( + // controller: phoneController, + // decoration: InputDecoration(labelText: LocaleKeys.phone.tr()), + // ), + + // const SizedBox(height: 16), + + // TextFormField( + // readOnly: true, + // decoration: InputDecoration( + // labelText: LocaleKeys.password.tr(), + // hintText: '.......................', + // suffix: GestureDetector( + // onTap: () { + // context.push(RouteNames.changePassword); + // }, + // child: Text( + // LocaleKeys.change.tr(), + // style: TextStyle( + // color: Theme.of(context).primaryColor, + // fontWeight: FontWeight.w600, + // ), + // ), + // ), + // ), + // obscureText: true, + // ), + + // const SizedBox(height: 32), + + // SizedBox( + // width: double.infinity, + // height: 52, + // child: ElevatedButton( + // onPressed: state.editProfileResource.isLoading == true + // ? null + // : () async { + // final token = await authStorage.getToken(); + // if (token == null) return; + + // if (state.selectedPhoto != null) { + // cubit.doIntent(UploadSelectedPhotoIntent()); + // } + + // cubit.doIntent( + // PerformEditProfile( + // firstName: firstNameController.text.trim(), + // lastName: lastNameController.text.trim(), + // email: emailController.text.trim(), + // phone: phoneController.text.trim(), + // ), + // ); + // }, + // child: Text( + // state.editProfileResource.isLoading == true + // ? LocaleKeys.loading.tr() + // : LocaleKeys.update.tr(), + // ), + // ), + // ), + // ], + // ), + // ), + // ); + } +} diff --git a/lib/features/profile/presentation/widgets/edit_driver_profile_page_body.dart b/lib/features/profile/presentation/widgets/edit_driver_profile_page_body.dart new file mode 100644 index 0000000..5ea55b3 --- /dev/null +++ b/lib/features/profile/presentation/widgets/edit_driver_profile_page_body.dart @@ -0,0 +1,48 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:tracking_app/app/core/widgets/show_snak_bar.dart'; +import 'package:tracking_app/features/profile/data/models/driver_model.dart'; +import 'package:tracking_app/features/profile/presentation/managers/profile_cubit.dart'; +import 'package:tracking_app/features/profile/presentation/managers/profile_state.dart'; +import 'edit_driver_profile_form.dart'; + +class EditDriverProfilePageBody extends StatelessWidget { + final DriverModel? user; + + const EditDriverProfilePageBody({super.key, this.user}); + + @override + Widget build(BuildContext context) { + return BlocListener( + listenWhen: (prev, curr) => + prev.editProfileResource != curr.editProfileResource || + prev.uploadPhotoResource != curr.uploadPhotoResource, + listener: (context, state) { + if (state.editProfileResource.isSuccess == true) { + showAppSnackbar(context, "Profile updated successfully"); + } else if (state.editProfileResource.isError == true) { + showAppSnackbar( + context, + state.editProfileResource.error ?? "Edit profile failed", + ); + } + + if (state.uploadPhotoResource.isSuccess == true) { + showAppSnackbar(context, "Photo uploaded successfully"); + } else if (state.uploadPhotoResource.isError == true) { + showAppSnackbar( + context, + state.uploadPhotoResource.error ?? "Upload photo failed", + ); + } + }, + child: EditDriverProfileForm( + firstName: user?.firstName ?? '', + lastName: user?.lastName ?? '', + email: user?.email ?? '', + phone: user?.phone ?? '', + photo: user?.photo, + ), + ); + } +} diff --git a/lib/features/profile/presentation/widgets/edit_vehicle_form.dart b/lib/features/profile/presentation/widgets/edit_vehicle_form.dart new file mode 100644 index 0000000..69209f4 --- /dev/null +++ b/lib/features/profile/presentation/widgets/edit_vehicle_form.dart @@ -0,0 +1,160 @@ +import 'dart:io'; + +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:image_picker/image_picker.dart'; +import 'package:tracking_app/features/profile/presentation/managers/profile_cubit.dart'; +import 'package:tracking_app/features/profile/presentation/managers/profile_intent.dart'; +import 'package:tracking_app/features/profile/presentation/managers/profile_state.dart'; +import 'package:tracking_app/generated/locale_keys.g.dart'; + +class EditVehicleForm extends StatefulWidget { + final String vehicleType; + final String vehicleNumber; + final String vehicleLicense; + + const EditVehicleForm({ + super.key, + required this.vehicleType, + required this.vehicleNumber, + required this.vehicleLicense, + }); + + @override + State createState() => _EditVehicleFormState(); +} + +class _EditVehicleFormState extends State { + late final TextEditingController vehicleTypeController; + late final TextEditingController vehicleNumberController; + late final TextEditingController vehicleLicenseController; + + final _formKey = GlobalKey(); + + @override + void initState() { + super.initState(); + vehicleTypeController = TextEditingController(text: widget.vehicleType); + vehicleNumberController = TextEditingController(text: widget.vehicleNumber); + vehicleLicenseController = TextEditingController( + text: widget.vehicleLicense, + ); + } + + @override + void dispose() { + vehicleTypeController.dispose(); + vehicleNumberController.dispose(); + vehicleLicenseController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final cubit = context.read(); + // final state = context.watch().state; + + return BlocListener( + listener: (context, state) { + if (state.driver != null) { + if (state.driver!.vehicleType != null && + vehicleTypeController.text != state.driver!.vehicleType) { + vehicleTypeController.text = state.driver!.vehicleType!; + } + if (state.driver!.vehicleNumber != null && + vehicleNumberController.text != state.driver!.vehicleNumber) { + vehicleNumberController.text = state.driver!.vehicleNumber!; + } + if (state.driver!.vehicleLicense != null && + vehicleLicenseController.text != state.driver!.vehicleLicense) { + vehicleLicenseController.text = state.driver!.vehicleLicense!; + } + } + }, + child: BlocBuilder( + builder: (context, state) { + return SingleChildScrollView( + padding: const EdgeInsets.all(16), + child: Form( + key: _formKey, + child: Column( + children: [ + TextFormField( + controller: vehicleTypeController, + decoration: InputDecoration( + labelText: LocaleKeys.vehicle_type.tr(), + ), + ), + + const SizedBox(height: 16), + + TextFormField( + controller: vehicleNumberController, + decoration: InputDecoration( + labelText: LocaleKeys.vehicle_number.tr(), + ), + ), + + const SizedBox(height: 16), + + TextFormField( + controller: vehicleLicenseController, + readOnly: true, + onTap: () async { + final picked = await ImagePicker().pickImage( + source: ImageSource.gallery, + ); + + if (picked != null) { + final file = File(picked.path); + + cubit.doIntent(SelectVehicleLicenseIntent(file)); + + vehicleLicenseController.text = picked.name; + + cubit.doIntent(UploadVehicleLicenseIntent()); + } + }, + decoration: InputDecoration( + labelText: LocaleKeys.vehicle_license.tr(), + suffixIcon: Icon(Icons.upload), + ), + ), + + const SizedBox(height: 32), + + SizedBox( + width: double.infinity, + height: 52, + child: ElevatedButton( + onPressed: state.editProfileResource.isLoading == true + ? null + : () { + cubit.doIntent( + PerformEditProfile( + vehicleType: vehicleTypeController.text + .trim(), + vehicleNumber: vehicleNumberController.text + .trim(), + vehicleLicense: vehicleLicenseController.text + .trim(), + ), + ); + }, + child: Text( + state.editProfileResource.isLoading == true + ? LocaleKeys.loading.tr() + : LocaleKeys.update.tr(), + ), + ), + ), + ], + ), + ), + ); + }, + ), + ); + } +} diff --git a/lib/features/profile/presentation/widgets/edit_vehicle_page_body.dart b/lib/features/profile/presentation/widgets/edit_vehicle_page_body.dart new file mode 100644 index 0000000..3a75c16 --- /dev/null +++ b/lib/features/profile/presentation/widgets/edit_vehicle_page_body.dart @@ -0,0 +1,36 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:tracking_app/app/core/widgets/show_snak_bar.dart'; +import 'package:tracking_app/features/profile/data/models/driver_model.dart'; +import 'package:tracking_app/features/profile/presentation/managers/profile_cubit.dart'; +import 'package:tracking_app/features/profile/presentation/managers/profile_state.dart'; +import 'edit_vehicle_form.dart'; + +class EditVehiclePageBody extends StatelessWidget { + final DriverModel? driver; + + const EditVehiclePageBody({super.key, this.driver}); + + @override + Widget build(BuildContext context) { + return BlocListener( + listenWhen: (prev, curr) => + prev.editProfileResource != curr.editProfileResource, + listener: (context, state) { + if (state.editProfileResource.isSuccess == true) { + showAppSnackbar(context, "Vehicle updated successfully"); + } else if (state.editProfileResource.isError == true) { + showAppSnackbar( + context, + state.editProfileResource.error ?? "Update failed", + ); + } + }, + child: EditVehicleForm( + vehicleType: driver?.vehicleType ?? '', + vehicleNumber: driver?.vehicleNumber ?? '', + vehicleLicense: driver?.vehicleLicense ?? '', + ), + ); + } +} diff --git a/lib/features/profile/presentation/widgets/info_card.dart b/lib/features/profile/presentation/widgets/info_card.dart new file mode 100644 index 0000000..4da8613 --- /dev/null +++ b/lib/features/profile/presentation/widgets/info_card.dart @@ -0,0 +1,25 @@ +import 'package:flutter/material.dart'; + +class InfoCard extends StatelessWidget { + final Widget? child; + const InfoCard({super.key, required this.child}); + + @override + Widget build(BuildContext context) { + return Card( + color: Colors.white10, + shape: RoundedRectangleBorder( + side: BorderSide(color: Colors.grey, width: 1.0), + borderRadius: BorderRadius.circular(8.0), + ), + child: SizedBox( + width: double.infinity, + height: 100, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 5), + child: child, + ), + ), + ); + } +} diff --git a/lib/features/profile/presentation/widgets/language_bottom_sheet.dart b/lib/features/profile/presentation/widgets/language_bottom_sheet.dart new file mode 100644 index 0000000..6bc92b0 --- /dev/null +++ b/lib/features/profile/presentation/widgets/language_bottom_sheet.dart @@ -0,0 +1,65 @@ +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; + +import '../../../../app/core/ui_helper/color/colors.dart'; +import '../../../../app/core/ui_helper/style/font_style.dart'; +import '../../../../generated/locale_keys.g.dart'; +import 'language_tile.dart'; + +class LanguageBottomSheet extends StatelessWidget { + const LanguageBottomSheet({super.key}); + + @override + Widget build(BuildContext context) { + return SafeArea( + top: false, + child: Padding( + padding: const EdgeInsets.fromLTRB(20, 10, 20, 24), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Center( + child: Container( + width: 44, + height: 5, + decoration: BoxDecoration( + color: Colors.black.withValues(alpha: 0.15), + borderRadius: BorderRadius.circular(99), + ), + ), + ), + const SizedBox(height: 16), + Text( + LocaleKeys.change_language.tr(), + style: AppStyles.black14Medium.copyWith( + color: AppColors.pink, + fontSize: 18, + ), + ), + const SizedBox(height: 16), + LanguageTile( + title: LocaleKeys.arabic.tr(), + value: const Locale('ar'), + groupValue: context.locale, + onChanged: (loc) async { + await context.setLocale(loc); + if (context.mounted) Navigator.pop(context); + }, + ), + const SizedBox(height: 12), + LanguageTile( + title: LocaleKeys.english.tr(), + value: const Locale('en'), + groupValue: context.locale, + onChanged: (loc) async { + await context.setLocale(loc); + if (context.mounted) Navigator.pop(context); + }, + ), + ], + ), + ), + ); + } +} diff --git a/lib/features/profile/presentation/widgets/language_tile.dart b/lib/features/profile/presentation/widgets/language_tile.dart new file mode 100644 index 0000000..d2a0086 --- /dev/null +++ b/lib/features/profile/presentation/widgets/language_tile.dart @@ -0,0 +1,60 @@ +import 'package:flutter/material.dart'; +import 'package:tracking_app/features/profile/presentation/widgets/radio_circle.dart'; +import '../../../../app/core/ui_helper/color/colors.dart'; +import '../../../../app/core/ui_helper/style/font_style.dart'; + +class LanguageTile extends StatelessWidget { + final String title; + final Locale value; + final Locale groupValue; + final ValueChanged onChanged; + + const LanguageTile({ + super.key, + required this.title, + required this.value, + required this.groupValue, + required this.onChanged, + }); + + @override + Widget build(BuildContext context) { + final selected = value == groupValue; + + return InkWell( + borderRadius: BorderRadius.circular(14), + onTap: () => onChanged(value), + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(14), + border: Border.all( + color: selected ? AppColors.pink : Colors.grey.shade200, + width: 1.2, + ), + boxShadow: [ + BoxShadow( + color: Colors.black.withValues(alpha: 0.05), + blurRadius: 10, + offset: const Offset(0, 4), + ), + ], + ), + child: Row( + children: [ + Expanded( + child: Text( + title, + style: selected + ? AppStyles.black14bold.copyWith(color: AppColors.pink) + : AppStyles.black14Medium, + ), + ), + RadioCircle(selected: selected), + ], + ), + ), + ); + } +} diff --git a/lib/features/profile/presentation/widgets/notification_with_badge_widget.dart b/lib/features/profile/presentation/widgets/notification_with_badge_widget.dart new file mode 100644 index 0000000..34d3f1c --- /dev/null +++ b/lib/features/profile/presentation/widgets/notification_with_badge_widget.dart @@ -0,0 +1,31 @@ +import 'package:flutter/material.dart'; + +class NotificationWithBadgeWidget extends StatelessWidget { + const NotificationWithBadgeWidget({super.key}); + + @override + Widget build(BuildContext context) { + return Stack( + children: [ + IconButton(icon: const Icon(Icons.notifications), onPressed: () {}), + Positioned( + right: 8, + top: 8, + child: Container( + padding: const EdgeInsets.all(2), + decoration: BoxDecoration( + color: Colors.red, + borderRadius: BorderRadius.circular(10), + ), + constraints: const BoxConstraints(minWidth: 16, minHeight: 16), + child: const Text( + '3', + style: TextStyle(color: Colors.white, fontSize: 10), + textAlign: TextAlign.center, + ), + ), + ), + ], + ); + } +} diff --git a/lib/features/profile/presentation/widgets/profile_avatar.dart b/lib/features/profile/presentation/widgets/profile_avatar.dart new file mode 100644 index 0000000..a5ae874 --- /dev/null +++ b/lib/features/profile/presentation/widgets/profile_avatar.dart @@ -0,0 +1,51 @@ +import 'package:flutter/material.dart'; + +class ProfileAvatar extends StatelessWidget { + final String? imageUrl; + final String userName; + + const ProfileAvatar({super.key, this.imageUrl, required this.userName}); + + String getInitials(String name) { + if (name.isEmpty) return ''; + final parts = name.trim().split(RegExp(r'\s+')); + if (parts.isEmpty || parts[0].isEmpty) return ''; + if (parts.length == 1) return parts[0][0]; + return parts[0][0] + parts[1][0]; + } + + Color getRandomBackgroundColor(String name) { + final colors = [ + Colors.blue, + Colors.green, + Colors.orange, + Colors.purple, + Colors.red, + Colors.teal, + Colors.brown, + ]; + final index = name.hashCode % colors.length; + return colors[index]; + } + + @override + Widget build(BuildContext context) { + return CircleAvatar( + radius: 30, + backgroundColor: imageUrl == null + ? getRandomBackgroundColor(userName) + : null, + backgroundImage: imageUrl != null ? NetworkImage(imageUrl!) : null, + child: imageUrl == null + ? Text( + getInitials(userName).toUpperCase(), + style: TextStyle( + fontSize: 50 / 2, + fontWeight: FontWeight.bold, + color: Colors.white, + ), + ) + : null, + ); + } +} diff --git a/lib/features/profile/presentation/widgets/profile_image_section.dart b/lib/features/profile/presentation/widgets/profile_image_section.dart new file mode 100644 index 0000000..89b31e3 --- /dev/null +++ b/lib/features/profile/presentation/widgets/profile_image_section.dart @@ -0,0 +1,60 @@ +import 'dart:io'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:image_picker/image_picker.dart'; +import 'package:tracking_app/app/core/ui_helper/color/colors.dart'; +import 'package:tracking_app/features/profile/presentation/managers/profile_cubit.dart'; +import 'package:tracking_app/features/profile/presentation/managers/profile_intent.dart'; + +class ProfileImageSection extends StatelessWidget { + const ProfileImageSection({super.key}); + + @override + Widget build(BuildContext context) { + final cubit = context.read(); + final state = context.watch().state; + + ImageProvider? image; + if (state.selectedPhoto != null) { + image = kIsWeb + ? NetworkImage(state.selectedPhoto!.path) + : FileImage(File(state.selectedPhoto!.path)); + } + + return Column( + children: [ + Stack( + alignment: Alignment.center, + children: [ + CircleAvatar( + radius: 50, + backgroundColor: Colors.grey.shade200, + backgroundImage: image, + child: image == null + ? const Icon(Icons.person, size: 50, color: Colors.grey) + : null, + ), + if (state.uploadPhotoResource.isLoading == true) + const CircularProgressIndicator(color: AppColors.pink), + ], + ), + const SizedBox(height: 8), + TextButton.icon( + onPressed: () async { + final picker = ImagePicker(); + final file = await picker.pickImage(source: ImageSource.gallery); + if (file != null) { + cubit.doIntent(SelectPhotoIntent(File(file.path))); + } + }, + icon: const Icon(Icons.camera_alt, color: AppColors.pink), + label: const Text( + "Change Photo", + style: TextStyle(color: AppColors.pink), + ), + ), + ], + ); + } +} diff --git a/lib/features/profile/presentation/widgets/profile_item.dart b/lib/features/profile/presentation/widgets/profile_item.dart new file mode 100644 index 0000000..e78d6a6 --- /dev/null +++ b/lib/features/profile/presentation/widgets/profile_item.dart @@ -0,0 +1,28 @@ +import 'package:flutter/material.dart'; +import 'package:tracking_app/app/core/ui_helper/color/colors.dart'; +import 'package:tracking_app/app/core/ui_helper/style/font_style.dart'; + +class ProfileItem extends StatelessWidget { + const ProfileItem({ + super.key, + required this.itemName, + required this.icon, + this.onTap, + this.trailing, + }); + + final String itemName; + final IconData icon; + final VoidCallback? onTap; + final Widget? trailing; + + @override + Widget build(BuildContext context) { + return ListTile( + leading: Icon(icon, color: AppColors.grey), + title: Text(itemName, style: AppStyles.font12Black), + trailing: trailing, + onTap: onTap, + ); + } +} diff --git a/lib/features/profile/presentation/widgets/profile_page_body.dart b/lib/features/profile/presentation/widgets/profile_page_body.dart new file mode 100644 index 0000000..94fbd56 --- /dev/null +++ b/lib/features/profile/presentation/widgets/profile_page_body.dart @@ -0,0 +1,175 @@ +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:go_router/go_router.dart'; +import 'package:tracking_app/app/core/router/route_names.dart'; +import 'package:tracking_app/app/core/ui_helper/color/colors.dart'; +import 'package:tracking_app/app/core/ui_helper/style/font_style.dart'; +import 'package:tracking_app/features/profile/presentation/managers/profile_cubit.dart'; +import 'package:tracking_app/features/profile/presentation/managers/profile_intent.dart'; +import 'package:tracking_app/features/profile/presentation/widgets/info_card.dart'; +import 'package:tracking_app/features/profile/presentation/widgets/profile_avatar.dart'; +import 'package:tracking_app/features/profile/presentation/widgets/profile_item.dart'; +import 'package:tracking_app/generated/locale_keys.g.dart'; +import '../../../auth/presentation/logout/manager/logout_cubit.dart'; +import '../../../auth/presentation/logout/manager/logout_intent.dart'; +import '../../../auth/presentation/logout/manager/logout_state.dart'; +import 'language_bottom_sheet.dart'; + +class ProfilePageBody extends StatelessWidget { + const ProfilePageBody({super.key}); + + @override + Widget build(BuildContext context) { + final state = context.watch().state; + final user = state.driver; + + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 12.0), + child: Column( + children: [ + const SizedBox(height: 16), + InkWell( + borderRadius: BorderRadius.circular(16), + onTap: () async { + await context.push(RouteNames.editDriverProfile, extra: user); + if (context.mounted) { + context.read().doIntent(GetProfileIntent()); + } + }, + child: InfoCard( + child: Row( + children: [ + ProfileAvatar( + userName: + "${user?.firstName ?? ''} ${user?.lastName ?? ''}", + imageUrl: user?.photo, + ), + const SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + "${user?.firstName ?? 'Admin'} ${user?.lastName ?? 'User'}", + style: AppStyles.black14bold, + overflow: TextOverflow.ellipsis, + ), + const SizedBox(height: 5), + Text( + user?.email ?? 'test@gmail.com', + style: AppStyles.black14Medium, + overflow: TextOverflow.ellipsis, + ), + const SizedBox(height: 5), + Text( + user?.phone ?? '01010101010', + style: AppStyles.black14Medium, + overflow: TextOverflow.ellipsis, + ), + ], + ), + ), + const Icon(Icons.arrow_forward_ios), + ], + ), + ), + ), + + const SizedBox(height: 16), + + InfoCard( + child: Row( + children: [ + Expanded( + child: InkWell( + onTap: () async { + await context.push(RouteNames.editVehicle, extra: user); + if (context.mounted) { + context.read().doIntent( + GetProfileIntent(), + ); + } + }, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text("Vehicle Info", style: AppStyles.black14bold), + const SizedBox(height: 5), + Text( + user?.vehicleType ?? "N/A", + style: AppStyles.black14Medium, + ), + const SizedBox(height: 5), + Text( + user?.vehicleNumber ?? "N/A", + style: AppStyles.black14Medium, + ), + ], + ), + ), + ), + const Icon(Icons.arrow_forward_ios), + ], + ), + ), + + const SizedBox(height: 16), + + ProfileItem( + itemName: "Language", + icon: Icons.language, + onTap: () { + showModalBottomSheet( + context: context, + builder: (context) => const LanguageBottomSheet(), + ); + }, + trailing: Text( + context.locale.languageCode == 'ar' ? "Arabic" : "English", + style: AppStyles.font14Black.copyWith(color: AppColors.pink), + ), + ), + BlocConsumer( + listener: (context, state) { + if (state.logoutResource.isSuccess) { + context.go(RouteNames.login); + } + if (state.logoutResource.isError) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text( + state.logoutResource.error ?? + LocaleKeys.logoutFailed.tr(), + ), + ), + ); + } + }, + builder: (context, state) { + final isLoading = state.logoutResource.isLoading; + return ProfileItem( + itemName: LocaleKeys.logout.tr(), + icon: Icons.logout, + trailing: isLoading + ? const SizedBox( + width: 22, + height: 22, + child: CircularProgressIndicator(strokeWidth: 2), + ) + : Icon(Icons.logout, color: AppColors.pink), + onTap: isLoading + ? null + : () { + context.read().doIntent(PerformLogout()); + }, + ); + }, + ), + ], + ), + ); + } +} diff --git a/lib/features/profile/presentation/widgets/radio_circle.dart b/lib/features/profile/presentation/widgets/radio_circle.dart new file mode 100644 index 0000000..ddea206 --- /dev/null +++ b/lib/features/profile/presentation/widgets/radio_circle.dart @@ -0,0 +1,31 @@ +import 'package:flutter/material.dart'; + +import '../../../../app/core/ui_helper/color/colors.dart'; + +class RadioCircle extends StatelessWidget { + final bool selected; + const RadioCircle({super.key, required this.selected}); + + @override + Widget build(BuildContext context) { + return Container( + width: 22, + height: 22, + decoration: BoxDecoration( + shape: BoxShape.circle, + border: Border.all( + color: selected ? AppColors.pink : Colors.grey.shade400, + width: 2, + ), + ), + child: AnimatedContainer( + duration: const Duration(milliseconds: 150), + margin: const EdgeInsets.all(4), + decoration: BoxDecoration( + shape: BoxShape.circle, + color: selected ? AppColors.pink : Colors.transparent, + ), + ), + ); + } +} diff --git a/lib/features/track_order/api/track_order_remote_source_impl.dart b/lib/features/track_order/api/track_order_remote_source_impl.dart new file mode 100644 index 0000000..05dc370 --- /dev/null +++ b/lib/features/track_order/api/track_order_remote_source_impl.dart @@ -0,0 +1,83 @@ +import 'package:injectable/injectable.dart'; +import 'package:tracking_app/app/config/auth_storage/auth_storage.dart'; +import 'package:tracking_app/app/core/network/api_result.dart'; +import 'package:tracking_app/features/track_order/data/datasource/track_order_remote_source.dart'; +import 'package:tracking_app/features/track_order/data/models/driver_model.dart'; +import 'package:tracking_app/features/track_order/data/models/track_order_model.dart'; +import 'package:cloud_firestore/cloud_firestore.dart'; + +@Injectable(as: TrackOrderRemoteDataSource) +class TrackOrderRemoteDataSourceImpl implements TrackOrderRemoteDataSource { + final FirebaseFirestore firestore; + final AuthStorage authStorage; + TrackOrderRemoteDataSourceImpl(this.firestore, this.authStorage); + @override + ApiResult>> trackOrder(String userId) { + try { + final stream = firestore + .collection('orders') + .orderBy('updatedAt', descending: true) + .snapshots() + .map((snapshot) { + return snapshot.docs + .map((doc) => TrackOrderModel.fromFirestore(doc.id, doc.data())) + .toList(); + }); + return SuccessApiResult>>(data: stream); + } catch (e) { + return ErrorApiResult>>(error: e.toString()); + } + } + + @override + ApiResult> trackDriver(String driverId) { + try { + final stream = firestore + .collection('drivers') + .doc(driverId) + .snapshots() + .map((snapshot) { + final data = snapshot.data(); + if (data == null) throw Exception("Driver not found"); + return DriverModel.fromFirestore(snapshot.id, data); + }); + return SuccessApiResult>(data: stream); + } catch (e) { + return ErrorApiResult>(error: e.toString()); + } + } + + @override + Future>> updateOrderStatus( + String orderId, + String status, + ) async { + try { + // 1. Fetch current order data to get deviceToken + final orderDoc = await firestore.collection('orders').doc(orderId).get(); + final orderData = orderDoc.data(); + final deviceToken = orderData?['deviceToken'] ?? ''; + + // 2. Update order status + await firestore.collection('orders').doc(orderId).update({ + 'status': status, + 'updatedAt': FieldValue.serverTimestamp(), + }); + + // 3. Add notification + await firestore.collection('notification').add({ + 'title': 'Order Status Updated', + 'description': 'Order $orderId status changed to $status', + 'orderId': orderId, + 'status': status, + 'createdAt': FieldValue.serverTimestamp(), + 'targetApp': 'flower_shop', + 'deviceToken': deviceToken, + }); + + return orderDoc; + } catch (e) { + rethrow; + } + } +} diff --git a/lib/features/track_order/data/datasource/track_order_remote_source.dart b/lib/features/track_order/data/datasource/track_order_remote_source.dart new file mode 100644 index 0000000..f7325f5 --- /dev/null +++ b/lib/features/track_order/data/datasource/track_order_remote_source.dart @@ -0,0 +1,13 @@ +import 'package:tracking_app/app/core/network/api_result.dart'; +import 'package:tracking_app/features/track_order/data/models/driver_model.dart'; +import 'package:tracking_app/features/track_order/data/models/track_order_model.dart'; +import 'package:cloud_firestore/cloud_firestore.dart'; + +abstract class TrackOrderRemoteDataSource { + ApiResult>> trackOrder(String userId); + ApiResult> trackDriver(String driverId); + Future>> updateOrderStatus( + String orderId, + String status, + ); +} diff --git a/lib/features/track_order/data/models/driver_model.dart b/lib/features/track_order/data/models/driver_model.dart new file mode 100644 index 0000000..ae5ef81 --- /dev/null +++ b/lib/features/track_order/data/models/driver_model.dart @@ -0,0 +1,29 @@ +class DriverModel { + final String id; + final double lat; + final double lng; + final String name; + final String phone; + final String deviceToken; + + DriverModel({ + required this.id, + required this.lat, + required this.lng, + required this.name, + required this.phone, + required this.deviceToken, + }); + + factory DriverModel.fromFirestore(String id, Map data) { + final location = data['currentLocation'] as Map?; + return DriverModel( + id: id, + lat: (location?['lat'] as num?)?.toDouble() ?? 0.0, + lng: (location?['lng'] as num?)?.toDouble() ?? 0.0, + name: data['name'] ?? '', + phone: data['phone'] ?? '', + deviceToken: data['deviceToken'] ?? '', + ); + } +} diff --git a/lib/features/track_order/data/models/track_order_model.dart b/lib/features/track_order/data/models/track_order_model.dart new file mode 100644 index 0000000..5f87ecd --- /dev/null +++ b/lib/features/track_order/data/models/track_order_model.dart @@ -0,0 +1,80 @@ +class TrackOrderModel { + final String driverId; + final String id; + final String status; + final String totalPrice; + final String userId; + final String pickupAddress; + final String pickupName; + final String userAddress; + final String userName; + final String deviceToken; + + TrackOrderModel({ + required this.driverId, + required this.id, + required this.status, + required this.totalPrice, + required this.userId, + required this.pickupAddress, + required this.pickupName, + required this.userAddress, + required this.userName, + required this.deviceToken, + }); + + factory TrackOrderModel.fromFirestore(String id, Map data) { + String safeString(dynamic value) { + if (value == null) return ''; + if (value is String) return value; + return value.toString(); + } + + dynamic userAddress = data['userAddress']; + String parsedUserId = ''; + if (userAddress is Map) { + parsedUserId = safeString(userAddress['user_id']); + } else { + parsedUserId = safeString(data['userId']); + } + + dynamic orderDt = data['oder_dt']; + String parsedStatus = ''; + String parsedTotal = ''; + if (orderDt is Map) { + parsedStatus = safeString(orderDt['status']); + parsedTotal = safeString(orderDt['totalPrice']); + } else { + parsedStatus = safeString(data['status']); + parsedTotal = safeString(data['totalPrice']); + } + + dynamic pickupAddr = data['pickupAddress']; + String pAddr = ''; + String pName = ''; + if (pickupAddr is Map) { + pAddr = safeString(pickupAddr['address'] ?? pickupAddr['adress']); + pName = safeString(pickupAddr['name']); + } + + String uAddr = ''; + String uName = ''; + if (userAddress is Map) { + uAddr = safeString(userAddress['address'] ?? userAddress['adress']); + uName = safeString(userAddress['name']); + } + + return TrackOrderModel( + id: id, + driverId: safeString(data['driver_id'] ?? data['driverId']), + status: parsedStatus, + totalPrice: parsedTotal, + userId: parsedUserId, + pickupAddress: pAddr, + pickupName: pName, + userAddress: uAddr, + userName: uName, + deviceToken: safeString(data['deviceToken']), + ); + } +} diff --git a/lib/features/track_order/data/repos/track_order_repo_imp.dart b/lib/features/track_order/data/repos/track_order_repo_imp.dart new file mode 100644 index 0000000..cf19ca0 --- /dev/null +++ b/lib/features/track_order/data/repos/track_order_repo_imp.dart @@ -0,0 +1,70 @@ +import 'package:injectable/injectable.dart'; +import 'package:tracking_app/app/core/network/api_result.dart'; +import 'package:tracking_app/features/track_order/data/datasource/track_order_remote_source.dart'; +import 'package:tracking_app/features/track_order/domain/entities/driver_entity.dart'; +import 'package:tracking_app/features/track_order/domain/entities/order_entity.dart'; +import 'package:tracking_app/features/track_order/domain/repos/track_order_repo.dart'; + +@Injectable(as: TrackOrderRepo) +class TrackOrderRepoImpl implements TrackOrderRepo { + final TrackOrderRemoteDataSource remoteDataSource; + + TrackOrderRepoImpl(this.remoteDataSource); + + @override + ApiResult>> trackOrder(String userId) { + final result = remoteDataSource.trackOrder(userId); + + return switch (result) { + SuccessApiResult() => SuccessApiResult( + data: (result.data).map( + (models) => models + .map( + (model) => OrderEntity( + id: model.id, + userId: model.userId, + status: model.status, + driverId: model.driverId, + totalPrice: model.totalPrice, + pickupAddress: model.pickupAddress, + pickupName: model.pickupName, + userAddress: model.userAddress, + userName: model.userName, + deviceToken: model.deviceToken, + ), + ) + .toList(), + ), + ), + + ErrorApiResult() => ErrorApiResult(error: result.error), + }; + } + + @override + ApiResult> trackOrderWithDriver(String driverId) { + final result = remoteDataSource.trackDriver(driverId); + + return switch (result) { + SuccessApiResult() => SuccessApiResult( + data: (result.data).map( + (model) => DriverEntity( + id: model.id, + lat: model.lat, + lng: model.lng, + name: model.name, + phone: model.phone, + deviceToken: model.deviceToken, + ), + ), + ), + + ErrorApiResult() => ErrorApiResult(error: result.error), + }; + } + + @override + Future updateOrderStatus(String orderId, String status) { + return remoteDataSource.updateOrderStatus(orderId, status); + } +} diff --git a/lib/features/track_order/domain/entities/driver_entity.dart b/lib/features/track_order/domain/entities/driver_entity.dart new file mode 100644 index 0000000..e426ca3 --- /dev/null +++ b/lib/features/track_order/domain/entities/driver_entity.dart @@ -0,0 +1,32 @@ +import 'package:equatable/equatable.dart'; + +class DriverEntity extends Equatable { + final String id; + final double lat; + final double lng; + final String name; + final String phone; + final String deviceToken; + final String? currentLocation; + + const DriverEntity({ + required this.id, + required this.lat, + required this.lng, + required this.name, + required this.phone, + required this.deviceToken, + this.currentLocation, + }); + + @override + List get props => [ + id, + lat, + lng, + name, + phone, + deviceToken, + currentLocation, + ]; +} diff --git a/lib/features/track_order/domain/entities/order_entity.dart b/lib/features/track_order/domain/entities/order_entity.dart new file mode 100644 index 0000000..1dc209a --- /dev/null +++ b/lib/features/track_order/domain/entities/order_entity.dart @@ -0,0 +1,42 @@ +import 'package:equatable/equatable.dart'; + +class OrderEntity extends Equatable { + final String id; + final String userId; + final String status; + + final String? driverId; + final String? totalPrice; + final String? pickupAddress; + final String? pickupName; + final String? userAddress; + final String? userName; + final String? deviceToken; + + const OrderEntity({ + required this.id, + required this.userId, + required this.status, + this.driverId, + this.totalPrice, + this.pickupAddress, + this.pickupName, + this.userAddress, + this.userName, + this.deviceToken, + }); + + @override + List get props => [ + id, + userId, + status, + driverId, + totalPrice, + pickupAddress, + pickupName, + userAddress, + userName, + deviceToken, + ]; +} diff --git a/lib/features/track_order/domain/repos/track_order_repo.dart b/lib/features/track_order/domain/repos/track_order_repo.dart new file mode 100644 index 0000000..7b25d59 --- /dev/null +++ b/lib/features/track_order/domain/repos/track_order_repo.dart @@ -0,0 +1,9 @@ +import 'package:tracking_app/app/core/network/api_result.dart'; +import 'package:tracking_app/features/track_order/domain/entities/driver_entity.dart'; +import 'package:tracking_app/features/track_order/domain/entities/order_entity.dart'; + +abstract class TrackOrderRepo { + ApiResult>> trackOrder(String userId); + ApiResult> trackOrderWithDriver(String driverId); + Future updateOrderStatus(String orderId, String status); +} diff --git a/lib/features/track_order/domain/usecases/driver_usecase.dart b/lib/features/track_order/domain/usecases/driver_usecase.dart new file mode 100644 index 0000000..d1215fa --- /dev/null +++ b/lib/features/track_order/domain/usecases/driver_usecase.dart @@ -0,0 +1,12 @@ +import 'package:injectable/injectable.dart'; +import 'package:tracking_app/app/core/network/api_result.dart'; +import 'package:tracking_app/features/track_order/domain/entities/driver_entity.dart'; +import 'package:tracking_app/features/track_order/domain/repos/track_order_repo.dart'; + +@injectable +class TrackDriverUseCase { + final TrackOrderRepo repository; + TrackDriverUseCase(this.repository); + ApiResult> call(String orderId) => + repository.trackOrderWithDriver(orderId); +} diff --git a/lib/features/track_order/domain/usecases/track_order_usecase.dart b/lib/features/track_order/domain/usecases/track_order_usecase.dart new file mode 100644 index 0000000..9326760 --- /dev/null +++ b/lib/features/track_order/domain/usecases/track_order_usecase.dart @@ -0,0 +1,14 @@ +import 'package:injectable/injectable.dart'; +import 'package:tracking_app/app/core/network/api_result.dart'; +import 'package:tracking_app/features/track_order/domain/entities/order_entity.dart'; +import 'package:tracking_app/features/track_order/domain/repos/track_order_repo.dart'; + +@injectable +class TrackOrderUseCase { + final TrackOrderRepo repository; + + TrackOrderUseCase(this.repository); + + ApiResult>> call(String userId) => + repository.trackOrder(userId); +} diff --git a/lib/features/track_order/domain/usecases/update_state_usecase.dart b/lib/features/track_order/domain/usecases/update_state_usecase.dart new file mode 100644 index 0000000..b0a39a3 --- /dev/null +++ b/lib/features/track_order/domain/usecases/update_state_usecase.dart @@ -0,0 +1,13 @@ +import 'package:injectable/injectable.dart'; +import 'package:tracking_app/features/track_order/domain/repos/track_order_repo.dart'; + +@injectable +class UpdateOrderStatusUseCase { + final TrackOrderRepo repository; + + UpdateOrderStatusUseCase(this.repository); + + Future call(String orderId, String status) { + return repository.updateOrderStatus(orderId, status); + } +} diff --git a/lib/features/track_order/presentation/manager/cubit/track_order_cubit.dart b/lib/features/track_order/presentation/manager/cubit/track_order_cubit.dart new file mode 100644 index 0000000..c382838 --- /dev/null +++ b/lib/features/track_order/presentation/manager/cubit/track_order_cubit.dart @@ -0,0 +1,73 @@ +import 'dart:async'; +import 'package:bloc/bloc.dart'; +import 'package:equatable/equatable.dart'; +import 'package:injectable/injectable.dart'; +import 'package:tracking_app/app/core/network/api_result.dart'; +import 'package:tracking_app/app/config/auth_storage/auth_storage.dart'; +import 'package:tracking_app/features/track_order/domain/entities/order_entity.dart'; +import 'package:tracking_app/features/track_order/domain/entities/driver_entity.dart'; +import 'package:tracking_app/features/track_order/domain/usecases/track_order_usecase.dart'; +import 'package:tracking_app/features/track_order/domain/usecases/driver_usecase.dart'; +import 'package:tracking_app/features/track_order/domain/usecases/update_state_usecase.dart'; +part 'track_order_intent.dart'; +part 'track_order_state.dart'; + +@injectable +class TrackOrderCubit extends Cubit { + final TrackOrderUseCase trackOrderUseCase; + final TrackDriverUseCase driverUseCase; + final UpdateOrderStatusUseCase updateOrderStatusUseCase; + final AuthStorage authStorage; + + StreamSubscription>? _ordersSubscription; + StreamSubscription? _driverSubscription; + + TrackOrderCubit( + this.trackOrderUseCase, + this.driverUseCase, + this.updateOrderStatusUseCase, + this.authStorage, + ) : super(const TrackOrderState()); + + Future loadUserOrders() async { + emit(state.copyWith(isLoading: true, error: null)); + + final token = await authStorage.getToken(); + + if (token == null) { + emit(state.copyWith(isLoading: false, error: "User not logged in")); + return; + } + + final result = trackOrderUseCase(token); + + if (result is SuccessApiResult>>) { + _ordersSubscription = result.data.listen( + (orders) { + emit(state.copyWith(orders: orders, isLoading: false)); + }, + onError: (error) { + emit(state.copyWith(isLoading: false, error: error.toString())); + }, + ); + } + } + + Future updateOrderStatus(String orderId, String status) async { + emit(state.copyWith(isLoading: true, error: null)); + + try { + await updateOrderStatusUseCase(orderId, status); + emit(state.copyWith(isLoading: false)); + } catch (e) { + emit(state.copyWith(isLoading: false, error: e.toString())); + } + } + + @override + Future close() async { + await _ordersSubscription?.cancel(); + await _driverSubscription?.cancel(); + return super.close(); + } +} diff --git a/lib/features/track_order/presentation/manager/cubit/track_order_intent.dart b/lib/features/track_order/presentation/manager/cubit/track_order_intent.dart new file mode 100644 index 0000000..9b4350d --- /dev/null +++ b/lib/features/track_order/presentation/manager/cubit/track_order_intent.dart @@ -0,0 +1,24 @@ +part of 'track_order_cubit.dart'; + + +abstract class TrackOrderIntent extends Equatable { + const TrackOrderIntent(); + + @override + List get props => []; +} + +class LoadUserOrdersIntent extends TrackOrderIntent { + const LoadUserOrdersIntent(); + + @override + List get props => []; +} + +class AcceptOrderIntent extends TrackOrderIntent { + final String orderId; + const AcceptOrderIntent(this.orderId); + + @override + List get props => [orderId]; +} \ No newline at end of file diff --git a/lib/features/track_order/presentation/manager/cubit/track_order_state.dart b/lib/features/track_order/presentation/manager/cubit/track_order_state.dart new file mode 100644 index 0000000..87a0190 --- /dev/null +++ b/lib/features/track_order/presentation/manager/cubit/track_order_state.dart @@ -0,0 +1,32 @@ +part of 'track_order_cubit.dart'; + +class TrackOrderState extends Equatable { + final List orders; + final DriverEntity? driver; + final bool isLoading; + final String? error; + + const TrackOrderState({ + this.orders = const [], + this.driver, + this.isLoading = false, + this.error, + }); + + TrackOrderState copyWith({ + List? orders, + DriverEntity? driver, + bool? isLoading, + String? error, + }) { + return TrackOrderState( + orders: orders ?? this.orders, + driver: driver ?? this.driver, + isLoading: isLoading ?? this.isLoading, + error: error, + ); + } + + @override + List get props => [orders, driver, isLoading, error]; +} diff --git a/lib/features/track_order/presentation/pages/address_tile.dart b/lib/features/track_order/presentation/pages/address_tile.dart new file mode 100644 index 0000000..7534607 --- /dev/null +++ b/lib/features/track_order/presentation/pages/address_tile.dart @@ -0,0 +1,80 @@ + +import 'package:flutter/material.dart'; + +class AddressTile extends StatelessWidget { + final String title; + final String name; + final String address; + final IconData icon; + final Color iconBg; + final Color iconColor; + + const AddressTile({ + super.key, + required this.title, + required this.name, + required this.address, + required this.icon, + required this.iconBg, + required this.iconColor, + }); + + @override + Widget build(BuildContext context) { + return Padding( + padding: + const EdgeInsets.symmetric(horizontal: 20, vertical: 16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + title.toUpperCase(), + style: TextStyle( + fontSize: 11, + fontWeight: FontWeight.w800, + color: Colors.grey.shade400, + letterSpacing: 0.5, + ), + ), + const SizedBox(height: 12), + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + padding: const EdgeInsets.all(10), + decoration: BoxDecoration( + color: iconBg, + shape: BoxShape.circle, + ), + child: Icon(icon, size: 20, color: iconColor), + ), + const SizedBox(width: 14), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + name, + style: const TextStyle( + fontWeight: FontWeight.bold, + fontSize: 14, + ), + ), + const SizedBox(height: 4), + Text( + address, + style: TextStyle( + fontSize: 13, + color: Colors.grey.shade500, + ), + ), + ], + ), + ), + ], + ), + ], + ), + ); + } +} \ No newline at end of file diff --git a/lib/features/track_order/presentation/pages/driver_header.dart b/lib/features/track_order/presentation/pages/driver_header.dart new file mode 100644 index 0000000..fab783e --- /dev/null +++ b/lib/features/track_order/presentation/pages/driver_header.dart @@ -0,0 +1,36 @@ +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; +import 'package:tracking_app/app/core/ui_helper/color/colors.dart'; +import 'package:tracking_app/generated/locale_keys.g.dart'; + +class DriverHeader extends StatelessWidget { + final String driverName; + + const DriverHeader({super.key, required this.driverName}); + + @override + Widget build(BuildContext context) { + return Row( + children: [ + CircleAvatar( + backgroundColor: AppColors.pink.withOpacity(0.1), + child: const Icon(Icons.person, color: AppColors.pink), + ), + const SizedBox(width: 12), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + LocaleKeys.welcomeBack.tr(), + style: const TextStyle(fontSize: 12, color: Colors.grey), + ), + Text( + driverName, + style: const TextStyle(fontSize: 16, fontWeight: FontWeight.bold), + ), + ], + ), + ], + ); + } +} diff --git a/lib/features/track_order/presentation/pages/order_card.dart b/lib/features/track_order/presentation/pages/order_card.dart new file mode 100644 index 0000000..0b87d76 --- /dev/null +++ b/lib/features/track_order/presentation/pages/order_card.dart @@ -0,0 +1,108 @@ +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; +import 'package:tracking_app/app/core/ui_helper/color/colors.dart'; +import 'package:tracking_app/features/track_order/domain/entities/order_entity.dart'; +import 'package:tracking_app/features/track_order/presentation/pages/address_tile.dart'; +import 'package:tracking_app/features/track_order/presentation/pages/order_header.dart'; +import 'package:tracking_app/features/track_order/presentation/pages/status_button.dart'; +import 'package:tracking_app/generated/locale_keys.g.dart'; + +class OrderCard extends StatelessWidget { + final OrderEntity order; + + const OrderCard({super.key, required this.order}); + + @override + Widget build(BuildContext context) { + final statusColor = _statusColor(order.status); + + return Container( + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(24), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.04), + blurRadius: 20, + offset: const Offset(0, 4), + ), + ], + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + OrderHeader(order: order, statusColor: statusColor), + const Divider(height: 1), + + AddressTile( + title: LocaleKeys.pickupAddress.tr(), + name: order.pickupName ?? LocaleKeys.store.tr(), + address: order.pickupAddress ?? '-', + icon: Icons.store_rounded, + iconBg: AppColors.pink.withOpacity(0.1), + iconColor: AppColors.pink, + ), + + AddressTile( + title: LocaleKeys.userAddress.tr(), + name: order.userName ?? LocaleKeys.customer.tr(), + address: order.userAddress ?? '-', + icon: Icons.person_pin_circle_rounded, + iconBg: Colors.grey.shade100, + iconColor: Colors.grey.shade600, + ), + + const Divider(height: 1), + + Padding( + padding: const EdgeInsets.all(20), + child: Column( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + LocaleKeys.totalPrice.tr(), + style: const TextStyle( + fontWeight: FontWeight.bold, + fontSize: 15, + ), + ), + Text( + '${LocaleKeys.egp.tr()} ${order.totalPrice ?? '0'}', + style: const TextStyle( + fontWeight: FontWeight.bold, + fontSize: 15, + ), + ), + ], + ), + const SizedBox(height: 20), + StatusButton(order: order), + ], + ), + ), + ], + ), + ); + } + + static Color _statusColor(String status) { + switch (status.toLowerCase()) { + case 'pending': + return Colors.orange; + case 'accepted': + return Colors.blue; + case 'arrived': + return Colors.deepPurple; + case 'picked': + return Colors.indigo; + case 'on the way': + return Colors.teal; + case 'delivered': + return Colors.green; + default: + return Colors.grey; + } + } +} diff --git a/lib/features/track_order/presentation/pages/order_header.dart b/lib/features/track_order/presentation/pages/order_header.dart new file mode 100644 index 0000000..d01635d --- /dev/null +++ b/lib/features/track_order/presentation/pages/order_header.dart @@ -0,0 +1,71 @@ +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; +import 'package:tracking_app/features/track_order/domain/entities/order_entity.dart'; +import 'package:tracking_app/generated/locale_keys.g.dart'; + +class OrderHeader extends StatelessWidget { + final OrderEntity order; + final Color statusColor; + + const OrderHeader({ + super.key, + required this.order, + required this.statusColor, + }); + + @override + Widget build(BuildContext context) { + String translatedStatus = order.status; + switch (order.status.toLowerCase()) { + case 'pending': + translatedStatus = LocaleKeys.pending.tr(); + break; + case 'accepted': + translatedStatus = LocaleKeys.accepted.tr(); + break; + case 'arrived': + translatedStatus = LocaleKeys.arrived.tr(); + break; + case 'picked': + translatedStatus = LocaleKeys.picked.tr(); + break; + case 'on the way': + translatedStatus = LocaleKeys.onTheWay.tr(); + break; + case 'delivered': + translatedStatus = LocaleKeys.delivered.tr(); + break; + } + + return Padding( + padding: const EdgeInsets.all(20), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Container( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), + decoration: BoxDecoration( + color: statusColor.withOpacity(0.12), + borderRadius: BorderRadius.circular(100), + ), + child: Text( + translatedStatus, + style: TextStyle( + color: statusColor, + fontWeight: FontWeight.bold, + fontSize: 12, + ), + ), + ), + Text( + "#${order.id.length >= 6 ? order.id.substring(0, 6).toUpperCase() : order.id.toUpperCase()}", + style: TextStyle( + color: Colors.grey.shade400, + fontWeight: FontWeight.bold, + ), + ), + ], + ), + ); + } +} diff --git a/lib/features/track_order/presentation/pages/status_button.dart b/lib/features/track_order/presentation/pages/status_button.dart new file mode 100644 index 0000000..c62ae75 --- /dev/null +++ b/lib/features/track_order/presentation/pages/status_button.dart @@ -0,0 +1,129 @@ +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:go_router/go_router.dart'; +import 'package:tracking_app/app/core/router/route_names.dart'; +import 'package:tracking_app/app/core/ui_helper/color/colors.dart'; +import 'package:tracking_app/app/core/widgets/custom_button.dart'; +import 'package:tracking_app/features/track_order/domain/entities/order_entity.dart'; +import 'package:tracking_app/features/track_order/presentation/manager/cubit/track_order_cubit.dart'; +import 'package:tracking_app/generated/locale_keys.g.dart'; + +class StatusButton extends StatelessWidget { + final OrderEntity order; + + const StatusButton({super.key, required this.order}); + + @override + Widget build(BuildContext context) { + return BlocBuilder( + builder: (context, state) { + final status = order.status.trim().toLowerCase(); + + String buttonText; + String nextStatus; + + switch (status) { + case 'pending': + buttonText = LocaleKeys.accept.tr(); + nextStatus = 'Accepted'; + break; + + case 'accepted': + buttonText = LocaleKeys.arrivedAtPickup.tr(); + nextStatus = 'Arrived'; + break; + + case 'arrived': + buttonText = LocaleKeys.pickUpOrder.tr(); + nextStatus = 'Picked'; + break; + + case 'picked': + buttonText = LocaleKeys.startDelivery.tr(); + nextStatus = 'On the Way'; + break; + + case 'on the way': + buttonText = LocaleKeys.markAsDelivered.tr(); + nextStatus = 'Delivered'; + break; + + case 'delivered': + case 'cancelled': + return const SizedBox.shrink(); + + default: + buttonText = LocaleKeys.accept.tr(); + nextStatus = 'Accepted'; + } + + /// 🔹 Pending → Show Reject + Accept + if (status == 'pending') { + return SizedBox( + width: double.infinity, + child: Row( + children: [ + Expanded( + child: CustomButton( + isEnabled: !state.isLoading, + isLoading: state.isLoading, + text: LocaleKeys.reject.tr(), + isOutlined: true, + color: AppColors.red, + onPressed: () async { + await context.read().updateOrderStatus( + order.id, + 'Cancelled', + ); + }, + ), + ), + const SizedBox(width: 12), + Expanded( + child: CustomButton( + isEnabled: !state.isLoading, + isLoading: state.isLoading, + text: buttonText, + color: AppColors.pink, + onPressed: () async { + if (order.deviceToken == null) return; + + await context.read().updateOrderStatus( + order.id, + nextStatus, + ); + + if (nextStatus == 'Accepted' && context.mounted) { + context.go(RouteNames.ordersDetailsPage); + } + }, + ), + ), + ], + ), + ); + } + + /// 🔹 Other statuses → Single button + return SizedBox( + width: double.infinity, + child: CustomButton( + isEnabled: !state.isLoading, + isLoading: state.isLoading, + text: buttonText, + color: AppColors.pink, + onPressed: () async { + if (order.deviceToken == null) return; + + await context.read().updateOrderStatus( + order.id, + nextStatus, + ); + }, + ), + ); + }, + ); + } +} diff --git a/lib/features/track_order/presentation/pages/status_color.dart b/lib/features/track_order/presentation/pages/status_color.dart new file mode 100644 index 0000000..e69de29 diff --git a/lib/features/track_order/presentation/pages/track_order_page.dart b/lib/features/track_order/presentation/pages/track_order_page.dart new file mode 100644 index 0000000..fdf225e --- /dev/null +++ b/lib/features/track_order/presentation/pages/track_order_page.dart @@ -0,0 +1,116 @@ +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:go_router/go_router.dart'; +import 'package:tracking_app/app/core/router/route_names.dart'; +import 'package:tracking_app/features/track_order/presentation/manager/cubit/track_order_cubit.dart'; +import 'package:tracking_app/app/core/ui_helper/color/colors.dart'; +import 'package:tracking_app/generated/locale_keys.g.dart'; +import 'package:tracking_app/features/track_order/presentation/pages/driver_header.dart'; +import 'package:tracking_app/features/track_order/presentation/pages/order_card.dart'; + +class TrackOrderPage extends StatefulWidget { + const TrackOrderPage({super.key}); + + @override + State createState() => _TrackOrderPageState(); +} + +class _TrackOrderPageState extends State { + @override + void initState() { + super.initState(); + WidgetsBinding.instance.addPostFrameCallback((_) { + context.read().loadUserOrders(); + }); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: Colors.grey.shade100, + appBar: AppBar( + automaticallyImplyLeading: false, + title: Text(LocaleKeys.track_order.tr()), + leading: IconButton( + icon: const Icon(Icons.arrow_back), + onPressed: () => context.go(RouteNames.appStart), + ), + ), + body: BlocConsumer( + listener: (context, state) { + if (state.error != null) { + ScaffoldMessenger.of( + context, + ).showSnackBar(SnackBar(content: Text(state.error!))); + } + }, + builder: (context, state) { + if (state.isLoading) { + return const Center(child: CircularProgressIndicator()); + } + + if (state.orders.isEmpty) { + return Center( + child: Padding( + padding: const EdgeInsets.all(24), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + if (state.error != null) ...[ + const Icon( + Icons.error_outline_rounded, + color: Colors.red, + size: 48, + ), + const SizedBox(height: 16), + Text( + state.error!.contains('multiple indexes') + ? "Database Index Required\nPlease click the link in your console to create the missing Firestore indexes, then retry." + : state.error!, + textAlign: TextAlign.center, + style: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.w500, + ), + ), + const SizedBox(height: 24), + ElevatedButton( + onPressed: () => + context.read().loadUserOrders(), + style: ElevatedButton.styleFrom( + backgroundColor: AppColors.pink, + foregroundColor: Colors.white, + ), + child: const Text("Retry"), + ), + ] else + Text(LocaleKeys.no_orders_found.tr()), + ], + ), + ), + ); + } + + final lastOrder = state.orders.last; + + return Padding( + padding: const EdgeInsets.all(16), + child: Column( + children: [ + if (state.driver != null) + DriverHeader(driverName: state.driver!.name), + + const SizedBox(height: 20), + + Expanded( + child: Center(child: OrderCard(order: lastOrder)), + ), + ], + ), + ); + }, + ), + ); + } +} diff --git a/lib/features/track_order/presentation/widgets/driver_section.dart b/lib/features/track_order/presentation/widgets/driver_section.dart new file mode 100644 index 0000000..7e639dd --- /dev/null +++ b/lib/features/track_order/presentation/widgets/driver_section.dart @@ -0,0 +1,30 @@ +// import 'package:flutter/material.dart'; + +// class DriverSection extends StatelessWidget { +// final dynamic driver; + +// const DriverSection({required this.driver}); + +// @override +// Widget build(BuildContext context) { +// return Card( +// color: Colors.blue.shade50, +// child: Padding( +// padding: const EdgeInsets.all(16), +// child: Column( +// crossAxisAlignment: CrossAxisAlignment.start, +// children: [ +// Text( +// "Driver Information", +// style: Theme.of(context).textTheme.titleMedium, +// ), +// const SizedBox(height: 8), +// Text("Driver ID: ${driver.id}"), +// const SizedBox(height: 8), +// Text("Phone: ${driver.phone ?? 'N/A'}"), +// ], +// ), +// ), +// ); +// } +// } diff --git a/lib/features/track_order/presentation/widgets/order_section.dart b/lib/features/track_order/presentation/widgets/order_section.dart new file mode 100644 index 0000000..8b55c57 --- /dev/null +++ b/lib/features/track_order/presentation/widgets/order_section.dart @@ -0,0 +1,29 @@ +// import 'package:flutter/material.dart'; + +// class OrderSection extends StatelessWidget { +// final dynamic order; + +// const OrderSection({required this.order}); + +// @override +// Widget build(BuildContext context) { +// return Card( +// child: Padding( +// padding: const EdgeInsets.all(16), +// child: Column( +// crossAxisAlignment: CrossAxisAlignment.start, +// children: [ +// Text( +// "Order ID: ${order.id}", +// style: Theme.of(context).textTheme.titleMedium, +// ), +// const SizedBox(height: 8), +// Text("Status: ${order.status}"), +// const SizedBox(height: 8), +// Text("Total: ${order.totalPrice} EGP"), +// ], +// ), +// ), +// ); +// } +// } diff --git a/lib/generated/locale_keys.g.dart b/lib/generated/locale_keys.g.dart new file mode 100644 index 0000000..d74c608 --- /dev/null +++ b/lib/generated/locale_keys.g.dart @@ -0,0 +1,276 @@ +// DO NOT EDIT. This is code generated via package:easy_localization/generate.dart + +// ignore_for_file: constant_identifier_names + +abstract class LocaleKeys { + static const firstName = 'firstName'; + static const lastName = 'lastName'; + static const email = 'email'; + static const password = 'password'; + static const confirmPassword = 'confirmPassword'; + static const phone = 'phone'; + static const gender = 'gender'; + static const enterFirstName = 'enterFirstName'; + static const enterLastName = 'enterLastName'; + static const enterEmail = 'enterEmail'; + static const enterPassword = 'enterPassword'; + static const enterPhoneNumber = 'enterPhoneNumber'; + static const enterRePassword = 'enterRePassword'; + static const femaleGender = 'femaleGender'; + static const maleGender = 'maleGender'; + static const femaleValue = 'femaleValue'; + static const maleValue = 'maleValue'; + static const createAccount = 'createAccount'; + static const termsAndConditions = 'termsAndConditions'; + static const alreadyHaveAccount = 'alreadyHaveAccount'; + static const login = 'login'; + static const signup = 'signup'; + static const emailRequired = 'emailRequired'; + static const emailInvalid = 'emailInvalid'; + static const passwordRequired = 'passwordRequired'; + static const passwordLengthInvalid = 'passwordLengthInvalid'; + static const passwordUpperLetterInvalid = 'passwordUpperLetterInvalid'; + static const passwordLowerLetterInvalid = 'passwordLowerLetterInvalid'; + static const passwordNumbersInvalid = 'passwordNumbersInvalid'; + static const passwordSpecialCharInvalid = 'passwordSpecialCharInvalid'; + static const confirmPasswordRequired = 'confirmPasswordRequired'; + static const passwordsDoNotMatch = 'passwordsDoNotMatch'; + static const phoneRequired = 'phoneRequired'; + static const phoneInvalid = 'phoneInvalid'; + static const firstNameRequired = 'firstNameRequired'; + static const lastNameRequired = 'lastNameRequired'; + static const nameInvalid = 'nameInvalid'; + static const genderRequired = 'genderRequired'; + static const loading = 'loading'; + static const registrationSuccessful = 'registrationSuccessful'; + static const ok = 'ok'; + static const error = 'error'; + static const success = 'success'; + static const emailVerification = 'emailVerification'; + static const rememberMe = 'rememberMe'; + static const forgotPassword = 'forgotPassword'; + static const forgotPasswordTitle = 'forgotPasswordTitle'; + static const continueAsGuest = 'continueAsGuest'; + static const dontHaveAnAccount = 'dontHaveAnAccount'; + static const signUp = 'signUp'; + static const enterYourEmail = 'enterYourEmail'; + static const enterYourPassword = 'enterYourPassword'; + static const associatedEmail = 'associatedEmail'; + static const userName = 'userName'; + static const newPassword = 'newPassword'; + static const confirm = 'confirm'; + static const continueTxt = 'continueTxt'; + static const instruction = 'instruction'; + static const didNotReceive = 'didNotReceive'; + static const resend = 'resend'; + static const resetPassword = 'resetPassword'; + static const yourEmailVerified = 'yourEmailVerified'; + static const check_email_for_verification_code = 'check_email_for_verification_code'; + static const passwordValidation = 'passwordValidation'; + static const connectionTimeout = 'connectionTimeout'; + static const noInternet = 'noInternet'; + static const unauthorized = 'unauthorized'; + static const serverError = 'serverError'; + static const unknownError = 'unknownError'; + static const an_error_occurred = 'an_error_occurred'; + static const weakPassword = 'weakPassword'; + static const passwordWithCapital = 'passwordWithCapital'; + static const passwordWithNumber = 'passwordWithNumber'; + static const passwordDontMatch = 'passwordDontMatch'; + static const confirmPasswordMsg = 'confirmPasswordMsg'; + static const invalidNumber = 'invalidNumber'; + static const required = 'required'; + static const least3Characters = 'least3Characters'; + static const least6Characters = 'least6Characters'; + static const invalidName = 'invalidName'; + static const phoneNumber = 'phoneNumber'; + static const passwordUpdated = 'passwordUpdated'; + static const addToCard = 'addToCard'; + static const noProductsfound = 'noProductsfound'; + static const viewAll = 'viewAll'; + static const search = 'search'; + static const categories = 'categories'; + static const bestSelling = 'bestSelling'; + static const occasions = 'occasions'; + static const allPricesIncludeTax = 'allPricesIncludeTax'; + static const productAddedToCart = 'productAddedToCart'; + static const something_went_wrong = 'something_went_wrong'; + static const cart = 'cart'; + static const items = 'items'; + static const deliverTo = 'deliverTo'; + static const egp = 'egp'; + static const subTotal = 'subTotal'; + static const deliveryFee = 'deliveryFee'; + static const total = 'total'; + static const checkout = 'checkout'; + static const productDeletedSuccessfully = 'productDeletedSuccessfully'; + static const productUpdated = 'productUpdated'; + static const currentPassword = 'currentPassword'; + static const enterCurrentPassword = 'enterCurrentPassword'; + static const enterNewPassword = 'enterNewPassword'; + static const confirmNewPassword = 'confirmNewPassword'; + static const update = 'update'; + static const changePassword = 'changePassword'; + static const no_products_found = 'no_products_found'; + static const change_language = 'change_language'; + static const arabic = 'arabic'; + static const english = 'english'; + static const initialSearchMsg = 'initialSearchMsg'; + static const welcomeMessage = 'welcomeMessage'; + static const home = 'home'; + static const profile = 'profile'; + static const defaultErrorMessage = 'defaultErrorMessage'; + static const bestseller = 'bestseller'; + static const sessionExpiredMessage = 'sessionExpiredMessage'; + static const notificationsKey = 'notificationsKey'; + static const noProfileFound = 'noProfileFound'; + static const register = 'register'; + static const pleaseLoginToAccessProfile = 'pleaseLoginToAccessProfile'; + static const aboutUs = 'aboutUs'; + static const language = 'language'; + static const notifications = 'notifications'; + static const savedAddresses = 'savedAddresses'; + static const myOrders = 'myOrders'; + static const noName = 'noName'; + static const noEmail = 'noEmail'; + static const editProfile = 'editProfile'; + static const logout = 'logout'; + static const logoutFailed = 'logoutFailed'; + static const order_success = 'order_success'; + static const failed_load_addresses = 'failed_load_addresses'; + static const no_addresses = 'no_addresses'; + static const order_status = 'order_status'; + static const delivered = 'delivered'; + static const paid = 'paid'; + static const pending = 'pending'; + static const instant_delivery_info = 'instant_delivery_info'; + static const schedule = 'schedule'; + static const delivery_address = 'delivery_address'; + static const add_new = 'add_new'; + static const payment_method = 'payment_method'; + static const cash_on_delivery = 'cash_on_delivery'; + static const credit_card = 'credit_card'; + static const it_is_a_gift = 'it_is_a_gift'; + static const recipient_name = 'recipient_name'; + static const recipient_phone = 'recipient_phone'; + static const place_order = 'place_order'; + static const instant = 'instant'; + static const arrive_by_datetime = 'arrive_by_datetime'; + static const in_cart = 'in_cart'; + static const invalidRecipientName = 'invalidRecipientName'; + static const invalidAddress = 'invalidAddress'; + static const requiredRecipientName = 'requiredRecipientName'; + static const requiredAddress = 'requiredAddress'; + static const requiredCity = 'requiredCity'; + static const requiredArea = 'requiredArea'; + static const address = 'address'; + static const enter_address = 'enter_address'; + static const phone_number = 'phone_number'; + static const enter_phone_number = 'enter_phone_number'; + static const enter_recipient_name = 'enter_recipient_name'; + static const save_address = 'save_address'; + static const area = 'area'; + static const city = 'city'; + static const location_permission = 'location_permission'; + static const location_service_off_message = 'location_service_off_message'; + static const location_permission_denied_forever_message = 'location_permission_denied_forever_message'; + static const location_permission_denied_message = 'location_permission_denied_message'; + static const open_settings = 'open_settings'; + static const open_location_settings = 'open_location_settings'; + static const allow_location = 'allow_location'; + static const move_map_to_choose_location = 'move_map_to_choose_location'; + static const address_saved_successfully = 'address_saved_successfully'; + static const failed_to_save_address = 'failed_to_save_address'; + static const addNewAddress = 'addNewAddress'; + static const savedAddress = 'savedAddress'; + static const sortBy = 'sortBy'; + static const lowestPrice = 'lowestPrice'; + static const highestPrice = 'highestPrice'; + static const newest = 'newest'; + static const oldest = 'oldest'; + static const discount = 'discount'; + static const filter = 'filter'; + static const active = 'active'; + static const completed = 'completed'; + static const no_orders_found = 'no_orders_found'; + static const track_order = 'track_order'; + static const order_number = 'order_number'; + static const all_notifications_cleared = 'all_notifications_cleared'; + static const notification_deleted_successfully = 'notification_deleted_successfully'; + static const clear_all = 'clear_all'; + static const no_notifications_yet = 'no_notifications_yet'; + static const orders = 'orders'; + static const onboardingTitle = 'onboardingTitle'; + static const onboardingDescription = 'onboardingDescription'; + static const applyNow = 'applyNow'; + static const wrongEmailOrPassword = 'wrongEmailOrPassword'; + static const apply = 'apply'; + static const welcomeApply = 'welcomeApply'; + static const joinTeamMessage = 'joinTeamMessage'; + static const country = 'country'; + static const firstLegalName = 'firstLegalName'; + static const enterFirstLegalName = 'enterFirstLegalName'; + static const secondLegalName = 'secondLegalName'; + static const enterSecondLegalName = 'enterSecondLegalName'; + static const vehicleType = 'vehicleType'; + static const vehicleNumber = 'vehicleNumber'; + static const enterVehicleNumber = 'enterVehicleNumber'; + static const vehicleLicense = 'vehicleLicense'; + static const uploadLicensePhoto = 'uploadLicensePhoto'; + static const idNumber = 'idNumber'; + static const enterNationalId = 'enterNationalId'; + static const idImage = 'idImage'; + static const uploadIdImage = 'uploadIdImage'; + static const continueText = 'continueText'; + static const requiredField = 'requiredField'; + static const licensePhotoRequired = 'licensePhotoRequired'; + static const idImageRequired = 'idImageRequired'; + static const failedToLoadCountries = 'failedToLoadCountries'; + static const failedToLoadVehicles = 'failedToLoadVehicles'; + static const applicationSubmittedSuccessfully = 'applicationSubmittedSuccessfully'; + static const submissionFailed = 'submissionFailed'; + static const applicationSubmitted = 'applicationSubmitted'; + static const congratulationsMessage = 'congratulationsMessage'; + static const reviewMessage = 'reviewMessage'; + static const backToLogin = 'backToLogin'; + static const checkEmailMessage = 'checkEmailMessage'; + static const welcomeBack = 'welcomeBack'; + static const pickupAddress = 'pickupAddress'; + static const userAddress = 'userAddress'; + static const store = 'store'; + static const customer = 'customer'; + static const totalPrice = 'totalPrice'; + static const accept = 'accept'; + static const arrivedAtPickup = 'arrivedAtPickup'; + static const pickUpOrder = 'pickUpOrder'; + static const startDelivery = 'startDelivery'; + static const markAsDelivered = 'markAsDelivered'; + static const accepted = 'accepted'; + static const arrived = 'arrived'; + static const picked = 'picked'; + static const onTheWay = 'onTheWay'; + static const change = 'change'; + static const vehicle_type = 'vehicle_type'; + static const vehicle_number = 'vehicle_number'; + static const vehicle_license = 'vehicle_license'; + static const editDriverProfile = 'editDriverProfile'; + static const editVehicle = 'editVehicle'; + static const cannotBeSame = 'cannotBeSame'; + static const orderDetails = 'orderDetails'; + static const status = 'status'; + static const orderId = 'orderId'; + static const arrivedAtPickupPoint = 'arrivedAtPickupPoint'; + static const arriverAtDestination = 'arriverAtDestination'; + static const confirmDelivery = 'confirmDelivery'; + static const deliveryConfirmed = 'deliveryConfirmed'; + static const orderCompleted = 'orderCompleted'; + static const pickedUp = 'pickedUp'; + static const outForDelivery = 'outForDelivery'; + static const driverOrderTitle = 'driverOrderTitle'; + static const unknownStore = 'unknownStore'; + static const noAddress = 'noAddress'; + static const reject = 'reject'; + static const noPendingOrders = 'noPendingOrders'; + static const floweryRider = 'floweryRider'; + +} diff --git a/lib/main.dart b/lib/main.dart index 5281b8d..be1f06a 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -11,7 +11,7 @@ import 'package:tracking_app/firebase_options.dart'; Future main() async { WidgetsFlutterBinding.ensureInitialized(); await EasyLocalization.ensureInitialized(); - configureDependencies(); + await configureDependencies(); await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform); FirebaseMessaging.onBackgroundMessage( CloudMessaging.firebaseMessagingBackgroundHandler, diff --git a/login_test_output.txt b/login_test_output.txt new file mode 100644 index 0000000..bb06f43 Binary files /dev/null and b/login_test_output.txt differ diff --git a/login_test_output_utf8.txt b/login_test_output_utf8.txt new file mode 100644 index 0000000..c91df39 --- /dev/null +++ b/login_test_output_utf8.txt @@ -0,0 +1,58 @@ +00:00 +0: loading C:/Users/20101/StudioProjects/tracking_app/test/features/auth/presentation/login/pages/loginScreen_test.dart +00:00 +0: (setUpAll) +[­ƒîÄ Easy Localization] [DEBUG] Localization initialized +00:00 +0: LoginScreen renders correctly +[­ƒîÄ Easy Localization] [DEBUG] Start +[­ƒîÄ Easy Localization] [DEBUG] Init state +[­ƒîÄ Easy Localization] [DEBUG] Build +[­ƒîÄ Easy Localization] [DEBUG] Init Localization Delegate +[­ƒîÄ Easy Localization] [DEBUG] Init provider +[­ƒîÄ Easy Localization] [WARNING] Localization key [login] not found +[­ƒîÄ Easy Localization] [WARNING] Localization key [email] not found +[­ƒîÄ Easy Localization] [WARNING] Localization key [enterEmail] not found +[­ƒîÄ Easy Localization] [WARNING] Localization key [password] not found +[­ƒîÄ Easy Localization] [WARNING] Localization key [enterPassword] not found +[­ƒîÄ Easy Localization] [WARNING] Localization key [rememberMe] not found +[­ƒîÄ Easy Localization] [WARNING] Localization key [forgotPasswordTitle] not found +[­ƒîÄ Easy Localization] [WARNING] Localization key [login] not found +ÔòÉÔòÉÔòí EXCEPTION CAUGHT BY FLUTTER TEST FRAMEWORK Ôò×ÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉ +The following TestFailure was thrown running a test: +Expected: exactly one matching candidate + Actual: _TextWidgetFinder: + Which: means none were found but one was expected + +When the exception was thrown, this was the stack: +#4 main. (file:///C:/Users/20101/StudioProjects/tracking_app/test/features/auth/presentation/login/pages/loginScreen_test.dart:64:5) + +#5 testWidgets.. (package:flutter_test/src/widget_tester.dart:192:15) + +#6 TestWidgetsFlutterBinding._runTestBody (package:flutter_test/src/binding.dart:1059:5) + + +(elided one frame from package:stack_trace) + +This was caught by the test expectation on the following line: + file:///C:/Users/20101/StudioProjects/tracking_app/test/features/auth/presentation/login/pages/loginScreen_test.dart line 64 +The test description was: + LoginScreen renders correctly +ÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉ +00:02 +0 -1: LoginScreen renders correctly [E] + Test failed. See exception logs above. + The test description was: LoginScreen renders correctly + +00:02 +0 -1: Enters text into email and password fields +[­ƒîÄ Easy Localization] [DEBUG] Start +[­ƒîÄ Easy Localization] [DEBUG] Init state +[­ƒîÄ Easy Localization] [DEBUG] Build +[­ƒîÄ Easy Localization] [DEBUG] Init Localization Delegate +[­ƒîÄ Easy Localization] [DEBUG] Init provider +[­ƒîÄ Easy Localization] [WARNING] Localization key [login] not found +[­ƒîÄ Easy Localization] [WARNING] Localization key [email] not found +[­ƒîÄ Easy Localization] [WARNING] Localization key [enterEmail] not found +[­ƒîÄ Easy Localization] [WARNING] Localization key [password] not found +[­ƒîÄ Easy Localization] [WARNING] Localization key [enterPassword] not found +[­ƒîÄ Easy Localization] [WARNING] Localization key [rememberMe] not found +[­ƒîÄ Easy Localization] [WARNING] Localization key [forgotPasswordTitle] not found +[­ƒîÄ Easy Localization] [WARNING] Localization key [login] not found +00:03 +1 -1: (tearDownAll) +00:03 +1 -1: Some tests failed. diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index cac8596..2038a48 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -5,22 +5,30 @@ import FlutterMacOS import Foundation +import cloud_firestore import file_selector_macos +import firebase_auth import firebase_core import firebase_crashlytics import firebase_messaging import flutter_local_notifications import geolocator_apple +import package_info_plus import shared_preferences_foundation +import sqflite_darwin import url_launcher_macos func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { + FLTFirebaseFirestorePlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseFirestorePlugin")) FileSelectorPlugin.register(with: registry.registrar(forPlugin: "FileSelectorPlugin")) + FLTFirebaseAuthPlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseAuthPlugin")) FLTFirebaseCorePlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseCorePlugin")) FLTFirebaseCrashlyticsPlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseCrashlyticsPlugin")) FLTFirebaseMessagingPlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseMessagingPlugin")) FlutterLocalNotificationsPlugin.register(with: registry.registrar(forPlugin: "FlutterLocalNotificationsPlugin")) GeolocatorPlugin.register(with: registry.registrar(forPlugin: "GeolocatorPlugin")) + FPPPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FPPPackageInfoPlusPlugin")) SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin")) + SqflitePlugin.register(with: registry.registrar(forPlugin: "SqflitePlugin")) UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin")) } diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..f60c6cd --- /dev/null +++ b/package-lock.json @@ -0,0 +1,6 @@ +{ + "name": "tracking_app", + "lockfileVersion": 3, + "requires": true, + "packages": {} +} diff --git a/pubspec.lock b/pubspec.lock index 779a2a3..f4eaa7d 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -5,34 +5,42 @@ packages: dependency: transitive description: name: _fe_analyzer_shared - sha256: "0b2f2bd91ba804e53a61d757b986f89f1f9eaed5b11e4b2f5a2468d86d6c9fc7" + sha256: c209688d9f5a5f26b2fb47a188131a6fb9e876ae9e47af3737c0b4f58a93470d url: "https://pub.dev" source: hosted - version: "67.0.0" + version: "91.0.0" _flutterfire_internals: dependency: transitive description: name: _flutterfire_internals - sha256: cd83f7d6bd4e4c0b0b4fef802e8796784032e1cc23d7b0e982cf5d05d9bbe182 + sha256: afe15ce18a287d2f89da95566e62892df339b1936bbe9b83587df45b944ee72a url: "https://pub.dev" source: hosted - version: "1.3.66" + version: "1.3.67" analyzer: dependency: transitive description: name: analyzer - sha256: "37577842a27e4338429a1cbc32679d508836510b056f1eedf0c8d20e39c1383d" + sha256: f51c8499b35f9b26820cfe914828a6a98a94efd5cc78b37bb7d03debae3a1d08 url: "https://pub.dev" source: hosted - version: "6.4.1" + version: "8.4.1" + ansicolor: + dependency: transitive + description: + name: ansicolor + sha256: "50e982d500bc863e1d703448afdbf9e5a72eb48840a4f766fa361ffd6877055f" + url: "https://pub.dev" + source: hosted + version: "2.0.3" archive: dependency: transitive description: name: archive - sha256: "2fde1607386ab523f7a36bb3e7edb43bd58e6edaf2ffb29d8a6d578b297fdbbd" + sha256: a96e8b390886ee8abb49b7bd3ac8df6f451c621619f52a26e815fdcf568959ff url: "https://pub.dev" source: hosted - version: "4.0.7" + version: "4.0.9" args: dependency: transitive description: @@ -77,18 +85,18 @@ packages: dependency: transitive description: name: build - sha256: "80184af8b6cb3e5c1c4ec6d8544d27711700bc3e6d2efad04238c7b5290889f0" + sha256: "275bf6bb2a00a9852c28d4e0b410da1d833a734d57d39d44f94bfc895a484ec3" url: "https://pub.dev" source: hosted - version: "2.4.1" + version: "4.0.4" build_config: dependency: transitive description: name: build_config - sha256: "4ae2de3e1e67ea270081eaee972e1bd8f027d459f249e0f1186730784c2e7e33" + sha256: "4f64382b97504dc2fcdf487d5aae33418e08b4703fc21249e4db6d804a4d0187" url: "https://pub.dev" source: hosted - version: "1.1.2" + version: "1.2.0" build_daemon: dependency: transitive description: @@ -97,30 +105,14 @@ packages: url: "https://pub.dev" source: hosted version: "4.1.1" - build_resolvers: - dependency: transitive - description: - name: build_resolvers - sha256: "339086358431fa15d7eca8b6a36e5d783728cf025e559b834f4609a1fcfb7b0a" - url: "https://pub.dev" - source: hosted - version: "2.4.2" build_runner: dependency: "direct dev" description: name: build_runner - sha256: "028819cfb90051c6b5440c7e574d1896f8037e3c96cf17aaeb054c9311cfbf4d" + sha256: "39ad4ca8a2876779737c60e4228b4bcd35d4352ef7e14e47514093edc012c734" url: "https://pub.dev" source: hosted - version: "2.4.13" - build_runner_core: - dependency: transitive - description: - name: build_runner_core - sha256: f8126682b87a7282a339b871298cc12009cb67109cfa1614d6436fb0289193e0 - url: "https://pub.dev" - source: hosted - version: "7.3.2" + version: "2.11.1" built_collection: dependency: transitive description: @@ -133,10 +125,34 @@ packages: dependency: transitive description: name: built_value - sha256: "7931c90b84bc573fef103548e354258ae4c9d28d140e41961df6843c5d60d4d8" + sha256: "6ae8a6435a8c6520c7077b107e77f1fb4ba7009633259a4d49a8afd8e7efc5e9" + url: "https://pub.dev" + source: hosted + version: "8.12.4" + cached_network_image: + dependency: "direct main" + description: + name: cached_network_image + sha256: "7c1183e361e5c8b0a0f21a28401eecdbde252441106a9816400dd4c2b2424916" + url: "https://pub.dev" + source: hosted + version: "3.4.1" + cached_network_image_platform_interface: + dependency: transitive + description: + name: cached_network_image_platform_interface + sha256: "35814b016e37fbdc91f7ae18c8caf49ba5c88501813f73ce8a07027a395e2829" url: "https://pub.dev" source: hosted - version: "8.12.3" + version: "4.1.1" + cached_network_image_web: + dependency: transitive + description: + name: cached_network_image_web + sha256: "980842f4e8e2535b8dbd3d5ca0b1f0ba66bf61d14cc3a17a9b4788a3685ba062" + url: "https://pub.dev" + source: hosted + version: "1.3.1" characters: dependency: transitive description: @@ -169,6 +185,38 @@ packages: url: "https://pub.dev" source: hosted version: "1.1.2" + cloud_firestore: + dependency: "direct main" + description: + name: cloud_firestore + sha256: "54484b2fc49f41b46f35b60a54b12351181eeaad22c0e3def276a81e17ae7c9b" + url: "https://pub.dev" + source: hosted + version: "6.1.2" + cloud_firestore_platform_interface: + dependency: transitive + description: + name: cloud_firestore_platform_interface + sha256: c110a968cc11a83f0e2b88c335340cb21d123ab9e440aaa834eb33c20505893c + url: "https://pub.dev" + source: hosted + version: "7.0.7" + cloud_firestore_web: + dependency: transitive + description: + name: cloud_firestore_web + sha256: ff6c87ecb167f35e84027a36b1f65994da6a81e1b4eb14d0a8cdd2be09e861c3 + url: "https://pub.dev" + source: hosted + version: "5.1.3" + code_assets: + dependency: transitive + description: + name: code_assets + sha256: "83ccdaa064c980b5596c35dd64a8d3ecc68620174ab9b90b6343b753aa721687" + url: "https://pub.dev" + source: hosted + version: "1.0.0" code_builder: dependency: transitive description: @@ -237,10 +285,10 @@ packages: dependency: transitive description: name: dart_style - sha256: "99e066ce75c89d6b29903d788a7bb9369cf754f7b24bf70bf4b6d6d6b26853b9" + sha256: a9c30492da18ff84efe2422ba2d319a89942d93e58eb0b73d32abe822ef54b7b url: "https://pub.dev" source: hosted - version: "2.3.6" + version: "3.1.3" dbus: dependency: transitive description: @@ -261,18 +309,18 @@ packages: dependency: "direct main" description: name: dio - sha256: b9d46faecab38fc8cc286f80bc4d61a3bb5d4ac49e51ed877b4d6706efe57b25 + sha256: aff32c08f92787a557dd5c0145ac91536481831a01b4648136373cddb0e64f8c url: "https://pub.dev" source: hosted - version: "5.9.1" + version: "5.9.2" dio_web_adapter: dependency: transitive description: name: dio_web_adapter - sha256: "7586e476d70caecaf1686d21eee7247ea43ef5c345eab9e0cc3583ff13378d78" + sha256: "2f9e64323a7c3c7ef69567d5c800424a11f8337b8b228bad02524c9fb3c1f340" url: "https://pub.dev" source: hosted - version: "2.1.1" + version: "2.1.2" easy_localization: dependency: "direct main" description: @@ -309,10 +357,10 @@ packages: dependency: transitive description: name: ffi - sha256: d07d37192dbf97461359c1518788f203b0c9102cfd2c35a716b823741219542c + sha256: "6d7fd89431262d8f3125e81b50d3847a091d846eafcd4fdb88dd06f36d705a45" url: "https://pub.dev" source: hosted - version: "2.1.5" + version: "2.2.0" file: dependency: transitive description: @@ -353,14 +401,38 @@ packages: url: "https://pub.dev" source: hosted version: "0.9.3+5" + firebase_auth: + dependency: "direct main" + description: + name: firebase_auth + sha256: "1c290de59ba88d3b193e5933441ea4793d623e802d75bd4135e36d550c3f6b62" + url: "https://pub.dev" + source: hosted + version: "6.2.0" + firebase_auth_platform_interface: + dependency: transitive + description: + name: firebase_auth_platform_interface + sha256: c830e2a1c69c27242a920296784458ad6eb71decdfa083578f7788dbde5d3a69 + url: "https://pub.dev" + source: hosted + version: "8.1.7" + firebase_auth_web: + dependency: transitive + description: + name: firebase_auth_web + sha256: "809d0807a7b6dbdd2d2dd04f217375aaa9835794750a4eec408c2990ed505e41" + url: "https://pub.dev" + source: hosted + version: "6.1.3" firebase_core: dependency: "direct main" description: name: firebase_core - sha256: "923085c881663ef685269b013e241b428e1fb03cdd0ebde265d9b40ff18abf80" + sha256: f0997fee80fbb6d2c658c5b88ae87ba1f9506b5b37126db64fc2e75d8e977fbb url: "https://pub.dev" source: hosted - version: "4.4.0" + version: "4.5.0" firebase_core_platform_interface: dependency: transitive description: @@ -373,50 +445,50 @@ packages: dependency: transitive description: name: firebase_core_web - sha256: "83e7356c704131ca4d8d8dd57e360d8acecbca38b1a3705c7ae46cc34c708084" + sha256: "856ca92bf2d75a63761286ab8e791bda3a85184c2b641764433b619647acfca6" url: "https://pub.dev" source: hosted - version: "3.4.0" + version: "3.5.0" firebase_crashlytics: dependency: "direct main" description: name: firebase_crashlytics - sha256: a6e6cb8b2ea1214533a54e4c1b11b19c40f6a29333f3ab0854a479fdc3237c5b + sha256: "2a6dc88d762af01790a05ff0cf814f7d4020050e8c69dec01962d9ed5dc1a531" url: "https://pub.dev" source: hosted - version: "5.0.7" + version: "5.0.8" firebase_crashlytics_platform_interface: dependency: transitive description: name: firebase_crashlytics_platform_interface - sha256: fc6837c4c64c48fa94cab8a872a632b9194fa9208ca76a822f424b3da945584d + sha256: "5fd59d76d691f370e42fd2b786d46078e69ed4126ca0d84b585119f55cd97937" url: "https://pub.dev" source: hosted - version: "3.8.17" + version: "3.8.18" firebase_messaging: dependency: "direct main" description: name: firebase_messaging - sha256: "06fad40ea14771e969a8f2bbce1944aa20ee2f4f57f4eca5b3ba346b65f3f644" + sha256: bd17823b70e629877904d384841cda72ed2cc197517404c0c90da5c0ba786a8c url: "https://pub.dev" source: hosted - version: "16.1.1" + version: "16.1.2" firebase_messaging_platform_interface: dependency: transitive description: name: firebase_messaging_platform_interface - sha256: "6c49e901c77e6e10e86d98e32056a087eb1ca1b93acdf58524f1961e617657b7" + sha256: "550435235cc7d53683f32bf0762c28ef8cfc20a8d36318a033676ae09526d7fb" url: "https://pub.dev" source: hosted - version: "4.7.6" + version: "4.7.7" firebase_messaging_web: dependency: transitive description: name: firebase_messaging_web - sha256: "2756f8fea583ffb9d294d15ddecb3a9ad429b023b70c9990c151fc92c54a32b3" + sha256: "6b1b93ed90309fbce91c219e3cd32aa831e8eccaf4a61f3afaea1625479275d2" url: "https://pub.dev" source: hosted - version: "4.1.2" + version: "4.1.3" fixnum: dependency: transitive description: @@ -438,6 +510,14 @@ packages: url: "https://pub.dev" source: hosted version: "9.1.1" + flutter_cache_manager: + dependency: transitive + description: + name: flutter_cache_manager + sha256: "400b6592f16a4409a7f2bb929a9a7e38c72cceb8ffb99ee57bbf2cb2cecf8386" + url: "https://pub.dev" + source: hosted + version: "3.4.1" flutter_lints: dependency: "direct dev" description: @@ -450,10 +530,10 @@ packages: dependency: "direct main" description: name: flutter_local_notifications - sha256: "76cd20bcfa72fabe50ea27eeaf165527f446f55d3033021462084b87805b4cac" + sha256: "2b50e938a275e1ad77352d6a25e25770f4130baa61eaf02de7a9a884680954ad" url: "https://pub.dev" source: hosted - version: "20.0.0" + version: "20.1.0" flutter_local_notifications_linux: dependency: transitive description: @@ -474,10 +554,10 @@ packages: dependency: transitive description: name: flutter_local_notifications_windows - sha256: "7ddd964fa85b6a23e96956c5b63ef55cdb9e5947b71b95712204db42ad46da61" + sha256: e97a1a3016512437d9c0b12fae7d1491c3c7b9aa7f03a69b974308840656b02a url: "https://pub.dev" source: hosted - version: "2.0.0" + version: "2.0.1" flutter_localizations: dependency: transitive description: flutter @@ -525,22 +605,30 @@ packages: url: "https://pub.dev" source: hosted version: "4.0.0" + geoclue: + dependency: transitive + description: + name: geoclue + sha256: c2a998c77474fc57aa00c6baa2928e58f4b267649057a1c76738656e9dbd2a7f + url: "https://pub.dev" + source: hosted + version: "0.1.1" geolocator: dependency: "direct main" description: name: geolocator - sha256: f4efb8d3c4cdcad2e226af9661eb1a0dd38c71a9494b22526f9da80ab79520e5 + sha256: "79939537046c9025be47ec645f35c8090ecadb6fe98eba146a0d25e8c1357516" url: "https://pub.dev" source: hosted - version: "10.1.1" + version: "14.0.2" geolocator_android: dependency: transitive description: name: geolocator_android - sha256: fcb1760a50d7500deca37c9a666785c047139b5f9ee15aa5469fae7dbbe3170d + sha256: "179c3cb66dfa674fc9ccbf2be872a02658724d1c067634e2c427cf6df7df901a" url: "https://pub.dev" source: hosted - version: "4.6.2" + version: "5.0.2" geolocator_apple: dependency: transitive description: @@ -549,6 +637,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.3.13" + geolocator_linux: + dependency: transitive + description: + name: geolocator_linux + sha256: d64112a205931926f4363bb6bd48f14cb38e7326833041d170615586cd143797 + url: "https://pub.dev" + source: hosted + version: "0.2.4" geolocator_platform_interface: dependency: transitive description: @@ -561,10 +657,10 @@ packages: dependency: transitive description: name: geolocator_web - sha256: "102e7da05b48ca6bf0a5bda0010f886b171d1a08059f01bfe02addd0175ebece" + sha256: b1ae9bdfd90f861fde8fd4f209c37b953d65e92823cb73c7dee1fa021b06f172 url: "https://pub.dev" source: hosted - version: "2.2.1" + version: "4.1.3" geolocator_windows: dependency: transitive description: @@ -577,10 +673,10 @@ packages: dependency: "direct main" description: name: get_it - sha256: "1d648d2dd2047d7f7450d5727ca24ee435f240385753d90b49650e3cdff32e56" + sha256: "568d62f0e68666fb5d95519743b3c24a34c7f19d834b0658c46e26d778461f66" url: "https://pub.dev" source: hosted - version: "9.2.0" + version: "9.2.1" glob: dependency: transitive description: @@ -593,10 +689,10 @@ packages: dependency: "direct main" description: name: go_router - sha256: b465e99ce64ba75e61c8c0ce3d87b66d8ac07f0b35d0a7e0263fcfc10f99e836 + sha256: "7974313e217a7771557add6ff2238acb63f635317c35fa590d348fb238f00896" url: "https://pub.dev" source: hosted - version: "13.2.5" + version: "17.1.0" google_maps: dependency: transitive description: @@ -609,34 +705,34 @@ packages: dependency: "direct main" description: name: google_maps_flutter - sha256: "819985697596a42e1054b5feb2f407ba1ac92262e02844a40168e742b9f36dca" + sha256: "9b0d6dab3de6955837575dc371dd772fcb5d0a90f6a4954e8c066472f9938550" url: "https://pub.dev" source: hosted - version: "2.14.0" + version: "2.14.2" google_maps_flutter_android: dependency: transitive description: name: google_maps_flutter_android - sha256: "98d7f5354f770f3e993db09fc798d40aeb6a254f04c1c468a94818ec2086e83e" + sha256: ba0947315ddc9107ecc8d95fa26eb3b87b4f27b221606ce72518314d99c7306c url: "https://pub.dev" source: hosted - version: "2.18.12" + version: "2.19.2" google_maps_flutter_ios: dependency: transitive description: name: google_maps_flutter_ios - sha256: "0504508a024410979936bd22bc2dc10a0df5cb1d15a21618d6cfbd973832464f" + sha256: "174d730bc3f253e1c06a342d7a5efb216f15003a6e26693c2d70d60973625af4" url: "https://pub.dev" source: hosted - version: "2.17.1" + version: "2.17.5" google_maps_flutter_platform_interface: dependency: transitive description: name: google_maps_flutter_platform_interface - sha256: e8b1232419fcdd35c1fdafff96843f5a40238480365599d8ca661dde96d283dd + sha256: "0f8c6674d70c7e9a09cd34f63b18ebaf8a5822e85b558128eae0fdf02b4a3e93" url: "https://pub.dev" source: hosted - version: "2.14.1" + version: "2.14.2" google_maps_flutter_web: dependency: transitive description: @@ -653,6 +749,30 @@ packages: url: "https://pub.dev" source: hosted version: "2.3.2" + gsettings: + dependency: transitive + description: + name: gsettings + sha256: "1b0ce661f5436d2db1e51f3c4295a49849f03d304003a7ba177d01e3a858249c" + url: "https://pub.dev" + source: hosted + version: "0.2.8" + hooks: + dependency: transitive + description: + name: hooks + sha256: "7a08a0d684cb3b8fb604b78455d5d352f502b68079f7b80b831c62220ab0a4f6" + url: "https://pub.dev" + source: hosted + version: "1.0.1" + hotreloader: + dependency: transitive + description: + name: hotreloader + sha256: bc167a1163807b03bada490bfe2df25b0d744df359227880220a5cbd04e5734b + url: "https://pub.dev" + source: hosted + version: "4.3.0" html: dependency: transitive description: @@ -697,10 +817,10 @@ packages: dependency: transitive description: name: image_picker_android - sha256: "518a16108529fc18657a3e6dde4a043dc465d16596d20ab2abd49a4cac2e703d" + sha256: eda9b91b7e266d9041084a42d605a74937d996b87083395c5e47835916a86156 url: "https://pub.dev" source: hosted - version: "0.8.13+13" + version: "0.8.13+14" image_picker_for_web: dependency: transitive description: @@ -713,10 +833,10 @@ packages: dependency: transitive description: name: image_picker_ios - sha256: "956c16a42c0c708f914021666ffcd8265dde36e673c9fa68c81f7d085d9774ad" + sha256: b9c4a438a9ff4f60808c9cf0039b93a42bb6c2211ef6ebb647394b2b3fa84588 url: "https://pub.dev" source: hosted - version: "0.8.13+3" + version: "0.8.13+6" image_picker_linux: dependency: transitive description: @@ -753,18 +873,18 @@ packages: dependency: "direct main" description: name: injectable - sha256: "32e9bac6fe9c84339c5add60478d27a01e363ce1ad5c22ca7e525c6b28a7559c" + sha256: "32b36a9d87f18662bee0b1951b81f47a01f2bf28cd6ea94f60bc5453c7bf598c" url: "https://pub.dev" source: hosted - version: "2.7.0" + version: "2.7.1+4" injectable_generator: dependency: "direct dev" description: name: injectable_generator - sha256: af403d76c7b18b4217335e0075e950cd0579fd7f8d7bd47ee7c85ada31680ba1 + sha256: "309c3f3546160dd00b575f16b341a6a3025479950441bcc7fcb2f8404a40d326" url: "https://pub.dev" source: hosted - version: "2.6.2" + version: "2.9.1" intl: dependency: "direct main" description: @@ -801,10 +921,10 @@ packages: dependency: "direct dev" description: name: json_serializable - sha256: ea1432d167339ea9b5bb153f0571d0039607a873d6e04e0117af043f14a1fd4b + sha256: c5b2ee75210a0f263c6c7b9eeea80553dbae96ea1bf57f02484e806a3ffdffa3 url: "https://pub.dev" source: hosted - version: "6.8.0" + version: "6.11.2" leak_tracker: dependency: transitive description: @@ -829,6 +949,14 @@ packages: url: "https://pub.dev" source: hosted version: "3.0.2" + lean_builder: + dependency: transitive + description: + name: lean_builder + sha256: "6af3cfbf34400eb14b89fe20111e5981e7083362f00ea10b9ed2a6e833250d76" + url: "https://pub.dev" + source: hosted + version: "0.1.6" lints: dependency: transitive description: @@ -889,10 +1017,10 @@ packages: dependency: "direct dev" description: name: mockito - sha256: "6841eed20a7befac0ce07df8116c8b8233ed1f4486a7647c7fc5a02ae6163917" + sha256: a45d1aa065b796922db7b9e7e7e45f921aed17adf3a8318a1f47097e7e695566 url: "https://pub.dev" source: hosted - version: "5.4.4" + version: "5.6.3" mocktail: dependency: "direct dev" description: @@ -901,6 +1029,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.4" + native_toolchain_c: + dependency: transitive + description: + name: native_toolchain_c + sha256: "89e83885ba09da5fdf2cdacc8002a712ca238c28b7f717910b34bcd27b0d03ac" + url: "https://pub.dev" + source: hosted + version: "0.17.4" nested: dependency: transitive description: @@ -925,6 +1061,22 @@ packages: url: "https://pub.dev" source: hosted version: "2.0.2" + objective_c: + dependency: transitive + description: + name: objective_c + sha256: "100a1c87616ab6ed41ec263b083c0ef3261ee6cd1dc3b0f35f8ddfa4f996fe52" + url: "https://pub.dev" + source: hosted + version: "9.3.0" + octo_image: + dependency: transitive + description: + name: octo_image + sha256: "34faa6639a78c7e3cbe79be6f9f96535867e879748ade7d17c9b1ae7536293bd" + url: "https://pub.dev" + source: hosted + version: "2.1.0" package_config: dependency: transitive description: @@ -933,6 +1085,22 @@ packages: url: "https://pub.dev" source: hosted version: "2.2.0" + package_info_plus: + dependency: transitive + description: + name: package_info_plus + sha256: f69da0d3189a4b4ceaeb1a3defb0f329b3b352517f52bed4290f83d4f06bc08d + url: "https://pub.dev" + source: hosted + version: "9.0.0" + package_info_plus_platform_interface: + dependency: transitive + description: + name: package_info_plus_platform_interface + sha256: "202a487f08836a592a6bd4f901ac69b3a8f146af552bbd14407b6b41e1c3f086" + url: "https://pub.dev" + source: hosted + version: "3.2.1" path: dependency: transitive description: @@ -949,6 +1117,30 @@ packages: url: "https://pub.dev" source: hosted version: "1.1.0" + path_provider: + dependency: transitive + description: + name: path_provider + sha256: "50c5dd5b6e1aaf6fb3a78b33f6aa3afca52bf903a8a5298f53101fdaee55bbcd" + url: "https://pub.dev" + source: hosted + version: "2.1.5" + path_provider_android: + dependency: transitive + description: + name: path_provider_android + sha256: f2c65e21139ce2c3dad46922be8272bb5963516045659e71bb16e151c93b580e + url: "https://pub.dev" + source: hosted + version: "2.2.22" + path_provider_foundation: + dependency: transitive + description: + name: path_provider_foundation + sha256: "2a376b7d6392d80cd3705782d2caa734ca4727776db0b6ec36ef3f1855197699" + url: "https://pub.dev" + source: hosted + version: "2.6.0" path_provider_linux: dependency: transitive description: @@ -977,10 +1169,10 @@ packages: dependency: transitive description: name: petitparser - sha256: "1a97266a94f7350d30ae522c0af07890c70b8e62c71e8e3920d1db4d23c057d1" + sha256: "91bd59303e9f769f108f8df05e371341b15d59e995e6806aefab827b58336675" url: "https://pub.dev" source: hosted - version: "7.0.1" + version: "7.0.2" platform: dependency: transitive description: @@ -1009,10 +1201,10 @@ packages: dependency: transitive description: name: posix - sha256: "6323a5b0fa688b6a010df4905a56b00181479e6d10534cecfecede2aa55add61" + sha256: "185ef7606574f789b40f289c233efa52e96dead518aed988e040a10737febb07" url: "https://pub.dev" source: hosted - version: "6.0.3" + version: "6.5.0" pretty_dio_logger: dependency: "direct main" description: @@ -1021,6 +1213,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.4.0" + protobuf: + dependency: transitive + description: + name: protobuf + sha256: "75ec242d22e950bdcc79ee38dd520ce4ee0bc491d7fadc4ea47694604d22bf06" + url: "https://pub.dev" + source: hosted + version: "6.0.0" provider: dependency: "direct main" description: @@ -1057,18 +1257,26 @@ packages: dependency: "direct main" description: name: retrofit - sha256: "84063c18a00d55af41d6b8401edf8473e8c215bd7068ef7ec5e34c60657ffdbe" + sha256: "0f629ed26b2c48c66fe54bd548313c6fdf7955be18bff37e08a46dd3f97f8eaf" url: "https://pub.dev" source: hosted - version: "4.9.1" + version: "4.9.2" retrofit_generator: dependency: "direct dev" description: name: retrofit_generator - sha256: "9499eb46b3657a62192ddbc208ff7e6c6b768b19e83c1ee6f6b119c864b99690" + sha256: "2381d86c7291b55bf1d3b30d12054a74c417ba97321afbd73cb25be0e6fa401f" + url: "https://pub.dev" + source: hosted + version: "10.2.3" + rxdart: + dependency: transitive + description: + name: rxdart + sha256: "5c3004a4a8dbb94bd4bf5412a4def4acdaa12e12f269737a5751369e12d1a962" url: "https://pub.dev" source: hosted - version: "7.0.8" + version: "0.28.0" sanitize_html: dependency: transitive description: @@ -1089,10 +1297,10 @@ packages: dependency: transitive description: name: shared_preferences_android - sha256: cbc40be9be1c5af4dab4d6e0de4d5d3729e6f3d65b89d21e1815d57705644a6f + sha256: "8374d6200ab33ac99031a852eba4c8eb2170c4bf20778b3e2c9eccb45384fb41" url: "https://pub.dev" source: hosted - version: "2.4.20" + version: "2.4.21" shared_preferences_foundation: dependency: transitive description: @@ -1161,10 +1369,10 @@ packages: dependency: transitive description: name: shelf_web_socket - sha256: cc36c297b52866d203dbf9332263c94becc2fe0ceaa9681d07b6ef9807023b67 + sha256: "3632775c8e90d6c9712f883e633716432a27758216dfb61bd86a8321c0580925" url: "https://pub.dev" source: hosted - version: "2.0.1" + version: "3.0.0" shimmer: dependency: "direct main" description: @@ -1177,10 +1385,10 @@ packages: dependency: "direct main" description: name: skeletonizer - sha256: "83157d8e2e41f0252079cfec496281c16e4c63660052dab8d4cd72a206bb7109" + sha256: "9f38f9b47ec3cf2235a6a4f154a88a95432bc55ba98b3e2eb6ced5c1974bc122" url: "https://pub.dev" source: hosted - version: "2.1.2" + version: "2.1.3" sky_engine: dependency: transitive description: flutter @@ -1190,18 +1398,18 @@ packages: dependency: transitive description: name: source_gen - sha256: "14658ba5f669685cd3d63701d01b31ea748310f7ab854e471962670abcf57832" + sha256: "1d562a3c1f713904ebbed50d2760217fd8a51ca170ac4b05b0db490699dbac17" url: "https://pub.dev" source: hosted - version: "1.5.0" + version: "4.2.0" source_helper: dependency: transitive description: name: source_helper - sha256: "86d247119aedce8e63f4751bd9626fc9613255935558447569ad42f9f5b48b3c" + sha256: "6a3c6cc82073a8797f8c4dc4572146114a39652851c157db37e964d9c7038723" url: "https://pub.dev" source: hosted - version: "1.3.5" + version: "1.3.8" source_map_stack_trace: dependency: transitive description: @@ -1226,6 +1434,46 @@ packages: url: "https://pub.dev" source: hosted version: "1.10.2" + sqflite: + dependency: transitive + description: + name: sqflite + sha256: e2297b1da52f127bc7a3da11439985d9b536f75070f3325e62ada69a5c585d03 + url: "https://pub.dev" + source: hosted + version: "2.4.2" + sqflite_android: + dependency: transitive + description: + name: sqflite_android + sha256: ecd684501ebc2ae9a83536e8b15731642b9570dc8623e0073d227d0ee2bfea88 + url: "https://pub.dev" + source: hosted + version: "2.4.2+2" + sqflite_common: + dependency: transitive + description: + name: sqflite_common + sha256: "6ef422a4525ecc601db6c0a2233ff448c731307906e92cabc9ba292afaae16a6" + url: "https://pub.dev" + source: hosted + version: "2.5.6" + sqflite_darwin: + dependency: transitive + description: + name: sqflite_darwin + sha256: "279832e5cde3fe99e8571879498c9211f3ca6391b0d818df4e17d9fff5c6ccb3" + url: "https://pub.dev" + source: hosted + version: "2.4.2" + sqflite_platform_interface: + dependency: transitive + description: + name: sqflite_platform_interface + sha256: "8dd4515c7bdcae0a785b0062859336de775e8c65db81ae33dd5445f35be61920" + url: "https://pub.dev" + source: hosted + version: "2.4.0" stack_trace: dependency: transitive description: @@ -1258,6 +1506,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.4.1" + synchronized: + dependency: transitive + description: + name: synchronized + sha256: c254ade258ec8282947a0acbbc90b9575b4f19673533ee46f2f6e9b3aeefd7c0 + url: "https://pub.dev" + source: hosted + version: "3.4.0" term_glyph: dependency: transitive description: @@ -1298,22 +1554,6 @@ packages: url: "https://pub.dev" source: hosted version: "0.10.1" - timing: - dependency: transitive - description: - name: timing - sha256: "62ee18aca144e4a9f29d212f5a4c6a053be252b895ab14b5821996cff4ed90fe" - url: "https://pub.dev" - source: hosted - version: "1.0.2" - tuple: - dependency: transitive - description: - name: tuple - sha256: a97ce2013f240b2f3807bcbaf218765b6f301c3eff91092bcfa23a039e7dd151 - url: "https://pub.dev" - source: hosted - version: "2.0.2" typed_data: dependency: transitive description: @@ -1342,10 +1582,10 @@ packages: dependency: transitive description: name: url_launcher_ios - sha256: cfde38aa257dae62ffe79c87fab20165dfdf6988c1d31b58ebf59b9106062aad + sha256: "580fe5dfb51671ae38191d316e027f6b76272b026370708c2d898799750a02b0" url: "https://pub.dev" source: hosted - version: "6.3.6" + version: "6.4.1" url_launcher_linux: dependency: transitive description: @@ -1374,10 +1614,10 @@ packages: dependency: transitive description: name: url_launcher_web - sha256: "4bd2b7b4dc4d4d0b94e5babfffbca8eac1a126c7f3d6ecbc1a11013faa3abba2" + sha256: d0412fcf4c6b31ecfdb7762359b7206ffba3bbffd396c6d9f9c4616ece476c1f url: "https://pub.dev" source: hosted - version: "2.4.1" + version: "2.4.2" url_launcher_windows: dependency: transitive description: @@ -1390,10 +1630,10 @@ packages: dependency: transitive description: name: uuid - sha256: a11b666489b1954e01d992f3d601b1804a33937b5a8fe677bd26b8a9f96f96e8 + sha256: "1fef9e8e11e2991bb773070d4656b7bd5d850967a2456cfc83cf47925ba79489" url: "https://pub.dev" source: hosted - version: "4.5.2" + version: "4.5.3" vector_graphics: dependency: transitive description: @@ -1414,10 +1654,10 @@ packages: dependency: transitive description: name: vector_graphics_compiler - sha256: "201e876b5d52753626af64b6359cd13ac6011b80728731428fd34bc840f71c9b" + sha256: "5a88dd14c0954a5398af544651c7fb51b457a2a556949bfb25369b210ef73a74" url: "https://pub.dev" source: hosted - version: "1.1.20" + version: "1.2.0" vector_math: dependency: transitive description: @@ -1438,10 +1678,10 @@ packages: dependency: transitive description: name: watcher - sha256: "1398c9f081a753f9226febe8900fce8f7d0a67163334e1c94a2438339d79d635" + sha256: "592ab6e2892f67760543fb712ff0177f4ec76c031f02f5b4ff8d3fc5eb9fb61a" url: "https://pub.dev" source: hosted - version: "1.2.1" + version: "1.1.4" web: dependency: transitive description: @@ -1474,6 +1714,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.2.1" + win32: + dependency: transitive + description: + name: win32 + sha256: d7cb55e04cd34096cd3a79b3330245f54cb96a370a1c27adb3c84b917de8b08e + url: "https://pub.dev" + source: hosted + version: "5.15.0" xdg_directories: dependency: transitive description: @@ -1490,6 +1738,14 @@ packages: url: "https://pub.dev" source: hosted version: "6.6.1" + xxh3: + dependency: transitive + description: + name: xxh3 + sha256: "399a0438f5d426785723c99da6b16e136f4953fb1e9db0bf270bd41dd4619916" + url: "https://pub.dev" + source: hosted + version: "1.2.0" yaml: dependency: transitive description: @@ -1499,5 +1755,5 @@ packages: source: hosted version: "3.1.3" sdks: - dart: ">=3.9.0 <4.0.0" - flutter: ">=3.35.0" + dart: ">=3.10.3 <4.0.0" + flutter: ">=3.38.4" diff --git a/pubspec.yaml b/pubspec.yaml index bb7cff1..84a9ac5 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -18,25 +18,28 @@ dependencies: flutter_otp_text_field: ^1.5.1+1 flutter_svg: ^2.2.3 get_it: ^9.2.0 - go_router: ^13.2.0 - injectable: 2.7.0 + go_router: ^17.1.0 + injectable: ^2.7.1+4 intl: ^0.20.2 json_annotation: ^4.9.0 pretty_dio_logger: ^1.4.0 provider: ^6.1.5+1 - retrofit: ^4.4.1 + retrofit: ^4.9.1 shared_preferences: ^2.2.2 shimmer: ^3.0.0 skeletonizer: ^2.1.2 image_picker: ^1.2.1 google_maps_flutter: ^2.14.0 - geolocator: ^10.1.0 + geolocator: ^14.0.2 firebase_core: ^4.4.0 lottie: ^3.3.2 url_launcher: ^6.1.10 firebase_messaging: ^16.1.1 flutter_local_notifications: ^20.0.0 firebase_crashlytics: ^5.0.7 + cloud_firestore: ^6.1.2 + firebase_auth: ^6.1.4 + cached_network_image: ^3.4.1 dev_dependencies: bloc_test: ^10.0.0 @@ -45,7 +48,7 @@ dev_dependencies: injectable_generator: ^2.4.1 json_serializable: ^6.8.0 mockito: ^5.4.4 - retrofit_generator: 7.0.8 + retrofit_generator: ^10.2.3 network_image_mock: ^2.1.1 mocktail: ^1.0.3 @@ -72,4 +75,4 @@ flutter: # fonts: # - asset: fonts/TrajanPro.ttf # - asset: fonts/TrajanPro_Bold.ttf - # weight: 700 + # weight: 700 \ No newline at end of file diff --git a/test/features/Onboarding/presentation/pages/onboardingScreen_test.dart b/test/features/Onboarding/presentation/pages/onboardingScreen_test.dart index 335f250..6d2f111 100644 --- a/test/features/Onboarding/presentation/pages/onboardingScreen_test.dart +++ b/test/features/Onboarding/presentation/pages/onboardingScreen_test.dart @@ -2,7 +2,6 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:mockito/annotations.dart'; -import 'package:mockito/mockito.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:tracking_app/features/Onboarding/presentation/pages/onboardingScreen.dart'; import 'package:tracking_app/features/auth/domain/repos/auth_repo.dart'; diff --git a/test/features/app_sections/presentation/widgets/app_section_view_test.dart b/test/features/app_sections/presentation/widgets/app_section_view_test.dart index 2203264..2ed1c48 100644 --- a/test/features/app_sections/presentation/widgets/app_section_view_test.dart +++ b/test/features/app_sections/presentation/widgets/app_section_view_test.dart @@ -5,19 +5,22 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:mockito/annotations.dart'; import 'package:mockito/mockito.dart'; import 'package:shared_preferences/shared_preferences.dart'; +import 'package:tracking_app/app/config/base_state/base_state.dart'; +import 'package:tracking_app/app/config/di/di.dart'; import 'package:tracking_app/features/app_sections/presentation/manager/app_section_cubit.dart'; import 'package:tracking_app/features/app_sections/presentation/manager/app_section_states.dart'; -import 'package:tracking_app/features/app_sections/presentation/pages/home_page_test.dart'; -import 'package:tracking_app/features/app_sections/presentation/pages/orders_page_test.dart'; -import 'package:tracking_app/features/app_sections/presentation/pages/profile_page_test.dart'; import 'package:tracking_app/features/app_sections/presentation/widgets/app_section_view.dart'; +import 'package:tracking_app/features/home/presentation/manger/driverorderCubit.dart'; +import 'package:tracking_app/features/home/presentation/manger/driverorderStates.dart'; +import 'package:tracking_app/features/home/presentation/pages/driverOrderScreen.dart'; import 'app_section_view_test.mocks.dart'; -@GenerateMocks([AppSectionCubit]) +@GenerateNiceMocks([MockSpec(), MockSpec()]) void main() { TestWidgetsFlutterBinding.ensureInitialized(); - late MockAppSectionCubit mockCubit; + late MockAppSectionCubit mockAppSectionCubit; + late MockDriverOrderCubit mockDriverOrderCubit; setUpAll(() async { SharedPreferences.setMockInitialValues({}); @@ -25,7 +28,18 @@ void main() { }); setUp(() { - mockCubit = MockAppSectionCubit(); + mockAppSectionCubit = MockAppSectionCubit(); + mockDriverOrderCubit = MockDriverOrderCubit(); + if (getIt.isRegistered()) { + getIt.unregister(); + } + getIt.registerFactory(() => mockDriverOrderCubit); + }); + + tearDown(() { + if (getIt.isRegistered()) { + getIt.unregister(); + } }); Widget buildTestableWidget() { @@ -34,8 +48,11 @@ void main() { path: 'assets/translations', fallbackLocale: const Locale('en'), child: MaterialApp( - home: BlocProvider( - create: (_) => mockCubit, + home: MultiBlocProvider( + providers: [ + BlocProvider(create: (_) => mockAppSectionCubit), + BlocProvider(create: (_) => mockDriverOrderCubit), + ], child: AppSectionsView(), ), ), @@ -43,54 +60,68 @@ void main() { } group('AppSectionsView Widget Test', () { - testWidgets('should show Home page by default', ( + testWidgets('should show DriverOrderScreen by default (index 0)', ( WidgetTester tester, ) async { - when(mockCubit.state).thenReturn(AppSectionStates(selectedIndex: 0)); - when(mockCubit.stream).thenAnswer( + when( + mockAppSectionCubit.state, + ).thenReturn(AppSectionStates(selectedIndex: 0)); + when(mockAppSectionCubit.stream).thenAnswer( (_) => Stream.value(AppSectionStates(selectedIndex: 0)), ); + // Stub DriverOrderCubit + when( + mockDriverOrderCubit.state, + ).thenReturn(DriverOrderState(orderResource: Resource.loading())); + when( + mockDriverOrderCubit.stream, + ).thenAnswer((_) => Stream.empty()); + await tester.pumpWidget(buildTestableWidget()); - await tester.tap(find.byIcon(Icons.home)); - await tester.pump(); + // No tap needed for default - expect(find.byType(HomePageTest), findsOneWidget); - expect(find.byType(OrdersPageTest), findsNothing); - expect(find.byType(ProfilePageTest), findsNothing); + expect(find.byType(DriverOrderScreen), findsOneWidget); }); - testWidgets('should navigate to Orders page when tapping Orders', ( - WidgetTester tester, - ) async { - when(mockCubit.state).thenReturn(AppSectionStates(selectedIndex: 1)); - when(mockCubit.stream).thenAnswer( - (_) => - Stream.value(AppSectionStates(selectedIndex: 1)), - ); - - await tester.pumpWidget(buildTestableWidget()); - await tester.tap(find.byIcon(Icons.fact_check_outlined)); - await tester.pump(); + // testWidgets('should navigate to Orders page when tapping Orders', ( + // WidgetTester tester, + // ) async { + // when( + // mockAppSectionCubit.state, + // ).thenReturn(AppSectionStates(selectedIndex: 1)); + // when(mockAppSectionCubit.stream).thenAnswer( + // (_) => + // Stream.value(AppSectionStates(selectedIndex: 1)), + // ); - expect(find.byType(OrdersPageTest), findsOneWidget); - }); + // // Stub DriverOrderCubit just in case (though not used in index 1 view) + // when( + // mockDriverOrderCubit.state, + // ).thenReturn(DriverOrderState(orderResource: Resource.loading())); + // when( + // mockDriverOrderCubit.stream, + // ).thenAnswer((_) => Stream.empty()); - testWidgets('should navigate to Profile page when tapping Profile', ( - WidgetTester tester, - ) async { - when(mockCubit.state).thenReturn(AppSectionStates(selectedIndex: 2)); - when(mockCubit.stream).thenAnswer( - (_) => - Stream.value(AppSectionStates(selectedIndex: 2)), - ); + // await tester.pumpWidget(buildTestableWidget()); + // await tester.tap(find.byIcon(Icons.fact_check_outlined)); + // await tester.pump(); - await tester.pumpWidget(buildTestableWidget()); - await tester.tap(find.byIcon(Icons.person_outlined)); - await tester.pump(); + // expect(find.byType(OrdersPageTest), findsOneWidget); + // }); - expect(find.byType(ProfilePageTest), findsOneWidget); - }); + // testWidgets('should navigate to Profile page when tapping Profile', ( + // WidgetTester tester, + // ) async { + // when(mockAppSectionCubit.state).thenReturn(AppSectionStates(selectedIndex: 2)); + // when(mockAppSectionCubit.stream).thenAnswer( + // (_) => Stream.value(AppSectionStates(selectedIndex: 2)), + // ); + // await tester.pumpWidget(buildTestableWidget()); + // await tester.tap(find.byIcon(Icons.person_outlined)); + // await tester.pump(); + // expect(find.byType(ProfilePage), findsOneWidget); + // }); }); } diff --git a/test/features/auth/api/datasource/auth_remote_datasource_impl_test.dart b/test/features/auth/api/datasource/auth_remote_datasource_impl_test.dart index 22b9953..3b78f8f 100644 --- a/test/features/auth/api/datasource/auth_remote_datasource_impl_test.dart +++ b/test/features/auth/api/datasource/auth_remote_datasource_impl_test.dart @@ -15,47 +15,72 @@ import 'package:tracking_app/features/auth/data/models/request/verifyreset_reque import 'package:tracking_app/features/auth/data/models/response/forgetpassword_response.dart'; import 'package:tracking_app/features/auth/data/models/response/resetpassword_response.dart'; import 'package:tracking_app/features/auth/data/models/response/verifyreset_response.dart'; - import 'auth_remote_datasource_impl_test.mocks.dart'; @GenerateMocks([ApiClient]) void main() { late MockApiClient mockApiClient; late AuthRemoteDataSourceImpl authRemoteDataSourceImpl; - late AuthRemoteDataSourceImpl dataSource; // initialize for login/change password tests + late AuthRemoteDataSourceImpl + dataSource; // initialize for login/change password tests setUpAll(() { mockApiClient = MockApiClient(); authRemoteDataSourceImpl = AuthRemoteDataSourceImpl(mockApiClient); dataSource = AuthRemoteDataSourceImpl(mockApiClient); + provideDummy>( + SuccessApiResult(data: ChangePasswordDto()), + ); + provideDummy>( + ErrorApiResult(error: ''), + ); }); - final forgetPasswordRequest = ForgetPasswordRequest(email: "test@example.com"); + final forgetPasswordRequest = ForgetPasswordRequest( + email: "test@example.com", + ); group("AuthRemoteDatasourceImpl.forgetPassword()", () { - test("returns SuccessApiResult when apiClient returns valid response", () async { - final expectedResponse = ForgetpasswordResponse(message: "Password reset code sent to email"); - final dioResponse = Response( - requestOptions: RequestOptions(path: '/forget-password'), - data: expectedResponse, - statusCode: 200, - ); - final fakeHttpResponse = HttpResponse(dioResponse.data!, dioResponse); - - when(mockApiClient.forgetPassword(any)).thenAnswer((_) async => fakeHttpResponse); - - final result = await authRemoteDataSourceImpl.forgetPassword(forgetPasswordRequest); - - expect(result, isA>()); - final successResult = result as SuccessApiResult; - expect(successResult.data.message, "Password reset code sent to email"); - verify(mockApiClient.forgetPassword(any)).called(1); - }); + test( + "returns SuccessApiResult when apiClient returns valid response", + () async { + final expectedResponse = ForgetpasswordResponse( + message: "Password reset code sent to email", + ); + final dioResponse = Response( + requestOptions: RequestOptions(path: '/forget-password'), + data: expectedResponse, + statusCode: 200, + ); + final fakeHttpResponse = HttpResponse( + dioResponse.data!, + dioResponse, + ); + + when( + mockApiClient.forgetPassword(any), + ).thenAnswer((_) async => fakeHttpResponse); + + final result = await authRemoteDataSourceImpl.forgetPassword( + forgetPasswordRequest, + ); + + expect(result, isA>()); + final successResult = + result as SuccessApiResult; + expect(successResult.data.message, "Password reset code sent to email"); + verify(mockApiClient.forgetPassword(any)).called(1); + }, + ); test("returns ErrorApiResult when apiClient throws Exception", () async { - when(mockApiClient.forgetPassword(any)).thenThrow(Exception("Network Error")); + when( + mockApiClient.forgetPassword(any), + ).thenThrow(Exception("Network Error")); - final result = await authRemoteDataSourceImpl.forgetPassword(forgetPasswordRequest); + final result = await authRemoteDataSourceImpl.forgetPassword( + forgetPasswordRequest, + ); expect(result, isA()); final errorResult = result as ErrorApiResult; @@ -65,31 +90,50 @@ void main() { }); group("AuthRemoteDatasourceImpl.resetPassword()", () { - final resetPasswordRequest = ResetPasswordRequest(email: "test@example.com", newPassword: "12345678"); - - test("returns SuccessApiResult when apiClient returns valid response", () async { - final expectedResponse = ResetpasswordResponse(message: "Password reset successfully"); - final dioResponse = Response( - requestOptions: RequestOptions(path: '/reset-password'), - data: expectedResponse, - statusCode: 200, - ); - final fakeHttpResponse = HttpResponse(dioResponse.data!, dioResponse); - - when(mockApiClient.resetPassword(any)).thenAnswer((_) async => fakeHttpResponse); - - final result = await authRemoteDataSourceImpl.resetPassword(resetPasswordRequest); - - expect(result, isA>()); - final successResult = result as SuccessApiResult; - expect(successResult.data.message, "Password reset successfully"); - verify(mockApiClient.resetPassword(any)).called(1); - }); + final resetPasswordRequest = ResetPasswordRequest( + email: "test@example.com", + newPassword: "12345678", + ); + + test( + "returns SuccessApiResult when apiClient returns valid response", + () async { + final expectedResponse = ResetpasswordResponse( + message: "Password reset successfully", + ); + final dioResponse = Response( + requestOptions: RequestOptions(path: '/reset-password'), + data: expectedResponse, + statusCode: 200, + ); + final fakeHttpResponse = HttpResponse( + dioResponse.data!, + dioResponse, + ); + + when( + mockApiClient.resetPassword(any), + ).thenAnswer((_) async => fakeHttpResponse); + + final result = await authRemoteDataSourceImpl.resetPassword( + resetPasswordRequest, + ); + + expect(result, isA>()); + final successResult = result as SuccessApiResult; + expect(successResult.data.message, "Password reset successfully"); + verify(mockApiClient.resetPassword(any)).called(1); + }, + ); test("returns ErrorApiResult when apiClient throws Exception", () async { - when(mockApiClient.resetPassword(any)).thenThrow(Exception("Reset failed")); + when( + mockApiClient.resetPassword(any), + ).thenThrow(Exception("Reset failed")); - final result = await authRemoteDataSourceImpl.resetPassword(resetPasswordRequest); + final result = await authRemoteDataSourceImpl.resetPassword( + resetPasswordRequest, + ); expect(result, isA()); final errorResult = result as ErrorApiResult; @@ -101,29 +145,45 @@ void main() { group("AuthRemoteDatasourceImpl.verifyResetCode()", () { final verifyResetCodeRequest = VerifyResetRequest(resetCode: "1234"); - test("returns SuccessApiResult when apiClient returns valid response", () async { - final expectedResponse = VerifyresetResponse(status: "Code verified successfully"); - final dioResponse = Response( - requestOptions: RequestOptions(path: '/verify-reset-code'), - data: expectedResponse, - statusCode: 200, - ); - final fakeHttpResponse = HttpResponse(dioResponse.data!, dioResponse); - - when(mockApiClient.verifyResetCode(any)).thenAnswer((_) async => fakeHttpResponse); - - final result = await authRemoteDataSourceImpl.verifyResetCode(verifyResetCodeRequest); - - expect(result, isA>()); - final successResult = result as SuccessApiResult; - expect(successResult.data.status, "Code verified successfully"); - verify(mockApiClient.verifyResetCode(any)).called(1); - }); + test( + "returns SuccessApiResult when apiClient returns valid response", + () async { + final expectedResponse = VerifyresetResponse( + status: "Code verified successfully", + ); + final dioResponse = Response( + requestOptions: RequestOptions(path: '/verify-reset-code'), + data: expectedResponse, + statusCode: 200, + ); + final fakeHttpResponse = HttpResponse( + dioResponse.data!, + dioResponse, + ); + + when( + mockApiClient.verifyResetCode(any), + ).thenAnswer((_) async => fakeHttpResponse); + + final result = await authRemoteDataSourceImpl.verifyResetCode( + verifyResetCodeRequest, + ); + + expect(result, isA>()); + final successResult = result as SuccessApiResult; + expect(successResult.data.status, "Code verified successfully"); + verify(mockApiClient.verifyResetCode(any)).called(1); + }, + ); test("returns ErrorApiResult when apiClient throws Exception", () async { - when(mockApiClient.verifyResetCode(any)).thenThrow(Exception("Invalid code")); + when( + mockApiClient.verifyResetCode(any), + ).thenThrow(Exception("Invalid code")); - final result = await authRemoteDataSourceImpl.verifyResetCode(verifyResetCodeRequest); + final result = await authRemoteDataSourceImpl.verifyResetCode( + verifyResetCodeRequest, + ); expect(result, isA()); final errorResult = result as ErrorApiResult; @@ -133,7 +193,10 @@ void main() { }); // ---------- login ---------- - final tLoginRequest = LoginRequest(email: 'test@example.com', password: 'password123'); + final tLoginRequest = LoginRequest( + email: 'test@example.com', + password: 'password123', + ); final tLoginResponse = LoginResponse(token: 'token123', message: 'Success'); group('AuthRemoteDataSourceImpl.login', () { @@ -145,61 +208,129 @@ void main() { verify(mockApiClient.login(tLoginRequest)).called(1); }); - test('should return ErrorApiResult with "wrongEmailOrPassword" on 401 error', () async { - when(mockApiClient.login(any)).thenThrow( - DioException( - requestOptions: RequestOptions(path: ''), - response: Response(requestOptions: RequestOptions(path: ''), statusCode: 401), - ), - ); - final result = await dataSource.login(tLoginRequest); - expect(result, isA>()); - expect((result as ErrorApiResult).error, 'wrongEmailOrPassword'); - }); - - test('should return ErrorApiResult with message from response on other DioErrors', () async { - const tErrorMessage = 'Some other error'; - when(mockApiClient.login(any)).thenThrow( - DioException( - requestOptions: RequestOptions(path: ''), - response: Response(requestOptions: RequestOptions(path: ''), statusCode: 400, data: {'message': tErrorMessage}), - ), - ); - final result = await dataSource.login(tLoginRequest); - expect(result, isA>()); - expect((result as ErrorApiResult).error, tErrorMessage); - }); - - test('should return ErrorApiResult with exception message on unknown error', () async { - const tExceptionMessage = 'Exception: Unknown error'; - when(mockApiClient.login(any)).thenThrow(Exception('Unknown error')); - final result = await dataSource.login(tLoginRequest); - expect(result, isA>()); - expect((result as ErrorApiResult).error, tExceptionMessage); - }); + test( + 'should return ErrorApiResult with "wrongEmailOrPassword" on 401 error', + () async { + when(mockApiClient.login(any)).thenThrow( + DioException( + requestOptions: RequestOptions(path: ''), + response: Response( + requestOptions: RequestOptions(path: ''), + statusCode: 401, + ), + ), + ); + final result = await dataSource.login(tLoginRequest); + expect(result, isA>()); + expect( + (result as ErrorApiResult).error, + 'wrongEmailOrPassword', + ); + }, + ); + + test( + 'should return ErrorApiResult with message from response on other DioErrors', + () async { + const tErrorMessage = 'Some other error'; + when(mockApiClient.login(any)).thenThrow( + DioException( + requestOptions: RequestOptions(path: ''), + response: Response( + requestOptions: RequestOptions(path: ''), + statusCode: 400, + data: {'message': tErrorMessage}, + ), + ), + ); + final result = await dataSource.login(tLoginRequest); + expect(result, isA>()); + expect((result as ErrorApiResult).error, tErrorMessage); + }, + ); + + test( + 'should return ErrorApiResult with exception message on unknown error', + () async { + const tExceptionMessage = 'Exception: Unknown error'; + when(mockApiClient.login(any)).thenThrow(Exception('Unknown error')); + final result = await dataSource.login(tLoginRequest); + expect(result, isA>()); + expect( + (result as ErrorApiResult).error, + tExceptionMessage, + ); + }, + ); }); group("AuthRemoteDatasourceImpl.changePassword()", () { test('should return ApiSuccess when change password succeeds', () async { - final fakeDto = ChangePasswordDto(message: 'Success', token: 'fake_token', error: 'error'); - final fakeResponse = HttpResponse(fakeDto, Response(requestOptions: RequestOptions(path: '/drivers/change-password'), statusCode: 200)); - when(mockApiClient.changePassword(any)).thenAnswer((_) async => fakeResponse); + final fakeDto = ChangePasswordDto( + message: 'Success', + token: 'fake_token', + error: null, + ); + final fakeResponse = HttpResponse( + fakeDto, + Response( + requestOptions: RequestOptions(path: '/change-password'), + statusCode: 200, + ), + ); + when( + mockApiClient.changePassword( + token: 'Bearer fake_token', + body: {'password': 'Mm@123456', 'newPassword': "Mmmmmm@1"}, + ), + ).thenAnswer((_) async => fakeResponse); - final result = await dataSource.changePassword(password: 'Mm@123456', newPassword: "Mmmmmm@1") as SuccessApiResult; + final result = + await dataSource.changePassword( + token: 'fake_token', + password: 'Mm@123456', + newPassword: "Mmmmmm@1", + ) + as SuccessApiResult; expect(result, isA>()); expect(result.data.token, fakeDto.token); expect(result.data.message, fakeDto.message); - verify(mockApiClient.changePassword(any)).called(1); + verify( + mockApiClient.changePassword( + token: 'Bearer fake_token', + body: {'password': 'Mm@123456', 'newPassword': "Mmmmmm@1"}, + ), + ).called(1); }); - test('should return ApiFailure when change password throws exception', () async { - when(mockApiClient.changePassword(any)).thenThrow(Exception('Network error')); - final result = await dataSource.changePassword(password: 'Mm@123456', newPassword: "Mmmmmm@1") as ErrorApiResult; - - expect(result, isA>()); - expect(result.error.toString(), contains("Network error")); - verify(mockApiClient.changePassword(any)).called(1); - }); + test( + 'should return ApiFailure when change password throws exception', + () async { + when( + mockApiClient.changePassword( + token: anyNamed('token'), + body: anyNamed('body'), + ), + ).thenThrow(Exception('Network error')); + + final result = + await dataSource.changePassword( + token: 'fake_token', + password: 'Mariam@123', + newPassword: "Mariam@1234", + ) + as ErrorApiResult; + + expect(result, isA>()); + expect(result.error.toString(), contains("Network error")); + verify( + mockApiClient.changePassword( + token: 'Bearer fake_token', + body: {'password': 'Mariam@123', 'newPassword': "Mariam@1234"}, + ), + ).called(1); + }, + ); }); } diff --git a/test/features/auth/data/models/response/forgetpassword_response_test.dart b/test/features/auth/data/models/response/forgetpassword_response_test.dart index 10005e0..206333e 100644 --- a/test/features/auth/data/models/response/forgetpassword_response_test.dart +++ b/test/features/auth/data/models/response/forgetpassword_response_test.dart @@ -3,13 +3,9 @@ import 'package:tracking_app/features/auth/data/models/response/forgetpassword_r void main() { group("ForgetpasswordResponse", () { - test("fromJson should parse correctly", () { // Arrange - final json = { - "message": "Reset email sent", - "info": "Check your inbox", - }; + final json = {"message": "Reset email sent", "info": "Check your inbox"}; // Act final model = ForgetpasswordResponse.fromJson(json); @@ -42,9 +38,7 @@ void main() { ); // Act - final updatedModel = model.copyWith( - message: "New message", - ); + final updatedModel = model.copyWith(message: "New message"); // Assert expect(updatedModel.message, "New message"); @@ -63,6 +57,5 @@ void main() { expect(json.containsKey("message"), true); expect(json.containsKey("info"), true); }); - }); } diff --git a/test/features/auth/data/models/response/resetpassword_response_test.dart b/test/features/auth/data/models/response/resetpassword_response_test.dart index febd035..3c37ce3 100644 --- a/test/features/auth/data/models/response/resetpassword_response_test.dart +++ b/test/features/auth/data/models/response/resetpassword_response_test.dart @@ -3,7 +3,6 @@ import 'package:tracking_app/features/auth/data/models/response/resetpassword_re void main() { group("ResetpasswordResponse", () { - test("fromJson should parse correctly", () { // Arrange final json = { @@ -42,9 +41,7 @@ void main() { ); // Act - final updated = model.copyWith( - message: "New message", - ); + final updated = model.copyWith(message: "New message"); // Assert expect(updated.message, "New message"); @@ -61,6 +58,5 @@ void main() { expect(json.containsKey("message"), true); expect(json.containsKey("token"), true); }); - }); } diff --git a/test/features/auth/data/models/response/verifyreset_response_test.dart b/test/features/auth/data/models/response/verifyreset_response_test.dart index 5c1d76f..4eb8623 100644 --- a/test/features/auth/data/models/response/verifyreset_response_test.dart +++ b/test/features/auth/data/models/response/verifyreset_response_test.dart @@ -3,12 +3,9 @@ import 'package:tracking_app/features/auth/data/models/response/verifyreset_resp void main() { group("VerifyresetResponse", () { - test("fromJson should parse correctly", () { // Arrange - final json = { - "status": "verified", - }; + final json = {"status": "verified"}; // Act final model = VerifyresetResponse.fromJson(json); @@ -19,9 +16,7 @@ void main() { test("toJson should return correct map", () { // Arrange - final model = VerifyresetResponse( - status: "verified", - ); + final model = VerifyresetResponse(status: "verified"); // Act final json = model.toJson(); @@ -32,14 +27,10 @@ void main() { test("copyWith should override provided field", () { // Arrange - final model = VerifyresetResponse( - status: "pending", - ); + final model = VerifyresetResponse(status: "pending"); // Act - final updated = model.copyWith( - status: "verified", - ); + final updated = model.copyWith(status: "verified"); // Assert expect(updated.status, "verified"); @@ -53,6 +44,5 @@ void main() { final json = model.toJson(); expect(json.containsKey("status"), true); }); - }); } diff --git a/test/features/auth/data/repos/auth_repo_impl_test.dart b/test/features/auth/data/repos/auth_repo_impl_test.dart index 3cc1078..98ecf25 100644 --- a/test/features/auth/data/repos/auth_repo_impl_test.dart +++ b/test/features/auth/data/repos/auth_repo_impl_test.dart @@ -23,7 +23,8 @@ void main() { late MockAuthRemoteDataSource datasource; late AuthRepoImpl repo; - late MockAuthRemoteDataSource mockDataSource; // for login/changePassword tests + late MockAuthRemoteDataSource + mockDataSource; // for login/changePassword tests late AuthRepoImpl repoImp; setUpAll(() { @@ -68,7 +69,10 @@ void main() { const email = "test@mail.com"; test("should return SuccessApiResult when datasource succeeds", () async { - final fakeDto = ForgetpasswordResponse(message: "Email sent", info: "Check inbox"); + final fakeDto = ForgetpasswordResponse( + message: "Email sent", + info: "Check inbox", + ); when(datasource.forgetPassword(any)).thenAnswer( (_) async => SuccessApiResult(data: fakeDto), @@ -86,7 +90,8 @@ void main() { test("should return ErrorApiResult when datasource fails", () async { when(datasource.forgetPassword(any)).thenAnswer( - (_) async => ErrorApiResult(error: "Network error"), + (_) async => + ErrorApiResult(error: "Network error"), ); final result = await repo.forgetPassword(email); @@ -138,10 +143,16 @@ void main() { // resetPassword // ============================================================ group("resetPassword", () { - final request = ResetPasswordRequest(email: "test@mail.com", newPassword: "12345678"); + final request = ResetPasswordRequest( + email: "test@mail.com", + newPassword: "12345678", + ); test("should return SuccessApiResult when datasource succeeds", () async { - final fakeDto = ResetpasswordResponse(message: "Password reset", token: "abc123"); + final fakeDto = ResetpasswordResponse( + message: "Password reset", + token: "abc123", + ); when(datasource.resetPassword(request)).thenAnswer( (_) async => SuccessApiResult(data: fakeDto), @@ -159,7 +170,8 @@ void main() { test("should return ErrorApiResult when datasource fails", () async { when(datasource.resetPassword(request)).thenAnswer( - (_) async => ErrorApiResult(error: "Server error"), + (_) async => + ErrorApiResult(error: "Server error"), ); final result = await repo.resetPassword(request); @@ -179,63 +191,121 @@ void main() { final tLoginResponse = LoginResponse(token: 'token123', message: 'Success'); group('AuthRepoImpl.login', () { - test('should return SuccessApiResult when remote data source call is successful', () async { - when(mockDataSource.login(any)).thenAnswer((_) async => SuccessApiResult(data: tLoginResponse)); - - final result = await repoImp.login(tEmail, tPassword); - - expect(result, isA>()); - expect((result as SuccessApiResult).data, tLoginResponse); - - verify(mockDataSource.login(any)).called(1); - verifyNoMoreInteractions(mockDataSource); - }); + test( + 'should return SuccessApiResult when remote data source call is successful', + () async { + when( + mockDataSource.login(any), + ).thenAnswer((_) async => SuccessApiResult(data: tLoginResponse)); + + final result = await repoImp.login(tEmail, tPassword); + + expect(result, isA>()); + expect( + (result as SuccessApiResult).data, + tLoginResponse, + ); + + verify(mockDataSource.login(any)).called(1); + verifyNoMoreInteractions(mockDataSource); + }, + ); - test('should return ErrorApiResult when remote data source call fails', () async { - const tErrorMessage = 'An error occurred'; - when(mockDataSource.login(any)).thenAnswer((_) async => ErrorApiResult(error: tErrorMessage)); + test( + 'should return ErrorApiResult when remote data source call fails', + () async { + const tErrorMessage = 'An error occurred'; + when( + mockDataSource.login(any), + ).thenAnswer((_) async => ErrorApiResult(error: tErrorMessage)); - final result = await repoImp.login(tEmail, tPassword); + final result = await repoImp.login(tEmail, tPassword); - expect(result, isA>()); - expect((result as ErrorApiResult).error, tErrorMessage); + expect(result, isA>()); + expect((result as ErrorApiResult).error, tErrorMessage); - verify(mockDataSource.login(any)).called(1); - verifyNoMoreInteractions(mockDataSource); - }); + verify(mockDataSource.login(any)).called(1); + verifyNoMoreInteractions(mockDataSource); + }, + ); }); // ============================================================ // changePassword // ============================================================ group("AuthRepoImpl.changePassword()", () { - test('should return ApiSuccess when changePassword datasource succeeds', () async { - final fakeDto = ChangePasswordDto(message: 'Success', token: 'fake_token', error: null); - - when(mockDataSource.changePassword(password: anyNamed('password'), newPassword: anyNamed('newPassword'))) - .thenAnswer((_) async => SuccessApiResult(data: fakeDto)); - - final result = await repoImp.changePassword(password: 'Mm@123456', newPassword: 'Mmmm@123') - as SuccessApiResult; - - expect(result, isA>()); - expect(result.data.token, fakeDto.token); - expect(result.data.message, fakeDto.message); - - verify(mockDataSource.changePassword(password: anyNamed('password'), newPassword: anyNamed('newPassword'))).called(1); - }); - - test('should return ApiFailure when changePassword datasource throws exception', () async { - when(mockDataSource.changePassword(password: anyNamed('password'), newPassword: anyNamed('newPassword'))) - .thenAnswer((_) async => ErrorApiResult(error: 'Network error')); - - final result = await repoImp.changePassword(password: 'Mm@123456', newPassword: 'Mmmm@123') - as ErrorApiResult; - - expect(result, isA>()); - expect(result.error.toString(), contains("Network error")); + test( + 'should return ApiSuccess when changePassword datasource succeeds', + () async { + final fakeDto = ChangePasswordDto( + message: 'Success', + token: 'fake_token', + error: null, + ); + + when( + mockDataSource.changePassword( + token: ('fake_token'), + password: anyNamed('password'), + newPassword: anyNamed('newPassword'), + ), + ).thenAnswer( + (_) async => SuccessApiResult(data: fakeDto), + ); + + final result = + await repoImp.changePassword( + token: 'fake_token', + password: 'Mm@123456', + newPassword: 'Mmmm@123', + ) + as SuccessApiResult; + + expect(result, isA>()); + expect(result.data.token, fakeDto.token); + expect(result.data.message, fakeDto.message); + verify( + mockDataSource.changePassword( + token: ('fake_token'), + password: anyNamed('password'), + newPassword: anyNamed('newPassword'), + ), + ).called(1); + }, + ); - verify(mockDataSource.changePassword(password: anyNamed('password'), newPassword: anyNamed('newPassword'))).called(1); - }); + test( + 'should return ApiFailure when changePassword datasource throws exception', + () async { + when( + mockDataSource.changePassword( + token: ('fake_token'), + password: anyNamed('password'), + newPassword: anyNamed('newPassword'), + ), + ).thenAnswer( + (_) async => + ErrorApiResult(error: 'Network error'), + ); + + final result = + await repoImp.changePassword( + token: 'fake_token', + password: 'Mm@123456', + newPassword: 'Mmmm@123', + ) + as ErrorApiResult; + + expect(result, isA>()); + expect(result.error.toString(), contains("Network error")); + verify( + mockDataSource.changePassword( + token: ('fake_token'), + password: anyNamed('password'), + newPassword: anyNamed('newPassword'), + ), + ).called(1); + }, + ); }); } diff --git a/test/features/auth/domain/usecase/change_password_usecase_test.dart b/test/features/auth/domain/usecase/change_password_usecase_test.dart index d597912..095c00b 100644 --- a/test/features/auth/domain/usecase/change_password_usecase_test.dart +++ b/test/features/auth/domain/usecase/change_password_usecase_test.dart @@ -30,6 +30,7 @@ void main() { test("returns SuccessApiResult when repos returns success", () async { when( mockRepo.changePassword( + token: ('fake_token'), password: anyNamed('password'), newPassword: anyNamed('newPassword'), ), @@ -38,20 +39,29 @@ void main() { ); final result = - await useCase.call('Mm@123456', 'Mmmm@123') + await useCase.call( + token: 'fake_token', + password: 'Test@123', + newPassword: 'Test@1234', + ) as SuccessApiResult; expect(result, isA>()); expect(result.data.token, fakeData.token); expect(result.data.message, fakeData.message); verify( - mockRepo.changePassword(password: 'Mm@123456', newPassword: 'Mmmm@123'), + mockRepo.changePassword( + token: 'fake_token', + password: 'Test@123', + newPassword: 'Test@1234', + ), ).called(1); }); test("returns ErrorApiResult when repos returns error", () async { when( mockRepo.changePassword( + token: ('fake_token'), password: anyNamed('password'), newPassword: anyNamed('newPassword'), ), @@ -62,13 +72,21 @@ void main() { ); final result = - await useCase.call('Mm@123456', 'Mmmm@123') + await useCase.call( + token: 'fake_token', + password: 'Test@123', + newPassword: 'Test@1234', + ) as ErrorApiResult; expect(result, isA>()); expect(result.error, 'change password failed'); verify( - mockRepo.changePassword(password: 'Mm@123456', newPassword: 'Mmmm@123'), + mockRepo.changePassword( + token: 'fake_token', + password: 'Test@123', + newPassword: 'Test@1234', + ), ).called(1); }); }); diff --git a/test/features/auth/domain/usecase/forgetpassword_usecase_test.dart b/test/features/auth/domain/usecase/forgetpassword_usecase_test.dart index a3438d7..b221227 100644 --- a/test/features/auth/domain/usecase/forgetpassword_usecase_test.dart +++ b/test/features/auth/domain/usecase/forgetpassword_usecase_test.dart @@ -31,8 +31,10 @@ void main() { const email = "test@mail.com"; test("returns SuccessApiResult when repo succeeds", () async { - final entity = - ForgetPasswordEntitiy(message: "Email sent", info: "Check inbox"); + final entity = ForgetPasswordEntitiy( + message: "Email sent", + info: "Check inbox", + ); when(mockRepo.forgetPassword(email)).thenAnswer( (_) async => SuccessApiResult(data: entity), diff --git a/test/features/auth/domain/usecase/resertpassword_usecase_test.dart b/test/features/auth/domain/usecase/resertpassword_usecase_test.dart index c095518..0f1630f 100644 --- a/test/features/auth/domain/usecase/resertpassword_usecase_test.dart +++ b/test/features/auth/domain/usecase/resertpassword_usecase_test.dart @@ -10,7 +10,6 @@ import 'package:tracking_app/features/auth/domain/usecase/resertpassword_usecase import 'forgetpassword_usecase_test.mocks.dart'; - @GenerateMocks([AuthRepo]) void main() { late MockAuthRepo mockRepo; @@ -36,8 +35,10 @@ void main() { ); test("returns SuccessApiResult when repo succeeds", () async { - final entity = - ResetPasswordEntity(token: "abc123", message: "Password reset"); + final entity = ResetPasswordEntity( + token: "abc123", + message: "Password reset", + ); when(mockRepo.resetPassword(request)).thenAnswer( (_) async => SuccessApiResult(data: entity), @@ -53,8 +54,7 @@ void main() { test("returns ErrorApiResult when repo fails", () async { when(mockRepo.resetPassword(request)).thenAnswer( - (_) async => - ErrorApiResult(error: "Server error"), + (_) async => ErrorApiResult(error: "Server error"), ); final result = await usecase.call(request); diff --git a/test/features/auth/presentation/apply/view/apply_screen_test.dart b/test/features/auth/presentation/apply/view/apply_screen_test.dart index 4c7f9e9..9f95f7a 100644 --- a/test/features/auth/presentation/apply/view/apply_screen_test.dart +++ b/test/features/auth/presentation/apply/view/apply_screen_test.dart @@ -1,4 +1,3 @@ -import 'package:bloc_test/bloc_test.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; diff --git a/test/features/auth/presentation/login/pages/loginScreen_test.dart b/test/features/auth/presentation/login/pages/loginScreen_test.dart index 45a98cd..2550f9d 100644 --- a/test/features/auth/presentation/login/pages/loginScreen_test.dart +++ b/test/features/auth/presentation/login/pages/loginScreen_test.dart @@ -1,12 +1,15 @@ +import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:get_it/get_it.dart'; import 'package:mockito/annotations.dart'; +import 'package:shared_preferences/shared_preferences.dart'; import 'package:tracking_app/app/config/auth_storage/auth_storage.dart'; import 'package:tracking_app/features/auth/domain/repos/auth_repo.dart'; import 'package:tracking_app/features/auth/domain/usecase/login_usecase.dart'; import 'package:tracking_app/features/auth/presentation/login/manager/login_cubit.dart'; import 'package:tracking_app/features/auth/presentation/login/pages/loginScreen.dart'; +import 'package:tracking_app/generated/locale_keys.g.dart'; import 'loginScreen_test.mocks.dart'; @@ -18,6 +21,11 @@ void main() { late LoginCubit loginCubit; late GetIt getIt; + setUpAll(() async { + SharedPreferences.setMockInitialValues({}); + await EasyLocalization.ensureInitialized(); + }); + setUp(() { getIt = GetIt.instance; mockAuthRepo = MockAuthRepo(); @@ -38,17 +46,23 @@ void main() { }); Widget createWidgetUnderTest() { - return MaterialApp(home: const LoginScreen()); + return EasyLocalization( + supportedLocales: const [Locale('en'), Locale('ar')], + path: 'assets/translations', + fallbackLocale: const Locale('en'), + child: const MaterialApp(home: LoginScreen()), + ); } testWidgets('LoginScreen renders correctly', (WidgetTester tester) async { // Act await tester.pumpWidget(createWidgetUnderTest()); + await tester.pumpAndSettle(); // Assert - expect(find.text('email'), findsOneWidget); - expect(find.text('password'), findsOneWidget); - expect(find.text('continueTxt'), findsOneWidget); + expect(find.text(LocaleKeys.email), findsOneWidget); + expect(find.text(LocaleKeys.password), findsOneWidget); + expect(find.text(LocaleKeys.login), findsNWidgets(2)); }); testWidgets('Enters text into email and password fields', ( @@ -56,6 +70,8 @@ void main() { ) async { // Act await tester.pumpWidget(createWidgetUnderTest()); + await tester.pumpAndSettle(); + await tester.enterText(find.byType(TextFormField).first, 'test@test.com'); await tester.enterText(find.byType(TextFormField).last, 'password123'); await tester.pump(); diff --git a/test/features/auth/presentation/reset_password/manager/change_password_cubit_test.dart b/test/features/auth/presentation/reset_password/manager/change_password_cubit_test.dart index 8b8c5ef..27fd74e 100644 --- a/test/features/auth/presentation/reset_password/manager/change_password_cubit_test.dart +++ b/test/features/auth/presentation/reset_password/manager/change_password_cubit_test.dart @@ -2,6 +2,7 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:mockito/annotations.dart'; import 'package:bloc_test/bloc_test.dart'; import 'package:mockito/mockito.dart'; +import 'package:tracking_app/app/config/auth_storage/auth_storage.dart'; import 'package:tracking_app/app/config/base_state/base_state.dart'; import 'package:tracking_app/app/core/network/api_result.dart'; import 'package:tracking_app/features/auth/domain/models/change_password_model.dart'; @@ -9,23 +10,24 @@ import 'package:tracking_app/features/auth/domain/usecase/change_password_usecas import 'package:tracking_app/features/auth/presentation/reset_password/manager/change_password_cubit.dart'; import 'package:tracking_app/features/auth/presentation/reset_password/manager/change_password_intent.dart'; import 'package:tracking_app/features/auth/presentation/reset_password/manager/change_password_states.dart'; - import 'change_password_cubit_test.mocks.dart'; -@GenerateMocks([ChangePasswordUsecase]) +@GenerateMocks([ChangePasswordUsecase, AuthStorage]) void main() { TestWidgetsFlutterBinding.ensureInitialized(); late MockChangePasswordUsecase mockUseCase; + late MockAuthStorage mockAuthStorage; late ChangePasswordCubit cubit; setUpAll(() { mockUseCase = MockChangePasswordUsecase(); + mockAuthStorage = MockAuthStorage(); provideDummy>( SuccessApiResult(data: ChangePasswordModel()), ); }); setUp(() { - cubit = ChangePasswordCubit(mockUseCase); + cubit = ChangePasswordCubit(mockUseCase, mockAuthStorage); }); tearDown(() async { await cubit.close(); @@ -39,8 +41,15 @@ void main() { token: 'fake_token', error: null, ); - - when(mockUseCase.call('Test@123', 'Test@1234')).thenAnswer( + when(mockAuthStorage.getToken()).thenAnswer((_) async => 'fake_token'); + when(mockAuthStorage.clearToken()).thenAnswer((_) async => isTrue); + when( + mockUseCase.call( + token: 'Bearer fake_token', + password: 'Test@123', + newPassword: 'Test@1234', + ), + ).thenAnswer( (_) async => SuccessApiResult(data: fakeData), ); return cubit; @@ -80,14 +89,28 @@ void main() { .having((s) => s.data!.data!.message, "message", "Success"), ], verify: (_) { - verify(mockUseCase.call('Test@123', 'Test@1234')).called(1); + verify( + mockUseCase.call( + token: 'Bearer fake_token', + password: 'Test@123', + newPassword: 'Test@1234', + ), + ).called(1); + verify(mockAuthStorage.clearToken()).called(1); }, ); blocTest( 'emits loading then error when usecase returns ErrorApiResult', build: () { - when(mockUseCase.call('Test@123', 'Test@1234')).thenAnswer( + when(mockAuthStorage.getToken()).thenAnswer((_) async => 'fake_token'); + when( + mockUseCase.call( + token: 'Bearer fake_token', + password: 'Test@123', + newPassword: 'Test@1234', + ), + ).thenAnswer( (_) async => ErrorApiResult( error: 'Change password failed', ), @@ -131,7 +154,13 @@ void main() { ], verify: (_) { - verify(mockUseCase.call('Test@123', 'Test@1234')).called(1); + verify( + mockUseCase.call( + token: 'Bearer fake_token', + password: 'Test@123', + newPassword: 'Test@1234', + ), + ).called(1); }, ); }); @@ -189,24 +218,6 @@ void main() { }); group('Form Validation', () { - blocTest( - 'emits isFormValid = true when passwords are valid and match', - build: () { - cubit.currentPass = 'Test@123'; - cubit.newPass = 'Test@1234'; - cubit.confirmPass = 'Test@1234'; - return cubit; - }, - act: (cubit) => cubit.doIntent(FormValidIntent()), - expect: () => [ - isA().having( - (s) => s.isFormValid, - 'isFormValid', - true, - ), - ], - ); - blocTest( 'emits isFormValid = false when confirm password does not match', build: () { diff --git a/test/features/auth/presentation/reset_password/pages/change_password_page_test.dart b/test/features/auth/presentation/reset_password/pages/change_password_page_test.dart index 4dbb095..6606975 100644 --- a/test/features/auth/presentation/reset_password/pages/change_password_page_test.dart +++ b/test/features/auth/presentation/reset_password/pages/change_password_page_test.dart @@ -136,21 +136,15 @@ void main() { }); testWidgets('Shows SnackBar on Status.success', (tester) async { - when(cubit.state).thenReturn( - ChangePasswordStates( - isFormValid: true, - data: Resource(status: Status.success), - ), - ); - when(cubit.stream).thenAnswer( - (_) => Stream.value( - ChangePasswordStates( - isFormValid: true, - data: Resource(status: Status.success), - ), - ), + final initialState = ChangePasswordStates(data: Resource.loading()); + final successState = ChangePasswordStates( + data: Resource.success(null), + isFormValid: true, ); + when(cubit.state).thenReturn(initialState); + when(cubit.stream).thenAnswer((_) => Stream.value(successState)); + final testRouter = GoRouter( initialLocation: '/change_password', routes: [ @@ -167,37 +161,26 @@ void main() { await tester.pumpWidget(MaterialApp.router(routerConfig: testRouter)); await tester.pump(); + await tester.pumpAndSettle(); - expect(find.text(LocaleKeys.passwordUpdated), findsOneWidget); + expect(find.text(LocaleKeys.passwordUpdated.tr()), findsOneWidget); + expect(find.text('Login Page'), findsOneWidget); }); testWidgets('Shows Error Dialog on Status.error', (tester) async { - when(cubit.state).thenReturn( - ChangePasswordStates( - isFormValid: true, - data: Resource(status: Status.error), - ), - ); - when(cubit.stream).thenAnswer( - (_) => Stream.value( - ChangePasswordStates( - isFormValid: true, - data: Resource(status: Status.error), - ), - ), + final initialState = ChangePasswordStates(); + final errorState = ChangePasswordStates( + data: Resource.error('Wrong Password'), + isFormValid: true, ); + when(cubit.state).thenReturn(initialState); + when(cubit.stream).thenAnswer((_) => Stream.value(errorState)); + await tester.pumpWidget(buildTestableWidget()); await tester.pump(); + await tester.pumpAndSettle(); - expect(find.text(LocaleKeys.an_error_occurred), findsOneWidget); + expect(find.text('Wrong Password'), findsOneWidget); }); } - -/* - - // when(cubit.state).thenReturn(ChangePasswordStates()); - // when(cubit.stream) - // .thenAnswer((_) => const Stream.empty()); - - */ diff --git a/test/features/driver_orders_details/api/datasource/order_details_remote_datasource_impl_test.dart b/test/features/driver_orders_details/api/datasource/order_details_remote_datasource_impl_test.dart new file mode 100644 index 0000000..bc715b1 --- /dev/null +++ b/test/features/driver_orders_details/api/datasource/order_details_remote_datasource_impl_test.dart @@ -0,0 +1,74 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:mockito/annotations.dart'; +import 'package:mockito/mockito.dart'; +import 'package:cloud_firestore/cloud_firestore.dart'; +import 'package:tracking_app/app/core/network/api_result.dart'; +import 'package:tracking_app/features/driver_orders_details/api/datasource/order_details_remote_datasource_impl.dart'; +import 'package:tracking_app/features/driver_orders_details/data/models/orders_dto.dart'; +import 'order_details_remote_datasource_impl_test.mocks.dart'; + +@GenerateMocks([ + FirebaseFirestore, + CollectionReference, + DocumentReference, + DocumentSnapshot, +]) +void main() { + late OrderDetailsRemoteDatasourceImpl dataSource; + late MockFirebaseFirestore mockFirestore; + late MockCollectionReference> mockCollection; + late MockDocumentReference> mockDocument; + late MockDocumentSnapshot> mockSnapshot; + + const String tOrderId = 'pxkMaEmWYVuvV5jkW0JK'; + + setUp(() { + mockFirestore = MockFirebaseFirestore(); + mockCollection = MockCollectionReference(); + mockDocument = MockDocumentReference(); + mockSnapshot = MockDocumentSnapshot(); + + dataSource = OrderDetailsRemoteDatasourceImpl(firestore: mockFirestore); + }); + group('getOrderStream', () { + final tOrderJson = { + 'driver_id': '1', + 'user_id': 'U11', + 'userAddress': {'name': 'mariam', 'address': 'alex', 'userId': 'U11'}, + 'oder_dt': { + 'items': [], + 'status': 'accepted', + 'totalPrice': 500.0, + 'orderId': tOrderId, + 'userAddress': 'alex', + 'pickupAddress': {'name': 'mariam', 'address': 'alex'}, + }, + }; + + test('should return SuccessApiResult with Stream of OrderDto', () async { + when(mockFirestore.collection('orders')).thenReturn(mockCollection); + when(mockCollection.doc(tOrderId)).thenReturn(mockDocument); + + when(mockSnapshot.exists).thenReturn(true); + when(mockSnapshot.data()).thenReturn(tOrderJson); + when(mockSnapshot.id).thenReturn(tOrderId); + + when( + mockDocument.snapshots(), + ).thenAnswer((_) => Stream.value(mockSnapshot)); + + final result = dataSource.getOrderStream(tOrderId); + + expect(result, isA>>()); + final stream = (result as SuccessApiResult>).data; + await expectLater( + stream, + emits( + isA() + .having((o) => o.orderId, 'orderId', tOrderId) + .having((o) => o.orderDetails.status, 'status', 'accepted'), + ), + ); + }); + }); +} diff --git a/test/features/driver_orders_details/data/mapper/order_dto_mapper_test.dart b/test/features/driver_orders_details/data/mapper/order_dto_mapper_test.dart new file mode 100644 index 0000000..d11f68f --- /dev/null +++ b/test/features/driver_orders_details/data/mapper/order_dto_mapper_test.dart @@ -0,0 +1,115 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:tracking_app/features/driver_orders_details/data/mapper/order_dto_mapper.dart'; +import 'package:tracking_app/features/driver_orders_details/data/models/orders_dto.dart'; +import 'package:tracking_app/features/driver_orders_details/domain/models/orders_model.dart'; + +void main() { + group('OrderDtoMapper', () { + test('Convert OrderDto to OrderModel correctly', () { + final tUserAddressDto = UserAddressDto( + address: 'Alex', + name: 'Mariam', + userId: 'U123', + ); + + final tOrderDto = OrderDto( + driverId: 'D123', + userAddress: tUserAddressDto, + userId: 'U789', + orderId: '22', + orderDetails: OrderDetailsDto( + items: [], + status: 'pending', + totalPrice: 500, + pickupAddress: PickedAddressDto( + name: 'Store', + address: '123 Main St', + ), + orderId: '22', + userAddress: 'alex', + ), + ); + + final result = tOrderDto.toOrderModel(); + + expect(result, isA()); + expect(result.driverId, tOrderDto.driverId); + expect(result.userAddress.name, tOrderDto.userAddress.name); + expect(result.userAddress.address, tOrderDto.userAddress.address); + expect(result.userAddress.userId, tOrderDto.userAddress.userId); + expect(result.userId, tOrderDto.userId); + }); + }); + + group('OrderDetailsDtoMapper', () { + test('Convert OrderDetailsDto to OrderDetailsModel correctly', () { + final tpickupAddressDto = PickedAddressDto( + name: 'Store', + address: '123 Main St', + ); + final tDto = OrderDetailsDto( + items: [], + status: 'pending', + totalPrice: 500, + pickupAddress: tpickupAddressDto, + orderId: '1', + userAddress: 'alex', + ); + + final result = tDto.toOrderDetailsModel(); + + expect(result, isA()); + expect(result.items, tDto.items); + expect(result.status, tDto.status); + expect(result.totalPrice, tDto.totalPrice); + expect(result.pickupAddress.name, tDto.pickupAddress.name); + expect(result.orderId, tDto.orderId); + }); + }); + + group('OrderItemDtoMapper', () { + test('Convert OrderItemDto to OrderItemModel correctly', () { + final tDto = OrderItemDto( + productId: '1', + title: 'Item 1', + price: 100, + quantity: 2, + image: 'image_url', + ); + + final result = tDto.toOrderItemModel(); + + expect(result.productId, tDto.productId); + expect(result.title, tDto.title); + expect(result.price, tDto.price); + expect(result.quantity, tDto.quantity); + expect(result.image, tDto.image); + }); + }); + + group('PickedAddressDtoMapper', () { + test('Convert PickedAddressDto to PickedAddressModel correctly', () { + final tDto = PickedAddressDto(name: 'Store', address: '123 Main St'); + + final result = tDto.toPickedAddressModel(); + + expect(result.name, tDto.name); + expect(result.address, tDto.address); + }); + }); + + group('UserAddressDtoMapper', () { + test('Convert UserAddressDto to UserAddressModel correctly', () { + final tDto = UserAddressDto( + name: 'Store', + address: '123 Main St', + userId: 'U123', + ); + + final result = tDto.toUserAddressModel(); + + expect(result.name, tDto.name); + expect(result.address, tDto.address); + }); + }); +} diff --git a/test/features/driver_orders_details/data/models/orders_dto_test.dart b/test/features/driver_orders_details/data/models/orders_dto_test.dart new file mode 100644 index 0000000..6206376 --- /dev/null +++ b/test/features/driver_orders_details/data/models/orders_dto_test.dart @@ -0,0 +1,213 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:tracking_app/features/driver_orders_details/data/models/orders_dto.dart'; + +void main() { + group('UserAddressDto Tests', () { + test('should return a valid UserAddressDto from JSON', () { + final Map json = { + 'adress': 'Alex', + 'name': 'Mariam', + 'user_id': 'U123', + }; + + final result = UserAddressDto.fromJson(json); + + expect(result.address, 'Alex'); + expect(result.name, 'Mariam'); + expect(result.userId, 'U123'); + }); + + test('should return a valid JSON map from UserAddressDto', () { + final dto = UserAddressDto( + address: 'Alex', + name: 'Mariam', + userId: 'U123', + ); + + final result = dto.toJson(); + + expect(result['adress'], 'Alex'); + expect(result['name'], 'Mariam'); + expect(result['user_id'], 'U123'); + }); + }); + + group('PickedAddressDto Tests', () { + test('should return a valid PickedAddressDto from JSON', () { + final Map json = {'address': 'Alex', 'name': 'Mariam'}; + + final result = PickedAddressDto.fromJson(json); + + expect(result.address, 'Alex'); + expect(result.name, 'Mariam'); + }); + + test('should return a valid JSON map from PickedAddressDto', () { + final dto = PickedAddressDto(address: 'Alex', name: 'Mariam'); + + final result = dto.toJson(); + + expect(result['address'], 'Alex'); + expect(result['name'], 'Mariam'); + }); + }); + + group('OrderItemDto Tests', () { + test('should return a valid OrderItemDto from JSON', () { + final Map json = { + 'productId': '1', + 'title': 'red flower', + 'image': 'url', + 'quantity': 1, + 'price': 100, + }; + + final result = OrderItemDto.fromJson(json); + + expect(result.image, 'url'); + expect(result.title, 'red flower'); + expect(result.quantity, 1); + expect(result.price, 100); + }); + + test('should return a valid JSON map from OrderItemDto', () { + final dto = OrderItemDto( + image: 'Alex', + productId: '1', + title: 'red flower', + quantity: 1, + price: 100, + ); + + final result = dto.toJson(); + + expect(result['image'], 'Alex'); + expect(result['title'], 'red flower'); + expect(result['quantity'], 1); + expect(result['price'], 100); + }); + }); + + group('OrderDetailsDto Tests', () { + test('should return a valid OrderDetailsDto from JSON', () { + final Map json = { + 'items': [], + 'status': 'accepted', + 'totalPrice': 100.0, + 'pickupAddress': {'name': 'Mariam', 'address': 'Alex'}, + 'orderId': 'O456', + 'userAddress': 'alex', + }; + + final result = OrderDetailsDto.fromJson(json); + + expect(result.status, 'accepted'); + expect(result.totalPrice, 100.0); + expect(result.orderId, 'O456'); + }); + + test('should return a valid JSON map from OrderDetailsDto', () { + final dto = OrderDetailsDto( + items: [ + OrderItemDto( + image: 'url', + productId: '1', + title: 'red flower', + quantity: 1, + price: 100, + ), + ], + status: 'accepted', + totalPrice: 100.0, + pickupAddress: PickedAddressDto(address: 'Alex', name: 'Mariam'), + orderId: 'O456', + userAddress: 'alex', + ); + + final result = dto.toJson(); + + expect(result['status'], 'accepted'); + expect(result['totalPrice'], 100.0); + final firstItem = result['items'][0]; + expect(firstItem['image'], 'url'); + expect(firstItem['title'], 'red flower'); + expect(firstItem['price'], 100.0); + expect(result['pickupAddress']['name'], 'Mariam'); + }); + }); + + group('OrderDto Tests', () { + final Map tOrderJson = { + 'driver_id': 'D123', + 'user_id': 'U789', + 'userAddress': { + 'name': 'Home', + 'address': 'Cairo, Egypt', + 'userId': 'U789', + }, + 'oder_dt': { + 'status': 'processing', + 'totalPrice': 250.0, + 'orderId': 'O100', + 'userAddress': 'Cairo, Egypt', + 'pickupAddress': {'name': 'Pharmacy', 'address': 'Downtown'}, + 'items': [ + { + 'productId': 'p1', + 'title': 'Panadol', + 'image': 'panadol.png', + 'quantity': 2, + 'price': 125.0, + }, + ], + }, + }; + + const String tOrderId = 'O100'; + + test('should return a valid OrderDto from JSON and ID', () { + final result = OrderDto.fromJson(tOrderJson, tOrderId); + + expect(result.orderId, tOrderId); + expect(result.driverId, 'D123'); + expect(result.userId, 'U789'); + expect(result.userAddress, isA()); + expect(result.userAddress.name, 'Home'); + + expect(result.orderDetails, isA()); + expect(result.orderDetails.status, 'processing'); + expect(result.orderDetails.items.length, 1); + expect(result.orderDetails.items[0].title, 'Panadol'); + }); + + test('should return a valid JSON map from OrderDto', () { + final dto = OrderDto( + orderId: tOrderId, + driverId: 'D123', + userId: 'U789', + userAddress: UserAddressDto( + name: 'Home', + address: 'Cairo', + userId: 'U789', + ), + orderDetails: OrderDetailsDto( + items: [], + status: 'pending', + totalPrice: 0.0, + pickupAddress: PickedAddressDto(name: 'Store', address: 'Street'), + orderId: tOrderId, + userAddress: 'Cairo', + ), + ); + + final result = dto.toJson(); + + expect(result['driver_id'], 'D123'); + expect(result['user_id'], 'U789'); + + expect(result['userAddress'], isA>()); + expect(result['oder_dt'], isA>()); + expect(result['oder_dt']['status'], 'pending'); + }); + }); +} diff --git a/test/features/driver_orders_details/data/repos/order_details_repo_impl_test.dart b/test/features/driver_orders_details/data/repos/order_details_repo_impl_test.dart new file mode 100644 index 0000000..b10ab3e --- /dev/null +++ b/test/features/driver_orders_details/data/repos/order_details_repo_impl_test.dart @@ -0,0 +1,90 @@ +import 'package:cloud_firestore/cloud_firestore.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mockito/annotations.dart'; +import 'package:mockito/mockito.dart'; +import 'package:tracking_app/app/core/network/api_result.dart'; +import 'package:tracking_app/features/driver_orders_details/data/datasource/order_details_remote_datasource.dart'; +import 'package:tracking_app/features/driver_orders_details/data/models/orders_dto.dart'; +import 'package:tracking_app/features/driver_orders_details/data/repos/order_details_repo_impl.dart'; +import 'package:tracking_app/features/driver_orders_details/domain/models/orders_model.dart'; +import 'order_details_repo_impl_test.mocks.dart'; + +@GenerateMocks([OrderDetailsRemoteDatasource, DocumentSnapshot]) +void main() { + late OrderDetailsRepoImpl repository; + late MockOrderDetailsRemoteDatasource mockRemoteDataSource; + + setUp(() { + mockRemoteDataSource = MockOrderDetailsRemoteDatasource(); + repository = OrderDetailsRepoImpl(mockRemoteDataSource); + provideDummy>>( + ErrorApiResult(error: 'dummy_error'), + ); + }); + + const tOrderId = 'pxkMaEmWYVuvV5jkW0JK'; + + final tOrderDto = OrderDto( + driverId: 'D123', + userAddress: UserAddressDto( + address: 'Alex', + name: 'Mariam', + userId: 'U123', + ), + userId: 'U789', + orderId: tOrderId, + orderDetails: OrderDetailsDto( + items: [], + status: 'accepted', + totalPrice: 150.0, + pickupAddress: PickedAddressDto(name: 'Pharmacy', address: 'Downtown'), + orderId: tOrderId, + userAddress: 'Alex', + ), + ); + + group('getOrderDetails', () { + test( + 'should emit OrderModel when the remote data source returns SuccessApiResult with Stream', + () async { + when( + mockRemoteDataSource.getOrderStream(tOrderId), + ).thenReturn(SuccessApiResult(data: Stream.value(tOrderDto))); + + final result = repository.getOrderDetails(tOrderId); + + expect(result, isA>>()); + final stream = (result as SuccessApiResult>).data; + await expectLater( + stream, + emits( + isA() + .having((o) => o.orderId, 'order id', tOrderId) + .having((o) => o.userAddress.name, 'user name', 'Mariam') + .having( + (o) => o.orderDetails.status, + 'order status', + 'accepted', + ) + .having((o) => o.orderDetails.totalPrice, 'total price', 150.0), + ), + ); + }, + ); + + test( + 'should throw an Exception when the document does not exist', + () async { + const errorMessage = "Network Error"; + when( + mockRemoteDataSource.getOrderStream(tOrderId), + ).thenReturn(ErrorApiResult(error: errorMessage)); + + final result = repository.getOrderDetails(tOrderId); + + expect(result, isA>>()); + expect((result as ErrorApiResult).error, errorMessage); + }, + ); + }); +} diff --git a/test/features/driver_orders_details/domain/models/orders_model_test.dart b/test/features/driver_orders_details/domain/models/orders_model_test.dart new file mode 100644 index 0000000..b4f986d --- /dev/null +++ b/test/features/driver_orders_details/domain/models/orders_model_test.dart @@ -0,0 +1,68 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:tracking_app/features/driver_orders_details/domain/models/orders_model.dart'; + +void main() { + group('OrderModel & UserAddressModel Tests', () { + test('should correctly initialize UserAddressModel with given values', () { + final tAddress = UserAddressModel( + address: 'Cairo', + name: 'Mohamed', + userId: '1', + ); + + expect(tAddress.address, 'Cairo'); + expect(tAddress.name, 'Mohamed'); + expect(tAddress.userId, '1'); + }); + + test('should correctly initialize OrderModel with given values', () { + final tUserAddress = UserAddressModel( + address: 'Cairo', + name: 'Mohamed', + userId: 'USR-555', + ); + + final tOrder = OrderModel( + driverId: 'DRV-101', + userAddress: tUserAddress, + userId: 'USR-555', + orderId: 'ORD-999', + orderDetails: OrderDetailsModel( + items: [], + status: 'picked_up', + totalPrice: 250, + pickupAddress: PickedAddressModel( + name: 'Pharmacy', + address: 'Downtown', + ), + orderId: 'ORD-999', + userAddress: 'Cairo', + ), + ); + + expect(tOrder.driverId, 'DRV-101'); + expect(tOrder.orderId, 'ORD-999'); + expect(tOrder.orderDetails.status, 'picked_up'); + expect(tOrder.orderDetails.totalPrice, 250); + expect(tOrder.userId, 'USR-555'); + + expect(tOrder.userAddress, isA()); + expect(tOrder.userAddress.name, 'Mohamed'); + }); + + test('should support equality check if needed (Optional)', () { + final address1 = UserAddressModel( + address: 'A', + name: 'B', + userId: 'USR-123', + ); + final address2 = UserAddressModel( + address: 'A', + name: 'B', + userId: 'USR-456', + ); + + expect(address1 == address2, isFalse); + }); + }); +} diff --git a/test/features/driver_orders_details/domain/usecases/get_order_details_usecase_test.dart b/test/features/driver_orders_details/domain/usecases/get_order_details_usecase_test.dart new file mode 100644 index 0000000..d27570b --- /dev/null +++ b/test/features/driver_orders_details/domain/usecases/get_order_details_usecase_test.dart @@ -0,0 +1,67 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:mockito/annotations.dart'; +import 'package:mockito/mockito.dart'; +import 'package:tracking_app/app/core/network/api_result.dart'; +import 'package:tracking_app/features/driver_orders_details/domain/models/orders_model.dart'; +import 'package:tracking_app/features/driver_orders_details/domain/repos/order_details_repo.dart'; +import 'package:tracking_app/features/driver_orders_details/domain/usecases/get_order_details_usecase.dart'; + +import 'get_order_details_usecase_test.mocks.dart'; + +@GenerateMocks([OrderDetailsRepo]) +void main() { + late GetOrderDetailsUsecase usecase; + late MockOrderDetailsRepo mockRepo; + + setUp(() { + mockRepo = MockOrderDetailsRepo(); + usecase = GetOrderDetailsUsecase(repo: mockRepo); + provideDummy>>(ErrorApiResult(error: 'dummy')); + }); + + const tOrderId = 'pxkMaEmWYVuvV5jkW0JK'; + + final tOrderModel = OrderModel( + driverId: 'D1', + userAddress: UserAddressModel(address: 'Shebin', name: 'Ali', userId: 'U1'), + userId: 'U1', + orderId: tOrderId, + orderDetails: OrderDetailsModel( + items: [], + status: 'accepted', + totalPrice: 500, + pickupAddress: PickedAddressModel(name: 'Pharmacy', address: 'Downtown'), + orderId: tOrderId, + userAddress: 'Shebin', + ), + ); + + group('GetOrderDetailsUsecase test', () { + test( + 'should return SuccessApiResult containing the Stream from the repository', + () async { + when( + mockRepo.getOrderDetails(any), + ).thenReturn(SuccessApiResult(data: Stream.value(tOrderModel))); + + final result = usecase.call(tOrderId); + + expect(result, isA>>()); + final stream = (result as SuccessApiResult>).data; + await expectLater(stream, emits(tOrderModel)); + verify(mockRepo.getOrderDetails(tOrderId)).called(1); + }, + ); + + test('should return ErrorApiResult when the repository fails', () async { + when( + mockRepo.getOrderDetails(any), + ).thenReturn(ErrorApiResult(error: 'Error from Repository')); + + final result = usecase.call(tOrderId); + + expect(result, isA>>()); + expect((result as ErrorApiResult).error, 'Error from Repository'); + }); + }); +} diff --git a/test/features/driver_orders_details/presentation/pages/drivers_orders_details_page_test.dart b/test/features/driver_orders_details/presentation/pages/drivers_orders_details_page_test.dart new file mode 100644 index 0000000..18f5d20 --- /dev/null +++ b/test/features/driver_orders_details/presentation/pages/drivers_orders_details_page_test.dart @@ -0,0 +1,123 @@ +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:mockito/annotations.dart'; +import 'package:mockito/mockito.dart'; +import 'package:tracking_app/app/config/base_state/base_state.dart'; +import 'package:tracking_app/app/config/di/di.dart'; +import 'package:tracking_app/features/driver_orders_details/domain/models/orders_model.dart'; +import 'package:tracking_app/features/driver_orders_details/presentation/manager/order_details_cubit.dart'; +import 'package:tracking_app/features/driver_orders_details/presentation/manager/order_details_states.dart'; +import 'package:tracking_app/features/driver_orders_details/presentation/pages/drivers_orders_details_page.dart'; +import 'package:tracking_app/features/driver_orders_details/presentation/widgets/address_card.dart'; +import 'drivers_orders_details_page_test.mocks.dart'; + +@GenerateMocks([OrderDetailsCubit]) +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + late MockOrderDetailsCubit mockCubit; + + setUp(() async { + await getIt.reset(); + mockCubit = MockOrderDetailsCubit(); + getIt.registerFactory(() => mockCubit); + when(mockCubit.state).thenReturn(OrderDetailsStates()); + when(mockCubit.stream).thenAnswer((_) => const Stream.empty()); + }); + + Widget buildTestableWidget() { + return EasyLocalization( + supportedLocales: const [Locale('en')], + path: 'assets/translations', + fallbackLocale: const Locale('en'), + startLocale: const Locale('en'), + saveLocale: false, + child: Builder( + builder: (context) { + return MaterialApp( + home: BlocProvider.value( + value: mockCubit, + child: const DriversOrdersDetailsPage(), + ), + ); + }, + ), + ); + } + + final tOrderModel = OrderModel( + driverId: 'D1', + userAddress: UserAddressModel(address: 'Shebin', name: 'Ali', userId: 'U1'), + userId: 'U1', + orderId: 'N123', + orderDetails: OrderDetailsModel( + items: [], + status: 'accepted', + totalPrice: 500, + pickupAddress: PickedAddressModel(name: 'Pharmacy', address: 'Downtown'), + orderId: 'N123', + userAddress: 'Shebin', + ), + ); + + group('DriversOrdersDetailsPage Widget Tests', () { + testWidgets('should show CircularProgressIndicator when state is loading', ( + tester, + ) async { + when( + mockCubit.state, + ).thenReturn(OrderDetailsStates(data: Resource.loading())); + when(mockCubit.stream).thenAnswer( + (_) => Stream.value(OrderDetailsStates(data: Resource.loading())), + ); + + await tester.pumpWidget(buildTestableWidget()); + await tester.pump(); + + expect(find.byType(CircularProgressIndicator), findsOneWidget); + }); + + testWidgets( + 'should display order details correctly when state is success', + (tester) async { + when( + mockCubit.state, + ).thenReturn(OrderDetailsStates(data: Resource.success(tOrderModel))); + when(mockCubit.stream).thenAnswer( + (_) => Stream.value( + OrderDetailsStates(data: Resource.success(tOrderModel)), + ), + ); + + await tester.pumpWidget(buildTestableWidget()); + await tester.pump(); + + expect(find.textContaining('N123'), findsOneWidget); + expect(find.text('Ali'), findsOneWidget); + expect(find.text('Shebin'), findsAtLeastNWidgets(1)); + expect(find.textContaining('500'), findsOneWidget); + expect(find.byType(AddressCard), findsAtLeastNWidgets(2)); + }, + ); + + testWidgets('should display error message when state is error', ( + tester, + ) async { + const errorMessage = 'Failed to load order'; + when( + mockCubit.state, + ).thenReturn(OrderDetailsStates(data: Resource.error(errorMessage))); + when(mockCubit.stream).thenAnswer( + (_) => Stream.value( + OrderDetailsStates(data: Resource.error(errorMessage)), + ), + ); + + await tester.pumpWidget(buildTestableWidget()); + await tester.pump(); + + expect(find.text(errorMessage), findsOneWidget); + }); + }); +} diff --git a/test/features/home/api/driverOrderDataS_imp_test.dart b/test/features/home/api/driverOrderDataS_imp_test.dart new file mode 100644 index 0000000..9071216 --- /dev/null +++ b/test/features/home/api/driverOrderDataS_imp_test.dart @@ -0,0 +1,75 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:mockito/annotations.dart'; +import 'package:mockito/mockito.dart'; +import 'package:retrofit/retrofit.dart'; +import 'package:tracking_app/app/core/api_manger/api_client.dart'; +import 'package:tracking_app/app/core/network/api_result.dart'; +import 'package:tracking_app/features/home/api/driverOrderDataS_imp.dart'; +import 'package:tracking_app/features/home/data/model/response/orderRespons.dart'; +import 'package:dio/dio.dart'; + +import 'driverOrderDataS_imp_test.mocks.dart'; + +@GenerateMocks([ApiClient]) +void main() { + late DriverOrderDataSourceImpl dataSource; + late MockApiClient mockApiClient; + + setUp(() { + mockApiClient = MockApiClient(); + dataSource = DriverOrderDataSourceImpl(mockApiClient); + }); + + group('DriverOrderDataSourceImpl', () { + const tToken = 'test_token'; + final tOrderResponse = OrderResponse(message: 'Success', orders: []); + + test( + 'should return SuccessApiResult when the call to ApiClient is successful', + () async { + // Arrange + final httpResponse = HttpResponse( + tOrderResponse, + Response( + data: tOrderResponse, + requestOptions: RequestOptions(path: ''), + statusCode: 200, + ), + ); + when( + mockApiClient.getPendingOrders(any), + ).thenAnswer((_) async => httpResponse); + + // Act + final result = await dataSource.getPendingOrders(tToken); + + // Assert + expect(result, isA>()); + verify(mockApiClient.getPendingOrders(tToken)); + verifyNoMoreInteractions(mockApiClient); + }, + ); + + test( + 'should return ErrorApiResult when the call to ApiClient throws an exception', + () async { + // Arrange + when(mockApiClient.getPendingOrders(any)).thenThrow( + DioException( + requestOptions: RequestOptions(path: ''), + error: 'Error', + type: DioExceptionType.unknown, + ), + ); + + // Act + final result = await dataSource.getPendingOrders(tToken); + + // Assert + expect(result, isA>()); + verify(mockApiClient.getPendingOrders(tToken)); + verifyNoMoreInteractions(mockApiClient); + }, + ); + }); +} diff --git a/test/features/home/data/model/response/orderRespons_test.dart b/test/features/home/data/model/response/orderRespons_test.dart new file mode 100644 index 0000000..60b946c --- /dev/null +++ b/test/features/home/data/model/response/orderRespons_test.dart @@ -0,0 +1,30 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:tracking_app/features/home/data/model/response/orderRespons.dart'; + +void main() { + group('OrderResponse', () { + final tOrderResponse = OrderResponse(message: 'Success'); + + test('should work with copyWith', () { + final result = tOrderResponse.copyWith(message: 'New Success'); + expect(result.message, 'New Success'); + }); + + test('fromJson should return a valid model', () { + final Map jsonMap = {"message": "Success", "orders": []}; + final result = OrderResponse.fromJson(jsonMap); + expect(result, isA()); + expect(result.message, "Success"); + }); + + test('toJson should return a JSON map containing proper data', () { + final result = tOrderResponse.toJson(); + final expectedMap = { + "message": "Success", + "metadata": null, + "orders": null, + }; + expect(result, expectedMap); + }); + }); +} diff --git a/test/features/home/data/repo/driverOrderRepo_impl_test.dart b/test/features/home/data/repo/driverOrderRepo_impl_test.dart new file mode 100644 index 0000000..f1f142e --- /dev/null +++ b/test/features/home/data/repo/driverOrderRepo_impl_test.dart @@ -0,0 +1,64 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:mockito/annotations.dart'; +import 'package:mockito/mockito.dart'; +import 'package:tracking_app/app/core/network/api_result.dart'; +import 'package:tracking_app/features/home/data/datascourse/driverOrderDatascource.dart'; +import 'package:tracking_app/features/home/data/model/response/orderRespons.dart'; +import 'package:tracking_app/features/home/data/repo/driverOrderRepo_impl.dart'; + +import 'driverOrderRepo_impl_test.mocks.dart'; + +@GenerateMocks([DriverOrderDataSource]) +void main() { + late DriverOrderRepositoryImpl repository; + late MockDriverOrderDataSource mockDataSource; + + setUp(() { + provideDummy>( + SuccessApiResult(data: OrderResponse()), + ); + mockDataSource = MockDriverOrderDataSource(); + repository = DriverOrderRepositoryImpl(mockDataSource); + }); + + group('DriverOrderRepositoryImpl', () { + const tToken = 'test_token'; + final tOrderResponse = OrderResponse(message: 'Success', orders: []); + + test( + 'should return data when the call to remote data source is successful', + () async { + // Arrange + when( + mockDataSource.getPendingOrders(any), + ).thenAnswer((_) async => SuccessApiResult(data: tOrderResponse)); + + // Act + final result = await repository.getPendingOrders(tToken); + + // Assert + expect(result, isA>()); + verify(mockDataSource.getPendingOrders(tToken)); + verifyNoMoreInteractions(mockDataSource); + }, + ); + + test( + 'should return error when the call to remote data source is unsuccessful', + () async { + // Arrange + when( + mockDataSource.getPendingOrders(any), + ).thenAnswer((_) async => ErrorApiResult(error: 'Error')); + + // Act + final result = await repository.getPendingOrders(tToken); + + // Assert + expect(result, isA>()); + verify(mockDataSource.getPendingOrders(tToken)); + verifyNoMoreInteractions(mockDataSource); + }, + ); + }); +} diff --git a/test/features/home/domain/usecases/getdriverOrderUsecase_test.dart b/test/features/home/domain/usecases/getdriverOrderUsecase_test.dart new file mode 100644 index 0000000..2d21ae9 --- /dev/null +++ b/test/features/home/domain/usecases/getdriverOrderUsecase_test.dart @@ -0,0 +1,58 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:mockito/annotations.dart'; +import 'package:mockito/mockito.dart'; +import 'package:tracking_app/app/core/network/api_result.dart'; +import 'package:tracking_app/features/home/data/model/response/orderRespons.dart'; +import 'package:tracking_app/features/home/domain/repo/driverOrderRepo.dart'; +import 'package:tracking_app/features/home/domain/usecase/getdriverOrderUsecase.dart'; + +import 'getdriverOrderUsecase_test.mocks.dart'; + +@GenerateMocks([DriverOrderRepo]) +void main() { + late GetDriverOrdersUseCase useCase; + late MockDriverOrderRepo mockRepository; + + setUp(() { + provideDummy>( + SuccessApiResult(data: OrderResponse()), + ); + mockRepository = MockDriverOrderRepo(); + useCase = GetDriverOrdersUseCase(mockRepository); + }); + + const tToken = 'test_token'; + final tOrderResponse = OrderResponse(message: 'Success', orders: []); + + group('GetDriverOrdersUseCase', () { + test('should get pending orders from the repository', () async { + // Arrange + when( + mockRepository.getPendingOrders(any), + ).thenAnswer((_) async => SuccessApiResult(data: tOrderResponse)); + + // Act + final result = await useCase(tToken); + + // Assert + expect(result, isA>()); + verify(mockRepository.getPendingOrders(tToken)); + verifyNoMoreInteractions(mockRepository); + }); + + test('should return error value from the repository', () async { + // Arrange + when( + mockRepository.getPendingOrders(any), + ).thenAnswer((_) async => ErrorApiResult(error: 'Error')); + + // Act + final result = await useCase(tToken); + + // Assert + expect(result, isA>()); + verify(mockRepository.getPendingOrders(tToken)); + verifyNoMoreInteractions(mockRepository); + }); + }); +} diff --git a/test/features/home/presentation/manger/driverorderCubit_test.dart b/test/features/home/presentation/manger/driverorderCubit_test.dart new file mode 100644 index 0000000..57251dc --- /dev/null +++ b/test/features/home/presentation/manger/driverorderCubit_test.dart @@ -0,0 +1,188 @@ +import 'package:bloc_test/bloc_test.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mockito/annotations.dart'; +import 'package:mockito/mockito.dart'; +import 'package:tracking_app/app/config/auth_storage/auth_storage.dart'; +import 'package:tracking_app/app/config/base_state/base_state.dart'; +import 'package:tracking_app/app/core/network/api_result.dart'; +import 'package:tracking_app/features/home/data/model/response/orderRespons.dart'; +import 'package:tracking_app/features/home/domain/repo/driverOrderRepo.dart'; +import 'package:tracking_app/features/home/domain/usecase/getdriverOrderUsecase.dart'; +import 'package:tracking_app/features/home/domain/usecase/upload_driver_fire_data_use_case.dart'; +import 'package:tracking_app/features/home/domain/usecase/upload_order_fire_data_use_case.dart'; +import 'package:tracking_app/features/home/presentation/manger/driverorderCubit.dart'; +import 'package:tracking_app/features/home/presentation/manger/driverorderIntent.dart'; +import 'package:tracking_app/features/home/presentation/manger/driverorderStates.dart'; + +import 'driverorderCubit_test.mocks.dart'; + +@GenerateMocks([ + DriverOrderRepo, + AuthStorage, + UploadDriverFireDataUseCase, + UploadOrderFireDataUseCase, +]) +void main() { + late DriverOrderCubit driverOrderCubit; + late MockDriverOrderRepo mockDriverOrderRepo; + late MockUploadDriverFireDataUseCase mockUploadDriverFireDataUseCase; + late MockUploadOrderFireDataUseCase mockUploadOrderFireDataUseCase; + late GetDriverOrdersUseCase getDriverOrdersUseCase; + late MockAuthStorage mockAuthStorage; + + setUp(() { + provideDummy>( + SuccessApiResult(data: OrderResponse()), + ); + mockDriverOrderRepo = MockDriverOrderRepo(); + mockAuthStorage = MockAuthStorage(); + mockUploadDriverFireDataUseCase = MockUploadDriverFireDataUseCase(); + mockUploadOrderFireDataUseCase = MockUploadOrderFireDataUseCase(); + getDriverOrdersUseCase = GetDriverOrdersUseCase(mockDriverOrderRepo); + driverOrderCubit = DriverOrderCubit( + getDriverOrdersUseCase, + mockAuthStorage, + mockUploadDriverFireDataUseCase, + mockUploadOrderFireDataUseCase, + mockDriverOrderRepo, + ); + }); + + tearDown(() { + driverOrderCubit.close(); + }); + + group('DriverOrderCubit', () { + test('initial state is DriverOrderState with Resource.initial', () { + expect(driverOrderCubit.state.orderResource.status, Status.initial); + }); + + final tOrderResponse = OrderResponse( + message: 'Success', + orders: [ + Order(id: '1', state: 'pending'), + Order(id: '2', state: 'pending'), + ], + ); + + group('GetPendingOrders', () { + blocTest( + 'emits [loading, success] when GetPendingOrders is added and token exists and api call is successful', + build: () { + when(mockAuthStorage.getToken()).thenAnswer((_) async => 'token'); + when( + mockDriverOrderRepo.getPendingOrders('token'), + ).thenAnswer((_) async => SuccessApiResult(data: tOrderResponse)); + return driverOrderCubit; + }, + act: (cubit) => cubit.onIntent(GetPendingOrders()), + expect: () => [ + isA().having( + (state) => state.orderResource.status, + 'status', + Status.loading, + ), + isA() + .having( + (state) => state.orderResource.status, + 'status', + Status.success, + ) + .having( + (state) => state.orderResource.data, + 'data', + tOrderResponse, + ), + ], + ); + + blocTest( + 'emits [loading, error] when GetPendingOrders is added and token is null', + build: () { + when(mockAuthStorage.getToken()).thenAnswer((_) async => null); + return driverOrderCubit; + }, + act: (cubit) => cubit.onIntent(GetPendingOrders()), + expect: () => [ + isA().having( + (state) => state.orderResource.status, + 'status', + Status.loading, + ), + isA() + .having( + (state) => state.orderResource.status, + 'status', + Status.error, + ) + .having( + (state) => state.orderResource.error, + 'error', + 'User not authenticated', + ), + ], + ); + + blocTest( + 'emits [loading, error] when GetPendingOrders is added and api call fails', + build: () { + when(mockAuthStorage.getToken()).thenAnswer((_) async => 'token'); + when( + mockDriverOrderRepo.getPendingOrders('token'), + ).thenAnswer((_) async => ErrorApiResult(error: 'API Error')); + return driverOrderCubit; + }, + act: (cubit) => cubit.onIntent(GetPendingOrders()), + expect: () => [ + isA().having( + (state) => state.orderResource.status, + 'status', + Status.loading, + ), + isA() + .having( + (state) => state.orderResource.status, + 'status', + Status.error, + ) + .having( + (state) => state.orderResource.error, + 'error', + 'API Error', + ), + ], + ); + }); + + group('RemoveOrder', () { + final orderToRemove = Order(id: '1', state: 'pending'); + final orderToKeep = Order(id: '2', state: 'pending'); + final initialOrders = [orderToRemove, orderToKeep]; + final initialOrderResponse = OrderResponse(orders: initialOrders); + + blocTest( + 'emits [success] with updated orders when RemoveOrder is added', + build: () => driverOrderCubit, + seed: () => DriverOrderState( + orderResource: Resource.success(initialOrderResponse), + ), + act: (cubit) => cubit.onIntent(RemoveOrder(orderToRemove)), + expect: () => [ + isA().having( + (state) => state.orderResource.data?.orders, + 'orders', + [orderToKeep], + ), + ], + ); + + blocTest( + 'does nothing when RemoveOrder is added but current state is not success', + build: () => driverOrderCubit, + seed: () => DriverOrderState(orderResource: Resource.loading()), + act: (cubit) => cubit.onIntent(RemoveOrder(orderToRemove)), + expect: () => [], + ); + }); + }); +} diff --git a/test/features/home/presentation/pages/driverOrderScreen_test.dart b/test/features/home/presentation/pages/driverOrderScreen_test.dart new file mode 100644 index 0000000..e1b5c3f --- /dev/null +++ b/test/features/home/presentation/pages/driverOrderScreen_test.dart @@ -0,0 +1,124 @@ +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:get_it/get_it.dart'; +import 'package:mockito/annotations.dart'; +import 'package:mockito/mockito.dart'; +import 'package:shared_preferences/shared_preferences.dart'; +import 'package:tracking_app/app/config/auth_storage/auth_storage.dart'; +import 'package:tracking_app/app/core/network/api_result.dart'; +import 'package:tracking_app/features/home/data/model/response/orderRespons.dart'; +import 'package:tracking_app/features/home/domain/repo/driverOrderRepo.dart'; +import 'package:tracking_app/features/home/domain/usecase/getdriverOrderUsecase.dart'; +import 'package:tracking_app/features/home/domain/usecase/upload_driver_fire_data_use_case.dart'; +import 'package:tracking_app/features/home/domain/usecase/upload_order_fire_data_use_case.dart'; +import 'package:tracking_app/features/home/presentation/manger/driverorderCubit.dart'; +import 'package:tracking_app/features/home/presentation/pages/driverOrderScreen.dart'; + +import 'driverOrderScreen_test.mocks.dart'; + +@GenerateMocks([ + DriverOrderRepo, + AuthStorage, + UploadDriverFireDataUseCase, + UploadOrderFireDataUseCase, +]) +void main() { + late MockDriverOrderRepo mockDriverOrderRepo; + late MockAuthStorage mockAuthStorage; + late MockUploadDriverFireDataUseCase mockUploadDriverFireDataUseCase; + late MockUploadOrderFireDataUseCase mockUploadOrderFireDataUseCase; + late GetDriverOrdersUseCase getDriverOrdersUseCase; + + setUpAll(() async { + SharedPreferences.setMockInitialValues({}); + await EasyLocalization.ensureInitialized(); + }); + + setUp(() async { + mockDriverOrderRepo = MockDriverOrderRepo(); + mockAuthStorage = MockAuthStorage(); + mockUploadDriverFireDataUseCase = MockUploadDriverFireDataUseCase(); + mockUploadOrderFireDataUseCase = MockUploadOrderFireDataUseCase(); + getDriverOrdersUseCase = GetDriverOrdersUseCase(mockDriverOrderRepo); + + provideDummy>( + SuccessApiResult(data: OrderResponse()), + ); + + await GetIt.I.reset(); + GetIt.I.registerFactory( + () => DriverOrderCubit( + getDriverOrdersUseCase, + mockAuthStorage, + mockUploadDriverFireDataUseCase, + mockUploadOrderFireDataUseCase, + mockDriverOrderRepo, + ), + ); + }); + + Widget createWidgetUnderTest() { + return EasyLocalization( + supportedLocales: const [Locale('en')], + path: 'assets/translations', + fallbackLocale: const Locale('en'), + child: const MaterialApp(home: DriverOrderScreen()), + ); + } + + group('DriverOrderScreen Integration Tests', () { + testWidgets('displays CircularProgressIndicator when loading', ( + tester, + ) async { + // Arrange + when(mockAuthStorage.getToken()).thenAnswer((_) async => 'token'); + + when(mockDriverOrderRepo.getPendingOrders(any)).thenAnswer((_) async { + await Future.delayed(const Duration(milliseconds: 100)); + return SuccessApiResult(data: OrderResponse(orders: [])); + }); + + // Act + await tester.pumpWidget(createWidgetUnderTest()); + await tester.pump(); + + // Assert + expect(find.byType(CircularProgressIndicator), findsOneWidget); + await tester.pumpAndSettle(); + }); + + testWidgets('displays error message when error occurs', (tester) async { + // Arrange + const errorMessage = 'Network Error'; + when(mockAuthStorage.getToken()).thenAnswer((_) async => 'token'); + when( + mockDriverOrderRepo.getPendingOrders(any), + ).thenAnswer((_) async => ErrorApiResult(error: errorMessage)); + + // Act + await tester.pumpWidget(createWidgetUnderTest()); + await tester.pumpAndSettle(); + + // Assert + expect(find.text(errorMessage), findsOneWidget); + }); + + testWidgets('displays "noPendingOrders" when success but empty list', ( + tester, + ) async { + // Arrange + when(mockAuthStorage.getToken()).thenAnswer((_) async => 'token'); + when(mockDriverOrderRepo.getPendingOrders(any)).thenAnswer( + (_) async => SuccessApiResult(data: OrderResponse(orders: [])), + ); + + // Act + await tester.pumpWidget(createWidgetUnderTest()); + await tester.pumpAndSettle(); + + // Assert + expect(find.text('noPendingOrders'), findsOneWidget); + }); + }); +} diff --git a/test/features/home/presentation/widgets/driverOrderButton_test.dart b/test/features/home/presentation/widgets/driverOrderButton_test.dart new file mode 100644 index 0000000..59fe9a8 --- /dev/null +++ b/test/features/home/presentation/widgets/driverOrderButton_test.dart @@ -0,0 +1,116 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:tracking_app/features/home/presentation/widgets/driverOrderButton.dart'; + +void main() { + group('DriverOrderButton Widget Tests', () { + testWidgets('renders button with correct text', (tester) async { + // Arrange + const buttonText = 'Accept'; + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: DriverOrderButton( + text: buttonText, + onTap: () {}, + isPrimary: true, + ), + ), + ), + ); + + // Assert + expect(find.text(buttonText), findsOneWidget); + }); + + testWidgets('calls onTap when tapped', (tester) async { + // Arrange + var isTapped = false; + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: DriverOrderButton( + text: 'Tap Me', + onTap: () { + isTapped = true; + }, + isPrimary: true, + ), + ), + ), + ); + + // Act + await tester.tap(find.byType(DriverOrderButton)); + await tester.pumpAndSettle(); + + // Assert + expect(isTapped, isTrue); + }); + + testWidgets('renders primary style correctly', (tester) async { + // Arrange + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: DriverOrderButton( + text: 'Primary', + onTap: () {}, + isPrimary: true, + ), + ), + ), + ); + + // Verify Container decoration + final container = tester.widget( + find.ancestor( + of: find.text('Primary'), + matching: find.byType(Container), + ), + ); + final decoration = container.decoration as BoxDecoration; + + // Assert + expect(decoration.color, const Color(0xFFE91E63)); // Primary color + expect(decoration.border, isNull); + + // Verify Text style + final text = tester.widget(find.text('Primary')); + expect(text.style?.color, Colors.white); + }); + + testWidgets('renders secondary style correctly', (tester) async { + // Arrange + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: DriverOrderButton( + text: 'Secondary', + onTap: () {}, + isPrimary: false, + ), + ), + ), + ); + + // Verify Container decoration + final container = tester.widget( + find.ancestor( + of: find.text('Secondary'), + matching: find.byType(Container), + ), + ); + final decoration = container.decoration as BoxDecoration; + + // Assert + expect(decoration.color, Colors.white); + expect(decoration.border, isNotNull); + // We can check border color if needed, but existence is good for now + + // Verify Text style + final text = tester.widget(find.text('Secondary')); + expect(text.style?.color, const Color(0xFFE91E63)); + }); + }); +} diff --git a/test/features/home/presentation/widgets/driverOrderInfoCard_test.dart b/test/features/home/presentation/widgets/driverOrderInfoCard_test.dart new file mode 100644 index 0000000..303e263 --- /dev/null +++ b/test/features/home/presentation/widgets/driverOrderInfoCard_test.dart @@ -0,0 +1,108 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:network_image_mock/network_image_mock.dart'; +import 'package:tracking_app/features/home/presentation/widgets/driverOrderInfoCard.dart'; + +void main() { + group('DriverOrderInfoCard Widget Tests', () { + testWidgets('renders correct title and subtitle', (tester) async { + const title = 'Test Title'; + const subtitle = 'Test Subtitle'; + + await tester.pumpWidget( + const MaterialApp( + home: Scaffold( + body: DriverOrderInfoCard( + image: null, + title: title, + subtitle: subtitle, + isStore: false, + ), + ), + ), + ); + + expect(find.text(title), findsOneWidget); + expect(find.text(subtitle), findsOneWidget); + }); + + testWidgets('renders store icon when isStore is true and image is null', ( + tester, + ) async { + await tester.pumpWidget( + const MaterialApp( + home: Scaffold( + body: DriverOrderInfoCard( + image: null, + title: 'Store', + subtitle: 'Address', + isStore: true, + ), + ), + ), + ); + + expect(find.byIcon(Icons.store), findsOneWidget); + expect(find.byIcon(Icons.person), findsNothing); + }); + + testWidgets('renders person icon when isStore is false and image is null', ( + tester, + ) async { + await tester.pumpWidget( + const MaterialApp( + home: Scaffold( + body: DriverOrderInfoCard( + image: null, + title: 'User', + subtitle: 'Address', + isStore: false, + ), + ), + ), + ); + + expect(find.byIcon(Icons.person), findsOneWidget); + expect(find.byIcon(Icons.store), findsNothing); + }); + + testWidgets('renders NetworkImage when image is provided', (tester) async { + const imageUrl = 'https://example.com/image.jpg'; + + await mockNetworkImagesFor(() async { + await tester.pumpWidget( + const MaterialApp( + home: Scaffold( + body: DriverOrderInfoCard( + image: imageUrl, + title: 'With Image', + subtitle: 'Address', + isStore: false, + ), + ), + ), + ); + }); + + // We need to find the specific container with the image. + // The hierarchy is Container > Row > [Container(image), SizedBox, Expanded(...)] + // So let's look for a Container with a BoxDecoration that has an image. + + final imageContainer = find.byWidgetPredicate((widget) { + if (widget is Container && widget.decoration is BoxDecoration) { + final decoration = widget.decoration as BoxDecoration; + return decoration.image != null && + decoration.image!.image is NetworkImage && + (decoration.image!.image as NetworkImage).url == imageUrl; + } + return false; + }); + + expect(imageContainer, findsOneWidget); + + // Verify no fallback icon is shown + expect(find.byIcon(Icons.person), findsNothing); + expect(find.byIcon(Icons.store), findsNothing); + }); + }); +} diff --git a/test/features/home/presentation/widgets/driverOrderItem_test.dart b/test/features/home/presentation/widgets/driverOrderItem_test.dart new file mode 100644 index 0000000..012b22b --- /dev/null +++ b/test/features/home/presentation/widgets/driverOrderItem_test.dart @@ -0,0 +1,119 @@ +// import 'package:easy_localization/easy_localization.dart'; +// import 'package:flutter/material.dart'; +// import 'package:flutter_test/flutter_test.dart'; +// import 'package:network_image_mock/network_image_mock.dart'; +// import 'package:shared_preferences/shared_preferences.dart'; +// import 'package:tracking_app/features/home/data/model/response/orderRespons.dart'; +// import 'package:tracking_app/features/home/presentation/widgets/driverOrderButton.dart'; +// import 'package:tracking_app/features/home/presentation/widgets/driverOrderInfoCard.dart'; +// import 'package:tracking_app/features/home/presentation/widgets/driverOrderItem.dart'; + +// void main() { +// setUpAll(() async { +// TestWidgetsFlutterBinding.ensureInitialized(); +// SharedPreferences.setMockInitialValues({}); +// await EasyLocalization.ensureInitialized(); +// }); + +// Widget createWidgetUnderTest( +// Order order, { +// VoidCallback? onAccept, +// VoidCallback? onReject, +// }) { +// return EasyLocalization( +// supportedLocales: const [Locale('en')], +// path: 'assets/translations', +// fallbackLocale: const Locale('en'), +// child: Builder( +// builder: (context) => MaterialApp( +// localizationsDelegates: context.localizationDelegates, +// supportedLocales: context.supportedLocales, +// locale: context.locale, +// home: Scaffold( +// body: DriverOrderItem( +// order: order, +// onAccept: onAccept ?? () {}, +// onReject: onReject ?? () {}, +// ), +// ), +// ), +// ), +// ); +// } + +// group('DriverOrderItem Widget Tests', () { +// final testOrder = Order( +// id: '1', +// totalPrice: 100, +// store: Store( +// name: 'Test Store', +// address: 'Store Address', +// image: 'store_image.jpg', +// ), +// user: User( +// firstName: 'John', +// lastName: 'Doe', +// photo: 'user_photo.jpg', +// ), +// shippingAddress: ShippingAddress(street: 'User Street'), +// ); + +// testWidgets('renders order details correctly', (tester) async { +// await mockNetworkImagesFor(() async { +// await tester.pumpWidget(createWidgetUnderTest(testOrder)); +// await tester.pumpAndSettle(); +// }); + +// expect(find.text('Test Store'), findsOneWidget); +// expect(find.text('Store Address'), findsOneWidget); +// expect(find.text('John Doe'), findsOneWidget); +// expect(find.text('User Street'), findsOneWidget); +// expect(find.textContaining('100'), findsOneWidget); + +// expect(find.byType(DriverOrderInfoCard), findsNWidgets(2)); +// expect(find.byType(DriverOrderButton), findsNWidgets(2)); +// }); + +// testWidgets('calls onAccept when accept button is tapped', (tester) async { +// var isAccepted = false; + +// await mockNetworkImagesFor(() async { +// await tester.pumpWidget( +// createWidgetUnderTest( +// testOrder, +// onAccept: () => isAccepted = true, +// ), +// ); +// await tester.pumpAndSettle(); +// }); + +// final acceptButton = find.byKey(const Key('accept_button')); + +// await tester.tap(acceptButton); +// await tester.pump(); + +// expect(isAccepted, isTrue); +// }); + +// testWidgets('calls onReject when reject button is tapped', (tester) async { +// var isRejected = false; + +// await mockNetworkImagesFor(() async { +// await tester.pumpWidget( +// createWidgetUnderTest( +// testOrder, +// onReject: () => isRejected = true, +// ), +// ); +// await tester.pumpAndSettle(); +// }); + +// final rejectButton = find.byKey(const Key('reject_button')); + +// await tester.tap(rejectButton); +// await tester.pump(); + +// expect(isRejected, isTrue); +// }); +// }); +// } \ No newline at end of file diff --git a/test/features/my_orders/api/datasource/my_orders_remote_data_source_imp_test.dart b/test/features/my_orders/api/datasource/my_orders_remote_data_source_imp_test.dart new file mode 100644 index 0000000..55ecd3e --- /dev/null +++ b/test/features/my_orders/api/datasource/my_orders_remote_data_source_imp_test.dart @@ -0,0 +1,85 @@ +import 'package:dio/dio.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mockito/annotations.dart'; +import 'package:mockito/mockito.dart'; +import 'package:retrofit/retrofit.dart'; +import 'package:tracking_app/app/core/api_manger/api_client.dart'; +import 'package:tracking_app/app/core/network/api_result.dart'; +import 'package:tracking_app/features/my_orders/api/datasource/my_orders_remote_data_source_imp.dart'; +import 'package:tracking_app/features/my_orders/data/models/response/my_order_response.dart'; + +import 'my_orders_remote_data_source_imp_test.mocks.dart'; + +@GenerateMocks([ApiClient]) +void main() { + late MyOrdersRemoteDataSourceImp dataSource; + late MockApiClient mockApiClient; + + setUp(() { + mockApiClient = MockApiClient(); + dataSource = MyOrdersRemoteDataSourceImp(mockApiClient); + }); + + const tToken = 'token123'; + const tLimit = 10; + const tPage = 1; + final tOrderResponse = MyOrderResponse(orders: []); + + group('MyOrdersRemoteDataSourceImp', () { + test( + 'should return SuccessApiResult when apiClient call is successful', + () async { + // Arrange + final httpResponse = HttpResponse( + tOrderResponse, + Response(requestOptions: RequestOptions(path: ''), statusCode: 200), + ); + when( + mockApiClient.getAllOrders( + token: anyNamed('token'), + limit: anyNamed('limit'), + page: anyNamed('page'), + ), + ).thenAnswer((_) async => httpResponse); + + // Act + final result = await dataSource.getAllOrders( + token: tToken, + limit: tLimit, + page: tPage, + ); + + // Assert + expect(result, isA>()); + expect( + (result as SuccessApiResult).data, + tOrderResponse, + ); + verify( + mockApiClient.getAllOrders(token: tToken, limit: tLimit, page: tPage), + ).called(1); + }, + ); + + test('should return ErrorApiResult when apiClient call fails', () async { + // Arrange + when( + mockApiClient.getAllOrders( + token: anyNamed('token'), + limit: anyNamed('limit'), + page: anyNamed('page'), + ), + ).thenThrow(DioException(requestOptions: RequestOptions(path: ''))); + + // Act + final result = await dataSource.getAllOrders( + token: tToken, + limit: tLimit, + page: tPage, + ); + + // Assert + expect(result, isA>()); + }); + }); +} diff --git a/test/features/my_orders/data/mappers/metadata_mapper_test.dart b/test/features/my_orders/data/mappers/metadata_mapper_test.dart new file mode 100644 index 0000000..b7a9da7 --- /dev/null +++ b/test/features/my_orders/data/mappers/metadata_mapper_test.dart @@ -0,0 +1,52 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:tracking_app/features/my_orders/data/mappers/metadata_mapper.dart'; +import 'package:tracking_app/features/my_orders/data/models/meta_data_dto.dart'; +import 'package:tracking_app/features/my_orders/domain/models/meta_data_entity.dart'; + +void main() { + group('MetadataMapper', () { + test('should map Metadata DTO to MetadataEntity correctly', () { + final dto = Metadata( + currentPage: 1, + totalPages: 10, + totalItems: 100, + limit: 10, + cancelledCount: 5, + completedCount: 95, + ); + + final result = dto.toEntity(); + + expect(result, isA()); + expect(result.currentPage, 1); + expect(result.totalPages, 10); + expect(result.totalItems, 100); + expect(result.limit, 10); + expect(result.cancelledCount, 5); + expect(result.completedCount, 95); + }); + + test( + 'should map Metadata DTO with null fields to MetadataEntity with default values', + () { + final dto = Metadata( + currentPage: null, + totalPages: null, + totalItems: null, + limit: null, + cancelledCount: null, + completedCount: null, + ); + + final result = dto.toEntity(); + + expect(result.currentPage, 0); + expect(result.totalPages, 0); + expect(result.totalItems, 0); + expect(result.limit, 10); + expect(result.cancelledCount, 0); + expect(result.completedCount, 0); + }, + ); + }); +} diff --git a/test/features/my_orders/data/mappers/order_item_mapper_test.dart b/test/features/my_orders/data/mappers/order_item_mapper_test.dart new file mode 100644 index 0000000..76dbe6f --- /dev/null +++ b/test/features/my_orders/data/mappers/order_item_mapper_test.dart @@ -0,0 +1,43 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:tracking_app/features/my_orders/data/mappers/order_item_mapper.dart'; +import 'package:tracking_app/features/my_orders/data/models/order_item_model.dart'; +import 'package:tracking_app/features/my_orders/data/models/product_model.dart'; +import 'package:tracking_app/features/my_orders/domain/models/order_item_entity.dart'; + +void main() { + group('OrderItemMapper', () { + test('should map OrderItem model to OrderItemEntity correctly', () { + final model = OrderItem( + id: 'i1', + product: Product(id: 'p1', price: 100), + price: 100, + quantity: 2, + ); + + final result = model.toEntity(); + + expect(result, isA()); + expect(result.product.id, 'p1'); + expect(result.price, 100); + expect(result.quantity, 2); + }); + + test( + 'should map OrderItem model with null fields to OrderItemEntity with default values', + () { + final model = OrderItem( + id: null, + product: null, + price: null, + quantity: null, + ); + + final result = model.toEntity(); + + expect(result.product.id, ''); + expect(result.price, 0); + expect(result.quantity, 0); + }, + ); + }); +} diff --git a/test/features/my_orders/data/mappers/order_mapper_test.dart b/test/features/my_orders/data/mappers/order_mapper_test.dart new file mode 100644 index 0000000..6480014 --- /dev/null +++ b/test/features/my_orders/data/mappers/order_mapper_test.dart @@ -0,0 +1,79 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:tracking_app/features/my_orders/data/mappers/order_mapper.dart'; +import 'package:tracking_app/features/my_orders/data/models/order_model.dart'; +import 'package:tracking_app/features/my_orders/data/models/user_model.dart'; +import 'package:tracking_app/features/my_orders/data/models/store_model.dart'; +import 'package:tracking_app/features/my_orders/data/models/order_item_model.dart'; +import 'package:tracking_app/features/my_orders/domain/models/order_entity.dart'; + +void main() { + group('OrderMapper', () { + test('should map Order model to OrderEntity correctly', () { + final model = Order( + id: 'o1', + user: User(id: 'u1', firstName: 'Noor', lastName: 'Mohamed'), + store: Store(name: 'Store Name'), + address: 'User Address', + orderItems: [OrderItem(price: 100, quantity: 1)], + totalPrice: 100, + paymentType: 'Cash', + isPaid: true, + isDelivered: true, + state: 'Delivered', + createdAt: '2023-01-01', + orderNumber: 'ORD123', + ); + + final result = model.toEntity(); + + expect(result, isA()); + expect(result.id, 'o1'); + expect(result.user.id, 'u1'); + expect(result.store?.name, 'Store Name'); + expect(result.address, 'User Address'); + expect(result.items.length, 1); + expect(result.totalPrice, 100); + expect(result.paymentType, 'Cash'); + expect(result.isPaid, true); + expect(result.isDelivered, true); + expect(result.state, 'Delivered'); + expect(result.createdAt, '2023-01-01'); + expect(result.orderNumber, 'ORD123'); + }); + + test( + 'should map Order model with null fields to OrderEntity with default values', + () { + final model = Order( + id: null, + user: User(id: null), + store: null, + address: null, + orderItems: null, + totalPrice: null, + paymentType: null, + isPaid: null, + isDelivered: null, + state: null, + createdAt: null, + orderNumber: null, + ); + + final result = model.toEntity(); + + expect(result.id, ''); + expect(result.user.id, ''); + expect(result.store, isNull); + expect(result.address, ''); + expect(result.items, isEmpty); + expect(result.totalPrice, 0); + expect(result.paymentType, ''); + expect(result.isPaid, false); + expect(result.isDelivered, false); + expect(result.state, ''); + expect(result.createdAt, ''); + expect(result.orderNumber, ''); + }, + ); + }); +} diff --git a/test/features/my_orders/data/mappers/orders_list_mapper_test.dart b/test/features/my_orders/data/mappers/orders_list_mapper_test.dart new file mode 100644 index 0000000..32d0a13 --- /dev/null +++ b/test/features/my_orders/data/mappers/orders_list_mapper_test.dart @@ -0,0 +1,37 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:tracking_app/features/my_orders/data/mappers/orders_list_mapper.dart'; +import 'package:tracking_app/features/my_orders/data/models/order_model.dart'; +import 'package:tracking_app/features/my_orders/data/models/user_model.dart'; +import 'package:tracking_app/features/my_orders/domain/models/order_entity.dart'; + +void main() { + group('OrdersListMapper', () { + test('should map List to List correctly', () { + final list = [ + Order( + id: 'o1', + user: User(id: 'u1'), + ), + Order( + id: 'o2', + user: User(id: 'u2'), + ), + ]; + + final result = list.toEntityList(); + + expect(result, isA>()); + expect(result.length, 2); + expect(result[0].id, 'o1'); + expect(result[1].id, 'o2'); + }); + + test('should map empty List to empty List', () { + final list = []; + + final result = list.toEntityList(); + + expect(result, isEmpty); + }); + }); +} diff --git a/test/features/my_orders/data/mappers/product_mapper_test.dart b/test/features/my_orders/data/mappers/product_mapper_test.dart new file mode 100644 index 0000000..510cc0e --- /dev/null +++ b/test/features/my_orders/data/mappers/product_mapper_test.dart @@ -0,0 +1,30 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:tracking_app/features/my_orders/data/mappers/product_mapper.dart'; +import 'package:tracking_app/features/my_orders/data/models/product_model.dart'; +import 'package:tracking_app/features/my_orders/domain/models/product_entity.dart'; + +void main() { + group('ProductMapper', () { + test('should map Product model to ProductEntity correctly', () { + final model = Product(id: 'p1', price: 100); + + final result = model.toEntity(); + + expect(result, isA()); + expect(result.id, 'p1'); + expect(result.price, 100); + }); + + test( + 'should map Product model with null fields to ProductEntity with default values', + () { + final model = Product(id: null, price: null); + + final result = model.toEntity(); + + expect(result.id, ''); + expect(result.price, 0); + }, + ); + }); +} diff --git a/test/features/my_orders/data/mappers/store_mapper_test.dart b/test/features/my_orders/data/mappers/store_mapper_test.dart new file mode 100644 index 0000000..3cac0f7 --- /dev/null +++ b/test/features/my_orders/data/mappers/store_mapper_test.dart @@ -0,0 +1,44 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:tracking_app/features/my_orders/data/mappers/store_mapper.dart'; +import 'package:tracking_app/features/my_orders/data/models/store_model.dart'; +import 'package:tracking_app/features/my_orders/domain/models/store_entity.dart'; + +void main() { + group('StoreMapper', () { + test('should map Store model to StoreEntity correctly', () { + final model = Store( + name: 'Store Name', + image: 'image_url', + address: 'Store Address', + phoneNumber: '01012345678', + ); + + final result = model.toEntity(); + + expect(result, isA()); + expect(result.name, 'Store Name'); + expect(result.image, 'image_url'); + expect(result.address, 'Store Address'); + expect(result.phoneNumber, '01012345678'); + }); + + test( + 'should map Store model with null fields to StoreEntity with default values', + () { + final model = Store( + name: null, + image: null, + address: null, + phoneNumber: null, + ); + + final result = model.toEntity(); + + expect(result.name, ''); + expect(result.image, ''); + expect(result.address, ''); + expect(result.phoneNumber, ''); + }, + ); + }); +} diff --git a/test/features/my_orders/data/mappers/user_mapper_test.dart b/test/features/my_orders/data/mappers/user_mapper_test.dart new file mode 100644 index 0000000..93e4502 --- /dev/null +++ b/test/features/my_orders/data/mappers/user_mapper_test.dart @@ -0,0 +1,48 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:tracking_app/features/my_orders/data/mappers/user_mapper.dart'; +import 'package:tracking_app/features/my_orders/data/models/user_model.dart'; +import 'package:tracking_app/features/my_orders/domain/models/user_entity.dart'; + +void main() { + group('UserMapper', () { + test('should map User model to UserEntity correctly', () { + final model = User( + id: 'u1', + firstName: 'Noor', + lastName: 'Mohamed', + phone: '01012345678', + photo: 'photo_url', + ); + + final result = model.toEntity(); + + expect(result, isA()); + expect(result.id, 'u1'); + expect(result.firstName, 'Noor'); + expect(result.lastName, 'Mohamed'); + expect(result.phone, '01012345678'); + expect(result.photo, 'photo_url'); + }); + + test( + 'should map User model with null fields to UserEntity with default values', + () { + final model = User( + id: null, + firstName: null, + lastName: null, + phone: null, + photo: null, + ); + + final result = model.toEntity(); + + expect(result.id, ''); + expect(result.firstName, ''); + expect(result.lastName, ''); + expect(result.phone, ''); + expect(result.photo, ''); + }, + ); + }); +} diff --git a/test/features/my_orders/data/repo/my_orders_repo_imp_test.dart b/test/features/my_orders/data/repo/my_orders_repo_imp_test.dart new file mode 100644 index 0000000..2d534b9 --- /dev/null +++ b/test/features/my_orders/data/repo/my_orders_repo_imp_test.dart @@ -0,0 +1,113 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:mockito/annotations.dart'; +import 'package:mockito/mockito.dart'; +import 'package:tracking_app/app/core/network/api_result.dart'; +import 'package:tracking_app/features/my_orders/data/datasource/my_orders_remote_data_source.dart'; +import 'package:tracking_app/features/my_orders/data/models/response/my_order_response.dart'; +import 'package:tracking_app/features/my_orders/data/models/order_model.dart'; +import 'package:tracking_app/features/my_orders/data/models/user_model.dart'; +import 'package:tracking_app/features/my_orders/data/repo/my_orders_repo_imp.dart'; +import 'package:tracking_app/features/my_orders/domain/repo/my_orders_repo.dart'; + +import 'my_orders_repo_imp_test.mocks.dart'; + +@GenerateMocks([MyOrdersRemoteDataSource]) +void main() { + late MyOrdersRepoImpl repo; + late MockMyOrdersRemoteDataSource mockRemoteDataSource; + + setUpAll(() { + provideDummy>( + SuccessApiResult(data: MyOrderResponse(orders: [])), + ); + }); + + setUp(() { + mockRemoteDataSource = MockMyOrdersRemoteDataSource(); + repo = MyOrdersRepoImpl(mockRemoteDataSource); + }); + + const tToken = 'token123'; + final tOrderModel = Order( + id: 'o1', + user: User(id: 'u1'), + ); + final tOrderResponse = MyOrderResponse(orders: [tOrderModel], metadata: null); + + group('MyOrdersRepoImpl', () { + test( + 'should return SuccessApiResult with data from remote data source when it is successful and not empty', + () async { + // Arrange + when( + mockRemoteDataSource.getAllOrders( + token: anyNamed('token'), + limit: anyNamed('limit'), + page: anyNamed('page'), + ), + ).thenAnswer((_) async => SuccessApiResult(data: tOrderResponse)); + + // Act + final result = await repo.getAllOrders(token: tToken); + + // Assert + expect(result, isA>()); + final data = (result as SuccessApiResult).data; + expect(data.orders.length, 1); + expect(data.orders[0].id, 'o1'); + verify( + mockRemoteDataSource.getAllOrders(token: tToken, limit: 10, page: 1), + ).called(1); + }, + ); + + test( + 'should return SuccessApiResult with dummy data when remote data source returns empty list', + () async { + // Arrange + final emptyResponse = MyOrderResponse(orders: [], metadata: null); + when( + mockRemoteDataSource.getAllOrders( + token: anyNamed('token'), + limit: anyNamed('limit'), + page: anyNamed('page'), + ), + ).thenAnswer((_) async => SuccessApiResult(data: emptyResponse)); + + // Act + final result = await repo.getAllOrders(token: tToken); + + // Assert + expect(result, isA>()); + final data = (result as SuccessApiResult).data; + expect(data.orders.isNotEmpty, true); + expect(data.orders[0].id, '123456'); + verify( + mockRemoteDataSource.getAllOrders(token: tToken, limit: 10, page: 1), + ).called(1); + }, + ); + + test( + 'should return ErrorApiResult when remote data source call fails', + () async { + // Arrange + const tError = 'Server error'; + when( + mockRemoteDataSource.getAllOrders( + token: anyNamed('token'), + limit: anyNamed('limit'), + page: anyNamed('page'), + ), + ).thenAnswer((_) async => ErrorApiResult(error: tError)); + + // Act + final result = await repo.getAllOrders(token: tToken); + + // Assert + expect(result, isA>()); + expect((result as ErrorApiResult).error, tError); + }, + ); + }); +} diff --git a/test/features/my_orders/domain/usecase/get_order_use_case_test.dart b/test/features/my_orders/domain/usecase/get_order_use_case_test.dart new file mode 100644 index 0000000..6c0a580 --- /dev/null +++ b/test/features/my_orders/domain/usecase/get_order_use_case_test.dart @@ -0,0 +1,99 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:mockito/annotations.dart'; +import 'package:mockito/mockito.dart'; +import 'package:tracking_app/app/core/network/api_result.dart'; +import 'package:tracking_app/features/my_orders/domain/repo/my_orders_repo.dart'; +import 'package:tracking_app/features/my_orders/domain/usecases/get_order_use_case.dart'; + +import 'get_order_use_case_test.mocks.dart'; + +@GenerateMocks([MyOrdersRepo]) +void main() { + late GetOrderUseCase getOrderUseCase; + late MockMyOrdersRepo mockMyOrdersRepo; + + setUpAll(() { + provideDummy>( + SuccessApiResult(data: MyOrdersResult(orders: [])), + ); + }); + + setUp(() { + mockMyOrdersRepo = MockMyOrdersRepo(); + getOrderUseCase = GetOrderUseCase(mockMyOrdersRepo); + }); + + const tToken = 'token123'; + const tPage = 1; + const tLimit = 10; + final tMyOrdersResult = MyOrdersResult(orders: []); + + group('GetOrderUseCase', () { + test( + 'should return SuccessApiResult when repo call is successful', + () async { + // Arrange + when( + mockMyOrdersRepo.getAllOrders( + token: anyNamed('token'), + page: anyNamed('page'), + limit: anyNamed('limit'), + ), + ).thenAnswer((_) async => SuccessApiResult(data: tMyOrdersResult)); + + // Act + final result = await getOrderUseCase.call( + token: tToken, + page: tPage, + limit: tLimit, + ); + + // Assert + expect(result, isA>()); + expect( + (result as SuccessApiResult).data, + tMyOrdersResult, + ); + verify( + mockMyOrdersRepo.getAllOrders( + token: tToken, + page: tPage, + limit: tLimit, + ), + ).called(1); + verifyNoMoreInteractions(mockMyOrdersRepo); + }, + ); + + test('should return ErrorApiResult when repo call fails', () async { + // Arrange + const tErrorMessage = 'An error occurred'; + when( + mockMyOrdersRepo.getAllOrders( + token: anyNamed('token'), + page: anyNamed('page'), + limit: anyNamed('limit'), + ), + ).thenAnswer((_) async => ErrorApiResult(error: tErrorMessage)); + + // Act + final result = await getOrderUseCase.call( + token: tToken, + page: tPage, + limit: tLimit, + ); + + // Assert + expect(result, isA>()); + expect((result as ErrorApiResult).error, tErrorMessage); + verify( + mockMyOrdersRepo.getAllOrders( + token: tToken, + page: tPage, + limit: tLimit, + ), + ).called(1); + verifyNoMoreInteractions(mockMyOrdersRepo); + }); + }); +} diff --git a/test/features/my_orders/presentation/manager/my_orders_cubit_test.dart b/test/features/my_orders/presentation/manager/my_orders_cubit_test.dart new file mode 100644 index 0000000..36d622b --- /dev/null +++ b/test/features/my_orders/presentation/manager/my_orders_cubit_test.dart @@ -0,0 +1,131 @@ +import 'package:bloc_test/bloc_test.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mockito/annotations.dart'; +import 'package:mockito/mockito.dart'; +import 'package:tracking_app/app/config/auth_storage/auth_storage.dart'; +import 'package:tracking_app/app/config/base_state/base_state.dart'; +import 'package:tracking_app/app/core/network/api_result.dart'; +import 'package:tracking_app/features/my_orders/domain/repo/my_orders_repo.dart'; +import 'package:tracking_app/features/my_orders/domain/usecases/get_order_use_case.dart'; +import 'package:tracking_app/features/my_orders/presentation/manager/my_orders_cubit.dart'; +import 'package:tracking_app/features/my_orders/presentation/manager/my_orders_intent.dart'; +import 'package:tracking_app/features/my_orders/presentation/manager/my_orders_state.dart'; + +import 'my_orders_cubit_test.mocks.dart'; + +@GenerateMocks([GetOrderUseCase, AuthStorage]) +void main() { + late MyOrdersCubit cubit; + late MockGetOrderUseCase mockGetOrderUseCase; + late MockAuthStorage mockAuthStorage; + + setUpAll(() { + provideDummy>( + SuccessApiResult(data: MyOrdersResult(orders: [])), + ); + }); + + setUp(() { + mockGetOrderUseCase = MockGetOrderUseCase(); + mockAuthStorage = MockAuthStorage(); + cubit = MyOrdersCubit(mockGetOrderUseCase, mockAuthStorage); + }); + + tearDown(() { + cubit.close(); + }); + + const tToken = 'token123'; + final tOrdersResult = MyOrdersResult(orders: []); + + group('MyOrdersCubit', () { + test('initial state should be correct', () { + expect(cubit.state.ordersResource.status, Status.initial); + expect(cubit.state.orders, isEmpty); + expect(cubit.state.isLoadingMore, false); + }); + + blocTest( + 'emits [loading, success] when GetMyOrdersIntent is successful', + build: () { + when(mockAuthStorage.getToken()).thenAnswer((_) async => tToken); + when( + mockGetOrderUseCase.call( + token: anyNamed('token'), + page: anyNamed('page'), + limit: anyNamed('limit'), + ), + ).thenAnswer((_) async => SuccessApiResult(data: tOrdersResult)); + return cubit; + }, + act: (cubit) => cubit.doIntent(GetMyOrdersIntent(page: 1, limit: 10)), + expect: () => [ + isA().having( + (s) => s.ordersResource.status, + 'status', + Status.loading, + ), + isA().having( + (s) => s.ordersResource.status, + 'status', + Status.success, + ), + ], + verify: (_) { + verify(mockAuthStorage.getToken()).called(1); + verify( + mockGetOrderUseCase.call(token: 'Bearer $tToken', page: 1, limit: 10), + ).called(1); + }, + ); + + blocTest( + 'emits [loading, error] when GetMyOrdersIntent fails', + build: () { + when(mockAuthStorage.getToken()).thenAnswer((_) async => tToken); + when( + mockGetOrderUseCase.call( + token: anyNamed('token'), + page: anyNamed('page'), + limit: anyNamed('limit'), + ), + ).thenAnswer((_) async => ErrorApiResult(error: 'Server error')); + return cubit; + }, + act: (cubit) => cubit.doIntent(GetMyOrdersIntent(page: 1, limit: 10)), + expect: () => [ + isA().having( + (s) => s.ordersResource.status, + 'status', + Status.loading, + ), + isA().having( + (s) => s.ordersResource.status, + 'status', + Status.error, + ), + ], + ); + + blocTest( + 'emits [loading, error] when token is missing', + build: () { + when(mockAuthStorage.getToken()).thenAnswer((_) async => null); + return cubit; + }, + act: (cubit) => cubit.doIntent(GetMyOrdersIntent(page: 1, limit: 10)), + expect: () => [ + isA().having( + (s) => s.ordersResource.status, + 'status', + Status.loading, + ), + isA().having( + (s) => s.ordersResource.status, + 'status', + Status.error, + ), + ], + ); + }); +} diff --git a/test/features/my_orders/presentation/pages/my_orders_page_test.dart b/test/features/my_orders/presentation/pages/my_orders_page_test.dart new file mode 100644 index 0000000..79e6ace --- /dev/null +++ b/test/features/my_orders/presentation/pages/my_orders_page_test.dart @@ -0,0 +1,63 @@ +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:get_it/get_it.dart'; +import 'package:mocktail/mocktail.dart'; +import 'package:network_image_mock/network_image_mock.dart'; +import 'package:shared_preferences/shared_preferences.dart'; +import 'package:bloc_test/bloc_test.dart'; +import 'package:tracking_app/features/my_orders/presentation/manager/my_orders_cubit.dart'; +import 'package:tracking_app/features/my_orders/presentation/manager/my_orders_intent.dart'; +import 'package:tracking_app/features/my_orders/presentation/manager/my_orders_state.dart'; +import 'package:tracking_app/features/my_orders/presentation/pages/my_orders_page.dart'; + +class MockMyOrdersCubit extends MockCubit + implements MyOrdersCubit {} + +void main() { + late MockMyOrdersCubit mockCubit; + late GetIt getIt; + + setUpAll(() async { + TestWidgetsFlutterBinding.ensureInitialized(); + SharedPreferences.setMockInitialValues({}); + await EasyLocalization.ensureInitialized(); + registerFallbackValue(GetMyOrdersIntent(page: 1, limit: 10)); + }); + + setUp(() { + getIt = GetIt.instance; + mockCubit = MockMyOrdersCubit(); + + if (getIt.isRegistered()) { + getIt.unregister(); + } + getIt.registerSingleton(mockCubit); + + when(() => mockCubit.doIntent(any())).thenAnswer((_) async {}); + when(() => mockCubit.state).thenReturn(MyOrdersState()); + }); + + tearDown(() { + getIt.reset(); + }); + + Widget createWidgetUnderTest() { + return EasyLocalization( + supportedLocales: const [Locale('en'), Locale('ar')], + path: 'assets/translations', + fallbackLocale: const Locale('en'), + child: const MaterialApp(home: MyOrdersPage()), + ); + } + + testWidgets('MyOrdersPage renders correctly', (WidgetTester tester) async { + await mockNetworkImagesFor(() async { + await tester.pumpWidget(createWidgetUnderTest()); + await tester.pumpAndSettle(); + + expect(find.text("My orders"), findsOneWidget); + expect(find.text("Recent orders"), findsOneWidget); + }); + }); +} diff --git a/test/features/my_orders/presentation/pages/order_details_page_test.dart b/test/features/my_orders/presentation/pages/order_details_page_test.dart new file mode 100644 index 0000000..dbe4e69 --- /dev/null +++ b/test/features/my_orders/presentation/pages/order_details_page_test.dart @@ -0,0 +1,77 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:network_image_mock/network_image_mock.dart'; +import 'package:tracking_app/features/my_orders/domain/models/order_entity.dart'; +import 'package:tracking_app/features/my_orders/domain/models/order_item_entity.dart'; +import 'package:tracking_app/features/my_orders/domain/models/product_entity.dart'; +import 'package:tracking_app/features/my_orders/domain/models/store_entity.dart'; +import 'package:tracking_app/features/my_orders/domain/models/user_entity.dart'; +import 'package:tracking_app/features/my_orders/presentation/pages/order_details_page.dart'; +import 'package:tracking_app/features/my_orders/presentation/widgets/order_item_tile.dart'; +import 'package:tracking_app/features/my_orders/presentation/widgets/summary_row.dart'; + +void main() { + final tOrder = OrderEntity( + id: "123456", + user: UserEntity( + id: "u1", + firstName: "Noor", + lastName: "mohamed", + phone: "01012345678", + photo: "https://example.com/user.png", + ), + store: StoreEntity( + name: "Flowery store", + image: "https://example.com/store.png", + address: "20th st, Sheikh Zayed, Giza", + phoneNumber: "01012345678", + ), + address: "20th st, Sheikh Zayed, Giza", + items: [ + OrderItemEntity( + product: ProductEntity( + id: "p1", + title: "Red roses", + image: "https://example.com/item.png", + price: 600, + ), + price: 600, + quantity: 1, + ), + ], + totalPrice: 3000, + paymentType: "Cash on delivery", + isPaid: true, + isDelivered: true, + state: "Completed", + createdAt: "2023-01-01", + orderNumber: "123456", + ); + + testWidgets('OrderDetailsPage renders correctly with given order', ( + WidgetTester tester, + ) async { + await mockNetworkImagesFor(() async { + await tester.pumpWidget( + MaterialApp(home: OrderDetailsPage(order: tOrder)), + ); + + expect(find.text("Order details"), findsWidgets); + expect(find.text("Completed"), findsOneWidget); + expect(find.text("# 123456"), findsOneWidget); + + expect(find.text("Pickup address"), findsOneWidget); + expect(find.text("Flowery store"), findsOneWidget); + + expect(find.text("User address"), findsOneWidget); + expect(find.text("Noor mohamed"), findsOneWidget); + + expect(find.byType(OrderItemTile), findsOneWidget); + expect(find.text("Red roses"), findsOneWidget); + + expect(find.byType(SummaryRow), findsNWidgets(2)); + expect(find.text("Egp 3000"), findsOneWidget); + expect(find.text("Cash on delivery"), findsOneWidget); + }); + }); +} diff --git a/test/features/my_orders/presentation/widgets/address_tile_test.dart b/test/features/my_orders/presentation/widgets/address_tile_test.dart new file mode 100644 index 0000000..d6b2994 --- /dev/null +++ b/test/features/my_orders/presentation/widgets/address_tile_test.dart @@ -0,0 +1,38 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:network_image_mock/network_image_mock.dart'; +import 'package:tracking_app/features/my_orders/presentation/widgets/address_title.dart'; + +void main() { + testWidgets('AddressTile renders correctly with given data', ( + WidgetTester tester, + ) async { + const title = 'Store Name'; + const address = '123 Street, City'; + const imageUrl = 'https://example.com/image.png'; + + await mockNetworkImagesFor(() async { + await tester.pumpWidget( + const MaterialApp( + home: Scaffold( + body: AddressTile( + title: title, + address: address, + image: imageUrl, + isStore: true, + ), + ), + ), + ); + + expect(find.text(title), findsOneWidget); + expect(find.text(address), findsOneWidget); + expect( + find.byType(NetworkImage), + findsNothing, + ); // Image is in BoxDecoration, not as a widget + // We can check if the container with decoration exists + expect(find.byType(Container), findsWidgets); + }); + }); +} diff --git a/test/features/my_orders/presentation/widgets/my_orders_page_body_test.dart b/test/features/my_orders/presentation/widgets/my_orders_page_body_test.dart new file mode 100644 index 0000000..cce2f6b --- /dev/null +++ b/test/features/my_orders/presentation/widgets/my_orders_page_body_test.dart @@ -0,0 +1,42 @@ +import 'package:bloc_test/bloc_test.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mocktail/mocktail.dart'; +import 'package:tracking_app/features/my_orders/presentation/manager/my_orders_cubit.dart'; +import 'package:tracking_app/features/my_orders/presentation/manager/my_orders_state.dart'; +import 'package:tracking_app/features/my_orders/presentation/widgets/my_orders_page_body.dart'; +import 'package:tracking_app/features/my_orders/presentation/widgets/orders_filters_row.dart'; +import 'package:tracking_app/features/my_orders/presentation/widgets/orders_list_view.dart'; + +class MockMyOrdersCubit extends MockCubit + implements MyOrdersCubit {} + +void main() { + late MockMyOrdersCubit mockCubit; + + setUp(() { + mockCubit = MockMyOrdersCubit(); + }); + + testWidgets('MyOrdersPageBody renders components correctly', ( + WidgetTester tester, + ) async { + when(() => mockCubit.state).thenReturn(MyOrdersState()); + + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: BlocProvider.value( + value: mockCubit, + child: const MyOrdersPageBody(), + ), + ), + ), + ); + + expect(find.byType(OrdersFiltersRow), findsOneWidget); + expect(find.text("Recent orders"), findsOneWidget); + expect(find.byType(OrdersListView), findsOneWidget); + }); +} diff --git a/test/features/my_orders/presentation/widgets/order_card_test.dart b/test/features/my_orders/presentation/widgets/order_card_test.dart new file mode 100644 index 0000000..68d18b9 --- /dev/null +++ b/test/features/my_orders/presentation/widgets/order_card_test.dart @@ -0,0 +1,61 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:network_image_mock/network_image_mock.dart'; +import 'package:tracking_app/features/my_orders/domain/models/order_entity.dart'; +import 'package:tracking_app/features/my_orders/domain/models/store_entity.dart'; +import 'package:tracking_app/features/my_orders/domain/models/user_entity.dart'; +import 'package:tracking_app/features/my_orders/presentation/widgets/order_card.dart'; + +void main() { + final tOrder = OrderEntity( + id: 'o1', + user: UserEntity( + id: 'u1', + firstName: 'Noor', + lastName: 'Mohamed', + phone: '010', + photo: 'https://example.com/u1.png', + ), + store: StoreEntity( + name: 'Test Store', + image: 'https://example.com/s1.png', + address: 'Store Address', + phoneNumber: '011', + ), + address: 'User Address', + items: [], + totalPrice: 100, + paymentType: 'Cash', + isPaid: true, + isDelivered: true, + state: 'Delivered', + createdAt: '2023-01-01', + orderNumber: 'ORD123', + ); + + testWidgets('OrderCard renders correctly and handles tap', ( + WidgetTester tester, + ) async { + bool tapped = false; + + await mockNetworkImagesFor(() async { + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: OrderCard(order: tOrder, onTap: () => tapped = true), + ), + ), + ); + + expect(find.text('Delivered'), findsOneWidget); + expect(find.text('# ORD123'), findsOneWidget); + expect(find.text('Test Store'), findsOneWidget); + expect(find.text('Store Address'), findsOneWidget); + expect(find.text('Noor Mohamed'), findsOneWidget); + expect(find.text('User Address'), findsOneWidget); + + await tester.tap(find.byType(OrderCard)); + expect(tapped, true); + }); + }); +} diff --git a/test/features/my_orders/presentation/widgets/order_item_tile_test.dart b/test/features/my_orders/presentation/widgets/order_item_tile_test.dart new file mode 100644 index 0000000..b764827 --- /dev/null +++ b/test/features/my_orders/presentation/widgets/order_item_tile_test.dart @@ -0,0 +1,36 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:network_image_mock/network_image_mock.dart'; +import 'package:tracking_app/features/my_orders/domain/models/order_item_entity.dart'; +import 'package:tracking_app/features/my_orders/domain/models/product_entity.dart'; +import 'package:tracking_app/features/my_orders/presentation/widgets/order_item_tile.dart'; + +void main() { + final tOrderItem = OrderItemEntity( + product: ProductEntity( + id: "p1", + title: "Red roses, 15 Pink Rose Bouquet", + image: "https://example.com/image.png", + price: 600, + ), + price: 600, + quantity: 2, + ); + + testWidgets('OrderItemTile renders correctly with given data', ( + WidgetTester tester, + ) async { + await mockNetworkImagesFor(() async { + await tester.pumpWidget( + MaterialApp( + home: Scaffold(body: OrderItemTile(item: tOrderItem)), + ), + ); + + expect(find.text("Red roses, 15 Pink Rose Bouquet"), findsOneWidget); + expect(find.text("EGP 600"), findsOneWidget); + expect(find.text("X2"), findsOneWidget); + expect(find.byType(Container), findsWidgets); + }); + }); +} diff --git a/test/features/my_orders/presentation/widgets/orders_filters_row_test.dart b/test/features/my_orders/presentation/widgets/orders_filters_row_test.dart new file mode 100644 index 0000000..4d285a2 --- /dev/null +++ b/test/features/my_orders/presentation/widgets/orders_filters_row_test.dart @@ -0,0 +1,95 @@ +import 'package:bloc_test/bloc_test.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mocktail/mocktail.dart'; +import 'package:shared_preferences/shared_preferences.dart'; +import 'package:tracking_app/features/my_orders/domain/models/meta_data_entity.dart'; +import 'package:tracking_app/features/my_orders/presentation/manager/my_orders_cubit.dart'; +import 'package:tracking_app/features/my_orders/presentation/manager/my_orders_intent.dart'; +import 'package:tracking_app/features/my_orders/presentation/manager/my_orders_state.dart'; +import 'package:tracking_app/features/my_orders/presentation/widgets/orders_filters_row.dart'; + +class MockMyOrdersCubit extends MockCubit + implements MyOrdersCubit {} + +void main() { + late MockMyOrdersCubit mockCubit; + + setUpAll(() async { + TestWidgetsFlutterBinding.ensureInitialized(); + SharedPreferences.setMockInitialValues({}); + await EasyLocalization.ensureInitialized(); + registerFallbackValue(FilterCancelledOrdersIntent()); + registerFallbackValue(FilterCompletedOrdersIntent()); + }); + + setUp(() { + mockCubit = MockMyOrdersCubit(); + when(() => mockCubit.doIntent(any())).thenAnswer((_) async {}); + }); + + Widget createWidgetUnderTest() { + return EasyLocalization( + supportedLocales: const [Locale('en'), Locale('ar')], + path: 'assets/translations', + fallbackLocale: const Locale('en'), + child: MaterialApp( + home: Scaffold( + body: BlocProvider.value( + value: mockCubit, + child: const OrdersFiltersRow(), + ), + ), + ), + ); + } + + testWidgets('OrdersFiltersRow renders correct counts from metadata', ( + WidgetTester tester, + ) async { + final state = MyOrdersState( + metadata: const MetadataEntity( + currentPage: 1, + totalPages: 1, + totalItems: 10, + limit: 10, + cancelledCount: 3, + completedCount: 7, + ), + ); + + when(() => mockCubit.state).thenReturn(state); + + await tester.pumpWidget(createWidgetUnderTest()); + await tester.pumpAndSettle(); + + expect(find.text('3'), findsOneWidget); + expect(find.text('7'), findsOneWidget); + expect(find.text('Cancelled'), findsOneWidget); + expect(find.text('Completed'), findsOneWidget); + }); + + testWidgets('OrdersFiltersRow triggers intents on tap', ( + WidgetTester tester, + ) async { + final state = MyOrdersState(); + when(() => mockCubit.state).thenReturn(state); + + await tester.pumpWidget(createWidgetUnderTest()); + await tester.pumpAndSettle(); + + await tester.tap(find.text('Cancelled')); + await tester.pump(); + verify( + () => mockCubit.doIntent(any(that: isA())), + ).called(1); + + await tester.tap(find.text('Completed')); + await tester.pump(); + verify( + () => mockCubit.doIntent(any(that: isA())), + ).called(1); + }); +} diff --git a/test/features/my_orders/presentation/widgets/orders_list_view_test.dart b/test/features/my_orders/presentation/widgets/orders_list_view_test.dart new file mode 100644 index 0000000..d0b47ec --- /dev/null +++ b/test/features/my_orders/presentation/widgets/orders_list_view_test.dart @@ -0,0 +1,107 @@ +import 'package:bloc_test/bloc_test.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mocktail/mocktail.dart'; +import 'package:network_image_mock/network_image_mock.dart'; +import 'package:tracking_app/app/config/base_state/base_state.dart'; +import 'package:tracking_app/features/my_orders/domain/models/order_entity.dart'; +import 'package:tracking_app/features/my_orders/domain/models/user_entity.dart'; +import 'package:tracking_app/features/my_orders/presentation/manager/my_orders_cubit.dart'; +import 'package:tracking_app/features/my_orders/presentation/manager/my_orders_state.dart'; +import 'package:tracking_app/features/my_orders/presentation/widgets/orders_list_view.dart'; +import 'package:tracking_app/features/my_orders/presentation/widgets/order_card.dart'; + +class MockMyOrdersCubit extends MockCubit + implements MyOrdersCubit {} + +void main() { + late MockMyOrdersCubit mockCubit; + + setUp(() { + mockCubit = MockMyOrdersCubit(); + }); + + testWidgets('OrdersListView shows loading indicator when loading', ( + WidgetTester tester, + ) async { + when( + () => mockCubit.state, + ).thenReturn(MyOrdersState(ordersResource: Resource.loading())); + + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: BlocProvider.value( + value: mockCubit, + child: const OrdersListView(), + ), + ), + ), + ); + + expect(find.byType(CircularProgressIndicator), findsOneWidget); + }); + + testWidgets('OrdersListView shows empty message when no orders', ( + WidgetTester tester, + ) async { + when(() => mockCubit.state).thenReturn( + MyOrdersState(ordersResource: Resource.success(null), orders: []), + ); + + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: BlocProvider.value( + value: mockCubit, + child: const OrdersListView(), + ), + ), + ), + ); + + expect(find.text("No orders found"), findsOneWidget); + }); + + testWidgets('OrdersListView renders list of orders', ( + WidgetTester tester, + ) async { + final tOrder = OrderEntity( + id: 'o1', + user: UserEntity( + id: 'u1', + firstName: 'Noor', + lastName: 'Mohamed', + phone: '01', + photo: 'https://img.com', + ), + items: [], + totalPrice: 100, + paymentType: 'Cash', + isPaid: true, + isDelivered: true, + state: 'Delivered', + createdAt: '2023', + orderNumber: '1', + ); + + when(() => mockCubit.state).thenReturn(MyOrdersState(orders: [tOrder])); + + await mockNetworkImagesFor(() async { + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: BlocProvider.value( + value: mockCubit, + child: const OrdersListView(), + ), + ), + ), + ); + + expect(find.byType(OrderCard), findsOneWidget); + expect(find.text('# 1'), findsOneWidget); + }); + }); +} diff --git a/test/features/my_orders/presentation/widgets/section_label_test.dart b/test/features/my_orders/presentation/widgets/section_label_test.dart new file mode 100644 index 0000000..60ff92f --- /dev/null +++ b/test/features/my_orders/presentation/widgets/section_label_test.dart @@ -0,0 +1,18 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:tracking_app/features/my_orders/presentation/widgets/section_lable.dart'; + +void main() { + testWidgets('SectionLabel renders correctly with given text', ( + WidgetTester tester, + ) async { + const testLabel = 'Test Label'; + await tester.pumpWidget( + const MaterialApp( + home: Scaffold(body: SectionLabel(label: testLabel)), + ), + ); + + expect(find.text(testLabel), findsOneWidget); + }); +} diff --git a/test/features/my_orders/presentation/widgets/summary_card_test.dart b/test/features/my_orders/presentation/widgets/summary_card_test.dart new file mode 100644 index 0000000..7c3baa8 --- /dev/null +++ b/test/features/my_orders/presentation/widgets/summary_card_test.dart @@ -0,0 +1,35 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:tracking_app/features/my_orders/presentation/widgets/summary_card.dart'; + +void main() { + testWidgets('SummaryCard renders correctly and handles tap', ( + WidgetTester tester, + ) async { + bool tapped = false; + const title = 'Cancelled'; + const count = '5'; + const icon = Icons.cancel; + + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: SummaryCard( + title: title, + count: count, + icon: icon, + color: Colors.red, + onTap: () => tapped = true, + ), + ), + ), + ); + + expect(find.text(title), findsOneWidget); + expect(find.text(count), findsOneWidget); + expect(find.byIcon(icon), findsOneWidget); + + await tester.tap(find.byType(SummaryCard)); + expect(tapped, true); + }); +} diff --git a/test/features/my_orders/presentation/widgets/summary_row_test.dart b/test/features/my_orders/presentation/widgets/summary_row_test.dart new file mode 100644 index 0000000..41c3d53 --- /dev/null +++ b/test/features/my_orders/presentation/widgets/summary_row_test.dart @@ -0,0 +1,24 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:tracking_app/features/my_orders/presentation/widgets/summary_row.dart'; + +void main() { + testWidgets('SummaryRow renders correctly with given label and value', ( + WidgetTester tester, + ) async { + const label = 'Total'; + const value = 'Egp 3000'; + + await tester.pumpWidget( + const MaterialApp( + home: Scaffold( + body: SummaryRow(label: label, value: value), + ), + ), + ); + + expect(find.text(label), findsOneWidget); + expect(find.text(value), findsOneWidget); + expect(find.byType(Container), findsOneWidget); + }); +} diff --git a/test/features/profile/api/profile_remote_datasource_imp_test.dart b/test/features/profile/api/profile_remote_datasource_imp_test.dart new file mode 100644 index 0000000..aeddc5d --- /dev/null +++ b/test/features/profile/api/profile_remote_datasource_imp_test.dart @@ -0,0 +1,152 @@ +import 'dart:io'; +import 'package:dio/dio.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mockito/annotations.dart'; +import 'package:mockito/mockito.dart'; +import 'package:retrofit/retrofit.dart'; +import 'package:tracking_app/app/core/api_manger/api_client.dart'; +import 'package:tracking_app/app/core/network/api_result.dart'; +import 'package:tracking_app/features/profile/api/profile_remote_datasource_imp.dart'; +import 'package:tracking_app/features/profile/data/models/requests/edit_profile_request.dart'; +import 'package:tracking_app/features/profile/data/models/responses/edit_profile_response.dart'; + +import 'profile_remote_datasource_imp_test.mocks.dart'; + +@GenerateMocks([ApiClient]) +void main() { + late MockApiClient mockApiClient; + late ProfileRemoteDatasourceImp dataSource; + + setUp(() { + mockApiClient = MockApiClient(); + dataSource = ProfileRemoteDatasourceImp(mockApiClient); + }); + + group('ProfileRemoteDatasourceImp.editProfile()', () { + final token = "test_token"; + final request = EditProfileRequest(firstName: "Test"); + + test( + 'returns SuccessApiResult when apiClient returns valid response', + () async { + // ARRANGE + final fakeResponse = EditProfileResponse(message: "Success"); + final dioResponse = Response( + requestOptions: RequestOptions(path: '/edit-profile'), + data: fakeResponse, + statusCode: 200, + ); + final httpResponse = HttpResponse( + fakeResponse, + dioResponse, + ); + + when( + mockApiClient.editProfile( + token: anyNamed('token'), + request: anyNamed('request'), + ), + ).thenAnswer((_) async => httpResponse); + + // ACT + final result = await dataSource.editProfile( + token: token, + request: request, + ); + + // ASSERT + expect(result, isA>()); + final data = (result as SuccessApiResult).data; + expect(data.message, "Success"); + verify( + mockApiClient.editProfile(token: token, request: request), + ).called(1); + }, + ); + + test('returns ErrorApiResult when apiClient throws Exception', () async { + // ARRANGE + when( + mockApiClient.editProfile( + token: anyNamed('token'), + request: anyNamed('request'), + ), + ).thenThrow(Exception("network error")); + + // ACT + final result = await dataSource.editProfile( + token: token, + request: request, + ); + + // ASSERT + expect(result, isA>()); + expect( + (result as ErrorApiResult).error.toString(), + contains("network error"), + ); + verify( + mockApiClient.editProfile(token: token, request: request), + ).called(1); + }); + }); + + group('ProfileRemoteDatasourceImp.uploadPhoto()', () { + final token = "test_token"; + final file = File('test_path'); + + test( + 'returns SuccessApiResult when apiClient returns valid response', + () async { + // ARRANGE + final fakeResponse = EditProfileResponse(message: "Photo Uploaded"); + final dioResponse = Response( + requestOptions: RequestOptions(path: '/upload-photo'), + data: fakeResponse, + statusCode: 200, + ); + final httpResponse = HttpResponse( + fakeResponse, + dioResponse, + ); + + when( + mockApiClient.uploadPhoto( + token: anyNamed('token'), + photo: anyNamed('photo'), + ), + ).thenAnswer((_) async => httpResponse); + + // ACT + final result = await dataSource.uploadPhoto(token: token, photo: file); + + // ASSERT + expect(result, isA>()); + final data = (result as SuccessApiResult).data; + expect(data.message, "Photo Uploaded"); + verify(mockApiClient.uploadPhoto(token: token, photo: file)).called(1); + }, + ); + + test('returns ErrorApiResult when apiClient throws Exception', () async { + // ARRANGE + when( + mockApiClient.uploadPhoto( + token: anyNamed('token'), + photo: anyNamed('photo'), + ), + ).thenThrow(Exception("network error")); + + // ACT + final result = await dataSource.uploadPhoto(token: token, photo: file); + + // ASSERT + expect(result, isA>()); + expect( + (result as ErrorApiResult).error.toString(), + contains("network error"), + ); + verify(mockApiClient.uploadPhoto(token: token, photo: file)).called(1); + }); + }); +} diff --git a/test/features/profile/data/repo/profile_repo_imp_test.dart b/test/features/profile/data/repo/profile_repo_imp_test.dart new file mode 100644 index 0000000..e50f219 --- /dev/null +++ b/test/features/profile/data/repo/profile_repo_imp_test.dart @@ -0,0 +1,141 @@ +import 'dart:io'; + +import 'package:flutter_test/flutter_test.dart'; +import 'package:mockito/annotations.dart'; +import 'package:mockito/mockito.dart'; +import 'package:tracking_app/app/core/network/api_result.dart'; +import 'package:tracking_app/features/profile/data/datasorce/profile_lacal_datasource.dart'; +import 'package:tracking_app/features/profile/data/datasorce/profile_remote_datasource.dart'; +import 'package:tracking_app/features/profile/data/models/responses/edit_profile_response.dart'; +import 'package:tracking_app/features/profile/data/repo/profile_repo_imp.dart'; + +import 'profile_repo_imp_test.mocks.dart'; + +@GenerateMocks([ProfileRemoteDatasource, ProfileLocalDataSource]) +void main() { + provideDummy>( + SuccessApiResult(data: EditProfileResponse()), + ); + provideDummy>( + ErrorApiResult(error: 'dummy error'), + ); + provideDummy>( + SuccessApiResult(data: EditProfileResponse()), + ); + provideDummy(File('dummy_path')); + + late MockProfileRemoteDatasource mockRemote; + late MockProfileLocalDataSource mockLocal; + late ProfileRepoImpl repo; + + setUp(() { + mockRemote = MockProfileRemoteDatasource(); + mockLocal = MockProfileLocalDataSource(); + repo = ProfileRepoImpl(mockRemote, mockLocal); + }); + + group('ProfileRepoImpl.editProfile()', () { + final token = "test_token"; + final firstName = "Test"; + final lastName = "User"; + + test( + 'returns SuccessApiResult when datasource returns SuccessApiResult', + () async { + final fakeResponse = EditProfileResponse(message: "Success"); + when( + mockRemote.editProfile( + token: anyNamed('token'), + request: anyNamed('request'), + ), + ).thenAnswer((_) async => SuccessApiResult(data: fakeResponse)); + + final result = await repo.editProfile( + token: token, + firstName: firstName, + lastName: lastName, + ); + + expect(result, isA>()); + final data = (result as SuccessApiResult).data; + expect(data.message, "Success"); + + verify( + mockRemote.editProfile(token: token, request: anyNamed('request')), + ).called(1); + verify(mockLocal.saveUser(any)).called(1); + }, + ); + + test( + 'returns ErrorApiResult when datasource returns ErrorApiResult', + () async { + when( + mockRemote.editProfile( + token: anyNamed('token'), + request: anyNamed('request'), + ), + ).thenAnswer((_) async => ErrorApiResult(error: "Network Error")); + + final result = await repo.editProfile( + token: token, + firstName: firstName, + lastName: lastName, + ); + + expect(result, isA>()); + expect((result as ErrorApiResult).error, "Network Error"); + + verify( + mockRemote.editProfile(token: token, request: anyNamed('request')), + ).called(1); + }, + ); + }); + + group('ProfileRepoImpl.uploadPhoto()', () { + final token = "test_token"; + final file = File('test_path'); + + test( + 'returns SuccessApiResult when datasource returns SuccessApiResult', + () async { + final fakeResponse = EditProfileResponse(message: "Photo Uploaded"); + when( + mockRemote.uploadPhoto( + token: anyNamed('token'), + photo: anyNamed('photo'), + ), + ).thenAnswer((_) async => SuccessApiResult(data: fakeResponse)); + + final result = await repo.uploadPhoto(token: token, photo: file); + + expect(result, isA>()); + final data = (result as SuccessApiResult).data; + expect(data.message, "Photo Uploaded"); + + verify(mockRemote.uploadPhoto(token: token, photo: file)).called(1); + verify(mockLocal.saveUser(any)).called(1); + }, + ); + + test( + 'returns ErrorApiResult when datasource returns ErrorApiResult', + () async { + when( + mockRemote.uploadPhoto( + token: anyNamed('token'), + photo: anyNamed('photo'), + ), + ).thenAnswer((_) async => ErrorApiResult(error: "Upload Failed")); + + final result = await repo.uploadPhoto(token: token, photo: file); + + expect(result, isA>()); + expect((result as ErrorApiResult).error, "Upload Failed"); + + verify(mockRemote.uploadPhoto(token: token, photo: file)).called(1); + }, + ); + }); +} diff --git a/test/features/profile/domain/usecases/edit_profile_usecase_test.dart b/test/features/profile/domain/usecases/edit_profile_usecase_test.dart new file mode 100644 index 0000000..8cc60b5 --- /dev/null +++ b/test/features/profile/domain/usecases/edit_profile_usecase_test.dart @@ -0,0 +1,113 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:mockito/annotations.dart'; +import 'package:mockito/mockito.dart'; +import 'package:tracking_app/app/core/network/api_result.dart'; +import 'package:tracking_app/features/profile/data/models/driver_model.dart'; +import 'package:tracking_app/features/profile/data/models/responses/edit_profile_response.dart'; +import 'package:tracking_app/features/profile/domain/repo/profile_repo.dart'; +import 'package:tracking_app/features/profile/domain/usecases/edit_profile_usecase.dart'; + +import 'edit_profile_usecase_test.mocks.dart'; + +@GenerateMocks([ProfileRepo]) +void main() { + late MockProfileRepo mockRepo; + late EditProfileUseCase useCase; + + setUp(() { + mockRepo = MockProfileRepo(); + useCase = EditProfileUseCase(mockRepo); + provideDummy>( + SuccessApiResult(data: EditProfileResponse()), + ); + }); + + group("EditProfileUseCase", () { + final fakeResponse = EditProfileResponse( + message: 'Success', + driver: DriverModel( + firstName: 'test', + lastName: 'test', + email: 'test@test.com', + ), + ); + + test("returns SuccessApiResult when repo returns success", () async { + when( + mockRepo.editProfile( + token: anyNamed('token'), + firstName: anyNamed('firstName'), + lastName: anyNamed('lastName'), + email: anyNamed('email'), + phone: anyNamed('phone'), + vehicleType: anyNamed('vehicleType'), + vehicleNumber: anyNamed('vehicleNumber'), + vehicleLicense: anyNamed('vehicleLicense'), + ), + ).thenAnswer( + (_) async => SuccessApiResult(data: fakeResponse), + ); + + final result = + await useCase.call( + token: 'fake_token', + firstName: 'test', + lastName: 'test', + email: 'test@test.com', + ) + as SuccessApiResult; + + expect(result, isA>()); + final data = (result as SuccessApiResult).data; + expect(data.message, fakeResponse.message); + expect(data.driver?.email, fakeResponse.driver?.email); + verify( + mockRepo.editProfile( + token: 'fake_token', + firstName: 'test', + lastName: 'test', + email: 'test@test.com', + ), + ).called(1); + }); + + test("returns ErrorApiResult when repo returns error", () async { + when( + mockRepo.editProfile( + token: anyNamed('token'), + firstName: anyNamed('firstName'), + lastName: anyNamed('lastName'), + email: anyNamed('email'), + phone: anyNamed('phone'), + vehicleType: anyNamed('vehicleType'), + vehicleNumber: anyNamed('vehicleNumber'), + vehicleLicense: anyNamed('vehicleLicense'), + ), + ).thenAnswer( + (_) async => + ErrorApiResult(error: 'Update failed'), + ); + + final result = + await useCase.call( + token: 'fake_token', + firstName: 'test', + lastName: 'test', + email: 'test@test.com', + ) + as ErrorApiResult; + + expect(result, isA>()); + final error = (result as ErrorApiResult).error; + expect(error, 'Update failed'); + verify( + mockRepo.editProfile( + token: 'fake_token', + firstName: 'test', + lastName: 'test', + email: 'test@test.com', + ), + ).called(1); + }); + }); +} diff --git a/test/features/profile/domain/usecases/upload_profile_photo_usecase_test.dart b/test/features/profile/domain/usecases/upload_profile_photo_usecase_test.dart new file mode 100644 index 0000000..a91a4ef --- /dev/null +++ b/test/features/profile/domain/usecases/upload_profile_photo_usecase_test.dart @@ -0,0 +1,78 @@ +import 'dart:io'; + +import 'package:flutter_test/flutter_test.dart'; +import 'package:mockito/annotations.dart'; +import 'package:mockito/mockito.dart'; +import 'package:tracking_app/app/core/network/api_result.dart'; +import 'package:tracking_app/features/profile/data/models/driver_model.dart'; +import 'package:tracking_app/features/profile/data/models/responses/edit_profile_response.dart'; +import 'package:tracking_app/features/profile/domain/repo/profile_repo.dart'; +import 'package:tracking_app/features/profile/domain/usecases/upload_profile_photo_usecase.dart'; + +import 'upload_profile_photo_usecase_test.mocks.dart'; + +@GenerateMocks([ProfileRepo]) +void main() { + late MockProfileRepo mockRepo; + late UploadProfilePhotoUseCase useCase; + + setUp(() { + mockRepo = MockProfileRepo(); + useCase = UploadProfilePhotoUseCase(mockRepo); + provideDummy>( + SuccessApiResult(data: EditProfileResponse()), + ); + }); + + group("UploadProfilePhotoUseCase", () { + final token = "test_token"; + final file = File('test_path'); + final fakeResponse = EditProfileResponse( + message: 'Photo Uploaded', + driver: DriverModel( + firstName: 'test', + lastName: 'test', + email: 'test@test.com', + photo: 'uploaded_photo.jpg', + ), + ); + + test("returns SuccessApiResult when repo returns success", () async { + when( + mockRepo.uploadPhoto( + token: anyNamed('token'), + photo: anyNamed('photo'), + ), + ).thenAnswer( + (_) async => SuccessApiResult(data: fakeResponse), + ); + + final result = await useCase.call(token: token, photo: file); + + expect(result, isA>()); + final data = (result as SuccessApiResult).data; + expect(data.message, fakeResponse.message); + expect(data.driver?.photo, fakeResponse.driver?.photo); + verify(mockRepo.uploadPhoto(token: token, photo: file)).called(1); + }); + + test("returns ErrorApiResult when repo returns error", () async { + when( + mockRepo.uploadPhoto( + token: anyNamed('token'), + photo: anyNamed('photo'), + ), + ).thenAnswer( + (_) async => + ErrorApiResult(error: 'Upload failed'), + ); + + final result = await useCase.call(token: token, photo: file); + + expect(result, isA>()); + final error = (result as ErrorApiResult).error; + expect(error, 'Upload failed'); + verify(mockRepo.uploadPhoto(token: token, photo: file)).called(1); + }); + }); +} diff --git a/test/features/profile/presentation/managers/profile_cubit_test.dart b/test/features/profile/presentation/managers/profile_cubit_test.dart new file mode 100644 index 0000000..b9babaf --- /dev/null +++ b/test/features/profile/presentation/managers/profile_cubit_test.dart @@ -0,0 +1,288 @@ +import 'dart:io'; +import 'package:bloc_test/bloc_test.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mockito/annotations.dart'; +import 'package:mockito/mockito.dart'; +import 'package:tracking_app/app/config/auth_storage/auth_storage.dart'; +import 'package:tracking_app/app/core/network/api_result.dart'; +import 'package:tracking_app/app/config/base_state/base_state.dart'; +import 'package:tracking_app/features/profile/data/models/driver_model.dart'; +import 'package:tracking_app/features/profile/data/models/responses/edit_profile_response.dart'; +import 'package:tracking_app/features/profile/domain/usecases/edit_profile_usecase.dart'; +import 'package:tracking_app/features/profile/domain/usecases/upload_profile_photo_usecase.dart'; +import 'package:tracking_app/features/profile/domain/usecases/get_profile_usecase.dart'; +import 'package:tracking_app/features/profile/presentation/managers/profile_cubit.dart'; +import 'package:tracking_app/features/profile/presentation/managers/profile_intent.dart'; +import 'package:tracking_app/features/profile/presentation/managers/profile_state.dart'; + +import 'profile_cubit_test.mocks.dart'; + +@GenerateMocks([ + EditProfileUseCase, + UploadProfilePhotoUseCase, + GetProfileUsecase, + AuthStorage, +]) +void main() { + provideDummy>( + SuccessApiResult(data: EditProfileResponse()), + ); + + provideDummy>( + ErrorApiResult(error: 'dummy error'), + ); + + provideDummy>( + SuccessApiResult(data: EditProfileResponse()), + ); + + late MockEditProfileUseCase mockEditProfileUseCase; + late MockUploadProfilePhotoUseCase mockUploadPhotoUseCase; + late MockGetProfileUsecase mockGetProfileUsecase; + late MockAuthStorage mockAuthStorage; + late ProfileCubit cubit; + + setUp(() { + mockEditProfileUseCase = MockEditProfileUseCase(); + mockUploadPhotoUseCase = MockUploadProfilePhotoUseCase(); + mockGetProfileUsecase = MockGetProfileUsecase(); + mockAuthStorage = MockAuthStorage(); + when(mockAuthStorage.getUserJson()).thenAnswer((_) async => null); + when(mockAuthStorage.getToken()).thenAnswer((_) async => 'test_token'); + when( + mockGetProfileUsecase.call(token: anyNamed('token')), + ).thenAnswer((_) async => SuccessApiResult(data: EditProfileResponse())); + + cubit = ProfileCubit( + mockEditProfileUseCase, + mockUploadPhotoUseCase, + mockGetProfileUsecase, + mockAuthStorage, + ); + }); + + tearDown(() { + cubit.close(); + }); + + group('GetProfileIntent', () { + final token = 'test_token'; + final response = EditProfileResponse( + message: 'Success', + driver: DriverModel(firstName: 'Ali', lastName: 'Besar'), + ); + + blocTest( + 'emits loading then success when usecase returns SuccessApiResult', + build: () { + when(mockAuthStorage.getToken()).thenAnswer((_) async => token); + when( + mockGetProfileUsecase.call(token: 'Bearer $token'), + ).thenAnswer((_) async => SuccessApiResult(data: response)); + when(mockAuthStorage.saveUserJson(any)).thenAnswer((_) async => {}); + return cubit; + }, + act: (cubit) => cubit.doIntent(GetProfileIntent()), + expect: () => [ + isA().having( + (s) => s.getProfileResource.status, + 'status', + Status.loading, + ), + isA() + .having( + (s) => s.getProfileResource.status, + 'status', + Status.success, + ) + .having((s) => s.driver?.firstName, 'firstName', 'Ali'), + ], + ); + + blocTest( + 'emits error when token is missing', + build: () { + when(mockAuthStorage.getToken()).thenAnswer((_) async => null); + return cubit; + }, + act: (cubit) => cubit.doIntent(GetProfileIntent()), + expect: () => [ + isA().having( + (s) => s.getProfileResource.status, + 'status', + Status.loading, + ), + isA().having( + (s) => s.getProfileResource.error, + 'error', + 'Token not found', + ), + ], + ); + }); + + group('PerformEditProfile Intent', () { + final intent = PerformEditProfile( + firstName: 'Test', + lastName: 'User', + email: 'test@example.com', + ); + final token = 'test_token'; + final response = EditProfileResponse( + message: 'Success', + driver: DriverModel(firstName: 'Test', lastName: 'User'), + ); + + blocTest( + 'emits loading then success when usecase returns SuccessApiResult', + build: () { + when(mockAuthStorage.getToken()).thenAnswer((_) async => token); + when( + mockEditProfileUseCase.call( + token: 'Bearer $token', + firstName: intent.firstName, + lastName: intent.lastName, + email: intent.email, + phone: intent.phone, + vehicleType: intent.vehicleType, + vehicleNumber: intent.vehicleNumber, + vehicleLicense: intent.vehicleLicense, + ), + ).thenAnswer((_) async => SuccessApiResult(data: response)); + when(mockAuthStorage.saveUserJson(any)).thenAnswer((_) async => {}); + return cubit; + }, + act: (cubit) => cubit.doIntent(intent), + expect: () => [ + isA().having( + (s) => s.editProfileResource.status, + 'status', + Status.loading, + ), + isA() + .having( + (s) => s.editProfileResource.status, + 'status', + Status.success, + ) + .having((s) => s.editProfileResource.data, 'data', response), + ], + verify: (_) { + verify(mockAuthStorage.getToken()).called(2); + verify( + mockEditProfileUseCase.call( + token: 'Bearer $token', + firstName: intent.firstName, + lastName: intent.lastName, + email: intent.email, + phone: intent.phone, + vehicleType: intent.vehicleType, + vehicleNumber: intent.vehicleNumber, + vehicleLicense: intent.vehicleLicense, + ), + ).called(1); + verify(mockAuthStorage.saveUserJson(any)).called(1); + }, + ); + + blocTest( + 'emits loading then error when usecase returns ErrorApiResult', + build: () { + when(mockAuthStorage.getToken()).thenAnswer((_) async => token); + when( + mockEditProfileUseCase.call( + token: 'Bearer $token', + firstName: intent.firstName, + lastName: intent.lastName, + email: intent.email, + phone: intent.phone, + vehicleType: intent.vehicleType, + vehicleNumber: intent.vehicleNumber, + vehicleLicense: intent.vehicleLicense, + ), + ).thenAnswer((_) async => ErrorApiResult(error: 'Update failed')); + return cubit; + }, + act: (cubit) => cubit.doIntent(intent), + expect: () => [ + isA().having( + (s) => s.editProfileResource.status, + 'status', + Status.loading, + ), + isA() + .having((s) => s.editProfileResource.status, 'status', Status.error) + .having( + (s) => s.editProfileResource.error, + 'error', + 'Update failed', + ), + ], + ); + + blocTest( + 'uploads photo then edits profile when photo is present', + build: () { + when(mockAuthStorage.getToken()).thenAnswer((_) async => token); + when( + mockUploadPhotoUseCase.call( + token: 'Bearer $token', + photo: anyNamed('photo'), + ), + ).thenAnswer((_) async => SuccessApiResult(data: response)); + + when( + mockEditProfileUseCase.call( + token: 'Bearer $token', + firstName: 'Test', + lastName: null, + email: null, + phone: null, + vehicleType: null, + vehicleNumber: null, + vehicleLicense: null, + ), + ).thenAnswer((_) async => SuccessApiResult(data: response)); + when(mockAuthStorage.saveUserJson(any)).thenAnswer((_) async => {}); + return cubit; + }, + act: (cubit) => cubit.doIntent( + PerformEditProfile(firstName: 'Test', photo: File('test_photo')), + ), + expect: () => [ + isA().having( + (s) => s.editProfileResource.status, + 'status', + Status.loading, + ), + isA() + .having( + (s) => s.editProfileResource.status, + 'status', + Status.success, + ) + .having((s) => s.selectedPhoto, 'selectedPhoto', isNull), + ], + verify: (_) { + verify( + mockUploadPhotoUseCase.call( + token: 'Bearer $token', + photo: anyNamed('photo'), + ), + ).called(1); + verify( + mockEditProfileUseCase.call( + token: 'Bearer $token', + firstName: 'Test', + lastName: null, + email: null, + phone: null, + vehicleType: null, + vehicleNumber: null, + vehicleLicense: null, + ), + ).called(1); + }, + ); + }); +} diff --git a/test/features/profile/presentation/widgets/edit_driver_profile_page_body_test.dart b/test/features/profile/presentation/widgets/edit_driver_profile_page_body_test.dart new file mode 100644 index 0000000..036c29a --- /dev/null +++ b/test/features/profile/presentation/widgets/edit_driver_profile_page_body_test.dart @@ -0,0 +1,123 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mockito/annotations.dart'; +import 'package:mockito/mockito.dart'; +import 'package:tracking_app/app/config/auth_storage/auth_storage.dart'; +import 'package:tracking_app/app/config/base_state/base_state.dart'; +import 'package:tracking_app/app/config/di/di.dart'; +import 'package:tracking_app/features/profile/data/models/driver_model.dart'; +import 'package:tracking_app/features/profile/presentation/managers/profile_cubit.dart'; +import 'package:tracking_app/features/profile/presentation/managers/profile_intent.dart'; +import 'package:tracking_app/features/profile/presentation/managers/profile_state.dart'; +import 'package:tracking_app/features/profile/presentation/widgets/edit_driver_profile_page_body.dart'; + +@GenerateMocks([ProfileCubit, AuthStorage]) +import 'edit_driver_profile_page_body_test.mocks.dart'; + +void main() { + group('EditDriverProfilePageBody Tests', () { + late MockProfileCubit mockCubit; + late MockAuthStorage mockAuthStorage; + + final fakeUser = DriverModel( + firstName: 'Ali', + lastName: 'Besar', + email: 'ali@example.com', + phone: '0123456789', + ); + + setUp(() { + mockCubit = MockProfileCubit(); + mockAuthStorage = MockAuthStorage(); + + if (!getIt.isRegistered()) { + getIt.registerSingleton(mockAuthStorage); + } + }); + + tearDown(() { + if (getIt.isRegistered()) { + getIt.unregister(); + } + }); + + Widget createWidgetUnderTest() { + return MaterialApp( + home: BlocProvider.value( + value: mockCubit, + child: Scaffold(body: EditDriverProfilePageBody(user: fakeUser)), + ), + ); + } + + testWidgets('initializes form fields with user data', (tester) async { + when(mockCubit.state).thenReturn(ProfileState()); + when(mockCubit.stream).thenAnswer((_) => const Stream.empty()); + + await tester.pumpWidget(createWidgetUnderTest()); + + expect(find.text('Ali'), findsOneWidget); + expect(find.text('Besar'), findsOneWidget); + expect(find.text('ali@example.com'), findsOneWidget); + expect(find.text('0123456789'), findsOneWidget); + }); + + testWidgets( + 'shows loading indicator on update button when state is loading', + (tester) async { + when( + mockCubit.state, + ).thenReturn(ProfileState(editProfileResource: Resource.loading())); + when(mockCubit.stream).thenAnswer((_) => const Stream.empty()); + + await tester.pumpWidget(createWidgetUnderTest()); + + expect(find.text('loading'), findsOneWidget); + }, + ); + + testWidgets('shows success snackbar when profile update is successful', ( + tester, + ) async { + final state1 = ProfileState(); + final state2 = ProfileState(editProfileResource: Resource.success(null)); + + when(mockCubit.state).thenReturn(state1); + when(mockCubit.stream).thenAnswer((_) => Stream.fromIterable([state2])); + + await tester.pumpWidget(createWidgetUnderTest()); + + await tester.pump(); + await tester.pump(const Duration(milliseconds: 100)); + + expect(find.text('Profile updated successfully'), findsOneWidget); + }); + + testWidgets( + 'calls PerformEditProfile intent when update button is pressed', + (tester) async { + when(mockCubit.state).thenReturn(ProfileState()); + when(mockCubit.stream).thenAnswer((_) => const Stream.empty()); + when(mockAuthStorage.getToken()).thenAnswer((_) async => 'test_token'); + + await tester.pumpWidget(createWidgetUnderTest()); + + await tester.tap(find.byType(ElevatedButton)); + await tester.pump(); + + verify( + mockCubit.doIntent( + argThat( + isA() + .having((i) => i.firstName, 'firstName', 'Ali') + .having((i) => i.lastName, 'lastName', 'Besar') + .having((i) => i.email, 'email', 'ali@example.com') + .having((i) => i.phone, 'phone', '0123456789'), + ), + ), + ).called(1); + }, + ); + }); +} diff --git a/test/features/profile/presentation/widgets/edit_vehicle_page_body_test.dart b/test/features/profile/presentation/widgets/edit_vehicle_page_body_test.dart new file mode 100644 index 0000000..3758dc6 --- /dev/null +++ b/test/features/profile/presentation/widgets/edit_vehicle_page_body_test.dart @@ -0,0 +1,111 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mockito/annotations.dart'; +import 'package:mockito/mockito.dart'; +import 'package:tracking_app/app/config/base_state/base_state.dart'; +import 'package:tracking_app/features/profile/data/models/driver_model.dart'; +import 'package:tracking_app/features/profile/presentation/managers/profile_cubit.dart'; +import 'package:tracking_app/features/profile/presentation/managers/profile_intent.dart'; +import 'package:tracking_app/features/profile/presentation/managers/profile_state.dart'; +import 'package:tracking_app/features/profile/presentation/widgets/edit_vehicle_page_body.dart'; + +@GenerateMocks([ProfileCubit]) +import 'edit_vehicle_page_body_test.mocks.dart'; + +void main() { + group('EditVehiclePageBody Tests', () { + late MockProfileCubit mockCubit; + + final fakeDriver = DriverModel( + vehicleType: 'Car', + vehicleNumber: '123456', + vehicleLicense: 'some_license.png', + ); + + setUp(() { + mockCubit = MockProfileCubit(); + }); + + Widget createWidgetUnderTest() { + return MaterialApp( + home: BlocProvider.value( + value: mockCubit, + child: Scaffold(body: EditVehiclePageBody(driver: fakeDriver)), + ), + ); + } + + testWidgets('initializes form fields with driver data', (tester) async { + when(mockCubit.state).thenReturn(ProfileState()); + when(mockCubit.stream).thenAnswer((_) => const Stream.empty()); + + await tester.pumpWidget(createWidgetUnderTest()); + + expect(find.text('Car'), findsOneWidget); + expect(find.text('123456'), findsOneWidget); + expect(find.text('some_license.png'), findsOneWidget); + }); + + testWidgets( + 'shows loading indicator on update button when state is loading', + (tester) async { + when( + mockCubit.state, + ).thenReturn(ProfileState(editProfileResource: Resource.loading())); + when(mockCubit.stream).thenAnswer((_) => const Stream.empty()); + + await tester.pumpWidget(createWidgetUnderTest()); + + expect(find.text('loading'), findsOneWidget); + }, + ); + + testWidgets('shows success snackbar when update is successful', ( + tester, + ) async { + final state1 = ProfileState(); + final state2 = ProfileState(editProfileResource: Resource.success(null)); + + when(mockCubit.state).thenReturn(state1); + when(mockCubit.stream).thenAnswer((_) => Stream.fromIterable([state2])); + + await tester.pumpWidget(createWidgetUnderTest()); + + mockCubit.emit(state2); + + await tester.pump(); + await tester.pump(const Duration(milliseconds: 100)); + + expect(find.text('Vehicle updated successfully'), findsOneWidget); + }); + + testWidgets( + 'calls PerformEditProfile intent when update button is pressed', + (tester) async { + when(mockCubit.state).thenReturn(ProfileState()); + when(mockCubit.stream).thenAnswer((_) => const Stream.empty()); + + await tester.pumpWidget(createWidgetUnderTest()); + + await tester.tap(find.byType(ElevatedButton)); + await tester.pump(); + + verify( + mockCubit.doIntent( + argThat( + isA() + .having((i) => i.vehicleType, 'vehicleType', 'Car') + .having((i) => i.vehicleNumber, 'vehicleNumber', '123456') + .having( + (i) => i.vehicleLicense, + 'vehicleLicense', + 'some_license.png', + ), + ), + ), + ).called(1); + }, + ); + }); +} diff --git a/test/features/track_order/api/track_order_remote_source_impl_test.dart b/test/features/track_order/api/track_order_remote_source_impl_test.dart new file mode 100644 index 0000000..12a1ed4 --- /dev/null +++ b/test/features/track_order/api/track_order_remote_source_impl_test.dart @@ -0,0 +1,179 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:mocktail/mocktail.dart'; +import 'package:tracking_app/app/config/auth_storage/auth_storage.dart'; +import 'package:cloud_firestore/cloud_firestore.dart'; +import 'package:tracking_app/app/core/network/api_result.dart'; +import 'package:tracking_app/features/track_order/api/track_order_remote_source_impl.dart'; +import 'package:tracking_app/features/track_order/data/models/track_order_model.dart'; +import 'package:tracking_app/features/track_order/data/models/driver_model.dart'; + +/// ---------------- MOCKS ---------------- + +class MockFirebaseFirestore extends Mock implements FirebaseFirestore {} + +class MockAuthStorage extends Mock implements AuthStorage {} + +class MockCollectionReference extends Mock + implements CollectionReference> {} + +class MockQuery extends Mock implements Query> {} + +class MockQuerySnapshot extends Mock + implements QuerySnapshot> {} + +class MockQueryDocumentSnapshot extends Mock + implements QueryDocumentSnapshot> {} + +class MockDocumentReference extends Mock + implements DocumentReference> {} + +class MockDocumentSnapshot extends Mock + implements DocumentSnapshot> {} + +/// ---------------------------------------- + +void main() { + late MockFirebaseFirestore mockFirestore; + late MockAuthStorage mockAuthStorage; + late TrackOrderRemoteDataSourceImpl dataSource; + + setUpAll(() { + registerFallbackValue(const {}); + }); + + setUp(() { + mockFirestore = MockFirebaseFirestore(); + mockAuthStorage = MockAuthStorage(); + dataSource = TrackOrderRemoteDataSourceImpl(mockFirestore, mockAuthStorage); + }); + + group('trackOrder', () { + test('returns SuccessApiResult with mapped models', () async { + final mockCollection = MockCollectionReference(); + final mockQuery = MockQuery(); + final mockSnapshot = MockQuerySnapshot(); + final mockDoc = MockQueryDocumentSnapshot(); + + when(() => mockFirestore.collection('orders')).thenReturn(mockCollection); + when( + () => + mockCollection.orderBy(any(), descending: any(named: 'descending')), + ).thenReturn(mockQuery); + when(() => mockQuery.where(any())).thenReturn(mockQuery); + + when( + () => mockQuery.snapshots(), + ).thenAnswer((_) => Stream.value(mockSnapshot)); + + when(() => mockSnapshot.docs).thenReturn([mockDoc]); + + when(() => mockDoc.id).thenReturn('1'); + + when(() => mockDoc.data()).thenReturn({ + 'status': 'delivered', + 'driver_id': 'd1', + 'totalPrice': '100', + 'userId': 'u1', + 'deviceToken': 'token1', + }); + + final result = dataSource.trackOrder('u1'); + + expect(result, isA()); + + final stream = (result as SuccessApiResult).data; + + final list = await stream.first; + + expect(list, isA>()); + expect(list.length, 1); + expect(list.first.id, '1'); + }); + + test('returns ErrorApiResult when firestore throws', () { + when( + () => mockFirestore.collection('orders'), + ).thenThrow(Exception('Firestore error')); + + final result = dataSource.trackOrder('u1'); + + expect(result, isA()); + }); + }); + + group('trackDriver', () { + test('returns SuccessApiResult with driver model', () async { + final mockCollection = MockCollectionReference(); + final mockDocRef = MockDocumentReference(); + final mockSnapshot = MockDocumentSnapshot(); + + when( + () => mockFirestore.collection('drivers'), + ).thenReturn(mockCollection); + + when(() => mockCollection.doc('d1')).thenReturn(mockDocRef); + + when( + () => mockDocRef.snapshots(), + ).thenAnswer((_) => Stream.value(mockSnapshot)); + + when(() => mockSnapshot.id).thenReturn('d1'); + + when(() => mockSnapshot.data()).thenReturn({ + 'currentLocation': {'lat': 30.0, 'lng': 31.0}, + 'name': 'Driver Name', + 'phone': '12345', + 'deviceToken': 't1', + }); + + final result = dataSource.trackDriver('d1'); + + expect(result, isA()); + + final stream = (result as SuccessApiResult).data; + final driver = await stream.first; + + expect(driver, isA()); + expect(driver.id, 'd1'); + expect(driver.lat, 30.0); + }); + + test('returns ErrorApiResult if firestore throws', () { + when( + () => mockFirestore.collection('drivers'), + ).thenThrow(Exception('Error')); + + final result = dataSource.trackDriver('d1'); + + expect(result, isA()); + }); + }); + + group('updateOrderStatus', () { + test('updates order and returns document snapshot', () async { + final mockCollection = MockCollectionReference(); + final mockDocRef = MockDocumentReference(); + final mockSnapshot = MockDocumentSnapshot(); + final mockNotificationCollection = MockCollectionReference(); + + when(() => mockFirestore.collection('orders')).thenReturn(mockCollection); + when( + () => mockFirestore.collection('notification'), + ).thenReturn(mockNotificationCollection); + + when(() => mockCollection.doc('1')).thenReturn(mockDocRef); + when(() => mockDocRef.update(any())).thenAnswer((_) async {}); + when(() => mockDocRef.get()).thenAnswer((_) async => mockSnapshot); + + when( + () => mockNotificationCollection.add(any()), + ).thenAnswer((_) async => mockDocRef); + + final result = await dataSource.updateOrderStatus('1', 'delivered'); + + expect(result, mockSnapshot); + + verify(() => mockDocRef.update(any())).called(1); + }); + }); +} diff --git a/test/features/track_order/data/models/driver_model_test.dart b/test/features/track_order/data/models/driver_model_test.dart new file mode 100644 index 0000000..e772c66 --- /dev/null +++ b/test/features/track_order/data/models/driver_model_test.dart @@ -0,0 +1,34 @@ +// import 'package:flutter_test/flutter_test.dart'; +// import 'package:tracking_app/features/track_order/data/models/driver_model.dart'; + +// void main() { +// group('DriverModel.fromFirestore', () { +// test('creates DriverModel correctly from map', () { +// final data = {'lat': 30.5, 'lng': 31.2}; + +// final model = DriverModel.fromFirestore('driver1', data); + +// expect(model.id, 'driver1'); +// expect(model.lat, 30.5); +// expect(model.lng, 31.2); +// }); + +// test('converts int to double', () { +// final data = {'lat': 30, 'lng': 31}; + +// final model = DriverModel.fromFirestore('driver2', data); + +// expect(model.lat, 30.0); +// expect(model.lng, 31.0); +// }); + +// test('throws error if lat is missing', () { +// final data = {'lng': 31}; + +// expect( +// () => DriverModel.fromFirestore('driver3', data), +// throwsA(isA()), +// ); +// }); +// }); +// } diff --git a/test/features/track_order/data/models/track_order_model_test.dart b/test/features/track_order/data/models/track_order_model_test.dart new file mode 100644 index 0000000..e246e84 --- /dev/null +++ b/test/features/track_order/data/models/track_order_model_test.dart @@ -0,0 +1,66 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:tracking_app/features/track_order/data/models/track_order_model.dart'; + +void main() { + group('TrackOrderModel.fromFirestore', () { + test('parses flat structure correctly', () { + final data = { + 'driver_id': 'driver1', + 'status': 'on_the_way', + 'totalPrice': '200', + 'userId': 'user1', + }; + + final model = TrackOrderModel.fromFirestore('order1', data); + + expect(model.id, 'order1'); + expect(model.driverId, 'driver1'); + expect(model.status, 'on_the_way'); + expect(model.totalPrice, '200'); + expect(model.userId, 'user1'); + }); + + test('parses nested structure correctly', () { + final data = { + 'driverId': 'driver2', + 'userAddress': {'user_id': 'user2'}, + 'oder_dt': {'status': 'delivered', 'totalPrice': 350}, + }; + + final model = TrackOrderModel.fromFirestore('order2', data); + + expect(model.id, 'order2'); + expect(model.driverId, 'driver2'); + expect(model.status, 'delivered'); + expect(model.totalPrice, '350'); // int converted to string + expect(model.userId, 'user2'); + }); + + test('handles null values safely', () { + final data = { + 'driver_id': null, + 'status': null, + 'totalPrice': null, + 'userId': null, + }; + + final model = TrackOrderModel.fromFirestore('order3', data); + + expect(model.driverId, ''); + expect(model.status, ''); + expect(model.totalPrice, ''); + expect(model.userId, ''); + }); + + test('handles missing nested maps', () { + final data = {}; + + final model = TrackOrderModel.fromFirestore('order4', data); + + expect(model.driverId, ''); + expect(model.status, ''); + expect(model.totalPrice, ''); + expect(model.userId, ''); + }); + }); +} diff --git a/test/features/track_order/data/repos/track_order_repo_imp_test.dart b/test/features/track_order/data/repos/track_order_repo_imp_test.dart new file mode 100644 index 0000000..0814307 --- /dev/null +++ b/test/features/track_order/data/repos/track_order_repo_imp_test.dart @@ -0,0 +1,123 @@ +import 'package:cloud_firestore/cloud_firestore.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mocktail/mocktail.dart'; +import 'package:tracking_app/app/core/network/api_result.dart'; +import 'package:tracking_app/features/track_order/data/models/driver_model.dart'; +import 'package:tracking_app/features/track_order/data/models/track_order_model.dart'; +import 'package:tracking_app/features/track_order/data/repos/track_order_repo_imp.dart'; +import 'package:tracking_app/features/track_order/domain/entities/driver_entity.dart'; +import 'package:tracking_app/features/track_order/domain/entities/order_entity.dart'; +import 'package:tracking_app/features/track_order/data/datasource/track_order_remote_source.dart'; + +class MockRemoteDataSource extends Mock implements TrackOrderRemoteDataSource {} + +class MockDocumentSnapshot extends Mock + implements DocumentSnapshot> {} + +void main() { + late MockRemoteDataSource mockRemote; + late TrackOrderRepoImpl repo; + + setUp(() { + mockRemote = MockRemoteDataSource(); + repo = TrackOrderRepoImpl(mockRemote); + }); + + group('trackOrder', () { + test('returns SuccessApiResult with mapped OrderEntity', () async { + final model = TrackOrderModel( + id: 'o1', + userId: 'u1', + driverId: 'd1', + status: 'delivered', + totalPrice: '100', + pickupAddress: 'p1', + pickupName: 'pn', + userAddress: 'u1', + userName: 'un', + deviceToken: 'token1', + ); + + when( + () => mockRemote.trackOrder('u1'), + ).thenReturn(SuccessApiResult(data: Stream.value([model]))); + + final result = repo.trackOrder('u1'); + + expect(result, isA>>>()); + + final list = await (result as SuccessApiResult).data.first; + + expect(list.length, 1); + expect(list.first, isA()); + expect(list.first.id, 'o1'); + expect(list.first.userId, 'u1'); + expect(list.first.status, 'delivered'); + }); + + test('returns ErrorApiResult if remote fails', () { + when( + () => mockRemote.trackOrder('u1'), + ).thenReturn(ErrorApiResult(error: 'Network Error')); + + final result = repo.trackOrder('u1'); + + expect(result, isA()); + expect((result as ErrorApiResult).error, 'Network Error'); + }); + }); + + group('trackOrderWithDriver', () { + test('returns SuccessApiResult with mapped DriverEntity', () async { + final model = DriverModel( + id: 'd1', + lat: 10.0, + lng: 20.0, + name: 'Driver Name', + phone: '12345678', + deviceToken: 'token1', + ); + + when( + () => mockRemote.trackDriver('d1'), + ).thenReturn(SuccessApiResult(data: Stream.value(model))); + + final result = repo.trackOrderWithDriver('d1'); + + expect(result, isA>>()); + + final driver = await (result as SuccessApiResult).data.first; + + expect(driver, isA()); + expect(driver.id, 'd1'); + expect(driver.lat, 10.0); + expect(driver.lng, 20.0); + expect(driver.name, 'Driver Name'); + }); + + test('returns ErrorApiResult if remote fails', () { + when( + () => mockRemote.trackDriver('d1'), + ).thenReturn(ErrorApiResult(error: 'Driver not found')); + + final result = repo.trackOrderWithDriver('d1'); + + expect(result, isA()); + expect((result as ErrorApiResult).error, 'Driver not found'); + }); + }); + + group('updateOrderStatus', () { + test('calls remoteDataSource.updateOrderStatus', () async { + when( + () => mockRemote.updateOrderStatus('o1', 'delivered',), + ).thenAnswer((_) async => MockDocumentSnapshot()); + + await repo.updateOrderStatus('o1', 'delivered',); + + verify( + () => mockRemote.updateOrderStatus('o1', 'delivered', ), + ).called(1); + }); + }); +} diff --git a/test/features/track_order/domain/entities/driver_entity_test.dart b/test/features/track_order/domain/entities/driver_entity_test.dart new file mode 100644 index 0000000..8663a81 --- /dev/null +++ b/test/features/track_order/domain/entities/driver_entity_test.dart @@ -0,0 +1,55 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:tracking_app/features/track_order/domain/entities/driver_entity.dart'; + +void main() { + group('DriverEntity', () { + test('should create a DriverEntity with correct values', () { + // Arrange + const id = 'driver1'; + const lat = 10.5; + const lng = 20.3; + const name = 'John Doe'; + const phone = '01234567890'; + const deviceToken = 'token123'; + + // Act + const driver = DriverEntity( + id: id, + lat: lat, + lng: lng, + name: name, + phone: phone, + deviceToken: deviceToken, + ); + + // Assert + expect(driver.id, id); + expect(driver.lat, lat); + expect(driver.lng, lng); + expect(driver.name, name); + expect(driver.phone, phone); + expect(driver.deviceToken, deviceToken); + }); + + test('should support value equality', () { + const driver1 = DriverEntity( + id: 'd1', + lat: 0.0, + lng: 0.0, + name: 'a', + phone: '1', + deviceToken: 't1', + ); + const driver2 = DriverEntity( + id: 'd1', + lat: 0.0, + lng: 0.0, + name: 'a', + phone: '1', + deviceToken: 't1', + ); + + expect(driver1, driver2); + }); + }); +} diff --git a/test/features/track_order/domain/entities/order_entity_test.dart b/test/features/track_order/domain/entities/order_entity_test.dart new file mode 100644 index 0000000..d737742 --- /dev/null +++ b/test/features/track_order/domain/entities/order_entity_test.dart @@ -0,0 +1,64 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:tracking_app/features/track_order/domain/entities/order_entity.dart'; + +void main() { + group('OrderEntity', () { + test('should create an OrderEntity with all fields', () { + // Arrange + const id = 'o1'; + const userId = 'u1'; + const status = 'delivered'; + const driverId = 'd1'; + const totalPrice = '100'; + const pickupAddress = 'Store Street'; + const pickupName = 'Flower Shop'; + const userAddress = 'Home Avenue'; + const userName = 'John Doe'; + + // Act + final order = OrderEntity( + id: id, + userId: userId, + status: status, + driverId: driverId, + totalPrice: totalPrice, + pickupAddress: pickupAddress, + pickupName: pickupName, + userAddress: userAddress, + userName: userName, + ); + + // Assert + expect(order.id, id); + expect(order.userId, userId); + expect(order.status, status); + expect(order.driverId, driverId); + expect(order.totalPrice, totalPrice); + expect(order.pickupAddress, pickupAddress); + expect(order.pickupName, pickupName); + expect(order.userAddress, userAddress); + expect(order.userName, userName); + }); + + test('should create an OrderEntity with only required fields', () { + // Arrange + const id = 'o2'; + const userId = 'u2'; + const status = 'pending'; + + // Act + final order = OrderEntity(id: id, userId: userId, status: status); + + // Assert + expect(order.id, id); + expect(order.userId, userId); + expect(order.status, status); + expect(order.driverId, isNull); + expect(order.totalPrice, isNull); + expect(order.pickupAddress, isNull); + expect(order.pickupName, isNull); + expect(order.userAddress, isNull); + expect(order.userName, isNull); + }); + }); +} diff --git a/test/features/track_order/domain/usecases/driver_usecase_test.dart b/test/features/track_order/domain/usecases/driver_usecase_test.dart new file mode 100644 index 0000000..639e603 --- /dev/null +++ b/test/features/track_order/domain/usecases/driver_usecase_test.dart @@ -0,0 +1,57 @@ +import 'dart:async'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mocktail/mocktail.dart'; +import 'package:tracking_app/app/core/network/api_result.dart'; +import 'package:tracking_app/features/track_order/domain/entities/driver_entity.dart'; +import 'package:tracking_app/features/track_order/domain/usecases/driver_usecase.dart'; +import 'package:tracking_app/features/track_order/domain/repos/track_order_repo.dart'; + +class MockTrackOrderRepo extends Mock implements TrackOrderRepo {} + +void main() { + late MockTrackOrderRepo mockRepo; + late TrackDriverUseCase useCase; + + setUp(() { + mockRepo = MockTrackOrderRepo(); + useCase = TrackDriverUseCase(mockRepo); + }); + + group('TrackDriverUseCase', () { + const driver = DriverEntity( + id: 'd1', + lat: 10.0, + lng: 20.0, + name: 'John', + phone: '12345', + deviceToken: 'token123', + ); + + test('returns SuccessApiResult with driver stream', () async { + when( + () => mockRepo.trackOrderWithDriver('d1'), + ).thenReturn(SuccessApiResult(data: Stream.value(driver))); + + final result = useCase.call('d1'); + + expect(result, isA>>()); + + final d = await (result as SuccessApiResult).data.first; + expect(d.id, 'd1'); + expect(d.lat, 10.0); + expect(d.lng, 20.0); + expect(d, driver); // Check equality since it's Equatable + }); + + test('returns ErrorApiResult when repository fails', () { + when( + () => mockRepo.trackOrderWithDriver('d1'), + ).thenReturn(ErrorApiResult(error: 'Driver not found')); + + final result = useCase.call('d1'); + + expect(result, isA()); + expect((result as ErrorApiResult).error, 'Driver not found'); + }); + }); +} diff --git a/test/features/track_order/domain/usecases/track_order_usecase_test.dart b/test/features/track_order/domain/usecases/track_order_usecase_test.dart new file mode 100644 index 0000000..1c47e50 --- /dev/null +++ b/test/features/track_order/domain/usecases/track_order_usecase_test.dart @@ -0,0 +1,50 @@ +// test/features/track_order/domain/usecases/track_order_usecase_test.dart + +import 'dart:async'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mocktail/mocktail.dart'; +import 'package:tracking_app/app/core/network/api_result.dart'; +import 'package:tracking_app/features/track_order/domain/entities/order_entity.dart'; +import 'package:tracking_app/features/track_order/domain/usecases/track_order_usecase.dart'; +import 'package:tracking_app/features/track_order/domain/repos/track_order_repo.dart'; + +class MockTrackOrderRepo extends Mock implements TrackOrderRepo {} + +void main() { + late MockTrackOrderRepo mockRepo; + late TrackOrderUseCase useCase; + + setUp(() { + mockRepo = MockTrackOrderRepo(); + useCase = TrackOrderUseCase(mockRepo); + }); + + group('TrackOrderUseCase', () { + final orders = [OrderEntity(id: 'o1', userId: 'u1', status: 'delivered')]; + + test('returns SuccessApiResult with orders stream', () async { + when( + () => mockRepo.trackOrder('u1'), + ).thenReturn(SuccessApiResult(data: Stream.value(orders))); + + final result = useCase.call('u1'); + + expect(result, isA>>>()); + + final list = await (result as SuccessApiResult).data.first; + expect(list.length, 1); + expect(list.first.id, 'o1'); + }); + + test('returns ErrorApiResult when repository fails', () { + when( + () => mockRepo.trackOrder('u1'), + ).thenReturn(ErrorApiResult(error: 'Network Error')); + + final result = useCase.call('u1'); + + expect(result, isA()); + expect((result as ErrorApiResult).error, 'Network Error'); + }); + }); +} diff --git a/test/features/track_order/presentation/manager/cubit/track_order_cubit_test.dart b/test/features/track_order/presentation/manager/cubit/track_order_cubit_test.dart new file mode 100644 index 0000000..368618c --- /dev/null +++ b/test/features/track_order/presentation/manager/cubit/track_order_cubit_test.dart @@ -0,0 +1,153 @@ +import 'dart:async'; +import 'package:bloc_test/bloc_test.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mocktail/mocktail.dart'; +import 'package:tracking_app/app/core/network/api_result.dart'; +import 'package:tracking_app/app/config/auth_storage/auth_storage.dart'; +import 'package:tracking_app/features/track_order/domain/entities/order_entity.dart'; +import 'package:tracking_app/features/track_order/domain/entities/driver_entity.dart'; +import 'package:tracking_app/features/track_order/domain/usecases/track_order_usecase.dart'; +import 'package:tracking_app/features/track_order/domain/usecases/driver_usecase.dart'; +import 'package:tracking_app/features/track_order/domain/usecases/update_state_usecase.dart'; +import 'package:tracking_app/features/track_order/presentation/manager/cubit/track_order_cubit.dart'; + +class MockTrackOrderUseCase extends Mock implements TrackOrderUseCase {} + +class MockTrackDriverUseCase extends Mock implements TrackDriverUseCase {} + +class MockUpdateOrderStatusUseCase extends Mock + implements UpdateOrderStatusUseCase {} + +class MockAuthStorage extends Mock implements AuthStorage {} + +void main() { + late MockTrackOrderUseCase mockTrackOrderUseCase; + late MockTrackDriverUseCase mockTrackDriverUseCase; + late MockUpdateOrderStatusUseCase mockUpdateOrderStatusUseCase; + late MockAuthStorage mockAuthStorage; + + setUp(() { + mockTrackOrderUseCase = MockTrackOrderUseCase(); + mockTrackDriverUseCase = MockTrackDriverUseCase(); + mockUpdateOrderStatusUseCase = MockUpdateOrderStatusUseCase(); + mockAuthStorage = MockAuthStorage(); + }); + + group('loadUserOrders', () { + final order = OrderEntity(id: 'o1', userId: 'u1', status: 'delivered'); + final ordersStream = Stream.value([order]); + + blocTest( + 'emits error if token is null', + build: () { + when(() => mockAuthStorage.getToken()).thenAnswer((_) async => null); + return TrackOrderCubit( + mockTrackOrderUseCase, + mockTrackDriverUseCase, + mockUpdateOrderStatusUseCase, + mockAuthStorage, + ); + }, + act: (cubit) => cubit.loadUserOrders(), + expect: () => [ + const TrackOrderState(isLoading: true), + const TrackOrderState(isLoading: false, error: 'User not logged in'), + ], + ); + + blocTest( + 'emits orders when SuccessApiResult is returned', + build: () { + when( + () => mockAuthStorage.getToken(), + ).thenAnswer((_) async => 'dummy.token.value'); + when( + () => mockTrackOrderUseCase.call(any()), + ).thenReturn(SuccessApiResult(data: ordersStream)); + when( + () => mockTrackDriverUseCase.call(any()), + ).thenReturn(ErrorApiResult(error: 'Driver error')); + return TrackOrderCubit( + mockTrackOrderUseCase, + mockTrackDriverUseCase, + mockUpdateOrderStatusUseCase, + mockAuthStorage, + ); + }, + act: (cubit) => cubit.loadUserOrders(), + expect: () => [ + const TrackOrderState(isLoading: true), + TrackOrderState(isLoading: false, orders: [order]), + ], + ); + }); + + group('trackDriver', () { + const driver = DriverEntity( + id: 'd1', + lat: 10.0, + lng: 20.0, + name: 'Driver 1', + phone: '12345678', + deviceToken: 't1', + ); + final driverStream = Stream.value(driver); + + blocTest( + 'emits driver when SuccessApiResult is returned', + build: () { + when( + () => mockTrackDriverUseCase.call('d1'), + ).thenReturn(SuccessApiResult(data: driverStream)); + return TrackOrderCubit( + mockTrackOrderUseCase, + mockTrackDriverUseCase, + mockUpdateOrderStatusUseCase, + mockAuthStorage, + ); + }, + act: (cubit) => cubit.trackDriver('d1'), + expect: () => [const TrackOrderState(driver: driver)], + ); + + blocTest( + 'emits error if stream has error', + build: () { + final errorStream = Stream.error('Driver not found'); + when( + () => mockTrackDriverUseCase.call('d1'), + ).thenReturn(SuccessApiResult(data: errorStream)); + return TrackOrderCubit( + mockTrackOrderUseCase, + mockTrackDriverUseCase, + mockUpdateOrderStatusUseCase, + mockAuthStorage, + ); + }, + act: (cubit) => cubit.trackDriver('d1'), + expect: () => [const TrackOrderState(error: 'Driver not found')], + ); + }); + + group('updateOrderStatus', () { + blocTest( + 'emits isLoading then success', + build: () { + when( + () => mockUpdateOrderStatusUseCase.call(any(), any()), + ).thenAnswer((_) async {}); + return TrackOrderCubit( + mockTrackOrderUseCase, + mockTrackDriverUseCase, + mockUpdateOrderStatusUseCase, + mockAuthStorage, + ); + }, + act: (cubit) => cubit.updateOrderStatus('o1', 'Delivered'), + expect: () => [ + const TrackOrderState(isLoading: true), + const TrackOrderState(isLoading: false), + ], + ); + }); +} diff --git a/test_generics.dart b/test_generics.dart new file mode 100644 index 0000000..e69de29 diff --git a/test_output.txt b/test_output.txt deleted file mode 100644 index 998b348..0000000 Binary files a/test_output.txt and /dev/null differ diff --git a/test_output_api.txt b/test_output_api.txt new file mode 100644 index 0000000..9e7ca8d Binary files /dev/null and b/test_output_api.txt differ diff --git a/tests_output.txt b/tests_output.txt new file mode 100644 index 0000000..b7a4951 Binary files /dev/null and b/tests_output.txt differ diff --git a/tests_output_utf8.txt b/tests_output_utf8.txt new file mode 100644 index 0000000..d79c4c5 --- /dev/null +++ b/tests_output_utf8.txt @@ -0,0 +1,424 @@ +00:00 +0: loading C:/flutter_projects/tracking_app/test/features/app_sections/presentation/manager/app_section_cubit_test.dart +00:00 +0: C:/flutter_projects/tracking_app/test/features/app_sections/presentation/manager/app_section_cubit_test.dart: App section cubit emits index 0 when updateIndex(0) is called +00:00 +1: C:/flutter_projects/tracking_app/test/features/app_sections/presentation/manager/app_section_cubit_test.dart: App section cubit emits index 1 when updateIndex(1) is called +00:00 +2: C:/flutter_projects/tracking_app/test/features/app_sections/presentation/manager/app_section_cubit_test.dart: App section cubit emits index 2 when updateIndex(2) is called +00:00 +3: C:/flutter_projects/tracking_app/test/features/app_sections/presentation/manager/app_section_cubit_test.dart: App section cubit does not emit when updating with the same index +00:00 +4: C:/flutter_projects/tracking_app/test/features/app_sections/presentation/manager/app_section_cubit_test.dart: App section cubit emits correct states when updateIndex is called multiple times +00:01 +5: C:/flutter_projects/tracking_app/test/features/app_sections/presentation/widgets/app_section_view_test.dart: (setUpAll) +[≡ƒîÄ Easy Localization] [DEBUG] Localization initialized +00:01 +5: C:/flutter_projects/tracking_app/test/features/app_sections/presentation/widgets/app_section_view_test.dart: AppSectionsView Widget Test should show Home page by default +00:02 +6: C:/flutter_projects/tracking_app/test/features/app_sections/presentation/widgets/app_section_view_test.dart: AppSectionsView Widget Test should show Home page by default +00:02 +7: C:/flutter_projects/tracking_app/test/features/app_sections/presentation/widgets/app_section_view_test.dart: AppSectionsView Widget Test should show Home page by default +00:02 +8: C:/flutter_projects/tracking_app/test/features/app_sections/presentation/widgets/app_section_view_test.dart: AppSectionsView Widget Test should show Home page by default +00:02 +9: C:/flutter_projects/tracking_app/test/features/app_sections/presentation/widgets/app_section_view_test.dart: AppSectionsView Widget Test should show Home page by default +00:02 +10: C:/flutter_projects/tracking_app/test/features/app_sections/presentation/widgets/app_section_view_test.dart: AppSectionsView Widget Test should show Home page by default +00:02 +11: C:/flutter_projects/tracking_app/test/features/app_sections/presentation/widgets/app_section_view_test.dart: AppSectionsView Widget Test should show Home page by default +00:02 +12: C:/flutter_projects/tracking_app/test/features/app_sections/presentation/widgets/app_section_view_test.dart: AppSectionsView Widget Test should show Home page by default +00:02 +13: C:/flutter_projects/tracking_app/test/features/app_sections/presentation/widgets/app_section_view_test.dart: AppSectionsView Widget Test should show Home page by default +00:02 +14: C:/flutter_projects/tracking_app/test/features/app_sections/presentation/widgets/app_section_view_test.dart: AppSectionsView Widget Test should show Home page by default +00:02 +15: C:/flutter_projects/tracking_app/test/features/app_sections/presentation/widgets/app_section_view_test.dart: AppSectionsView Widget Test should show Home page by default +00:02 +16: C:/flutter_projects/tracking_app/test/features/app_sections/presentation/widgets/app_section_view_test.dart: AppSectionsView Widget Test should show Home page by default +00:02 +17: C:/flutter_projects/tracking_app/test/features/app_sections/presentation/widgets/app_section_view_test.dart: AppSectionsView Widget Test should show Home page by default +00:03 +18: C:/flutter_projects/tracking_app/test/features/app_sections/presentation/widgets/app_section_view_test.dart: AppSectionsView Widget Test should show Home page by default +[≡ƒîÄ Easy Localization] [DEBUG] Start +[≡ƒîÄ Easy Localization] [DEBUG] Init state +[≡ƒîÄ Easy Localization] [DEBUG] Build +[≡ƒîÄ Easy Localization] [DEBUG] Init Localization Delegate +[≡ƒîÄ Easy Localization] [DEBUG] Init provider +[≡ƒîÄ Easy Localization] [WARNING] Localization key [home] not found +[≡ƒîÄ Easy Localization] [WARNING] Localization key [orders] not found +[≡ƒîÄ Easy Localization] [WARNING] Localization key [profile] not found +[≡ƒîÄ Easy Localization] [WARNING] Localization key [home] not found +[≡ƒîÄ Easy Localization] [WARNING] Localization key [orders] not found +[≡ƒîÄ Easy Localization] [WARNING] Localization key [profile] not found +00:03 +19: C:/flutter_projects/tracking_app/test/features/app_sections/presentation/widgets/app_section_view_test.dart: AppSectionsView Widget Test should navigate to Orders page when tapping Orders +[≡ƒîÄ Easy Localization] [DEBUG] Start +[≡ƒîÄ Easy Localization] [DEBUG] Init state +[≡ƒîÄ Easy Localization] [DEBUG] Build +[≡ƒîÄ Easy Localization] [DEBUG] Init Localization Delegate +[≡ƒîÄ Easy Localization] [DEBUG] Init provider +[≡ƒîÄ Easy Localization] [WARNING] Localization key [home] not found +[≡ƒîÄ Easy Localization] [WARNING] Localization key [orders] not found +[≡ƒîÄ Easy Localization] [WARNING] Localization key [profile] not found +[≡ƒîÄ Easy Localization] [WARNING] Localization key [home] not found +[≡ƒîÄ Easy Localization] [WARNING] Localization key [orders] not found +[≡ƒîÄ Easy Localization] [WARNING] Localization key [profile] not found +00:03 +20: C:/flutter_projects/tracking_app/test/features/app_sections/presentation/widgets/app_section_view_test.dart: AppSectionsView Widget Test should navigate to Profile page when tapping Profile +[≡ƒîÄ Easy Localization] [DEBUG] Start +[≡ƒîÄ Easy Localization] [DEBUG] Init state +[≡ƒîÄ Easy Localization] [DEBUG] Build +[≡ƒîÄ Easy Localization] [DEBUG] Init Localization Delegate +[≡ƒîÄ Easy Localization] [DEBUG] Init provider +[≡ƒîÄ Easy Localization] [WARNING] Localization key [home] not found +[≡ƒîÄ Easy Localization] [WARNING] Localization key [orders] not found +[≡ƒîÄ Easy Localization] [WARNING] Localization key [profile] not found +[≡ƒîÄ Easy Localization] [WARNING] Localization key [home] not found +[≡ƒîÄ Easy Localization] [WARNING] Localization key [orders] not found +[≡ƒîÄ Easy Localization] [WARNING] Localization key [profile] not found +00:03 +21: C:/flutter_projects/tracking_app/test/features/app_sections/presentation/widgets/app_section_view_test.dart: (tearDownAll) +00:03 +21: C:/flutter_projects/tracking_app/test/features/auth/data/model/request/LoginRequest_test.dart: LoginRequest should be a subclass of LoginRequest entity +00:03 +22: C:/flutter_projects/tracking_app/test/features/auth/data/model/request/LoginRequest_test.dart: LoginRequest fromJson should return a valid model +00:03 +23: C:/flutter_projects/tracking_app/test/features/auth/data/model/request/LoginRequest_test.dart: LoginRequest toJson should return a JSON map containing proper data +00:04 +24: C:/flutter_projects/tracking_app/test/features/auth/data/model/response/change_password_dto_test.dart: ChangePasswordDto Json serialization fromJson should parse correctly +00:04 +25: C:/flutter_projects/tracking_app/test/features/auth/data/model/response/change_password_dto_test.dart: ChangePasswordDto Json serialization toJson should parse correctly +00:05 +26: C:/flutter_projects/tracking_app/test/features/auth/data/model/response/LoginResponse_test.dart: LoginResponse should be a subclass of LoginResponse entity +00:05 +27: C:/flutter_projects/tracking_app/test/features/auth/data/model/response/LoginResponse_test.dart: LoginResponse fromJson should return a valid model +00:05 +28: C:/flutter_projects/tracking_app/test/features/auth/data/model/response/LoginResponse_test.dart: LoginResponse toJson should return a JSON map containing proper data +00:05 +29: C:/flutter_projects/tracking_app/test/features/auth/data/models/response/forgetpassword_response_test.dart: ForgetpasswordResponse fromJson should parse correctly +00:05 +30: C:/flutter_projects/tracking_app/test/features/auth/data/models/response/forgetpassword_response_test.dart: ForgetpasswordResponse toJson should return correct map +00:05 +31: C:/flutter_projects/tracking_app/test/features/auth/data/models/response/forgetpassword_response_test.dart: ForgetpasswordResponse copyWith should override only provided fields +00:05 +32: C:/flutter_projects/tracking_app/test/features/auth/data/models/response/forgetpassword_response_test.dart: ForgetpasswordResponse should handle null values correctly +00:06 +33: C:/flutter_projects/tracking_app/test/features/auth/data/models/response/resetpassword_response_test.dart: ResetpasswordResponse fromJson should parse correctly +00:06 +34: C:/flutter_projects/tracking_app/test/features/auth/data/models/response/resetpassword_response_test.dart: ResetpasswordResponse toJson should return correct map +00:06 +35: C:/flutter_projects/tracking_app/test/features/auth/data/models/response/resetpassword_response_test.dart: ResetpasswordResponse copyWith should override only provided fields +00:06 +36: C:/flutter_projects/tracking_app/test/features/auth/data/models/response/resetpassword_response_test.dart: ResetpasswordResponse should handle null values +00:06 +37: C:/flutter_projects/tracking_app/test/features/auth/data/models/response/verifyreset_response_test.dart: VerifyresetResponse fromJson should parse correctly +00:06 +38: C:/flutter_projects/tracking_app/test/features/auth/data/models/response/verifyreset_response_test.dart: VerifyresetResponse toJson should return correct map +00:06 +39: C:/flutter_projects/tracking_app/test/features/auth/data/models/response/verifyreset_response_test.dart: VerifyresetResponse copyWith should override provided field +00:06 +40: C:/flutter_projects/tracking_app/test/features/auth/data/models/response/verifyreset_response_test.dart: VerifyresetResponse should handle null values +00:07 +41: C:/flutter_projects/tracking_app/test/features/auth/data/repos/auth_repo_impl_test.dart: (setUpAll) +00:07 +41: C:/flutter_projects/tracking_app/test/features/auth/data/repos/auth_repo_impl_test.dart: forgetPassword should return SuccessApiResult when datasource succeeds +00:07 +42: C:/flutter_projects/tracking_app/test/features/auth/data/repos/auth_repo_impl_test.dart: forgetPassword should return ErrorApiResult when datasource fails +00:07 +43: C:/flutter_projects/tracking_app/test/features/auth/data/repos/auth_repo_impl_test.dart: verifyResetCode should return SuccessApiResult when datasource succeeds +00:07 +44: C:/flutter_projects/tracking_app/test/features/auth/data/repos/auth_repo_impl_test.dart: verifyResetCode should return ErrorApiResult when datasource fails +00:07 +45: C:/flutter_projects/tracking_app/test/features/auth/data/repos/auth_repo_impl_test.dart: resetPassword should return SuccessApiResult when datasource succeeds +00:07 +46: C:/flutter_projects/tracking_app/test/features/auth/data/repos/auth_repo_impl_test.dart: resetPassword should return ErrorApiResult when datasource fails +00:07 +47: C:/flutter_projects/tracking_app/test/features/auth/data/repos/auth_repo_impl_test.dart: AuthRepoImpl.login should return SuccessApiResult when remote data source call is successful +00:07 +48: C:/flutter_projects/tracking_app/test/features/auth/data/repos/auth_repo_impl_test.dart: AuthRepoImpl.login should return ErrorApiResult when remote data source call fails +00:07 +49: C:/flutter_projects/tracking_app/test/features/auth/data/repos/auth_repo_impl_test.dart: AuthRepoImpl.changePassword() should return ApiSuccess when changePassword datasource succeeds +00:07 +50: C:/flutter_projects/tracking_app/test/features/auth/data/repos/auth_repo_impl_test.dart: AuthRepoImpl.changePassword() should return ApiFailure when changePassword datasource throws exception +00:07 +51: C:/flutter_projects/tracking_app/test/features/auth/data/repos/auth_repo_impl_test.dart: (tearDownAll) +00:07 +51: C:/flutter_projects/tracking_app/test/features/auth/domain/models/change_password_model_test.dart: ChangePasswordModel should create instance with correct values +00:08 +52: C:/flutter_projects/tracking_app/test/features/auth/domain/usecase/apply_usecase_test.dart: ApplyUseCase - (setUpAll) +00:08 +52: C:/flutter_projects/tracking_app/test/features/auth/domain/usecase/apply_usecase_test.dart: ApplyUseCase - should return SuccessApiResult when apply succeeds +00:08 +53: C:/flutter_projects/tracking_app/test/features/auth/domain/usecase/apply_usecase_test.dart: ApplyUseCase - should return ErrorApiResult when apply fails +00:08 +54: C:/flutter_projects/tracking_app/test/features/auth/domain/usecase/apply_usecase_test.dart: ApplyUseCase - should call repository apply method with correct parameters +00:08 +55: C:/flutter_projects/tracking_app/test/features/auth/domain/usecase/apply_usecase_test.dart: ApplyUseCase - (tearDownAll) +00:08 +55: C:/flutter_projects/tracking_app/test/features/auth/domain/usecase/change_password_usecase_test.dart: (setUpAll) +00:08 +55: C:/flutter_projects/tracking_app/test/features/auth/domain/usecase/change_password_usecase_test.dart: ChangePasswordUseCase returns SuccessApiResult when repos returns success +00:08 +56: C:/flutter_projects/tracking_app/test/features/auth/domain/usecase/change_password_usecase_test.dart: ChangePasswordUseCase returns ErrorApiResult when repos returns error +00:08 +57: C:/flutter_projects/tracking_app/test/features/auth/domain/usecase/change_password_usecase_test.dart: (tearDownAll) +00:09 +57: C:/flutter_projects/tracking_app/test/features/auth/domain/usecase/forgetpassword_usecase_test.dart: (setUpAll) +00:09 +57: C:/flutter_projects/tracking_app/test/features/auth/domain/usecase/forgetpassword_usecase_test.dart: ForgetPasswordUsecase returns SuccessApiResult when repo succeeds +00:09 +58: C:/flutter_projects/tracking_app/test/features/auth/domain/usecase/forgetpassword_usecase_test.dart: ForgetPasswordUsecase returns ErrorApiResult when repo fails +00:09 +59: C:/flutter_projects/tracking_app/test/features/auth/domain/usecase/forgetpassword_usecase_test.dart: (tearDownAll) +00:09 +59: C:/flutter_projects/tracking_app/test/features/auth/domain/usecase/get_all_vehicles_usecase_test.dart: GetAllVehiclesUseCase - should return SuccessApiResult when getAllVehicles succeeds +00:09 +60: C:/flutter_projects/tracking_app/test/features/auth/domain/usecase/get_all_vehicles_usecase_test.dart: GetAllVehiclesUseCase - should return ErrorApiResult when getAllVehicles fails +00:09 +61: C:/flutter_projects/tracking_app/test/features/auth/domain/usecase/get_all_vehicles_usecase_test.dart: GetAllVehiclesUseCase - should call repository getAllVehicles method +00:09 +62: C:/flutter_projects/tracking_app/test/features/auth/domain/usecase/get_all_vehicles_usecase_test.dart: GetAllVehiclesUseCase - should return empty list when no vehicles available +00:09 +63: C:/flutter_projects/tracking_app/test/features/auth/domain/usecase/get_all_vehicles_usecase_test.dart: GetAllVehiclesUseCase - should handle vehicles with null ids +00:10 +64: C:/flutter_projects/tracking_app/test/features/auth/domain/usecase/get_countries_usecase_test.dart: GetCountriesUseCase - should return SuccessApiResult when getCountries succeeds +00:10 +65: C:/flutter_projects/tracking_app/test/features/auth/domain/usecase/get_countries_usecase_test.dart: GetCountriesUseCase - should return ErrorApiResult when getCountries fails +00:10 +66: C:/flutter_projects/tracking_app/test/features/auth/domain/usecase/get_countries_usecase_test.dart: GetCountriesUseCase - should call repository getCountries method +00:10 +67: C:/flutter_projects/tracking_app/test/features/auth/domain/usecase/get_countries_usecase_test.dart: GetCountriesUseCase - should return empty list when no countries available +00:10 +68: C:/flutter_projects/tracking_app/test/features/auth/domain/usecase/login_usecase_test.dart: (setUpAll) +00:10 +68: C:/flutter_projects/tracking_app/test/features/auth/domain/usecase/login_usecase_test.dart: LoginUseCase should return SuccessApiResult when repo call is successful +00:10 +69: C:/flutter_projects/tracking_app/test/features/auth/domain/usecase/login_usecase_test.dart: LoginUseCase should return ErrorApiResult when repo call fails +00:10 +70: C:/flutter_projects/tracking_app/test/features/auth/domain/usecase/login_usecase_test.dart: (tearDownAll) +00:10 +70: C:/flutter_projects/tracking_app/test/features/auth/domain/usecase/resertpassword_usecase_test.dart: (setUpAll) +00:10 +70: C:/flutter_projects/tracking_app/test/features/auth/domain/usecase/resertpassword_usecase_test.dart: ResetPasswordUsecase returns SuccessApiResult when repo succeeds +00:10 +71: C:/flutter_projects/tracking_app/test/features/auth/domain/usecase/resertpassword_usecase_test.dart: ResetPasswordUsecase returns ErrorApiResult when repo fails +00:10 +72: C:/flutter_projects/tracking_app/test/features/auth/domain/usecase/resertpassword_usecase_test.dart: (tearDownAll) +00:11 +72: C:/flutter_projects/tracking_app/test/features/auth/domain/usecase/verifyreaset_usecase_test.dart: (setUpAll) +00:11 +72: C:/flutter_projects/tracking_app/test/features/auth/domain/usecase/verifyreaset_usecase_test.dart: VerifyResetCodeUsecase returns SuccessApiResult when repo succeeds +00:11 +73: C:/flutter_projects/tracking_app/test/features/auth/domain/usecase/verifyreaset_usecase_test.dart: VerifyResetCodeUsecase returns ErrorApiResult when repo fails +00:11 +74: C:/flutter_projects/tracking_app/test/features/auth/domain/usecase/verifyreaset_usecase_test.dart: (tearDownAll) +00:11 +74: C:/flutter_projects/tracking_app/test/features/auth/presentation/apply/manager/apply_cubit_test.dart: ApplyCubit - GetCountriesIntent emits [loading, success] when GetCountriesIntent succeeds +00:12 +75: C:/flutter_projects/tracking_app/test/features/auth/presentation/apply/manager/apply_cubit_test.dart: ApplyCubit - GetCountriesIntent emits [loading, failure] when GetCountriesIntent fails +00:12 +76: C:/flutter_projects/tracking_app/test/features/auth/presentation/apply/manager/apply_cubit_test.dart: ApplyCubit - GetVehiclesIntent emits [loading, success] when GetVehiclesIntent succeeds +00:12 +77: C:/flutter_projects/tracking_app/test/features/auth/presentation/apply/manager/apply_cubit_test.dart: ApplyCubit - GetVehiclesIntent emits [loading, failure] when GetVehiclesIntent fails +00:12 +78: C:/flutter_projects/tracking_app/test/features/auth/presentation/apply/manager/apply_cubit_test.dart: ApplyCubit - SubmitApplyIntent (setUpAll) +00:12 +78: C:/flutter_projects/tracking_app/test/features/auth/presentation/apply/manager/apply_cubit_test.dart: ApplyCubit - SubmitApplyIntent emits [loading, success] when SubmitApplyIntent succeeds +00:12 +79: C:/flutter_projects/tracking_app/test/features/auth/presentation/apply/manager/apply_cubit_test.dart: ApplyCubit - SubmitApplyIntent emits [loading, failure] when SubmitApplyIntent fails +00:12 +80: C:/flutter_projects/tracking_app/test/features/auth/presentation/apply/manager/apply_cubit_test.dart: ApplyCubit - SubmitApplyIntent (tearDownAll) +00:12 +80: C:/flutter_projects/tracking_app/test/features/auth/presentation/apply/manager/apply_cubit_test.dart: ApplyCubit - initial state is correct +00:13 +81: loading C:/flutter_projects/tracking_app/test/features/auth/presentation/apply/view/apply_screen_test.dart +[≡ƒîÄ Easy Localization] [DEBUG] Localization initialized +00:13 +81: C:/flutter_projects/tracking_app/test/features/auth/presentation/apply/view/apply_screen_test.dart: ApplySuccessScreen Widget Tests - should display success message +00:14 +82: C:/flutter_projects/tracking_app/test/features/auth/presentation/apply/view/apply_screen_test.dart: ApplySuccessScreen Widget Tests - should display success message +00:14 +83: C:/flutter_projects/tracking_app/test/features/auth/presentation/apply/view/apply_screen_test.dart: ApplySuccessScreen Widget Tests - should display success message +00:14 +84: C:/flutter_projects/tracking_app/test/features/auth/presentation/apply/view/apply_screen_test.dart: ApplySuccessScreen Widget Tests - should display success message +00:14 +85: C:/flutter_projects/tracking_app/test/features/auth/presentation/apply/view/apply_screen_test.dart: ApplySuccessScreen Widget Tests - should display success message +[≡ƒîÄ Easy Localization] [DEBUG] Start +[≡ƒîÄ Easy Localization] [DEBUG] Init state +[≡ƒîÄ Easy Localization] [DEBUG] Build +[≡ƒîÄ Easy Localization] [DEBUG] Init Localization Delegate +[≡ƒîÄ Easy Localization] [DEBUG] Init provider +[≡ƒîÄ Easy Localization] [WARNING] Localization key [applicationSubmitted] not found +[≡ƒîÄ Easy Localization] [WARNING] Localization key [congratulationsMessage] not found +[≡ƒîÄ Easy Localization] [WARNING] Localization key [reviewMessage] not found +[≡ƒîÄ Easy Localization] [WARNING] Localization key [backToLogin] not found +Found texts: [applicationSubmitted, congratulationsMessage, reviewMessage, backToLogin] +00:15 +86: C:/flutter_projects/tracking_app/test/features/auth/presentation/apply/view/apply_screen_test.dart: ApplySuccessScreen Widget Tests - should display back to login button +[≡ƒîÄ Easy Localization] [DEBUG] Start +[≡ƒîÄ Easy Localization] [DEBUG] Init state +[≡ƒîÄ Easy Localization] [DEBUG] Build +[≡ƒîÄ Easy Localization] [DEBUG] Init Localization Delegate +[≡ƒîÄ Easy Localization] [DEBUG] Init provider +[≡ƒîÄ Easy Localization] [WARNING] Localization key [applicationSubmitted] not found +[≡ƒîÄ Easy Localization] [WARNING] Localization key [congratulationsMessage] not found +[≡ƒîÄ Easy Localization] [WARNING] Localization key [reviewMessage] not found +[≡ƒîÄ Easy Localization] [WARNING] Localization key [backToLogin] not found +00:15 +87: C:/flutter_projects/tracking_app/test/features/auth/presentation/apply/view/apply_screen_test.dart: ApplySuccessScreen Widget Tests - should display success icon +[≡ƒîÄ Easy Localization] [DEBUG] Start +[≡ƒîÄ Easy Localization] [DEBUG] Init state +[≡ƒîÄ Easy Localization] [DEBUG] Build +[≡ƒîÄ Easy Localization] [DEBUG] Init Localization Delegate +[≡ƒîÄ Easy Localization] [DEBUG] Init provider +[≡ƒîÄ Easy Localization] [WARNING] Localization key [applicationSubmitted] not found +[≡ƒîÄ Easy Localization] [WARNING] Localization key [congratulationsMessage] not found +[≡ƒîÄ Easy Localization] [WARNING] Localization key [reviewMessage] not found +[≡ƒîÄ Easy Localization] [WARNING] Localization key [backToLogin] not found +00:15 +88: C:/flutter_projects/tracking_app/test/features/auth/presentation/apply/view/apply_screen_test.dart: ApplySuccessScreen Widget Tests - should navigate when back button is tapped +[≡ƒîÄ Easy Localization] [DEBUG] Start +[≡ƒîÄ Easy Localization] [DEBUG] Init state +[≡ƒîÄ Easy Localization] [DEBUG] Build +[≡ƒîÄ Easy Localization] [DEBUG] Init Localization Delegate +[≡ƒîÄ Easy Localization] [DEBUG] Init provider +[≡ƒîÄ Easy Localization] [WARNING] Localization key [applicationSubmitted] not found +[≡ƒîÄ Easy Localization] [WARNING] Localization key [congratulationsMessage] not found +[≡ƒîÄ Easy Localization] [WARNING] Localization key [reviewMessage] not found +[≡ƒîÄ Easy Localization] [WARNING] Localization key [backToLogin] not found +00:15 +89: C:/flutter_projects/tracking_app/test/features/auth/presentation/login/pages/loginScreen_test.dart: LoginScreen renders correctly +00:15 +90: C:/flutter_projects/tracking_app/test/features/auth/presentation/login/pages/loginScreen_test.dart: LoginScreen renders correctly +00:15 +91: C:/flutter_projects/tracking_app/test/features/auth/presentation/login/pages/loginScreen_test.dart: LoginScreen renders correctly +00:15 +92: C:/flutter_projects/tracking_app/test/features/auth/presentation/login/pages/loginScreen_test.dart: LoginScreen renders correctly +00:15 +93: C:/flutter_projects/tracking_app/test/features/auth/presentation/login/pages/loginScreen_test.dart: LoginScreen renders correctly +00:15 +94: C:/flutter_projects/tracking_app/test/features/auth/presentation/login/pages/loginScreen_test.dart: LoginScreen renders correctly +00:15 +95: C:/flutter_projects/tracking_app/test/features/auth/presentation/login/pages/loginScreen_test.dart: LoginScreen renders correctly +00:15 +96: C:/flutter_projects/tracking_app/test/features/auth/presentation/login/pages/loginScreen_test.dart: LoginScreen renders correctly +00:15 +97: C:/flutter_projects/tracking_app/test/features/auth/presentation/login/pages/loginScreen_test.dart: LoginScreen renders correctly +00:15 +98: C:/flutter_projects/tracking_app/test/features/auth/presentation/login/pages/loginScreen_test.dart: LoginScreen renders correctly +00:15 +99: C:/flutter_projects/tracking_app/test/features/auth/presentation/login/pages/loginScreen_test.dart: LoginScreen renders correctly +00:15 +100: C:/flutter_projects/tracking_app/test/features/auth/presentation/login/pages/loginScreen_test.dart: LoginScreen renders correctly +00:15 +101: C:/flutter_projects/tracking_app/test/features/auth/presentation/login/pages/loginScreen_test.dart: LoginScreen renders correctly +00:15 +102: C:/flutter_projects/tracking_app/test/features/auth/presentation/login/pages/loginScreen_test.dart: LoginScreen renders correctly +00:15 +102: C:/flutter_projects/tracking_app/test/features/auth/presentation/reset_password/manager/change_password_cubit_test.dart: Form Validation emits isFormValid = false when confirm password does not match +[≡ƒîÄ Easy Localization] [WARNING] Localization key [passwordsDoNotMatch] not found +00:15 +103: C:/flutter_projects/tracking_app/test/features/auth/presentation/login/pages/loginScreen_test.dart: LoginScreen renders correctly +00:15 +103: C:/flutter_projects/tracking_app/test/features/auth/presentation/reset_password/manager/change_password_cubit_test.dart: Form Validation emits isFormValid = false when any password is invalid +[≡ƒîÄ Easy Localization] [WARNING] Localization key [passwordLengthInvalid] not found +00:15 +104: C:/flutter_projects/tracking_app/test/features/auth/presentation/login/pages/loginScreen_test.dart: LoginScreen renders correctly +00:16 +104: C:/flutter_projects/tracking_app/test/features/auth/presentation/reset_password/pages/change_password_page_test.dart: (setUpAll) +[≡ƒîÄ Easy Localization] [DEBUG] Localization initialized +00:16 +104: C:/flutter_projects/tracking_app/test/features/auth/presentation/login/pages/loginScreen_test.dart: LoginScreen renders correctly +[≡ƒîÄ Easy Localization] [WARNING] Localization key [login] not found +[≡ƒîÄ Easy Localization] [WARNING] Localization key [email] not found +[≡ƒîÄ Easy Localization] [WARNING] Localization key [enterEmail] not found +[≡ƒîÄ Easy Localization] [WARNING] Localization key [password] not found +[≡ƒîÄ Easy Localization] [WARNING] Localization key [enterPassword] not found +[≡ƒîÄ Easy Localization] [WARNING] Localization key [rememberMe] not found +[≡ƒîÄ Easy Localization] [WARNING] Localization key [forgotPasswordTitle] not found +[≡ƒîÄ Easy Localization] [WARNING] Localization key [login] not found +00:16 +105: C:/flutter_projects/tracking_app/test/features/auth/presentation/reset_password/pages/change_password_page_test.dart: renders all password fields +00:17 +105: C:/flutter_projects/tracking_app/test/features/auth/presentation/login/pages/loginScreen_test.dart: Enters text into email and password fields +[≡ƒîÄ Easy Localization] [WARNING] Localization key [login] not found +[≡ƒîÄ Easy Localization] [WARNING] Localization key [email] not found +[≡ƒîÄ Easy Localization] [WARNING] Localization key [enterEmail] not found +[≡ƒîÄ Easy Localization] [WARNING] Localization key [password] not found +[≡ƒîÄ Easy Localization] [WARNING] Localization key [enterPassword] not found +[≡ƒîÄ Easy Localization] [WARNING] Localization key [rememberMe] not found +[≡ƒîÄ Easy Localization] [WARNING] Localization key [forgotPasswordTitle] not found +[≡ƒîÄ Easy Localization] [WARNING] Localization key [login] not found +00:17 +106: C:/flutter_projects/tracking_app/test/features/auth/presentation/reset_password/pages/change_password_page_test.dart: renders all password fields +00:17 +106: C:/flutter_projects/tracking_app/test/features/Onboarding/presentation/pages/onboardingScreen_test.dart: (setUpAll) +[≡ƒîÄ Easy Localization] [DEBUG] Localization initialized +00:17 +106: C:/flutter_projects/tracking_app/test/features/auth/presentation/reset_password/pages/change_password_page_test.dart: renders all password fields +[≡ƒîÄ Easy Localization] [DEBUG] Start +[≡ƒîÄ Easy Localization] [DEBUG] Init state +[≡ƒîÄ Easy Localization] [INFO] Start locale loaded en +[≡ƒîÄ Easy Localization] [DEBUG] Build +[≡ƒîÄ Easy Localization] [DEBUG] Init Localization Delegate +[≡ƒîÄ Easy Localization] [DEBUG] Init provider +00:18 +107: C:/flutter_projects/tracking_app/test/features/auth/presentation/reset_password/pages/change_password_page_test.dart: renders all password fields +[≡ƒîÄ Easy Localization] [WARNING] Localization key [resetPassword] not found +[≡ƒîÄ Easy Localization] [WARNING] Localization key [currentPassword] not found +[≡ƒîÄ Easy Localization] [WARNING] Localization key [currentPassword] not found +[≡ƒîÄ Easy Localization] [WARNING] Localization key [newPassword] not found +[≡ƒîÄ Easy Localization] [WARNING] Localization key [newPassword] not found +[≡ƒîÄ Easy Localization] [WARNING] Localization key [confirmPassword] not found +[≡ƒîÄ Easy Localization] [WARNING] Localization key [confirmPassword] not found +[≡ƒîÄ Easy Localization] [WARNING] Localization key [update] not found +[≡ƒîÄ Easy Localization] [WARNING] Localization key [currentPassword] not found +[≡ƒîÄ Easy Localization] [WARNING] Localization key [currentPassword] not found +[≡ƒîÄ Easy Localization] [WARNING] Localization key [newPassword] not found +00:18 +108: C:/flutter_projects/tracking_app/test/features/auth/presentation/reset_password/pages/change_password_page_test.dart: renders all password fields +[≡ƒîÄ Easy Localization] [WARNING] Localization key [newPassword] not found +[≡ƒîÄ Easy Localization] [WARNING] Localization key [confirmPassword] not found +[≡ƒîÄ Easy Localization] [WARNING] Localization key [confirmPassword] not found +[≡ƒîÄ Easy Localization] [WARNING] Localization key [update] not found +[≡ƒîÄ Easy Localization] [WARNING] Localization key [newPassword] not found +[≡ƒîÄ Easy Localization] [WARNING] Localization key [newPassword] not found +[≡ƒîÄ Easy Localization] [WARNING] Localization key [newPassword] not found +[≡ƒîÄ Easy Localization] [WARNING] Localization key [newPassword] not found +[≡ƒîÄ Easy Localization] [WARNING] Localization key [newPassword] not found +[≡ƒîÄ Easy Localization] [WARNING] Localization key [newPassword] not found +[≡ƒîÄ Easy Localization] [WARNING] Localization key [newPassword] not found +[≡ƒîÄ Easy Localization] [WARNING] Localization key [newPassword] not found +[≡ƒîÄ Easy Localization] [WARNING] Localization key [newPassword] not found +00:18 +109: C:/flutter_projects/tracking_app/test/features/Onboarding/presentation/pages/onboardingScreen_test.dart: Onboardingscreen Widget Test renders all UI elements correctly +00:18 +110: C:/flutter_projects/tracking_app/test/features/Onboarding/presentation/pages/onboardingScreen_test.dart: Onboardingscreen Widget Test renders all UI elements correctly +00:18 +111: C:/flutter_projects/tracking_app/test/features/Onboarding/presentation/pages/onboardingScreen_test.dart: Onboardingscreen Widget Test renders all UI elements correctly +00:18 +112: C:/flutter_projects/tracking_app/test/features/Onboarding/presentation/pages/onboardingScreen_test.dart: Onboardingscreen Widget Test renders all UI elements correctly +00:18 +112: C:/flutter_projects/tracking_app/test/features/auth/presentation/reset_password/pages/change_password_page_test.dart: Toggling visibility icon changes obscureText property +[≡ƒîÄ Easy Localization] [DEBUG] Start +[≡ƒîÄ Easy Localization] [DEBUG] Init state +[≡ƒîÄ Easy Localization] [INFO] Start locale loaded en +[≡ƒîÄ Easy Localization] [DEBUG] Build +[≡ƒîÄ Easy Localization] [DEBUG] Init Localization Delegate +[≡ƒîÄ Easy Localization] [DEBUG] Init provider +[≡ƒîÄ Easy Localization] [WARNING] Localization key [resetPassword] not found +[≡ƒîÄ Easy Localization] [WARNING] Localization key [currentPassword] not found +[≡ƒîÄ Easy Localization] [WARNING] Localization key [currentPassword] not found +[≡ƒîÄ Easy Localization] [WARNING] Localization key [newPassword] not found +[≡ƒîÄ Easy Localization] [WARNING] Localization key [newPassword] not found +[≡ƒîÄ Easy Localization] [WARNING] Localization key [confirmPassword] not found +[≡ƒîÄ Easy Localization] [WARNING] Localization key [confirmPassword] not found +[≡ƒîÄ Easy Localization] [WARNING] Localization key [update] not found +[≡ƒîÄ Easy Localization] [WARNING] Localization key [currentPassword] not found +[≡ƒîÄ Easy Localization] [WARNING] Localization key [currentPassword] not found +[≡ƒîÄ Easy Localization] [WARNING] Localization key [newPassword] not found +[≡ƒîÄ Easy Localization] [WARNING] Localization key [newPassword] not found +[≡ƒîÄ Easy Localization] [WARNING] Localization key [confirmPassword] not found +[≡ƒîÄ Easy Localization] [WARNING] Localization key [confirmPassword] not found +[≡ƒîÄ Easy Localization] [WARNING] Localization key [update] not found +[≡ƒîÄ Easy Localization] [WARNING] Localization key [currentPassword] not found +[≡ƒîÄ Easy Localization] [WARNING] Localization key [currentPassword] not found +[≡ƒîÄ Easy Localization] [WARNING] Localization key [newPassword] not found +[≡ƒîÄ Easy Localization] [WARNING] Localization key [newPassword] not found +[≡ƒîÄ Easy Localization] [WARNING] Localization key [confirmPassword] not found +[≡ƒîÄ Easy Localization] [WARNING] Localization key [confirmPassword] not found +[≡ƒîÄ Easy Localization] [WARNING] Localization key [update] not found +00:18 +113: C:/flutter_projects/tracking_app/test/features/Onboarding/presentation/pages/onboardingScreen_test.dart: Onboardingscreen Widget Test renders all UI elements correctly +00:18 +113 -1: C:/flutter_projects/tracking_app/test/features/Onboarding/presentation/pages/onboardingScreen_test.dart: Onboardingscreen Widget Test renders all UI elements correctly +00:18 +113 -1: C:/flutter_projects/tracking_app/test/features/track_order/data/models/driver_model_test.dart: DriverModel.fromFirestore creates DriverModel correctly from map [E] + Expected: <30.5> + Actual: <0.0> + + package:matcher expect + package:flutter_test/src/widget_tester.dart 473:18 expect + test\features\track_order\data\models\driver_model_test.dart 12:7 main.. + +00:18 +113 -2: C:/flutter_projects/tracking_app/test/features/Onboarding/presentation/pages/onboardingScreen_test.dart: Onboardingscreen Widget Test renders all UI elements correctly +00:18 +113 -2: C:/flutter_projects/tracking_app/test/features/track_order/data/models/driver_model_test.dart: DriverModel.fromFirestore converts int to double [E] + Expected: <30.0> + Actual: <0.0> + + package:matcher expect + package:flutter_test/src/widget_tester.dart 473:18 expect + test\features\track_order\data\models\driver_model_test.dart 21:7 main.. + +00:18 +113 -3: C:/flutter_projects/tracking_app/test/features/Onboarding/presentation/pages/onboardingScreen_test.dart: Onboardingscreen Widget Test renders all UI elements correctly +00:18 +113 -3: C:/flutter_projects/tracking_app/test/features/track_order/data/models/driver_model_test.dart: DriverModel.fromFirestore throws error if lat is missing [E] + Expected: throws + Actual: DriverModel> + Which: returned + + package:matcher expect + package:flutter_test/src/widget_tester.dart 473:18 expect + test\features\track_order\data\models\driver_model_test.dart 28:7 main.. + +00:18 +113 -3: C:/flutter_projects/tracking_app/test/features/auth/presentation/reset_password/pages/change_password_page_test.dart: Typing in text fields triggers Cubit intents +[≡ƒîÄ Easy Localization] [DEBUG] Start +[≡ƒîÄ Easy Localization] [DEBUG] Init state +[≡ƒîÄ Easy Localization] [INFO] Start locale loaded en +[≡ƒîÄ Easy Localization] [DEBUG] Build +[≡ƒîÄ Easy Localization] [DEBUG] Init Localization Delegate +[≡ƒîÄ Easy Localization] [DEBUG] Init provider +[≡ƒîÄ Easy Localization] [WARNING] Localization key [resetPassword] not found +[≡ƒîÄ Easy Localization] [WARNING] Localization key [currentPassword] not found +[≡ƒîÄ Easy Localization] [WARNING] Localization key [currentPassword] not found +[≡ƒîÄ Easy Localization] [WARNING] Localization key [newPassword] not found +[≡ƒîÄ Easy Localization] [WARNING] Localization key [newPassword] not found +[≡ƒîÄ Easy Localization] [WARNING] Localization key [confirmPassword] not found +[≡ƒîÄ Easy Localization] [WARNING] Localization key [confirmPassword] not found +[≡ƒîÄ Easy Localization] [WARNING] Localization key [update] not found +[≡ƒîÄ Easy Localization] [WARNING] Localization key [currentPassword] not found +[≡ƒîÄ Easy Localization] [WARNING] Localization key [currentPassword] not found +[≡ƒîÄ Easy Localization] [WARNING] Localization key [newPassword] not found +[≡ƒîÄ Easy Localization] [WARNING] Localization key [newPassword] not found +[≡ƒîÄ Easy Localization] [WARNING] Localization key [confirmPassword] not found +[≡ƒîÄ Easy Localization] [WARNING] Localization key [confirmPassword] not found +[≡ƒîÄ Easy Localization] [WARNING] Localization key [update] not found +00:18 +114 -3: C:/flutter_projects/tracking_app/test/features/Onboarding/presentation/pages/onboardingScreen_test.dart: Onboardingscreen Widget Test renders all UI elements correctly +00:19 +115 -3: C:/flutter_projects/tracking_app/test/features/Onboarding/presentation/pages/onboardingScreen_test.dart: Onboardingscreen Widget Test renders all UI elements correctly +00:19 +116 -3: C:/flutter_projects/tracking_app/test/features/Onboarding/presentation/pages/onboardingScreen_test.dart: Onboardingscreen Widget Test renders all UI elements correctly +00:19 +117 -3: C:/flutter_projects/tracking_app/test/features/Onboarding/presentation/pages/onboardingScreen_test.dart: Onboardingscreen Widget Test renders all UI elements correctly +00:19 +118 -3: C:/flutter_projects/tracking_app/test/features/Onboarding/presentation/pages/onboardingScreen_test.dart: Onboardingscreen Widget Test renders all UI elements correctly +[≡ƒîÄ Easy Localization] [DEBUG] Start +[≡ƒîÄ Easy Localization] [DEBUG] Init state +[≡ƒîÄ Easy Localization] [INFO] Start locale loaded en +[≡ƒîÄ Easy Localization] [DEBUG] Build +[≡ƒîÄ Easy Localization] [DEBUG] Init Localization Delegate +[≡ƒîÄ Easy Localization] [DEBUG] Init provider +[≡ƒîÄ Easy Localization] [DEBUG] Load Localization Delegate +[≡ƒîÄ Easy Localization] [DEBUG] Build +[≡ƒîÄ Easy Localization] [DEBUG] Init Localization Delegate +[≡ƒîÄ Easy Localization] [DEBUG] Init provider +00:19 +119 -3: C:/flutter_projects/tracking_app/test/features/auth/presentation/reset_password/pages/change_password_page_test.dart: Shows SnackBar on Status.success +[≡ƒîÄ Easy Localization] [WARNING] Localization key [resetPassword] not found +[≡ƒîÄ Easy Localization] [WARNING] Localization key [currentPassword] not found +[≡ƒîÄ Easy Localization] [WARNING] Localization key [currentPassword] not found +[≡ƒîÄ Easy Localization] [WARNING] Localization key [newPassword] not found +[≡ƒîÄ Easy Localization] [WARNING] Localization key [newPassword] not found +[≡ƒîÄ Easy Localization] [WARNING] Localization key [confirmPassword] not found +[≡ƒîÄ Easy Localization] [WARNING] Localization key [confirmPassword] not found +[≡ƒîÄ Easy Localization] [WARNING] Localization key [update] not found +[≡ƒîÄ Easy Localization] [WARNING] Localization key [passwordUpdated] not found +[≡ƒîÄ Easy Localization] [WARNING] Localization key [resetPassword] not found +[≡ƒîÄ Easy Localization] [WARNING] Localization key [currentPassword] not found +[≡ƒîÄ Easy Localization] [WARNING] Localization key [currentPassword] not found +[≡ƒîÄ Easy Localization] [WARNING] Localization key [newPassword] not found +[≡ƒîÄ Easy Localization] [WARNING] Localization key [newPassword] not found +[≡ƒîÄ Easy Localization] [WARNING] Localization key [confirmPassword] not found +[≡ƒîÄ Easy Localization] [WARNING] Localization key [confirmPassword] not found +[≡ƒîÄ Easy Localization] [WARNING] Localization key [update] not found +00:19 +120 -3: C:/flutter_projects/tracking_app/test/features/auth/presentation/reset_password/pages/change_password_page_test.dart: Shows Error Dialog on Status.error +00:19 +121 -3: C:/flutter_projects/tracking_app/test/features/auth/presentation/reset_password/pages/change_password_page_test.dart: Shows Error Dialog on Status.error +00:19 +122 -3: C:/flutter_projects/tracking_app/test/features/auth/presentation/reset_password/pages/change_password_page_test.dart: Shows Error Dialog on Status.error +00:19 +123 -3: C:/flutter_projects/tracking_app/test/features/auth/presentation/reset_password/pages/change_password_page_test.dart: Shows Error Dialog on Status.error +00:19 +124 -3: C:/flutter_projects/tracking_app/test/features/auth/presentation/reset_password/pages/change_password_page_test.dart: Shows Error Dialog on Status.error +00:19 +125 -3: C:/flutter_projects/tracking_app/test/features/auth/presentation/reset_password/pages/change_password_page_test.dart: Shows Error Dialog on Status.error +[≡ƒîÄ Easy Localization] [DEBUG] Start +[≡ƒîÄ Easy Localization] [DEBUG] Init state +[≡ƒîÄ Easy Localization] [INFO] Start locale loaded en +[≡ƒîÄ Easy Localization] [DEBUG] Build +[≡ƒîÄ Easy Localization] [DEBUG] Init Localization Delegate +[≡ƒîÄ Easy Localization] [DEBUG] Init provider +[≡ƒîÄ Easy Localization] [WARNING] Localization key [resetPassword] not found +[≡ƒîÄ Easy Localization] [WARNING] Localization key [currentPassword] not found +[≡ƒîÄ Easy Localization] [WARNING] Localization key [currentPassword] not found +[≡ƒîÄ Easy Localization] [WARNING] Localization key [newPassword] not found +[≡ƒîÄ Easy Localization] [WARNING] Localization key [newPassword] not found +[≡ƒîÄ Easy Localization] [WARNING] Localization key [confirmPassword] not found +[≡ƒîÄ Easy Localization] [WARNING] Localization key [confirmPassword] not found +[≡ƒîÄ Easy Localization] [WARNING] Localization key [update] not found +[≡ƒîÄ Easy Localization] [WARNING] Localization key [an_error_occurred] not found +[≡ƒîÄ Easy Localization] [WARNING] Localization key [ok] not found +[≡ƒîÄ Easy Localization] [WARNING] Localization key [currentPassword] not found +[≡ƒîÄ Easy Localization] [WARNING] Localization key [currentPassword] not found +[≡ƒîÄ Easy Localization] [WARNING] Localization key [newPassword] not found +[≡ƒîÄ Easy Localization] [WARNING] Localization key [newPassword] not found +[≡ƒîÄ Easy Localization] [WARNING] Localization key [confirmPassword] not found +[≡ƒîÄ Easy Localization] [WARNING] Localization key [confirmPassword] not found +[≡ƒîÄ Easy Localization] [WARNING] Localization key [update] not found +00:19 +126 -3: C:/flutter_projects/tracking_app/test/features/auth/presentation/reset_password/pages/change_password_page_test.dart: (tearDownAll) +00:19 +126 -3: C:/flutter_projects/tracking_app/test/features/track_order/domain/entities/driver_entity_test.dart: DriverEntity should create a DriverEntity with correct values +00:19 +127 -3: C:/flutter_projects/tracking_app/test/features/track_order/domain/entities/driver_entity_test.dart: DriverEntity should support value equality +00:20 +128 -3: C:/flutter_projects/tracking_app/test/features/track_order/domain/entities/order_entity_test.dart: OrderEntity should create an OrderEntity with all fields +00:20 +129 -3: C:/flutter_projects/tracking_app/test/features/track_order/domain/entities/order_entity_test.dart: OrderEntity should create an OrderEntity with only required fields +00:20 +130 -3: C:/flutter_projects/tracking_app/test/features/track_order/domain/usecases/driver_usecase_test.dart: TrackDriverUseCase returns SuccessApiResult with driver stream +00:20 +131 -3: C:/flutter_projects/tracking_app/test/features/track_order/domain/usecases/driver_usecase_test.dart: TrackDriverUseCase returns ErrorApiResult when repository fails +00:21 +132 -3: C:/flutter_projects/tracking_app/test/features/track_order/domain/usecases/track_order_usecase_test.dart: TrackOrderUseCase returns SuccessApiResult with orders stream +00:21 +133 -3: C:/flutter_projects/tracking_app/test/features/track_order/domain/usecases/track_order_usecase_test.dart: TrackOrderUseCase returns ErrorApiResult when repository fails +00:21 +134 -3: C:/flutter_projects/tracking_app/test/features/track_order/presentation/manager/cubit/track_order_cubit_test.dart: loadUserOrders emits error if token is null +DEBUG: loadUserOrders called with string length: null +00:21 +135 -3: C:/flutter_projects/tracking_app/test/features/track_order/presentation/manager/cubit/track_order_cubit_test.dart: loadUserOrders emits orders when SuccessApiResult is returned +DEBUG: loadUserOrders called with string length: 17 +DEBUG: Token decode error: Exception: Illegal base64url string! +DEBUG: Successfully subscribed to track orders stream +DEBUG: Stream emitted new orders list. Count: 1 +00:21 +136 -3: C:/flutter_projects/tracking_app/test/features/track_order/presentation/manager/cubit/track_order_cubit_test.dart: trackDriver emits driver when SuccessApiResult is returned +00:21 +137 -3: C:/flutter_projects/tracking_app/test/features/track_order/presentation/manager/cubit/track_order_cubit_test.dart: trackDriver emits error if stream has error +00:21 +138 -3: C:/flutter_projects/tracking_app/test/features/track_order/presentation/manager/cubit/track_order_cubit_test.dart: updateOrderStatus emits isLoading then success +00:21 +139 -3: Some tests failed. diff --git a/tests_results.txt b/tests_results.txt new file mode 100644 index 0000000..8a6303b Binary files /dev/null and b/tests_results.txt differ diff --git a/web/firebase-messaging-sw.js b/web/firebase-messaging-sw.js new file mode 100644 index 0000000..32d89e8 --- /dev/null +++ b/web/firebase-messaging-sw.js @@ -0,0 +1,25 @@ +importScripts("https://www.gstatic.com/firebasejs/8.10.0/firebase-app.js"); +importScripts("https://www.gstatic.com/firebasejs/8.10.0/firebase-messaging.js"); + +firebase.initializeApp({ + apiKey: "AIzaSyDKWdkFjeKkEAfKFrMO2svs48t2d9OqRGw", + appId: "1:725835190067:web:86225b1572d53a90e53846", + messagingSenderId: "725835190067", + projectId: "elevate-flower-app", + authDomain: "elevate-flower-app.firebaseapp.com", + storageBucket: "elevate-flower-app.firebasestorage.app" +}); + +const messaging = firebase.messaging(); + +messaging.onBackgroundMessage(function(payload) { + console.log('[firebase-messaging-sw.js] Received background message ', payload); + const notificationTitle = payload.notification.title; + const notificationOptions = { + body: payload.notification.body, + icon: '/icons/Icon-192.png' + }; + + self.registration.showNotification(notificationTitle, + notificationOptions); +}); diff --git a/windows/flutter/generated_plugin_registrant.cc b/windows/flutter/generated_plugin_registrant.cc index b762e91..7b36576 100644 --- a/windows/flutter/generated_plugin_registrant.cc +++ b/windows/flutter/generated_plugin_registrant.cc @@ -6,14 +6,20 @@ #include "generated_plugin_registrant.h" +#include #include +#include #include #include #include void RegisterPlugins(flutter::PluginRegistry* registry) { + CloudFirestorePluginCApiRegisterWithRegistrar( + registry->GetRegistrarForPlugin("CloudFirestorePluginCApi")); FileSelectorWindowsRegisterWithRegistrar( registry->GetRegistrarForPlugin("FileSelectorWindows")); + FirebaseAuthPluginCApiRegisterWithRegistrar( + registry->GetRegistrarForPlugin("FirebaseAuthPluginCApi")); FirebaseCorePluginCApiRegisterWithRegistrar( registry->GetRegistrarForPlugin("FirebaseCorePluginCApi")); GeolocatorWindowsRegisterWithRegistrar( diff --git a/windows/flutter/generated_plugins.cmake b/windows/flutter/generated_plugins.cmake index b5e0031..2a1542b 100644 --- a/windows/flutter/generated_plugins.cmake +++ b/windows/flutter/generated_plugins.cmake @@ -3,7 +3,9 @@ # list(APPEND FLUTTER_PLUGIN_LIST + cloud_firestore file_selector_windows + firebase_auth firebase_core geolocator_windows url_launcher_windows