commit 401cf600e8b2e06912d411255071ae45836d483a Author: Suriya Date: Tue Jun 23 11:29:07 2026 +0530 Initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3820a95 --- /dev/null +++ b/.gitignore @@ -0,0 +1,45 @@ +# Miscellaneous +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.build/ +.buildlog/ +.history +.svn/ +.swiftpm/ +migrate_working_dir/ + +# IntelliJ related +*.iml +*.ipr +*.iws +.idea/ + +# 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. +#.vscode/ + +# Flutter/Dart/Pub related +**/doc/api/ +**/ios/Flutter/.last_build_id +.dart_tool/ +.flutter-plugins-dependencies +.pub-cache/ +.pub/ +/build/ +/coverage/ + +# Symbolication related +app.*.symbols + +# Obfuscation related +app.*.map.json + +# Android Studio will place build artifacts here +/android/app/debug +/android/app/profile +/android/app/release diff --git a/.metadata b/.metadata new file mode 100644 index 0000000..b0f490a --- /dev/null +++ b/.metadata @@ -0,0 +1,39 @@ +# This file tracks properties of this Flutter project. +# Used by Flutter tool to assess capabilities and perform upgrades etc. +# +# This file should be version controlled and should not be manually edited. + +version: + revision: "924134a44c189315be2148659913dda1671cbe99" + channel: "stable" + +project_type: app + +# Tracks metadata for the flutter migrate command +migration: + platforms: + - platform: root + create_revision: 924134a44c189315be2148659913dda1671cbe99 + base_revision: 924134a44c189315be2148659913dda1671cbe99 + - platform: android + create_revision: 924134a44c189315be2148659913dda1671cbe99 + base_revision: 924134a44c189315be2148659913dda1671cbe99 + - platform: ios + create_revision: 924134a44c189315be2148659913dda1671cbe99 + base_revision: 924134a44c189315be2148659913dda1671cbe99 + - platform: macos + create_revision: 924134a44c189315be2148659913dda1671cbe99 + base_revision: 924134a44c189315be2148659913dda1671cbe99 + - platform: web + create_revision: 924134a44c189315be2148659913dda1671cbe99 + base_revision: 924134a44c189315be2148659913dda1671cbe99 + + # User provided section + + # List of Local paths (relative to this file) that should be + # ignored by the migrate tool. + # + # Files that are not part of the templates will be ignored by default. + unmanaged_files: + - 'lib/main.dart' + - 'ios/Runner.xcodeproj/project.pbxproj' diff --git a/README.md b/README.md new file mode 100644 index 0000000..7fb0cb9 --- /dev/null +++ b/README.md @@ -0,0 +1,63 @@ +# Doormile — EV-First Parcel Delivery (Flutter) + +A complete, fully-navigable Flutter app built from the Doormile design brief. +EV-first delivery, MileTruth AI routing narrative, corporate-red design system. + +## What's inside (all screens implemented) + +| Flow | Screens | +|------|---------| +| Onboarding | Splash → 3-slide Onboarding | +| Auth | Login (phone) → OTP verify | +| Home | Home hub (hero, quick actions, active shipment, eco banner) | +| Booking | Pickup/Drop → Parcel details & slot → Service & price → Review → Payment sheet → Success | +| Tracking | My Parcels (filters) → Live Tracking (map + rider) → Shipment Journey timeline | +| Wallet | Balance card, add money, saved methods, transactions | +| Profile | Stats, settings list, logout | +| Support | FAQ accordion + chat assistant | + +Everything is wired with real navigation and in-memory state (Provider): +placing an order adds it to **My Parcels**, paying by wallet deducts the balance, +"Add money" updates the wallet, logout returns to login, etc. + +## Design system + +- **Colours / type / spacing** mirror `DESIGN.md` (primary `#960019`, Public Sans). +- Public Sans is **bundled** (`assets/fonts/PublicSans.ttf`) — no network fonts. +- Reusable widgets in `lib/widgets/` (cards, buttons, chips, progress bars). + +## How to run + +Open the Terminal app, then (the `flutter` tool lives in Homebrew): + +```bash +export PATH="/opt/homebrew/bin:$PATH" +cd /Users/apple/Desktop/Doormile-app/doormile + +# Pick ONE target: +flutter run -d chrome # opens in Chrome (fastest, no setup) +flutter run -d macos # native macOS window +flutter run # phone/emulator if one is connected +``` + +Demo tips: at login enter **any 10-digit number**, and at OTP enter **any 6 digits** — +there's no backend, it's a front-end prototype. + +## Project layout + +``` +lib/ + main.dart # app entry, theme, named routes + theme/ # colours + text styles (design tokens) + models/ # Order, AddressEntry, WalletTransaction, ... + state/app_state.dart # Provider store + mock data + booking draft + widgets/ # shared UI (AppCard, PrimaryButton, StatusChip, logo) + screens/ # one file per screen, grouped by flow +test/ # smoke, full-journey, and per-screen render tests +``` + +## Tests + +```bash +flutter test # 18 tests: boot, full user journey, every screen renders clean +``` diff --git a/analysis_options.yaml b/analysis_options.yaml new file mode 100644 index 0000000..0d29021 --- /dev/null +++ b/analysis_options.yaml @@ -0,0 +1,28 @@ +# This file configures the analyzer, which statically analyzes Dart code to +# check for errors, warnings, and lints. +# +# The issues identified by the analyzer are surfaced in the UI of Dart-enabled +# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be +# invoked from the command line by running `flutter analyze`. + +# The following line activates a set of recommended lints for Flutter apps, +# packages, and plugins designed to encourage good coding practices. +include: package:flutter_lints/flutter.yaml + +linter: + # The lint rules applied to this project can be customized in the + # section below to disable rules from the `package:flutter_lints/flutter.yaml` + # included above or to enable additional rules. A list of all available lints + # and their documentation is published at https://dart.dev/lints. + # + # Instead of disabling a lint rule for the entire project in the + # section below, it can also be suppressed for a single line of code + # or a specific dart file by using the `// ignore: name_of_lint` and + # `// ignore_for_file: name_of_lint` syntax on the line or in the file + # producing the lint. + rules: + # avoid_print: false # Uncomment to disable the `avoid_print` rule + # prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule + +# Additional information about this file can be found at +# https://dart.dev/guides/language/analysis-options diff --git a/android/.gitignore b/android/.gitignore new file mode 100644 index 0000000..be3943c --- /dev/null +++ b/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/app/build.gradle.kts b/android/app/build.gradle.kts new file mode 100644 index 0000000..b47be3a --- /dev/null +++ b/android/app/build.gradle.kts @@ -0,0 +1,70 @@ +import java.util.Properties +import java.io.FileInputStream + +val keystorePropertiesFile = rootProject.file("key.properties") +val keystoreProperties = Properties() +if (keystorePropertiesFile.exists()) { + keystoreProperties.load(FileInputStream(keystorePropertiesFile)) +} + +plugins { + id("com.android.application") + id("org.jetbrains.kotlin.android") + // The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins. + id("dev.flutter.flutter-gradle-plugin") + id("com.google.gms.google-services") +} + +android { + namespace = "com.doormile.customer" + compileSdk = flutter.compileSdkVersion + ndkVersion = flutter.ndkVersion + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 + } + + defaultConfig { + // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). + applicationId = "com.doormile.customer" + // 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 + versionName = flutter.versionName + } + + signingConfigs { + create("release") { + keyAlias = keystoreProperties["keyAlias"] as String? + keyPassword = keystoreProperties["keyPassword"] as String? + storeFile = keystoreProperties["storeFile"]?.let { file(it as String) } + storePassword = keystoreProperties["storePassword"] as String? + } + } + + buildTypes { + release { + signingConfig = signingConfigs.getByName("release") + } + } +} + +kotlin { + compilerOptions { + jvmTarget = org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_17 + } +} + +flutter { + source = "../.." +} + +dependencies { + // Import the Firebase BoM + implementation(platform("com.google.firebase:firebase-bom:34.14.1")) + // Add the dependencies for Firebase products you want to use + implementation("com.google.firebase:firebase-analytics") +} diff --git a/android/app/google-services.json b/android/app/google-services.json new file mode 100644 index 0000000..776587f --- /dev/null +++ b/android/app/google-services.json @@ -0,0 +1,29 @@ +{ + "project_info": { + "project_number": "923472686173", + "project_id": "doormilecustomer", + "storage_bucket": "doormilecustomer.firebasestorage.app" + }, + "client": [ + { + "client_info": { + "mobilesdk_app_id": "1:923472686173:android:175b38552da45f2d4ef1b4", + "android_client_info": { + "package_name": "com.doormile.customer" + } + }, + "oauth_client": [], + "api_key": [ + { + "current_key": "AIzaSyAoxaF4fmYXU372qZkYAo7WftFqgiEg_7k" + } + ], + "services": { + "appinvite_service": { + "other_platform_oauth_client": [] + } + } + } + ], + "configuration_version": "1" +} \ No newline at end of file diff --git a/android/app/proguard-rules.pro b/android/app/proguard-rules.pro new file mode 100644 index 0000000..7ff2f16 --- /dev/null +++ b/android/app/proguard-rules.pro @@ -0,0 +1,24 @@ +# Flutter Wrapper +-keep class io.flutter.app.** { *; } +-keep class io.flutter.plugin.** { *; } +-keep class io.flutter.util.** { *; } +-keep class io.flutter.view.** { *; } +-keep class io.flutter.** { *; } +-keep class io.flutter.plugins.** { *; } + +# Firebase +-keep class com.google.firebase.** { *; } + +# Flutter Deferred Components (Play Core) +-dontwarn com.google.android.play.core.** + +# Add project specific ProGuard rules here. +# By default, the flags in this file are appended to flags specified +# in C:\sdk\tools\proguard\proguard-android.txt +# You can edit the include path and order by changing the proguardFiles +# directive in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# Add any project specific keep options here: diff --git a/android/app/src/debug/AndroidManifest.xml b/android/app/src/debug/AndroidManifest.xml new file mode 100644 index 0000000..399f698 --- /dev/null +++ b/android/app/src/debug/AndroidManifest.xml @@ -0,0 +1,7 @@ + + + + diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..6d17223 --- /dev/null +++ b/android/app/src/main/AndroidManifest.xml @@ -0,0 +1,52 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/android/app/src/main/kotlin/com/doormile/customer/MainActivity.kt b/android/app/src/main/kotlin/com/doormile/customer/MainActivity.kt new file mode 100644 index 0000000..22837d1 --- /dev/null +++ b/android/app/src/main/kotlin/com/doormile/customer/MainActivity.kt @@ -0,0 +1,5 @@ +package com.doormile.customer + +import io.flutter.embedding.android.FlutterActivity + +class MainActivity : FlutterActivity() diff --git a/android/app/src/main/res/drawable-hdpi/android12splash.png b/android/app/src/main/res/drawable-hdpi/android12splash.png new file mode 100644 index 0000000..9d7eec9 Binary files /dev/null and b/android/app/src/main/res/drawable-hdpi/android12splash.png differ diff --git a/android/app/src/main/res/drawable-hdpi/splash.png b/android/app/src/main/res/drawable-hdpi/splash.png new file mode 100644 index 0000000..9d7eec9 Binary files /dev/null and b/android/app/src/main/res/drawable-hdpi/splash.png differ diff --git a/android/app/src/main/res/drawable-mdpi/android12splash.png b/android/app/src/main/res/drawable-mdpi/android12splash.png new file mode 100644 index 0000000..9a2c0a5 Binary files /dev/null and b/android/app/src/main/res/drawable-mdpi/android12splash.png differ diff --git a/android/app/src/main/res/drawable-mdpi/splash.png b/android/app/src/main/res/drawable-mdpi/splash.png new file mode 100644 index 0000000..9a2c0a5 Binary files /dev/null and b/android/app/src/main/res/drawable-mdpi/splash.png differ diff --git a/android/app/src/main/res/drawable-night-hdpi/android12splash.png b/android/app/src/main/res/drawable-night-hdpi/android12splash.png new file mode 100644 index 0000000..9d7eec9 Binary files /dev/null and b/android/app/src/main/res/drawable-night-hdpi/android12splash.png differ diff --git a/android/app/src/main/res/drawable-night-mdpi/android12splash.png b/android/app/src/main/res/drawable-night-mdpi/android12splash.png new file mode 100644 index 0000000..9a2c0a5 Binary files /dev/null and b/android/app/src/main/res/drawable-night-mdpi/android12splash.png differ diff --git a/android/app/src/main/res/drawable-night-xhdpi/android12splash.png b/android/app/src/main/res/drawable-night-xhdpi/android12splash.png new file mode 100644 index 0000000..3e7236c Binary files /dev/null and b/android/app/src/main/res/drawable-night-xhdpi/android12splash.png differ diff --git a/android/app/src/main/res/drawable-night-xxhdpi/android12splash.png b/android/app/src/main/res/drawable-night-xxhdpi/android12splash.png new file mode 100644 index 0000000..cd98261 Binary files /dev/null and b/android/app/src/main/res/drawable-night-xxhdpi/android12splash.png differ diff --git a/android/app/src/main/res/drawable-night-xxxhdpi/android12splash.png b/android/app/src/main/res/drawable-night-xxxhdpi/android12splash.png new file mode 100644 index 0000000..c894515 Binary files /dev/null and b/android/app/src/main/res/drawable-night-xxxhdpi/android12splash.png differ diff --git a/android/app/src/main/res/drawable-v21/background.png b/android/app/src/main/res/drawable-v21/background.png new file mode 100644 index 0000000..8e21404 Binary files /dev/null and b/android/app/src/main/res/drawable-v21/background.png differ diff --git a/android/app/src/main/res/drawable-v21/launch_background.xml b/android/app/src/main/res/drawable-v21/launch_background.xml new file mode 100644 index 0000000..3cc4948 --- /dev/null +++ b/android/app/src/main/res/drawable-v21/launch_background.xml @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/android/app/src/main/res/drawable-xhdpi/android12splash.png b/android/app/src/main/res/drawable-xhdpi/android12splash.png new file mode 100644 index 0000000..3e7236c Binary files /dev/null and b/android/app/src/main/res/drawable-xhdpi/android12splash.png differ diff --git a/android/app/src/main/res/drawable-xhdpi/splash.png b/android/app/src/main/res/drawable-xhdpi/splash.png new file mode 100644 index 0000000..3e7236c Binary files /dev/null and b/android/app/src/main/res/drawable-xhdpi/splash.png differ diff --git a/android/app/src/main/res/drawable-xxhdpi/android12splash.png b/android/app/src/main/res/drawable-xxhdpi/android12splash.png new file mode 100644 index 0000000..cd98261 Binary files /dev/null and b/android/app/src/main/res/drawable-xxhdpi/android12splash.png differ diff --git a/android/app/src/main/res/drawable-xxhdpi/splash.png b/android/app/src/main/res/drawable-xxhdpi/splash.png new file mode 100644 index 0000000..cd98261 Binary files /dev/null and b/android/app/src/main/res/drawable-xxhdpi/splash.png differ diff --git a/android/app/src/main/res/drawable-xxxhdpi/android12splash.png b/android/app/src/main/res/drawable-xxxhdpi/android12splash.png new file mode 100644 index 0000000..c894515 Binary files /dev/null and b/android/app/src/main/res/drawable-xxxhdpi/android12splash.png differ diff --git a/android/app/src/main/res/drawable-xxxhdpi/splash.png b/android/app/src/main/res/drawable-xxxhdpi/splash.png new file mode 100644 index 0000000..c894515 Binary files /dev/null and b/android/app/src/main/res/drawable-xxxhdpi/splash.png differ diff --git a/android/app/src/main/res/drawable/background.png b/android/app/src/main/res/drawable/background.png new file mode 100644 index 0000000..8e21404 Binary files /dev/null and b/android/app/src/main/res/drawable/background.png differ diff --git a/android/app/src/main/res/drawable/launch_background.xml b/android/app/src/main/res/drawable/launch_background.xml new file mode 100644 index 0000000..3cc4948 --- /dev/null +++ b/android/app/src/main/res/drawable/launch_background.xml @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/android/app/src/main/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 0000000..db77bb4 Binary files /dev/null and b/android/app/src/main/res/mipmap-hdpi/ic_launcher.png differ diff --git a/android/app/src/main/res/mipmap-hdpi/launcher_icon.png b/android/app/src/main/res/mipmap-hdpi/launcher_icon.png new file mode 100644 index 0000000..c00f9f5 Binary files /dev/null and b/android/app/src/main/res/mipmap-hdpi/launcher_icon.png differ diff --git a/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/android/app/src/main/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 0000000..17987b7 Binary files /dev/null and b/android/app/src/main/res/mipmap-mdpi/ic_launcher.png differ diff --git a/android/app/src/main/res/mipmap-mdpi/launcher_icon.png b/android/app/src/main/res/mipmap-mdpi/launcher_icon.png new file mode 100644 index 0000000..37a9220 Binary files /dev/null and b/android/app/src/main/res/mipmap-mdpi/launcher_icon.png differ diff --git a/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 0000000..09d4391 Binary files /dev/null and b/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/android/app/src/main/res/mipmap-xhdpi/launcher_icon.png b/android/app/src/main/res/mipmap-xhdpi/launcher_icon.png new file mode 100644 index 0000000..910882d Binary files /dev/null and b/android/app/src/main/res/mipmap-xhdpi/launcher_icon.png differ diff --git a/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 0000000..d5f1c8d Binary files /dev/null and b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/android/app/src/main/res/mipmap-xxhdpi/launcher_icon.png b/android/app/src/main/res/mipmap-xxhdpi/launcher_icon.png new file mode 100644 index 0000000..82a9ded Binary files /dev/null and b/android/app/src/main/res/mipmap-xxhdpi/launcher_icon.png differ diff --git a/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 0000000..4d6372e Binary files /dev/null and b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/android/app/src/main/res/mipmap-xxxhdpi/launcher_icon.png b/android/app/src/main/res/mipmap-xxxhdpi/launcher_icon.png new file mode 100644 index 0000000..dc5e36c Binary files /dev/null and b/android/app/src/main/res/mipmap-xxxhdpi/launcher_icon.png differ diff --git a/android/app/src/main/res/values-night-v31/styles.xml b/android/app/src/main/res/values-night-v31/styles.xml new file mode 100644 index 0000000..2df1a97 --- /dev/null +++ b/android/app/src/main/res/values-night-v31/styles.xml @@ -0,0 +1,21 @@ + + + + + + + diff --git a/android/app/src/main/res/values-night/styles.xml b/android/app/src/main/res/values-night/styles.xml new file mode 100644 index 0000000..dbc9ea9 --- /dev/null +++ b/android/app/src/main/res/values-night/styles.xml @@ -0,0 +1,22 @@ + + + + + + + diff --git a/android/app/src/main/res/values-v31/styles.xml b/android/app/src/main/res/values-v31/styles.xml new file mode 100644 index 0000000..dbdf6f9 --- /dev/null +++ b/android/app/src/main/res/values-v31/styles.xml @@ -0,0 +1,21 @@ + + + + + + + diff --git a/android/app/src/main/res/values/styles.xml b/android/app/src/main/res/values/styles.xml new file mode 100644 index 0000000..0d1fa8f --- /dev/null +++ b/android/app/src/main/res/values/styles.xml @@ -0,0 +1,22 @@ + + + + + + + diff --git a/android/app/src/profile/AndroidManifest.xml b/android/app/src/profile/AndroidManifest.xml new file mode 100644 index 0000000..399f698 --- /dev/null +++ b/android/app/src/profile/AndroidManifest.xml @@ -0,0 +1,7 @@ + + + + diff --git a/android/build.gradle.kts b/android/build.gradle.kts new file mode 100644 index 0000000..dbee657 --- /dev/null +++ b/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/gradle.properties b/android/gradle.properties new file mode 100644 index 0000000..7e36110 --- /dev/null +++ b/android/gradle.properties @@ -0,0 +1,7 @@ +org.gradle.jvmargs=-Xmx8G -XX:MaxMetaspaceSize=4G -XX:ReservedCodeCacheSize=512m -XX:+HeapDumpOnOutOfMemoryError +android.useAndroidX=true +# This newDsl flag was added by the Flutter template +android.newDsl=false +# This builtInKotlin flag was added by the Flutter template +android.builtInKotlin=false +kotlin.incremental=false diff --git a/android/gradle/wrapper/gradle-wrapper.properties b/android/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..2d428bf --- /dev/null +++ b/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-9.1.0-all.zip diff --git a/android/settings.gradle.kts b/android/settings.gradle.kts new file mode 100644 index 0000000..27ce201 --- /dev/null +++ b/android/settings.gradle.kts @@ -0,0 +1,27 @@ +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 "9.0.1" apply false + id("org.jetbrains.kotlin.android") version "2.3.20" apply false + id("com.google.gms.google-services") version "4.4.4" apply false +} + +include(":app") diff --git a/assets/fonts/PublicSans.ttf b/assets/fonts/PublicSans.ttf new file mode 100644 index 0000000..c747e1d Binary files /dev/null and b/assets/fonts/PublicSans.ttf differ diff --git a/assets/images/CreateMpin.png b/assets/images/CreateMpin.png new file mode 100644 index 0000000..8bb3c08 Binary files /dev/null and b/assets/images/CreateMpin.png differ diff --git a/assets/images/Gps.png b/assets/images/Gps.png new file mode 100644 index 0000000..bd29205 Binary files /dev/null and b/assets/images/Gps.png differ diff --git a/assets/images/Introsplashscreen.png b/assets/images/Introsplashscreen.png new file mode 100644 index 0000000..debe563 Binary files /dev/null and b/assets/images/Introsplashscreen.png differ diff --git a/assets/images/Mpin.png b/assets/images/Mpin.png new file mode 100644 index 0000000..aa929c3 Binary files /dev/null and b/assets/images/Mpin.png differ diff --git a/assets/images/Nearle Bike.png b/assets/images/Nearle Bike.png new file mode 100644 index 0000000..8f17e40 Binary files /dev/null and b/assets/images/Nearle Bike.png differ diff --git a/assets/images/_icon_preview.png b/assets/images/_icon_preview.png new file mode 100644 index 0000000..ac86c5a Binary files /dev/null and b/assets/images/_icon_preview.png differ diff --git a/assets/images/cancel.png b/assets/images/cancel.png new file mode 100644 index 0000000..64d20aa Binary files /dev/null and b/assets/images/cancel.png differ diff --git a/assets/images/customermap.png b/assets/images/customermap.png new file mode 100644 index 0000000..72fc07c Binary files /dev/null and b/assets/images/customermap.png differ diff --git a/assets/images/deliveryicon.png b/assets/images/deliveryicon.png new file mode 100644 index 0000000..750dff5 Binary files /dev/null and b/assets/images/deliveryicon.png differ diff --git a/assets/images/doormile-logistics-launcher.png b/assets/images/doormile-logistics-launcher.png new file mode 100644 index 0000000..1bee52b Binary files /dev/null and b/assets/images/doormile-logistics-launcher.png differ diff --git a/assets/images/doormile-logo.png b/assets/images/doormile-logo.png new file mode 100644 index 0000000..8f17e40 Binary files /dev/null and b/assets/images/doormile-logo.png differ diff --git a/assets/images/gps-navigation.png b/assets/images/gps-navigation.png new file mode 100644 index 0000000..6c34078 Binary files /dev/null and b/assets/images/gps-navigation.png differ diff --git a/assets/images/homeicon.png b/assets/images/homeicon.png new file mode 100644 index 0000000..015c931 Binary files /dev/null and b/assets/images/homeicon.png differ diff --git a/assets/images/in.png b/assets/images/in.png new file mode 100644 index 0000000..a6b9737 Binary files /dev/null and b/assets/images/in.png differ diff --git a/assets/images/information-point.png b/assets/images/information-point.png new file mode 100644 index 0000000..cdb28ee Binary files /dev/null and b/assets/images/information-point.png differ diff --git a/assets/images/intro.png b/assets/images/intro.png new file mode 100644 index 0000000..d1a5bdf Binary files /dev/null and b/assets/images/intro.png differ diff --git a/assets/images/intro2.png b/assets/images/intro2.png new file mode 100644 index 0000000..c1b0549 Binary files /dev/null and b/assets/images/intro2.png differ diff --git a/assets/images/intro4.png b/assets/images/intro4.png new file mode 100644 index 0000000..a819dd4 Binary files /dev/null and b/assets/images/intro4.png differ diff --git a/assets/images/launcher_doormile.png b/assets/images/launcher_doormile.png new file mode 100644 index 0000000..475ee8f Binary files /dev/null and b/assets/images/launcher_doormile.png differ diff --git a/assets/images/launcher_icon.png b/assets/images/launcher_icon.png new file mode 100644 index 0000000..1bee52b Binary files /dev/null and b/assets/images/launcher_icon.png differ diff --git a/assets/images/map.png b/assets/images/map.png new file mode 100644 index 0000000..e741e2c Binary files /dev/null and b/assets/images/map.png differ diff --git a/assets/images/nearlelauncher.png b/assets/images/nearlelauncher.png new file mode 100644 index 0000000..1bee52b Binary files /dev/null and b/assets/images/nearlelauncher.png differ diff --git a/assets/images/nearleplaystore1.png b/assets/images/nearleplaystore1.png new file mode 100644 index 0000000..b21eaa6 Binary files /dev/null and b/assets/images/nearleplaystore1.png differ diff --git a/assets/images/nearlesplash2.png b/assets/images/nearlesplash2.png new file mode 100644 index 0000000..8f17e40 Binary files /dev/null and b/assets/images/nearlesplash2.png differ diff --git a/assets/images/onlinebottom1.png b/assets/images/onlinebottom1.png new file mode 100644 index 0000000..b51a3d5 Binary files /dev/null and b/assets/images/onlinebottom1.png differ diff --git a/assets/images/onlineoffline.png b/assets/images/onlineoffline.png new file mode 100644 index 0000000..2b8699d Binary files /dev/null and b/assets/images/onlineoffline.png differ diff --git a/assets/images/orderssample.png b/assets/images/orderssample.png new file mode 100644 index 0000000..2d7d31a Binary files /dev/null and b/assets/images/orderssample.png differ diff --git a/assets/images/pending.png b/assets/images/pending.png new file mode 100644 index 0000000..752f474 Binary files /dev/null and b/assets/images/pending.png differ diff --git a/assets/images/phone-call .png b/assets/images/phone-call .png new file mode 100644 index 0000000..eb5657a Binary files /dev/null and b/assets/images/phone-call .png differ diff --git a/assets/images/profileicon.png b/assets/images/profileicon.png new file mode 100644 index 0000000..c33407d Binary files /dev/null and b/assets/images/profileicon.png differ diff --git a/assets/images/selcart.png b/assets/images/selcart.png new file mode 100644 index 0000000..9b023b3 Binary files /dev/null and b/assets/images/selcart.png differ diff --git a/assets/images/selecteddelivery.png b/assets/images/selecteddelivery.png new file mode 100644 index 0000000..9041dc4 Binary files /dev/null and b/assets/images/selecteddelivery.png differ diff --git a/assets/images/selectedprofile.png b/assets/images/selectedprofile.png new file mode 100644 index 0000000..fe9ebb6 Binary files /dev/null and b/assets/images/selectedprofile.png differ diff --git a/assets/images/selectedsummary.png b/assets/images/selectedsummary.png new file mode 100644 index 0000000..c1e562b Binary files /dev/null and b/assets/images/selectedsummary.png differ diff --git a/assets/images/selecthome.png b/assets/images/selecthome.png new file mode 100644 index 0000000..4223161 Binary files /dev/null and b/assets/images/selecthome.png differ diff --git a/assets/images/shoppingbag.png b/assets/images/shoppingbag.png new file mode 100644 index 0000000..29a659f Binary files /dev/null and b/assets/images/shoppingbag.png differ diff --git a/assets/images/signin_banner.png b/assets/images/signin_banner.png new file mode 100644 index 0000000..9bbd24d Binary files /dev/null and b/assets/images/signin_banner.png differ diff --git a/assets/images/splashimg.png b/assets/images/splashimg.png new file mode 100644 index 0000000..8f17e40 Binary files /dev/null and b/assets/images/splashimg.png differ diff --git a/assets/images/summary.png b/assets/images/summary.png new file mode 100644 index 0000000..921d14f Binary files /dev/null and b/assets/images/summary.png differ diff --git a/assets/images/today.png b/assets/images/today.png new file mode 100644 index 0000000..7ecc08c Binary files /dev/null and b/assets/images/today.png differ diff --git a/assets/images/total.png b/assets/images/total.png new file mode 100644 index 0000000..8a3699e Binary files /dev/null and b/assets/images/total.png differ diff --git a/assets/images/totalorders.png b/assets/images/totalorders.png new file mode 100644 index 0000000..860b438 Binary files /dev/null and b/assets/images/totalorders.png differ diff --git a/assets/images/unslccart.png b/assets/images/unslccart.png new file mode 100644 index 0000000..b1a6ca0 Binary files /dev/null and b/assets/images/unslccart.png differ diff --git a/assets/images/update.png b/assets/images/update.png new file mode 100644 index 0000000..f4e2b5a Binary files /dev/null and b/assets/images/update.png differ diff --git a/assets/images/verify.png b/assets/images/verify.png new file mode 100644 index 0000000..aaab82a Binary files /dev/null and b/assets/images/verify.png differ diff --git a/assets/images/week.png b/assets/images/week.png new file mode 100644 index 0000000..9c5cccb Binary files /dev/null and b/assets/images/week.png differ diff --git a/assets/images/white.png b/assets/images/white.png new file mode 100644 index 0000000..76f6982 Binary files /dev/null and b/assets/images/white.png differ diff --git a/ios/.gitignore b/ios/.gitignore new file mode 100644 index 0000000..7a7f987 --- /dev/null +++ b/ios/.gitignore @@ -0,0 +1,34 @@ +**/dgph +*.mode1v3 +*.mode2v3 +*.moved-aside +*.pbxuser +*.perspectivev3 +**/*sync/ +.sconsign.dblite +.tags* +**/.vagrant/ +**/DerivedData/ +Icon? +**/Pods/ +**/.symlinks/ +profile +xcuserdata +**/.generated/ +Flutter/App.framework +Flutter/Flutter.framework +Flutter/Flutter.podspec +Flutter/Generated.xcconfig +Flutter/ephemeral/ +Flutter/app.flx +Flutter/app.zip +Flutter/flutter_assets/ +Flutter/flutter_export_environment.sh +ServiceDefinitions.json +Runner/GeneratedPluginRegistrant.* + +# Exceptions to above rules. +!default.mode1v3 +!default.mode2v3 +!default.pbxuser +!default.perspectivev3 diff --git a/ios/Flutter/AppFrameworkInfo.plist b/ios/Flutter/AppFrameworkInfo.plist new file mode 100644 index 0000000..391a902 --- /dev/null +++ b/ios/Flutter/AppFrameworkInfo.plist @@ -0,0 +1,24 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + App + CFBundleIdentifier + io.flutter.flutter.app + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + App + CFBundlePackageType + FMWK + CFBundleShortVersionString + 1.0 + CFBundleSignature + ???? + CFBundleVersion + 1.0 + + diff --git a/ios/Flutter/Debug.xcconfig b/ios/Flutter/Debug.xcconfig new file mode 100644 index 0000000..592ceee --- /dev/null +++ b/ios/Flutter/Debug.xcconfig @@ -0,0 +1 @@ +#include "Generated.xcconfig" diff --git a/ios/Flutter/Release.xcconfig b/ios/Flutter/Release.xcconfig new file mode 100644 index 0000000..592ceee --- /dev/null +++ b/ios/Flutter/Release.xcconfig @@ -0,0 +1 @@ +#include "Generated.xcconfig" diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj new file mode 100644 index 0000000..f3490c3 --- /dev/null +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -0,0 +1,644 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 54; + objects = { + +/* Begin PBXBuildFile section */ + 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; + 331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C807B294A618700263BE5 /* RunnerTests.swift */; }; + 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; + 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; }; + 7884E8682EC3CC0700C636F2 /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7884E8672EC3CC0400C636F2 /* SceneDelegate.swift */; }; + 78A318202AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage in Frameworks */ = {isa = PBXBuildFile; productRef = 78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */; }; + 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; + 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; + 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + 331C8085294A63A400263BE5 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 97C146E61CF9000F007C117D /* Project object */; + proxyType = 1; + remoteGlobalIDString = 97C146ED1CF9000F007C117D; + remoteInfo = Runner; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXCopyFilesBuildPhase section */ + 9705A1C41CF9048500538489 /* Embed Frameworks */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 10; + files = ( + ); + name = "Embed Frameworks"; + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXCopyFilesBuildPhase section */ + +/* Begin PBXFileReference section */ + 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; }; + 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; + 331C807B294A618700263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = ""; }; + 331C8081294A63A400263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; + 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = ""; }; + 74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + 7884E8672EC3CC0400C636F2 /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = ""; }; + 78E0A7A72DC9AD7400C4905E /* FlutterGeneratedPluginSwiftPackage */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = FlutterGeneratedPluginSwiftPackage; path = Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage; sourceTree = ""; }; + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; + 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; }; + 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = ""; }; + 97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; + 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; + 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 97C146EB1CF9000F007C117D /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 78A318202AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 331C8082294A63A400263BE5 /* RunnerTests */ = { + isa = PBXGroup; + children = ( + 331C807B294A618700263BE5 /* RunnerTests.swift */, + ); + path = RunnerTests; + sourceTree = ""; + }; + 9740EEB11CF90186004384FC /* Flutter */ = { + isa = PBXGroup; + children = ( + 78E0A7A72DC9AD7400C4905E /* FlutterGeneratedPluginSwiftPackage */, + 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */, + 9740EEB21CF90195004384FC /* Debug.xcconfig */, + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, + 9740EEB31CF90195004384FC /* Generated.xcconfig */, + ); + name = Flutter; + sourceTree = ""; + }; + 97C146E51CF9000F007C117D = { + isa = PBXGroup; + children = ( + 9740EEB11CF90186004384FC /* Flutter */, + 97C146F01CF9000F007C117D /* Runner */, + 97C146EF1CF9000F007C117D /* Products */, + 331C8082294A63A400263BE5 /* RunnerTests */, + ); + sourceTree = ""; + }; + 97C146EF1CF9000F007C117D /* Products */ = { + isa = PBXGroup; + children = ( + 97C146EE1CF9000F007C117D /* Runner.app */, + 331C8081294A63A400263BE5 /* RunnerTests.xctest */, + ); + name = Products; + sourceTree = ""; + }; + 97C146F01CF9000F007C117D /* Runner */ = { + isa = PBXGroup; + children = ( + 97C146FA1CF9000F007C117D /* Main.storyboard */, + 97C146FD1CF9000F007C117D /* Assets.xcassets */, + 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */, + 97C147021CF9000F007C117D /* Info.plist */, + 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */, + 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */, + 74858FAE1ED2DC5600515810 /* AppDelegate.swift */, + 7884E8672EC3CC0400C636F2 /* SceneDelegate.swift */, + 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */, + ); + path = Runner; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 331C8080294A63A400263BE5 /* RunnerTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */; + buildPhases = ( + 331C807D294A63A400263BE5 /* Sources */, + 331C807F294A63A400263BE5 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + 331C8086294A63A400263BE5 /* PBXTargetDependency */, + ); + name = RunnerTests; + productName = RunnerTests; + productReference = 331C8081294A63A400263BE5 /* RunnerTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; + 97C146ED1CF9000F007C117D /* Runner */ = { + isa = PBXNativeTarget; + buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; + buildPhases = ( + 9740EEB61CF901F6004384FC /* Run Script */, + 97C146EA1CF9000F007C117D /* Sources */, + 97C146EB1CF9000F007C117D /* Frameworks */, + 97C146EC1CF9000F007C117D /* Resources */, + 9705A1C41CF9048500538489 /* Embed Frameworks */, + 3B06AD1E1E4923F5004D2608 /* Thin Binary */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = Runner; + packageProductDependencies = ( + 78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */, + ); + productName = Runner; + productReference = 97C146EE1CF9000F007C117D /* Runner.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 97C146E61CF9000F007C117D /* Project object */ = { + isa = PBXProject; + attributes = { + BuildIndependentTargetsInParallel = YES; + LastUpgradeCheck = 1510; + ORGANIZATIONNAME = ""; + TargetAttributes = { + 331C8080294A63A400263BE5 = { + CreatedOnToolsVersion = 14.0; + TestTargetID = 97C146ED1CF9000F007C117D; + }; + 97C146ED1CF9000F007C117D = { + CreatedOnToolsVersion = 7.3.1; + LastSwiftMigration = 1100; + }; + }; + }; + buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */; + compatibilityVersion = "Xcode 9.3"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 97C146E51CF9000F007C117D; + packageReferences = ( + 781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage" */, + ); + productRefGroup = 97C146EF1CF9000F007C117D /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 97C146ED1CF9000F007C117D /* Runner */, + 331C8080294A63A400263BE5 /* RunnerTests */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 331C807F294A63A400263BE5 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 97C146EC1CF9000F007C117D /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */, + 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */, + 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */, + 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { + isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + "${TARGET_BUILD_DIR}/${INFOPLIST_PATH}", + ); + name = "Thin Binary"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin"; + }; + 9740EEB61CF901F6004384FC /* Run Script */ = { + isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "Run Script"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build"; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 331C807D294A63A400263BE5 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 97C146EA1CF9000F007C117D /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */, + 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */, + 7884E8682EC3CC0700C636F2 /* SceneDelegate.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + 331C8086294A63A400263BE5 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 97C146ED1CF9000F007C117D /* Runner */; + targetProxy = 331C8085294A63A400263BE5 /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin PBXVariantGroup section */ + 97C146FA1CF9000F007C117D /* Main.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 97C146FB1CF9000F007C117D /* Base */, + ); + name = Main.storyboard; + sourceTree = ""; + }; + 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 97C147001CF9000F007C117D /* Base */, + ); + name = LaunchScreen.storyboard; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + 249021D3217E4FDB00AE95B9 /* Profile */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + SUPPORTED_PLATFORMS = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + }; + name = Profile; + }; + 249021D4217E4FDB00AE95B9 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.example.doormile; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Profile; + }; + 331C8088294A63A400263BE5 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.example.doormile.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner"; + }; + name = Debug; + }; + 331C8089294A63A400263BE5 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.example.doormile.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner"; + }; + name = Release; + }; + 331C808A294A63A400263BE5 /* Profile */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.example.doormile.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner"; + }; + name = Profile; + }; + 97C147031CF9000F007C117D /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = AppIcon; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 97C147041CF9000F007C117D /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = AppIcon; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + SUPPORTED_PLATFORMS = iphoneos; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + 97C147061CF9000F007C117D /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.example.doormile; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Debug; + }; + 97C147071CF9000F007C117D /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.example.doormile; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 331C8088294A63A400263BE5 /* Debug */, + 331C8089294A63A400263BE5 /* Release */, + 331C808A294A63A400263BE5 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 97C147031CF9000F007C117D /* Debug */, + 97C147041CF9000F007C117D /* Release */, + 249021D3217E4FDB00AE95B9 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 97C147061CF9000F007C117D /* Debug */, + 97C147071CF9000F007C117D /* Release */, + 249021D4217E4FDB00AE95B9 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + +/* Begin XCLocalSwiftPackageReference section */ + 781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage" */ = { + isa = XCLocalSwiftPackageReference; + relativePath = Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage; + }; +/* End XCLocalSwiftPackageReference section */ + +/* Begin XCSwiftPackageProductDependency section */ + 78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */ = { + isa = XCSwiftPackageProductDependency; + productName = FlutterGeneratedPluginSwiftPackage; + }; +/* End XCSwiftPackageProductDependency section */ + }; + rootObject = 97C146E61CF9000F007C117D /* Project object */; +} diff --git a/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..919434a --- /dev/null +++ b/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000..18d9810 --- /dev/null +++ b/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings new file mode 100644 index 0000000..f9b0d7c --- /dev/null +++ b/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings @@ -0,0 +1,8 @@ + + + + + PreviewsEnabled + + + diff --git a/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme new file mode 100644 index 0000000..c3fedb2 --- /dev/null +++ b/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -0,0 +1,119 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ios/Runner.xcworkspace/contents.xcworkspacedata b/ios/Runner.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..1d526a1 --- /dev/null +++ b/ios/Runner.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000..18d9810 --- /dev/null +++ b/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings new file mode 100644 index 0000000..f9b0d7c --- /dev/null +++ b/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings @@ -0,0 +1,8 @@ + + + + + PreviewsEnabled + + + diff --git a/ios/Runner/AppDelegate.swift b/ios/Runner/AppDelegate.swift new file mode 100644 index 0000000..c30b367 --- /dev/null +++ b/ios/Runner/AppDelegate.swift @@ -0,0 +1,16 @@ +import Flutter +import UIKit + +@main +@objc class AppDelegate: FlutterAppDelegate, FlutterImplicitEngineDelegate { + override func application( + _ application: UIApplication, + didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? + ) -> Bool { + return super.application(application, didFinishLaunchingWithOptions: launchOptions) + } + + func didInitializeImplicitFlutterEngine(_ engineBridge: FlutterImplicitEngineBridge) { + GeneratedPluginRegistrant.register(with: engineBridge.pluginRegistry) + } +} diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000..d0d98aa --- /dev/null +++ b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1 @@ +{"images":[{"size":"20x20","idiom":"iphone","filename":"Icon-App-20x20@2x.png","scale":"2x"},{"size":"20x20","idiom":"iphone","filename":"Icon-App-20x20@3x.png","scale":"3x"},{"size":"29x29","idiom":"iphone","filename":"Icon-App-29x29@1x.png","scale":"1x"},{"size":"29x29","idiom":"iphone","filename":"Icon-App-29x29@2x.png","scale":"2x"},{"size":"29x29","idiom":"iphone","filename":"Icon-App-29x29@3x.png","scale":"3x"},{"size":"40x40","idiom":"iphone","filename":"Icon-App-40x40@2x.png","scale":"2x"},{"size":"40x40","idiom":"iphone","filename":"Icon-App-40x40@3x.png","scale":"3x"},{"size":"57x57","idiom":"iphone","filename":"Icon-App-57x57@1x.png","scale":"1x"},{"size":"57x57","idiom":"iphone","filename":"Icon-App-57x57@2x.png","scale":"2x"},{"size":"60x60","idiom":"iphone","filename":"Icon-App-60x60@2x.png","scale":"2x"},{"size":"60x60","idiom":"iphone","filename":"Icon-App-60x60@3x.png","scale":"3x"},{"size":"20x20","idiom":"ipad","filename":"Icon-App-20x20@1x.png","scale":"1x"},{"size":"20x20","idiom":"ipad","filename":"Icon-App-20x20@2x.png","scale":"2x"},{"size":"29x29","idiom":"ipad","filename":"Icon-App-29x29@1x.png","scale":"1x"},{"size":"29x29","idiom":"ipad","filename":"Icon-App-29x29@2x.png","scale":"2x"},{"size":"40x40","idiom":"ipad","filename":"Icon-App-40x40@1x.png","scale":"1x"},{"size":"40x40","idiom":"ipad","filename":"Icon-App-40x40@2x.png","scale":"2x"},{"size":"50x50","idiom":"ipad","filename":"Icon-App-50x50@1x.png","scale":"1x"},{"size":"50x50","idiom":"ipad","filename":"Icon-App-50x50@2x.png","scale":"2x"},{"size":"72x72","idiom":"ipad","filename":"Icon-App-72x72@1x.png","scale":"1x"},{"size":"72x72","idiom":"ipad","filename":"Icon-App-72x72@2x.png","scale":"2x"},{"size":"76x76","idiom":"ipad","filename":"Icon-App-76x76@1x.png","scale":"1x"},{"size":"76x76","idiom":"ipad","filename":"Icon-App-76x76@2x.png","scale":"2x"},{"size":"83.5x83.5","idiom":"ipad","filename":"Icon-App-83.5x83.5@2x.png","scale":"2x"},{"size":"1024x1024","idiom":"ios-marketing","filename":"Icon-App-1024x1024@1x.png","scale":"1x"}],"info":{"version":1,"author":"xcode"}} \ No newline at end of file diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png new file mode 100644 index 0000000..7e1d2ed Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png new file mode 100644 index 0000000..7595a7e Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png new file mode 100644 index 0000000..7738947 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png new file mode 100644 index 0000000..64124ad Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png new file mode 100644 index 0000000..b5889a4 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png new file mode 100644 index 0000000..d8f775a Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png new file mode 100644 index 0000000..5cf2162 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png new file mode 100644 index 0000000..7738947 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png new file mode 100644 index 0000000..5340d2d Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png new file mode 100644 index 0000000..f859f53 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-50x50@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-50x50@1x.png new file mode 100644 index 0000000..8af4782 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-50x50@1x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-50x50@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-50x50@2x.png new file mode 100644 index 0000000..536e923 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-50x50@2x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-57x57@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-57x57@1x.png new file mode 100644 index 0000000..ebb7ce3 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-57x57@1x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-57x57@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-57x57@2x.png new file mode 100644 index 0000000..246122f Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-57x57@2x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png new file mode 100644 index 0000000..f859f53 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png new file mode 100644 index 0000000..79add0b Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-72x72@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-72x72@1x.png new file mode 100644 index 0000000..c00f9f5 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-72x72@1x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-72x72@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-72x72@2x.png new file mode 100644 index 0000000..82a9ded Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-72x72@2x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png new file mode 100644 index 0000000..1512576 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png new file mode 100644 index 0000000..ff6cacb Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png new file mode 100644 index 0000000..9a70464 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png differ diff --git a/ios/Runner/Assets.xcassets/LaunchBackground.imageset/Contents.json b/ios/Runner/Assets.xcassets/LaunchBackground.imageset/Contents.json new file mode 100644 index 0000000..9f447e1 --- /dev/null +++ b/ios/Runner/Assets.xcassets/LaunchBackground.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "background.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/ios/Runner/Assets.xcassets/LaunchBackground.imageset/background.png b/ios/Runner/Assets.xcassets/LaunchBackground.imageset/background.png new file mode 100644 index 0000000..8e21404 Binary files /dev/null and b/ios/Runner/Assets.xcassets/LaunchBackground.imageset/background.png differ diff --git a/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json b/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json new file mode 100644 index 0000000..00cabce --- /dev/null +++ b/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "filename" : "LaunchImage.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "LaunchImage@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "LaunchImage@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png b/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png new file mode 100644 index 0000000..9a2c0a5 Binary files /dev/null and b/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png differ diff --git a/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png b/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png new file mode 100644 index 0000000..3e7236c Binary files /dev/null and b/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png differ diff --git a/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png b/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png new file mode 100644 index 0000000..cd98261 Binary files /dev/null and b/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png differ diff --git a/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md b/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md new file mode 100644 index 0000000..89c2725 --- /dev/null +++ b/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md @@ -0,0 +1,5 @@ +# Launch Screen Assets + +You can customize the launch screen with your own desired assets by replacing the image files in this directory. + +You can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images. \ No newline at end of file diff --git a/ios/Runner/Base.lproj/LaunchScreen.storyboard b/ios/Runner/Base.lproj/LaunchScreen.storyboard new file mode 100644 index 0000000..d53ffb9 --- /dev/null +++ b/ios/Runner/Base.lproj/LaunchScreen.storyboard @@ -0,0 +1,44 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ios/Runner/Base.lproj/Main.storyboard b/ios/Runner/Base.lproj/Main.storyboard new file mode 100644 index 0000000..f3c2851 --- /dev/null +++ b/ios/Runner/Base.lproj/Main.storyboard @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ios/Runner/Info.plist b/ios/Runner/Info.plist new file mode 100644 index 0000000..2c0ecc0 --- /dev/null +++ b/ios/Runner/Info.plist @@ -0,0 +1,72 @@ + + + + + CADisableMinimumFrameDurationOnPhone + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleDisplayName + Doormile + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + doormile + CFBundlePackageType + APPL + CFBundleShortVersionString + $(FLUTTER_BUILD_NAME) + CFBundleSignature + ???? + CFBundleVersion + $(FLUTTER_BUILD_NUMBER) + LSRequiresIPhoneOS + + UIApplicationSceneManifest + + UIApplicationSupportsMultipleScenes + + UISceneConfigurations + + UIWindowSceneSessionRoleApplication + + + UISceneClassName + UIWindowScene + UISceneConfigurationName + flutter + UISceneDelegateClassName + $(PRODUCT_MODULE_NAME).SceneDelegate + UISceneStoryboardFile + Main + + + + + UIApplicationSupportsIndirectInputEvents + + UILaunchStoryboardName + LaunchScreen + UIMainStoryboardFile + Main + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UIStatusBarHidden + + + diff --git a/ios/Runner/Runner-Bridging-Header.h b/ios/Runner/Runner-Bridging-Header.h new file mode 100644 index 0000000..308a2a5 --- /dev/null +++ b/ios/Runner/Runner-Bridging-Header.h @@ -0,0 +1 @@ +#import "GeneratedPluginRegistrant.h" diff --git a/ios/Runner/SceneDelegate.swift b/ios/Runner/SceneDelegate.swift new file mode 100644 index 0000000..b9ce8ea --- /dev/null +++ b/ios/Runner/SceneDelegate.swift @@ -0,0 +1,6 @@ +import Flutter +import UIKit + +class SceneDelegate: FlutterSceneDelegate { + +} diff --git a/ios/RunnerTests/RunnerTests.swift b/ios/RunnerTests/RunnerTests.swift new file mode 100644 index 0000000..86a7c3b --- /dev/null +++ b/ios/RunnerTests/RunnerTests.swift @@ -0,0 +1,12 @@ +import Flutter +import UIKit +import XCTest + +class RunnerTests: XCTestCase { + + func testExample() { + // If you add code to the Runner application, consider adding tests here. + // See https://developer.apple.com/documentation/xctest for more information about using XCTest. + } + +} diff --git a/lib/core/constants/api_constants.dart b/lib/core/constants/api_constants.dart new file mode 100644 index 0000000..30d2080 --- /dev/null +++ b/lib/core/constants/api_constants.dart @@ -0,0 +1,30 @@ +class ApiConstants { + static const String baseUrl = 'https://api.doormile.com/api/v1'; + + // ---- Auth ---- + static const String register = '$baseUrl/customer/register'; + static const String login = '$baseUrl/customer/login'; + static const String verifyPin = '$baseUrl/customer/verify-pin'; + static const String resetPin = '$baseUrl/customer/reset-pin'; + static const String logout = '$baseUrl/customer/logout'; + + // ---- Profile ---- + static const String profile = '$baseUrl/customer/profile'; + + // ---- Pickup Locations ---- + static const String locations = '$baseUrl/customer/locations'; + static String updateLocation(String id) => '$baseUrl/customer/locations/$id'; + static String deleteLocation(String id) => '$baseUrl/customer/locations/$id'; + + // ---- Pickups (Bookings) ---- + static const String createPickup = '$baseUrl/customer/bookings'; + static const String getPickups = '$baseUrl/customer/bookings'; + static String getPickupDetails(String bookingId) => '$baseUrl/customer/bookings/$bookingId'; + static String cancelPickup(String bookingId) => '$baseUrl/customer/bookings/$bookingId/cancel'; + + // ---- Pricing ---- + static const String checkPrice = '$baseUrl/pricing/check'; + + // ---- Tracking ---- + static String trackPickup(String trackingNo) => '$baseUrl/customer/track/$trackingNo'; +} diff --git a/lib/core/theme/app_colors.dart b/lib/core/theme/app_colors.dart new file mode 100644 index 0000000..0079732 --- /dev/null +++ b/lib/core/theme/app_colors.dart @@ -0,0 +1,56 @@ +import 'package:flutter/material.dart'; + +class AppColors { + AppColors._(); + + // Backgrounds & Surfaces + static const surface = Color(0xFFF7F4F2); + static const surfaceDim = Color(0xFFE8DED8); + static const surfaceContainerLowest = Color(0xFFFFFFFF); + static const surfaceContainerLow = Color(0xFFFCFAF8); + static const surfaceContainer = Color(0xFFF0E8E3); + static const surfaceContainerHigh = Color(0xFFE6DAD3); + static const surfaceContainerHighest = Color(0xFFD8C8BF); + static const surfaceVariant = Color(0xFFEDE3DD); + + // Text Colors + static const onSurface = Color(0xFF271D1B); + static const onSurfaceVariant = Color(0xFF756762); + static const outline = Color(0xFFA9958D); + static const outlineVariant = Color(0xFFD8C8BF); + static const inverseSurface = Color(0xFF271D1B); + + // Primary (brand red) + static const primary = Color(0xFFC31723); + static const primaryContainer = Color(0xFF7D1017); + static const onPrimary = Color(0xFFFFFFFF); + static const onPrimaryContainer = Color(0xFFFFEDEE); + static const primaryFixed = Color(0xFFFFE9E7); + static const primaryFixedDim = Color(0xFFFFCFCB); + + // Secondary (blue) + static const secondary = Color(0xFF126E82); + static const secondaryContainer = Color(0xFFDDF4F4); + static const secondaryFixed = Color(0xFFC6ECEC); + static const secondaryFixedDim = Color(0xFF9FDADB); + static const onSecondaryFixedVariant = Color(0xFF064A5A); + + // Tertiary (eco green) + static const tertiary = Color(0xFF16824E); + static const tertiaryContainer = Color(0xFFE7F7ED); + static const tertiaryFixed = Color(0xFFDCFCE7); + static const tertiaryFixedDim = Color(0xFFBBF7D0); + static const onTertiaryFixedVariant = Color(0xFF14532D); + + // Status / Warning + static const error = Color(0xFFDC2626); + static const errorContainer = Color(0xFFFEE2E2); + static const onErrorContainer = Color(0xFF7F1D1D); + static const warning = Color(0xFFFF7A00); // Orange + static const success = Color(0xFF16A34A); // Green + + // Journey accents (brand narrative) + static const firstMile = Color(0xFFEA580C); // orange — pickup + static const midMile = Color(0xFF0E7C7B); // teal — transit + static const lastMile = Color(0xFF1D4EDB); // blue — delivery +} diff --git a/lib/core/theme/app_theme.dart b/lib/core/theme/app_theme.dart new file mode 100644 index 0000000..a4f69b9 --- /dev/null +++ b/lib/core/theme/app_theme.dart @@ -0,0 +1,97 @@ +import 'package:flutter/material.dart'; + +import 'package:doormile/core/theme/app_colors.dart'; + +/// Central theme + reusable text styles based on the Doormile design system. +class AppTheme { + AppTheme._(); + + static const fontFamily = 'Public Sans'; + + static ThemeData get light { + final base = ThemeData.light(useMaterial3: true); + + return base.copyWith( + scaffoldBackgroundColor: AppColors.surface, + colorScheme: const ColorScheme.light( + primary: AppColors.primary, + onPrimary: AppColors.onPrimary, + primaryContainer: AppColors.primaryContainer, + onPrimaryContainer: AppColors.onPrimaryContainer, + secondary: AppColors.secondary, + onSecondary: Colors.white, + tertiary: AppColors.tertiary, + onTertiary: Colors.white, + error: AppColors.error, + surface: AppColors.surface, + onSurface: AppColors.onSurface, + surfaceContainerHighest: AppColors.surfaceContainerHighest, + outline: AppColors.outline, + outlineVariant: AppColors.outlineVariant, + ), + textTheme: base.textTheme.apply( + fontFamily: fontFamily, + bodyColor: AppColors.onSurface, + displayColor: AppColors.onSurface, + ), + appBarTheme: const AppBarTheme( + backgroundColor: Colors.transparent, + elevation: 0, + scrolledUnderElevation: 0, + foregroundColor: AppColors.primary, + centerTitle: false, + ), + inputDecorationTheme: InputDecorationTheme( + filled: true, + fillColor: AppColors.surfaceContainerLowest, + contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 15), + hintStyle: bodyMd.copyWith(color: AppColors.outline), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(16), + borderSide: const BorderSide(color: AppColors.outlineVariant), + ), + enabledBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(16), + borderSide: const BorderSide(color: AppColors.outlineVariant), + ), + focusedBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(16), + borderSide: const BorderSide(color: AppColors.primary, width: 1.4), + ), + ), + splashFactory: InkRipple.splashFactory, + ); + } + + static const _f = fontFamily; + + /// Builds a style backed by the variable font's true `wght` axis so weights + /// render at their real glyph widths (no synthetic-bold widening). + static TextStyle _ts(double size, int wght, double lineHeight, [Color? color, double? letterSpacing]) { + return TextStyle( + fontFamily: _f, + fontSize: size, + height: lineHeight / size, + letterSpacing: letterSpacing, + color: color ?? AppColors.onSurface, + fontWeight: FontWeight.values[(wght ~/ 100) - 1], + fontVariations: [FontVariation('wght', wght.toDouble())], + ); + } + + // ---- Typography helpers (mirror DESIGN.md scale) ---- + static final TextStyle headlineLg = _ts(22, 800, 28, null, -0.5); + static final TextStyle headlineMd = _ts(17, 700, 24, null, -0.3); + static final TextStyle headlineSm = _ts(15, 700, 22, null, -0.2); + static final TextStyle bodyLg = _ts(15, 400, 22); + static final TextStyle bodyMd = _ts(13, 400, 20); + static final TextStyle labelMd = _ts(13, 700, 20); + static final TextStyle caption = _ts(11, 500, 16, AppColors.onSurfaceVariant); + static final TextStyle button = _ts(14, 700, 20, null, 0.2); + + /// Ambient card shadow (Level 1 elevation from the design brief). + static List get cardShadow => const [ + BoxShadow(color: Color(0x12000000), blurRadius: 28, offset: Offset(0, 12)), + BoxShadow(color: Color(0x08C31723), blurRadius: 8, offset: Offset(0, 2)), + ]; +} diff --git a/lib/core/widgets/common.dart b/lib/core/widgets/common.dart new file mode 100644 index 0000000..c9470c2 --- /dev/null +++ b/lib/core/widgets/common.dart @@ -0,0 +1,434 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; + +import 'package:doormile/core/theme/app_colors.dart'; +import 'package:doormile/core/theme/app_theme.dart'; + +/// White rounded card with the design-system ambient shadow. +class AppCard extends StatelessWidget { + final Widget child; + final EdgeInsetsGeometry padding; + final VoidCallback? onTap; + final Color color; + final BorderRadius? radius; + final Border? border; + + const AppCard({ + super.key, + required this.child, + this.padding = const EdgeInsets.all(16), + this.onTap, + this.color = AppColors.surfaceContainerLowest, + this.radius, + this.border, + }); + + @override + Widget build(BuildContext context) { + final br = radius ?? BorderRadius.circular(18); + return Material( + color: color, + borderRadius: br, + child: InkWell( + onTap: onTap, + borderRadius: br, + child: Ink( + decoration: BoxDecoration( + color: color, + borderRadius: br, + border: border ?? Border.all(color: AppColors.surfaceContainer.withValues(alpha: 0.7)), + boxShadow: AppTheme.cardShadow, + ), + child: Padding(padding: padding, child: child), + ), + ), + ); + } +} + +/// Primary filled action button (min height 52, 12px radius). +class PrimaryButton extends StatelessWidget { + final String label; + final VoidCallback? onPressed; + final IconData? trailingIcon; + final bool loading; + final Color color; + final double height; + + const PrimaryButton({ + super.key, + required this.label, + required this.onPressed, + this.trailingIcon, + this.loading = false, + this.color = AppColors.primary, + this.height = 52, + }); + + @override + Widget build(BuildContext context) { + return SizedBox( + width: double.infinity, + height: height, + child: ElevatedButton( + onPressed: loading ? null : onPressed, + style: ElevatedButton.styleFrom( + backgroundColor: color, + foregroundColor: Colors.white, + disabledBackgroundColor: color.withValues(alpha: 0.6), + disabledForegroundColor: Colors.white, + elevation: 0, + shadowColor: AppColors.primary.withValues(alpha: 0.2), + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)), + ), + child: loading + ? const SizedBox( + width: 22, + height: 22, + child: CircularProgressIndicator(strokeWidth: 2.4, color: Colors.white), + ) + : Row( + mainAxisAlignment: MainAxisAlignment.center, + mainAxisSize: MainAxisSize.min, + children: [ + Flexible( + child: Text(label, + style: AppTheme.button, + overflow: TextOverflow.ellipsis, + maxLines: 1), + ), + if (trailingIcon != null) ...[ + const SizedBox(width: 8), + Icon(trailingIcon, size: 20), + ], + ], + ), + ), + ); + } +} + +/// Secondary tinted button (pink bg, red text). +class SecondaryButton extends StatelessWidget { + final String label; + final VoidCallback? onPressed; + const SecondaryButton({super.key, required this.label, this.onPressed}); + + @override + Widget build(BuildContext context) { + return SizedBox( + height: 44, + child: OutlinedButton( + onPressed: onPressed, + style: OutlinedButton.styleFrom( + backgroundColor: AppColors.surfaceContainerLowest, + foregroundColor: AppColors.primary, + side: const BorderSide(color: AppColors.outlineVariant), + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)), + padding: const EdgeInsets.symmetric(horizontal: 18), + ), + child: Text(label, style: AppTheme.labelMd.copyWith(color: AppColors.primary)), + ), + ); + } +} + +/// Pill-shaped status / category chip. +class StatusChip extends StatelessWidget { + final String label; + final Color color; + final Color bg; + final IconData? icon; + const StatusChip({ + super.key, + required this.label, + required this.color, + required this.bg, + this.icon, + }); + + @override + Widget build(BuildContext context) { + return Container( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 5), + decoration: BoxDecoration( + color: bg, + borderRadius: BorderRadius.circular(999), + border: Border.all(color: color.withValues(alpha: 0.12)), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + if (icon != null) ...[Icon(icon, size: 14, color: color), const SizedBox(width: 4)], + Text(label, + style: AppTheme.caption.copyWith(color: color, fontWeight: FontWeight.w600)), + ], + ), + ); + } +} + +/// A horizontal stepper progress bar with a fraction filled. +class JourneyProgressBar extends StatelessWidget { + final double value; // 0..1 + final bool gradient; + const JourneyProgressBar({super.key, required this.value, this.gradient = false}); + + @override + Widget build(BuildContext context) { + return ClipRRect( + borderRadius: BorderRadius.circular(999), + child: Stack( + children: [ + Container(height: 6, color: AppColors.surfaceContainer), + FractionallySizedBox( + widthFactor: value.clamp(0, 1), + child: Container( + height: 6, + decoration: BoxDecoration( + color: gradient ? null : AppColors.primary, + gradient: gradient + ? const LinearGradient(colors: [ + AppColors.firstMile, + AppColors.midMile, + AppColors.lastMile, + ]) + : null, + ), + ), + ), + ], + ), + ); + } +} + +/// Eco / EV reassurance banner used across screens. +class EcoBanner extends StatelessWidget { + final String text; + const EcoBanner({super.key, required this.text}); + + @override + Widget build(BuildContext context) { + return Container( + padding: const EdgeInsets.all(14), + decoration: BoxDecoration( + color: AppColors.tertiaryFixed.withValues(alpha: 0.4), + borderRadius: BorderRadius.circular(16), + border: Border.all(color: AppColors.tertiary.withValues(alpha: 0.1)), + ), + child: Row( + children: [ + const Icon(Icons.eco, color: AppColors.tertiary, size: 22), + const SizedBox(width: 12), + Expanded( + child: Text(text, + style: AppTheme.bodyMd.copyWith(color: AppColors.tertiary, height: 1.35)), + ), + ], + ), + ); + } +} + +/// Warm page backdrop with restrained route/map texture. +class AppBackground extends StatelessWidget { + final Widget child; + final EdgeInsetsGeometry padding; + const AppBackground({ + super.key, + required this.child, + this.padding = EdgeInsets.zero, + }); + + @override + Widget build(BuildContext context) { + return DecoratedBox( + decoration: const BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + colors: [Color(0xFFFFFBF8), AppColors.surface], + ), + ), + child: Stack( + children: [ + const Positioned( + top: 240, + left: -70, + child: _SoftCircle(size: 180, color: Color(0x1A126E82)), + ), + Padding(padding: padding, child: child), + ], + ), + ); + } +} + +class _SoftCircle extends StatelessWidget { + final double size; + final Color color; + const _SoftCircle({required this.size, required this.color}); + + @override + Widget build(BuildContext context) { + return Container( + width: size, + height: size, + decoration: BoxDecoration(shape: BoxShape.circle, color: color), + ); + } +} + +/// Reusable immersive red header used by primary tabs. +class ImmersiveHeader extends StatelessWidget { + final String title; + final String subtitle; + final IconData icon; + final Widget? trailing; + final bool showBack; + + const ImmersiveHeader({ + super.key, + required this.title, + required this.subtitle, + required this.icon, + this.trailing, + this.showBack = false, + }); + + @override + Widget build(BuildContext context) { + return Container( + decoration: const BoxDecoration( + gradient: LinearGradient( + colors: [AppColors.primaryContainer, AppColors.primary], + begin: Alignment.topLeft, + end: Alignment.bottomRight, + ), + borderRadius: BorderRadius.vertical(bottom: Radius.circular(24)), + ), + child: ClipRRect( + borderRadius: const BorderRadius.vertical(bottom: Radius.circular(24)), + child: AnnotatedRegion( + value: SystemUiOverlayStyle.light, + child: Stack( + children: [ + Positioned( + right: -20, + bottom: -30, + child: Icon(icon, size: 140, color: Colors.white.withValues(alpha: 0.04)), + ), + Positioned( + right: 100, + top: -10, + child: Transform.rotate( + angle: 0.2, + child: Icon(Icons.auto_awesome_rounded, size: 60, color: Colors.white.withValues(alpha: 0.03)), + ), + ), + SafeArea( + bottom: false, + child: Padding( + padding: const EdgeInsets.fromLTRB(20, 16, 20, 16), + child: Row( + children: [ + if (showBack) ...[ + IconButton( + onPressed: () => Navigator.pop(context), + icon: const Icon(Icons.arrow_back, color: Colors.white), + padding: EdgeInsets.zero, + alignment: Alignment.centerLeft, + ), + const SizedBox(width: 4), + ], + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(title, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: AppTheme.headlineLg.copyWith(color: Colors.white)), + const SizedBox(height: 4), + Text(subtitle, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: AppTheme.bodyMd.copyWith(color: Colors.white.withValues(alpha: 0.8))), + ], + ), + ), + if (trailing != null) trailing!, + ], + ), + ), + ), + ], + ), + ), + ), + ); + } +} + +/// A small circular icon avatar with a tinted background. +class IconBadge extends StatelessWidget { + final IconData icon; + final Color bg; + final Color color; + final double size; + final double iconSize; + final bool circle; + const IconBadge({ + super.key, + required this.icon, + required this.bg, + required this.color, + this.size = 48, + this.iconSize = 22, + this.circle = false, + }); + + @override + Widget build(BuildContext context) { + return Container( + width: size, + height: size, + decoration: BoxDecoration( + color: bg, + borderRadius: BorderRadius.circular(circle ? size : 12), + ), + child: Icon(icon, color: color, size: iconSize), + ); + } +} + +/// Title row with an optional trailing action ("View All"). +class SectionHeader extends StatelessWidget { + final String title; + final String? action; + final VoidCallback? onAction; + const SectionHeader({super.key, required this.title, this.action, this.onAction}); + + @override + Widget build(BuildContext context) { + return Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Flexible( + child: Text(title, + style: AppTheme.headlineSm, overflow: TextOverflow.ellipsis, maxLines: 1), + ), + if (action != null) + GestureDetector( + onTap: onAction, + child: Padding( + padding: const EdgeInsets.only(left: 8), + child: Text(action!, + style: AppTheme.labelMd.copyWith(color: AppColors.primary)), + ), + ), + ], + ); + } +} diff --git a/lib/core/widgets/logo.dart b/lib/core/widgets/logo.dart new file mode 100644 index 0000000..ef1ac09 --- /dev/null +++ b/lib/core/widgets/logo.dart @@ -0,0 +1,69 @@ +import 'package:flutter/material.dart'; + +import 'package:doormile/core/theme/app_colors.dart'; +import 'package:doormile/core/theme/app_theme.dart'; + +/// Stylised Doormile badge mark — a rounded pin holding a package glyph. +class DoormileMark extends StatelessWidget { + final double size; + final Color color; + final Color glyphColor; + const DoormileMark({ + super.key, + this.size = 48, + this.color = AppColors.primary, + this.glyphColor = Colors.white, + }); + + @override + Widget build(BuildContext context) { + return Container( + width: size, + height: size, + decoration: BoxDecoration( + color: color, + borderRadius: BorderRadius.circular(size * 0.28), + boxShadow: [ + BoxShadow( + color: color.withValues(alpha: 0.25), + blurRadius: size * 0.3, + offset: Offset(0, size * 0.12), + ), + ], + ), + child: Icon(Icons.local_shipping_rounded, color: glyphColor, size: size * 0.55), + ); + } +} + +/// Full wordmark: badge + "Doormile" text. +class DoormileWordmark extends StatelessWidget { + final double markSize; + final double fontSize; + final Color textColor; + final Color markColor; + final Color glyphColor; + const DoormileWordmark({ + super.key, + this.markSize = 40, + this.fontSize = 24, + this.textColor = AppColors.primary, + this.markColor = AppColors.primary, + this.glyphColor = Colors.white, + }); + + @override + Widget build(BuildContext context) { + return Row( + mainAxisSize: MainAxisSize.min, + children: [ + DoormileMark(size: markSize, color: markColor, glyphColor: glyphColor), + const SizedBox(width: 10), + Text( + 'Doormile', + style: AppTheme.headlineLg.copyWith(color: textColor, fontSize: fontSize), + ), + ], + ); + } +} diff --git a/lib/features/auth/screens/login_screen.dart b/lib/features/auth/screens/login_screen.dart new file mode 100644 index 0000000..d1d24bf --- /dev/null +++ b/lib/features/auth/screens/login_screen.dart @@ -0,0 +1,349 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter/gestures.dart'; +import 'package:provider/provider.dart'; + +import 'package:doormile/main.dart'; +import 'package:doormile/shared/state/app_state.dart'; +import 'package:doormile/core/theme/app_colors.dart'; +import 'package:doormile/core/theme/app_theme.dart'; +import 'package:doormile/core/widgets/common.dart'; +import 'package:doormile/core/widgets/logo.dart'; + +class LoginScreen extends StatefulWidget { + const LoginScreen({super.key}); + + @override + State createState() => _LoginScreenState(); +} + +class _LoginScreenState extends State { + final _phone = TextEditingController(); + bool _loading = false; + + bool get _valid => _phone.text.replaceAll(' ', '').length == 10; + + void _login() async { + if (!_valid) return; + FocusScope.of(context).unfocus(); + setState(() => _loading = true); + + try { + await context.read().initiateLogin(_phone.text); + if (!mounted) return; + setState(() => _loading = false); + Navigator.pushNamed(context, Routes.enterPin); + } catch (e) { + if (!mounted) return; + setState(() => _loading = false); + ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(e.toString().replaceAll('Exception: ', '')))); + } + } + + @override + void dispose() { + _phone.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final size = MediaQuery.of(context).size; + final width = size.width; + final height = size.height; + + return Scaffold( + body: Container( + width: double.infinity, + height: double.infinity, + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: [ + AppColors.primary.withValues(alpha: 0.05), + Colors.white, + AppColors.tertiary.withValues(alpha: 0.05), + ], + ), + ), + child: SafeArea( + child: SingleChildScrollView( + child: Padding( + padding: EdgeInsets.symmetric( + horizontal: width * 0.05, + vertical: height * 0.02, + ), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + SizedBox(height: height * 0.08), + const DoormileMark(size: 140), + SizedBox(height: height * 0.04), + Text( + "Sign In", + style: TextStyle( + fontSize: 32, + fontWeight: FontWeight.bold, + color: AppColors.primary, + fontFamily: AppTheme.fontFamily, + ), + ), + SizedBox(height: height * 0.02), + Text( + "Enter your mobile number to get started.", + textAlign: TextAlign.center, + style: TextStyle( + color: Colors.black54, + fontSize: 18, + fontFamily: AppTheme.fontFamily, + fontWeight: FontWeight.w500, + ), + ), + SizedBox(height: height * 0.05), + + // Phone number field + SizedBox( + height: 60, + width: width * 0.9, + child: _PhoneField( + controller: _phone, + onChanged: () { + setState(() {}); + if (_valid) { + FocusScope.of(context).unfocus(); + } + }, + ), + ), + + if (!_valid && _phone.text.isNotEmpty) + Padding( + padding: EdgeInsets.only( + left: width * 0.02, + top: height * 0.005, + ), + child: Align( + alignment: Alignment.centerLeft, + child: Text( + 'Enter a valid 10-digit mobile number', + style: TextStyle( + color: Colors.red.shade700, + fontSize: 12, + ), + ), + ), + ), + + SizedBox(height: height * 0.04), + RichText( + textAlign: TextAlign.center, + text: TextSpan( + style: const TextStyle( + fontSize: 14, + fontFamily: AppTheme.fontFamily, + fontWeight: FontWeight.w500, + color: Colors.black87, + ), + children: [ + const TextSpan(text: 'By continuing, you agree to '), + TextSpan( + text: 'T&C', + style: const TextStyle( + color: AppColors.primary, + decoration: TextDecoration.none, + ), + recognizer: TapGestureRecognizer() + ..onTap = () { + // Launch T&C + }, + ), + const TextSpan(text: ' and '), + TextSpan( + text: 'Privacy Policy', + style: const TextStyle( + color: AppColors.primary, + decoration: TextDecoration.none, + ), + recognizer: TapGestureRecognizer() + ..onTap = () { + // Launch Privacy Policy + }, + ), + ], + ), + ), + SizedBox(height: height * 0.04), + RichText( + text: TextSpan( + style: AppTheme.bodyMd.copyWith(color: AppColors.onSurfaceVariant), + children: [ + const TextSpan(text: 'New to Doormile? '), + TextSpan( + text: 'Create account', + style: AppTheme.labelMd.copyWith(color: AppColors.primary), + recognizer: TapGestureRecognizer() + ..onTap = () { + Navigator.pushNamed(context, Routes.signup); + }, + ), + ], + ), + ), + ], + ), + ), + ), + ), + ), + bottomNavigationBar: SafeArea( + child: Padding( + padding: EdgeInsets.all(width * 0.05), + child: SizedBox( + height: 56, + width: double.infinity, + child: ElevatedButton( + onPressed: _valid && !_loading ? _login : null, + style: ElevatedButton.styleFrom( + backgroundColor: _valid ? AppColors.primary : Colors.grey.shade300, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + elevation: 0, + ), + child: _loading + ? const SizedBox( + height: 24, + width: 24, + child: CircularProgressIndicator( + color: Colors.white, + strokeWidth: 3, + ), + ) + : const Text( + 'Next', + style: TextStyle( + color: Colors.white, + fontSize: 18, + fontWeight: FontWeight.bold, + ), + ), + ), + ), + ), + ), + ); + } +} + +class _PhoneField extends StatelessWidget { + final TextEditingController controller; + final VoidCallback onChanged; + const _PhoneField({required this.controller, required this.onChanged}); + + @override + Widget build(BuildContext context) { + return Container( + height: 65, + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(16), + boxShadow: [ + BoxShadow( + color: Colors.black.withValues(alpha: 0.04), + blurRadius: 20, + offset: const Offset(0, 10), + ), + ], + border: Border.all(color: Colors.grey.shade200, width: 1.5), + ), + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Row( + children: [ + // Simulated Flag/Prefix + Row( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + width: 28, + height: 20, + decoration: BoxDecoration( + color: Colors.grey.shade100, + borderRadius: BorderRadius.circular(4), + border: Border.all(color: Colors.grey.shade300), + ), + child: const Center( + child: Text('🇮🇳', style: TextStyle(fontSize: 14)), + ), + ), + const SizedBox(width: 10), + Text( + '+91', + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.w600, + fontFamily: AppTheme.fontFamily, + color: Colors.black87, + ), + ), + ], + ), + Container( + width: 1.5, + height: 32, + margin: const EdgeInsets.symmetric(horizontal: 16), + color: Colors.grey.shade200, + ), + Expanded( + child: TextField( + controller: controller, + onChanged: (_) => onChanged(), + keyboardType: TextInputType.phone, + maxLength: 11, + inputFormatters: [_PhoneFormatter()], + style: const TextStyle( + fontSize: 18, + fontWeight: FontWeight.w500, + color: Colors.black, + letterSpacing: 1, + ), + cursorColor: AppColors.primary, + decoration: InputDecoration( + counterText: '', + border: InputBorder.none, + enabledBorder: InputBorder.none, + focusedBorder: InputBorder.none, + filled: false, + hintText: 'Enter mobile number', + hintStyle: TextStyle( + color: Colors.grey.shade400, + fontSize: 16, + fontWeight: FontWeight.normal, + letterSpacing: 0, + ), + ), + ), + ), + ], + ), + ); + } +} + +/// Formats input as "XXXXX XXXXX". +class _PhoneFormatter extends TextInputFormatter { + @override + TextEditingValue formatEditUpdate(TextEditingValue oldValue, TextEditingValue newValue) { + final digits = newValue.text.replaceAll(RegExp(r'\D'), ''); + final trimmed = digits.length > 10 ? digits.substring(0, 10) : digits; + String out = trimmed; + if (trimmed.length > 5) { + out = '${trimmed.substring(0, 5)} ${trimmed.substring(5)}'; + } + return TextEditingValue( + text: out, + selection: TextSelection.collapsed(offset: out.length), + ); + } +} diff --git a/lib/features/auth/screens/onboarding_screen.dart b/lib/features/auth/screens/onboarding_screen.dart new file mode 100644 index 0000000..4efcc45 --- /dev/null +++ b/lib/features/auth/screens/onboarding_screen.dart @@ -0,0 +1,261 @@ +import 'dart:ui'; +import 'package:flutter/material.dart'; + +import 'package:doormile/main.dart'; +import 'package:doormile/core/theme/app_colors.dart'; +import 'package:doormile/core/theme/app_theme.dart'; +import 'package:doormile/core/widgets/common.dart'; +import 'package:doormile/core/widgets/logo.dart'; + +class _Slide { + final IconData icon; + final Color tint; + final String title; + final String body; + final bool ev; + const _Slide(this.icon, this.tint, this.title, this.body, {this.ev = false}); +} + +class OnboardingScreen extends StatefulWidget { + const OnboardingScreen({super.key}); + + @override + State createState() => _OnboardingScreenState(); +} + +class _OnboardingScreenState extends State { + final _controller = PageController(); + int _index = 0; + + static const _slides = [ + _Slide(Icons.touch_app_rounded, AppColors.primary, 'Send a parcel in one touch', + 'No forms, no waiting. Tap, confirm, done.'), + _Slide(Icons.map_rounded, AppColors.secondary, 'Watch every mile, live', + 'Origin to hub to your doorstep — real-time, always.'), + _Slide(Icons.electric_moped_rounded, AppColors.tertiary, 'EV-first, on time, every time', + '99% on-time, powered by MileTruth AI.', + ev: true), + ]; + + void _next() { + if (_index < _slides.length - 1) { + _controller.nextPage( + duration: const Duration(milliseconds: 350), curve: Curves.easeOut); + } else { + Navigator.pushReplacementNamed(context, Routes.login); + } + } + + @override + void dispose() { + _controller.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final isLast = _index == _slides.length - 1; + return Scaffold( + backgroundColor: AppColors.surface, + body: AppBackground( + child: SafeArea( + child: Column( + children: [ + // Header + Padding( + padding: const EdgeInsets.fromLTRB(16, 8, 16, 0), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + const DoormileMark(size: 36), + AnimatedOpacity( + opacity: isLast ? 0 : 1, + duration: const Duration(milliseconds: 200), + child: TextButton( + onPressed: isLast + ? null + : () => Navigator.pushReplacementNamed(context, Routes.login), + child: Text('Skip', + style: AppTheme.labelMd.copyWith(color: AppColors.onSurfaceVariant)), + ), + ), + ], + ), + ), + Expanded( + child: PageView.builder( + controller: _controller, + onPageChanged: (i) => setState(() => _index = i), + itemCount: _slides.length, + itemBuilder: (context, i) => _SlideView(slide: _slides[i]), + ), + ), + // Dots + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: List.generate(_slides.length, (i) { + final active = i == _index; + return AnimatedContainer( + duration: const Duration(milliseconds: 300), + margin: const EdgeInsets.symmetric(horizontal: 4), + height: 8, + width: active ? 24 : 8, + decoration: BoxDecoration( + color: active ? AppColors.primary : AppColors.outlineVariant, + borderRadius: BorderRadius.circular(999), + ), + ); + }), + ), + Padding( + padding: const EdgeInsets.fromLTRB(16, 28, 16, 24), + child: PrimaryButton( + label: isLast ? 'Get Started' : 'Next', + onPressed: _next, + ), + ), + ], + ), + ), + ), + ); + } +} + +class _SlideView extends StatelessWidget { + final _Slide slide; + const _SlideView({required this.slide}); + + @override + Widget build(BuildContext context) { + return SingleChildScrollView( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 24), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + ConstrainedBox( + constraints: const BoxConstraints(maxWidth: 340, maxHeight: 340), + child: AspectRatio( + aspectRatio: 1, + child: Container( + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: [ + slide.tint.withValues(alpha: 0.15), + slide.tint.withValues(alpha: 0.02), + ], + ), + borderRadius: BorderRadius.circular(36), + boxShadow: [ + BoxShadow( + color: slide.tint.withValues(alpha: 0.1), + blurRadius: 40, + offset: const Offset(0, 20), + ), + ], + border: Border.all(color: Colors.white.withValues(alpha: 0.8), width: 2), + ), + child: Stack( + clipBehavior: Clip.none, + children: [ + // Decorative abstract shapes + Positioned( + top: 20, + left: -10, + child: Container( + width: 100, + height: 100, + decoration: BoxDecoration( + color: slide.tint.withValues(alpha: 0.1), + shape: BoxShape.circle, + ), + ), + ), + Positioned( + right: -30, + bottom: -20, + child: Icon( + Icons.blur_on, + size: 160, + color: slide.tint.withValues(alpha: 0.1), + ), + ), + // Main Icon Glassmorphism Container + Center( + child: ClipRRect( + borderRadius: BorderRadius.circular(40), + child: BackdropFilter( + filter: ImageFilter.blur(sigmaX: 10, sigmaY: 10), + child: Container( + width: 160, + height: 160, + decoration: BoxDecoration( + color: Colors.white.withValues(alpha: 0.4), + borderRadius: BorderRadius.circular(40), + border: Border.all(color: Colors.white.withValues(alpha: 0.6)), + boxShadow: [ + BoxShadow( + color: Colors.black.withValues(alpha: 0.05), + blurRadius: 20, + ), + ], + ), + child: Icon(slide.icon, size: 80, color: slide.tint), + ), + ), + ), + ), + // EV badge + if (slide.ev) + Positioned( + top: 24, + right: 24, + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(20), + boxShadow: [ + BoxShadow( + color: Colors.black.withValues(alpha: 0.05), + blurRadius: 10, + offset: const Offset(0, 4), + ), + ], + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon(Icons.eco, color: AppColors.tertiary, size: 16), + const SizedBox(width: 6), + Text( + 'EV-Powered', + style: TextStyle( + color: AppColors.tertiary, + fontSize: 12, + fontWeight: FontWeight.bold, + fontFamily: AppTheme.fontFamily, + ), + ), + ], + ), + ), + ), + ], + ), + ), + ), + ), + const SizedBox(height: 40), + Text(slide.title, textAlign: TextAlign.center, style: AppTheme.headlineLg), + const SizedBox(height: 8), + Text(slide.body, + textAlign: TextAlign.center, + style: AppTheme.bodyLg.copyWith(color: AppColors.onSurfaceVariant)), + ], + ), + ); + } +} diff --git a/lib/features/auth/screens/otp_screen.dart b/lib/features/auth/screens/otp_screen.dart new file mode 100644 index 0000000..0494a27 --- /dev/null +++ b/lib/features/auth/screens/otp_screen.dart @@ -0,0 +1,247 @@ +import 'dart:async'; + +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:provider/provider.dart'; + +import 'package:doormile/main.dart'; +import 'package:doormile/shared/state/app_state.dart'; +import 'package:doormile/core/theme/app_colors.dart'; +import 'package:doormile/core/theme/app_theme.dart'; +import 'package:doormile/core/widgets/common.dart'; + +class OtpScreen extends StatefulWidget { + const OtpScreen({super.key}); + + @override + State createState() => _OtpScreenState(); +} + +class _OtpScreenState extends State { + final List _controllers = + List.generate(6, (_) => TextEditingController()); + final List _nodes = List.generate(6, (_) => FocusNode()); + Timer? _timer; + int _seconds = 28; + bool _loading = false; + + @override + void initState() { + super.initState(); + _startTimer(); + // Pre-fill demo OTP for the no-backend prototype. + WidgetsBinding.instance.addPostFrameCallback((_) => _nodes[0].requestFocus()); + } + + void _startTimer() { + _seconds = 28; + _timer?.cancel(); + _timer = Timer.periodic(const Duration(seconds: 1), (t) { + if (_seconds == 0) { + t.cancel(); + } else { + setState(() => _seconds--); + } + }); + } + + String get _code => _controllers.map((c) => c.text).join(); + + void _verify() async { + if (_loading) return; + FocusScope.of(context).unfocus(); + setState(() => _loading = true); + + await context.read().verifyOtp( + _code, + onSuccess: () { + if (!mounted) return; + final state = context.read(); + if (state.isResettingPin) { + Navigator.pushReplacementNamed(context, Routes.resetPin); + } else { + Navigator.pushReplacementNamed(context, Routes.setPin); + } + }, + onError: (err) { + if (!mounted) return; + setState(() => _loading = false); + ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(err))); + for (var c in _controllers) { + c.clear(); + } + _nodes[0].requestFocus(); + }, + ); + } + + void _resendOtp() async { + setState(() => _loading = true); + await context.read().sendOtp( + context.read().phone, + onSuccess: () { + if (!mounted) return; + setState(() => _loading = false); + _startTimer(); + ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('OTP Resent'))); + }, + onError: (err) { + if (!mounted) return; + setState(() => _loading = false); + ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(err))); + }, + ); + } + + void _onChanged(int i, String v) { + if (v.length > 1) { + final chars = v.replaceAll(RegExp(r'[^0-9]'), '').split(''); + for (int j = 0; j < 6; j++) { + _controllers[j].text = j < chars.length ? chars[j] : ''; + } + _nodes[5].requestFocus(); + setState(() {}); + if (_code.length == 6) _verify(); + return; + } + if (v.isNotEmpty && i < 5) _nodes[i + 1].requestFocus(); + if (v.isEmpty && i > 0) _nodes[i - 1].requestFocus(); + setState(() {}); + if (_code.length == 6) _verify(); + } + + @override + void dispose() { + _timer?.cancel(); + for (final c in _controllers) { + c.dispose(); + } + for (final n in _nodes) { + n.dispose(); + } + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final phone = context.watch().phone; + final filled = _code.length == 6; + return Scaffold( + backgroundColor: AppColors.surface, + appBar: AppBar( + leading: BackButton(color: AppColors.primary, onPressed: () => Navigator.pop(context)), + ), + body: SafeArea( + child: Center( + child: SingleChildScrollView( + padding: const EdgeInsets.symmetric(horizontal: 20), + child: AppCard( + padding: const EdgeInsets.all(28), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + width: 88, + height: 88, + decoration: const BoxDecoration( + color: AppColors.primaryFixed, shape: BoxShape.circle), + child: const Icon(Icons.sms_rounded, size: 40, color: AppColors.primary), + ), + const SizedBox(height: 24), + Text('Verify your number', + textAlign: TextAlign.center, style: AppTheme.headlineLg), + const SizedBox(height: 8), + Text.rich( + TextSpan( + style: AppTheme.bodyMd.copyWith(color: AppColors.onSurfaceVariant), + children: [ + const TextSpan(text: 'We sent a 6-digit code to '), + TextSpan( + text: '+91 ${phone.isEmpty ? '98111 22334' : phone}', + style: AppTheme.labelMd, + ), + ], + ), + textAlign: TextAlign.center, + ), + const SizedBox(height: 28), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: List.generate(6, (i) => _OtpBox( + controller: _controllers[i], + node: _nodes[i], + onChanged: (v) => _onChanged(i, v), + )), + ), + const SizedBox(height: 28), + if (_seconds > 0) + Text('Resend code in 0:${_seconds.toString().padLeft(2, '0')}', + style: AppTheme.labelMd.copyWith(color: AppColors.onSurfaceVariant)) + else + TextButton( + onPressed: _resendOtp, + child: Text('Resend code now', + style: AppTheme.labelMd + .copyWith(color: AppColors.primary, fontWeight: FontWeight.w700)), + ), + const SizedBox(height: 24), + PrimaryButton( + label: 'Verify & Continue', + trailingIcon: Icons.arrow_forward, + loading: _loading, + onPressed: filled ? _verify : null, + ), + const SizedBox(height: 8), + Text('Tip: enter any 6 digits to continue', + style: AppTheme.caption.copyWith(color: AppColors.outline)), + ], + ), + ), + ), + ), + ), + ); + } +} + +class _OtpBox extends StatelessWidget { + final TextEditingController controller; + final FocusNode node; + final ValueChanged onChanged; + const _OtpBox({required this.controller, required this.node, required this.onChanged}); + + @override + Widget build(BuildContext context) { + final filled = controller.text.isNotEmpty; + return SizedBox( + width: 46, + height: 56, + child: TextField( + controller: controller, + focusNode: node, + onChanged: onChanged, + textAlign: TextAlign.center, + keyboardType: TextInputType.number, + maxLength: 6, + autofillHints: const [AutofillHints.oneTimeCode], + cursorColor: AppColors.primary, + style: AppTheme.headlineMd, + inputFormatters: [FilteringTextInputFormatter.digitsOnly], + decoration: InputDecoration( + counterText: '', + filled: true, + contentPadding: EdgeInsets.zero, + fillColor: filled ? AppColors.primaryFixed.withValues(alpha: 0.3) : AppColors.surface, + enabledBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + borderSide: const BorderSide(color: AppColors.outlineVariant), + ), + focusedBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + borderSide: const BorderSide(color: AppColors.primary, width: 2), + ), + ), + ), + ); + } +} diff --git a/lib/features/auth/screens/pin_screen.dart b/lib/features/auth/screens/pin_screen.dart new file mode 100644 index 0000000..96d6b2f --- /dev/null +++ b/lib/features/auth/screens/pin_screen.dart @@ -0,0 +1,316 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:provider/provider.dart'; + +import 'package:doormile/main.dart'; +import 'package:doormile/shared/state/app_state.dart'; +import 'package:doormile/core/theme/app_colors.dart'; +import 'package:doormile/core/theme/app_theme.dart'; +import 'package:doormile/core/widgets/common.dart'; + +enum PinMode { enter, set, reset } + +class PinScreen extends StatefulWidget { + final PinMode mode; + const PinScreen({super.key, required this.mode}); + + @override + State createState() => _PinScreenState(); +} + +class _PinScreenState extends State { + final _controllers = List.generate(4, (_) => TextEditingController()); + final _nodes = List.generate(4, (_) => FocusNode()); + String _code = ''; + bool _loading = false; + + @override + void initState() { + super.initState(); + WidgetsBinding.instance.addPostFrameCallback((_) { + if (mounted) _nodes[0].requestFocus(); + }); + } + + void _onBackspaceOnEmpty(int i) { + if (i > 0) { + _controllers[i - 1].clear(); + _nodes[i - 1].requestFocus(); + _updateCode(); + } + } + + void _onChanged(int i, String v) { + if (v.length > 1) { + final chars = v.replaceAll(RegExp(r'[^0-9]'), '').split(''); + for (int j = 0; j < 4; j++) { + _controllers[j].text = j < chars.length ? chars[j] : ''; + } + _nodes[3].requestFocus(); + _updateCode(); + if (_code.length == 4) _submit(); + return; + } + if (v.isNotEmpty && i < 3) _nodes[i + 1].requestFocus(); + _updateCode(); + if (_code.length == 4) _submit(); + } + + void _updateCode() { + setState(() { + _code = _controllers.map((c) => c.text).join(); + }); + } + + void _submit() async { + FocusScope.of(context).unfocus(); + setState(() => _loading = true); + final state = context.read(); + + try { + if (widget.mode == PinMode.set) { + await state.registerWithPin(_code); + } else if (widget.mode == PinMode.reset) { + await state.resetPinWithBackend(_code); + } else { + await state.verifyPinWithBackend(_code); + } + + if (!mounted) return; + Navigator.pushNamedAndRemoveUntil(context, Routes.home, (r) => false); + } catch (e) { + if (!mounted) return; + setState(() => _loading = false); + ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(e.toString().replaceAll('Exception: ', '')))); + for (var c in _controllers) c.clear(); + _nodes[0].requestFocus(); + _updateCode(); + } + } + + void _forgotPin() async { + setState(() => _loading = true); + await context.read().sendOtp( + context.read().phone, + resetPin: true, + onSuccess: () { + if (!mounted) return; + setState(() => _loading = false); + Navigator.pushNamed(context, Routes.otp); + }, + onError: (err) { + if (!mounted) return; + setState(() => _loading = false); + ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(err))); + }, + ); + } + + @override + Widget build(BuildContext context) { + String title = 'Enter PIN'; + String subtitle = 'Enter your 4-digit security PIN to log in.'; + if (widget.mode == PinMode.set) { + title = 'Set PIN'; + subtitle = 'Create a 4-digit security PIN for your account.'; + } else if (widget.mode == PinMode.reset) { + title = 'Set New PIN'; + subtitle = 'Create a new 4-digit security PIN.'; + } + + return Scaffold( + body: Container( + width: double.infinity, + height: double.infinity, + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topRight, + end: Alignment.bottomLeft, + colors: [ + AppColors.tertiary.withValues(alpha: 0.05), + Colors.white, + AppColors.primary.withValues(alpha: 0.05), + ], + ), + ), + child: Stack( + children: [ + SafeArea( + child: SingleChildScrollView( + padding: const EdgeInsets.symmetric(horizontal: 24), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + SizedBox(height: MediaQuery.of(context).size.height * 0.12), + Container( + width: 100, + height: 100, + decoration: BoxDecoration( + color: AppColors.primary.withValues(alpha: 0.1), + shape: BoxShape.circle, + ), + child: const Icon(Icons.lock_outline, size: 48, color: AppColors.primary), + ), + const SizedBox(height: 32), + Text( + title, + style: TextStyle( + fontSize: 32, + fontWeight: FontWeight.bold, + color: AppColors.primary, + fontFamily: AppTheme.fontFamily, + ), + ), + const SizedBox(height: 12), + Text( + subtitle, + textAlign: TextAlign.center, + style: const TextStyle( + fontSize: 18, + color: Colors.black54, + fontFamily: AppTheme.fontFamily, + ), + ), + const SizedBox(height: 48), + Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: List.generate( + 4, + (i) => _PinBox( + controller: _controllers[i], + node: _nodes[i], + onChanged: (v) => _onChanged(i, v), + onBackspaceOnEmpty: () => _onBackspaceOnEmpty(i), + ), + ), + ), + const SizedBox(height: 24), + if (widget.mode == PinMode.enter) + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + const SizedBox(), // Empty space for left side + InkWell( + onTap: _forgotPin, + child: const Text( + "Forget Pin?", + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + color: AppColors.primary, + fontFamily: AppTheme.fontFamily, + decoration: TextDecoration.underline, + ), + ), + ), + ], + ), + if (_loading) ...[ + const SizedBox(height: 32), + const CircularProgressIndicator(color: AppColors.primary), + ], + ], + ), + ), + ), + + // Back Button + Positioned( + top: MediaQuery.of(context).padding.top + 16, + left: 16, + child: InkWell( + onTap: () => Navigator.pop(context), + borderRadius: BorderRadius.circular(30), + child: Container( + padding: const EdgeInsets.all(10), + decoration: const BoxDecoration( + color: Colors.black12, + shape: BoxShape.circle, + ), + child: const Icon( + Icons.arrow_back, + color: Colors.black, + size: 24, + ), + ), + ), + ), + ], + ), + ), + ); + } +} + +class _PinBox extends StatelessWidget { + final TextEditingController controller; + final FocusNode node; + final ValueChanged onChanged; + final VoidCallback onBackspaceOnEmpty; + + const _PinBox({ + required this.controller, + required this.node, + required this.onChanged, + required this.onBackspaceOnEmpty, + }); + + @override + Widget build(BuildContext context) { + final filled = controller.text.isNotEmpty; + + return Container( + width: 58, + height: 64, + decoration: BoxDecoration( + color: filled ? AppColors.primary : Colors.white, + borderRadius: BorderRadius.circular(16), + boxShadow: [ + BoxShadow( + color: filled ? AppColors.primary.withValues(alpha: 0.3) : Colors.black.withValues(alpha: 0.04), + blurRadius: filled ? 12 : 20, + offset: const Offset(0, 8), + ), + ], + border: Border.all( + color: filled ? AppColors.primary : Colors.grey.shade200, + width: 1.5, + ), + ), + child: RawKeyboardListener( + focusNode: FocusNode(), + onKey: (event) { + if (event is RawKeyDownEvent && + event.logicalKey == LogicalKeyboardKey.backspace && + controller.text.isEmpty) { + onBackspaceOnEmpty(); + } + }, + child: TextField( + controller: controller, + focusNode: node, + onChanged: onChanged, + textAlign: TextAlign.center, + textAlignVertical: TextAlignVertical.center, + obscureText: true, + obscuringCharacter: '●', + keyboardType: TextInputType.number, + maxLength: 1, + style: TextStyle( + fontSize: 28, + fontWeight: FontWeight.bold, + color: filled ? Colors.white : AppColors.primary, + ), + decoration: const InputDecoration( + counterText: "", + contentPadding: EdgeInsets.zero, + border: InputBorder.none, + enabledBorder: InputBorder.none, + focusedBorder: InputBorder.none, + filled: false, + ), + ), + ), + ); + } +} diff --git a/lib/features/auth/screens/signup_screen.dart b/lib/features/auth/screens/signup_screen.dart new file mode 100644 index 0000000..aa0121f --- /dev/null +++ b/lib/features/auth/screens/signup_screen.dart @@ -0,0 +1,274 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:provider/provider.dart'; + +import 'package:doormile/main.dart'; +import 'package:doormile/shared/state/app_state.dart'; +import 'package:doormile/core/theme/app_colors.dart'; +import 'package:doormile/core/theme/app_theme.dart'; +import 'package:doormile/core/widgets/common.dart'; +import 'package:doormile/core/widgets/logo.dart'; + +class SignupScreen extends StatefulWidget { + const SignupScreen({super.key}); + + @override + State createState() => _SignupScreenState(); +} + +class _SignupScreenState extends State { + final _name = TextEditingController(); + final _email = TextEditingController(); + final _phone = TextEditingController(); + bool _loading = false; + + bool get _valid => + _name.text.trim().isNotEmpty && + _email.text.trim().isNotEmpty && + _phone.text.replaceAll(' ', '').length == 10; + + void _sendOtp() async { + if (!_valid) return; + FocusScope.of(context).unfocus(); + setState(() => _loading = true); + + await context.read().sendOtp( + _phone.text, + firstName: _name.text.trim(), + email: _email.text.trim(), + onSuccess: () { + if (!mounted) return; + setState(() => _loading = false); + Navigator.pushNamed(context, Routes.otp); + }, + onError: (err) { + if (!mounted) return; + setState(() => _loading = false); + ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(err))); + }, + ); + } + + @override + void dispose() { + _name.dispose(); + _email.dispose(); + _phone.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: AppColors.surface, + appBar: AppBar( + leading: BackButton(color: AppColors.primary, onPressed: () => Navigator.pop(context)), + ), + body: SafeArea( + child: Column( + children: [ + Expanded( + child: SingleChildScrollView( + padding: const EdgeInsets.symmetric(horizontal: 24), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const DoormileMark(size: 64), + const SizedBox(height: 32), + Text('Create Account', style: AppTheme.headlineLg), + const SizedBox(height: 6), + Text('Join Doormile for eco-friendly deliveries', + style: AppTheme.bodyMd.copyWith(color: AppColors.onSurfaceVariant)), + const SizedBox(height: 40), + + // Name Field + Align( + alignment: Alignment.centerLeft, + child: Padding( + padding: const EdgeInsets.only(left: 4, bottom: 6), + child: Text('Full Name', + style: AppTheme.labelMd + .copyWith(color: AppColors.onSurfaceVariant)), + ), + ), + _CustomTextField( + controller: _name, + hint: 'e.g. Rahul Sharma', + icon: Icons.person_outline, + onChanged: () => setState(() {}), + ), + const SizedBox(height: 20), + + // Email Field + Align( + alignment: Alignment.centerLeft, + child: Padding( + padding: const EdgeInsets.only(left: 4, bottom: 6), + child: Text('Email Address', + style: AppTheme.labelMd + .copyWith(color: AppColors.onSurfaceVariant)), + ), + ), + _CustomTextField( + controller: _email, + hint: 'e.g. rahul@example.com', + icon: Icons.email_outlined, + keyboardType: TextInputType.emailAddress, + onChanged: () => setState(() {}), + ), + const SizedBox(height: 20), + + // Phone Field + Align( + alignment: Alignment.centerLeft, + child: Padding( + padding: const EdgeInsets.only(left: 4, bottom: 6), + child: Text('Mobile Number', + style: AppTheme.labelMd + .copyWith(color: AppColors.onSurfaceVariant)), + ), + ), + _PhoneField(controller: _phone, onChanged: () => setState(() {})), + const SizedBox(height: 36), + + PrimaryButton( + label: 'Send OTP', + loading: _loading, + onPressed: _valid ? _sendOtp : null, + ), + const SizedBox(height: 20), + ], + ), + ), + ), + ], + ), + ), + ); + } +} + +class _CustomTextField extends StatelessWidget { + final TextEditingController controller; + final String hint; + final IconData icon; + final TextInputType? keyboardType; + final VoidCallback onChanged; + + const _CustomTextField({ + required this.controller, + required this.hint, + required this.icon, + this.keyboardType, + required this.onChanged, + }); + + @override + Widget build(BuildContext context) { + return Container( + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(14), + border: Border.all(color: AppColors.outlineVariant), + ), + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Row( + children: [ + Icon(icon, size: 20, color: AppColors.onSurfaceVariant), + const SizedBox(width: 12), + Expanded( + child: TextField( + controller: controller, + onChanged: (_) => onChanged(), + keyboardType: keyboardType, + style: AppTheme.bodyLg, + cursorColor: AppColors.primary, + decoration: InputDecoration( + filled: false, + border: InputBorder.none, + focusedBorder: InputBorder.none, + enabledBorder: InputBorder.none, + errorBorder: InputBorder.none, + disabledBorder: InputBorder.none, + hintText: hint, + hintStyle: AppTheme.bodyLg.copyWith(color: AppColors.outlineVariant), + ), + ), + ), + ], + ), + ); + } +} + +class _PhoneField extends StatelessWidget { + final TextEditingController controller; + final VoidCallback onChanged; + const _PhoneField({required this.controller, required this.onChanged}); + + @override + Widget build(BuildContext context) { + return Container( + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(14), + border: Border.all(color: AppColors.outlineVariant), + ), + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Row( + children: [ + Text('+91', + style: AppTheme.bodyMd + .copyWith(color: AppColors.primary, fontWeight: FontWeight.w600)), + const Icon(Icons.expand_more, size: 20, color: AppColors.onSurfaceVariant), + Container( + width: 1, + height: 28, + margin: const EdgeInsets.symmetric(horizontal: 12), + color: AppColors.outlineVariant, + ), + Expanded( + child: TextField( + controller: controller, + onChanged: (_) => onChanged(), + keyboardType: TextInputType.phone, + maxLength: 11, + inputFormatters: [_PhoneFormatter()], + style: AppTheme.bodyLg.copyWith(letterSpacing: 1), + cursorColor: AppColors.primary, + decoration: InputDecoration( + filled: false, + counterText: '', + border: InputBorder.none, + focusedBorder: InputBorder.none, + enabledBorder: InputBorder.none, + errorBorder: InputBorder.none, + disabledBorder: InputBorder.none, + hintText: '00000 00000', + hintStyle: + AppTheme.bodyLg.copyWith(color: AppColors.outlineVariant, letterSpacing: 1), + ), + ), + ), + ], + ), + ); + } +} + +/// Formats input as "XXXXX XXXXX". +class _PhoneFormatter extends TextInputFormatter { + @override + TextEditingValue formatEditUpdate(TextEditingValue oldValue, TextEditingValue newValue) { + final digits = newValue.text.replaceAll(RegExp(r'\D'), ''); + final trimmed = digits.length > 10 ? digits.substring(0, 10) : digits; + String out = trimmed; + if (trimmed.length > 5) { + out = '${trimmed.substring(0, 5)} ${trimmed.substring(5)}'; + } + return TextEditingValue( + text: out, + selection: TextSelection.collapsed(offset: out.length), + ); + } +} diff --git a/lib/features/auth/screens/splash_screen.dart b/lib/features/auth/screens/splash_screen.dart new file mode 100644 index 0000000..be2271f --- /dev/null +++ b/lib/features/auth/screens/splash_screen.dart @@ -0,0 +1,90 @@ +import 'package:flutter/material.dart'; +import 'package:shared_preferences/shared_preferences.dart'; + +import 'package:doormile/main.dart'; +import 'package:doormile/core/theme/app_colors.dart'; +import 'package:doormile/core/theme/app_theme.dart'; +import 'package:doormile/core/widgets/logo.dart'; + +class SplashScreen extends StatefulWidget { + const SplashScreen({super.key}); + + @override + State createState() => _SplashScreenState(); +} + +class _SplashScreenState extends State with TickerProviderStateMixin { + late final AnimationController _entrance = + AnimationController(vsync: this, duration: const Duration(milliseconds: 1200))..forward(); + late final AnimationController _bar = + AnimationController(vsync: this, duration: const Duration(seconds: 3))..repeat(); + + @override + void initState() { + super.initState(); + _checkSession(); + } + + void _checkSession() async { + final prefs = await SharedPreferences.getInstance(); + final token = prefs.getString('auth_token'); + + await Future.delayed(const Duration(milliseconds: 2600)); + + if (mounted) { + if (token != null && token.isNotEmpty) { + Navigator.pushReplacementNamed(context, Routes.home); + } else { + Navigator.pushReplacementNamed(context, Routes.onboarding); + } + } + } + + @override + void dispose() { + _entrance.dispose(); + _bar.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final fade = CurvedAnimation(parent: _entrance, curve: Curves.easeOutCubic); + return Scaffold( + backgroundColor: Colors.white, + body: Stack( + fit: StackFit.expand, + children: [ + // Background Image + FadeTransition( + opacity: fade, + child: Image.asset( + 'assets/images/Introsplashscreen.png', + fit: BoxFit.cover, + ), + ), + + // Loading Indicator + Positioned( + bottom: 60, + left: 0, + right: 0, + child: Center( + child: FadeTransition( + opacity: fade, + child: const SizedBox( + width: 32, + height: 32, + child: CircularProgressIndicator( + strokeWidth: 3, + color: AppColors.primary, + ), + ), + ), + ), + ), + ], + ), + ); + } +} diff --git a/lib/features/booking/screens/booking_pickup_screen.dart b/lib/features/booking/screens/booking_pickup_screen.dart new file mode 100644 index 0000000..aba40b0 --- /dev/null +++ b/lib/features/booking/screens/booking_pickup_screen.dart @@ -0,0 +1,843 @@ +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; +import 'package:geolocator/geolocator.dart'; +import 'package:geocoding/geocoding.dart'; + +import 'package:doormile/main.dart'; +import 'package:doormile/shared/state/app_state.dart'; +import 'package:doormile/core/theme/app_colors.dart'; +import 'package:doormile/core/theme/app_theme.dart'; +import 'package:doormile/core/widgets/common.dart'; +import 'package:doormile/features/booking/screens/booking_scaffold.dart'; + +class BookingPickupScreen extends StatefulWidget { + const BookingPickupScreen({super.key}); + + @override + State createState() => _BookingPickupScreenState(); +} + +class _BookingPickupScreenState extends State { + late final TextEditingController _pickup; + late final TextEditingController _destPincode; + late final TextEditingController _name; + late final TextEditingController _mobile; + late final TextEditingController _notes; + late final _formKey = GlobalKey(); + bool _loading = false; + + String _category = ''; + String _serviceType = ''; + + @override + void initState() { + super.initState(); + final draft = context.read().draft; + _pickup = TextEditingController(text: draft.pickupAddress); + _destPincode = TextEditingController(text: draft.dropPincode); + _name = TextEditingController(text: draft.contactName); + _mobile = TextEditingController(text: draft.contactMobile); + _notes = TextEditingController(text: draft.notes); + _category = draft.parcelType; + _serviceType = draft.service; + } + + @override + void dispose() { + _pickup.dispose(); + _destPincode.dispose(); + _name.dispose(); + _mobile.dispose(); + _notes.dispose(); + super.dispose(); + } + + void _showValidationError(String title, String message) { + showModalBottomSheet( + context: context, + backgroundColor: Colors.transparent, + builder: (ctx) => Container( + padding: const EdgeInsets.all(24), + decoration: const BoxDecoration( + color: AppColors.surface, + borderRadius: BorderRadius.vertical(top: Radius.circular(28)), + ), + child: SafeArea( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + const Icon(Icons.error_outline_rounded, size: 56, color: AppColors.error), + const SizedBox(height: 16), + Text(title, style: AppTheme.headlineSm), + const SizedBox(height: 8), + Text( + message, + textAlign: TextAlign.center, + style: AppTheme.bodyLg.copyWith(color: AppColors.onSurfaceVariant), + ), + const SizedBox(height: 24), + SizedBox( + width: double.infinity, + child: FilledButton( + onPressed: () => Navigator.pop(ctx), + style: FilledButton.styleFrom( + backgroundColor: AppColors.primary, + padding: const EdgeInsets.symmetric(vertical: 16), + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)), + ), + child: const Text('Got it'), + ), + ), + ], + ), + ), + ), + ); + } + + Future _submit() async { + if (_pickup.text.trim().isEmpty) { + _showValidationError('Missing Location', 'Please enter or select a pickup address where the Miler should arrive.'); + return; + } + if (_destPincode.text.trim().length < 6) { + _showValidationError('Missing Pincode', 'Please enter the 6-digit receiver pincode so we can estimate the fare.'); + return; + } + if (_category.isEmpty) { + _showValidationError('Missing Category', 'Please select a parcel category so we can assign the right Miler.'); + return; + } + if (_serviceType.isEmpty) { + _showValidationError('Missing Service Type', 'Please select whether you need Normal or Express delivery.'); + return; + } + + final appState = context.read(); + final draft = appState.draft; + draft.pickupAddress = _pickup.text; + draft.dropPincode = _destPincode.text.trim(); + draft.contactName = _name.text; + draft.contactMobile = _mobile.text; + draft.notes = _notes.text; + draft.parcelType = _category; + draft.service = _serviceType; + + setState(() => _loading = true); + await appState.fetchPriceEstimate(); + setState(() => _loading = false); + + if (mounted) Navigator.pushNamed(context, Routes.bookingPreview); + } + + @override + Widget build(BuildContext context) { + final state = context.watch(); + return BookingScaffold( + title: 'Book a Pickup', + step: 1, + stepLabel: 'Pickup Request', + buttonLabel: 'Review Booking', + enabled: !_loading, + loading: _loading, + onContinue: _submit, + children: [ + // Pickup location card + AppCard( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Container( + width: 24, + height: 24, + decoration: BoxDecoration( + color: AppColors.success.withValues(alpha: 0.15), + borderRadius: BorderRadius.circular(999), + ), + child: const Icon(Icons.location_on, size: 14, color: AppColors.success), + ), + const SizedBox(width: 10), + Text('PICKUP LOCATION', + style: AppTheme.caption.copyWith( + letterSpacing: 1, fontWeight: FontWeight.w600)), + ], + ), + const SizedBox(height: 8), + TextField( + controller: _pickup, + style: AppTheme.headlineSm, + cursorColor: AppColors.primary, + maxLines: 2, + minLines: 1, + decoration: const InputDecoration( + isDense: true, + border: InputBorder.none, + hintText: 'Enter pickup address manually', + ), + ), + const Divider(height: 20, color: AppColors.surfaceContainer), + InkWell( + onTap: () async { + setState(() => _loading = true); + + try { + LocationPermission permission = await Geolocator.checkPermission(); + if (permission == LocationPermission.denied) { + permission = await Geolocator.requestPermission(); + if (permission == LocationPermission.denied) { + throw Exception('Location permissions are denied'); + } + } + + if (permission == LocationPermission.deniedForever) { + if (!mounted) return; + setState(() => _loading = false); + bool? open = await showDialog( + context: context, + builder: (context) => AlertDialog( + title: const Text('Permission Denied'), + content: const Text('Location permissions are permanently denied. Please enable them in app settings.'), + actions: [ + TextButton(onPressed: () => Navigator.pop(context, false), child: const Text('Cancel')), + TextButton(onPressed: () => Navigator.pop(context, true), child: const Text('Settings')), + ], + ), + ); + if (open == true) await Geolocator.openAppSettings(); + return; + } + + bool serviceEnabled = await Geolocator.isLocationServiceEnabled(); + if (!serviceEnabled) { + if (!mounted) return; + setState(() => _loading = false); + _showLocationDisabledSheet(); + return; + } + + Position position = await Geolocator.getCurrentPosition(desiredAccuracy: LocationAccuracy.high); + List placemarks = await placemarkFromCoordinates(position.latitude, position.longitude); + + if (!mounted) return; + setState(() => _loading = false); + + final state = context.read(); + if (placemarks.isNotEmpty) { + final place = placemarks.first; + final area = '${place.subLocality ?? place.locality}, ${place.administrativeArea}'; + final pincode = place.postalCode ?? ''; + final fullAddress = '${place.street}, $area, $pincode'; + _showLocationConfirmationSheet(context, state, area, pincode, fullAddress); + } else { + throw Exception('Could not determine address from coordinates.'); + } + } catch (e) { + if (!mounted) return; + setState(() => _loading = false); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text(e.toString().replaceAll('Exception: ', ''))), + ); + } + }, + child: Row( + children: [ + if (_loading) + const SizedBox(width: 18, height: 18, child: CircularProgressIndicator(strokeWidth: 2, color: AppColors.primary)) + else + const Icon(Icons.my_location, size: 18, color: AppColors.primary), + const SizedBox(width: 8), + Flexible( + child: Text(_loading ? 'Locating...' : 'Use GPS current location', + style: AppTheme.labelMd.copyWith(color: AppColors.primary), + overflow: TextOverflow.ellipsis), + ), + ], + ), + ), + const Divider(height: 20, color: AppColors.surfaceContainer), + Text('SAVED ADDRESSES', style: AppTheme.caption.copyWith(letterSpacing: 1, fontWeight: FontWeight.w600)), + const SizedBox(height: 12), + SingleChildScrollView( + scrollDirection: Axis.horizontal, + clipBehavior: Clip.none, + child: Row( + children: [ + _SavedAddressChip( + icon: Icons.home_rounded, + label: 'Home', + address: '12th Main, Indiranagar, Bengaluru, Karnataka 560038', + onSelect: (addr) => setState(() { + _pickup.text = addr; + final state = context.read(); + _name.text = state.firstName; + _mobile.text = state.phone; + }), + ), + const SizedBox(width: 12), + _SavedAddressChip( + icon: Icons.business_rounded, + label: 'Office', + address: 'Sector 4, HSR Layout, Bengaluru, Karnataka 560102', + onSelect: (addr) => setState(() { + _pickup.text = addr; + final state = context.read(); + _name.text = state.firstName; + _mobile.text = state.phone; + }), + ), + const SizedBox(width: 12), + _SavedAddressChip( + icon: Icons.favorite_rounded, + label: "Mom's House", + address: 'Jayanagar 4th Block, Bengaluru, Karnataka 560011', + onSelect: (addr) => setState(() { + _pickup.text = addr; + final state = context.read(); + _name.text = state.firstName; + _mobile.text = state.phone; + }), + ), + ], + ), + ), + ], + ), + ), + const SizedBox(height: 16), + + // Destination Pincode card + AppCard( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Container( + width: 24, + height: 24, + decoration: BoxDecoration( + color: AppColors.primary.withValues(alpha: 0.15), + borderRadius: BorderRadius.circular(999), + ), + child: const Icon(Icons.pin_drop, size: 14, color: AppColors.primary), + ), + const SizedBox(width: 10), + Text('RECEIVER PINCODE', + style: AppTheme.caption.copyWith( + letterSpacing: 1, fontWeight: FontWeight.w600)), + ], + ), + const SizedBox(height: 8), + TextField( + controller: _destPincode, + keyboardType: TextInputType.number, + maxLength: 6, + style: AppTheme.headlineSm, + cursorColor: AppColors.primary, + decoration: const InputDecoration( + isDense: true, + border: InputBorder.none, + counterText: "", + hintText: 'Enter 6-digit delivery pincode', + ), + ), + ], + ), + ), + const SizedBox(height: 16), + + // Category Card + AppCard( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text('Category', + style: AppTheme.caption.copyWith( + letterSpacing: 1, fontWeight: FontWeight.w600)), + const SizedBox(height: 12), + Column( + children: [ + Row( + children: [ + Expanded(child: _SelectionCard(width: null, icon: Icons.category, label: 'General Goods', selected: _category == 'General Goods', onTap: () => setState(() => _category = 'General Goods'))), + const SizedBox(width: 8), + Expanded(child: _SelectionCard(width: null, icon: Icons.menu_book, label: 'Books & Documents', selected: _category == 'Books & Documents', onTap: () => setState(() => _category = 'Books & Documents'))), + const SizedBox(width: 8), + Expanded(child: _SelectionCard(width: null, icon: Icons.devices, label: 'Electronics & Gadgets', selected: _category == 'Electronics & Gadgets', onTap: () => setState(() => _category = 'Electronics & Gadgets'))), + const SizedBox(width: 8), + Expanded(child: _SelectionCard(width: null, icon: Icons.checkroom, label: 'Clothing & Textiles', selected: _category == 'Clothing & Textiles', onTap: () => setState(() => _category = 'Clothing & Textiles'))), + ], + ), + const SizedBox(height: 12), + Row( + children: [ + Expanded(child: _SelectionCard(width: null, icon: Icons.wine_bar, label: 'Fragile Items', selected: _category == 'Fragile Items', onTap: () => setState(() => _category = 'Fragile Items'))), + const SizedBox(width: 8), + Expanded(child: _SelectionCard(width: null, icon: Icons.medical_services, label: 'Medical & Pharma', selected: _category == 'Medical & Pharma', onTap: () => setState(() => _category = 'Medical & Pharma'))), + const SizedBox(width: 8), + Expanded(child: _SelectionCard(width: null, icon: Icons.directions_car, label: 'Automotive Parts', selected: _category == 'Automotive Parts', onTap: () => setState(() => _category = 'Automotive Parts'))), + const SizedBox(width: 8), + Expanded(child: _SelectionCard(width: null, icon: Icons.restaurant, label: 'Food & Perishables', selected: _category == 'Food & Perishables', onTap: () => setState(() => _category = 'Food & Perishables'))), + ], + ), + ], + ), + ], + ), + ), + const SizedBox(height: 16), + + + + + + // Type Card + AppCard( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text('Service Type', + style: AppTheme.caption.copyWith( + letterSpacing: 1, fontWeight: FontWeight.w600)), + const SizedBox(height: 12), + Row( + children: [ + Expanded( + child: _ServiceCard( + title: 'Normal', + subtitle: 'Standard delivery', + icon: Icons.local_shipping_outlined, + selected: _serviceType == 'Normal', + onTap: () => setState(() => _serviceType = 'Normal'), + ), + ), + const SizedBox(width: 12), + Expanded( + child: _ServiceCard( + title: 'Express', + subtitle: 'Lightning fast', + icon: Icons.bolt, + selected: _serviceType == 'Express', + onTap: () => setState(() => _serviceType = 'Express'), + ), + ), + ], + ), + ], + ), + ), + const SizedBox(height: 16), + + // Contact Details Card + AppCard( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text('Contact Person (Optional)', + style: AppTheme.caption.copyWith( + letterSpacing: 1, fontWeight: FontWeight.w600)), + const SizedBox(height: 12), + TextField( + controller: _name, + style: AppTheme.bodyLg, + decoration: InputDecoration( + hintText: 'Name (e.g. Arun)', + isDense: true, + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + borderSide: const BorderSide(color: AppColors.outlineVariant), + ), + focusedBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + borderSide: const BorderSide(color: AppColors.primary), + ), + ), + ), + const SizedBox(height: 12), + TextField( + controller: _mobile, + keyboardType: TextInputType.phone, + style: AppTheme.bodyLg, + decoration: InputDecoration( + hintText: 'Mobile Number', + isDense: true, + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + borderSide: const BorderSide(color: AppColors.outlineVariant), + ), + focusedBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + borderSide: const BorderSide(color: AppColors.primary), + ), + ), + ), + ], + ), + ), + const SizedBox(height: 16), + + // Notes Card + AppCard( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text('Pickup Notes (Optional)', + style: AppTheme.caption.copyWith( + letterSpacing: 1, fontWeight: FontWeight.w600)), + const SizedBox(height: 12), + TextField( + controller: _notes, + maxLines: 3, + style: AppTheme.bodyLg, + decoration: InputDecoration( + hintText: 'Call before coming, Shop closes at 8 PM, etc.', + isDense: true, + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + borderSide: const BorderSide(color: AppColors.outlineVariant), + ), + focusedBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + borderSide: const BorderSide(color: AppColors.primary), + ), + ), + ), + ], + ), + ), + const SizedBox(height: 24), + + Text('RECENT & SAVED', + style: AppTheme.caption + .copyWith(letterSpacing: 1.2, fontWeight: FontWeight.w700)), + const SizedBox(height: 12), + ...state.savedAddresses.map((a) => Padding( + padding: const EdgeInsets.only(bottom: 12), + child: AppCard( + onTap: () => setState(() => _pickup.text = '${a.label}, ${a.line}'), + child: Row( + children: [ + IconBadge( + icon: a.icon, bg: a.iconBg, color: a.iconColor, circle: true), + const SizedBox(width: 14), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(a.label, style: AppTheme.labelMd), + const SizedBox(height: 2), + Text(a.line, style: AppTheme.caption), + ], + ), + ), + ], + ), + ), + )), + const SizedBox(height: 8), + const EcoBanner( + text: 'Miler will collect parcel details, weight, and pricing upon arrival.'), + ], + ); + } + + void _showLocationConfirmationSheet(BuildContext context, AppState state, String area, String pincode, String fullAddress) { + showModalBottomSheet( + context: context, + isScrollControlled: true, + backgroundColor: Colors.transparent, + builder: (context) => Container( + padding: const EdgeInsets.all(24), + decoration: const BoxDecoration( + color: AppColors.surface, + borderRadius: BorderRadius.vertical(top: Radius.circular(28)), + ), + child: SafeArea( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + height: 120, + width: double.infinity, + decoration: BoxDecoration( + color: AppColors.surfaceContainerHigh, + borderRadius: BorderRadius.circular(16), + image: const DecorationImage( + image: AssetImage('assets/images/map_placeholder.png'), // Will failover smoothly if missing + fit: BoxFit.cover, + ), + ), + child: const Center( + child: IconBadge(icon: Icons.location_on, bg: AppColors.primaryContainer, color: AppColors.primary, circle: true), + ), + ), + const SizedBox(height: 24), + Text('GPS Location Found', style: AppTheme.headlineSm), + const SizedBox(height: 8), + Text(area, style: AppTheme.labelMd.copyWith(color: AppColors.primary)), + const SizedBox(height: 4), + Text('Pincode: $pincode', style: AppTheme.caption), + const SizedBox(height: 24), + Row( + children: [ + Expanded( + child: OutlinedButton( + onPressed: () => Navigator.pop(context), + style: OutlinedButton.styleFrom( + padding: const EdgeInsets.symmetric(vertical: 16), + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)), + ), + child: const Text('Cancel'), + ), + ), + const SizedBox(width: 12), + Expanded( + child: PrimaryButton( + label: 'Confirm', + onPressed: () { + setState(() { + _pickup.text = fullAddress; + _name.text = state.firstName; + _mobile.text = state.phone; + }); + Navigator.pop(context); + }, + ), + ), + ], + ), + ], + ), + ), + ), + ); + } + + void _showLocationDisabledSheet() { + showModalBottomSheet( + context: context, + isScrollControlled: true, + backgroundColor: Colors.transparent, + builder: (ctx) => Container( + padding: const EdgeInsets.all(24), + decoration: const BoxDecoration( + color: AppColors.surface, + borderRadius: BorderRadius.vertical(top: Radius.circular(28)), + ), + child: SafeArea( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + const Icon(Icons.location_off_rounded, size: 64, color: AppColors.error), + const SizedBox(height: 16), + Text( + "Location is turned off", + style: AppTheme.headlineSm, + ), + const SizedBox(height: 8), + Text( + "Please turn on location to accurately detect your pickup address.", + textAlign: TextAlign.center, + style: AppTheme.bodyLg.copyWith(color: AppColors.onSurfaceVariant), + ), + const SizedBox(height: 24), + Row( + children: [ + Expanded( + child: OutlinedButton( + onPressed: () => Navigator.pop(ctx), + style: OutlinedButton.styleFrom( + padding: const EdgeInsets.symmetric(vertical: 16), + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)), + ), + child: const Text('Cancel'), + ), + ), + const SizedBox(width: 12), + Expanded( + child: PrimaryButton( + label: 'Turn On', + onPressed: () async { + Navigator.pop(ctx); + await Geolocator.openLocationSettings(); + }, + ), + ), + ], + ), + ], + ), + ), + ), + ); + } +} + +class _SavedAddressChip extends StatelessWidget { + final IconData icon; + final String label; + final String address; + final Function(String) onSelect; + + const _SavedAddressChip({ + required this.icon, + required this.label, + required this.address, + required this.onSelect, + }); + + @override + Widget build(BuildContext context) { + return InkWell( + onTap: () => onSelect(address), + borderRadius: BorderRadius.circular(12), + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 10), + decoration: BoxDecoration( + border: Border.all(color: AppColors.surfaceContainerHigh), + borderRadius: BorderRadius.circular(12), + color: AppColors.surface, + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon(icon, size: 16, color: AppColors.primary), + const SizedBox(width: 8), + Text(label, style: AppTheme.labelMd), + ], + ), + ), + ); + } +} + +class _SelectionCard extends StatelessWidget { + final IconData icon; + final String label; + final bool selected; + final VoidCallback onTap; + final double? width; + + const _SelectionCard({ + required this.icon, + required this.label, + required this.selected, + required this.onTap, + this.width = 85, + }); + + @override + Widget build(BuildContext context) { + return GestureDetector( + onTap: onTap, + child: AnimatedContainer( + duration: const Duration(milliseconds: 200), + width: width, + padding: const EdgeInsets.symmetric(vertical: 10, horizontal: 4), + decoration: BoxDecoration( + color: selected ? AppColors.primaryFixed : AppColors.surface, + borderRadius: BorderRadius.circular(16), + border: Border.all( + color: selected ? AppColors.primary : AppColors.surfaceContainerHigh, + width: selected ? 2 : 1, + ), + boxShadow: selected + ? [BoxShadow(color: AppColors.primary.withValues(alpha: 0.15), blurRadius: 12, offset: const Offset(0, 4))] + : [], + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Icon(icon, color: selected ? AppColors.primary : AppColors.onSurfaceVariant, size: 24), + const SizedBox(height: 12), + Text( + label, + textAlign: TextAlign.center, + maxLines: 2, + style: AppTheme.labelMd.copyWith( + color: selected ? AppColors.primary : AppColors.onSurfaceVariant, + fontWeight: selected ? FontWeight.w700 : FontWeight.w500, + fontSize: 10, + ), + ), + ], + ), + ), + ); + } +} + +class _ServiceCard extends StatelessWidget { + final String title; + final String subtitle; + final IconData icon; + final bool selected; + final VoidCallback onTap; + + const _ServiceCard({ + required this.title, + required this.subtitle, + required this.icon, + required this.selected, + required this.onTap, + }); + + @override + Widget build(BuildContext context) { + return GestureDetector( + onTap: onTap, + child: AnimatedContainer( + duration: const Duration(milliseconds: 200), + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + gradient: selected + ? const LinearGradient( + colors: [AppColors.primaryContainer, AppColors.primary], + begin: Alignment.topLeft, + end: Alignment.bottomRight, + ) + : null, + color: selected ? null : AppColors.surface, + borderRadius: BorderRadius.circular(20), + border: Border.all( + color: selected ? AppColors.primary : AppColors.surfaceContainerHigh, + width: selected ? 2 : 1, + ), + boxShadow: selected + ? [BoxShadow(color: AppColors.primary.withValues(alpha: 0.3), blurRadius: 16, offset: const Offset(0, 6))] + : [], + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + padding: const EdgeInsets.all(6), + decoration: BoxDecoration( + color: selected ? Colors.white.withValues(alpha: 0.2) : AppColors.surfaceContainer, + borderRadius: BorderRadius.circular(12), + ), + child: Icon(icon, color: selected ? Colors.white : AppColors.onSurfaceVariant, size: 24), + ), + const SizedBox(height: 16), + Text( + title, + style: AppTheme.headlineSm.copyWith( + color: selected ? Colors.white : AppColors.onSurface, + fontSize: 14, + ), + ), + const SizedBox(height: 4), + Text( + subtitle, + style: AppTheme.caption.copyWith( + color: selected ? Colors.white.withValues(alpha: 0.8) : AppColors.onSurfaceVariant, + ), + ), + ], + ), + ), + ); + } +} diff --git a/lib/features/booking/screens/booking_preview_screen.dart b/lib/features/booking/screens/booking_preview_screen.dart new file mode 100644 index 0000000..9e10514 --- /dev/null +++ b/lib/features/booking/screens/booking_preview_screen.dart @@ -0,0 +1,252 @@ +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; + +import 'package:doormile/main.dart'; +import 'package:doormile/shared/state/app_state.dart'; +import 'package:doormile/core/theme/app_colors.dart'; +import 'package:doormile/core/theme/app_theme.dart'; +import 'package:doormile/core/widgets/common.dart'; +import 'package:doormile/features/booking/screens/booking_scaffold.dart'; +import 'package:doormile/features/booking/screens/payment_sheet.dart'; + +class BookingPreviewScreen extends StatefulWidget { + const BookingPreviewScreen({super.key}); + + @override + State createState() => _BookingPreviewScreenState(); +} + +class _BookingPreviewScreenState extends State { + bool _loading = false; + + void _confirmOrder() async { + setState(() => _loading = true); + try { + await context.read().placeOrder(); + if (!mounted) return; + setState(() => _loading = false); + Navigator.pushNamedAndRemoveUntil(context, Routes.bookingSuccess, ModalRoute.withName(Routes.home)); + } catch (e) { + if (!mounted) return; + setState(() => _loading = false); + ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(e.toString().replaceAll('Exception: ', '')))); + } + } + + @override + Widget build(BuildContext context) { + final state = context.watch(); + final draft = state.draft; + + return BookingScaffold( + title: 'Review Booking', + step: 4, + stepLabel: 'Step 4: Review', + buttonLabel: 'Confirm booking', + loading: _loading, + enabled: !_loading, + onContinue: _confirmOrder, + children: [ + Center( + child: Column( + children: [ + StatusChip( + label: 'Step 4 of 4', + color: AppColors.primary, + bg: AppColors.primaryFixed.withValues(alpha: 0.5), + ), + const SizedBox(height: 10), + Text('Review Booking', style: AppTheme.headlineLg), + const SizedBox(height: 4), + Text('Please verify your delivery details before confirming payment.', + textAlign: TextAlign.center, + style: AppTheme.bodyMd.copyWith(color: AppColors.onSurfaceVariant)), + ], + ), + ), + const SizedBox(height: 20), + // Pickup location + AppCard( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + const Icon(Icons.gps_fixed, size: 18, color: AppColors.primary), + const SizedBox(width: 8), + Expanded(child: Text('Pickup Location', style: AppTheme.labelMd)), + SecondaryButton( + label: 'Change', + onPressed: () => Navigator.popUntil( + context, (r) => r.settings.name == Routes.bookingPickup), + ), + ], + ), + const SizedBox(height: 10), + Text(draft.pickupAddress, + style: AppTheme.headlineSm.copyWith(fontWeight: FontWeight.w700)), + Text(draft.dropPincode.isNotEmpty + ? 'Drop details will be collected by Miler at Pincode: ${draft.dropPincode}' + : 'Drop details will be collected by Miler', + style: AppTheme.bodyMd.copyWith(color: AppColors.onSurfaceVariant)), + ], + ), + ), + const SizedBox(height: 12), + Row( + children: [ + Expanded( + child: _MiniCard( + icon: Icons.inventory_2, + title: 'Category', + value: draft.parcelType, + ), + ), + const SizedBox(width: 12), + Expanded( + child: _MiniCard( + icon: Icons.speed, + title: 'Service', + value: draft.service, + ), + ), + ], + ), + const SizedBox(height: 12), + AppCard( + border: Border.all(color: AppColors.outlineVariant), + child: Row( + children: [ + const IconBadge( + icon: Icons.local_shipping, + bg: AppColors.primary, + color: Colors.white, + ), + const SizedBox(width: 14), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text('Doormile Collection', + style: AppTheme.labelMd.copyWith(fontWeight: FontWeight.w700)), + Text('Assigned optimal vehicle size', style: AppTheme.caption), + ], + ), + ), + StatusChip( + label: 'EV First', + icon: Icons.eco, + color: AppColors.tertiary, + bg: AppColors.tertiaryFixed.withValues(alpha: 0.5), + ), + ], + ), + ), + const SizedBox(height: 12), + // Fare summary + AppCard( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text('Estimated Price Range', style: AppTheme.headlineSm), + const SizedBox(height: 14), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Flexible( + child: Text('Final price determined at pickup', + style: AppTheme.bodyMd.copyWith(color: AppColors.onSurfaceVariant), overflow: TextOverflow.ellipsis), + ), + const SizedBox(width: 12), + Text('₹${draft.minPrice.toInt()} - ₹${draft.maxPrice.toInt()}', + style: AppTheme.headlineMd.copyWith(color: AppColors.primary)), + ], + ), + ], + ), + ), + + const SizedBox(height: 12), + Center( + child: Text('Taxes and fees are included in the total price.', + style: AppTheme.caption), + ), + ], + ); + } + + Widget _fareRow(String label, String value) => Padding( + padding: const EdgeInsets.symmetric(vertical: 5), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Flexible( + child: Text(label, + style: AppTheme.bodyMd.copyWith(color: AppColors.onSurfaceVariant), + overflow: TextOverflow.ellipsis), + ), + const SizedBox(width: 12), + Text(value, style: AppTheme.labelMd), + ], + ), + ); +} + +class _MiniCard extends StatelessWidget { + final IconData icon; + final String title; + final String value; + final String? subtitle; + final List? chips; + const _MiniCard({ + required this.icon, + required this.title, + required this.value, + this.subtitle, + this.chips, + }); + + @override + Widget build(BuildContext context) { + return AppCard( + padding: const EdgeInsets.all(14), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + IconBadge( + icon: icon, + bg: AppColors.surfaceContainer, + color: AppColors.primary, + size: 34, + iconSize: 18, + ), + const SizedBox(height: 10), + Text(title, style: AppTheme.caption), + Text(value, style: AppTheme.labelMd.copyWith(fontWeight: FontWeight.w700)), + if (subtitle != null) + Text(subtitle!, style: AppTheme.caption.copyWith(color: AppColors.onSurfaceVariant)), + if (chips != null) ...[ + const SizedBox(height: 6), + Wrap( + spacing: 6, + runSpacing: 6, + children: chips! + .map((c) => Align( + alignment: Alignment.centerLeft, + child: FittedBox( + fit: BoxFit.scaleDown, + child: StatusChip( + label: c, + color: AppColors.secondary, + bg: AppColors.secondaryFixed, + ), + ), + )) + .toList(), + ), + ], + ], + ), + ); + } +} diff --git a/lib/features/booking/screens/booking_scaffold.dart b/lib/features/booking/screens/booking_scaffold.dart new file mode 100644 index 0000000..ea265b7 --- /dev/null +++ b/lib/features/booking/screens/booking_scaffold.dart @@ -0,0 +1,147 @@ +import 'package:flutter/material.dart'; + +import 'package:doormile/core/theme/app_colors.dart'; +import 'package:doormile/core/theme/app_theme.dart'; +import 'package:doormile/core/widgets/common.dart'; + +/// Shared layout for the multi-step booking flow: app bar, step progress, +/// scrollable body, and a sticky bottom action button. +class BookingScaffold extends StatelessWidget { + final String title; + final int step; // 1..4 + final String stepLabel; + final String buttonLabel; + final VoidCallback onContinue; + final List children; + final Widget? bottomExtra; + final bool enabled; + final bool loading; + + const BookingScaffold({ + super.key, + required this.title, + required this.step, + required this.stepLabel, + required this.buttonLabel, + required this.onContinue, + required this.children, + this.bottomExtra, + this.enabled = true, + this.loading = false, + }); + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: AppColors.surface, + body: AppBackground( + child: Column( + children: [ + Container( + decoration: const BoxDecoration( + gradient: LinearGradient( + colors: [AppColors.primaryContainer, AppColors.primary], + begin: Alignment.topLeft, + end: Alignment.bottomRight, + ), + borderRadius: BorderRadius.vertical(bottom: Radius.circular(24)), + ), + child: ClipRRect( + borderRadius: const BorderRadius.vertical(bottom: Radius.circular(24)), + child: Stack( + children: [ + Positioned( + right: -20, + top: 10, + child: Transform.rotate( + angle: 0.1, + child: Icon(Icons.add_location_alt_rounded, size: 140, color: Colors.white.withValues(alpha: 0.04)), + ), + ), + SafeArea( + bottom: false, + child: Padding( + padding: const EdgeInsets.fromLTRB(8, 10, 16, 18), + child: Column( + children: [ + Row( + children: [ + IconButton( + onPressed: () => Navigator.pop(context), + icon: const Icon(Icons.arrow_back, color: Colors.white), + ), + Expanded( + child: Text(title, + overflow: TextOverflow.ellipsis, + style: AppTheme.headlineMd.copyWith(color: Colors.white)), + ), + const Icon(Icons.location_on_outlined, color: Colors.white), + ], + ), + const SizedBox(height: 12), + AppCard( + padding: const EdgeInsets.all(14), + color: Colors.white.withValues(alpha: 0.94), + child: Column( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Flexible( + child: Text(stepLabel, + style: AppTheme.labelMd.copyWith(color: AppColors.primary), + overflow: TextOverflow.ellipsis), + ), + const SizedBox(width: 8), + Text('Step $step', + style: AppTheme.caption.copyWith(fontWeight: FontWeight.w700)), + ], + ), + const SizedBox(height: 10), + JourneyProgressBar(value: 1, gradient: true), + ], + ), + ), + ], + ), + ), + ), + ], + ), + ), + ), + Expanded( + child: ListView( + padding: const EdgeInsets.fromLTRB(16, 18, 16, 24), + children: children, + ), + ), + ], + ), + ), + bottomNavigationBar: Container( + decoration: BoxDecoration( + color: AppColors.surfaceContainerLowest.withValues(alpha: 0.95), + border: const Border(top: BorderSide(color: AppColors.surfaceContainer)), + ), + child: SafeArea( + top: false, + minimum: const EdgeInsets.fromLTRB(16, 12, 16, 12), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + if (bottomExtra != null) ...[bottomExtra!, const SizedBox(height: 10)], + PrimaryButton( + label: buttonLabel, + loading: loading, + trailingIcon: Icons.arrow_forward, + height: 54, + onPressed: enabled ? onContinue : null, + ), + ], + ), + ), + ), + ); + } +} diff --git a/lib/features/booking/screens/booking_success_screen.dart b/lib/features/booking/screens/booking_success_screen.dart new file mode 100644 index 0000000..35b4fd9 --- /dev/null +++ b/lib/features/booking/screens/booking_success_screen.dart @@ -0,0 +1,113 @@ +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; + +import 'package:doormile/main.dart'; +import 'package:doormile/shared/state/app_state.dart'; +import 'package:doormile/core/theme/app_colors.dart'; +import 'package:doormile/core/theme/app_theme.dart'; +import 'package:doormile/core/widgets/common.dart'; + +class BookingSuccessScreen extends StatefulWidget { + const BookingSuccessScreen({super.key}); + + @override + State createState() => _BookingSuccessScreenState(); +} + +class _BookingSuccessScreenState extends State + with SingleTickerProviderStateMixin { + late final AnimationController _c = + AnimationController(vsync: this, duration: const Duration(milliseconds: 600))..forward(); + + @override + void dispose() { + _c.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final order = context.read().orders.first; + return Scaffold( + backgroundColor: AppColors.surface, + body: SafeArea( + child: Padding( + padding: const EdgeInsets.all(24), + child: Column( + children: [ + const Spacer(), + ScaleTransition( + scale: CurvedAnimation(parent: _c, curve: Curves.elasticOut), + child: Container( + width: 110, + height: 110, + decoration: BoxDecoration( + color: AppColors.success.withValues(alpha: 0.12), + shape: BoxShape.circle, + ), + child: const Icon(Icons.check_circle, + color: AppColors.success, size: 72), + ), + ), + const SizedBox(height: 28), + Text('Booking Confirmed!', style: AppTheme.headlineLg), + const SizedBox(height: 8), + Text( + 'Your EV pickup is scheduled. We’ll notify you when the rider is on the way.', + textAlign: TextAlign.center, + style: AppTheme.bodyMd.copyWith(color: AppColors.onSurfaceVariant), + ), + const SizedBox(height: 24), + AppCard( + child: Column( + children: [ + _row('Order ID', order.id), + const Divider(height: 22, color: AppColors.surfaceContainer), + _row('Estimated Fare', '₹${order.amount.toStringAsFixed(0)}'), + const Divider(height: 22, color: AppColors.surfaceContainer), + _row('Pickup Status', 'Waiting for Miler assignment'), + ], + ), + ), + const SizedBox(height: 16), + const EcoBanner( + text: 'This delivery is 100% electric — offsetting ~1.2kg of CO₂.'), + const Spacer(), + PrimaryButton( + label: 'Track Live', + trailingIcon: Icons.navigation, + onPressed: () => Navigator.pushNamedAndRemoveUntil( + context, + Routes.liveTracking, + (r) => r.settings.name == Routes.home, + arguments: order, + ), + ), + const SizedBox(height: 12), + TextButton( + onPressed: () => Navigator.popUntil( + context, (r) => r.settings.name == Routes.home), + child: Text('Back to Home', + style: AppTheme.labelMd.copyWith(color: AppColors.primary)), + ), + ], + ), + ), + ), + ); + } + + Widget _row(String k, String v) => Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(k, style: AppTheme.bodyMd.copyWith(color: AppColors.onSurfaceVariant)), + const SizedBox(width: 12), + Flexible( + child: Text(v, + textAlign: TextAlign.right, + style: AppTheme.labelMd.copyWith(fontWeight: FontWeight.w700)), + ), + ], + ); +} diff --git a/lib/features/booking/screens/parcel_details_screen.dart b/lib/features/booking/screens/parcel_details_screen.dart new file mode 100644 index 0000000..8873b09 --- /dev/null +++ b/lib/features/booking/screens/parcel_details_screen.dart @@ -0,0 +1,349 @@ +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; + +import 'package:doormile/main.dart'; +import 'package:doormile/shared/state/app_state.dart'; +import 'package:doormile/core/theme/app_colors.dart'; +import 'package:doormile/core/theme/app_theme.dart'; +import 'package:doormile/core/widgets/common.dart'; +import 'package:doormile/features/booking/screens/booking_scaffold.dart'; + +class ParcelDetailsScreen extends StatefulWidget { + const ParcelDetailsScreen({super.key}); + + @override + State createState() => _ParcelDetailsScreenState(); +} + +class _ParcelDetailsScreenState extends State { + final _types = const [ + ('Documents', Icons.description), + ('Electronics', Icons.devices), + ('Food', Icons.restaurant), + ('Clothing', Icons.checkroom), + ('Fragile', Icons.auto_awesome), + ('Other', Icons.more_horiz), + ]; + final _weights = const ['0–1 kg', '1–5 kg', '5–10 kg', '10 kg+']; + final _sizes = const [ + ('Small', 'Fits in a backpack (e.g. Phone, Keys)'), + ('Medium', 'Fits in a courier box (e.g. Laptop, Shoes)'), + ('Large', 'Requires vehicle trunk (e.g. Monitor, Luggage)'), + ]; + final _days = const ['Today', 'Tomorrow']; + final _slots = const ['10:00 AM - 12:00 PM', '12:00 PM - 02:00 PM', '02:00 PM - 04:00 PM']; + + @override + Widget build(BuildContext context) { + final draft = context.watch().draft; + return BookingScaffold( + title: 'Parcel Details', + step: 2, + stepLabel: 'Step 2: Details', + buttonLabel: 'Continue', + onContinue: () => Navigator.pushNamed(context, Routes.servicePrice), + children: [ + Text('What are you sending?', style: AppTheme.headlineSm), + const SizedBox(height: 14), + GridView.count( + crossAxisCount: 3, + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + mainAxisSpacing: 12, + crossAxisSpacing: 12, + childAspectRatio: 1.05, + children: _types.map((t) { + final active = draft.parcelType == t.$1; + return _SelectTile( + active: active, + onTap: () { + draft.parcelType = t.$1; + context.read().touch(); + }, + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon(t.$2, color: active ? Colors.white : AppColors.primary, size: 26), + const SizedBox(height: 8), + Text(t.$1, + style: AppTheme.labelMd.copyWith( + color: active ? Colors.white : AppColors.onSurface)), + ], + ), + ); + }).toList(), + ), + const SizedBox(height: 24), + Text('Approximate Weight', style: AppTheme.headlineSm), + const SizedBox(height: 12), + Wrap( + spacing: 8, + runSpacing: 8, + children: _weights + .map((w) => _Pill( + label: w, + active: draft.weight == w, + onTap: () { + draft.weight = w; + context.read().touch(); + }, + )) + .toList(), + ), + const SizedBox(height: 24), + Text('Package Size', style: AppTheme.headlineSm), + const SizedBox(height: 12), + ..._sizes.map((s) => Padding( + padding: const EdgeInsets.only(bottom: 12), + child: _RadioCard( + active: draft.size == s.$1, + onTap: () { + draft.size = s.$1; + context.read().touch(); + }, + child: Row( + children: [ + IconBadge( + icon: Icons.inventory_2, + bg: AppColors.primaryContainer.withValues(alpha: 0.1), + color: AppColors.primary, + ), + const SizedBox(width: 14), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(s.$1, + style: AppTheme.labelMd + .copyWith(fontWeight: FontWeight.w700)), + const SizedBox(height: 2), + Text(s.$2, style: AppTheme.caption), + ], + ), + ), + ], + ), + ), + )), + const SizedBox(height: 12), + Text('Pickup Slot', style: AppTheme.headlineSm), + const SizedBox(height: 12), + Row( + children: _days + .map((d) => Padding( + padding: const EdgeInsets.only(right: 8), + child: _Pill( + label: d, + active: draft.pickupDay == d, + onTap: () { + draft.pickupDay = d; + context.read().touch(); + }, + ), + )) + .toList(), + ), + const SizedBox(height: 12), + ..._slots.map((slot) => Padding( + padding: const EdgeInsets.only(bottom: 8), + child: _RadioCard( + active: draft.pickupSlot == slot, + onTap: () { + draft.pickupSlot = slot; + context.read().touch(); + }, + child: Text(slot, + style: AppTheme.bodyMd.copyWith(fontWeight: FontWeight.w700)), + ), + )), + const SizedBox(height: 16), + // Fragile toggle + AppCard( + border: Border.all(color: AppColors.outlineVariant), + child: Row( + children: [ + const Icon(Icons.warning_amber_rounded, color: AppColors.warning), + const SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text('Mark as Fragile', + style: AppTheme.labelMd.copyWith(fontWeight: FontWeight.w700)), + Text('Handle with extra care', style: AppTheme.caption), + ], + ), + ), + Switch( + value: draft.fragile, + activeTrackColor: AppColors.primary, + onChanged: (v) { + draft.fragile = v; + context.read().touch(); + }, + ), + ], + ), + ), + const SizedBox(height: 16), + Text('Additional Notes (Optional)', + style: AppTheme.labelMd.copyWith(color: AppColors.onSurfaceVariant)), + const SizedBox(height: 8), + TextField( + maxLines: 3, + cursorColor: AppColors.primary, + onChanged: (v) => draft.notes = v, + decoration: InputDecoration( + hintText: 'Gate code, specific handling instructions, etc.', + hintStyle: AppTheme.bodyMd.copyWith(color: AppColors.outline), + filled: true, + fillColor: AppColors.surfaceContainerLowest, + enabledBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(14), + borderSide: const BorderSide(color: AppColors.outlineVariant), + ), + focusedBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(14), + borderSide: const BorderSide(color: AppColors.primary), + ), + ), + ), + const SizedBox(height: 16), + Container( + padding: const EdgeInsets.all(14), + decoration: BoxDecoration( + color: AppColors.tertiaryFixedDim.withValues(alpha: 0.2), + borderRadius: BorderRadius.circular(16), + border: Border.all(color: AppColors.tertiary.withValues(alpha: 0.1)), + ), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const IconBadge( + icon: Icons.electric_rickshaw, + bg: Color(0x1A005251), + color: AppColors.tertiary, + size: 40, + iconSize: 20, + circle: true, + ), + const SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text('EV-First & 3-Wheeler Collection', + style: AppTheme.labelMd + .copyWith(color: AppColors.tertiary, fontWeight: FontWeight.w700)), + const SizedBox(height: 2), + Text( + 'A Doormile agent arrives in a 3-wheeler during your slot. Delivered by EV to cut carbon footprint.', + style: AppTheme.caption + .copyWith(color: AppColors.onTertiaryFixedVariant), + ), + ], + ), + ), + ], + ), + ), + ], + ); + } +} + +// ---- Shared selection widgets ---- + +class _SelectTile extends StatelessWidget { + final bool active; + final Widget child; + final VoidCallback onTap; + const _SelectTile({required this.active, required this.child, required this.onTap}); + + @override + Widget build(BuildContext context) { + return GestureDetector( + onTap: onTap, + child: AnimatedContainer( + duration: const Duration(milliseconds: 150), + decoration: BoxDecoration( + color: active ? AppColors.primary : AppColors.surfaceContainerLowest, + borderRadius: BorderRadius.circular(16), + border: Border.all( + color: active ? AppColors.primary : AppColors.outlineVariant), + ), + child: child, + ), + ); + } +} + +class _Pill extends StatelessWidget { + final String label; + final bool active; + final VoidCallback onTap; + const _Pill({required this.label, required this.active, required this.onTap}); + + @override + Widget build(BuildContext context) { + return GestureDetector( + onTap: onTap, + child: AnimatedContainer( + duration: const Duration(milliseconds: 150), + padding: const EdgeInsets.symmetric(horizontal: 18, vertical: 10), + decoration: BoxDecoration( + color: active ? AppColors.primary : AppColors.surfaceContainer, + borderRadius: BorderRadius.circular(999), + ), + child: Text(label, + style: AppTheme.labelMd + .copyWith(color: active ? Colors.white : AppColors.onSurface)), + ), + ); + } +} + +class _RadioCard extends StatelessWidget { + final bool active; + final Widget child; + final VoidCallback onTap; + const _RadioCard({required this.active, required this.child, required this.onTap}); + + @override + Widget build(BuildContext context) { + return GestureDetector( + onTap: onTap, + child: AnimatedContainer( + duration: const Duration(milliseconds: 150), + padding: const EdgeInsets.all(14), + decoration: BoxDecoration( + color: active ? const Color(0xFFF8E0E3) : AppColors.surfaceContainerLowest, + borderRadius: BorderRadius.circular(16), + border: Border.all( + color: active ? AppColors.primary : AppColors.outlineVariant, + width: active ? 2 : 1, + ), + ), + child: Row( + children: [ + Expanded(child: child), + const SizedBox(width: 12), + Container( + width: 20, + height: 20, + decoration: BoxDecoration( + shape: BoxShape.circle, + border: Border.all( + color: active ? AppColors.primary : AppColors.outline, width: 2), + ), + child: active + ? const Center( + child: CircleAvatar(radius: 5, backgroundColor: AppColors.primary)) + : null, + ), + ], + ), + ), + ); + } +} diff --git a/lib/features/booking/screens/payment_sheet.dart b/lib/features/booking/screens/payment_sheet.dart new file mode 100644 index 0000000..03fb6dd --- /dev/null +++ b/lib/features/booking/screens/payment_sheet.dart @@ -0,0 +1,165 @@ +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; + +import 'package:doormile/main.dart'; +import 'package:doormile/shared/state/app_state.dart'; +import 'package:doormile/core/theme/app_colors.dart'; +import 'package:doormile/core/theme/app_theme.dart'; +import 'package:doormile/core/widgets/common.dart'; + +Future showPaymentSheet(BuildContext context, double amount) { + return showModalBottomSheet( + context: context, + isScrollControlled: true, + backgroundColor: AppColors.surface, + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.vertical(top: Radius.circular(24)), + ), + builder: (_) => _PaymentSheet(amount: amount), + ); +} + +class _Method { + final String name; + final String? sub; + final IconData icon; + final Color iconColor; + const _Method(this.name, this.icon, this.iconColor, {this.sub}); +} + +class _PaymentSheet extends StatefulWidget { + final double amount; + const _PaymentSheet({required this.amount}); + + @override + State<_PaymentSheet> createState() => _PaymentSheetState(); +} + +class _PaymentSheetState extends State<_PaymentSheet> { + final _methods = const [ + _Method('UPI (GPay, PhonePe, Paytm)', Icons.account_balance, AppColors.secondary), + _Method('Credit / Debit Cards', Icons.credit_card, AppColors.onSurface), + _Method('Net banking', Icons.account_balance_outlined, AppColors.onSurface), + _Method('Cash on Delivery', Icons.payments_outlined, AppColors.onSurface), + ]; + int _selected = 0; + bool _processing = false; + + void _pay() async { + setState(() => _processing = true); + final state = context.read(); + state.draft.paymentMethod = _methods[_selected].name; + await Future.delayed(const Duration(milliseconds: 1400)); + if (!mounted) return; + state.placeOrder(); + Navigator.pop(context); // close sheet + Navigator.pushNamedAndRemoveUntil( + context, + Routes.bookingSuccess, + (r) => r.settings.name == Routes.home, + ); + } + + @override + Widget build(BuildContext context) { + return Padding( + padding: EdgeInsets.only(bottom: MediaQuery.of(context).viewInsets.bottom), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + const SizedBox(height: 10), + Container( + width: 40, + height: 4, + decoration: BoxDecoration( + color: AppColors.surfaceContainerHighest, + borderRadius: BorderRadius.circular(99)), + ), + Padding( + padding: const EdgeInsets.fromLTRB(20, 18, 20, 8), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text('Pay ₹${widget.amount.toStringAsFixed(0)}', style: AppTheme.headlineLg), + Text('Order ID: 293810', + style: AppTheme.bodyMd.copyWith(color: AppColors.onSurfaceVariant)), + ], + ), + ), + ...List.generate(_methods.length, (i) { + final m = _methods[i]; + final active = i == _selected; + return Padding( + padding: const EdgeInsets.fromLTRB(16, 6, 16, 6), + child: GestureDetector( + onTap: () => setState(() => _selected = i), + child: Container( + padding: const EdgeInsets.all(14), + decoration: BoxDecoration( + color: active ? AppColors.surfaceContainer : AppColors.surface, + borderRadius: BorderRadius.circular(14), + border: Border.all( + color: active ? AppColors.primary : AppColors.outlineVariant), + ), + child: Row( + children: [ + Container( + width: 44, + height: 44, + decoration: BoxDecoration( + color: AppColors.surfaceContainerLowest, + borderRadius: BorderRadius.circular(10), + ), + child: Icon(m.icon, color: m.iconColor, size: 22), + ), + const SizedBox(width: 14), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(m.name, + style: AppTheme.labelMd + .copyWith(fontWeight: FontWeight.w700)), + if (m.sub != null) + Text(m.sub!, + style: AppTheme.caption.copyWith(color: AppColors.primary)), + ], + ), + ), + Container( + width: 22, + height: 22, + decoration: BoxDecoration( + shape: BoxShape.circle, + border: Border.all( + color: active ? AppColors.primary : AppColors.outline, + width: 2), + color: active ? AppColors.primary : Colors.transparent, + ), + child: active + ? const Icon(Icons.circle, size: 8, color: Colors.white) + : null, + ), + ], + ), + ), + ), + ); + }), + Padding( + padding: const EdgeInsets.fromLTRB(16, 14, 16, 8), + child: SafeArea( + top: false, + child: PrimaryButton( + label: 'Pay ₹${widget.amount.toStringAsFixed(0)}', + loading: _processing, + height: 54, + onPressed: _pay, + ), + ), + ), + ], + ), + ); + } +} diff --git a/lib/features/booking/screens/service_price_screen.dart b/lib/features/booking/screens/service_price_screen.dart new file mode 100644 index 0000000..e827554 --- /dev/null +++ b/lib/features/booking/screens/service_price_screen.dart @@ -0,0 +1,253 @@ +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; + +import 'package:doormile/main.dart'; +import 'package:doormile/shared/state/app_state.dart'; +import 'package:doormile/core/theme/app_colors.dart'; +import 'package:doormile/core/theme/app_theme.dart'; +import 'package:doormile/core/widgets/common.dart'; +import 'package:doormile/features/booking/screens/booking_scaffold.dart'; + +class ServicePriceScreen extends StatelessWidget { + const ServicePriceScreen({super.key}); + + @override + Widget build(BuildContext context) { + final state = context.watch(); + final draft = state.draft; + return BookingScaffold( + title: 'Service & Price', + step: 3, + stepLabel: 'Step 3: Choose Service', + buttonLabel: 'Confirm & Continue', + onContinue: () => Navigator.pushNamed(context, Routes.bookingPreview), + children: [ + Text('CHOOSE SERVICE', + style: AppTheme.caption + .copyWith(letterSpacing: 1.2, fontWeight: FontWeight.w700)), + const SizedBox(height: 12), + _ServiceCard( + active: draft.service == 'Standard', + title: 'Standard', + badge: 'EV-FIRST', + subtitle: 'Today by 6:00 PM', + note: '3-Wheeler Collection included', + price: '₹${draft.minPrice.toStringAsFixed(0)}', + icon: Icons.eco, + iconColor: AppColors.tertiary, + onTap: () { + draft.service = 'Standard'; + state.touch(); + }, + ), + const SizedBox(height: 12), + _ServiceCard( + active: draft.service == 'Express', + title: 'Express', + subtitle: 'Arrival in 2 hrs', + price: '₹${draft.maxPrice.toStringAsFixed(0)}', + icon: Icons.bolt, + iconColor: AppColors.secondary, + onTap: () { + draft.service = 'Express'; + state.touch(); + }, + ), + const SizedBox(height: 20), + // Selected slot + Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: AppColors.surfaceContainerLow, + borderRadius: BorderRadius.circular(16), + border: Border.all(color: AppColors.outlineVariant), + ), + child: Row( + children: [ + const Icon(Icons.calendar_month, color: AppColors.primary), + const SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text('SELECTED SLOT', + style: AppTheme.caption + .copyWith(letterSpacing: 1, fontWeight: FontWeight.w600)), + Text('Pickup: ${draft.pickupDay}, ${draft.pickupSlot}', + style: AppTheme.labelMd), + ], + ), + ), + ], + ), + ), + const SizedBox(height: 16), + // Fare breakdown + AppCard( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text('ESTIMATED FARE', + style: AppTheme.caption.copyWith( + color: AppColors.onSurfaceVariant, + letterSpacing: 1.2, + fontWeight: FontWeight.w700)), + const SizedBox(height: 14), + _fareRow('Estimated Range', '₹${draft.minPrice.toStringAsFixed(0)} - ₹${draft.maxPrice.toStringAsFixed(0)}'), + const Divider(height: 24, color: AppColors.surfaceContainerHigh), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text('Final price determined at pickup', style: AppTheme.caption), + ], + ), + ], + ), + ), + const SizedBox(height: 16), + // Pay-by row + AppCard( + onTap: () {}, + child: Row( + children: [ + const IconBadge( + icon: Icons.account_balance_wallet, + bg: AppColors.surfaceContainer, + color: AppColors.primary, + size: 36, + iconSize: 18, + circle: true, + ), + const SizedBox(width: 12), + Expanded(child: Text('Pay by: UPI', style: AppTheme.labelMd)), + const Icon(Icons.expand_more, color: AppColors.onSurfaceVariant), + ], + ), + ), + const SizedBox(height: 12), + const Row( + children: [ + Icon(Icons.shield_outlined, size: 16, color: AppColors.tertiary), + SizedBox(width: 6), + Text('Secured by Doormile Pay Protection', + style: TextStyle( + fontSize: 12, color: AppColors.onSurfaceVariant)), + ], + ), + ], + ); + } + + Widget _fareRow(String label, String value) => Padding( + padding: const EdgeInsets.symmetric(vertical: 5), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text(label, style: AppTheme.bodyMd.copyWith(color: AppColors.onSurfaceVariant)), + Text(value, style: AppTheme.labelMd), + ], + ), + ); +} + +class _ServiceCard extends StatelessWidget { + final bool active; + final String title; + final String? badge; + final String subtitle; + final String? note; + final String price; + final IconData icon; + final Color iconColor; + final VoidCallback onTap; + + const _ServiceCard({ + required this.active, + required this.title, + this.badge, + required this.subtitle, + this.note, + required this.price, + required this.icon, + required this.iconColor, + required this.onTap, + }); + + @override + Widget build(BuildContext context) { + return GestureDetector( + onTap: onTap, + child: AnimatedContainer( + duration: const Duration(milliseconds: 150), + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: active ? const Color(0xFFFBE3E6) : AppColors.surfaceContainerLowest, + borderRadius: BorderRadius.circular(16), + border: Border.all( + color: active ? AppColors.primary : AppColors.outlineVariant, + width: active ? 2 : 1, + ), + boxShadow: active ? null : AppTheme.cardShadow, + ), + child: Row( + children: [ + Container( + width: 48, + height: 48, + decoration: BoxDecoration( + color: AppColors.surfaceContainerLowest, + borderRadius: BorderRadius.circular(12), + ), + child: Icon(icon, color: iconColor), + ), + const SizedBox(width: 14), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Flexible( + child: Text(title, + style: AppTheme.headlineSm, overflow: TextOverflow.ellipsis), + ), + if (badge != null) ...[ + const SizedBox(width: 8), + StatusChip( + label: badge!, + color: AppColors.tertiary, + bg: AppColors.tertiaryFixed.withValues(alpha: 0.5), + ), + ], + ], + ), + const SizedBox(height: 2), + Text(subtitle, style: AppTheme.bodyMd.copyWith(color: AppColors.onSurfaceVariant)), + if (note != null) + Text(note!, + style: AppTheme.caption.copyWith( + color: AppColors.tertiary, fontWeight: FontWeight.w600)), + ], + ), + ), + const SizedBox(width: 8), + Column( + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + if (active) + const CircleAvatar( + radius: 11, + backgroundColor: AppColors.primary, + child: Icon(Icons.check, size: 14, color: Colors.white), + ), + const SizedBox(height: 6), + Text(price, + style: AppTheme.headlineSm.copyWith(color: AppColors.primary)), + ], + ), + ], + ), + ), + ); + } +} diff --git a/lib/features/dashboard/screens/home_screen.dart b/lib/features/dashboard/screens/home_screen.dart new file mode 100644 index 0000000..d9b9feb --- /dev/null +++ b/lib/features/dashboard/screens/home_screen.dart @@ -0,0 +1,475 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:provider/provider.dart'; + +import 'package:doormile/main.dart'; +import 'package:doormile/shared/models/models.dart'; +import 'package:doormile/shared/state/app_state.dart'; +import 'package:doormile/core/theme/app_colors.dart'; +import 'package:doormile/core/theme/app_theme.dart'; +import 'package:doormile/core/widgets/common.dart'; +import 'package:doormile/core/widgets/logo.dart'; + +class HomeScreen extends StatefulWidget { + const HomeScreen({super.key}); + + @override + State createState() => _HomeScreenState(); +} + +class _HomeScreenState extends State { + @override + void initState() { + super.initState(); + WidgetsBinding.instance.addPostFrameCallback((_) { + context.read().fetchOrders(); + }); + } + + @override + Widget build(BuildContext context) { + final state = context.watch(); + final active = state.orders.where((o) => o.status == OrderStatus.active).firstOrNull; + + return AppBackground( + child: Stack( + children: [ + SingleChildScrollView( + padding: const EdgeInsets.only(bottom: 100), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _hero(context, state), + const SizedBox(height: 26), + active != null ? _activeShipment(context, active) : _emptyShipment(context), + const SizedBox(height: 18), + _ecoBanner(), + ], + ), + ), + Positioned( + top: 0, + left: 0, + right: 0, + child: Container( + height: MediaQuery.of(context).padding.top, + color: AppColors.primaryContainer, + ), + ), + ], + ), + ); + } + + Widget _hero(BuildContext context, AppState state) { + return Stack( + clipBehavior: Clip.none, + children: [ + Container( + height: 196, + decoration: const BoxDecoration( + gradient: LinearGradient( + colors: [AppColors.primaryContainer, AppColors.primary], + begin: Alignment.topLeft, + end: Alignment.bottomRight, + ), + borderRadius: BorderRadius.vertical(bottom: Radius.circular(24)), + ), + child: ClipRRect( + borderRadius: const BorderRadius.vertical(bottom: Radius.circular(24)), + child: AnnotatedRegion( + value: SystemUiOverlayStyle.light, + child: Stack( + children: [ + Positioned( + right: -20, + top: 20, + child: Transform.rotate( + angle: -0.1, + child: Icon(Icons.local_shipping_rounded, size: 150, color: Colors.white.withValues(alpha: 0.04)), + ), + ), + Positioned( + left: 40, + bottom: -30, + child: Transform.rotate( + angle: 0.15, + child: Icon(Icons.route_rounded, size: 100, color: Colors.white.withValues(alpha: 0.03)), + ), + ), + SafeArea( + bottom: false, + child: Padding( + padding: const EdgeInsets.fromLTRB(20, 18, 20, 0), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const DoormileMark(size: 48, color: Colors.white, glyphColor: AppColors.primary), + const SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + const Icon(Icons.location_on_rounded, color: Colors.white70, size: 15), + const SizedBox(width: 4), + Text('Bengaluru', style: AppTheme.labelMd.copyWith(color: Colors.white)), + ], + ), + const SizedBox(height: 4), + Text( + 'Hi, ${state.firstName.isEmpty ? 'Admin' : state.firstName}', + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: AppTheme.headlineMd.copyWith(color: Colors.white), + ), + Text( + 'Let deliveries move with less friction', + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: AppTheme.caption.copyWith(color: Colors.white70), + ), + ], + ), + ), + Container( + width: 44, + height: 44, + decoration: BoxDecoration( + color: Colors.white.withValues(alpha: 0.16), + borderRadius: BorderRadius.circular(16), + border: Border.all(color: Colors.white24), + ), + child: const Icon(Icons.notifications_none_rounded, color: Colors.white), + ), + ], + ), + ), + ), + ], + ), + ), + ), + ), + Positioned( + left: 20, + right: 20, + top: 130, + child: AppCard( + padding: const EdgeInsets.all(18), + radius: BorderRadius.circular(24), + border: Border.all(color: Colors.white), + child: Row( + children: [ + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text('Send a parcel', style: AppTheme.headlineLg), + const SizedBox(height: 6), + Text( + 'Fast pickup, live tracking, EV-first delivery.', + style: AppTheme.bodyMd.copyWith(color: AppColors.onSurfaceVariant), + ), + const SizedBox(height: 16), + SizedBox( + height: 44, + child: ElevatedButton.icon( + onPressed: () { + final draft = context.read().draft; + draft.pickupAddress = 'Current Location (GPS)'; + Navigator.pushNamed(context, Routes.bookingPickup); + }, + icon: const Icon(Icons.arrow_forward_rounded, size: 18), + label: const Text('Book Now'), + style: ElevatedButton.styleFrom( + backgroundColor: AppColors.primary, + foregroundColor: Colors.white, + elevation: 0, + padding: const EdgeInsets.symmetric(horizontal: 16), + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(14)), + ), + ), + ), + ], + ), + ), + const SizedBox(width: 10), + SizedBox( + width: 90, + height: 90, + child: Stack( + alignment: Alignment.center, + children: [ + const Icon(Icons.local_shipping_rounded, size: 68, color: AppColors.primary), + Positioned( + right: 0, + top: 0, + child: IconBadge( + icon: Icons.eco_rounded, + bg: AppColors.tertiaryContainer, + color: AppColors.tertiary, + size: 28, + iconSize: 16, + circle: true, + ), + ), + ], + ), + ), + ], + ), + ), + ), + const SizedBox(height: 310), + ], + ); + } + + Widget _activeShipment(BuildContext context, Order active) { + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 20), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text('Live Shipment', style: AppTheme.headlineSm.copyWith(fontWeight: FontWeight.w800)), + TextButton( + onPressed: () => Navigator.pushNamed(context, Routes.myOrders), + style: TextButton.styleFrom( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4), + minimumSize: Size.zero, + tapTargetSize: MaterialTapTargetSize.shrinkWrap, + ), + child: Text('View Details', style: AppTheme.labelMd.copyWith(color: AppColors.primary)), + ), + ], + ), + const SizedBox(height: 14), + GestureDetector( + onTap: () => Navigator.pushNamed(context, Routes.liveTracking, arguments: active), + child: Container( + decoration: BoxDecoration( + gradient: const LinearGradient( + colors: [AppColors.primary, AppColors.secondary], + begin: Alignment.topLeft, + end: Alignment.bottomRight, + ), + borderRadius: BorderRadius.circular(24), + boxShadow: [ + BoxShadow(color: AppColors.primary.withValues(alpha: 0.3), blurRadius: 24, offset: const Offset(0, 10)), + ], + ), + padding: const EdgeInsets.all(2), // Gradient border width + child: Container( + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(22), + ), + child: Padding( + padding: const EdgeInsets.all(20), + child: Column( + children: [ + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: AppColors.primaryFixed, + borderRadius: BorderRadius.circular(16), + ), + child: const Icon(Icons.inventory_2_rounded, color: AppColors.primary, size: 28), + ), + const SizedBox(width: 16), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Container( + width: 8, + height: 8, + decoration: const BoxDecoration( + color: AppColors.primary, + shape: BoxShape.circle, + ), + ), + const SizedBox(width: 6), + Text('PENDING PICKUP', style: AppTheme.caption.copyWith(color: AppColors.primary, fontWeight: FontWeight.w800, letterSpacing: 1.2)), + ], + ), + const SizedBox(height: 4), + Text(active.id, style: AppTheme.headlineSm), + ], + ), + ), + Container( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), + decoration: BoxDecoration( + color: AppColors.surfaceContainerLowest, + borderRadius: BorderRadius.circular(999), + border: Border.all(color: AppColors.outlineVariant), + ), + child: const Row( + mainAxisSize: MainAxisSize.min, + children: [ + Text('Live', style: TextStyle(color: AppColors.onSurface, fontWeight: FontWeight.w600, fontSize: 12)), + SizedBox(width: 4), + Icon(Icons.arrow_forward_ios_rounded, size: 10, color: AppColors.onSurfaceVariant), + ], + ), + ), + ], + ), + const SizedBox(height: 24), + Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: AppColors.surfaceContainerLowest, + borderRadius: BorderRadius.circular(16), + border: Border.all(color: AppColors.surfaceContainerHigh), + ), + child: Row( + children: [ + const Column( + children: [ + Icon(Icons.my_location_rounded, size: 20, color: AppColors.primary), + SizedBox(height: 4), + SizedBox( + height: 24, + child: VerticalDivider(color: AppColors.outlineVariant, thickness: 2), + ), + SizedBox(height: 4), + Icon(Icons.pin_drop_rounded, size: 20, color: AppColors.tertiary), + ], + ), + const SizedBox(width: 16), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text('Pickup Location', style: AppTheme.caption), + Text(active.fromLabel, maxLines: 1, overflow: TextOverflow.ellipsis, style: AppTheme.labelMd), + const SizedBox(height: 20), + Text('Destination', style: AppTheme.caption), + Text(active.toLabel, maxLines: 1, overflow: TextOverflow.ellipsis, style: AppTheme.labelMd), + ], + ), + ), + ], + ), + ), + ], + ), + ), + ), + ), + ), + ], + ), + ); + } + + Widget _trackNode(String title, String subtitle, bool active) { + return Expanded( + child: Column( + children: [ + Text(title, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: AppTheme.caption.copyWith( + color: active ? AppColors.secondary : AppColors.onSurfaceVariant, + fontWeight: FontWeight.w700, + )), + const SizedBox(height: 2), + Text(subtitle, maxLines: 1, overflow: TextOverflow.ellipsis, style: AppTheme.caption), + ], + ), + ); + } + + Widget _emptyShipment(BuildContext context) { + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 20), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const SectionHeader(title: 'Active Shipment'), + const SizedBox(height: 12), + AppCard( + padding: const EdgeInsets.symmetric(vertical: 24, horizontal: 18), + child: Column( + children: [ + Container( + width: 80, + height: 80, + decoration: BoxDecoration( + shape: BoxShape.circle, + gradient: LinearGradient( + colors: [AppColors.primary.withValues(alpha: 0.1), AppColors.primary.withValues(alpha: 0.25)], + begin: Alignment.topLeft, + end: Alignment.bottomRight, + ), + boxShadow: [ + BoxShadow( + color: AppColors.primary.withValues(alpha: 0.15), + blurRadius: 20, + spreadRadius: 2, + offset: const Offset(0, 8), + ), + ], + ), + child: Center( + child: Container( + width: 54, + height: 54, + decoration: const BoxDecoration( + color: Colors.white, + shape: BoxShape.circle, + ), + child: const Icon( + Icons.inbox_rounded, + size: 28, + color: AppColors.primary, + ), + ), + ), + ), + const SizedBox(height: 14), + Text('No active shipments', style: AppTheme.labelMd.copyWith(fontWeight: FontWeight.w800)), + const SizedBox(height: 4), + Text( + 'You do not have any deliveries in progress right now.', + textAlign: TextAlign.center, + style: AppTheme.caption, + ), + const SizedBox(height: 16), + SecondaryButton( + label: 'Book a Shipment', + onPressed: () { + final draft = context.read().draft; + draft.pickupAddress = 'Current Location (GPS)'; + Navigator.pushNamed(context, Routes.bookingPickup); + }, + ), + ], + ), + ), + ], + ), + ); + } + + Widget _ecoBanner() { + return const Padding( + padding: EdgeInsets.symmetric(horizontal: 20), + child: EcoBanner(text: 'This delivery offsets 1.2kg of CO2 emissions.'), + ); + } + +} diff --git a/lib/features/dashboard/screens/main_shell.dart b/lib/features/dashboard/screens/main_shell.dart new file mode 100644 index 0000000..9d46532 --- /dev/null +++ b/lib/features/dashboard/screens/main_shell.dart @@ -0,0 +1,189 @@ +import 'package:flutter/material.dart'; +import 'package:permission_handler/permission_handler.dart'; + +import 'package:doormile/main.dart'; +import 'package:doormile/core/theme/app_colors.dart'; +import 'package:doormile/core/theme/app_theme.dart'; +import 'package:doormile/features/dashboard/screens/home_screen.dart'; +import 'package:doormile/features/tracking/screens/my_orders_screen.dart'; +import 'package:doormile/features/dashboard/screens/summary_screen.dart'; +import 'package:doormile/features/dashboard/screens/profile_screen.dart'; + +/// Root scaffold hosting the 5-tab bottom navigation. +class MainShell extends StatefulWidget { + final int initialIndex; + const MainShell({super.key, this.initialIndex = 0}); + + @override + State createState() => _MainShellState(); +} + +class _MainShellState extends State { + late int _index = widget.initialIndex; + + @override + void initState() { + super.initState(); + _requestPermissions(); + } + + Future _requestPermissions() async { + final status = await Permission.locationWhenInUse.status; + if (!status.isGranted) { + await Permission.locationWhenInUse.request(); + } + } + + // Tab indices: 0 Home, 1 Track, 3 Wallet, 4 Profile. 2 = Book (action). + final _pages = const [ + HomeScreen(), + MyOrdersScreen(embedded: true), + SizedBox.shrink(), // Book slot — never shown (navigates away) + SummaryScreen(), + ProfileScreen(), + ]; + + void _onTap(int i) { + if (i == 2) { + Navigator.pushNamed(context, Routes.bookingPickup); + return; + } + setState(() => _index = i); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: AppColors.surface, + body: IndexedStack(index: _index, children: _pages), + extendBody: true, + bottomNavigationBar: _BottomBar(index: _index, onTap: _onTap), + ); + } +} + +class _BottomBar extends StatelessWidget { + final int index; + final ValueChanged onTap; + const _BottomBar({required this.index, required this.onTap}); + + static const _items = [ + (_NavItem(Icons.home_rounded, Icons.home_outlined, 'Home')), + (_NavItem(Icons.map_rounded, Icons.map_outlined, 'Track')), + (_NavItem(Icons.add_circle, Icons.add_circle, 'Book')), + (_NavItem(Icons.bar_chart_rounded, Icons.bar_chart_outlined, 'Summary')), + (_NavItem(Icons.person_rounded, Icons.person_outline, 'Profile')), + ]; + + @override + Widget build(BuildContext context) { + return SafeArea( + top: false, + minimum: const EdgeInsets.fromLTRB(14, 0, 14, 12), + child: Container( + height: 72, + decoration: BoxDecoration( + color: AppColors.surfaceContainerLowest.withValues(alpha: 0.96), + borderRadius: BorderRadius.circular(24), + border: Border.all(color: Colors.white), + boxShadow: AppTheme.cardShadow, + ), + child: ClipRRect( + borderRadius: BorderRadius.circular(24), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceAround, + children: List.generate(_items.length, (i) { + final item = _items[i]; + final active = i == index; + if (i == 2) { + // Centre "Book" action button. + return _BookButton(onTap: () => onTap(2)); + } + return Expanded( + child: InkWell( + onTap: () => onTap(i), + borderRadius: BorderRadius.circular(18), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + mainAxisSize: MainAxisSize.min, + children: [ + AnimatedContainer( + duration: const Duration(milliseconds: 180), + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 5), + decoration: BoxDecoration( + color: active ? AppColors.primaryFixed : Colors.transparent, + borderRadius: BorderRadius.circular(999), + ), + child: Icon(active ? item.active : item.inactive, + color: active ? AppColors.primary : AppColors.onSurfaceVariant, + size: 22), + ), + const SizedBox(height: 2), + Text(item.label, + maxLines: 1, + style: AppTheme.caption.copyWith( + color: active ? AppColors.primary : AppColors.onSurfaceVariant, + fontWeight: active ? FontWeight.w700 : FontWeight.w500, + )), + ], + ), + ), + ); + }), + ), + ), + ), + ); + } +} + +class _BookButton extends StatelessWidget { + final VoidCallback onTap; + const _BookButton({required this.onTap}); + + @override + Widget build(BuildContext context) { + return Expanded( + child: InkWell( + onTap: onTap, + borderRadius: BorderRadius.circular(40), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Container( + width: 46, + height: 46, + decoration: BoxDecoration( + gradient: const LinearGradient( + colors: [AppColors.primaryContainer, AppColors.primary], + begin: Alignment.topLeft, + end: Alignment.bottomRight, + ), + shape: BoxShape.circle, + boxShadow: [ + BoxShadow( + color: AppColors.primary.withValues(alpha: 0.28), + blurRadius: 16, + offset: const Offset(0, 8), + ) + ], + ), + child: const Icon(Icons.add, color: Colors.white, size: 28), + ), + const SizedBox(height: 2), + Text('Book', + style: AppTheme.caption.copyWith( + color: AppColors.primary, fontWeight: FontWeight.w700)), + ], + ), + ), + ); + } +} + +class _NavItem { + final IconData active; + final IconData inactive; + final String label; + const _NavItem(this.active, this.inactive, this.label); +} diff --git a/lib/features/dashboard/screens/profile_screen.dart b/lib/features/dashboard/screens/profile_screen.dart new file mode 100644 index 0000000..f887744 --- /dev/null +++ b/lib/features/dashboard/screens/profile_screen.dart @@ -0,0 +1,264 @@ +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; + +import 'package:doormile/main.dart'; +import 'package:doormile/shared/state/app_state.dart'; +import 'package:doormile/core/theme/app_colors.dart'; +import 'package:doormile/core/theme/app_theme.dart'; +import 'package:doormile/core/widgets/common.dart'; +import 'package:doormile/core/widgets/logo.dart'; + +class ProfileScreen extends StatelessWidget { + const ProfileScreen({super.key}); + + @override + Widget build(BuildContext context) { + final state = context.watch(); + return AppBackground( + child: Column( + children: [ + const ImmersiveHeader( + title: 'My Profile', + subtitle: 'Addresses, support, and preferences', + icon: Icons.person_rounded, + ), + Expanded( + child: SingleChildScrollView( + padding: const EdgeInsets.only(bottom: 100), + child: Column( + children: [ + _buildProfileAvatar(context, state), + const SizedBox(height: 20), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 20), + child: Column( + children: [ + const SizedBox(height: 20), + Row( + children: [ + Expanded( + child: _ProfileStatCard( + value: '${state.orders.length * 4 + 2}', + label: 'Parcels', + icon: Icons.inventory_2_rounded, + color: AppColors.primary, + ), + ), + const SizedBox(width: 16), + Expanded( + child: _ProfileStatCard( + value: '4.9', + label: 'Rating', + icon: Icons.star_rounded, + color: Colors.amber, + ), + ), + ], + ), + const SizedBox(height: 16), + _tile(context, Icons.location_on_outlined, 'My addresses', + onTap: () => Navigator.pushNamed(context, Routes.addresses)), + _tile(context, Icons.notifications_outlined, 'Notifications', dot: true), + _tile(context, Icons.card_giftcard_outlined, 'Refer & earn'), + _tile(context, Icons.language_outlined, 'Language', sub: 'English'), + _tile(context, Icons.help_outline, 'Help & support', + onTap: () => Navigator.pushNamed(context, Routes.help)), + const SizedBox(height: 4), + AppCard( + onTap: () => _confirmLogout(context, state), + child: Row( + children: [ + const Icon(Icons.logout, color: AppColors.primary), + const SizedBox(width: 16), + Text('Logout', + style: AppTheme.labelMd.copyWith( + color: AppColors.primary, fontWeight: FontWeight.w700)), + ], + ), + ), + const SizedBox(height: 24), + Center( + child: Column( + children: [ + Text('Doormile Customers V1.0.0', style: AppTheme.caption), + ], + ), + ), + ], + ), + ), + ], + ), + ), + ), +], + ), + ); + } + + Widget _buildProfileAvatar(BuildContext context, AppState state) { + return Column( + children: [ + const SizedBox(height: 24), + Center( + child: Column( + children: [ + Stack( + children: [ + Container( + width: 100, + height: 100, + decoration: BoxDecoration( + shape: BoxShape.circle, + border: Border.all(color: Colors.white, width: 4), + color: Colors.white, + boxShadow: [BoxShadow(color: Colors.black.withValues(alpha: 0.1), blurRadius: 10)], + ), + child: ClipOval( + child: Image.asset( + 'assets/images/profileicon.png', + fit: BoxFit.cover, + errorBuilder: (_, __, ___) => const Icon(Icons.person, size: 50, color: Color(0xFFC60018)), + ), + ), + ), + Positioned( + right: 0, + bottom: 0, + child: Container( + width: 32, + height: 32, + decoration: const BoxDecoration(color: Color(0xFFC60018), shape: BoxShape.circle), + child: const Icon(Icons.edit, size: 16, color: Colors.white), + ), + ), + ], + ), + const SizedBox(height: 16), + Text(state.userName.isEmpty ? 'Admin User' : state.userName, style: const TextStyle(fontSize: 20, fontWeight: FontWeight.bold, color: Colors.black87)), + Text('+91 ${state.phone.isEmpty ? '98765 43210' : state.phone}', style: const TextStyle(fontSize: 14, color: Colors.black54)), + ], + ), + ), + ], + ); + } + + Widget _stat(String value, String label, Color color) { + return const SizedBox.shrink(); // Replaced by _ProfileStatCard + } + + Widget _tile(BuildContext context, IconData icon, String title, + {String? sub, bool dot = false, VoidCallback? onTap}) { + return Padding( + padding: const EdgeInsets.only(bottom: 12), + child: AppCard( + onTap: onTap ?? () {}, + padding: const EdgeInsets.all(16), + child: Row( + children: [ + Container( + padding: const EdgeInsets.all(10), + decoration: BoxDecoration( + color: AppColors.primary.withValues(alpha: 0.08), + borderRadius: BorderRadius.circular(12), + ), + child: Icon(icon, color: AppColors.primary, size: 20), + ), + const SizedBox(width: 16), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(title, style: AppTheme.labelMd), + if (sub != null) ...[ + const SizedBox(height: 2), + Text(sub, style: AppTheme.caption.copyWith(color: AppColors.onSurfaceVariant)), + ] + ], + ), + ), + if (dot) + Container( + width: 8, + height: 8, + margin: const EdgeInsets.only(right: 8), + decoration: const BoxDecoration( + color: AppColors.error, shape: BoxShape.circle), + ), + const Icon(Icons.chevron_right_rounded, color: AppColors.onSurfaceVariant), + ], + ), + ), + ); + } + + void _confirmLogout(BuildContext context, AppState state) { + showDialog( + context: context, + builder: (_) => AlertDialog( + backgroundColor: AppColors.surfaceContainerLowest, + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)), + title: Text('Log out?', style: AppTheme.headlineSm), + content: Text('You can sign back in anytime with your number.', + style: AppTheme.bodyMd.copyWith(color: AppColors.onSurfaceVariant)), + actions: [ + TextButton( + onPressed: () => Navigator.pop(context), + child: Text('Cancel', + style: AppTheme.labelMd.copyWith(color: AppColors.onSurfaceVariant)), + ), + TextButton( + onPressed: () { + state.logout(); + Navigator.pop(context); + Navigator.pushNamedAndRemoveUntil(context, Routes.login, (r) => false); + }, + child: Text('Logout', style: AppTheme.labelMd.copyWith(color: AppColors.primary)), + ), + ], + ), + ); + } +} + +class _ProfileStatCard extends StatelessWidget { + final String value; + final String label; + final IconData icon; + final Color color; + + const _ProfileStatCard({required this.value, required this.label, required this.icon, required this.color}); + + @override + Widget build(BuildContext context) { + return Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(20), + border: Border.all(color: AppColors.surfaceContainerHigh, width: 1), + boxShadow: [ + BoxShadow(color: Colors.black.withValues(alpha: 0.03), blurRadius: 10, offset: const Offset(0, 4)), + ], + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + color: color.withValues(alpha: 0.1), + borderRadius: BorderRadius.circular(12), + ), + child: Icon(icon, color: color, size: 20), + ), + const SizedBox(height: 16), + Text(label, style: AppTheme.caption.copyWith(fontWeight: FontWeight.w600)), + const SizedBox(height: 4), + Text(value, style: AppTheme.headlineMd.copyWith(color: AppColors.onSurface)), + ], + ), + ); + } +} diff --git a/lib/features/dashboard/screens/saved_addresses_screen.dart b/lib/features/dashboard/screens/saved_addresses_screen.dart new file mode 100644 index 0000000..392b38b --- /dev/null +++ b/lib/features/dashboard/screens/saved_addresses_screen.dart @@ -0,0 +1,217 @@ +import 'package:flutter/material.dart'; + +import 'package:doormile/core/theme/app_colors.dart'; +import 'package:doormile/core/theme/app_theme.dart'; +import 'package:doormile/core/widgets/common.dart'; + +class SavedAddressesScreen extends StatefulWidget { + const SavedAddressesScreen({super.key}); + + @override + State createState() => _SavedAddressesScreenState(); +} + +class _SavedAddressesScreenState extends State { + final List> _addresses = [ + { + 'title': 'Home', + 'address': '12th Main, Indiranagar, Bengaluru, Karnataka 560038', + 'icon': Icons.home_rounded, + }, + { + 'title': 'Office', + 'address': 'Sector 4, HSR Layout, Bengaluru, Karnataka 560102', + 'icon': Icons.business_rounded, + }, + { + 'title': "Mom's House", + 'address': 'Jayanagar 4th Block, Bengaluru, Karnataka 560011', + 'icon': Icons.favorite_rounded, + }, + ]; + + void _showAddAddressSheet() { + final titleCtrl = TextEditingController(); + final addressCtrl = TextEditingController(); + + showModalBottomSheet( + context: context, + isScrollControlled: true, + backgroundColor: Colors.transparent, + builder: (context) => Container( + padding: EdgeInsets.only( + bottom: MediaQuery.of(context).viewInsets.bottom, + ), + decoration: const BoxDecoration( + color: AppColors.surface, + borderRadius: BorderRadius.vertical(top: Radius.circular(28)), + ), + child: SafeArea( + child: Padding( + padding: const EdgeInsets.all(24), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text('Add New Address', style: AppTheme.headlineSm), + IconButton( + icon: const Icon(Icons.close_rounded), + onPressed: () => Navigator.pop(context), + ), + ], + ), + const SizedBox(height: 24), + Text('LABEL (e.g. Gym)', style: AppTheme.caption.copyWith(letterSpacing: 1)), + const SizedBox(height: 8), + TextField( + controller: titleCtrl, + decoration: InputDecoration( + hintText: 'Enter label...', + filled: true, + fillColor: AppColors.surfaceContainerLowest, + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(16), + borderSide: const BorderSide(color: AppColors.surfaceContainer), + ), + enabledBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(16), + borderSide: const BorderSide(color: AppColors.surfaceContainer), + ), + ), + ), + const SizedBox(height: 16), + Text('FULL ADDRESS', style: AppTheme.caption.copyWith(letterSpacing: 1)), + const SizedBox(height: 8), + TextField( + controller: addressCtrl, + maxLines: 3, + decoration: InputDecoration( + hintText: 'Enter complete address...', + filled: true, + fillColor: AppColors.surfaceContainerLowest, + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(16), + borderSide: const BorderSide(color: AppColors.surfaceContainer), + ), + enabledBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(16), + borderSide: const BorderSide(color: AppColors.surfaceContainer), + ), + ), + ), + const SizedBox(height: 24), + PrimaryButton( + label: 'Save Address', + onPressed: () { + if (titleCtrl.text.trim().isNotEmpty && addressCtrl.text.trim().isNotEmpty) { + setState(() { + _addresses.add({ + 'title': titleCtrl.text.trim(), + 'address': addressCtrl.text.trim(), + 'icon': Icons.location_on_rounded, // default icon + }); + }); + Navigator.pop(context); + } + }, + ), + ], + ), + ), + ), + ), + ); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: AppColors.surface, + body: AppBackground( + child: Column( + children: [ + const ImmersiveHeader( + title: 'Saved Addresses', + subtitle: 'Manage your pickup & drop-off locations', + icon: Icons.location_on_rounded, + showBack: true, + ), + Expanded( + child: ListView.separated( + padding: const EdgeInsets.all(20), + itemCount: _addresses.length + 1, + separatorBuilder: (_, __) => const SizedBox(height: 16), + itemBuilder: (context, i) { + if (i == _addresses.length) { + return Padding( + padding: const EdgeInsets.only(top: 8), + child: PrimaryButton( + label: 'Add New Address', + trailingIcon: Icons.add_rounded, + onPressed: _showAddAddressSheet, + ), + ); + } + final item = _addresses[i]; + return _AddressCard( + title: item['title'], + address: item['address'], + icon: item['icon'], + ); + }, + ), + ), + ], + ), + ), + ); + } +} + +class _AddressCard extends StatelessWidget { + final String title; + final String address; + final IconData icon; + + const _AddressCard({required this.title, required this.address, required this.icon}); + + @override + Widget build(BuildContext context) { + return AppCard( + padding: const EdgeInsets.all(16), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: AppColors.primary.withValues(alpha: 0.08), + borderRadius: BorderRadius.circular(14), + ), + child: Icon(icon, color: AppColors.primary, size: 24), + ), + const SizedBox(width: 16), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text(title, style: AppTheme.labelMd), + const Icon(Icons.more_horiz_rounded, color: AppColors.onSurfaceVariant, size: 20), + ], + ), + const SizedBox(height: 4), + Text(address, style: AppTheme.bodyMd.copyWith(color: AppColors.onSurfaceVariant)), + ], + ), + ), + ], + ), + ); + } +} diff --git a/lib/features/dashboard/screens/summary_screen.dart b/lib/features/dashboard/screens/summary_screen.dart new file mode 100644 index 0000000..44d7f48 --- /dev/null +++ b/lib/features/dashboard/screens/summary_screen.dart @@ -0,0 +1,168 @@ +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; + +import 'package:doormile/shared/state/app_state.dart'; +import 'package:doormile/core/theme/app_colors.dart'; +import 'package:doormile/core/theme/app_theme.dart'; +import 'package:doormile/core/widgets/common.dart'; + +class SummaryScreen extends StatelessWidget { + const SummaryScreen({super.key}); + + @override + Widget build(BuildContext context) { + final state = context.watch(); + + // Dynamically calculate stats from real orders + final totalDeliveries = state.orders.length; + final totalSpent = state.orders.fold(0, (sum, order) => sum + order.amount); + + return AppBackground( + child: Column( + children: [ + const ImmersiveHeader( + title: 'Summary', + subtitle: 'Your delivery and eco-impact stats', + icon: Icons.bar_chart_rounded, + ), + Expanded( + child: SingleChildScrollView( + padding: const EdgeInsets.only(bottom: 100), + child: Column( + children: [ + const SizedBox(height: 24), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 20), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SectionHeader(title: 'Overview', action: 'This Month', onAction: () {}), + const SizedBox(height: 12), + Row( + children: [ + _StatCard( + title: 'Total Deliveries', + value: '$totalDeliveries', + icon: Icons.inventory_2_rounded, + color: AppColors.primary, + ), + const SizedBox(width: 16), + _StatCard( + title: 'Total Spent', + value: '₹${totalSpent.toStringAsFixed(0)}', + icon: Icons.payments_rounded, + color: AppColors.tertiary, + ), + ], + ), + + const SizedBox(height: 24), + SectionHeader(title: 'Top Locations', action: 'Manage', onAction: () => Navigator.pushNamed(context, '/addresses')), + const SizedBox(height: 12), + AppCard( + onTap: () => Navigator.pushNamed(context, '/addresses'), + padding: const EdgeInsets.all(0), + child: Column( + children: [ + _LocationRow(title: 'Home', subtitle: 'Indiranagar, Bengaluru', isFirst: true), + const Divider(height: 1, color: AppColors.surfaceContainer), + _LocationRow(title: 'Office', subtitle: 'HSR Layout, Bengaluru', isFirst: false), + ], + ), + ), + ], + ), + ), + ], + ), + ), + ), + ], + ), + ); + } +} + +class _StatCard extends StatelessWidget { + final String title; + final String value; + final IconData icon; + final Color color; + + const _StatCard({required this.title, required this.value, required this.icon, required this.color}); + + @override + Widget build(BuildContext context) { + return Expanded( + child: Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(20), + border: Border.all(color: AppColors.surfaceContainerHigh, width: 1), + boxShadow: [ + BoxShadow(color: Colors.black.withValues(alpha: 0.03), blurRadius: 10, offset: const Offset(0, 4)), + ], + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + color: color.withValues(alpha: 0.1), + borderRadius: BorderRadius.circular(12), + ), + child: Icon(icon, color: color, size: 20), + ), + const SizedBox(height: 16), + Text(title, style: AppTheme.caption.copyWith(fontWeight: FontWeight.w600)), + const SizedBox(height: 4), + Text(value, style: AppTheme.headlineMd.copyWith(color: AppColors.onSurface)), + ], + ), + ), + ); + } +} + + + +class _LocationRow extends StatelessWidget { + final String title; + final String subtitle; + final bool isFirst; + const _LocationRow({required this.title, required this.subtitle, required this.isFirst}); + + @override + Widget build(BuildContext context) { + return Padding( + padding: EdgeInsets.fromLTRB(16, isFirst ? 16 : 12, 16, isFirst ? 12 : 16), + child: Row( + children: [ + Container( + padding: const EdgeInsets.all(10), + decoration: BoxDecoration( + color: AppColors.surfaceContainerLowest, + shape: BoxShape.circle, + border: Border.all(color: AppColors.surfaceContainerHigh), + ), + child: const Icon(Icons.business_rounded, size: 20, color: AppColors.primary), + ), + const SizedBox(width: 16), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(title, style: AppTheme.labelMd.copyWith(fontWeight: FontWeight.w600)), + const SizedBox(height: 2), + Text(subtitle, style: AppTheme.caption), + ], + ), + ), + const Icon(Icons.chevron_right_rounded, color: AppColors.onSurfaceVariant), + ], + ), + ); + } +} diff --git a/lib/features/support/screens/chat_screen.dart b/lib/features/support/screens/chat_screen.dart new file mode 100644 index 0000000..09c2b6e --- /dev/null +++ b/lib/features/support/screens/chat_screen.dart @@ -0,0 +1,124 @@ +import 'package:flutter/material.dart'; + +import 'package:doormile/core/theme/app_colors.dart'; +import 'package:doormile/core/theme/app_theme.dart'; +import 'package:doormile/core/widgets/common.dart'; + +class ChatScreen extends StatefulWidget { + final String riderName; + const ChatScreen({super.key, required this.riderName}); + + @override + State createState() => _ChatScreenState(); +} + +class _ChatScreenState extends State { + final TextEditingController _messageController = TextEditingController(); + final List _messages = [ + 'Hi! I am picking up your parcel now.', + 'Please let me know if there are any specific instructions.', + ]; + + @override + void dispose() { + _messageController.dispose(); + super.dispose(); + } + + void _sendMessage() { + if (_messageController.text.trim().isEmpty) return; + setState(() { + _messages.add(_messageController.text.trim()); + _messageController.clear(); + }); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: AppColors.surface, + appBar: AppBar( + backgroundColor: AppColors.primary, + foregroundColor: Colors.white, + title: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(widget.riderName, style: AppTheme.headlineSm.copyWith(color: Colors.white, fontSize: 18)), + Text('Doormile EV Rider', style: AppTheme.caption.copyWith(color: Colors.white70)), + ], + ), + ), + body: Column( + children: [ + Expanded( + child: ListView.builder( + padding: const EdgeInsets.all(16), + itemCount: _messages.length, + itemBuilder: (context, index) { + final isRider = index < 2; // Mocking first 2 messages from rider + return Align( + alignment: isRider ? Alignment.centerLeft : Alignment.centerRight, + child: Container( + margin: const EdgeInsets.only(bottom: 12), + constraints: const BoxConstraints(maxWidth: 280), + padding: const EdgeInsets.all(14), + decoration: BoxDecoration( + color: isRider ? AppColors.surfaceContainerHigh : AppColors.primary, + borderRadius: BorderRadius.circular(16).copyWith( + bottomLeft: isRider ? Radius.zero : const Radius.circular(16), + bottomRight: !isRider ? Radius.zero : const Radius.circular(16), + ), + ), + child: Text( + _messages[index], + style: AppTheme.bodyMd.copyWith(color: isRider ? AppColors.onSurface : Colors.white), + ), + ), + ); + }, + ), + ), + Container( + padding: const EdgeInsets.all(16).copyWith(bottom: MediaQuery.of(context).padding.bottom + 16), + decoration: const BoxDecoration( + color: Colors.white, + border: Border(top: BorderSide(color: AppColors.surfaceContainer)), + ), + child: Row( + children: [ + Expanded( + child: TextField( + controller: _messageController, + decoration: InputDecoration( + hintText: 'Message ${widget.riderName}...', + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(999), + borderSide: BorderSide.none, + ), + filled: true, + fillColor: AppColors.surfaceContainerLowest, + contentPadding: const EdgeInsets.symmetric(horizontal: 20, vertical: 14), + ), + ), + ), + const SizedBox(width: 12), + InkWell( + onTap: _sendMessage, + borderRadius: BorderRadius.circular(999), + child: Container( + padding: const EdgeInsets.all(14), + decoration: const BoxDecoration( + color: AppColors.primary, + shape: BoxShape.circle, + ), + child: const Icon(Icons.send_rounded, color: Colors.white, size: 20), + ), + ), + ], + ), + ), + ], + ), + ); + } +} diff --git a/lib/features/support/screens/help_support_screen.dart b/lib/features/support/screens/help_support_screen.dart new file mode 100644 index 0000000..99c4b39 --- /dev/null +++ b/lib/features/support/screens/help_support_screen.dart @@ -0,0 +1,258 @@ +import 'package:flutter/material.dart'; + +import 'package:doormile/shared/models/models.dart'; +import 'package:doormile/core/theme/app_colors.dart'; +import 'package:doormile/core/theme/app_theme.dart'; +import 'package:doormile/core/widgets/common.dart'; + +class HelpSupportScreen extends StatefulWidget { + const HelpSupportScreen({super.key}); + + @override + State createState() => _HelpSupportScreenState(); +} + +class _HelpSupportScreenState extends State { + final _faqs = const [ + FaqItem('How do I track?', + 'Track your delivery in real-time through the "Track" tab in the bottom navigation. Our EV fleet provides precision GPS updates every 10 seconds.'), + FaqItem('Price calculation?', + 'Fare = base fare + distance charge + any demand/weather surge. EV sustainability discounts are applied automatically at checkout.'), + FaqItem('Cancellation?', + 'You can cancel free of charge before a rider is assigned. After assignment, a small fee may apply to cover the EV trip.'), + ]; + + final List _messages = [ + const ChatMessage("Hi there! I'm Doormile Assistant. How can I help you today with your EV delivery?"), + const ChatMessage('I need to update my delivery address for order #DM-9921.', fromUser: true), + const ChatMessage('Sure! Our system shows your EV driver is still 10 mins away from pickup. I can update that for you now.'), + ]; + + final _input = TextEditingController(); + int? _openFaq = 0; + + void _send() { + final text = _input.text.trim(); + if (text.isEmpty) return; + setState(() { + _messages.add(ChatMessage(text, fromUser: true)); + _input.clear(); + }); + Future.delayed(const Duration(milliseconds: 700), () { + if (!mounted) return; + setState(() => _messages.add(const ChatMessage( + 'Thanks! A Doormile specialist will follow up shortly. Is there anything else?'))); + }); + } + + @override + void dispose() { + _input.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: AppColors.surface, + appBar: AppBar( + leading: BackButton(color: AppColors.primary, onPressed: () => Navigator.pop(context)), + title: Text('Help & Support', + style: AppTheme.headlineMd.copyWith(color: AppColors.primary)), + actions: const [ + Padding( + padding: EdgeInsets.only(right: 16), + child: Icon(Icons.help_outline, color: AppColors.primary), + ), + ], + ), + body: ListView( + padding: const EdgeInsets.fromLTRB(16, 8, 16, 24), + children: [ + // Search + Container( + padding: const EdgeInsets.symmetric(horizontal: 14), + decoration: BoxDecoration( + color: AppColors.surfaceContainerLowest, + borderRadius: BorderRadius.circular(14), + boxShadow: AppTheme.cardShadow, + ), + child: Row( + children: [ + const Icon(Icons.search, color: AppColors.onSurfaceVariant), + const SizedBox(width: 8), + Expanded( + child: TextField( + cursorColor: AppColors.primary, + decoration: InputDecoration( + hintText: 'Search for help...', + hintStyle: AppTheme.bodyMd.copyWith(color: AppColors.outline), + border: InputBorder.none, + ), + ), + ), + ], + ), + ), + const SizedBox(height: 20), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Flexible( + child: Text('Frequently Asked Questions', + style: AppTheme.headlineSm, overflow: TextOverflow.ellipsis), + ), + const SizedBox(width: 8), + Text('View all', + style: AppTheme.labelMd.copyWith(color: AppColors.primary)), + ], + ), + const SizedBox(height: 12), + ...List.generate(_faqs.length, (i) { + final open = _openFaq == i; + return Padding( + padding: const EdgeInsets.only(bottom: 10), + child: AppCard( + onTap: () => setState(() => _openFaq = open ? null : i), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Expanded( + child: Text(_faqs[i].question, + style: AppTheme.labelMd.copyWith(fontWeight: FontWeight.w700)), + ), + Icon(open ? Icons.expand_less : Icons.expand_more, + color: AppColors.onSurfaceVariant), + ], + ), + AnimatedCrossFade( + duration: const Duration(milliseconds: 200), + crossFadeState: + open ? CrossFadeState.showFirst : CrossFadeState.showSecond, + firstChild: Padding( + padding: const EdgeInsets.only(top: 8), + child: Text(_faqs[i].answer, + style: AppTheme.bodyMd + .copyWith(color: AppColors.onSurfaceVariant, height: 1.4)), + ), + secondChild: const SizedBox(width: double.infinity), + ), + ], + ), + ), + ); + }), + const SizedBox(height: 16), + Text('Recent Support', style: AppTheme.headlineSm), + const SizedBox(height: 12), + Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: AppColors.surfaceContainer, + borderRadius: BorderRadius.circular(16), + ), + child: Column( + children: [ + ..._messages.map(_bubble), + const SizedBox(height: 8), + _composer(), + ], + ), + ), + const SizedBox(height: 16), + // Chat with us banner + Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: AppColors.primaryContainer, + borderRadius: BorderRadius.circular(16), + ), + child: Row( + children: [ + Container( + width: 44, + height: 44, + decoration: BoxDecoration( + color: Colors.white.withValues(alpha: 0.2), + borderRadius: BorderRadius.circular(12)), + child: const Icon(Icons.chat, color: Colors.white), + ), + const SizedBox(width: 14), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text('Chat with us', + style: AppTheme.labelMd.copyWith( + color: Colors.white, fontWeight: FontWeight.w700)), + Text('Get instant support from our team', + style: AppTheme.caption + .copyWith(color: Colors.white.withValues(alpha: 0.9))), + ], + ), + ), + const Icon(Icons.chevron_right, color: Colors.white), + ], + ), + ), + ], + ), + ); + } + + Widget _bubble(ChatMessage m) { + return Align( + alignment: m.fromUser ? Alignment.centerRight : Alignment.centerLeft, + child: Container( + margin: const EdgeInsets.symmetric(vertical: 4), + padding: const EdgeInsets.all(12), + constraints: const BoxConstraints(maxWidth: 260), + decoration: BoxDecoration( + color: m.fromUser ? AppColors.primary : AppColors.surfaceContainerLowest, + borderRadius: BorderRadius.circular(14), + ), + child: Text(m.text, + style: AppTheme.bodyMd.copyWith( + color: m.fromUser ? Colors.white : AppColors.onSurface, height: 1.35)), + ), + ); + } + + Widget _composer() { + return Container( + padding: const EdgeInsets.only(left: 14, right: 6), + decoration: BoxDecoration( + color: AppColors.surfaceContainerLowest, + borderRadius: BorderRadius.circular(999), + ), + child: Row( + children: [ + Expanded( + child: TextField( + controller: _input, + cursorColor: AppColors.primary, + onSubmitted: (_) => _send(), + decoration: InputDecoration( + hintText: 'Type a message...', + hintStyle: AppTheme.bodyMd.copyWith(color: AppColors.outline), + border: InputBorder.none, + ), + ), + ), + GestureDetector( + onTap: _send, + child: Container( + width: 40, + height: 40, + decoration: const BoxDecoration( + color: AppColors.primary, shape: BoxShape.circle), + child: const Icon(Icons.send, color: Colors.white, size: 18), + ), + ), + ], + ), + ); + } +} diff --git a/lib/features/tracking/screens/live_tracking_screen.dart b/lib/features/tracking/screens/live_tracking_screen.dart new file mode 100644 index 0000000..8e685a2 --- /dev/null +++ b/lib/features/tracking/screens/live_tracking_screen.dart @@ -0,0 +1,362 @@ +import 'package:flutter/material.dart'; +import 'package:url_launcher/url_launcher.dart'; + +import 'package:doormile/shared/models/models.dart'; +import 'package:doormile/core/theme/app_colors.dart'; +import 'package:doormile/core/theme/app_theme.dart'; +import 'package:doormile/core/widgets/common.dart'; + +class LiveTrackingScreen extends StatelessWidget { + final Order? order; + const LiveTrackingScreen({super.key, this.order}); + + @override + Widget build(BuildContext context) { + final id = order?.id ?? 'DM-CHN-BLR-7741'; + final eta = order?.eta ?? '2:30 PM'; + return Scaffold( + backgroundColor: AppColors.surface, + body: Stack( + children: [ + // Faux map + Positioned.fill( + child: CustomPaint(painter: _MapPainter()), + ), + // Top bar + SafeArea( + child: Padding( + padding: const EdgeInsets.all(12), + child: Row( + children: [ + _circleBtn(Icons.arrow_back, () => Navigator.pop(context)), + const SizedBox(width: 10), + Expanded( + child: Text('Live Tracking', + style: AppTheme.headlineSm.copyWith(color: AppColors.primary), + overflow: TextOverflow.ellipsis), + ), + const SizedBox(width: 8), + StatusChip( + label: 'EV Priority', + icon: Icons.eco, + color: AppColors.tertiary, + bg: Colors.white, + ), + ], + ), + ), + ), + // Bottom detail sheet + Align( + alignment: Alignment.bottomCenter, + child: Container( + width: double.infinity, + decoration: const BoxDecoration( + color: AppColors.surface, + borderRadius: BorderRadius.vertical(top: Radius.circular(24)), + boxShadow: [ + BoxShadow(color: Color(0x1A000000), blurRadius: 24, offset: Offset(0, -6)), + ], + ), + child: SafeArea( + top: false, + child: Padding( + padding: const EdgeInsets.fromLTRB(16, 14, 16, 16), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text('PARCEL ID', + style: AppTheme.caption.copyWith(letterSpacing: 1)), + Text(id, + style: AppTheme.headlineSm + .copyWith(fontWeight: FontWeight.w700)), + ], + ), + ), + StatusChip( + label: 'In transit', + color: AppColors.tertiary, + bg: AppColors.tertiaryFixed.withValues(alpha: 0.5), + ), + ], + ), + const SizedBox(height: 4), + Text('ETA $eta', + style: AppTheme.labelMd.copyWith(color: AppColors.primary)), + const SizedBox(height: 16), + const _MileStepper(activeIndex: 2), + const SizedBox(height: 16), + _riderCard(context), + const SizedBox(height: 12), + const EcoBanner( + text: 'Eco-Friendly Journey · saving 1.2kg of CO₂ on this delivery.'), + ], + ), + ), + ), + ), + ), + ], + ), + ); + } + + Widget _circleBtn(IconData icon, VoidCallback onTap) { + return Material( + color: Colors.white, + shape: const CircleBorder(), + elevation: 2, + child: InkWell( + onTap: onTap, + customBorder: const CircleBorder(), + child: SizedBox( + width: 40, height: 40, child: Icon(icon, color: AppColors.primary, size: 22)), + ), + ); + } + + Widget _riderCard(BuildContext context) { + return AppCard( + color: AppColors.surfaceContainerLow, + child: Column( + children: [ + Row( + children: [ + const CircleAvatar( + radius: 24, + backgroundColor: AppColors.primaryFixed, + child: Icon(Icons.person, color: AppColors.primary, size: 28), + ), + const SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text('Rahul Sharma', + style: AppTheme.labelMd.copyWith(fontWeight: FontWeight.w700)), + Row( + children: [ + StatusChip( + label: 'EV', + icon: Icons.bolt, + color: AppColors.tertiary, + bg: AppColors.tertiaryFixed.withValues(alpha: 0.5)), + const SizedBox(width: 6), + Flexible( + child: Text('KA-01-EV-4412', + style: AppTheme.caption, overflow: TextOverflow.ellipsis), + ), + ], + ), + ], + ), + ), + Column( + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + Text('★ 4.9', + style: AppTheme.labelMd.copyWith(color: AppColors.warning)), + Text('4821 trips', style: AppTheme.caption), + ], + ), + ], + ), + const SizedBox(height: 14), + Row( + children: [ + Expanded( + child: OutlinedButton.icon( + onPressed: () async { + final uri = Uri(scheme: 'tel', path: '+919876543210'); + try { + await launchUrl(uri); + } catch (e) { + if (context.mounted) _toast(context, 'Could not launch dialer.'); + } + }, + icon: const Icon(Icons.call, size: 18), + label: const Text('Call Rider'), + style: OutlinedButton.styleFrom( + foregroundColor: AppColors.primary, + side: const BorderSide(color: AppColors.primary), + padding: const EdgeInsets.symmetric(vertical: 12), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12)), + ), + ), + ), + const SizedBox(width: 12), + Expanded( + child: ElevatedButton.icon( + onPressed: () => Navigator.pushNamed(context, '/chat', arguments: 'Rahul Sharma'), + icon: const Icon(Icons.chat_bubble_outline, size: 18), + label: const Text('Chat'), + style: ElevatedButton.styleFrom( + backgroundColor: AppColors.primary, + foregroundColor: Colors.white, + elevation: 0, + padding: const EdgeInsets.symmetric(vertical: 12), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12)), + ), + ), + ), + ], + ), + ], + ), + ); + } + + void _toast(BuildContext context, String msg) { + ScaffoldMessenger.of(context) + ..hideCurrentSnackBar() + ..showSnackBar(SnackBar( + content: Text(msg), + backgroundColor: AppColors.inverseSurface, + behavior: SnackBarBehavior.floating, + )); + } +} + +class _MileStepper extends StatelessWidget { + final int activeIndex; + const _MileStepper({required this.activeIndex}); + + static const _stops = [ + ('Booked', AppColors.primary), + ('First Mile', AppColors.firstMile), + ('Mid Mile', AppColors.midMile), + ('Last Mile', AppColors.lastMile), + ]; + + @override + Widget build(BuildContext context) { + return Row( + children: List.generate(_stops.length, (i) { + final done = i <= activeIndex; + final stop = _stops[i]; + return Expanded( + child: Column( + children: [ + Row( + children: [ + Expanded( + child: Container( + height: 3, + color: i == 0 + ? Colors.transparent + : (i <= activeIndex ? stop.$2 : AppColors.surfaceContainerHigh), + ), + ), + Container( + width: 14, + height: 14, + decoration: BoxDecoration( + shape: BoxShape.circle, + color: done ? stop.$2 : AppColors.surfaceContainerHigh, + border: Border.all( + color: done ? stop.$2 : AppColors.outlineVariant, width: 2), + ), + child: done + ? const Icon(Icons.check, size: 8, color: Colors.white) + : null, + ), + Expanded( + child: Container( + height: 3, + color: i == _stops.length - 1 + ? Colors.transparent + : (i < activeIndex ? _stops[i + 1].$2 : AppColors.surfaceContainerHigh), + ), + ), + ], + ), + const SizedBox(height: 6), + Text(stop.$1, + style: AppTheme.caption.copyWith( + color: done ? stop.$2 : AppColors.onSurfaceVariant, + fontWeight: i == activeIndex ? FontWeight.w700 : FontWeight.w500, + )), + ], + ), + ); + }), + ); + } +} + +/// Lightweight stylised street map with a dashed delivery route. +class _MapPainter extends CustomPainter { + @override + void paint(Canvas canvas, Size size) { + final bg = Paint()..color = const Color(0xFFEDEAE9); + canvas.drawRect(Offset.zero & size, bg); + + final road = Paint() + ..color = Colors.white + ..strokeWidth = 10; + final thin = Paint() + ..color = const Color(0xFFE0DCDB) + ..strokeWidth = 4; + + for (double x = 30; x < size.width; x += 70) { + canvas.drawLine(Offset(x, 0), Offset(x, size.height), thin); + } + for (double y = 80; y < size.height; y += 70) { + canvas.drawLine(Offset(0, y), Offset(size.width, y), thin); + } + canvas.drawLine(Offset(0, size.height * 0.35), + Offset(size.width, size.height * 0.42), road); + canvas.drawLine(Offset(size.width * 0.4, 0), + Offset(size.width * 0.5, size.height * 0.7), road); + + // Dashed route + final routePaint = Paint() + ..color = AppColors.primary + ..strokeWidth = 4 + ..style = PaintingStyle.stroke; + final path = Path() + ..moveTo(size.width * 0.2, size.height * 0.6) + ..quadraticBezierTo(size.width * 0.35, size.height * 0.3, + size.width * 0.55, size.height * 0.35) + ..quadraticBezierTo(size.width * 0.75, size.height * 0.4, + size.width * 0.7, size.height * 0.18); + _drawDashed(canvas, path, routePaint); + + // Origin & destination markers + final dot = Paint()..color = AppColors.primary; + canvas.drawCircle(Offset(size.width * 0.2, size.height * 0.6), 7, dot); + canvas.drawCircle(Offset(size.width * 0.2, size.height * 0.6), 3, + Paint()..color = Colors.white); + canvas.drawCircle(Offset(size.width * 0.7, size.height * 0.18), 9, + Paint()..color = AppColors.primary); + // Vehicle marker (mid route) + final mid = Offset(size.width * 0.55, size.height * 0.35); + canvas.drawCircle(mid, 16, Paint()..color = AppColors.primary.withValues(alpha: 0.2)); + canvas.drawCircle(mid, 9, Paint()..color = AppColors.tertiary); + } + + void _drawDashed(Canvas canvas, Path path, Paint paint) { + const dash = 8.0; + const gap = 6.0; + for (final metric in path.computeMetrics()) { + double dist = 0; + while (dist < metric.length) { + canvas.drawPath( + metric.extractPath(dist, dist + dash), paint); + dist += dash + gap; + } + } + } + + @override + bool shouldRepaint(covariant CustomPainter oldDelegate) => false; +} diff --git a/lib/features/tracking/screens/my_orders_screen.dart b/lib/features/tracking/screens/my_orders_screen.dart new file mode 100644 index 0000000..30bb116 --- /dev/null +++ b/lib/features/tracking/screens/my_orders_screen.dart @@ -0,0 +1,278 @@ +import 'dart:async'; +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; + +import 'package:doormile/main.dart'; +import 'package:doormile/shared/models/models.dart'; +import 'package:doormile/shared/state/app_state.dart'; +import 'package:doormile/core/theme/app_colors.dart'; +import 'package:doormile/core/theme/app_theme.dart'; +import 'package:doormile/core/widgets/common.dart'; + +class MyOrdersScreen extends StatefulWidget { + /// When embedded in the bottom-nav shell we hide the back button. + final bool embedded; + const MyOrdersScreen({super.key, this.embedded = false}); + + @override + State createState() => _MyOrdersScreenState(); +} + +class _MyOrdersScreenState extends State { + final _filters = const ['All', 'Active', 'Delivered', 'Cancelled']; + int _filter = 0; + Timer? _pollingTimer; + + @override + void initState() { + super.initState(); + WidgetsBinding.instance.addPostFrameCallback((_) { + context.read().fetchOrders(); + }); + + // Start polling every 10 seconds for live status updates + _pollingTimer = Timer.periodic(const Duration(seconds: 10), (_) { + if (mounted) { + context.read().fetchOrders(); + } + }); + } + + @override + void dispose() { + _pollingTimer?.cancel(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final state = context.watch(); + final orders = state.orders; + final filtered = switch (_filter) { + 1 => orders.where((o) => o.status == OrderStatus.active).toList(), + 2 => orders.where((o) => o.status == OrderStatus.delivered).toList(), + 3 => orders.where((o) => o.status == OrderStatus.cancelled).toList(), + _ => orders, + }; + + return AppBackground( + child: Column( + children: [ + ImmersiveHeader( + title: 'My Parcels', + subtitle: 'Track all your deliveries', + icon: Icons.inventory_2_rounded, + showBack: !widget.embedded, + ), + const SizedBox(height: 16), + SizedBox( + height: 44, + child: ListView.separated( + scrollDirection: Axis.horizontal, + padding: const EdgeInsets.symmetric(horizontal: 16), + itemCount: _filters.length, + separatorBuilder: (_, __) => const SizedBox(width: 8), + itemBuilder: (context, i) { + final active = i == _filter; + return GestureDetector( + onTap: () => setState(() => _filter = i), + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 18), + alignment: Alignment.center, + decoration: BoxDecoration( + color: active ? AppColors.primary : AppColors.surfaceContainer, + borderRadius: BorderRadius.circular(999), + ), + child: Text(_filters[i], + style: AppTheme.labelMd.copyWith( + color: active ? Colors.white : AppColors.onSurfaceVariant)), + ), + ); + }, + ), + ), + Expanded( + child: state.isLoadingOrders + ? const Center(child: CircularProgressIndicator(color: AppColors.primary)) + : filtered.isEmpty + ? Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Container( + width: 100, + height: 100, + decoration: BoxDecoration( + shape: BoxShape.circle, + gradient: LinearGradient( + colors: [AppColors.primary.withValues(alpha: 0.1), AppColors.primary.withValues(alpha: 0.25)], + begin: Alignment.topLeft, + end: Alignment.bottomRight, + ), + boxShadow: [ + BoxShadow( + color: AppColors.primary.withValues(alpha: 0.15), + blurRadius: 24, + spreadRadius: 4, + offset: const Offset(0, 10), + ), + ], + ), + child: Center( + child: Container( + width: 68, + height: 68, + decoration: const BoxDecoration( + color: Colors.white, + shape: BoxShape.circle, + ), + child: const Icon( + Icons.inventory_2_rounded, + size: 34, + color: AppColors.primary, + ), + ), + ), + ), + const SizedBox(height: 24), + Text('No parcels found', + style: AppTheme.headlineSm.copyWith(color: AppColors.onSurface, fontWeight: FontWeight.w700)), + const SizedBox(height: 8), + Text('You have no active bookings in this category.', + style: AppTheme.bodyMd.copyWith(color: AppColors.onSurfaceVariant)), + const SizedBox(height: 48), // Padding to lift it slightly above true center + ], + ), + ) + : ListView.separated( + padding: const EdgeInsets.fromLTRB(16, 16, 16, 24), + itemCount: filtered.length, + separatorBuilder: (_, __) => const SizedBox(height: 14), + itemBuilder: (context, i) => _OrderCard(order: filtered[i]), + ), + ), + ], + ), + ); + } +} + +class _OrderCard extends StatelessWidget { + final Order order; + const _OrderCard({required this.order}); + + @override + Widget build(BuildContext context) { + final active = order.status == OrderStatus.active; + return AppCard( + onTap: () => Navigator.pushNamed( + context, + active ? Routes.liveTracking : Routes.shipmentJourney, + arguments: order, + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text('Order ID', style: AppTheme.caption), + Text('#${order.id}', + style: AppTheme.labelMd.copyWith(fontWeight: FontWeight.w700)), + ], + ), + ), + StatusChip( + label: order.status.label, color: order.status.color, bg: order.status.bg), + ], + ), + const SizedBox(height: 14), + // Route mini timeline + _routeRow(Icons.radio_button_unchecked, order.fromLabel, order.fromSub, + AppColors.primary, dashed: true), + _routeRow(Icons.location_on, order.toLabel, order.toSub, AppColors.onSurfaceVariant), + if (active) ...[ + const SizedBox(height: 12), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Flexible( + child: Text('Journey Progress', + style: AppTheme.caption.copyWith(color: AppColors.primary), + overflow: TextOverflow.ellipsis), + ), + const SizedBox(width: 8), + Text('${(order.progress * 3).toStringAsFixed(1)} / 3.0 Miles', + style: AppTheme.caption), + ], + ), + const SizedBox(height: 6), + JourneyProgressBar(value: order.progress), + ], + const SizedBox(height: 12), + Row( + children: [ + if (order.ecoRoute) ...[ + const Icon(Icons.eco, size: 14, color: AppColors.tertiary), + const SizedBox(width: 4), + Text('CO₂ saved: ${order.co2Saved} (EV)', + style: AppTheme.caption.copyWith(color: AppColors.tertiary)), + ], + ], + ), + const Divider(height: 22, color: AppColors.surfaceContainer), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text(order.date, + style: AppTheme.bodyMd.copyWith(color: AppColors.onSurfaceVariant)), + Text('₹${order.amount.toStringAsFixed(2)}', + style: AppTheme.headlineSm.copyWith(fontWeight: FontWeight.w700)), + ], + ), + ], + ), + ); + } + + Widget _routeRow(IconData icon, String title, String sub, Color color, + {bool dashed = false}) { + return IntrinsicHeight( + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Column( + children: [ + Icon(icon, size: 18, color: color), + if (dashed) + Expanded( + child: Container( + width: 1.5, + margin: const EdgeInsets.symmetric(vertical: 2), + color: AppColors.outlineVariant, + ), + ), + ], + ), + const SizedBox(width: 12), + Expanded( + child: Padding( + padding: EdgeInsets.only(bottom: dashed ? 10 : 0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(title, style: AppTheme.labelMd.copyWith(fontWeight: FontWeight.w700)), + Text(sub, style: AppTheme.caption), + ], + ), + ), + ), + ], + ), + ); + } +} diff --git a/lib/features/tracking/screens/shipment_journey_screen.dart b/lib/features/tracking/screens/shipment_journey_screen.dart new file mode 100644 index 0000000..d22f465 --- /dev/null +++ b/lib/features/tracking/screens/shipment_journey_screen.dart @@ -0,0 +1,319 @@ +import 'package:flutter/material.dart'; + +import 'package:doormile/shared/models/models.dart'; +import 'package:doormile/core/theme/app_colors.dart'; +import 'package:doormile/core/theme/app_theme.dart'; +import 'package:doormile/core/widgets/common.dart'; +import 'package:doormile/core/widgets/logo.dart'; + +class ShipmentJourneyScreen extends StatelessWidget { + final Order? order; + const ShipmentJourneyScreen({super.key, this.order}); + + @override + Widget build(BuildContext context) { + final steps = const [ + JourneyStep( + title: 'First Mile', + subtitle: 'Picked up from Guindy Hub, Chennai\nAssigned to EV-Fleet #TN01-EV-4421', + time: '7 Jun, 10:45 AM', + icon: Icons.inventory_2, + color: AppColors.firstMile, + done: true, + ), + JourneyStep( + title: 'Mid Mile · IN TRANSIT', + subtitle: + 'Departed Chennai Regional Hub\nEnroute to Nelamangala Distribution Center', + time: 'Active Now', + icon: Icons.local_shipping, + color: AppColors.midMile, + active: true, + note: + 'AI REROUTE · Avoided AH45 congestion near Krishnagiri. Diverted to optimized arterial route.', + ), + JourneyStep( + title: 'Last Mile', + subtitle: 'Out for Delivery\nTo: Indiranagar, Bengaluru Hub', + time: 'Expected 9 Jun', + icon: Icons.home_filled, + color: AppColors.lastMile, + ), + ]; + + final logs = const [ + ActivityLog('Security Check Complete', 'Chennai Regional Center · 08 Jun 14:12', + Icons.verified_user_outlined), + ActivityLog('Sorted for Destination', 'Chennai Regional Center · 08 Jun 11:30', + Icons.alt_route), + ActivityLog('Arrived at Sorting Hub', 'Chennai Hub A · 07 Jun 18:45', + Icons.warehouse_outlined), + ActivityLog('Shipment Created', 'Electronic City Office · 07 Jun 09:15', + Icons.add_box_outlined), + ]; + + return Scaffold( + backgroundColor: AppColors.surface, + appBar: AppBar( + leading: BackButton(color: AppColors.primary, onPressed: () => Navigator.pop(context)), + title: const DoormileWordmark(markSize: 32, fontSize: 22), + actions: const [ + Padding( + padding: EdgeInsets.only(right: 16), + child: Icon(Icons.location_on_outlined, color: AppColors.primary), + ), + ], + ), + body: ListView( + padding: const EdgeInsets.fromLTRB(16, 8, 16, 24), + children: [ + // Summary card + AppCard( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text('Shipment Contents', style: AppTheme.caption), + Text('Documents & electronics · 3.2 kg', + style: AppTheme.labelMd.copyWith(fontWeight: FontWeight.w700)), + ], + ), + ), + StatusChip( + label: 'EV EXPRESS', + icon: Icons.eco, + color: AppColors.tertiary, + bg: AppColors.tertiaryFixed.withValues(alpha: 0.6), + ), + ], + ), + const SizedBox(height: 16), + Row( + children: [ + _city('MAA', 'Chennai'), + const Expanded( + child: Icon(Icons.bolt, color: AppColors.primary), + ), + _city('BLR', 'Bengaluru', end: true), + ], + ), + const Divider(height: 28, color: AppColors.surfaceContainer), + Row( + children: [ + const Icon(Icons.schedule, size: 18, color: AppColors.onSurfaceVariant), + const SizedBox(width: 8), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text('Estimated Arrival', style: AppTheme.caption), + Text('9 Jun 2:30 PM', + style: AppTheme.labelMd.copyWith(fontWeight: FontWeight.w700)), + ], + ), + ), + PrimaryButton( + label: 'Track Live', + height: 44, + onPressed: () => Navigator.pushNamed(context, '/tracking/live', + arguments: order), + ).constrainedTo(120), + ], + ), + ], + ), + ), + const SizedBox(height: 12), + // AI insight banner + Container( + padding: const EdgeInsets.all(14), + decoration: BoxDecoration( + color: AppColors.tertiaryFixed.withValues(alpha: 0.35), + borderRadius: BorderRadius.circular(14), + border: Border.all(color: AppColors.tertiary.withValues(alpha: 0.15)), + ), + child: Row( + children: [ + const Icon(Icons.psychology_alt, color: AppColors.tertiary), + const SizedBox(width: 12), + Expanded( + child: Text('MileTruth AI Insight · Rerouted via Hosur bypass — ETA protected despite traffic.', + style: AppTheme.bodyMd + .copyWith(color: AppColors.tertiary, height: 1.35)), + ), + ], + ), + ), + const SizedBox(height: 24), + Text('Journey Timeline', style: AppTheme.headlineMd), + const SizedBox(height: 12), + ...List.generate(steps.length, (i) => + _TimelineTile(step: steps[i], isLast: i == steps.length - 1)), + const SizedBox(height: 16), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text('Activity Log', style: AppTheme.headlineMd), + Text('All Updates', + style: AppTheme.labelMd.copyWith(color: AppColors.primary)), + ], + ), + const SizedBox(height: 12), + AppCard( + child: Column( + children: [ + for (int i = 0; i < logs.length; i++) ...[ + Row( + children: [ + IconBadge( + icon: logs[i].icon, + bg: AppColors.surfaceContainer, + color: AppColors.onSurfaceVariant, + size: 38, + iconSize: 18, + circle: true, + ), + const SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(logs[i].title, + style: AppTheme.labelMd + .copyWith(fontWeight: FontWeight.w700)), + Text(logs[i].subtitle, style: AppTheme.caption), + ], + ), + ), + ], + ), + if (i < logs.length - 1) + const Divider(height: 22, color: AppColors.surfaceContainer), + ], + ], + ), + ), + ], + ), + ); + } + + Widget _city(String code, String name, {bool end = false}) { + return Column( + crossAxisAlignment: end ? CrossAxisAlignment.end : CrossAxisAlignment.start, + children: [ + Text(code, style: AppTheme.headlineMd.copyWith(color: AppColors.primary)), + Text(name, style: AppTheme.caption), + ], + ); + } +} + +class _TimelineTile extends StatelessWidget { + final JourneyStep step; + final bool isLast; + const _TimelineTile({required this.step, required this.isLast}); + + @override + Widget build(BuildContext context) { + return IntrinsicHeight( + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Column( + children: [ + Container( + width: 40, + height: 40, + decoration: BoxDecoration( + color: step.done || step.active + ? step.color.withValues(alpha: 0.15) + : AppColors.surfaceContainer, + shape: BoxShape.circle, + border: step.active + ? Border.all(color: step.color, width: 2) + : null, + ), + child: Icon(step.icon, + color: step.done || step.active ? step.color : AppColors.outline, + size: 20), + ), + if (!isLast) + Expanded( + child: Container( + width: 2.5, + color: step.done ? step.color : AppColors.surfaceContainerHigh, + ), + ), + ], + ), + const SizedBox(width: 14), + Expanded( + child: Padding( + padding: const EdgeInsets.only(bottom: 24), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Expanded( + child: Text(step.title, + style: AppTheme.labelMd.copyWith( + color: step.color, fontWeight: FontWeight.w700)), + ), + Text(step.time, + style: AppTheme.caption.copyWith( + color: step.active + ? AppColors.tertiary + : AppColors.onSurfaceVariant, + fontWeight: + step.active ? FontWeight.w700 : FontWeight.w400)), + ], + ), + const SizedBox(height: 4), + Text(step.subtitle, + style: AppTheme.bodyMd.copyWith(color: AppColors.onSurfaceVariant)), + if (step.note != null) ...[ + const SizedBox(height: 10), + Container( + padding: const EdgeInsets.all(10), + decoration: BoxDecoration( + color: AppColors.warning.withValues(alpha: 0.08), + borderRadius: BorderRadius.circular(10), + border: Border.all( + color: AppColors.warning.withValues(alpha: 0.25)), + ), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Icon(Icons.warning_amber_rounded, + size: 16, color: AppColors.warning), + const SizedBox(width: 8), + Expanded( + child: Text(step.note!, + style: AppTheme.caption.copyWith( + color: AppColors.warning, height: 1.4)), + ), + ], + ), + ), + ], + ], + ), + ), + ), + ], + ), + ); + } +} + +extension on Widget { + Widget constrainedTo(double width) => + ConstrainedBox(constraints: BoxConstraints(maxWidth: width), child: this); +} diff --git a/lib/main.dart b/lib/main.dart new file mode 100644 index 0000000..7b38251 --- /dev/null +++ b/lib/main.dart @@ -0,0 +1,176 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:provider/provider.dart'; +import 'package:firebase_core/firebase_core.dart'; +import 'package:shared_preferences/shared_preferences.dart'; + +import 'package:doormile/shared/state/app_state.dart'; +import 'package:doormile/core/theme/app_theme.dart'; +import 'package:doormile/core/theme/app_colors.dart'; +import 'package:doormile/features/auth/screens/onboarding_screen.dart'; +import 'package:doormile/features/auth/screens/login_screen.dart'; +import 'package:doormile/features/auth/screens/signup_screen.dart'; +import 'package:doormile/features/auth/screens/otp_screen.dart'; +import 'package:doormile/features/auth/screens/pin_screen.dart'; +import 'package:doormile/features/dashboard/screens/main_shell.dart'; +import 'package:doormile/features/booking/screens/booking_pickup_screen.dart'; +import 'package:doormile/features/booking/screens/parcel_details_screen.dart'; +import 'package:doormile/features/booking/screens/service_price_screen.dart'; +import 'package:doormile/features/booking/screens/booking_preview_screen.dart'; +import 'package:doormile/features/booking/screens/booking_success_screen.dart'; +import 'package:doormile/features/tracking/screens/my_orders_screen.dart'; +import 'package:doormile/features/tracking/screens/live_tracking_screen.dart'; +import 'package:doormile/features/tracking/screens/shipment_journey_screen.dart'; +import 'package:doormile/features/support/screens/help_support_screen.dart'; +import 'package:doormile/features/dashboard/screens/saved_addresses_screen.dart'; +import 'package:doormile/features/support/screens/chat_screen.dart'; +import 'package:doormile/shared/models/models.dart'; + +void main() async { + WidgetsFlutterBinding.ensureInitialized(); + try { + await Firebase.initializeApp(); + } catch (e) { + debugPrint('Firebase initialization error: $e'); + } + + final prefs = await SharedPreferences.getInstance(); + final token = prefs.getString('auth_token'); + final initialRoute = (token != null && token.isNotEmpty) ? Routes.home : Routes.onboarding; + + SystemChrome.setSystemUIOverlayStyle(const SystemUiOverlayStyle( + statusBarColor: Colors.transparent, + statusBarIconBrightness: Brightness.dark, + )); + runApp(DoormileApp(initialRoute: initialRoute)); +} + +class Routes { + static const splash = '/'; + static const onboarding = '/onboarding'; + static const login = '/login'; + static const signup = '/signup'; + static const otp = '/otp'; + static const enterPin = '/enter-pin'; + static const setPin = '/set-pin'; + static const resetPin = '/reset-pin'; + static const home = '/home'; + static const bookingPickup = '/booking/pickup'; + static const parcelDetails = '/booking/details'; + static const servicePrice = '/booking/service'; + static const bookingPreview = '/booking/preview'; + static const bookingSuccess = '/booking/success'; + static const myOrders = '/orders'; + static const liveTracking = '/tracking/live'; + static const shipmentJourney = '/tracking/journey'; + static const help = '/help'; + static const addresses = '/addresses'; + static const chat = '/chat'; +} + +class DoormileApp extends StatelessWidget { + final String initialRoute; + const DoormileApp({super.key, required this.initialRoute}); + + @override + Widget build(BuildContext context) { + return ChangeNotifierProvider( + create: (_) => AppState(), + child: _buildApp(), + ); + } + + Widget _buildApp() { + return MaterialApp( + title: 'Doormile', + debugShowCheckedModeBanner: false, + theme: AppTheme.light, + initialRoute: initialRoute, + onGenerateRoute: (settings) { + Widget page; + switch (settings.name) { + case Routes.onboarding: + page = const OnboardingScreen(); + break; + case Routes.login: + page = const LoginScreen(); + break; + case Routes.signup: + page = const SignupScreen(); + break; + case Routes.otp: + page = const OtpScreen(); + break; + case Routes.enterPin: + page = const PinScreen(mode: PinMode.enter); + break; + case Routes.setPin: + page = const PinScreen(mode: PinMode.set); + break; + case Routes.resetPin: + page = const PinScreen(mode: PinMode.reset); + break; + case Routes.home: + page = const MainShell(); + break; + case Routes.bookingPickup: + page = const BookingPickupScreen(); + break; + case Routes.parcelDetails: + page = const ParcelDetailsScreen(); + break; + case Routes.servicePrice: + page = const ServicePriceScreen(); + break; + case Routes.bookingPreview: + page = const BookingPreviewScreen(); + break; + case Routes.bookingSuccess: + page = const BookingSuccessScreen(); + break; + case Routes.myOrders: + page = const MyOrdersScreen(); + break; + case Routes.liveTracking: + page = LiveTrackingScreen(order: settings.arguments as Order?); + break; + case Routes.shipmentJourney: + page = ShipmentJourneyScreen(order: settings.arguments as Order?); + break; + case Routes.help: + page = const HelpSupportScreen(); + break; + case Routes.addresses: + page = const SavedAddressesScreen(); + break; + case Routes.chat: + page = ChatScreen(riderName: (settings.arguments as String?) ?? 'Rahul Sharma'); + break; + default: + page = const MainShell(); + } + return PageRouteBuilder( + settings: settings, + pageBuilder: (_, __, ___) => page, + transitionsBuilder: (_, animation, __, child) { + return FadeTransition( + opacity: animation, + child: SlideTransition( + position: Tween(begin: const Offset(0, 0.02), end: Offset.zero) + .animate(CurvedAnimation(parent: animation, curve: Curves.easeOut)), + child: child, + ), + ); + }, + transitionDuration: const Duration(milliseconds: 280), + ); + }, + ); + } +} + +/// Selection colour helper used by text fields. +final selectionTheme = const TextSelectionThemeData( + selectionColor: AppColors.primaryFixed, + cursorColor: AppColors.primary, +); diff --git a/lib/shared/models/models.dart b/lib/shared/models/models.dart new file mode 100644 index 0000000..55e0314 --- /dev/null +++ b/lib/shared/models/models.dart @@ -0,0 +1,122 @@ +import 'package:flutter/material.dart'; + +import 'package:doormile/core/theme/app_colors.dart'; + +/// A saved or recent address entry. +class AddressEntry { + final String label; // e.g. "Home", "Work" + final String line; // full address line + final IconData icon; + final Color iconBg; + final Color iconColor; + + const AddressEntry({ + required this.label, + required this.line, + required this.icon, + this.iconBg = AppColors.surfaceContainer, + this.iconColor = AppColors.onSurfaceVariant, + }); +} + +enum OrderStatus { active, delivered, cancelled } + +extension OrderStatusX on OrderStatus { + String get label => switch (this) { + OrderStatus.active => 'In Transit', + OrderStatus.delivered => 'Delivered', + OrderStatus.cancelled => 'Cancelled', + }; + + Color get color => switch (this) { + OrderStatus.active => AppColors.tertiary, + OrderStatus.delivered => AppColors.success, + OrderStatus.cancelled => AppColors.error, + }; + + Color get bg => switch (this) { + OrderStatus.active => AppColors.tertiaryFixed, + OrderStatus.delivered => const Color(0xFFE3F6EC), + OrderStatus.cancelled => AppColors.errorContainer, + }; +} + +/// One stop in a shipment journey timeline. +class JourneyStep { + final String title; + final String subtitle; + final String time; + final IconData icon; + final Color color; + final bool done; + final bool active; + final String? note; + + const JourneyStep({ + required this.title, + required this.subtitle, + required this.time, + required this.icon, + required this.color, + this.done = false, + this.active = false, + this.note, + }); +} + +class ActivityLog { + final String title; + final String subtitle; + final IconData icon; + const ActivityLog(this.title, this.subtitle, this.icon); +} + +/// A parcel order. +class Order { + final String id; + final String fromLabel; + final String fromSub; + final String toLabel; + final String toSub; + final String date; + final double amount; + final OrderStatus status; + final String parcelType; + final String weight; + final String size; + final double progress; // 0..1 + final String co2Saved; + final String eta; + final bool ecoRoute; + + const Order({ + required this.id, + required this.fromLabel, + required this.fromSub, + required this.toLabel, + required this.toSub, + required this.date, + required this.amount, + required this.status, + this.parcelType = 'Documents', + this.weight = '0–1 kg', + this.size = 'Small', + this.progress = 0.0, + this.co2Saved = '0.8kg', + this.eta = '—', + this.ecoRoute = true, + }); +} + + +class FaqItem { + final String question; + final String answer; + const FaqItem(this.question, this.answer); +} + +class ChatMessage { + final String text; + final bool fromUser; + const ChatMessage(this.text, {this.fromUser = false}); +} diff --git a/lib/shared/state/app_state.dart b/lib/shared/state/app_state.dart new file mode 100644 index 0000000..9d87f53 --- /dev/null +++ b/lib/shared/state/app_state.dart @@ -0,0 +1,563 @@ +import 'dart:convert'; +import 'package:flutter/material.dart'; +import 'package:flutter/foundation.dart'; +import 'package:firebase_auth/firebase_auth.dart'; +import 'package:http/http.dart' as http; +import 'package:shared_preferences/shared_preferences.dart'; +import 'package:geolocator/geolocator.dart'; + +import 'package:doormile/core/constants/api_constants.dart'; + +import 'package:doormile/shared/models/models.dart'; +import 'package:doormile/core/theme/app_colors.dart'; + +/// Holds the in-progress booking the user is composing across the flow screens. +class BookingDraft { + String pickupAddress = ''; + String contactName = ''; + String contactMobile = ''; + String notes = ''; + + String destination = ''; + String service = ''; + String parcelType = ''; + String weight = '2 kg'; + String size = 'Small'; + String pickupDay = 'Today'; + String pickupSlot = '10:00 AM - 12:00 PM'; + bool fragile = false; + String dropAddress = ''; + String dropPincode = ''; + String paymentMethod = 'Cash'; + bool needsInsurance = false; + double declaredValue = 0.0; + + double minPrice = 45.0; + double maxPrice = 86.0; + + void reset() { + pickupAddress = ''; + contactName = ''; + contactMobile = ''; + notes = ''; + destination = ''; + service = ''; + parcelType = ''; + weight = '2 kg'; + size = 'Small'; + pickupDay = 'Today'; + pickupSlot = '10:00 AM - 12:00 PM'; + fragile = false; + dropAddress = ''; + dropPincode = ''; + paymentMethod = 'Cash'; + needsInsurance = false; + declaredValue = 0.0; + minPrice = 45.0; + maxPrice = 86.0; + } +} + +/// Global, in-memory app state — auth, wallet, orders, and the booking draft. +class AppState extends ChangeNotifier { + // ---- Auth ---- + bool isLoggedIn = false; + String phone = ''; + String? verificationId; + String userName = ''; // Updated via profile/login + String firstName = ''; + + String? pendingFirstName; + String? pendingEmail; + String? authToken; + bool isResettingPin = false; + + AppState() { + _loadSession(); + } + + Future _loadSession() async { + final prefs = await SharedPreferences.getInstance(); + authToken = prefs.getString('auth_token'); + if (authToken != null && authToken!.isNotEmpty) { + isLoggedIn = true; + firstName = prefs.getString('first_name') ?? 'User'; + userName = prefs.getString('user_name') ?? 'User'; + phone = prefs.getString('phone') ?? ''; + + // Wipe mock data because this is a real authenticated session + orders.clear(); + savedAddresses.clear(); + + notifyListeners(); + } + } + + Future _saveSession() async { + final prefs = await SharedPreferences.getInstance(); + if (authToken != null) await prefs.setString('auth_token', authToken!); + await prefs.setString('first_name', firstName); + await prefs.setString('user_name', userName); + await prefs.setString('phone', phone); + } + + void setPhone(String value) { + phone = value; + notifyListeners(); + } + + Future sendOtp(String phoneNumber, {String? firstName, String? email, bool resetPin = false, required Function onSuccess, required Function(String) onError}) async { + final fullPhone = '+91${phoneNumber.replaceAll(' ', '')}'; + setPhone(phoneNumber); + pendingFirstName = firstName; + pendingEmail = email; + isResettingPin = resetPin; + + try { + if (kIsWeb) { + debugPrint('⚠️ RUNNING ON WEB: Bypassing Firebase OTP (Mocking success)'); + Future.delayed(const Duration(seconds: 1), () { + verificationId = 'mock_verification_id_$fullPhone'; + onSuccess(); + }); + return; + } + + debugPrint('===================================='); + debugPrint('🚀 INITIATING LIVE FIREBASE OTP FOR: $fullPhone'); + await FirebaseAuth.instance.verifyPhoneNumber( + phoneNumber: fullPhone, + verificationCompleted: (PhoneAuthCredential credential) async { + debugPrint('✅ VERIFICATION COMPLETED AUTOMATICALLY'); + try { + await FirebaseAuth.instance.signInWithCredential(credential); + } catch (e) { + debugPrint('❌ ERROR SIGNING IN: $e'); + onError(e.toString()); + } + }, + verificationFailed: (FirebaseAuthException e) { + debugPrint('❌ VERIFICATION FAILED: ${e.code} - ${e.message}'); + onError(e.message ?? 'Verification failed'); + }, + codeSent: (String vId, int? resendToken) { + debugPrint('✅ CODE SUCCESSFULLY SENT. Verification ID: $vId'); + verificationId = vId; + onSuccess(); + }, + codeAutoRetrievalTimeout: (String vId) { + debugPrint('⚠️ AUTO RETRIEVAL TIMEOUT. Verification ID: $vId'); + verificationId = vId; + }, + ); + } catch (e) { + debugPrint('💥 FIREBASE OTP CRASHED COMPLETELY: $e'); + onError('Firebase OTP Error: $e'); + } + } + + Future verifyOtp(String smsCode, {required Function onSuccess, required Function(String) onError}) async { + if (verificationId == null) { + onError('Verification ID is missing. Please resend OTP.'); + return; + } + try { + if (kIsWeb) { + debugPrint('⚠️ RUNNING ON WEB: Bypassing Firebase OTP verification (Mocking success)'); + Future.delayed(const Duration(seconds: 1), () { + if (smsCode.length == 6) { + onSuccess(); + } else { + onError('Invalid mock OTP code (must be 6 digits).'); + } + }); + return; + } + + PhoneAuthCredential credential = PhoneAuthProvider.credential( + verificationId: verificationId!, + smsCode: smsCode, + ); + await FirebaseAuth.instance.signInWithCredential(credential); + onSuccess(); + } catch (e) { + onError('Error: $e'); + } + } + + // --- Backend Wrappers --- + + Future registerWithPin(String pin) async { + final rawPhone = phone.replaceAll(' ', ''); + final parts = (pendingFirstName ?? 'User').split(' '); + final first = parts.first; + final last = parts.length > 1 ? parts.sublist(1).join(' ') : ''; + + double lat = 0.0; + double lng = 0.0; + + try { + bool serviceEnabled = await Geolocator.isLocationServiceEnabled(); + if (serviceEnabled) { + LocationPermission permission = await Geolocator.checkPermission(); + if (permission == LocationPermission.denied) { + permission = await Geolocator.requestPermission(); + } + if (permission == LocationPermission.whileInUse || permission == LocationPermission.always) { + Position position = await Geolocator.getCurrentPosition(desiredAccuracy: LocationAccuracy.medium); + lat = position.latitude; + lng = position.longitude; + } + } + } catch (e) { + debugPrint('Error getting location for registration: $e'); + } + + final res = await http.post( + Uri.parse(ApiConstants.register), + headers: {'Content-Type': 'application/json'}, + body: jsonEncode({ + "firstname": first, + "lastname": last, + "phone": rawPhone, + "email": pendingEmail ?? '', + "pin": pin, + "configid": 1001, + "defaultlatitude": lat, + "defaultlongitude": lng + }), + ); + if (res.statusCode >= 200 && res.statusCode < 300) { + final data = jsonDecode(res.body); + authToken = data['token']; + + if (data['user'] != null && data['user']['firstname'] != null) { + firstName = data['user']['firstname']; + userName = '$firstName ${data['user']['lastname'] ?? ''}'.trim(); + } else { + firstName = pendingFirstName ?? 'User'; + userName = pendingFirstName ?? 'User'; + } + + login(); + } else { + throw Exception('Registration Failed: ${res.body}'); + } + } + + Future initiateLogin(String phoneNumber) async { + final rawPhone = phoneNumber.replaceAll(' ', ''); + setPhone(phoneNumber); + + // Easter Egg + if (rawPhone == '9876543210') { + return; + } + + final initRes = await http.post( + Uri.parse(ApiConstants.login), + headers: {'Content-Type': 'application/json'}, + body: jsonEncode({"phone": rawPhone, "configid": 1001}), + ); + + if (initRes.statusCode < 200 || initRes.statusCode >= 300) { + throw Exception('Phone number not registered. Please sign up first.'); + } + } + + Future verifyPinWithBackend(String pin) async { + final rawPhone = phone.replaceAll(' ', ''); + + // Easter Egg + if (rawPhone == '9876543210' && pin == '1234') { + authToken = 'easter-egg-token'; + firstName = 'Admin'; + userName = 'Admin User'; + login(); + return; + } + + final verifyRes = await http.post( + Uri.parse(ApiConstants.verifyPin), + headers: {'Content-Type': 'application/json'}, + body: jsonEncode({"phone": rawPhone, "pin": pin, "configid": 1001}), + ); + + if (verifyRes.statusCode >= 200 && verifyRes.statusCode < 300) { + final data = jsonDecode(verifyRes.body); + authToken = data['token']; + if (data['user'] != null && data['user']['firstname'] != null) { + firstName = data['user']['firstname']; + userName = '$firstName ${data['user']['lastname'] ?? ''}'.trim(); + } + login(); + } else { + throw Exception('Incorrect PIN. Please try again.'); + } + } + + Future resetPinWithBackend(String newPin) async { + final rawPhone = phone.replaceAll(' ', ''); + // TODO: Create this endpoint in Go backend + final res = await http.post( + Uri.parse('${ApiConstants.baseUrl}/customer/reset-pin'), + headers: {'Content-Type': 'application/json'}, + body: jsonEncode({"phone": rawPhone, "new_pin": newPin}), + ); + + if (res.statusCode >= 200 && res.statusCode < 300) { + final data = jsonDecode(res.body); + if (data['token'] != null) authToken = data['token']; + login(); + } else { + throw Exception('PIN Reset Failed: ${res.body}'); + } + } + + void login() { + isLoggedIn = true; + // Wipe hardcoded mock data so real users only see their own actual data! + orders.clear(); + savedAddresses.clear(); + _saveSession(); + notifyListeners(); + } + + Future logout() async { + isLoggedIn = false; + authToken = null; + // Reset in-memory user details so the previous account does not linger. + firstName = 'User'; + userName = 'User'; + phone = ''; + verificationId = null; + pendingFirstName = null; + pendingEmail = null; + orders.clear(); + savedAddresses.clear(); + notifyListeners(); + final prefs = await SharedPreferences.getInstance(); + await prefs.remove('auth_token'); + await prefs.remove('first_name'); + await prefs.remove('user_name'); + await prefs.remove('phone'); + } + + // ---- Booking draft ---- + final BookingDraft draft = BookingDraft(); + void touch() => notifyListeners(); + + + // ---- Orders ---- + List orders = []; + + bool isLoadingOrders = false; + + /// Fetches real bookings from the Go backend + Future fetchOrders() async { + if (authToken == null) return; + + isLoadingOrders = true; + notifyListeners(); + + try { + print('\n=== DOORMILE API FETCH ORDERS ==='); + print('Endpoint: ${ApiConstants.getPickups}'); + + final res = await http.get( + Uri.parse(ApiConstants.getPickups), + headers: { + 'Content-Type': 'application/json', + 'Authorization': 'Bearer $authToken', + }, + ); + + print('Status Code: ${res.statusCode}'); + + if (res.statusCode >= 200 && res.statusCode < 300) { + final List data = jsonDecode(res.body); + + orders = data.map((b) { + return Order( + id: b['booking_no'] ?? 'DM-UNK', + fromLabel: b['pickupaddress'] ?? 'Unknown Location', + fromSub: 'Pickup', + toLabel: b['deliverypincode'] != null ? 'Pincode: ${b['deliverypincode']}' : 'Pending Destination', + toSub: 'Delivery', + date: b['created_at']?.split('T')[0] ?? 'Today', + amount: (b['estimated_price'] ?? 0.0).toDouble(), + status: _parseBackendStatus(b['status']), + progress: b['status'] == 'Pending_Pickup' ? 0.1 : 0.5, + ); + }).toList(); + } + } catch (e) { + print('Failed to fetch orders: $e'); + } finally { + isLoadingOrders = false; + notifyListeners(); + } + } + + OrderStatus _parseBackendStatus(String? status) { + if (status == 'Pending_Pickup') return OrderStatus.active; + if (status == 'Delivered') return OrderStatus.delivered; + if (status == 'Cancelled') return OrderStatus.cancelled; + return OrderStatus.active; + } + + /// Fetches estimated pricing from the Redis-backed Go API + Future fetchPriceEstimate() async { + // Map UI categories to Backend enums + String backendCategory = "General"; + switch (draft.parcelType) { + case "General Goods": backendCategory = "General"; break; + case "Books & Documents": backendCategory = "Documents"; break; + case "Electronics & Gadgets": backendCategory = "Electronics"; break; + case "Clothing & Textiles": backendCategory = "Clothing"; break; + case "Fragile Items": backendCategory = "Fragile"; break; + case "Medical & Pharma": backendCategory = "Medical"; break; + case "Automotive Parts": backendCategory = "Automotive"; break; + case "Food & Perishables": backendCategory = "Food"; break; + default: backendCategory = "General"; + } + + final payload = { + "zone": "Local", + "destinationpin": draft.dropPincode, + "service_type": draft.service.isNotEmpty ? draft.service : "Normal", + "weight": 1.0, // Default since we removed it from UI + "category": backendCategory + }; + + print('\n=== DOORMILE API PRICING REQUEST ==='); + print('Endpoint: ${ApiConstants.checkPrice}'); + print('Payload: ${jsonEncode(payload)}'); + print('====================================\n'); + + try { + final res = await http.post( + Uri.parse(ApiConstants.checkPrice), + headers: {'Content-Type': 'application/json'}, + body: jsonEncode(payload), + ); + + print('\n=== DOORMILE API PRICING RESPONSE ==='); + print('Status Code: ${res.statusCode}'); + print('Response Body: ${res.body}'); + print('=============================\n'); + + if (res.statusCode >= 200 && res.statusCode < 300) { + final data = jsonDecode(res.body); + if (data['results'] != null && data['results'].isNotEmpty) { + final result = data['results'][0]; + draft.minPrice = (result['min_price'] as num).toDouble(); + draft.maxPrice = (result['max_price'] as num).toDouble(); + notifyListeners(); + } + } + } catch (e) { + print('Pricing API error: $e'); + // On error, we just keep the default 45 - 86 fallback + } + } + + /// Confirms the current draft into a new active order. + Future placeOrder() async { + // Map UI categories to Backend enums + String backendCategory = "General"; + switch (draft.parcelType) { + case "General Goods": backendCategory = "General"; break; + case "Books & Documents": backendCategory = "Documents"; break; + case "Electronics & Gadgets": backendCategory = "Electronics"; break; + case "Clothing & Textiles": backendCategory = "Clothing"; break; + case "Fragile Items": backendCategory = "Fragile"; break; + case "Medical & Pharma": backendCategory = "Medical"; break; + case "Automotive Parts": backendCategory = "Automotive"; break; + case "Food & Perishables": backendCategory = "Food"; break; + default: backendCategory = "General"; + } + + final payload = { + "pickupaddress": draft.pickupAddress.isNotEmpty ? draft.pickupAddress : "Current Location (GPS)", + "pickuppincode": "560001", // Default pincode until we parse from GPS + "deliverypincode": draft.dropPincode, + "parcels": [ + { + "itemcategory": backendCategory, + "itemdescription": draft.notes.isNotEmpty ? draft.notes : "No description provided" + } + ] + }; + + print('\n=== DOORMILE API BOOKING REQUEST ==='); + print('Endpoint: ${ApiConstants.createPickup}'); + print('Payload: ${jsonEncode(payload)}'); + print('====================================\n'); + + final res = await http.post( + Uri.parse(ApiConstants.createPickup), + headers: { + 'Content-Type': 'application/json', + if (authToken != null) 'Authorization': 'Bearer $authToken', + }, + body: jsonEncode(payload), + ); + + print('\n=== DOORMILE API RESPONSE ==='); + print('Status Code: ${res.statusCode}'); + print('Response Body: ${res.body}'); + print('=============================\n'); + + if (res.statusCode >= 200 && res.statusCode < 300) { + final data = jsonDecode(res.body); + + final newOrder = Order( + id: data['booking_no'] ?? 'DM-PU-000${101 + orders.length}', + fromLabel: draft.pickupAddress, + fromSub: draft.contactName.isEmpty ? 'Pickup requested' : 'Contact: ${draft.contactName}', + toLabel: draft.dropPincode.isNotEmpty ? 'Pincode: ${draft.dropPincode}' : 'Awaiting Miler input', + toSub: 'Drop details pending', + date: 'Today', + amount: draft.minPrice, + status: OrderStatus.active, + progress: 0.1, + eta: 'Assigning Miler...', + ); + + orders.insert(0, newOrder); + draft.reset(); + notifyListeners(); + return newOrder; + } else { + throw Exception('Failed to create pickup request: ${res.body}'); + } + } + + // ---- Saved addresses ---- + List savedAddresses = [ + AddressEntry( + label: 'Home', + line: 'Sector 45, Gurgaon, Haryana, 122003', + icon: Icons.home_rounded, + iconBg: AppColors.primaryFixed, + iconColor: AppColors.primary, + ), + AddressEntry( + label: 'Work', + line: 'Cyber City Building 10, DLF Phase 2', + icon: Icons.work_rounded, + iconBg: AppColors.tertiaryFixed, + iconColor: AppColors.tertiary, + ), + AddressEntry( + label: 'Select City Walk Mall', + line: 'Saket District Centre, New Delhi', + icon: Icons.history_rounded, + iconBg: AppColors.surfaceContainerHigh, + iconColor: AppColors.onSurfaceVariant, + ), + ]; + + +} diff --git a/macos/.gitignore b/macos/.gitignore new file mode 100644 index 0000000..746adbb --- /dev/null +++ b/macos/.gitignore @@ -0,0 +1,7 @@ +# Flutter-related +**/Flutter/ephemeral/ +**/Pods/ + +# Xcode-related +**/dgph +**/xcuserdata/ diff --git a/macos/Flutter/Flutter-Debug.xcconfig b/macos/Flutter/Flutter-Debug.xcconfig new file mode 100644 index 0000000..c2efd0b --- /dev/null +++ b/macos/Flutter/Flutter-Debug.xcconfig @@ -0,0 +1 @@ +#include "ephemeral/Flutter-Generated.xcconfig" diff --git a/macos/Flutter/Flutter-Release.xcconfig b/macos/Flutter/Flutter-Release.xcconfig new file mode 100644 index 0000000..c2efd0b --- /dev/null +++ b/macos/Flutter/Flutter-Release.xcconfig @@ -0,0 +1 @@ +#include "ephemeral/Flutter-Generated.xcconfig" diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift new file mode 100644 index 0000000..79c1265 --- /dev/null +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -0,0 +1,22 @@ +// +// Generated file. Do not edit. +// + +import FlutterMacOS +import Foundation + +import firebase_auth +import firebase_core +import geolocator_apple +import package_info_plus +import shared_preferences_foundation +import url_launcher_macos + +func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { + FLTFirebaseAuthPlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseAuthPlugin")) + FLTFirebaseCorePlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseCorePlugin")) + GeolocatorPlugin.register(with: registry.registrar(forPlugin: "GeolocatorPlugin")) + FPPPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FPPPackageInfoPlusPlugin")) + SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin")) + UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin")) +} diff --git a/macos/Runner.xcodeproj/project.pbxproj b/macos/Runner.xcodeproj/project.pbxproj new file mode 100644 index 0000000..37f10db --- /dev/null +++ b/macos/Runner.xcodeproj/project.pbxproj @@ -0,0 +1,729 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 54; + objects = { + +/* Begin PBXAggregateTarget section */ + 33CC111A2044C6BA0003C045 /* Flutter Assemble */ = { + isa = PBXAggregateTarget; + buildConfigurationList = 33CC111B2044C6BA0003C045 /* Build configuration list for PBXAggregateTarget "Flutter Assemble" */; + buildPhases = ( + 33CC111E2044C6BF0003C045 /* ShellScript */, + ); + dependencies = ( + ); + name = "Flutter Assemble"; + productName = FLX; + }; +/* End PBXAggregateTarget section */ + +/* Begin PBXBuildFile section */ + 331C80D8294CF71000263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C80D7294CF71000263BE5 /* RunnerTests.swift */; }; + 335BBD1B22A9A15E00E9071D /* GeneratedPluginRegistrant.swift in Sources */ = {isa = PBXBuildFile; fileRef = 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */; }; + 33CC10F12044A3C60003C045 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC10F02044A3C60003C045 /* AppDelegate.swift */; }; + 33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F22044A3C60003C045 /* Assets.xcassets */; }; + 33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F42044A3C60003C045 /* MainMenu.xib */; }; + 33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */; }; + 78A318202AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage in Frameworks */ = {isa = PBXBuildFile; productRef = 78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + 331C80D9294CF71000263BE5 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 33CC10E52044A3C60003C045 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 33CC10EC2044A3C60003C045; + remoteInfo = Runner; + }; + 33CC111F2044C79F0003C045 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 33CC10E52044A3C60003C045 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 33CC111A2044C6BA0003C045; + remoteInfo = FLX; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXCopyFilesBuildPhase section */ + 33CC110E2044A8840003C045 /* Bundle Framework */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 10; + files = ( + ); + name = "Bundle Framework"; + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXCopyFilesBuildPhase section */ + +/* Begin PBXFileReference section */ + 331C80D5294CF71000263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + 331C80D7294CF71000263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = ""; }; + 333000ED22D3DE5D00554162 /* Warnings.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Warnings.xcconfig; sourceTree = ""; }; + 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GeneratedPluginRegistrant.swift; sourceTree = ""; }; + 33CC10ED2044A3C60003C045 /* doormile.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "doormile.app"; sourceTree = BUILT_PRODUCTS_DIR; }; + 33CC10F02044A3C60003C045 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + 33CC10F22044A3C60003C045 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Assets.xcassets; path = Runner/Assets.xcassets; sourceTree = ""; }; + 33CC10F52044A3C60003C045 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/MainMenu.xib; sourceTree = ""; }; + 33CC10F72044A3C60003C045 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; name = Info.plist; path = Runner/Info.plist; sourceTree = ""; }; + 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainFlutterWindow.swift; sourceTree = ""; }; + 33CEB47222A05771004F2AC0 /* Flutter-Debug.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Flutter-Debug.xcconfig"; sourceTree = ""; }; + 33CEB47422A05771004F2AC0 /* Flutter-Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Flutter-Release.xcconfig"; sourceTree = ""; }; + 33CEB47722A0578A004F2AC0 /* Flutter-Generated.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = "Flutter-Generated.xcconfig"; path = "ephemeral/Flutter-Generated.xcconfig"; sourceTree = ""; }; + 33E51913231747F40026EE4D /* DebugProfile.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = DebugProfile.entitlements; sourceTree = ""; }; + 33E51914231749380026EE4D /* Release.entitlements */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.entitlements; path = Release.entitlements; sourceTree = ""; }; + 33E5194F232828860026EE4D /* AppInfo.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = AppInfo.xcconfig; sourceTree = ""; }; + 78E0A7A72DC9AD7400C4905E /* FlutterGeneratedPluginSwiftPackage */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = FlutterGeneratedPluginSwiftPackage; path = ephemeral/Packages/FlutterGeneratedPluginSwiftPackage; sourceTree = ""; }; + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Release.xcconfig; sourceTree = ""; }; + 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Debug.xcconfig; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 331C80D2294CF70F00263BE5 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 33CC10EA2044A3C60003C045 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 78A318202AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 331C80D6294CF71000263BE5 /* RunnerTests */ = { + isa = PBXGroup; + children = ( + 331C80D7294CF71000263BE5 /* RunnerTests.swift */, + ); + path = RunnerTests; + sourceTree = ""; + }; + 33BA886A226E78AF003329D5 /* Configs */ = { + isa = PBXGroup; + children = ( + 33E5194F232828860026EE4D /* AppInfo.xcconfig */, + 9740EEB21CF90195004384FC /* Debug.xcconfig */, + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, + 333000ED22D3DE5D00554162 /* Warnings.xcconfig */, + ); + path = Configs; + sourceTree = ""; + }; + 33CC10E42044A3C60003C045 = { + isa = PBXGroup; + children = ( + 33FAB671232836740065AC1E /* Runner */, + 33CEB47122A05771004F2AC0 /* Flutter */, + 331C80D6294CF71000263BE5 /* RunnerTests */, + 33CC10EE2044A3C60003C045 /* Products */, + D73912EC22F37F3D000D13A0 /* Frameworks */, + ); + sourceTree = ""; + }; + 33CC10EE2044A3C60003C045 /* Products */ = { + isa = PBXGroup; + children = ( + 33CC10ED2044A3C60003C045 /* doormile.app */, + 331C80D5294CF71000263BE5 /* RunnerTests.xctest */, + ); + name = Products; + sourceTree = ""; + }; + 33CC11242044D66E0003C045 /* Resources */ = { + isa = PBXGroup; + children = ( + 33CC10F22044A3C60003C045 /* Assets.xcassets */, + 33CC10F42044A3C60003C045 /* MainMenu.xib */, + 33CC10F72044A3C60003C045 /* Info.plist */, + ); + name = Resources; + path = ..; + sourceTree = ""; + }; + 33CEB47122A05771004F2AC0 /* Flutter */ = { + isa = PBXGroup; + children = ( + 78E0A7A72DC9AD7400C4905E /* FlutterGeneratedPluginSwiftPackage */, + 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */, + 33CEB47222A05771004F2AC0 /* Flutter-Debug.xcconfig */, + 33CEB47422A05771004F2AC0 /* Flutter-Release.xcconfig */, + 33CEB47722A0578A004F2AC0 /* Flutter-Generated.xcconfig */, + ); + path = Flutter; + sourceTree = ""; + }; + 33FAB671232836740065AC1E /* Runner */ = { + isa = PBXGroup; + children = ( + 33CC10F02044A3C60003C045 /* AppDelegate.swift */, + 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */, + 33E51913231747F40026EE4D /* DebugProfile.entitlements */, + 33E51914231749380026EE4D /* Release.entitlements */, + 33CC11242044D66E0003C045 /* Resources */, + 33BA886A226E78AF003329D5 /* Configs */, + ); + path = Runner; + sourceTree = ""; + }; + D73912EC22F37F3D000D13A0 /* Frameworks */ = { + isa = PBXGroup; + children = ( + ); + name = Frameworks; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 331C80D4294CF70F00263BE5 /* RunnerTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 331C80DE294CF71000263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */; + buildPhases = ( + 331C80D1294CF70F00263BE5 /* Sources */, + 331C80D2294CF70F00263BE5 /* Frameworks */, + 331C80D3294CF70F00263BE5 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + 331C80DA294CF71000263BE5 /* PBXTargetDependency */, + ); + name = RunnerTests; + productName = RunnerTests; + productReference = 331C80D5294CF71000263BE5 /* RunnerTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; + 33CC10EC2044A3C60003C045 /* Runner */ = { + isa = PBXNativeTarget; + buildConfigurationList = 33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget "Runner" */; + buildPhases = ( + 33CC10E92044A3C60003C045 /* Sources */, + 33CC10EA2044A3C60003C045 /* Frameworks */, + 33CC10EB2044A3C60003C045 /* Resources */, + 33CC110E2044A8840003C045 /* Bundle Framework */, + 3399D490228B24CF009A79C7 /* ShellScript */, + ); + buildRules = ( + ); + dependencies = ( + 33CC11202044C79F0003C045 /* PBXTargetDependency */, + ); + name = Runner; + packageProductDependencies = ( + 78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */, + ); + productName = Runner; + productReference = 33CC10ED2044A3C60003C045 /* doormile.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 33CC10E52044A3C60003C045 /* Project object */ = { + isa = PBXProject; + attributes = { + BuildIndependentTargetsInParallel = YES; + LastSwiftUpdateCheck = 0920; + LastUpgradeCheck = 1510; + ORGANIZATIONNAME = ""; + TargetAttributes = { + 331C80D4294CF70F00263BE5 = { + CreatedOnToolsVersion = 14.0; + TestTargetID = 33CC10EC2044A3C60003C045; + }; + 33CC10EC2044A3C60003C045 = { + CreatedOnToolsVersion = 9.2; + LastSwiftMigration = 1100; + ProvisioningStyle = Automatic; + SystemCapabilities = { + com.apple.Sandbox = { + enabled = 1; + }; + }; + }; + 33CC111A2044C6BA0003C045 = { + CreatedOnToolsVersion = 9.2; + ProvisioningStyle = Manual; + }; + }; + }; + buildConfigurationList = 33CC10E82044A3C60003C045 /* Build configuration list for PBXProject "Runner" */; + compatibilityVersion = "Xcode 9.3"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 33CC10E42044A3C60003C045; + packageReferences = ( + 781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage" */, + ); + productRefGroup = 33CC10EE2044A3C60003C045 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 33CC10EC2044A3C60003C045 /* Runner */, + 331C80D4294CF70F00263BE5 /* RunnerTests */, + 33CC111A2044C6BA0003C045 /* Flutter Assemble */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 331C80D3294CF70F00263BE5 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 33CC10EB2044A3C60003C045 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */, + 33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + 3399D490228B24CF009A79C7 /* ShellScript */ = { + isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + ); + outputFileListPaths = ( + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "echo \"$PRODUCT_NAME.app\" > \"$PROJECT_DIR\"/Flutter/ephemeral/.app_filename && \"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh embed\n"; + }; + 33CC111E2044C6BF0003C045 /* ShellScript */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + Flutter/ephemeral/FlutterInputs.xcfilelist, + ); + inputPaths = ( + Flutter/ephemeral/tripwire, + ); + outputFileListPaths = ( + Flutter/ephemeral/FlutterOutputs.xcfilelist, + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh && touch Flutter/ephemeral/tripwire"; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 331C80D1294CF70F00263BE5 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 331C80D8294CF71000263BE5 /* RunnerTests.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 33CC10E92044A3C60003C045 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */, + 33CC10F12044A3C60003C045 /* AppDelegate.swift in Sources */, + 335BBD1B22A9A15E00E9071D /* GeneratedPluginRegistrant.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + 331C80DA294CF71000263BE5 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 33CC10EC2044A3C60003C045 /* Runner */; + targetProxy = 331C80D9294CF71000263BE5 /* PBXContainerItemProxy */; + }; + 33CC11202044C79F0003C045 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 33CC111A2044C6BA0003C045 /* Flutter Assemble */; + targetProxy = 33CC111F2044C79F0003C045 /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin PBXVariantGroup section */ + 33CC10F42044A3C60003C045 /* MainMenu.xib */ = { + isa = PBXVariantGroup; + children = ( + 33CC10F52044A3C60003C045 /* Base */, + ); + name = MainMenu.xib; + path = Runner; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + 331C80DB294CF71000263BE5 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.example.doormile.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/doormile.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/doormile"; + }; + name = Debug; + }; + 331C80DC294CF71000263BE5 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.example.doormile.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/doormile.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/doormile"; + }; + name = Release; + }; + 331C80DD294CF71000263BE5 /* Profile */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.example.doormile.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/doormile.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/doormile"; + }; + name = Profile; + }; + 338D0CE9231458BD00FA5F75 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CODE_SIGN_IDENTITY = "-"; + COPY_PHASE_STRIP = NO; + DEAD_CODE_STRIPPING = YES; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + MACOSX_DEPLOYMENT_TARGET = 10.15; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = macosx; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + }; + name = Profile; + }; + 338D0CEA231458BD00FA5F75 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + ); + PROVISIONING_PROFILE_SPECIFIER = ""; + SWIFT_VERSION = 5.0; + }; + name = Profile; + }; + 338D0CEB231458BD00FA5F75 /* Profile */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Manual; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Profile; + }; + 33CC10F92044A3C60003C045 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CODE_SIGN_IDENTITY = "-"; + COPY_PHASE_STRIP = NO; + DEAD_CODE_STRIPPING = YES; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + MACOSX_DEPLOYMENT_TARGET = 10.15; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = macosx; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + }; + name = Debug; + }; + 33CC10FA2044A3C60003C045 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CODE_SIGN_IDENTITY = "-"; + COPY_PHASE_STRIP = NO; + DEAD_CODE_STRIPPING = YES; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + MACOSX_DEPLOYMENT_TARGET = 10.15; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = macosx; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + }; + name = Release; + }; + 33CC10FC2044A3C60003C045 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + ); + PROVISIONING_PROFILE_SPECIFIER = ""; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + }; + name = Debug; + }; + 33CC10FD2044A3C60003C045 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_ENTITLEMENTS = Runner/Release.entitlements; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + ); + PROVISIONING_PROFILE_SPECIFIER = ""; + SWIFT_VERSION = 5.0; + }; + name = Release; + }; + 33CC111C2044C6BA0003C045 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Manual; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Debug; + }; + 33CC111D2044C6BA0003C045 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Automatic; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 331C80DE294CF71000263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 331C80DB294CF71000263BE5 /* Debug */, + 331C80DC294CF71000263BE5 /* Release */, + 331C80DD294CF71000263BE5 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 33CC10E82044A3C60003C045 /* Build configuration list for PBXProject "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 33CC10F92044A3C60003C045 /* Debug */, + 33CC10FA2044A3C60003C045 /* Release */, + 338D0CE9231458BD00FA5F75 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 33CC10FC2044A3C60003C045 /* Debug */, + 33CC10FD2044A3C60003C045 /* Release */, + 338D0CEA231458BD00FA5F75 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 33CC111B2044C6BA0003C045 /* Build configuration list for PBXAggregateTarget "Flutter Assemble" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 33CC111C2044C6BA0003C045 /* Debug */, + 33CC111D2044C6BA0003C045 /* Release */, + 338D0CEB231458BD00FA5F75 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + +/* Begin XCLocalSwiftPackageReference section */ + 781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage" */ = { + isa = XCLocalSwiftPackageReference; + relativePath = Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage; + }; +/* End XCLocalSwiftPackageReference section */ + +/* Begin XCSwiftPackageProductDependency section */ + 78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */ = { + isa = XCSwiftPackageProductDependency; + productName = FlutterGeneratedPluginSwiftPackage; + }; +/* End XCSwiftPackageProductDependency section */ + }; + rootObject = 33CC10E52044A3C60003C045 /* Project object */; +} diff --git a/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000..18d9810 --- /dev/null +++ b/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme new file mode 100644 index 0000000..a58741a --- /dev/null +++ b/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -0,0 +1,117 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/macos/Runner.xcworkspace/contents.xcworkspacedata b/macos/Runner.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..1d526a1 --- /dev/null +++ b/macos/Runner.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000..18d9810 --- /dev/null +++ b/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/macos/Runner/AppDelegate.swift b/macos/Runner/AppDelegate.swift new file mode 100644 index 0000000..b3c1761 --- /dev/null +++ b/macos/Runner/AppDelegate.swift @@ -0,0 +1,13 @@ +import Cocoa +import FlutterMacOS + +@main +class AppDelegate: FlutterAppDelegate { + override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool { + return true + } + + override func applicationSupportsSecureRestorableState(_ app: NSApplication) -> Bool { + return true + } +} diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000..a2ec33f --- /dev/null +++ b/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,68 @@ +{ + "images" : [ + { + "size" : "16x16", + "idiom" : "mac", + "filename" : "app_icon_16.png", + "scale" : "1x" + }, + { + "size" : "16x16", + "idiom" : "mac", + "filename" : "app_icon_32.png", + "scale" : "2x" + }, + { + "size" : "32x32", + "idiom" : "mac", + "filename" : "app_icon_32.png", + "scale" : "1x" + }, + { + "size" : "32x32", + "idiom" : "mac", + "filename" : "app_icon_64.png", + "scale" : "2x" + }, + { + "size" : "128x128", + "idiom" : "mac", + "filename" : "app_icon_128.png", + "scale" : "1x" + }, + { + "size" : "128x128", + "idiom" : "mac", + "filename" : "app_icon_256.png", + "scale" : "2x" + }, + { + "size" : "256x256", + "idiom" : "mac", + "filename" : "app_icon_256.png", + "scale" : "1x" + }, + { + "size" : "256x256", + "idiom" : "mac", + "filename" : "app_icon_512.png", + "scale" : "2x" + }, + { + "size" : "512x512", + "idiom" : "mac", + "filename" : "app_icon_512.png", + "scale" : "1x" + }, + { + "size" : "512x512", + "idiom" : "mac", + "filename" : "app_icon_1024.png", + "scale" : "2x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png new file mode 100644 index 0000000..82b6f9d Binary files /dev/null and b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png differ diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png new file mode 100644 index 0000000..13b35eb Binary files /dev/null and b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png differ diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png new file mode 100644 index 0000000..0a3f5fa Binary files /dev/null and b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png differ diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png new file mode 100644 index 0000000..bdb5722 Binary files /dev/null and b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png differ diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png new file mode 100644 index 0000000..f083318 Binary files /dev/null and b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png differ diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png new file mode 100644 index 0000000..326c0e7 Binary files /dev/null and b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png differ diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png new file mode 100644 index 0000000..2f1632c Binary files /dev/null and b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png differ diff --git a/macos/Runner/Base.lproj/MainMenu.xib b/macos/Runner/Base.lproj/MainMenu.xib new file mode 100644 index 0000000..80e867a --- /dev/null +++ b/macos/Runner/Base.lproj/MainMenu.xib @@ -0,0 +1,343 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/macos/Runner/Configs/AppInfo.xcconfig b/macos/Runner/Configs/AppInfo.xcconfig new file mode 100644 index 0000000..40af5b9 --- /dev/null +++ b/macos/Runner/Configs/AppInfo.xcconfig @@ -0,0 +1,14 @@ +// Application-level settings for the Runner target. +// +// This may be replaced with something auto-generated from metadata (e.g., pubspec.yaml) in the +// future. If not, the values below would default to using the project name when this becomes a +// 'flutter create' template. + +// The application's name. By default this is also the title of the Flutter window. +PRODUCT_NAME = doormile + +// The application's bundle identifier +PRODUCT_BUNDLE_IDENTIFIER = com.example.doormile + +// The copyright displayed in application information +PRODUCT_COPYRIGHT = Copyright © 2026 com.example. All rights reserved. diff --git a/macos/Runner/Configs/Debug.xcconfig b/macos/Runner/Configs/Debug.xcconfig new file mode 100644 index 0000000..36b0fd9 --- /dev/null +++ b/macos/Runner/Configs/Debug.xcconfig @@ -0,0 +1,2 @@ +#include "../../Flutter/Flutter-Debug.xcconfig" +#include "Warnings.xcconfig" diff --git a/macos/Runner/Configs/Release.xcconfig b/macos/Runner/Configs/Release.xcconfig new file mode 100644 index 0000000..dff4f49 --- /dev/null +++ b/macos/Runner/Configs/Release.xcconfig @@ -0,0 +1,2 @@ +#include "../../Flutter/Flutter-Release.xcconfig" +#include "Warnings.xcconfig" diff --git a/macos/Runner/Configs/Warnings.xcconfig b/macos/Runner/Configs/Warnings.xcconfig new file mode 100644 index 0000000..42bcbf4 --- /dev/null +++ b/macos/Runner/Configs/Warnings.xcconfig @@ -0,0 +1,13 @@ +WARNING_CFLAGS = -Wall -Wconditional-uninitialized -Wnullable-to-nonnull-conversion -Wmissing-method-return-type -Woverlength-strings +GCC_WARN_UNDECLARED_SELECTOR = YES +CLANG_UNDEFINED_BEHAVIOR_SANITIZER_NULLABILITY = YES +CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE +CLANG_WARN__DUPLICATE_METHOD_MATCH = YES +CLANG_WARN_PRAGMA_PACK = YES +CLANG_WARN_STRICT_PROTOTYPES = YES +CLANG_WARN_COMMA = YES +GCC_WARN_STRICT_SELECTOR_MATCH = YES +CLANG_WARN_OBJC_REPEATED_USE_OF_WEAK = YES +CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES +GCC_WARN_SHADOW = YES +CLANG_WARN_UNREACHABLE_CODE = YES diff --git a/macos/Runner/DebugProfile.entitlements b/macos/Runner/DebugProfile.entitlements new file mode 100644 index 0000000..dddb8a3 --- /dev/null +++ b/macos/Runner/DebugProfile.entitlements @@ -0,0 +1,12 @@ + + + + + com.apple.security.app-sandbox + + com.apple.security.cs.allow-jit + + com.apple.security.network.server + + + diff --git a/macos/Runner/Info.plist b/macos/Runner/Info.plist new file mode 100644 index 0000000..4789daa --- /dev/null +++ b/macos/Runner/Info.plist @@ -0,0 +1,32 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIconFile + + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + APPL + CFBundleShortVersionString + $(FLUTTER_BUILD_NAME) + CFBundleVersion + $(FLUTTER_BUILD_NUMBER) + LSMinimumSystemVersion + $(MACOSX_DEPLOYMENT_TARGET) + NSHumanReadableCopyright + $(PRODUCT_COPYRIGHT) + NSMainNibFile + MainMenu + NSPrincipalClass + NSApplication + + diff --git a/macos/Runner/MainFlutterWindow.swift b/macos/Runner/MainFlutterWindow.swift new file mode 100644 index 0000000..3cc05eb --- /dev/null +++ b/macos/Runner/MainFlutterWindow.swift @@ -0,0 +1,15 @@ +import Cocoa +import FlutterMacOS + +class MainFlutterWindow: NSWindow { + override func awakeFromNib() { + let flutterViewController = FlutterViewController() + let windowFrame = self.frame + self.contentViewController = flutterViewController + self.setFrame(windowFrame, display: true) + + RegisterGeneratedPlugins(registry: flutterViewController) + + super.awakeFromNib() + } +} diff --git a/macos/Runner/Release.entitlements b/macos/Runner/Release.entitlements new file mode 100644 index 0000000..852fa1a --- /dev/null +++ b/macos/Runner/Release.entitlements @@ -0,0 +1,8 @@ + + + + + com.apple.security.app-sandbox + + + diff --git a/macos/RunnerTests/RunnerTests.swift b/macos/RunnerTests/RunnerTests.swift new file mode 100644 index 0000000..61f3bd1 --- /dev/null +++ b/macos/RunnerTests/RunnerTests.swift @@ -0,0 +1,12 @@ +import Cocoa +import FlutterMacOS +import XCTest + +class RunnerTests: XCTestCase { + + func testExample() { + // If you add code to the Runner application, consider adding tests here. + // See https://developer.apple.com/documentation/xctest for more information about using XCTest. + } + +} diff --git a/pubspec.lock b/pubspec.lock new file mode 100644 index 0000000..9ebf0de --- /dev/null +++ b/pubspec.lock @@ -0,0 +1,850 @@ +# Generated by pub +# See https://dart.dev/tools/pub/glossary#lockfile +packages: + _flutterfire_internals: + dependency: transitive + description: + name: _flutterfire_internals + sha256: "0d1f0adfabbab9f46a1a80ce84a4d8b852b6e4dbf53ce413b30e0cf7d3631b71" + url: "https://pub.dev" + source: hosted + version: "1.3.72" + 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: a96e8b390886ee8abb49b7bd3ac8df6f451c621619f52a26e815fdcf568959ff + url: "https://pub.dev" + source: hosted + version: "4.0.9" + args: + dependency: transitive + description: + name: args + sha256: d0481093c50b1da8910eb0bb301626d4d8eb7284aa739614d2b394ee09e3ea04 + url: "https://pub.dev" + source: hosted + version: "2.7.0" + async: + dependency: transitive + description: + name: async + sha256: e2eb0491ba5ddb6177742d2da23904574082139b07c1e33b8503b9f46f3e1a37 + url: "https://pub.dev" + source: hosted + version: "2.13.1" + boolean_selector: + dependency: transitive + description: + name: boolean_selector + sha256: "8aab1771e1243a5063b8b0ff68042d67334e3feab9e95b9490f9a6ebf73b42ea" + url: "https://pub.dev" + source: hosted + version: "2.1.2" + characters: + dependency: transitive + description: + name: characters + sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803 + url: "https://pub.dev" + source: hosted + version: "1.4.0" + checked_yaml: + dependency: transitive + description: + name: checked_yaml + sha256: "959525d3162f249993882720d52b7e0c833978df229be20702b33d48d91de70f" + url: "https://pub.dev" + source: hosted + version: "2.0.4" + cli_util: + dependency: transitive + description: + name: cli_util + sha256: ff6785f7e9e3c38ac98b2fb035701789de90154024a75b6cb926445e83197d1c + url: "https://pub.dev" + source: hosted + version: "0.4.2" + clock: + dependency: transitive + description: + name: clock + sha256: fddb70d9b5277016c77a80201021d40a2247104d9f4aa7bab7157b7e3f05b84b + url: "https://pub.dev" + source: hosted + version: "1.1.2" + collection: + dependency: transitive + description: + name: collection + sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76" + url: "https://pub.dev" + source: hosted + version: "1.19.1" + crypto: + dependency: transitive + description: + name: crypto + sha256: c8ea0233063ba03258fbcf2ca4d6dadfefe14f02fab57702265467a19f27fadf + url: "https://pub.dev" + source: hosted + version: "3.0.7" + csslib: + dependency: transitive + description: + name: csslib + sha256: "09bad715f418841f976c77db72d5398dc1253c21fb9c0c7f0b0b985860b2d58e" + url: "https://pub.dev" + source: hosted + version: "1.0.2" + cupertino_icons: + dependency: "direct main" + description: + name: cupertino_icons + sha256: "41e005c33bd814be4d3096aff55b1908d419fde52ca656c8c47719ec745873cd" + url: "https://pub.dev" + source: hosted + version: "1.0.9" + dbus: + dependency: transitive + description: + name: dbus + sha256: "0ce9b0a839e6dee59a37a623d2fc26a35bbbe6404213e419b0d6411023d62645" + url: "https://pub.dev" + source: hosted + version: "0.7.14" + fake_async: + dependency: transitive + description: + name: fake_async + sha256: "5368f224a74523e8d2e7399ea1638b37aecfca824a3cc4dfdf77bf1fa905ac44" + url: "https://pub.dev" + source: hosted + version: "1.3.3" + ffi: + dependency: transitive + description: + name: ffi + sha256: "6d7fd89431262d8f3125e81b50d3847a091d846eafcd4fdb88dd06f36d705a45" + url: "https://pub.dev" + source: hosted + version: "2.2.0" + ffi_leak_tracker: + dependency: transitive + description: + name: ffi_leak_tracker + sha256: "4093d4ef9ca06ffe2786e73bfb25e22aa92112b9bb4ec941f11e3e6b61489a97" + url: "https://pub.dev" + source: hosted + version: "0.1.2" + file: + dependency: transitive + description: + name: file + sha256: a3b4f84adafef897088c160faf7dfffb7696046cb13ae90b508c2cbc95d3b8d4 + url: "https://pub.dev" + source: hosted + version: "7.0.1" + firebase_auth: + dependency: "direct main" + description: + name: firebase_auth + sha256: "9905a315f1235a649e29df19b246adde7350a11234837cd605254b8e25144711" + url: "https://pub.dev" + source: hosted + version: "6.5.2" + firebase_auth_platform_interface: + dependency: transitive + description: + name: firebase_auth_platform_interface + sha256: f757dc5636ce2b204a82383f7246ef0d9c925f9e96c8c9a4a6c315d673d662bb + url: "https://pub.dev" + source: hosted + version: "9.0.2" + firebase_auth_web: + dependency: transitive + description: + name: firebase_auth_web + sha256: "1c44330eef3c82068e0c2919b6b015897d49211dcccdf64f90a2ed73ce216ecb" + url: "https://pub.dev" + source: hosted + version: "6.2.2" + firebase_core: + dependency: "direct main" + description: + name: firebase_core + sha256: ec46a100a560d3bd5f97f2d89ba7492cb09b6dd0a4a28753d1258f360d6bd9f9 + url: "https://pub.dev" + source: hosted + version: "4.10.0" + firebase_core_platform_interface: + dependency: transitive + description: + name: firebase_core_platform_interface + sha256: "4a120366dbf7d5a8ee9438978530b664b855728fb8dcc3a201017660817e555b" + url: "https://pub.dev" + source: hosted + version: "7.0.1" + firebase_core_web: + dependency: transitive + description: + name: firebase_core_web + sha256: "5ad1be848692ec148f2d6a8ad2a3838cb852ea5f3c9e6479a7afce479e1854f8" + url: "https://pub.dev" + source: hosted + version: "3.8.0" + fixnum: + dependency: transitive + description: + name: fixnum + sha256: b6dc7065e46c974bc7c5f143080a6764ec7a4be6da1285ececdc37be96de53be + url: "https://pub.dev" + source: hosted + version: "1.1.1" + flutter: + dependency: "direct main" + description: flutter + source: sdk + version: "0.0.0" + flutter_launcher_icons: + dependency: "direct dev" + description: + name: flutter_launcher_icons + sha256: "10f13781741a2e3972126fae08393d3c4e01fa4cd7473326b94b72cf594195e7" + url: "https://pub.dev" + source: hosted + version: "0.14.4" + flutter_lints: + dependency: "direct dev" + description: + name: flutter_lints + sha256: "3f41d009ba7172d5ff9be5f6e6e6abb4300e263aab8866d2a0842ed2a70f8f0c" + url: "https://pub.dev" + source: hosted + version: "4.0.0" + flutter_native_splash: + dependency: "direct dev" + description: + name: flutter_native_splash + sha256: "4fb9f4113350d3a80841ce05ebf1976a36de622af7d19aca0ca9a9911c7ff002" + url: "https://pub.dev" + source: hosted + version: "2.4.7" + flutter_test: + dependency: "direct dev" + description: flutter + source: sdk + version: "0.0.0" + flutter_web_plugins: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" + geoclue: + dependency: transitive + description: + name: geoclue + sha256: c2a998c77474fc57aa00c6baa2928e58f4b267649057a1c76738656e9dbd2a7f + url: "https://pub.dev" + source: hosted + version: "0.1.1" + geocoding: + dependency: "direct main" + description: + name: geocoding + sha256: "606be036287842d779d7ec4e2f6c9435fc29bbbd3c6da6589710f981d8852895" + url: "https://pub.dev" + source: hosted + version: "4.0.0" + geocoding_android: + dependency: transitive + description: + name: geocoding_android + sha256: ba810da90d6633cbb82bbab630e5b4a3b7d23503263c00ae7f1ef0316dcae5b9 + url: "https://pub.dev" + source: hosted + version: "4.0.1" + geocoding_ios: + dependency: transitive + description: + name: geocoding_ios + sha256: "18ab1c8369e2b0dcb3a8ccc907319334f35ee8cf4cfef4d9c8e23b13c65cb825" + url: "https://pub.dev" + source: hosted + version: "3.1.0" + geocoding_platform_interface: + dependency: transitive + description: + name: geocoding_platform_interface + sha256: "8c2c8226e5c276594c2e18bfe88b19110ed770aeb7c1ab50ede570be8b92229b" + url: "https://pub.dev" + source: hosted + version: "3.2.0" + geolocator: + dependency: "direct main" + description: + name: geolocator + sha256: "79939537046c9025be47ec645f35c8090ecadb6fe98eba146a0d25e8c1357516" + url: "https://pub.dev" + source: hosted + version: "14.0.2" + geolocator_android: + dependency: transitive + description: + name: geolocator_android + sha256: "179c3cb66dfa674fc9ccbf2be872a02658724d1c067634e2c427cf6df7df901a" + url: "https://pub.dev" + source: hosted + version: "5.0.2" + geolocator_apple: + dependency: transitive + description: + name: geolocator_apple + sha256: dbdd8789d5aaf14cf69f74d4925ad1336b4433a6efdf2fce91e8955dc921bf22 + url: "https://pub.dev" + source: hosted + version: "2.3.13" + geolocator_linux: + dependency: transitive + description: + name: geolocator_linux + sha256: b02993d59b753bf1967ed28862002841aec5cdd5090c1f495213b32ce488aced + url: "https://pub.dev" + source: hosted + version: "0.2.5" + geolocator_platform_interface: + dependency: transitive + description: + name: geolocator_platform_interface + sha256: dde05dae7d584db6e82feb87dd9fb0b4b4c83ed68065667b4bef637be38e13a7 + url: "https://pub.dev" + source: hosted + version: "4.2.7" + geolocator_web: + dependency: transitive + description: + name: geolocator_web + sha256: b1ae9bdfd90f861fde8fd4f209c37b953d65e92823cb73c7dee1fa021b06f172 + url: "https://pub.dev" + source: hosted + version: "4.1.3" + geolocator_windows: + dependency: transitive + description: + name: geolocator_windows + sha256: "175435404d20278ffd220de83c2ca293b73db95eafbdc8131fe8609be1421eb6" + url: "https://pub.dev" + source: hosted + version: "0.2.5" + gsettings: + dependency: transitive + description: + name: gsettings + sha256: "1b0ce661f5436d2db1e51f3c4295a49849f03d304003a7ba177d01e3a858249c" + url: "https://pub.dev" + source: hosted + version: "0.2.8" + html: + dependency: transitive + description: + name: html + sha256: "6d1264f2dffa1b1101c25a91dff0dc2daee4c18e87cd8538729773c073dbf602" + url: "https://pub.dev" + source: hosted + version: "0.15.6" + http: + dependency: "direct main" + description: + name: http + sha256: "87721a4a50b19c7f1d49001e51409bddc46303966ce89a65af4f4e6004896412" + url: "https://pub.dev" + source: hosted + version: "1.6.0" + http_parser: + dependency: transitive + description: + name: http_parser + sha256: "178d74305e7866013777bab2c3d8726205dc5a4dd935297175b19a23a2e66571" + url: "https://pub.dev" + source: hosted + version: "4.1.2" + image: + dependency: transitive + description: + name: image + sha256: f9881ff4998044947ec38d098bc7c8316ae1186fa786eddffdb867b9bc94dfce + url: "https://pub.dev" + source: hosted + version: "4.8.0" + json_annotation: + dependency: transitive + description: + name: json_annotation + sha256: "2a743920d81b7910627f68ee2c9ac1fc0bfee32b9fc3403587d7c6791ca12f80" + url: "https://pub.dev" + source: hosted + version: "4.12.0" + leak_tracker: + dependency: transitive + description: + name: leak_tracker + sha256: "33e2e26bdd85a0112ec15400c8cbffea70d0f9c3407491f672a2fad47915e2de" + url: "https://pub.dev" + source: hosted + version: "11.0.2" + leak_tracker_flutter_testing: + dependency: transitive + description: + name: leak_tracker_flutter_testing + sha256: "1dbc140bb5a23c75ea9c4811222756104fbcd1a27173f0c34ca01e16bea473c1" + url: "https://pub.dev" + source: hosted + version: "3.0.10" + leak_tracker_testing: + dependency: transitive + description: + name: leak_tracker_testing + sha256: "8d5a2d49f4a66b49744b23b018848400d23e54caf9463f4eb20df3eb8acb2eb1" + url: "https://pub.dev" + source: hosted + version: "3.0.2" + lints: + dependency: transitive + description: + name: lints + sha256: "976c774dd944a42e83e2467f4cc670daef7eed6295b10b36ae8c85bcbf828235" + url: "https://pub.dev" + source: hosted + version: "4.0.0" + matcher: + dependency: transitive + description: + name: matcher + sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2 + url: "https://pub.dev" + source: hosted + version: "0.12.17" + material_color_utilities: + dependency: transitive + description: + name: material_color_utilities + sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec + url: "https://pub.dev" + source: hosted + version: "0.11.1" + meta: + dependency: transitive + description: + name: meta + sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394" + url: "https://pub.dev" + source: hosted + version: "1.17.0" + nested: + dependency: transitive + description: + name: nested + sha256: "03bac4c528c64c95c722ec99280375a6f2fc708eec17c7b3f07253b626cd2a20" + url: "https://pub.dev" + source: hosted + version: "1.0.0" + package_info_plus: + dependency: transitive + description: + name: package_info_plus + sha256: "4bf625947f6c7713ee242296a682e23e44823c09cf9d79e4f1238923c92db852" + url: "https://pub.dev" + source: hosted + version: "10.1.0" + package_info_plus_platform_interface: + dependency: transitive + description: + name: package_info_plus_platform_interface + sha256: db762cb2f4f25ee60fb6359773861b0f199e00b90d237bd85a76a1e806b46ef4 + url: "https://pub.dev" + source: hosted + version: "4.1.0" + path: + dependency: transitive + description: + name: path + sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5" + url: "https://pub.dev" + source: hosted + version: "1.9.1" + path_provider_linux: + dependency: transitive + description: + name: path_provider_linux + sha256: f7a1fe3a634fe7734c8d3f2766ad746ae2a2884abe22e241a8b301bf5cac3279 + url: "https://pub.dev" + source: hosted + version: "2.2.1" + path_provider_platform_interface: + dependency: transitive + description: + name: path_provider_platform_interface + sha256: "88f5779f72ba699763fa3a3b06aa4bf6de76c8e5de842cf6f29e2e06476c2334" + url: "https://pub.dev" + source: hosted + version: "2.1.2" + path_provider_windows: + dependency: transitive + description: + name: path_provider_windows + sha256: bd6f00dbd873bfb70d0761682da2b3a2c2fccc2b9e84c495821639601d81afe7 + url: "https://pub.dev" + source: hosted + version: "2.3.0" + permission_handler: + dependency: "direct main" + description: + name: permission_handler + sha256: fe54465bcc62a4564c6e4db337bbaded6c0c0fa6e10487414436d163114784f6 + url: "https://pub.dev" + source: hosted + version: "12.0.3" + permission_handler_android: + dependency: transitive + description: + name: permission_handler_android + sha256: "1e3bc410ca1bf84662104b100eb126e066cb55791b7451307f9708d4007350e6" + url: "https://pub.dev" + source: hosted + version: "13.0.1" + permission_handler_apple: + dependency: transitive + description: + name: permission_handler_apple + sha256: e20daf680eef1ca62ffe8c8c526b778cc386d50137c77ac71c8ec9c88c13fb9d + url: "https://pub.dev" + source: hosted + version: "9.4.9" + permission_handler_html: + dependency: transitive + description: + name: permission_handler_html + sha256: "38f000e83355abb3392140f6bc3030660cfaef189e1f87824facb76300b4ff24" + url: "https://pub.dev" + source: hosted + version: "0.1.3+5" + permission_handler_platform_interface: + dependency: transitive + description: + name: permission_handler_platform_interface + sha256: eb99b295153abce5d683cac8c02e22faab63e50679b937fa1bf67d58bb282878 + url: "https://pub.dev" + source: hosted + version: "4.3.0" + permission_handler_windows: + dependency: transitive + description: + name: permission_handler_windows + sha256: "1a790728016f79a41216d88672dbc5df30e686e811ad4e698bfc51f76ad91f1e" + url: "https://pub.dev" + source: hosted + version: "0.2.1" + petitparser: + dependency: transitive + description: + name: petitparser + sha256: "91bd59303e9f769f108f8df05e371341b15d59e995e6806aefab827b58336675" + url: "https://pub.dev" + source: hosted + version: "7.0.2" + platform: + dependency: transitive + description: + name: platform + sha256: "5d6b1b0036a5f331ebc77c850ebc8506cbc1e9416c27e59b439f917a902a4984" + url: "https://pub.dev" + source: hosted + version: "3.1.6" + plugin_platform_interface: + dependency: transitive + description: + name: plugin_platform_interface + sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02" + url: "https://pub.dev" + source: hosted + version: "2.1.8" + posix: + dependency: transitive + description: + name: posix + sha256: "185ef7606574f789b40f289c233efa52e96dead518aed988e040a10737febb07" + url: "https://pub.dev" + source: hosted + version: "6.5.0" + provider: + dependency: "direct main" + description: + name: provider + sha256: "4e82183fa20e5ca25703ead7e05de9e4cceed1fbd1eadc1ac3cb6f565a09f272" + url: "https://pub.dev" + source: hosted + version: "6.1.5+1" + shared_preferences: + dependency: "direct main" + description: + name: shared_preferences + sha256: c3025c5534b01739267eb7d76959bbc25a6d10f6988e1c2a3036940133dd10bf + url: "https://pub.dev" + source: hosted + version: "2.5.5" + shared_preferences_android: + dependency: transitive + description: + name: shared_preferences_android + sha256: e8d4762b1e2e8578fc4d0fd548cebf24afd24f49719c08974df92834565e2c53 + url: "https://pub.dev" + source: hosted + version: "2.4.23" + shared_preferences_foundation: + dependency: transitive + description: + name: shared_preferences_foundation + sha256: "4e7eaffc2b17ba398759f1151415869a34771ba11ebbccd1b0145472a619a64f" + url: "https://pub.dev" + source: hosted + version: "2.5.6" + shared_preferences_linux: + dependency: transitive + description: + name: shared_preferences_linux + sha256: "580abfd40f415611503cae30adf626e6656dfb2f0cee8f465ece7b6defb40f2f" + url: "https://pub.dev" + source: hosted + version: "2.4.1" + shared_preferences_platform_interface: + dependency: transitive + description: + name: shared_preferences_platform_interface + sha256: "649dc798a33931919ea356c4305c2d1f81619ea6e92244070b520187b5140ef9" + url: "https://pub.dev" + source: hosted + version: "2.4.2" + shared_preferences_web: + dependency: transitive + description: + name: shared_preferences_web + sha256: c49bd060261c9a3f0ff445892695d6212ff603ef3115edbb448509d407600019 + url: "https://pub.dev" + source: hosted + version: "2.4.3" + shared_preferences_windows: + dependency: transitive + description: + name: shared_preferences_windows + sha256: "94ef0f72b2d71bc3e700e025db3710911bd51a71cefb65cc609dd0d9a982e3c1" + url: "https://pub.dev" + source: hosted + version: "2.4.1" + sky_engine: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" + source_span: + dependency: transitive + description: + name: source_span + sha256: "56a02f1f4cd1a2d96303c0144c93bd6d909eea6bee6bf5a0e0b685edbd4c47ab" + url: "https://pub.dev" + source: hosted + version: "1.10.2" + stack_trace: + dependency: transitive + description: + name: stack_trace + sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1" + url: "https://pub.dev" + source: hosted + version: "1.12.1" + stream_channel: + dependency: transitive + description: + name: stream_channel + sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d" + url: "https://pub.dev" + source: hosted + version: "2.1.4" + string_scanner: + dependency: transitive + description: + name: string_scanner + sha256: "921cd31725b72fe181906c6a94d987c78e3b98c2e205b397ea399d4054872b43" + url: "https://pub.dev" + source: hosted + version: "1.4.1" + term_glyph: + dependency: transitive + description: + name: term_glyph + sha256: "7f554798625ea768a7518313e58f83891c7f5024f88e46e7182a4558850a4b8e" + url: "https://pub.dev" + source: hosted + version: "1.2.2" + test_api: + dependency: transitive + description: + name: test_api + sha256: ab2726c1a94d3176a45960b6234466ec367179b87dd74f1611adb1f3b5fb9d55 + url: "https://pub.dev" + source: hosted + version: "0.7.7" + typed_data: + dependency: transitive + description: + name: typed_data + sha256: f9049c039ebfeb4cf7a7104a675823cd72dba8297f264b6637062516699fa006 + url: "https://pub.dev" + source: hosted + version: "1.4.0" + universal_io: + dependency: transitive + description: + name: universal_io + sha256: f63cbc48103236abf48e345e07a03ce5757ea86285ed313a6a032596ed9301e2 + url: "https://pub.dev" + source: hosted + version: "2.3.1" + url_launcher: + dependency: "direct main" + description: + name: url_launcher + sha256: f6a7e5c4835bb4e3026a04793a4199ca2d14c739ec378fdfe23fc8075d0439f8 + url: "https://pub.dev" + source: hosted + version: "6.3.2" + url_launcher_android: + dependency: transitive + description: + name: url_launcher_android + sha256: "17bc677f0b301615530dd1d67e0a9828cafa2d0b6b6eae4cd3679b7eac4a273c" + url: "https://pub.dev" + source: hosted + version: "6.3.30" + url_launcher_ios: + dependency: transitive + description: + name: url_launcher_ios + sha256: "580fe5dfb51671ae38191d316e027f6b76272b026370708c2d898799750a02b0" + url: "https://pub.dev" + source: hosted + version: "6.4.1" + url_launcher_linux: + dependency: transitive + description: + name: url_launcher_linux + sha256: d5e14138b3bc193a0f63c10a53c94b91d399df0512b1f29b94a043db7482384a + url: "https://pub.dev" + source: hosted + version: "3.2.2" + url_launcher_macos: + dependency: transitive + description: + name: url_launcher_macos + sha256: "368adf46f71ad3c21b8f06614adb38346f193f3a59ba8fe9a2fd74133070ba18" + url: "https://pub.dev" + source: hosted + version: "3.2.5" + url_launcher_platform_interface: + dependency: transitive + description: + name: url_launcher_platform_interface + sha256: "552f8a1e663569be95a8190206a38187b531910283c3e982193e4f2733f01029" + url: "https://pub.dev" + source: hosted + version: "2.3.2" + url_launcher_web: + dependency: transitive + description: + name: url_launcher_web + sha256: "85c81589622fbc87c1c683aaea164d3604a7777495a79d91e39ffcdec39ddb34" + url: "https://pub.dev" + source: hosted + version: "2.4.3" + url_launcher_windows: + dependency: transitive + description: + name: url_launcher_windows + sha256: "712c70ab1b99744ff066053cbe3e80c73332b38d46e5e945c98689b2e66fc15f" + url: "https://pub.dev" + source: hosted + version: "3.1.5" + uuid: + dependency: transitive + description: + name: uuid + sha256: "1fef9e8e11e2991bb773070d4656b7bd5d850967a2456cfc83cf47925ba79489" + url: "https://pub.dev" + source: hosted + version: "4.5.3" + vector_math: + dependency: transitive + description: + name: vector_math + sha256: d530bd74fea330e6e364cda7a85019c434070188383e1cd8d9777ee586914c5b + url: "https://pub.dev" + source: hosted + version: "2.2.0" + vm_service: + dependency: transitive + description: + name: vm_service + sha256: "0016aef94fc66495ac78af5859181e3f3bf2026bd8eecc72b9565601e19ab360" + url: "https://pub.dev" + source: hosted + version: "15.2.0" + web: + dependency: transitive + description: + name: web + sha256: "868d88a33d8a87b18ffc05f9f030ba328ffefba92d6c127917a2ba740f9cfe4a" + url: "https://pub.dev" + source: hosted + version: "1.1.1" + win32: + dependency: transitive + description: + name: win32 + sha256: ba6f4bba816c8d7e3c1580e170f3786d216951cc6b94babc3b814c08d2cb2738 + url: "https://pub.dev" + source: hosted + version: "6.3.0" + xdg_directories: + dependency: transitive + description: + name: xdg_directories + sha256: "7a3f37b05d989967cdddcbb571f1ea834867ae2faa29725fd085180e0883aa15" + url: "https://pub.dev" + source: hosted + version: "1.1.0" + xml: + dependency: transitive + description: + name: xml + sha256: "971043b3a0d3da28727e40ed3e0b5d18b742fa5a68665cca88e74b7876d5e025" + url: "https://pub.dev" + source: hosted + version: "6.6.1" + yaml: + dependency: transitive + description: + name: yaml + sha256: b9da305ac7c39faa3f030eccd175340f968459dae4af175130b3fc47e40d76ce + url: "https://pub.dev" + source: hosted + version: "3.1.3" +sdks: + dart: ">=3.10.0 <4.0.0" + flutter: ">=3.38.1" diff --git a/pubspec.yaml b/pubspec.yaml new file mode 100644 index 0000000..24625aa --- /dev/null +++ b/pubspec.yaml @@ -0,0 +1,44 @@ +name: doormile +description: "Doormile — EV-first parcel delivery app." +publish_to: 'none' +version: 1.0.0+1 + +environment: + sdk: ">=3.0.0 <4.0.0" + +dependencies: + flutter: + sdk: flutter + cupertino_icons: ^1.0.6 + provider: ^6.1.2 + firebase_core: ^4.10.0 + firebase_auth: ^6.5.2 + http: ^1.6.0 + shared_preferences: ^2.5.5 + permission_handler: ^12.0.3 + geolocator: ^14.0.2 + url_launcher: ^6.3.2 + geocoding: ^4.0.0 + +dev_dependencies: + flutter_test: + sdk: flutter + flutter_lints: ^4.0.0 + flutter_launcher_icons: ^0.14.4 + flutter_native_splash: ^2.4.7 + +flutter: + uses-material-design: true + fonts: + - family: Public Sans + fonts: + - asset: assets/fonts/PublicSans.ttf + + assets: + - assets/images/ + +flutter_launcher_icons: + android: "launcher_icon" + ios: true + image_path: "assets/images/launcher_doormile.png" + diff --git a/test/flow_test.dart b/test/flow_test.dart new file mode 100644 index 0000000..b46a1c8 --- /dev/null +++ b/test/flow_test.dart @@ -0,0 +1,88 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; + +import 'package:doormile/main.dart'; + +/// Drives the app through its main flows at a phone-sized viewport to catch +/// layout overflows and runtime exceptions on each screen. +void main() { + testWidgets('full user journey renders without errors', (WidgetTester tester) async { + tester.view.physicalSize = const Size(1170, 2532); // iPhone-ish @3x + tester.view.devicePixelRatio = 3.0; + addTearDown(tester.view.reset); + + await tester.pumpWidget(const DoormileApp()); + + // Splash -> Onboarding + await tester.pump(const Duration(seconds: 3)); + await tester.pumpAndSettle(); + expect(find.text('Send a parcel in one touch'), findsOneWidget); + + // Skip onboarding -> Login + await tester.tap(find.text('Skip')); + await tester.pumpAndSettle(); + expect(find.text('Welcome back'), findsOneWidget); + + // Enter phone and request OTP + await tester.enterText(find.byType(TextField).first, '9876543210'); + await tester.pump(); + await tester.tap(find.text('Send OTP')); + await tester.pump(const Duration(milliseconds: 1400)); + await tester.pumpAndSettle(); + expect(find.text('Verify your number'), findsOneWidget); + + // Fill the 6 OTP boxes + final otpBoxes = find.byType(TextField); + for (int i = 0; i < 6; i++) { + await tester.enterText(otpBoxes.at(i), '1'); + await tester.pump(); + } + await tester.tap(find.text('Verify & Continue')); + await tester.pump(const Duration(milliseconds: 1300)); + await tester.pumpAndSettle(); + + // Home + expect(find.text('Send a parcel'), findsOneWidget); + + // Start a booking + await tester.tap(find.text('Book Now')); + await tester.pumpAndSettle(); + expect(find.text('Step 1: Confirm Pickup'), findsOneWidget); + + await tester.tap(find.text('Continue')); + await tester.pumpAndSettle(); + expect(find.text('What are you sending?'), findsOneWidget); + + await tester.tap(find.text('Continue')); + await tester.pumpAndSettle(); + expect(find.text('CHOOSE SERVICE'), findsOneWidget); + + // Confirm & Continue -> review + await tester.tap(find.textContaining('Confirm & Continue')); + await tester.pumpAndSettle(); + expect(find.text('Review Booking'), findsWidgets); + + // Pop all the way back to the home shell. + for (int i = 0; i < 4; i++) { + if (find.text('Send a parcel').evaluate().isNotEmpty) break; + await tester.pageBack(); + await tester.pumpAndSettle(); + } + expect(find.text('Send a parcel'), findsOneWidget); + + // Wallet tab + await tester.tap(find.text('Wallet').first); + await tester.pumpAndSettle(); + expect(find.text('Doormile Wallet'), findsOneWidget); + + // Profile tab + await tester.tap(find.text('Profile').first); + await tester.pumpAndSettle(); + expect(find.text('Riya Sharma'), findsOneWidget); + + // Track tab (orders list) + await tester.tap(find.text('Track').first); + await tester.pumpAndSettle(); + expect(find.text('My Parcels'), findsOneWidget); + }); +} diff --git a/test/screens_test.dart b/test/screens_test.dart new file mode 100644 index 0000000..dc8fd63 --- /dev/null +++ b/test/screens_test.dart @@ -0,0 +1,73 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:provider/provider.dart'; + +import 'package:doormile/state/app_state.dart'; +import 'package:doormile/theme/app_theme.dart'; +import 'package:doormile/screens/onboarding_screen.dart'; +import 'package:doormile/screens/login_screen.dart'; +import 'package:doormile/screens/otp_screen.dart'; +import 'package:doormile/screens/main_shell.dart'; +import 'package:doormile/screens/home_screen.dart'; +import 'package:doormile/screens/wallet_screen.dart'; +import 'package:doormile/screens/profile_screen.dart'; +import 'package:doormile/screens/help_support_screen.dart'; +import 'package:doormile/screens/booking/booking_pickup_screen.dart'; +import 'package:doormile/screens/booking/parcel_details_screen.dart'; +import 'package:doormile/screens/booking/service_price_screen.dart'; +import 'package:doormile/screens/booking/booking_preview_screen.dart'; +import 'package:doormile/screens/booking/booking_success_screen.dart'; +import 'package:doormile/screens/tracking/my_orders_screen.dart'; +import 'package:doormile/screens/tracking/live_tracking_screen.dart'; +import 'package:doormile/screens/tracking/shipment_journey_screen.dart'; + +/// Renders each screen in isolation at a phone viewport and settles it, so the +/// test framework flags any overflow in the *settled* layout (no transition +/// transients). A pumped order is placed first so success/tracking have data. +Future _renderScreen(WidgetTester tester, Widget child) async { + final state = AppState() + ..login() + ..setPhone('98765 43210'); + state.placeOrder(); + await tester.pumpWidget( + ChangeNotifierProvider.value( + value: state, + child: MaterialApp(theme: AppTheme.light, home: child), + ), + ); + await tester.pump(const Duration(milliseconds: 400)); + await tester.pump(const Duration(milliseconds: 400)); +} + +void main() { + setUp(() {}); + + final screens = { + 'Onboarding': () => const OnboardingScreen(), + 'Login': () => const LoginScreen(), + 'OTP': () => const OtpScreen(), + 'MainShell/Home': () => const MainShell(), + 'Home': () => const Scaffold(body: HomeScreen()), + 'Wallet': () => const Scaffold(body: WalletScreen()), + 'Profile': () => const Scaffold(body: ProfileScreen()), + 'Help': () => const HelpSupportScreen(), + 'BookingPickup': () => const BookingPickupScreen(), + 'ParcelDetails': () => const ParcelDetailsScreen(), + 'ServicePrice': () => const ServicePriceScreen(), + 'BookingPreview': () => const BookingPreviewScreen(), + 'BookingSuccess': () => const BookingSuccessScreen(), + 'MyOrders': () => const MyOrdersScreen(), + 'LiveTracking': () => const LiveTrackingScreen(), + 'ShipmentJourney': () => const ShipmentJourneyScreen(), + }; + + for (final entry in screens.entries) { + testWidgets('${entry.key} renders cleanly at phone size', (tester) async { + tester.view.physicalSize = const Size(1170, 2532); + tester.view.devicePixelRatio = 3.0; + addTearDown(tester.view.reset); + await _renderScreen(tester, entry.value()); + expect(tester.takeException(), isNull); + }); + } +} diff --git a/test/widget_test.dart b/test/widget_test.dart new file mode 100644 index 0000000..d90fb7b --- /dev/null +++ b/test/widget_test.dart @@ -0,0 +1,19 @@ +import 'package:flutter_test/flutter_test.dart'; + +import 'package:doormile/main.dart'; + +void main() { + testWidgets('App boots and advances from splash to onboarding', + (WidgetTester tester) async { + await tester.pumpWidget(const DoormileApp()); + + // Splash shows the brand wordmark. + expect(find.text('Doormile'), findsWidgets); + + // Let the splash auto-navigation timer fire, then settle on onboarding. + await tester.pump(const Duration(seconds: 3)); + await tester.pumpAndSettle(); + + expect(find.text('Send a parcel in one touch'), findsOneWidget); + }); +} diff --git a/web/favicon.png b/web/favicon.png new file mode 100644 index 0000000..8aaa46a Binary files /dev/null and b/web/favicon.png differ diff --git a/web/icons/Icon-192.png b/web/icons/Icon-192.png new file mode 100644 index 0000000..b749bfe Binary files /dev/null and b/web/icons/Icon-192.png differ diff --git a/web/icons/Icon-512.png b/web/icons/Icon-512.png new file mode 100644 index 0000000..88cfd48 Binary files /dev/null and b/web/icons/Icon-512.png differ diff --git a/web/icons/Icon-maskable-192.png b/web/icons/Icon-maskable-192.png new file mode 100644 index 0000000..eb9b4d7 Binary files /dev/null and b/web/icons/Icon-maskable-192.png differ diff --git a/web/icons/Icon-maskable-512.png b/web/icons/Icon-maskable-512.png new file mode 100644 index 0000000..d69c566 Binary files /dev/null and b/web/icons/Icon-maskable-512.png differ diff --git a/web/index.html b/web/index.html new file mode 100644 index 0000000..134aee5 --- /dev/null +++ b/web/index.html @@ -0,0 +1,116 @@ + + + + + + + + + + + + + + + + + + doormile + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/web/manifest.json b/web/manifest.json new file mode 100644 index 0000000..1ac7b7a --- /dev/null +++ b/web/manifest.json @@ -0,0 +1,35 @@ +{ + "name": "doormile", + "short_name": "doormile", + "start_url": ".", + "display": "standalone", + "background_color": "#0175C2", + "theme_color": "#0175C2", + "description": "A new Flutter project.", + "orientation": "portrait-primary", + "prefer_related_applications": false, + "icons": [ + { + "src": "icons/Icon-192.png", + "sizes": "192x192", + "type": "image/png" + }, + { + "src": "icons/Icon-512.png", + "sizes": "512x512", + "type": "image/png" + }, + { + "src": "icons/Icon-maskable-192.png", + "sizes": "192x192", + "type": "image/png", + "purpose": "maskable" + }, + { + "src": "icons/Icon-maskable-512.png", + "sizes": "512x512", + "type": "image/png", + "purpose": "maskable" + } + ] +} diff --git a/web/splash/img/dark-1x.png b/web/splash/img/dark-1x.png new file mode 100644 index 0000000..9a2c0a5 Binary files /dev/null and b/web/splash/img/dark-1x.png differ diff --git a/web/splash/img/dark-2x.png b/web/splash/img/dark-2x.png new file mode 100644 index 0000000..3e7236c Binary files /dev/null and b/web/splash/img/dark-2x.png differ diff --git a/web/splash/img/dark-3x.png b/web/splash/img/dark-3x.png new file mode 100644 index 0000000..cd98261 Binary files /dev/null and b/web/splash/img/dark-3x.png differ diff --git a/web/splash/img/dark-4x.png b/web/splash/img/dark-4x.png new file mode 100644 index 0000000..c894515 Binary files /dev/null and b/web/splash/img/dark-4x.png differ diff --git a/web/splash/img/light-1x.png b/web/splash/img/light-1x.png new file mode 100644 index 0000000..9a2c0a5 Binary files /dev/null and b/web/splash/img/light-1x.png differ diff --git a/web/splash/img/light-2x.png b/web/splash/img/light-2x.png new file mode 100644 index 0000000..3e7236c Binary files /dev/null and b/web/splash/img/light-2x.png differ diff --git a/web/splash/img/light-3x.png b/web/splash/img/light-3x.png new file mode 100644 index 0000000..cd98261 Binary files /dev/null and b/web/splash/img/light-3x.png differ diff --git a/web/splash/img/light-4x.png b/web/splash/img/light-4x.png new file mode 100644 index 0000000..c894515 Binary files /dev/null and b/web/splash/img/light-4x.png differ