├── .gitignore ├── .metadata ├── .vscode └── launch.json ├── LICENSE.md ├── README.md ├── analysis_options.yaml ├── android ├── .gitignore ├── app │ ├── build.gradle │ ├── proguard-rules.pro │ └── src │ │ ├── debug │ │ └── AndroidManifest.xml │ │ ├── main │ │ ├── AndroidManifest.xml │ │ ├── java │ │ │ └── io │ │ │ │ └── flutter │ │ │ │ └── app │ │ │ │ └── FlutterMultiDexApplication.java │ │ ├── kotlin │ │ │ └── com │ │ │ │ └── tarekalabd │ │ │ │ └── flutter_ecommerce │ │ │ │ └── MainActivity.kt │ │ └── res │ │ │ ├── drawable-v21 │ │ │ └── launch_background.xml │ │ │ ├── drawable │ │ │ └── launch_background.xml │ │ │ ├── mipmap-hdpi │ │ │ └── ic_launcher.png │ │ │ ├── mipmap-mdpi │ │ │ └── ic_launcher.png │ │ │ ├── mipmap-xhdpi │ │ │ └── ic_launcher.png │ │ │ ├── mipmap-xxhdpi │ │ │ └── ic_launcher.png │ │ │ ├── mipmap-xxxhdpi │ │ │ └── ic_launcher.png │ │ │ ├── values-night │ │ │ └── styles.xml │ │ │ └── values │ │ │ └── styles.xml │ │ └── profile │ │ └── AndroidManifest.xml ├── build.gradle ├── gradle.properties ├── gradle │ └── wrapper │ │ └── gradle-wrapper.properties └── settings.gradle ├── assets ├── facebook-svgrepo-com.svg └── google-svgrepo-com.svg ├── ios ├── .gitignore ├── Flutter │ ├── AppFrameworkInfo.plist │ ├── Debug.xcconfig │ └── Release.xcconfig ├── Podfile ├── Podfile.lock ├── Runner.xcodeproj │ ├── project.pbxproj │ ├── project.xcworkspace │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata │ │ │ ├── IDEWorkspaceChecks.plist │ │ │ └── WorkspaceSettings.xcsettings │ └── xcshareddata │ │ └── xcschemes │ │ └── Runner.xcscheme ├── Runner.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ ├── IDEWorkspaceChecks.plist │ │ └── WorkspaceSettings.xcsettings └── Runner │ ├── AppDelegate.swift │ ├── Assets.xcassets │ ├── AppIcon.appiconset │ │ ├── Contents.json │ │ ├── Icon-App-1024x1024@1x.png │ │ ├── Icon-App-20x20@1x.png │ │ ├── Icon-App-20x20@2x.png │ │ ├── Icon-App-20x20@3x.png │ │ ├── Icon-App-29x29@1x.png │ │ ├── Icon-App-29x29@2x.png │ │ ├── Icon-App-29x29@3x.png │ │ ├── Icon-App-40x40@1x.png │ │ ├── Icon-App-40x40@2x.png │ │ ├── Icon-App-40x40@3x.png │ │ ├── Icon-App-60x60@2x.png │ │ ├── Icon-App-60x60@3x.png │ │ ├── Icon-App-76x76@1x.png │ │ ├── Icon-App-76x76@2x.png │ │ └── Icon-App-83.5x83.5@2x.png │ └── LaunchImage.imageset │ │ ├── Contents.json │ │ ├── LaunchImage.png │ │ ├── LaunchImage@2x.png │ │ ├── LaunchImage@3x.png │ │ └── README.md │ ├── Base.lproj │ ├── LaunchScreen.storyboard │ └── Main.storyboard │ ├── Info.plist │ └── Runner-Bridging-Header.h ├── lib ├── controllers │ ├── auth │ │ ├── auth_cubit.dart │ │ └── auth_state.dart │ ├── auth_controller.dart │ ├── cart │ │ ├── cart_cubit.dart │ │ └── cart_state.dart │ ├── checkout │ │ ├── checkout_cubit.dart │ │ └── checkout_state.dart │ ├── database_controller.dart │ ├── home │ │ ├── home_cubit.dart │ │ └── home_state.dart │ └── product_details │ │ ├── product_details_cubit.dart │ │ └── product_details_state.dart ├── main.dart ├── models │ ├── add_to_cart_model.dart │ ├── delivery_method.dart │ ├── payment_method.dart │ ├── product.dart │ ├── shipping_address.dart │ └── user_data.dart ├── services │ ├── auth.dart │ ├── auth_services.dart │ ├── cart_services.dart │ ├── checkout_services.dart │ ├── firestore_services.dart │ ├── home_services.dart │ ├── product_details_services.dart │ └── stripe_services.dart ├── utilities │ ├── api_path.dart │ ├── args_models │ │ └── add_shipping_address_args.dart │ ├── assets.dart │ ├── constants.dart │ ├── enums.dart │ ├── router.dart │ └── routes.dart └── views │ ├── pages │ ├── auth_page.dart │ ├── bottom_navbar.dart │ ├── cart_page.dart │ ├── checkout │ │ ├── add_shipping_address_page.dart │ │ ├── checkout_page.dart │ │ ├── payment_methods_page.dart │ │ └── shipping_addresses_page.dart │ ├── home_page.dart │ ├── product_details.dart │ └── profle_page.dart │ └── widgets │ ├── cart_list_item.dart │ ├── checkout │ ├── add_new_card_bottom_sheet.dart │ ├── checkout_order_details.dart │ ├── delivery_method_item.dart │ ├── payment_component.dart │ ├── shipping_address_component.dart │ └── shipping_address_state_item.dart │ ├── drop_down_menu.dart │ ├── header_of_list.dart │ ├── list_header.dart │ ├── list_item_home.dart │ ├── main_button.dart │ ├── main_dialog.dart │ ├── order_summary_component.dart │ └── social_media_button.dart ├── pubspec.lock ├── pubspec.yaml └── test └── widget_test.dart /.gitignore: -------------------------------------------------------------------------------- 1 | # Miscellaneous 2 | *.class 3 | *.log 4 | *.pyc 5 | *.swp 6 | .DS_Store 7 | .atom/ 8 | .buildlog/ 9 | .history 10 | .svn/ 11 | migrate_working_dir/ 12 | 13 | # IntelliJ related 14 | *.iml 15 | *.ipr 16 | *.iws 17 | .idea/ 18 | 19 | # The .vscode folder contains launch configuration and tasks you configure in 20 | # VS Code which you may wish to be included in version control, so this line 21 | # is commented out by default. 22 | #.vscode/ 23 | 24 | # Flutter/Dart/Pub related 25 | **/doc/api/ 26 | **/ios/Flutter/.last_build_id 27 | .dart_tool/ 28 | .flutter-plugins 29 | .flutter-plugins-dependencies 30 | .packages 31 | .pub-cache/ 32 | .pub/ 33 | /build/ 34 | 35 | # Web related 36 | lib/generated_plugin_registrant.dart 37 | 38 | # Symbolication related 39 | app.*.symbols 40 | 41 | # Obfuscation related 42 | app.*.map.json 43 | 44 | # Android Studio will place build artifacts here 45 | /android/app/debug 46 | /android/app/profile 47 | /android/app/release 48 | -------------------------------------------------------------------------------- /.metadata: -------------------------------------------------------------------------------- 1 | # This file tracks properties of this Flutter project. 2 | # Used by Flutter tool to assess capabilities and perform upgrades etc. 3 | # 4 | # This file should be version controlled. 5 | 6 | version: 7 | revision: ee4e09cce01d6f2d7f4baebd247fde02e5008851 8 | channel: stable 9 | 10 | project_type: app 11 | 12 | # Tracks metadata for the flutter migrate command 13 | migration: 14 | platforms: 15 | - platform: root 16 | create_revision: ee4e09cce01d6f2d7f4baebd247fde02e5008851 17 | base_revision: ee4e09cce01d6f2d7f4baebd247fde02e5008851 18 | - platform: android 19 | create_revision: ee4e09cce01d6f2d7f4baebd247fde02e5008851 20 | base_revision: ee4e09cce01d6f2d7f4baebd247fde02e5008851 21 | - platform: ios 22 | create_revision: ee4e09cce01d6f2d7f4baebd247fde02e5008851 23 | base_revision: ee4e09cce01d6f2d7f4baebd247fde02e5008851 24 | 25 | # User provided section 26 | 27 | # List of Local paths (relative to this file) that should be 28 | # ignored by the migrate tool. 29 | # 30 | # Files that are not part of the templates will be ignored by default. 31 | unmanaged_files: 32 | - 'lib/main.dart' 33 | - 'ios/Runner.xcodeproj/project.pbxproj' 34 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "name": "flutter_ecommerce", 9 | "request": "launch", 10 | "type": "dart" 11 | }, 12 | { 13 | "name": "flutter_ecommerce (profile mode)", 14 | "request": "launch", 15 | "type": "dart", 16 | "flutterMode": "profile" 17 | }, 18 | { 19 | "name": "flutter_ecommerce (release mode)", 20 | "request": "launch", 21 | "type": "dart", 22 | "flutterMode": "release" 23 | } 24 | ] 25 | } -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Tarek Alabd 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Flutter E-commerce App 2 | 3 | This is an e-commerce app built with Flutter & Dart, built (and still working on it) in this [playlist on YouTube](https://www.youtube.com/playlist?list=PL0vtyWBHY2NXpW_Hazx7jCYqwVlwe7SYk). 4 | 5 | ## Why does this playlist exist? 6 | 7 | The main reason for this playlist is to simulate a part of the process we take with our teams to build a mobile application, we need to plan for the project, decide on how we will handle the state management, routing approaches, theming and so on, writing the code and after that refactoring it after seeing a better approach to do something or after code reviewing. 8 | 9 | ## Feature Set ✨ 10 | 11 | -------------------------------------------------------------------------------- /analysis_options.yaml: -------------------------------------------------------------------------------- 1 | # This file configures the analyzer, which statically analyzes Dart code to 2 | # check for errors, warnings, and lints. 3 | # 4 | # The issues identified by the analyzer are surfaced in the UI of Dart-enabled 5 | # IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be 6 | # invoked from the command line by running `flutter analyze`. 7 | 8 | # The following line activates a set of recommended lints for Flutter apps, 9 | # packages, and plugins designed to encourage good coding practices. 10 | include: package:flutter_lints/flutter.yaml 11 | 12 | linter: 13 | # The lint rules applied to this project can be customized in the 14 | # section below to disable rules from the `package:flutter_lints/flutter.yaml` 15 | # included above or to enable additional rules. A list of all available lints 16 | # and their documentation is published at 17 | # https://dart-lang.github.io/linter/lints/index.html. 18 | # 19 | # Instead of disabling a lint rule for the entire project in the 20 | # section below, it can also be suppressed for a single line of code 21 | # or a specific dart file by using the `// ignore: name_of_lint` and 22 | # `// ignore_for_file: name_of_lint` syntax on the line or in the file 23 | # producing the lint. 24 | rules: 25 | # avoid_print: false # Uncomment to disable the `avoid_print` rule 26 | # prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule 27 | 28 | # Additional information about this file can be found at 29 | # https://dart.dev/guides/language/analysis-options 30 | -------------------------------------------------------------------------------- /android/.gitignore: -------------------------------------------------------------------------------- 1 | gradle-wrapper.jar 2 | /.gradle 3 | /captures/ 4 | /gradlew 5 | /gradlew.bat 6 | /local.properties 7 | GeneratedPluginRegistrant.java 8 | 9 | # Remember to never publicly share your keystore. 10 | # See https://flutter.dev/docs/deployment/android#reference-the-keystore-from-the-app 11 | key.properties 12 | **/*.keystore 13 | **/*.jks 14 | **/google-services.json 15 | -------------------------------------------------------------------------------- /android/app/build.gradle: -------------------------------------------------------------------------------- 1 | def localProperties = new Properties() 2 | def localPropertiesFile = rootProject.file('local.properties') 3 | if (localPropertiesFile.exists()) { 4 | localPropertiesFile.withReader('UTF-8') { reader -> 5 | localProperties.load(reader) 6 | } 7 | } 8 | 9 | def flutterRoot = localProperties.getProperty('flutter.sdk') 10 | if (flutterRoot == null) { 11 | throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.") 12 | } 13 | 14 | def flutterVersionCode = localProperties.getProperty('flutter.versionCode') 15 | if (flutterVersionCode == null) { 16 | flutterVersionCode = '1' 17 | } 18 | 19 | def flutterVersionName = localProperties.getProperty('flutter.versionName') 20 | if (flutterVersionName == null) { 21 | flutterVersionName = '1.0' 22 | } 23 | 24 | apply plugin: 'com.android.application' 25 | apply plugin: 'kotlin-android' 26 | apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" 27 | apply plugin: 'com.google.gms.google-services' 28 | 29 | android { 30 | compileSdkVersion flutter.compileSdkVersion 31 | ndkVersion flutter.ndkVersion 32 | 33 | compileOptions { 34 | sourceCompatibility JavaVersion.VERSION_1_8 35 | targetCompatibility JavaVersion.VERSION_1_8 36 | } 37 | 38 | kotlinOptions { 39 | jvmTarget = '1.8' 40 | } 41 | 42 | sourceSets { 43 | main.java.srcDirs += 'src/main/kotlin' 44 | } 45 | 46 | defaultConfig { 47 | // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). 48 | applicationId "com.tarekalabd.flutter_ecommerce" 49 | // You can update the following values to match your application needs. 50 | // For more information, see: https://docs.flutter.dev/deployment/android#reviewing-the-build-configuration. 51 | minSdkVersion 21 52 | targetSdkVersion flutter.targetSdkVersion 53 | versionCode flutterVersionCode.toInteger() 54 | versionName flutterVersionName 55 | } 56 | 57 | buildTypes { 58 | release { 59 | // TODO: Add your own signing config for the release build. 60 | // Signing with the debug keys for now, so `flutter run --release` works. 61 | signingConfig signingConfigs.debug 62 | } 63 | } 64 | } 65 | 66 | flutter { 67 | source '../..' 68 | } 69 | 70 | dependencies { 71 | implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" 72 | // Import the Firebase BoM 73 | implementation platform('com.google.firebase:firebase-bom:30.1.0') 74 | 75 | 76 | // Add the dependency for the Firebase SDK for Google Analytics 77 | // When using the BoM, don't specify versions in Firebase dependencies 78 | implementation 'com.google.firebase:firebase-analytics' 79 | } 80 | -------------------------------------------------------------------------------- /android/app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | -dontwarn com.stripe.android.pushProvisioning.PushProvisioningActivity$g 2 | -dontwarn com.stripe.android.pushProvisioning.PushProvisioningActivityStarter$Args 3 | -dontwarn com.stripe.android.pushProvisioning.PushProvisioningActivityStarter$Error 4 | -dontwarn com.stripe.android.pushProvisioning.PushProvisioningActivityStarter 5 | -dontwarn com.stripe.android.pushProvisioning.PushProvisioningEphemeralKeyProvider -------------------------------------------------------------------------------- /android/app/src/debug/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /android/app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 7 | 15 | 19 | 23 | 24 | 25 | 26 | 27 | 28 | 30 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /android/app/src/main/java/io/flutter/app/FlutterMultiDexApplication.java: -------------------------------------------------------------------------------- 1 | // Generated file. 2 | // 3 | // If you wish to remove Flutter's multidex support, delete this entire file. 4 | // 5 | // Modifications to this file should be done in a copy under a different name 6 | // as this file may be regenerated. 7 | 8 | package io.flutter.app; 9 | 10 | import android.app.Application; 11 | import android.content.Context; 12 | import androidx.annotation.CallSuper; 13 | import androidx.multidex.MultiDex; 14 | 15 | /** 16 | * Extension of {@link android.app.Application}, adding multidex support. 17 | */ 18 | public class FlutterMultiDexApplication extends Application { 19 | @Override 20 | @CallSuper 21 | protected void attachBaseContext(Context base) { 22 | super.attachBaseContext(base); 23 | MultiDex.install(this); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /android/app/src/main/kotlin/com/tarekalabd/flutter_ecommerce/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package com.tarekalabd.flutter_ecommerce 2 | 3 | import io.flutter.embedding.android.FlutterFragmentActivity 4 | 5 | class MainActivity: FlutterFragmentActivity() { 6 | } 7 | -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-v21/launch_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 12 | 13 | -------------------------------------------------------------------------------- /android/app/src/main/res/drawable/launch_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 12 | 13 | -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TarekAlabd/flutter-ecommerce-live-coding/87ba16b871c018237bc9800e20a79b28c40f0823/android/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TarekAlabd/flutter-ecommerce-live-coding/87ba16b871c018237bc9800e20a79b28c40f0823/android/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TarekAlabd/flutter-ecommerce-live-coding/87ba16b871c018237bc9800e20a79b28c40f0823/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TarekAlabd/flutter-ecommerce-live-coding/87ba16b871c018237bc9800e20a79b28c40f0823/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TarekAlabd/flutter-ecommerce-live-coding/87ba16b871c018237bc9800e20a79b28c40f0823/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/values-night/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 15 | 18 | 19 | -------------------------------------------------------------------------------- /android/app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 15 | 18 | 19 | -------------------------------------------------------------------------------- /android/app/src/profile/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /android/build.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | ext.kotlin_version = '1.6.10' 3 | repositories { 4 | google() 5 | mavenCentral() 6 | } 7 | 8 | dependencies { 9 | classpath 'com.android.tools.build:gradle:7.1.2' 10 | classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" 11 | classpath 'com.google.gms:google-services:4.3.10' 12 | } 13 | } 14 | 15 | allprojects { 16 | repositories { 17 | google() 18 | mavenCentral() 19 | } 20 | } 21 | 22 | rootProject.buildDir = '../build' 23 | subprojects { 24 | project.buildDir = "${rootProject.buildDir}/${project.name}" 25 | } 26 | subprojects { 27 | project.evaluationDependsOn(':app') 28 | } 29 | 30 | task clean(type: Delete) { 31 | delete rootProject.buildDir 32 | } 33 | -------------------------------------------------------------------------------- /android/gradle.properties: -------------------------------------------------------------------------------- 1 | org.gradle.jvmargs=-Xmx1536M 2 | android.useAndroidX=true 3 | android.enableJetifier=true 4 | -------------------------------------------------------------------------------- /android/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Fri Jun 23 08:50:38 CEST 2017 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | distributionUrl=https\://services.gradle.org/distributions/gradle-7.4-all.zip 7 | -------------------------------------------------------------------------------- /android/settings.gradle: -------------------------------------------------------------------------------- 1 | include ':app' 2 | 3 | def localPropertiesFile = new File(rootProject.projectDir, "local.properties") 4 | def properties = new Properties() 5 | 6 | assert localPropertiesFile.exists() 7 | localPropertiesFile.withReader("UTF-8") { reader -> properties.load(reader) } 8 | 9 | def flutterSdkPath = properties.getProperty("flutter.sdk") 10 | assert flutterSdkPath != null, "flutter.sdk not set in local.properties" 11 | apply from: "$flutterSdkPath/packages/flutter_tools/gradle/app_plugin_loader.gradle" 12 | 13 | // plugins{ 14 | // id 'com.android.application' version '7.1.2' 15 | //} -------------------------------------------------------------------------------- /assets/facebook-svgrepo-com.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | 8 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /assets/google-svgrepo-com.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /ios/.gitignore: -------------------------------------------------------------------------------- 1 | **/dgph 2 | *.mode1v3 3 | *.mode2v3 4 | *.moved-aside 5 | *.pbxuser 6 | *.perspectivev3 7 | **/*sync/ 8 | .sconsign.dblite 9 | .tags* 10 | **/.vagrant/ 11 | **/DerivedData/ 12 | Icon? 13 | **/Pods/ 14 | **/.symlinks/ 15 | profile 16 | xcuserdata 17 | **/.generated/ 18 | Flutter/App.framework 19 | Flutter/Flutter.framework 20 | Flutter/Flutter.podspec 21 | Flutter/Generated.xcconfig 22 | Flutter/ephemeral/ 23 | Flutter/app.flx 24 | Flutter/app.zip 25 | Flutter/flutter_assets/ 26 | Flutter/flutter_export_environment.sh 27 | ServiceDefinitions.json 28 | Runner/GeneratedPluginRegistrant.* 29 | Runner/GoogleService-Info.plist 30 | 31 | # Exceptions to above rules. 32 | !default.mode1v3 33 | !default.mode2v3 34 | !default.pbxuser 35 | !default.perspectivev3 36 | -------------------------------------------------------------------------------- /ios/Flutter/AppFrameworkInfo.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | App 9 | CFBundleIdentifier 10 | io.flutter.flutter.app 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | App 15 | CFBundlePackageType 16 | FMWK 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1.0 23 | MinimumOSVersion 24 | 12.0 25 | 26 | 27 | -------------------------------------------------------------------------------- /ios/Flutter/Debug.xcconfig: -------------------------------------------------------------------------------- 1 | #include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" 2 | #include "Generated.xcconfig" 3 | -------------------------------------------------------------------------------- /ios/Flutter/Release.xcconfig: -------------------------------------------------------------------------------- 1 | #include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" 2 | #include "Generated.xcconfig" 3 | -------------------------------------------------------------------------------- /ios/Podfile: -------------------------------------------------------------------------------- 1 | # Uncomment this line to define a global platform for your project 2 | platform :ios, '13.0' 3 | 4 | # CocoaPods analytics sends network stats synchronously affecting flutter build latency. 5 | ENV['COCOAPODS_DISABLE_STATS'] = 'true' 6 | 7 | project 'Runner', { 8 | 'Debug' => :debug, 9 | 'Profile' => :release, 10 | 'Release' => :release, 11 | } 12 | 13 | def flutter_root 14 | generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__) 15 | unless File.exist?(generated_xcode_build_settings_path) 16 | raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first" 17 | end 18 | 19 | File.foreach(generated_xcode_build_settings_path) do |line| 20 | matches = line.match(/FLUTTER_ROOT\=(.*)/) 21 | return matches[1].strip if matches 22 | end 23 | raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get" 24 | end 25 | 26 | require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) 27 | 28 | flutter_ios_podfile_setup 29 | 30 | target 'Runner' do 31 | use_frameworks! 32 | use_modular_headers! 33 | 34 | flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__)) 35 | end 36 | 37 | post_install do |installer| 38 | installer.pods_project.targets.each do |target| 39 | flutter_additional_ios_build_settings(target) 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PreviewsEnabled 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 37 | 38 | 39 | 40 | 41 | 42 | 52 | 54 | 60 | 61 | 62 | 63 | 69 | 71 | 77 | 78 | 79 | 80 | 82 | 83 | 86 | 87 | 88 | -------------------------------------------------------------------------------- /ios/Runner.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PreviewsEnabled 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /ios/Runner/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | import Flutter 3 | 4 | @UIApplicationMain 5 | @objc class AppDelegate: FlutterAppDelegate { 6 | override func application( 7 | _ application: UIApplication, 8 | didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? 9 | ) -> Bool { 10 | GeneratedPluginRegistrant.register(with: self) 11 | return super.application(application, didFinishLaunchingWithOptions: launchOptions) 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "size" : "20x20", 5 | "idiom" : "iphone", 6 | "filename" : "Icon-App-20x20@2x.png", 7 | "scale" : "2x" 8 | }, 9 | { 10 | "size" : "20x20", 11 | "idiom" : "iphone", 12 | "filename" : "Icon-App-20x20@3x.png", 13 | "scale" : "3x" 14 | }, 15 | { 16 | "size" : "29x29", 17 | "idiom" : "iphone", 18 | "filename" : "Icon-App-29x29@1x.png", 19 | "scale" : "1x" 20 | }, 21 | { 22 | "size" : "29x29", 23 | "idiom" : "iphone", 24 | "filename" : "Icon-App-29x29@2x.png", 25 | "scale" : "2x" 26 | }, 27 | { 28 | "size" : "29x29", 29 | "idiom" : "iphone", 30 | "filename" : "Icon-App-29x29@3x.png", 31 | "scale" : "3x" 32 | }, 33 | { 34 | "size" : "40x40", 35 | "idiom" : "iphone", 36 | "filename" : "Icon-App-40x40@2x.png", 37 | "scale" : "2x" 38 | }, 39 | { 40 | "size" : "40x40", 41 | "idiom" : "iphone", 42 | "filename" : "Icon-App-40x40@3x.png", 43 | "scale" : "3x" 44 | }, 45 | { 46 | "size" : "60x60", 47 | "idiom" : "iphone", 48 | "filename" : "Icon-App-60x60@2x.png", 49 | "scale" : "2x" 50 | }, 51 | { 52 | "size" : "60x60", 53 | "idiom" : "iphone", 54 | "filename" : "Icon-App-60x60@3x.png", 55 | "scale" : "3x" 56 | }, 57 | { 58 | "size" : "20x20", 59 | "idiom" : "ipad", 60 | "filename" : "Icon-App-20x20@1x.png", 61 | "scale" : "1x" 62 | }, 63 | { 64 | "size" : "20x20", 65 | "idiom" : "ipad", 66 | "filename" : "Icon-App-20x20@2x.png", 67 | "scale" : "2x" 68 | }, 69 | { 70 | "size" : "29x29", 71 | "idiom" : "ipad", 72 | "filename" : "Icon-App-29x29@1x.png", 73 | "scale" : "1x" 74 | }, 75 | { 76 | "size" : "29x29", 77 | "idiom" : "ipad", 78 | "filename" : "Icon-App-29x29@2x.png", 79 | "scale" : "2x" 80 | }, 81 | { 82 | "size" : "40x40", 83 | "idiom" : "ipad", 84 | "filename" : "Icon-App-40x40@1x.png", 85 | "scale" : "1x" 86 | }, 87 | { 88 | "size" : "40x40", 89 | "idiom" : "ipad", 90 | "filename" : "Icon-App-40x40@2x.png", 91 | "scale" : "2x" 92 | }, 93 | { 94 | "size" : "76x76", 95 | "idiom" : "ipad", 96 | "filename" : "Icon-App-76x76@1x.png", 97 | "scale" : "1x" 98 | }, 99 | { 100 | "size" : "76x76", 101 | "idiom" : "ipad", 102 | "filename" : "Icon-App-76x76@2x.png", 103 | "scale" : "2x" 104 | }, 105 | { 106 | "size" : "83.5x83.5", 107 | "idiom" : "ipad", 108 | "filename" : "Icon-App-83.5x83.5@2x.png", 109 | "scale" : "2x" 110 | }, 111 | { 112 | "size" : "1024x1024", 113 | "idiom" : "ios-marketing", 114 | "filename" : "Icon-App-1024x1024@1x.png", 115 | "scale" : "1x" 116 | } 117 | ], 118 | "info" : { 119 | "version" : 1, 120 | "author" : "xcode" 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TarekAlabd/flutter-ecommerce-live-coding/87ba16b871c018237bc9800e20a79b28c40f0823/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TarekAlabd/flutter-ecommerce-live-coding/87ba16b871c018237bc9800e20a79b28c40f0823/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TarekAlabd/flutter-ecommerce-live-coding/87ba16b871c018237bc9800e20a79b28c40f0823/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TarekAlabd/flutter-ecommerce-live-coding/87ba16b871c018237bc9800e20a79b28c40f0823/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TarekAlabd/flutter-ecommerce-live-coding/87ba16b871c018237bc9800e20a79b28c40f0823/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TarekAlabd/flutter-ecommerce-live-coding/87ba16b871c018237bc9800e20a79b28c40f0823/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TarekAlabd/flutter-ecommerce-live-coding/87ba16b871c018237bc9800e20a79b28c40f0823/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TarekAlabd/flutter-ecommerce-live-coding/87ba16b871c018237bc9800e20a79b28c40f0823/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TarekAlabd/flutter-ecommerce-live-coding/87ba16b871c018237bc9800e20a79b28c40f0823/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TarekAlabd/flutter-ecommerce-live-coding/87ba16b871c018237bc9800e20a79b28c40f0823/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TarekAlabd/flutter-ecommerce-live-coding/87ba16b871c018237bc9800e20a79b28c40f0823/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TarekAlabd/flutter-ecommerce-live-coding/87ba16b871c018237bc9800e20a79b28c40f0823/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TarekAlabd/flutter-ecommerce-live-coding/87ba16b871c018237bc9800e20a79b28c40f0823/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TarekAlabd/flutter-ecommerce-live-coding/87ba16b871c018237bc9800e20a79b28c40f0823/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TarekAlabd/flutter-ecommerce-live-coding/87ba16b871c018237bc9800e20a79b28c40f0823/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "LaunchImage.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "filename" : "LaunchImage@2x.png", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "filename" : "LaunchImage@3x.png", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "version" : 1, 21 | "author" : "xcode" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TarekAlabd/flutter-ecommerce-live-coding/87ba16b871c018237bc9800e20a79b28c40f0823/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TarekAlabd/flutter-ecommerce-live-coding/87ba16b871c018237bc9800e20a79b28c40f0823/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TarekAlabd/flutter-ecommerce-live-coding/87ba16b871c018237bc9800e20a79b28c40f0823/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md: -------------------------------------------------------------------------------- 1 | # Launch Screen Assets 2 | 3 | You can customize the launch screen with your own desired assets by replacing the image files in this directory. 4 | 5 | You can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images. -------------------------------------------------------------------------------- /ios/Runner/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /ios/Runner/Base.lproj/Main.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /ios/Runner/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CADisableMinimumFrameDurationOnPhone 6 | 7 | CFBundleDevelopmentRegion 8 | $(DEVELOPMENT_LANGUAGE) 9 | CFBundleDisplayName 10 | Flutter Ecommerce 11 | CFBundleExecutable 12 | $(EXECUTABLE_NAME) 13 | CFBundleIdentifier 14 | $(PRODUCT_BUNDLE_IDENTIFIER) 15 | CFBundleInfoDictionaryVersion 16 | 6.0 17 | CFBundleName 18 | flutter_ecommerce 19 | CFBundlePackageType 20 | APPL 21 | CFBundleShortVersionString 22 | $(FLUTTER_BUILD_NAME) 23 | CFBundleSignature 24 | ???? 25 | CFBundleVersion 26 | $(FLUTTER_BUILD_NUMBER) 27 | LSRequiresIPhoneOS 28 | 29 | UILaunchStoryboardName 30 | LaunchScreen 31 | UIMainStoryboardFile 32 | Main 33 | UISupportedInterfaceOrientations 34 | 35 | UIInterfaceOrientationPortrait 36 | 37 | UISupportedInterfaceOrientations~ipad 38 | 39 | UIInterfaceOrientationPortrait 40 | UIInterfaceOrientationPortraitUpsideDown 41 | UIInterfaceOrientationLandscapeLeft 42 | UIInterfaceOrientationLandscapeRight 43 | 44 | UIViewControllerBasedStatusBarAppearance 45 | 46 | UIApplicationSupportsIndirectInputEvents 47 | 48 | 49 | 50 | -------------------------------------------------------------------------------- /ios/Runner/Runner-Bridging-Header.h: -------------------------------------------------------------------------------- 1 | #import "GeneratedPluginRegistrant.h" 2 | -------------------------------------------------------------------------------- /lib/controllers/auth/auth_cubit.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_bloc/flutter_bloc.dart'; 2 | import 'package:flutter_ecommerce/services/auth_services.dart'; 3 | import 'package:flutter_ecommerce/utilities/enums.dart'; 4 | import 'package:meta/meta.dart'; 5 | 6 | part 'auth_state.dart'; 7 | 8 | class AuthCubit extends Cubit { 9 | AuthCubit() : super(AuthInitial()); 10 | 11 | final authServices = AuthServicesImpl(); 12 | var authFormType = AuthFormType.login; 13 | 14 | Future login(String email, String password) async { 15 | emit(AuthLoading()); 16 | try { 17 | final user = 18 | await authServices.loginWithEmailAndPassword(email, password); 19 | 20 | if (user != null) { 21 | emit(AuthSuccess()); 22 | } else { 23 | emit(AuthFailed('Incorrect credentials!')); 24 | } 25 | } catch (e) { 26 | emit(AuthFailed(e.toString())); 27 | } 28 | } 29 | 30 | Future signUp(String email, String password) async { 31 | emit(AuthLoading()); 32 | try { 33 | final user = 34 | await authServices.signUpWithEmailAndPassword(email, password); 35 | 36 | if (user != null) { 37 | emit(AuthSuccess()); 38 | } else { 39 | emit(AuthFailed('Incorrect credentials!')); 40 | } 41 | } catch (e) { 42 | emit(AuthFailed(e.toString())); 43 | } 44 | } 45 | 46 | void authStatus() { 47 | final user = authServices.currentUser; 48 | if (user != null) { 49 | emit(AuthSuccess()); 50 | } else { 51 | emit(AuthInitial()); 52 | } 53 | } 54 | 55 | Future logout() async { 56 | emit(AuthLoading()); 57 | try { 58 | await authServices.logout(); 59 | emit(AuthInitial()); 60 | } catch (e) { 61 | emit(AuthFailed(e.toString())); 62 | } 63 | } 64 | 65 | void toggleFormType() { 66 | final formType = authFormType == AuthFormType.login 67 | ? AuthFormType.register 68 | : AuthFormType.login; 69 | emit(ToggleFormType(formType)); 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /lib/controllers/auth/auth_state.dart: -------------------------------------------------------------------------------- 1 | part of 'auth_cubit.dart'; 2 | 3 | @immutable 4 | sealed class AuthState {} 5 | 6 | final class AuthInitial extends AuthState {} 7 | 8 | final class AuthSuccess extends AuthState {} 9 | 10 | final class AuthFailed extends AuthState { 11 | final String error; 12 | 13 | AuthFailed(this.error); 14 | } 15 | 16 | final class AuthLoading extends AuthState {} 17 | 18 | final class ToggleFormType extends AuthState { 19 | final AuthFormType authFormType; 20 | 21 | ToggleFormType(this.authFormType); 22 | } 23 | -------------------------------------------------------------------------------- /lib/controllers/auth_controller.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_ecommerce/controllers/database_controller.dart'; 3 | import 'package:flutter_ecommerce/models/user_data.dart'; 4 | import 'package:flutter_ecommerce/services/auth.dart'; 5 | import 'package:flutter_ecommerce/utilities/constants.dart'; 6 | import 'package:flutter_ecommerce/utilities/enums.dart'; 7 | 8 | class AuthController with ChangeNotifier { 9 | final AuthBase auth; 10 | String email; 11 | String password; 12 | AuthFormType authFormType; 13 | // TODO: It's not a best practice thing but it's temporary 14 | final database = FirestoreDatabase('123'); 15 | 16 | AuthController({ 17 | required this.auth, 18 | this.email = '', 19 | this.password = '', 20 | this.authFormType = AuthFormType.login, 21 | }); 22 | 23 | Future submit() async { 24 | try { 25 | if (authFormType == AuthFormType.login) { 26 | await auth.loginWithEmailAndPassword(email, password); 27 | } else { 28 | final user = await auth.signUpWithEmailAndPassword(email, password); 29 | await database.setUserData(UserData( 30 | uid: user?.uid ?? documentIdFromLocalData(), 31 | email: email, 32 | )); 33 | } 34 | } catch (e) { 35 | rethrow; 36 | } 37 | } 38 | 39 | void toggleFormType() { 40 | final formType = authFormType == AuthFormType.login 41 | ? AuthFormType.register 42 | : AuthFormType.login; 43 | copyWith( 44 | email: '', 45 | password: '', 46 | authFormType: formType, 47 | ); 48 | } 49 | 50 | void updateEmail(String email) => copyWith(email: email); 51 | 52 | void updatePassword(String password) => copyWith(password: password); 53 | 54 | void copyWith({ 55 | String? email, 56 | String? password, 57 | AuthFormType? authFormType, 58 | }) { 59 | this.email = email ?? this.email; 60 | this.password = password ?? this.password; 61 | this.authFormType = authFormType ?? this.authFormType; 62 | notifyListeners(); 63 | } 64 | 65 | Future logout() async { 66 | try { 67 | await auth.logout(); 68 | } catch (e) { 69 | rethrow; 70 | } 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /lib/controllers/cart/cart_cubit.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_bloc/flutter_bloc.dart'; 2 | import 'package:flutter_ecommerce/models/add_to_cart_model.dart'; 3 | import 'package:flutter_ecommerce/services/auth_services.dart'; 4 | import 'package:flutter_ecommerce/services/cart_services.dart'; 5 | import 'package:meta/meta.dart'; 6 | 7 | part 'cart_state.dart'; 8 | 9 | class CartCubit extends Cubit { 10 | CartCubit() : super(CartInitial()); 11 | 12 | final authServices = AuthServicesImpl(); 13 | final cartServices = CartServicesImpl(); 14 | 15 | Future getCartItems() async { 16 | emit(CartLoading()); 17 | try { 18 | final currentUser = authServices.currentUser; 19 | final cartProducts = await cartServices.getCartProducts(currentUser!.uid); 20 | final totalAmount = cartProducts.fold( 21 | 0, (previousValue, element) => previousValue + element.price); 22 | emit(CartLoaded(cartProducts, totalAmount)); 23 | } catch (e) { 24 | emit(CartError(e.toString())); 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /lib/controllers/cart/cart_state.dart: -------------------------------------------------------------------------------- 1 | part of 'cart_cubit.dart'; 2 | 3 | @immutable 4 | sealed class CartState {} 5 | 6 | final class CartInitial extends CartState {} 7 | 8 | final class CartLoading extends CartState {} 9 | 10 | final class CartLoaded extends CartState { 11 | final List cartProducts; 12 | final double totalAmount; 13 | 14 | CartLoaded(this.cartProducts, this.totalAmount); 15 | } 16 | 17 | final class CartError extends CartState { 18 | final String message; 19 | 20 | CartError(this.message); 21 | } 22 | -------------------------------------------------------------------------------- /lib/controllers/checkout/checkout_cubit.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_bloc/flutter_bloc.dart'; 3 | import 'package:flutter_ecommerce/models/delivery_method.dart'; 4 | import 'package:flutter_ecommerce/models/payment_method.dart'; 5 | import 'package:flutter_ecommerce/models/shipping_address.dart'; 6 | import 'package:flutter_ecommerce/services/auth_services.dart'; 7 | import 'package:flutter_ecommerce/services/checkout_services.dart'; 8 | import 'package:flutter_ecommerce/services/stripe_services.dart'; 9 | import 'package:meta/meta.dart'; 10 | 11 | part 'checkout_state.dart'; 12 | 13 | class CheckoutCubit extends Cubit { 14 | CheckoutCubit() : super(CheckoutInitial()); 15 | 16 | final checkoutServices = CheckoutServicesImpl(); 17 | final authServices = AuthServicesImpl(); 18 | final stripeServices = StripeServices.instance; 19 | 20 | Future makePayment(double amount) async { 21 | emit(MakingPayment()); 22 | 23 | try { 24 | await stripeServices.makePayment(amount, 'usd'); 25 | emit(PaymentMade()); 26 | } catch (e) { 27 | debugPrint(e.toString()); 28 | emit(PaymentMakingFailed(e.toString())); 29 | } 30 | } 31 | 32 | Future addCard(PaymentMethod paymentMethod) async { 33 | emit(AddingCards()); 34 | 35 | try { 36 | await checkoutServices.setPaymentMethod(paymentMethod); 37 | emit(CardsAdded()); 38 | } catch (e) { 39 | emit(CardsAddingFailed(e.toString())); 40 | } 41 | } 42 | 43 | Future deleteCard(PaymentMethod paymentMethod) async { 44 | emit(DeletingCards(paymentMethod.id)); 45 | 46 | try { 47 | await checkoutServices.deletePaymentMethod(paymentMethod); 48 | emit(CardsDeleted()); 49 | await fetchCards(); 50 | } catch (e) { 51 | emit(CardsDeletingFailed(e.toString())); 52 | } 53 | } 54 | 55 | Future fetchCards() async { 56 | emit(FetchingCards()); 57 | 58 | try { 59 | final paymentMethods = await checkoutServices.paymentMethods(); 60 | emit(CardsFetched(paymentMethods)); 61 | } catch (e) { 62 | emit(CardsFetchingFailed(e.toString())); 63 | } 64 | } 65 | 66 | Future makePreferred(PaymentMethod paymentMethod) async { 67 | emit(FetchingCards()); 68 | 69 | try { 70 | final preferredPaymentMethods = 71 | await checkoutServices.paymentMethods(true); 72 | for (var method in preferredPaymentMethods) { 73 | final newPaymentMethod = method.copyWith(isPreferred: false); 74 | await checkoutServices.setPaymentMethod(newPaymentMethod); 75 | } 76 | final newPreferredMethod = paymentMethod.copyWith(isPreferred: true); 77 | await checkoutServices.setPaymentMethod(newPreferredMethod); 78 | emit(PreferredMade()); 79 | } catch (e) { 80 | emit(PreferredMakingFailed(e.toString())); 81 | } 82 | } 83 | 84 | Future getCheckoutData() async { 85 | emit(CheckoutLoading()); 86 | try { 87 | final currentUser = authServices.currentUser; 88 | final shippingAddresses = 89 | await checkoutServices.shippingAddresses(currentUser!.uid); 90 | final deliveryMethods = await checkoutServices.deliveryMethods(); 91 | 92 | emit(CheckoutLoaded( 93 | deliveryMethods: deliveryMethods, 94 | shippingAddress: 95 | shippingAddresses.isEmpty ? null : shippingAddresses[0], 96 | )); 97 | } catch (e) { 98 | emit(CheckoutLoadingFailed(e.toString())); 99 | } 100 | } 101 | 102 | Future getShippingAddresses() async { 103 | emit(FetchingAddresses()); 104 | try { 105 | final currentUser = authServices.currentUser; 106 | final shippingAddresses = 107 | await checkoutServices.shippingAddresses(currentUser!.uid); 108 | 109 | emit(AddressesFetched(shippingAddresses)); 110 | } catch (e) { 111 | emit(AddressesFetchingFailed(e.toString())); 112 | } 113 | } 114 | 115 | Future saveAddress(ShippingAddress address) async { 116 | emit(AddingAddress()); 117 | try { 118 | final currentUser = authServices.currentUser; 119 | await checkoutServices.saveAddress(currentUser!.uid, address); 120 | emit(AddressAdded()); 121 | } catch (e) { 122 | emit(AddressAddingFailed(e.toString())); 123 | } 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /lib/controllers/checkout/checkout_state.dart: -------------------------------------------------------------------------------- 1 | part of 'checkout_cubit.dart'; 2 | 3 | @immutable 4 | sealed class CheckoutState {} 5 | 6 | final class CheckoutInitial extends CheckoutState {} 7 | 8 | final class CheckoutLoading extends CheckoutState {} 9 | 10 | final class CheckoutLoaded extends CheckoutState { 11 | final List deliveryMethods; 12 | final ShippingAddress? shippingAddress; 13 | 14 | CheckoutLoaded({ 15 | required this.deliveryMethods, 16 | this.shippingAddress, 17 | }); 18 | } 19 | 20 | final class CheckoutLoadingFailed extends CheckoutState { 21 | final String error; 22 | 23 | CheckoutLoadingFailed(this.error); 24 | } 25 | 26 | final class FetchingAddresses extends CheckoutState {} 27 | 28 | final class AddressesFetched extends CheckoutState { 29 | final List shippingAddresses; 30 | 31 | AddressesFetched(this.shippingAddresses); 32 | } 33 | 34 | final class AddressesFetchingFailed extends CheckoutState { 35 | final String error; 36 | 37 | AddressesFetchingFailed(this.error); 38 | } 39 | 40 | final class AddingAddress extends CheckoutState {} 41 | 42 | final class AddressAdded extends CheckoutState {} 43 | 44 | final class AddressAddingFailed extends CheckoutState { 45 | final String error; 46 | 47 | AddressAddingFailed(this.error); 48 | } 49 | 50 | final class AddingCards extends CheckoutState {} 51 | 52 | final class CardsAdded extends CheckoutState {} 53 | 54 | final class CardsAddingFailed extends CheckoutState { 55 | final String error; 56 | 57 | CardsAddingFailed(this.error); 58 | } 59 | 60 | final class DeletingCards extends CheckoutState { 61 | final String paymentId; 62 | 63 | DeletingCards(this.paymentId); 64 | } 65 | 66 | final class CardsDeleted extends CheckoutState {} 67 | 68 | final class CardsDeletingFailed extends CheckoutState { 69 | final String error; 70 | 71 | CardsDeletingFailed(this.error); 72 | } 73 | 74 | final class FetchingCards extends CheckoutState {} 75 | 76 | final class CardsFetched extends CheckoutState { 77 | final List paymentMethods; 78 | 79 | CardsFetched(this.paymentMethods); 80 | } 81 | 82 | final class CardsFetchingFailed extends CheckoutState { 83 | final String error; 84 | 85 | CardsFetchingFailed(this.error); 86 | } 87 | 88 | final class MakingPreferred extends CheckoutState {} 89 | 90 | final class PreferredMade extends CheckoutState {} 91 | 92 | final class PreferredMakingFailed extends CheckoutState { 93 | final String error; 94 | 95 | PreferredMakingFailed(this.error); 96 | } 97 | 98 | final class MakingPayment extends CheckoutState {} 99 | 100 | final class PaymentMade extends CheckoutState {} 101 | 102 | final class PaymentMakingFailed extends CheckoutState { 103 | final String error; 104 | 105 | PaymentMakingFailed(this.error); 106 | } 107 | -------------------------------------------------------------------------------- /lib/controllers/database_controller.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_ecommerce/models/add_to_cart_model.dart'; 2 | import 'package:flutter_ecommerce/models/delivery_method.dart'; 3 | import 'package:flutter_ecommerce/models/product.dart'; 4 | import 'package:flutter_ecommerce/models/shipping_address.dart'; 5 | import 'package:flutter_ecommerce/models/user_data.dart'; 6 | import 'package:flutter_ecommerce/services/firestore_services.dart'; 7 | import 'package:flutter_ecommerce/utilities/api_path.dart'; 8 | 9 | abstract class Database { 10 | Stream> salesProductsStream(); 11 | Stream> newProductsStream(); 12 | Stream> myProductsCart(); 13 | Stream> deliveryMethodsStream(); 14 | Stream> getShippingAddresses(); 15 | 16 | Future setUserData(UserData userData); 17 | Future addToCart(AddToCartModel product); 18 | Future saveAddress(ShippingAddress address); 19 | } 20 | 21 | class FirestoreDatabase implements Database { 22 | final String uid; 23 | final _service = FirestoreServices.instance; 24 | 25 | FirestoreDatabase(this.uid); 26 | 27 | @override 28 | Stream> salesProductsStream() => _service.collectionsStream( 29 | path: ApiPath.products(), 30 | builder: (data, documentId) => Product.fromMap(data!, documentId), 31 | queryBuilder: (query) => query.where('discountValue', isNotEqualTo: 0), 32 | ); 33 | 34 | @override 35 | Stream> newProductsStream() => _service.collectionsStream( 36 | path: ApiPath.products(), 37 | builder: (data, documentId) => Product.fromMap(data!, documentId), 38 | ); 39 | 40 | @override 41 | Future setUserData(UserData userData) async => await _service.setData( 42 | path: ApiPath.user(userData.uid), 43 | data: userData.toMap(), 44 | ); 45 | 46 | @override 47 | Future addToCart(AddToCartModel product) async => _service.setData( 48 | path: ApiPath.addToCart(uid, product.id), 49 | data: product.toMap(), 50 | ); 51 | 52 | @override 53 | Stream> myProductsCart() => _service.collectionsStream( 54 | path: ApiPath.myProductsCart(uid), 55 | builder: (data, documentId) => 56 | AddToCartModel.fromMap(data!, documentId), 57 | ); 58 | 59 | @override 60 | Stream> deliveryMethodsStream() => 61 | _service.collectionsStream( 62 | path: ApiPath.deliveryMethods(), 63 | builder: (data, documentId) => 64 | DeliveryMethod.fromMap(data!, documentId)); 65 | 66 | @override 67 | Stream> getShippingAddresses() => 68 | _service.collectionsStream( 69 | path: ApiPath.userShippingAddress(uid), 70 | builder: (data, documentId) => 71 | ShippingAddress.fromMap(data!, documentId), 72 | ); 73 | 74 | @override 75 | Future saveAddress(ShippingAddress address) => _service.setData( 76 | path: ApiPath.newAddress( 77 | uid, 78 | address.id, 79 | ), 80 | data: address.toMap(), 81 | ); 82 | } 83 | -------------------------------------------------------------------------------- /lib/controllers/home/home_cubit.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_bloc/flutter_bloc.dart'; 2 | import 'package:flutter_ecommerce/models/product.dart'; 3 | import 'package:flutter_ecommerce/services/home_services.dart'; 4 | import 'package:meta/meta.dart'; 5 | 6 | part 'home_state.dart'; 7 | 8 | class HomeCubit extends Cubit { 9 | HomeCubit() : super(HomeInitial()); 10 | 11 | final homeServices = HomeServicesImpl(); 12 | 13 | Future getHomeContent() async { 14 | emit(HomeLoading()); 15 | try { 16 | final newProducts = await homeServices.getNewProducts(); 17 | final salesProducts = await homeServices.getSalesProducts(); 18 | 19 | emit(HomeSuccess( 20 | salesProducts: salesProducts, 21 | newProducts: newProducts, 22 | )); 23 | } catch (e) { 24 | emit(HomeFailed(e.toString())); 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /lib/controllers/home/home_state.dart: -------------------------------------------------------------------------------- 1 | part of 'home_cubit.dart'; 2 | 3 | @immutable 4 | sealed class HomeState {} 5 | 6 | final class HomeInitial extends HomeState {} 7 | 8 | final class HomeLoading extends HomeState {} 9 | 10 | final class HomeSuccess extends HomeState { 11 | final List salesProducts; 12 | final List newProducts; 13 | 14 | HomeSuccess({required this.salesProducts, required this.newProducts,}); 15 | } 16 | 17 | final class HomeFailed extends HomeState { 18 | final String error; 19 | 20 | HomeFailed(this.error); 21 | } 22 | -------------------------------------------------------------------------------- /lib/controllers/product_details/product_details_cubit.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_bloc/flutter_bloc.dart'; 2 | import 'package:flutter_ecommerce/models/add_to_cart_model.dart'; 3 | import 'package:flutter_ecommerce/models/product.dart'; 4 | import 'package:flutter_ecommerce/services/auth_services.dart'; 5 | import 'package:flutter_ecommerce/services/cart_services.dart'; 6 | import 'package:flutter_ecommerce/services/product_details_services.dart'; 7 | import 'package:flutter_ecommerce/utilities/constants.dart'; 8 | import 'package:meta/meta.dart'; 9 | 10 | part 'product_details_state.dart'; 11 | 12 | class ProductDetailsCubit extends Cubit { 13 | ProductDetailsCubit() : super(ProductDetailsInitial()); 14 | 15 | final productDetailsServices = ProductDetailsServicesImpl(); 16 | final cartServices = CartServicesImpl(); 17 | final authServices = AuthServicesImpl(); 18 | 19 | String? size; 20 | 21 | Future getProductDetails(String productId) async { 22 | emit(ProductDetailsLoading()); 23 | try { 24 | final product = await productDetailsServices.getProductDetails(productId); 25 | emit(ProductDetailsLoaded(product)); 26 | } catch (e) { 27 | emit(ProductDetailsError(e.toString())); 28 | } 29 | } 30 | 31 | Future addToCart(Product product) async { 32 | emit(AddingToCart()); 33 | try { 34 | final currentUser = authServices.currentUser; 35 | if (size == null) { 36 | emit(AddToCartError('Please select a size')); 37 | } 38 | final addToCartProduct = AddToCartModel( 39 | id: documentIdFromLocalData(), 40 | title: product.title, 41 | price: product.price, 42 | productId: product.id, 43 | imgUrl: product.imgUrl, 44 | size: size!, 45 | ); 46 | await cartServices.addProductToCart(currentUser!.uid, addToCartProduct); 47 | emit(AddedToCart()); 48 | } catch (e) { 49 | emit(AddToCartError(e.toString())); 50 | } 51 | } 52 | 53 | void setSize(String newSize) { 54 | size = newSize; 55 | emit(SizeSelected(newSize)); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /lib/controllers/product_details/product_details_state.dart: -------------------------------------------------------------------------------- 1 | part of 'product_details_cubit.dart'; 2 | 3 | @immutable 4 | sealed class ProductDetailsState {} 5 | 6 | final class ProductDetailsInitial extends ProductDetailsState {} 7 | 8 | final class ProductDetailsLoading extends ProductDetailsState {} 9 | 10 | final class ProductDetailsLoaded extends ProductDetailsState { 11 | final Product product; 12 | 13 | ProductDetailsLoaded(this.product); 14 | } 15 | 16 | final class ProductDetailsError extends ProductDetailsState { 17 | final String error; 18 | 19 | ProductDetailsError(this.error); 20 | } 21 | 22 | final class AddingToCart extends ProductDetailsState {} 23 | 24 | final class AddedToCart extends ProductDetailsState {} 25 | 26 | final class AddToCartError extends ProductDetailsState { 27 | final String error; 28 | 29 | AddToCartError(this.error); 30 | } 31 | 32 | final class SizeSelected extends ProductDetailsState { 33 | final String size; 34 | 35 | SizeSelected(this.size); 36 | } 37 | -------------------------------------------------------------------------------- /lib/main.dart: -------------------------------------------------------------------------------- 1 | import 'package:firebase_core/firebase_core.dart'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:flutter_bloc/flutter_bloc.dart'; 4 | import 'package:flutter_ecommerce/controllers/auth/auth_cubit.dart'; 5 | import 'package:flutter_ecommerce/services/auth.dart'; 6 | import 'package:flutter_ecommerce/utilities/constants.dart'; 7 | import 'package:flutter_ecommerce/utilities/router.dart'; 8 | import 'package:flutter_ecommerce/utilities/routes.dart'; 9 | import 'package:flutter_stripe/flutter_stripe.dart'; 10 | import 'package:provider/provider.dart'; 11 | 12 | Future main() async { 13 | await initSetup(); 14 | runApp(const MyApp()); 15 | } 16 | 17 | Future initSetup() async { 18 | WidgetsFlutterBinding.ensureInitialized(); 19 | await Firebase.initializeApp(); 20 | Stripe.publishableKey = AppConstants.publishableKey; 21 | } 22 | 23 | class MyApp extends StatelessWidget { 24 | const MyApp({Key? key}) : super(key: key); 25 | 26 | // This widget is the root of your application. 27 | @override 28 | Widget build(BuildContext context) { 29 | return BlocProvider( 30 | create: (context) { 31 | final cubit = AuthCubit(); 32 | cubit.authStatus(); 33 | return cubit; 34 | }, 35 | child: Builder( 36 | builder: (context) { 37 | return BlocBuilder( 38 | bloc: BlocProvider.of(context), 39 | buildWhen: (previous, current) => current is AuthSuccess || current is AuthInitial, 40 | builder: (context, state) { 41 | return MaterialApp( 42 | debugShowCheckedModeBanner: false, 43 | title: 'Ecommerce App', 44 | // TODO: Refactor this theme away from the main file 45 | theme: ThemeData( 46 | scaffoldBackgroundColor: const Color(0xFFE5E5E5), 47 | primaryColor: Colors.red, 48 | appBarTheme: const AppBarTheme( 49 | backgroundColor: Colors.white, 50 | elevation: 2, 51 | iconTheme: IconThemeData( 52 | color: Colors.black, 53 | ), 54 | ), 55 | inputDecorationTheme: InputDecorationTheme( 56 | labelStyle: Theme.of(context).textTheme.labelMedium, 57 | focusedBorder: OutlineInputBorder( 58 | borderRadius: BorderRadius.circular(16.0), 59 | borderSide: const BorderSide( 60 | color: Colors.grey, 61 | ), 62 | ), 63 | disabledBorder: OutlineInputBorder( 64 | borderRadius: BorderRadius.circular(16.0), 65 | borderSide: const BorderSide( 66 | color: Colors.grey, 67 | ), 68 | ), 69 | enabledBorder: OutlineInputBorder( 70 | borderRadius: BorderRadius.circular(16.0), 71 | borderSide: const BorderSide( 72 | color: Colors.grey, 73 | ), 74 | ), 75 | errorBorder: OutlineInputBorder( 76 | borderRadius: BorderRadius.circular(16.0), 77 | borderSide: const BorderSide( 78 | color: Colors.red, 79 | ), 80 | ), 81 | focusedErrorBorder: OutlineInputBorder( 82 | borderRadius: BorderRadius.circular(16.0), 83 | borderSide: const BorderSide( 84 | color: Colors.red, 85 | ), 86 | ), 87 | )), 88 | onGenerateRoute: onGenerate, 89 | initialRoute: state is AuthSuccess ? AppRoutes.bottomNavBarRoute : AppRoutes.loginPageRoute, 90 | ); 91 | }, 92 | ); 93 | } 94 | ), 95 | ); 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /lib/models/add_to_cart_model.dart: -------------------------------------------------------------------------------- 1 | class AddToCartModel { 2 | final String id; 3 | final String productId; 4 | final String title; 5 | final int price; 6 | final int quantity; 7 | final String imgUrl; 8 | final int discountValue; 9 | final String color; 10 | final String size; 11 | 12 | AddToCartModel({ 13 | required this.id, 14 | required this.title, 15 | required this.price, 16 | required this.productId, 17 | this.quantity = 1, 18 | required this.imgUrl, 19 | this.discountValue = 0, 20 | this.color = 'Black', 21 | required this.size, 22 | }); 23 | 24 | Map toMap() { 25 | final result = {}; 26 | 27 | result.addAll({'id': id}); 28 | result.addAll({'productId': productId}); 29 | result.addAll({'title': title}); 30 | result.addAll({'price': price}); 31 | result.addAll({'quantity': quantity}); 32 | result.addAll({'imgUrl': imgUrl}); 33 | result.addAll({'discountValue': discountValue}); 34 | result.addAll({'color': color}); 35 | result.addAll({'size': size}); 36 | 37 | return result; 38 | } 39 | 40 | factory AddToCartModel.fromMap(Map map, String documentId) { 41 | return AddToCartModel( 42 | id: documentId, 43 | title: map['title'] ?? '', 44 | productId: map['productId'] ?? '', 45 | price: map['price']?.toInt() ?? 0, 46 | quantity: map['quantity']?.toInt() ?? 0, 47 | imgUrl: map['imgUrl'] ?? '', 48 | discountValue: map['discountValue']?.toInt() ?? 0, 49 | color: map['color'] ?? '', 50 | size: map['size'] ?? '', 51 | ); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /lib/models/delivery_method.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | 3 | class DeliveryMethod { 4 | final String id; 5 | final String name; 6 | final String days; 7 | final String imgUrl; 8 | final int price; 9 | 10 | DeliveryMethod({ 11 | required this.id, 12 | required this.name, 13 | required this.days, 14 | required this.imgUrl, 15 | required this.price, 16 | }); 17 | 18 | Map toMap() { 19 | final result = {}; 20 | 21 | result.addAll({'id': id}); 22 | result.addAll({'name': name}); 23 | result.addAll({'days': days}); 24 | result.addAll({'imgUrl': imgUrl}); 25 | result.addAll({'price': price}); 26 | 27 | return result; 28 | } 29 | 30 | factory DeliveryMethod.fromMap(Map map, String documentId) { 31 | return DeliveryMethod( 32 | id: documentId, 33 | name: map['name'] ?? '', 34 | days: map['days'] ?? '', 35 | imgUrl: map['imgUrl'] ?? '', 36 | price: map['price']?.toInt() ?? 0, 37 | ); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /lib/models/payment_method.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | 3 | class PaymentMethod { 4 | final String id; 5 | final String name; 6 | final String cardNumber; 7 | final String expiryDate; 8 | final String cvv; 9 | final bool isPreferred; 10 | 11 | const PaymentMethod({ 12 | required this.id, 13 | required this.name, 14 | required this.cardNumber, 15 | required this.expiryDate, 16 | required this.cvv, 17 | this.isPreferred = false, 18 | }); 19 | 20 | Map toMap() { 21 | final result = {}; 22 | 23 | result.addAll({'id': id}); 24 | result.addAll({'name': name}); 25 | result.addAll({'cardNumber': cardNumber}); 26 | result.addAll({'expiryDate': expiryDate}); 27 | result.addAll({'cvv': cvv}); 28 | result.addAll({'isPreferred': isPreferred}); 29 | 30 | return result; 31 | } 32 | 33 | factory PaymentMethod.fromMap(Map map) { 34 | return PaymentMethod( 35 | id: map['id'] ?? '', 36 | name: map['name'] ?? '', 37 | cardNumber: map['cardNumber'] ?? '', 38 | expiryDate: map['expiryDate'] ?? '', 39 | cvv: map['cvv'] ?? '', 40 | isPreferred: map['isPreferred'] ?? '', 41 | ); 42 | } 43 | 44 | String toJson() => json.encode(toMap()); 45 | 46 | factory PaymentMethod.fromJson(String source) => 47 | PaymentMethod.fromMap(json.decode(source)); 48 | 49 | PaymentMethod copyWith({ 50 | String? id, 51 | String? name, 52 | String? cardNumber, 53 | String? expiryDate, 54 | String? cvv, 55 | bool? isPreferred, 56 | }) { 57 | return PaymentMethod( 58 | id: id ?? this.id, 59 | name: name ?? this.name, 60 | cardNumber: cardNumber ?? this.cardNumber, 61 | expiryDate: expiryDate ?? this.expiryDate, 62 | cvv: cvv ?? this.cvv, 63 | isPreferred: isPreferred ?? this.isPreferred, 64 | ); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /lib/models/product.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_ecommerce/utilities/assets.dart'; 2 | 3 | class Product { 4 | final String id; 5 | final String title; 6 | final int price; 7 | final String imgUrl; 8 | final int? discountValue; 9 | final String category; 10 | final int? rate; 11 | 12 | Product({ 13 | required this.id, 14 | required this.title, 15 | required this.price, 16 | required this.imgUrl, 17 | this.discountValue, 18 | this.category = 'Other', 19 | this.rate, 20 | }); 21 | 22 | Map toMap() { 23 | return { 24 | 'id': id, 25 | 'title': title, 26 | 'price': price, 27 | 'imgUrl': imgUrl, 28 | 'discountValue': discountValue, 29 | 'category': category, 30 | 'rate': rate, 31 | }; 32 | } 33 | 34 | factory Product.fromMap(Map map, String documentId) { 35 | return Product( 36 | id: documentId, 37 | title: map['title'] as String, 38 | price: map['price'] as int, 39 | imgUrl: map['imgUrl'] as String, 40 | discountValue: map['discountValue'] as int, 41 | category: map['category'] as String, 42 | rate: map['rate'] as int, 43 | ); 44 | } 45 | } 46 | 47 | List dummyProducts = [ 48 | Product( 49 | id: '1', 50 | title: 'T-shirt', 51 | price: 300, 52 | imgUrl: AppAssets.tempProductAsset1, 53 | category: 'Clothes', 54 | discountValue: 20, 55 | ), 56 | Product( 57 | id: '1', 58 | title: 'T-shirt', 59 | price: 300, 60 | imgUrl: AppAssets.tempProductAsset1, 61 | category: 'Clothes', 62 | discountValue: 20, 63 | ), 64 | Product( 65 | id: '1', 66 | title: 'T-shirt', 67 | price: 300, 68 | imgUrl: AppAssets.tempProductAsset1, 69 | category: 'Clothes', 70 | discountValue: 20, 71 | ), 72 | Product( 73 | id: '1', 74 | title: 'T-shirt', 75 | price: 300, 76 | imgUrl: AppAssets.tempProductAsset1, 77 | category: 'Clothes', 78 | discountValue: 20, 79 | ), 80 | Product( 81 | id: '1', 82 | title: 'T-shirt', 83 | price: 300, 84 | imgUrl: AppAssets.tempProductAsset1, 85 | category: 'Clothes', 86 | ), 87 | Product( 88 | id: '1', 89 | title: 'T-shirt', 90 | price: 300, 91 | imgUrl: AppAssets.tempProductAsset1, 92 | category: 'Clothes', 93 | discountValue: 20, 94 | ), 95 | ]; 96 | -------------------------------------------------------------------------------- /lib/models/shipping_address.dart: -------------------------------------------------------------------------------- 1 | class ShippingAddress { 2 | final String id; 3 | final String fullName; 4 | final String country; 5 | final String address; 6 | final String city; 7 | final String state; 8 | final String zipCode; 9 | final bool isDefault; 10 | 11 | ShippingAddress({ 12 | required this.id, 13 | required this.fullName, 14 | required this.country, 15 | required this.address, 16 | required this.city, 17 | required this.state, 18 | required this.zipCode, 19 | this.isDefault = false, 20 | }); 21 | 22 | Map toMap() { 23 | final result = {}; 24 | 25 | result.addAll({'id': id}); 26 | result.addAll({'fullName': fullName}); 27 | result.addAll({'country': country}); 28 | result.addAll({'address': address}); 29 | result.addAll({'city': city}); 30 | result.addAll({'state': state}); 31 | result.addAll({'zipCode': zipCode}); 32 | result.addAll({'isDefault': isDefault}); 33 | 34 | return result; 35 | } 36 | 37 | factory ShippingAddress.fromMap(Map map, String documentId) { 38 | return ShippingAddress( 39 | id: documentId, 40 | fullName: map['fullName'] ?? '', 41 | country: map['country'] ?? '', 42 | address: map['address'] ?? '', 43 | city: map['city'] ?? '', 44 | state: map['state'] ?? '', 45 | zipCode: map['zipCode'] ?? '', 46 | isDefault: map['isDefault'] ?? false, 47 | ); 48 | } 49 | 50 | ShippingAddress copyWith({ 51 | String? id, 52 | String? fullName, 53 | String? country, 54 | String? address, 55 | String? city, 56 | String? state, 57 | String? zipCode, 58 | bool? isDefault, 59 | }) { 60 | return ShippingAddress( 61 | id: id ?? this.id, 62 | fullName: fullName ?? this.fullName, 63 | country: country ?? this.country, 64 | address: address ?? this.address, 65 | city: city ?? this.city, 66 | state: state ?? this.state, 67 | zipCode: zipCode ?? this.zipCode, 68 | isDefault: isDefault ?? this.isDefault, 69 | ); 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /lib/models/user_data.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | 3 | class UserData { 4 | final String uid; 5 | final String email; 6 | 7 | UserData({required this.uid, required this.email}); 8 | 9 | Map toMap() { 10 | final result = {}; 11 | 12 | result.addAll({'uid': uid}); 13 | result.addAll({'email': email}); 14 | 15 | return result; 16 | } 17 | 18 | factory UserData.fromMap(Map map, String documentId) { 19 | return UserData( 20 | uid: documentId, 21 | email: map['email'] ?? '', 22 | ); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /lib/services/auth.dart: -------------------------------------------------------------------------------- 1 | import 'package:firebase_auth/firebase_auth.dart'; 2 | 3 | abstract class AuthBase { 4 | User? get currentUser; 5 | 6 | Stream authStateChanges(); 7 | 8 | Future loginWithEmailAndPassword(String email, String password); 9 | 10 | Future signUpWithEmailAndPassword(String email, String password); 11 | 12 | Future logout(); 13 | } 14 | 15 | class Auth implements AuthBase { 16 | final _firebaseAuth = FirebaseAuth.instance; 17 | 18 | @override 19 | Future loginWithEmailAndPassword(String email, String password) async { 20 | final userAuth = await _firebaseAuth.signInWithEmailAndPassword( 21 | email: email, password: password); 22 | return userAuth.user; 23 | } 24 | 25 | @override 26 | Future signUpWithEmailAndPassword( 27 | String email, String password) async { 28 | final userAuth = await _firebaseAuth.createUserWithEmailAndPassword( 29 | email: email, password: password); 30 | return userAuth.user; 31 | } 32 | 33 | @override 34 | Stream authStateChanges() => _firebaseAuth.authStateChanges(); 35 | 36 | @override 37 | User? get currentUser => _firebaseAuth.currentUser; 38 | 39 | @override 40 | Future logout() async => await _firebaseAuth.signOut(); 41 | } 42 | -------------------------------------------------------------------------------- /lib/services/auth_services.dart: -------------------------------------------------------------------------------- 1 | import 'package:firebase_auth/firebase_auth.dart'; 2 | 3 | abstract class AuthServices { 4 | User? get currentUser; 5 | 6 | Future loginWithEmailAndPassword(String email, String password); 7 | 8 | Future signUpWithEmailAndPassword(String email, String password); 9 | 10 | Future logout(); 11 | } 12 | 13 | class AuthServicesImpl implements AuthServices { 14 | final firebaseAuth = FirebaseAuth.instance; 15 | 16 | @override 17 | User? get currentUser => firebaseAuth.currentUser; 18 | 19 | @override 20 | Future loginWithEmailAndPassword(String email, String password) async { 21 | final userCredential = await firebaseAuth.signInWithEmailAndPassword( 22 | email: email, password: password,); 23 | return userCredential.user; 24 | } 25 | 26 | @override 27 | Future logout() async { 28 | await firebaseAuth.signOut(); 29 | } 30 | 31 | @override 32 | Future signUpWithEmailAndPassword(String email, String password) async { 33 | final userCredential = await firebaseAuth.createUserWithEmailAndPassword( 34 | email: email, password: password,); 35 | return userCredential.user; 36 | } 37 | } -------------------------------------------------------------------------------- /lib/services/cart_services.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_ecommerce/models/add_to_cart_model.dart'; 2 | import 'package:flutter_ecommerce/services/firestore_services.dart'; 3 | import 'package:flutter_ecommerce/utilities/api_path.dart'; 4 | 5 | abstract class CartServices { 6 | Future addProductToCart(String userId, AddToCartModel cartProduct); 7 | Future> getCartProducts(String userId); 8 | } 9 | 10 | class CartServicesImpl implements CartServices { 11 | final firestoreServices = FirestoreServices.instance; 12 | 13 | @override 14 | Future addProductToCart( 15 | String userId, AddToCartModel cartProduct) async => 16 | await firestoreServices.setData( 17 | path: ApiPath.addToCart(userId, cartProduct.id), 18 | data: cartProduct.toMap(), 19 | ); 20 | 21 | @override 22 | Future> getCartProducts(String userId) async => 23 | await firestoreServices.getCollection( 24 | path: ApiPath.myProductsCart(userId), 25 | builder: (data, documentId) => AddToCartModel.fromMap(data, documentId), 26 | ); 27 | } 28 | -------------------------------------------------------------------------------- /lib/services/checkout_services.dart: -------------------------------------------------------------------------------- 1 | import 'package:cloud_firestore/cloud_firestore.dart'; 2 | import 'package:flutter_ecommerce/models/delivery_method.dart'; 3 | import 'package:flutter_ecommerce/models/payment_method.dart'; 4 | import 'package:flutter_ecommerce/models/shipping_address.dart'; 5 | import 'package:flutter_ecommerce/services/auth.dart'; 6 | import 'package:flutter_ecommerce/services/firestore_services.dart'; 7 | import 'package:flutter_ecommerce/utilities/api_path.dart'; 8 | 9 | abstract class CheckoutServices { 10 | Future setPaymentMethod(PaymentMethod paymentMethod); 11 | Future deletePaymentMethod(PaymentMethod paymentMethod); 12 | Future> paymentMethods(); 13 | Future> shippingAddresses(String userId); 14 | Future> deliveryMethods(); 15 | Future saveAddress(String userId, ShippingAddress address); 16 | } 17 | 18 | class CheckoutServicesImpl implements CheckoutServices { 19 | final firestoreServices = FirestoreServices.instance; 20 | final authServices = Auth(); 21 | 22 | @override 23 | Future setPaymentMethod(PaymentMethod paymentMethod) async { 24 | final currentUser = authServices.currentUser; 25 | 26 | await firestoreServices.setData( 27 | path: ApiPath.addCard(currentUser!.uid, paymentMethod.id), 28 | data: paymentMethod.toMap(), 29 | ); 30 | } 31 | 32 | @override 33 | Future deletePaymentMethod(PaymentMethod paymentMethod) async { 34 | final currentUser = authServices.currentUser; 35 | 36 | await firestoreServices.deleteData( 37 | path: ApiPath.addCard(currentUser!.uid, paymentMethod.id), 38 | ); 39 | } 40 | 41 | @override 42 | Future> paymentMethods( 43 | [bool fetchPreferred = false]) async { 44 | final currentUser = authServices.currentUser; 45 | 46 | return await firestoreServices.getCollection( 47 | path: ApiPath.cards(currentUser!.uid), 48 | builder: (data, documentId) => PaymentMethod.fromMap(data), 49 | queryBuilder: fetchPreferred == true 50 | ? (query) => query.where('isPreferred', isEqualTo: true) 51 | : null, 52 | ); 53 | } 54 | 55 | @override 56 | Future> deliveryMethods() async => 57 | await firestoreServices.getCollection( 58 | path: ApiPath.deliveryMethods(), 59 | builder: (data, documentId) => DeliveryMethod.fromMap(data, documentId), 60 | ); 61 | 62 | @override 63 | Future> shippingAddresses(String userId) async => 64 | await firestoreServices.getCollection( 65 | path: ApiPath.userShippingAddress(userId), 66 | builder: (data, documentId) => 67 | ShippingAddress.fromMap(data, documentId), 68 | ); 69 | 70 | @override 71 | Future saveAddress(String userId, ShippingAddress address) async => 72 | await firestoreServices.setData( 73 | path: ApiPath.newAddress(userId, address.id), 74 | data: address.toMap(), 75 | ); 76 | } 77 | -------------------------------------------------------------------------------- /lib/services/firestore_services.dart: -------------------------------------------------------------------------------- 1 | import 'package:cloud_firestore/cloud_firestore.dart'; 2 | import 'package:flutter/foundation.dart'; 3 | 4 | class FirestoreServices { 5 | FirestoreServices._(); 6 | 7 | static final instance = FirestoreServices._(); 8 | 9 | final _fireStore = FirebaseFirestore.instance; 10 | 11 | Future setData({ 12 | required String path, 13 | required Map data, 14 | }) async { 15 | final reference = _fireStore.doc(path); 16 | debugPrint('Request Data: $data'); 17 | await reference.set(data); 18 | } 19 | 20 | Future deleteData({required String path}) async { 21 | final reference = _fireStore.doc(path); 22 | debugPrint('Path: $path'); 23 | await reference.delete(); 24 | } 25 | 26 | Stream documentsStream({ 27 | required String path, 28 | required T Function(Map? data, String documentId) builder, 29 | }) { 30 | final reference = _fireStore.doc(path); 31 | final snapshots = reference.snapshots(); 32 | return snapshots.map((snapshot) => builder(snapshot.data(), snapshot.id)); 33 | } 34 | 35 | Stream> collectionsStream({ 36 | required String path, 37 | required T Function(Map? data, String documentId) builder, 38 | Query Function(Query query)? queryBuilder, 39 | int Function(T lhs, T rhs)? sort, 40 | }) { 41 | Query query = _fireStore.collection(path); 42 | if (queryBuilder != null) { 43 | query = queryBuilder(query); 44 | } 45 | final snapshots = query.snapshots(); 46 | return snapshots.map((snapshot) { 47 | final result = snapshot.docs 48 | .map( 49 | (snapshot) => builder( 50 | snapshot.data() as Map, 51 | snapshot.id, 52 | ), 53 | ) 54 | .where((value) => value != null) 55 | .toList(); 56 | if (sort != null) { 57 | result.sort(sort); 58 | } 59 | return result; 60 | }); 61 | } 62 | 63 | Future getDocument({ 64 | required String path, 65 | required T Function(Map data, String documentID) builder, 66 | }) async { 67 | final reference = _fireStore.doc(path); 68 | final snapshot = await reference.get(); 69 | return builder(snapshot.data() as Map, snapshot.id); 70 | } 71 | 72 | Future> getCollection({ 73 | required String path, 74 | required T Function(Map data, String documentId) builder, 75 | Query Function(Query query)? queryBuilder, 76 | int Function(T lhs, T rhs)? sort, 77 | }) async { 78 | Query query = _fireStore.collection(path); 79 | if (queryBuilder != null) { 80 | query = queryBuilder(query); 81 | } 82 | final snapshots = await query.get(); 83 | final result = snapshots.docs 84 | .map((snapshot) => builder(snapshot.data() as Map, snapshot.id)) 85 | .where((value) => value != null) 86 | .toList(); 87 | if (sort != null) { 88 | result.sort(sort); 89 | } 90 | return result; 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /lib/services/home_services.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_ecommerce/models/product.dart'; 2 | import 'package:flutter_ecommerce/services/firestore_services.dart'; 3 | import 'package:flutter_ecommerce/utilities/api_path.dart'; 4 | 5 | abstract class HomeServices { 6 | Future> getSalesProducts(); 7 | Future> getNewProducts(); 8 | } 9 | 10 | class HomeServicesImpl implements HomeServices { 11 | final firestoreServices = FirestoreServices.instance; 12 | 13 | @override 14 | Future> getNewProducts() async => 15 | await firestoreServices.getCollection( 16 | path: ApiPath.products(), 17 | builder: (data, documentId) => Product.fromMap(data, documentId), 18 | ); 19 | 20 | @override 21 | Future> getSalesProducts() async => 22 | await firestoreServices.getCollection( 23 | path: ApiPath.products(), 24 | builder: (data, documentId) => Product.fromMap(data, documentId), 25 | queryBuilder: (query) => query.where('discountValue', isNotEqualTo: 0), 26 | ); 27 | } 28 | -------------------------------------------------------------------------------- /lib/services/product_details_services.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_ecommerce/models/product.dart'; 2 | import 'package:flutter_ecommerce/services/firestore_services.dart'; 3 | import 'package:flutter_ecommerce/utilities/api_path.dart'; 4 | 5 | abstract class ProductDetailsServices { 6 | Future getProductDetails(String productId); 7 | } 8 | 9 | class ProductDetailsServicesImpl implements ProductDetailsServices { 10 | final firestoreServices = FirestoreServices.instance; 11 | 12 | @override 13 | Future getProductDetails(String productId) async => 14 | await firestoreServices.getDocument( 15 | path: ApiPath.product(productId), 16 | builder: (data, documentId) => Product.fromMap(data, documentId), 17 | ); 18 | } 19 | -------------------------------------------------------------------------------- /lib/services/stripe_services.dart: -------------------------------------------------------------------------------- 1 | import 'package:dio/dio.dart'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:flutter_ecommerce/utilities/constants.dart'; 4 | import 'package:flutter_stripe/flutter_stripe.dart'; 5 | 6 | class StripeServices { 7 | StripeServices._(); 8 | 9 | static final StripeServices instance = StripeServices._(); 10 | 11 | Future makePayment(double amount, String currency) async { 12 | try { 13 | final clientSecret = await _createPaymentIntent(amount, currency); 14 | if (clientSecret == null) return; 15 | await Stripe.instance.initPaymentSheet( 16 | paymentSheetParameters: SetupPaymentSheetParameters( 17 | paymentIntentClientSecret: clientSecret, 18 | merchantDisplayName: 'E-commerce Live Coding by Tarek', 19 | ), 20 | ); 21 | await Stripe.instance.presentPaymentSheet(); 22 | } catch (e) { 23 | debugPrint('Make Payment: ${e.toString()}'); 24 | rethrow; 25 | } 26 | } 27 | 28 | Future _createPaymentIntent(double amount, String currency) async { 29 | try { 30 | final aDio = Dio(); 31 | Map body = { 32 | 'amount': _getFinalAmount(amount), 33 | 'currency': currency, 34 | }; 35 | 36 | final headers = { 37 | 'Authorization': 'Bearer ${AppConstants.secretKey}', 38 | 'Content-Type': 'application/x-www-form-urlencoded', 39 | }; 40 | final response = await aDio.post( 41 | AppConstants.paymentIntentPath, 42 | data: body, 43 | options: Options( 44 | contentType: Headers.formUrlEncodedContentType, 45 | headers: headers, 46 | ), 47 | ); 48 | if (response.data != null) { 49 | debugPrint(response.data.toString()); 50 | return response.data['client_secret']; 51 | } 52 | } catch (e) { 53 | debugPrint('Create Payment Intent: ${e.toString()}'); 54 | rethrow; 55 | } 56 | return null; 57 | } 58 | 59 | int _getFinalAmount(double amount) { 60 | return (amount * 100).toInt(); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /lib/utilities/api_path.dart: -------------------------------------------------------------------------------- 1 | class ApiPath { 2 | static String products() => 'products/'; 3 | static String product(String id) => 'products/$id'; 4 | 5 | static String deliveryMethods() => 'deliveryMethods/'; 6 | static String user(String uid) => 'users/$uid'; 7 | static String userShippingAddress(String uid) => 'users/$uid/shippingAddresses/'; 8 | static String newAddress(String uid, String addressId) => 'users/$uid/shippingAddresses/$addressId'; 9 | static String addToCart(String uid, String addToCartId) => 'users/$uid/cart/$addToCartId'; 10 | static String myProductsCart(String uid) => 'users/$uid/cart/'; 11 | 12 | static String addCard(String uid, String cardId) => 'users/$uid/cards/$cardId'; 13 | static String cards(String uid) => 'users/$uid/cards/'; 14 | } 15 | -------------------------------------------------------------------------------- /lib/utilities/args_models/add_shipping_address_args.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_ecommerce/controllers/checkout/checkout_cubit.dart'; 2 | import 'package:flutter_ecommerce/models/shipping_address.dart'; 3 | 4 | class AddShippingAddressArgs { 5 | final ShippingAddress? shippingAddress; 6 | final CheckoutCubit checkoutCubit; 7 | 8 | AddShippingAddressArgs({this.shippingAddress, required this.checkoutCubit,}); 9 | } 10 | -------------------------------------------------------------------------------- /lib/utilities/assets.dart: -------------------------------------------------------------------------------- 1 | class AppAssets { 2 | /// Home Page Images 3 | static const String topBannerHomePageAsset = 4 | 'https://i0.wp.com/www.sifascorner.com/wp-content/uploads/2020/09/Best-Online-Clothing-Stores-for-Budget-Shopping-Sifas-Corner-2-scaled.jpg'; 5 | static const String tempProductAsset1 = 6 | 'https://m.media-amazon.com/images/I/61-jBuhtgZL._UX569_.jpg'; 7 | 8 | /// Shop Page Images 9 | 10 | /// Authentication Page Images 11 | static const facebookIcon = 'assets/facebook-svgrepo-com.svg'; 12 | static const googleIcon = 'assets/google-svgrepo-com.svg'; 13 | 14 | /// Checkout Images 15 | static const mastercardIcon = 16 | 'https://upload.wikimedia.org/wikipedia/commons/0/04/Mastercard-logo.png'; 17 | } 18 | -------------------------------------------------------------------------------- /lib/utilities/constants.dart: -------------------------------------------------------------------------------- 1 | String documentIdFromLocalData() => DateTime.now().toIso8601String(); 2 | 3 | class AppConstants { 4 | static const String paymentIntentPath = 5 | 'https://api.stripe.com/v1/payment_intents'; 6 | 7 | /// Stripe Keys 8 | static const String publishableKey = 9 | 'pk_test_51PPo8hRoXZnvJgvhmlmpg1Lfyg5cqkwG28eNHEdJHazS5y9vVvgK9vRMZA0ABR79ZBgLxLMkCoUuMXsNU760dHST0074OEqGCn'; 10 | static const String secretKey = 11 | 'sk_test_51PPo8hRoXZnvJgvhEDx2v6s8LjltgvUsOLqnxSufQa4opacp3bmHckSAI6dU9iAAca9icN3BlavcqwJxTrRgaP7C00u2Ow7wsN'; 12 | } 13 | -------------------------------------------------------------------------------- /lib/utilities/enums.dart: -------------------------------------------------------------------------------- 1 | enum AuthFormType { 2 | login, 3 | register 4 | } -------------------------------------------------------------------------------- /lib/utilities/router.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/cupertino.dart'; 2 | import 'package:flutter_bloc/flutter_bloc.dart'; 3 | import 'package:flutter_ecommerce/controllers/checkout/checkout_cubit.dart'; 4 | import 'package:flutter_ecommerce/controllers/product_details/product_details_cubit.dart'; 5 | import 'package:flutter_ecommerce/models/shipping_address.dart'; 6 | import 'package:flutter_ecommerce/utilities/args_models/add_shipping_address_args.dart'; 7 | import 'package:flutter_ecommerce/utilities/routes.dart'; 8 | import 'package:flutter_ecommerce/views/pages/bottom_navbar.dart'; 9 | import 'package:flutter_ecommerce/views/pages/checkout/add_shipping_address_page.dart'; 10 | import 'package:flutter_ecommerce/views/pages/checkout/checkout_page.dart'; 11 | import 'package:flutter_ecommerce/views/pages/checkout/payment_methods_page.dart'; 12 | import 'package:flutter_ecommerce/views/pages/checkout/shipping_addresses_page.dart'; 13 | import 'package:flutter_ecommerce/views/pages/auth_page.dart'; 14 | import 'package:flutter_ecommerce/views/pages/product_details.dart'; 15 | 16 | Route onGenerate(RouteSettings settings) { 17 | switch (settings.name) { 18 | case AppRoutes.loginPageRoute: 19 | return CupertinoPageRoute( 20 | builder: (_) => const AuthPage(), 21 | settings: settings, 22 | ); 23 | case AppRoutes.bottomNavBarRoute: 24 | return CupertinoPageRoute( 25 | builder: (_) => const BottomNavbar(), 26 | settings: settings, 27 | ); 28 | case AppRoutes.checkoutPageRoute: 29 | return CupertinoPageRoute( 30 | builder: (_) => BlocProvider( 31 | create: (context) { 32 | final cubit = CheckoutCubit(); 33 | cubit.getCheckoutData(); 34 | return cubit; 35 | }, 36 | child: const CheckoutPage(), 37 | ), 38 | settings: settings, 39 | ); 40 | case AppRoutes.productDetailsRoute: 41 | final productId = settings.arguments as String; 42 | 43 | return CupertinoPageRoute( 44 | builder: (_) => BlocProvider( 45 | create: (context) { 46 | final cubit = ProductDetailsCubit(); 47 | cubit.getProductDetails(productId); 48 | return cubit; 49 | }, 50 | child: const ProductDetails(), 51 | ), 52 | settings: settings, 53 | ); 54 | 55 | case AppRoutes.shippingAddressesRoute: 56 | final checkoutCubit = settings.arguments as CheckoutCubit; 57 | return CupertinoPageRoute( 58 | builder: (_) => BlocProvider.value( 59 | value: checkoutCubit, 60 | child: const ShippingAddressesPage(), 61 | ), 62 | settings: settings, 63 | ); 64 | case AppRoutes.paymentMethodsRoute: 65 | return CupertinoPageRoute( 66 | builder: (_) => BlocProvider( 67 | create: (context) { 68 | final cubit = CheckoutCubit(); 69 | cubit.fetchCards(); 70 | return cubit; 71 | }, 72 | child: const PaymentMethodsPage(), 73 | ), 74 | settings: settings, 75 | ); 76 | case AppRoutes.addShippingAddressRoute: 77 | final args = settings.arguments as AddShippingAddressArgs; 78 | final checkoutCubit = args.checkoutCubit; 79 | final shippingAddress = args.shippingAddress; 80 | 81 | return CupertinoPageRoute( 82 | builder: (_) => BlocProvider.value( 83 | value: checkoutCubit, 84 | child: AddShippingAddressPage( 85 | shippingAddress: shippingAddress, 86 | ), 87 | ), 88 | settings: settings, 89 | ); 90 | default: 91 | return CupertinoPageRoute( 92 | builder: (_) => const AuthPage(), 93 | settings: settings, 94 | ); 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /lib/utilities/routes.dart: -------------------------------------------------------------------------------- 1 | class AppRoutes { 2 | static const String loginPageRoute = '/'; 3 | static const String registerPageRoute = '/register'; 4 | static const String bottomNavBarRoute = '/navbar'; 5 | static const String productDetailsRoute = '/product-details'; 6 | static const String checkoutPageRoute = '/checkout'; 7 | static const String shippingAddressesRoute = '/checkout/shipping-addresses'; 8 | static const String addShippingAddressRoute = 9 | '/checkout/add-shipping-address'; 10 | static const String paymentMethodsRoute = '/checkout/payment-methods'; 11 | } 12 | -------------------------------------------------------------------------------- /lib/views/pages/auth_page.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_bloc/flutter_bloc.dart'; 3 | import 'package:flutter_ecommerce/controllers/auth/auth_cubit.dart'; 4 | import 'package:flutter_ecommerce/utilities/assets.dart'; 5 | import 'package:flutter_ecommerce/utilities/enums.dart'; 6 | import 'package:flutter_ecommerce/utilities/routes.dart'; 7 | import 'package:flutter_ecommerce/views/widgets/main_button.dart'; 8 | import 'package:flutter_ecommerce/views/widgets/main_dialog.dart'; 9 | import 'package:flutter_ecommerce/views/widgets/social_media_button.dart'; 10 | 11 | class AuthPage extends StatefulWidget { 12 | const AuthPage({super.key}); 13 | 14 | @override 15 | State createState() => _AuthPageState(); 16 | } 17 | 18 | class _AuthPageState extends State { 19 | final _formKey = GlobalKey(); 20 | final _emailController = TextEditingController(); 21 | final _passwordController = TextEditingController(); 22 | final _emailFocusNode = FocusNode(); 23 | final _passwordFocusNode = FocusNode(); 24 | 25 | @override 26 | void dispose() { 27 | _emailController.dispose(); 28 | _passwordController.dispose(); 29 | super.dispose(); 30 | } 31 | 32 | @override 33 | Widget build(BuildContext context) { 34 | final size = MediaQuery.of(context).size; 35 | final authCubit = BlocProvider.of(context); 36 | 37 | return Scaffold( 38 | resizeToAvoidBottomInset: true, 39 | body: SafeArea( 40 | child: Padding( 41 | padding: const EdgeInsets.symmetric( 42 | vertical: 60.0, 43 | horizontal: 32.0, 44 | ), 45 | child: Form( 46 | key: _formKey, 47 | child: SingleChildScrollView( 48 | child: Column( 49 | crossAxisAlignment: CrossAxisAlignment.start, 50 | children: [ 51 | Text( 52 | authCubit.authFormType == AuthFormType.login 53 | ? 'Login' 54 | : 'Register', 55 | style: Theme.of(context).textTheme.titleLarge, 56 | ), 57 | const SizedBox(height: 80.0), 58 | TextFormField( 59 | controller: _emailController, 60 | focusNode: _emailFocusNode, 61 | onEditingComplete: () => 62 | FocusScope.of(context).requestFocus(_passwordFocusNode), 63 | textInputAction: TextInputAction.next, 64 | validator: (val) => 65 | val!.isEmpty ? 'Please enter your email!' : null, 66 | decoration: const InputDecoration( 67 | labelText: 'Email', 68 | hintText: 'Enter your email!', 69 | ), 70 | ), 71 | const SizedBox(height: 24.0), 72 | TextFormField( 73 | controller: _passwordController, 74 | focusNode: _passwordFocusNode, 75 | validator: (val) => 76 | val!.isEmpty ? 'Please enter your password!' : null, 77 | obscureText: true, 78 | decoration: const InputDecoration( 79 | labelText: 'Password', 80 | hintText: 'Enter your pasword!', 81 | ), 82 | ), 83 | const SizedBox(height: 16.0), 84 | if (authCubit.authFormType == AuthFormType.login) 85 | Align( 86 | alignment: Alignment.topRight, 87 | child: InkWell( 88 | child: const Text('Forgot your password?'), 89 | onTap: () {}, 90 | ), 91 | ), 92 | const SizedBox(height: 24.0), 93 | BlocConsumer( 94 | bloc: authCubit, 95 | listenWhen: (previous, current) => 96 | current is AuthFailed || current is AuthSuccess, 97 | listener: (context, state) { 98 | if (state is AuthFailed) { 99 | MainDialog( 100 | title: state.error, 101 | content: state.error, 102 | context: context, 103 | ).showAlertDialog(); 104 | } else if (state is AuthSuccess) { 105 | Navigator.of(context) 106 | .pushReplacementNamed(AppRoutes.bottomNavBarRoute); 107 | } 108 | }, 109 | buildWhen: (previous, current) => 110 | current is AuthLoading || 111 | current is AuthSuccess || 112 | current is AuthFailed || 113 | current is AuthInitial, 114 | builder: (context, state) { 115 | if (state is AuthLoading) { 116 | return MainButton( 117 | child: const CircularProgressIndicator.adaptive(), 118 | ); 119 | } 120 | return MainButton( 121 | text: authCubit.authFormType == AuthFormType.login 122 | ? 'Login' 123 | : 'Register', 124 | onTap: () async { 125 | if (_formKey.currentState!.validate()) { 126 | authCubit.authFormType == AuthFormType.login 127 | ? await authCubit.login(_emailController.text, 128 | _passwordController.text) 129 | : await authCubit.signUp(_emailController.text, 130 | _passwordController.text); 131 | } 132 | }, 133 | ); 134 | }, 135 | ), 136 | const SizedBox(height: 16.0), 137 | Align( 138 | alignment: Alignment.center, 139 | child: InkWell( 140 | child: Text( 141 | authCubit.authFormType == AuthFormType.login 142 | ? 'Don\'t have an account? Register' 143 | : 'Have an account? Login', 144 | ), 145 | onTap: () { 146 | _formKey.currentState!.reset(); 147 | authCubit.toggleFormType(); 148 | }, 149 | ), 150 | ), 151 | SizedBox(height: size.height * 0.09), 152 | Align( 153 | alignment: Alignment.center, 154 | child: Text( 155 | authCubit.authFormType == AuthFormType.login 156 | ? 'Or Login with' 157 | : 'Or Register with', 158 | style: Theme.of(context).textTheme.labelMedium, 159 | )), 160 | const SizedBox(height: 16.0), 161 | Row( 162 | mainAxisAlignment: MainAxisAlignment.center, 163 | children: [ 164 | SocialMediaButton( 165 | iconName: AppAssets.facebookIcon, 166 | onPress: () {}, 167 | ), 168 | const SizedBox(width: 16.0), 169 | SocialMediaButton( 170 | iconName: AppAssets.googleIcon, 171 | onPress: () {}, 172 | ), 173 | ], 174 | ), 175 | ], 176 | ), 177 | ), 178 | ), 179 | ), 180 | ), 181 | ); 182 | } 183 | } 184 | -------------------------------------------------------------------------------- /lib/views/pages/bottom_navbar.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/cupertino.dart'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:flutter_bloc/flutter_bloc.dart'; 4 | import 'package:flutter_ecommerce/controllers/cart/cart_cubit.dart'; 5 | import 'package:flutter_ecommerce/controllers/home/home_cubit.dart'; 6 | import 'package:flutter_ecommerce/views/pages/cart_page.dart'; 7 | import 'package:flutter_ecommerce/views/pages/home_page.dart'; 8 | import 'package:flutter_ecommerce/views/pages/profle_page.dart'; 9 | import 'package:persistent_bottom_nav_bar_v2/persistent_bottom_nav_bar_v2.dart'; 10 | 11 | class BottomNavbar extends StatefulWidget { 12 | const BottomNavbar({super.key}); 13 | 14 | @override 15 | State createState() => _BottomNavbarState(); 16 | } 17 | 18 | class _BottomNavbarState extends State { 19 | final _bottomNavbarController = PersistentTabController(); 20 | 21 | List _buildScreens() { 22 | return [ 23 | BlocProvider( 24 | create: (context) { 25 | final cubit = HomeCubit(); 26 | cubit.getHomeContent(); 27 | return cubit; 28 | }, 29 | child: const HomePage(), 30 | ), 31 | Container(), 32 | BlocProvider( 33 | create: (context) { 34 | final cubit = CartCubit(); 35 | cubit.getCartItems(); 36 | return cubit; 37 | }, 38 | child: const CartPage(), 39 | ), 40 | Container(), 41 | const ProfilePage() 42 | ]; 43 | } 44 | 45 | // List _navBarsItems() { 46 | // return [ 47 | // PersistentTabConfig( 48 | // icon: const Icon(CupertinoIcons.home), 49 | // title: ("Home"), 50 | // activeColorPrimary: CupertinoColors.activeBlue, 51 | // inactiveColorPrimary: CupertinoColors.systemGrey, 52 | // ), 53 | // PersistentTabConfig( 54 | // icon: const Icon(CupertinoIcons.bag), 55 | // title: ("Shop"), 56 | // activeColorPrimary: CupertinoColors.activeBlue, 57 | // inactiveColorPrimary: CupertinoColors.systemGrey, 58 | // ), 59 | // PersistentTabConfig( 60 | // icon: const Icon(CupertinoIcons.shopping_cart), 61 | // title: ("Cart"), 62 | // activeColorPrimary: CupertinoColors.activeBlue, 63 | // inactiveColorPrimary: CupertinoColors.systemGrey, 64 | // ), 65 | // PersistentTabConfig( 66 | // icon: const Icon(Icons.favorite_border), 67 | // title: ("Favorites"), 68 | // activeColorPrimary: CupertinoColors.activeBlue, 69 | // inactiveColorPrimary: CupertinoColors.systemGrey, 70 | // ), 71 | // PersistentTabConfig( 72 | // icon: const Icon(CupertinoIcons.profile_circled), 73 | // title: ("Profile"), 74 | // activeColorPrimary: CupertinoColors.activeBlue, 75 | // inactiveColorPrimary: CupertinoColors.systemGrey, 76 | // ), 77 | // ]; 78 | // } 79 | 80 | @override 81 | Widget build(BuildContext context) { 82 | return Scaffold( 83 | body: PersistentTabView( 84 | controller: _bottomNavbarController, 85 | tabs: [ 86 | PersistentTabConfig( 87 | screen: _buildScreens()[0], 88 | item: ItemConfig( 89 | icon: const Icon(CupertinoIcons.home), 90 | title: ("Home"), 91 | activeForegroundColor: Colors.redAccent, 92 | // activeColorPrimary: CupertinoColors.activeBlue, 93 | // inactiveColorPrimary: CupertinoColors.systemGrey, 94 | ), 95 | ), 96 | PersistentTabConfig( 97 | screen: _buildScreens()[1], 98 | item: ItemConfig( 99 | icon: const Icon(CupertinoIcons.bag), 100 | title: ("Shop"), 101 | activeForegroundColor: Colors.redAccent, 102 | // activeColorPrimary: CupertinoColors.activeBlue, 103 | // inactiveColorPrimary: CupertinoColors.systemGrey, 104 | ), 105 | ), 106 | PersistentTabConfig( 107 | screen: _buildScreens()[2], 108 | item: ItemConfig( 109 | icon: const Icon(CupertinoIcons.shopping_cart), 110 | title: ("Cart"), 111 | activeForegroundColor: Colors.redAccent, 112 | // activeColorPrimary: CupertinoColors.activeBlue, 113 | // inactiveColorPrimary: CupertinoColors.systemGrey, 114 | ), 115 | ), 116 | PersistentTabConfig( 117 | screen: _buildScreens()[3], 118 | item: ItemConfig( 119 | icon: const Icon(Icons.favorite_border), 120 | title: ("Favorites"), 121 | activeForegroundColor: Colors.redAccent, 122 | // activeColorPrimary: CupertinoColors.activeBlue, 123 | // inactiveColorPrimary: CupertinoColors.systemGrey, 124 | ), 125 | ), 126 | PersistentTabConfig( 127 | screen: _buildScreens()[4], 128 | item: ItemConfig( 129 | icon: const Icon(CupertinoIcons.profile_circled), 130 | title: ("Profile"), 131 | activeForegroundColor: Colors.redAccent, 132 | // activeColorPrimary: CupertinoColors.activeBlue, 133 | // inactiveColorPrimary: CupertinoColors.systemGrey, 134 | ), 135 | ), 136 | ], 137 | navBarBuilder: (navbarConfig) => Style1BottomNavBar( 138 | navBarConfig: navbarConfig, 139 | ), 140 | // screens: _buildScreens(), 141 | // items: _navBarsItems(), 142 | // confineInSafeArea: true, 143 | backgroundColor: Colors.white, // Default is Colors.white. 144 | handleAndroidBackButtonPress: true, // Default is true. 145 | resizeToAvoidBottomInset: 146 | true, // This needs to be true if you want to move up the screen when keyboard appears. Default is true. 147 | stateManagement: true, // Default is true. 148 | // hideNavigationBarWhenKeyboardShows: 149 | // true, // Recommended to set 'resizeToAvoidBottomInset' as true while using this argument. Default is true. 150 | // decoration: NavBarDecoration( 151 | // borderRadius: BorderRadius.circular(10.0), 152 | // colorBehindNavBar: Colors.white, 153 | // ), 154 | popAllScreensOnTapOfSelectedTab: true, 155 | popActionScreens: PopActionScreensType.all, 156 | // itemAnimationProperties: ItemAnimationProperties( 157 | // // Navigation Bar's items animation properties. 158 | // duration: Duration(milliseconds: 200), 159 | // curve: Curves.ease, 160 | // ), 161 | screenTransitionAnimation: const ScreenTransitionAnimation( 162 | // Screen transition animation on change of selected tab. 163 | // animateTabTransition: true, 164 | curve: Curves.ease, 165 | duration: Duration(milliseconds: 200), 166 | ), 167 | // navBarStyle: 168 | // NavBarStyle.style1, // Choose the nav bar style with this property. 169 | ), 170 | ); 171 | } 172 | } 173 | -------------------------------------------------------------------------------- /lib/views/pages/cart_page.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_bloc/flutter_bloc.dart'; 3 | import 'package:flutter_ecommerce/controllers/cart/cart_cubit.dart'; 4 | import 'package:flutter_ecommerce/utilities/routes.dart'; 5 | import 'package:flutter_ecommerce/views/widgets/cart_list_item.dart'; 6 | import 'package:flutter_ecommerce/views/widgets/main_button.dart'; 7 | import 'package:flutter_ecommerce/views/widgets/order_summary_component.dart'; 8 | 9 | class CartPage extends StatelessWidget { 10 | const CartPage({super.key}); 11 | 12 | @override 13 | Widget build(BuildContext context) { 14 | final cartCubit = BlocProvider.of(context); 15 | 16 | return SafeArea( 17 | child: BlocBuilder( 18 | bloc: cartCubit, 19 | buildWhen: (previous, current) => 20 | current is CartLoaded || 21 | current is CartLoading || 22 | current is CartError, 23 | builder: (context, state) { 24 | if (state is CartLoading) { 25 | return const Center( 26 | child: CircularProgressIndicator.adaptive(), 27 | ); 28 | } else if (state is CartLoaded) { 29 | final totalAmount = state.totalAmount; 30 | final cartProducts = state.cartProducts; 31 | 32 | return Padding( 33 | padding: 34 | const EdgeInsets.symmetric(vertical: 8.0, horizontal: 16.0), 35 | child: RefreshIndicator( 36 | onRefresh: () async { 37 | await cartCubit.getCartItems(); 38 | }, 39 | child: ListView( 40 | children: [ 41 | Row( 42 | mainAxisAlignment: MainAxisAlignment.spaceBetween, 43 | children: [ 44 | const SizedBox.shrink(), 45 | IconButton( 46 | onPressed: () {}, 47 | icon: const Icon(Icons.search), 48 | ), 49 | ], 50 | ), 51 | const SizedBox(height: 16.0), 52 | Text( 53 | 'My Cart', 54 | style: Theme.of(context).textTheme.titleLarge!.copyWith( 55 | fontWeight: FontWeight.bold, 56 | color: Colors.black, 57 | ), 58 | ), 59 | const SizedBox(height: 16.0), 60 | if (cartProducts.isEmpty) 61 | Center( 62 | child: Text( 63 | 'No Data Available!', 64 | style: Theme.of(context).textTheme.labelMedium, 65 | ), 66 | ), 67 | if (cartProducts.isNotEmpty) 68 | ListView.builder( 69 | itemCount: cartProducts.length, 70 | shrinkWrap: true, 71 | physics: const NeverScrollableScrollPhysics(), 72 | itemBuilder: (BuildContext context, int i) { 73 | final cartItem = cartProducts[i]; 74 | return CartListItem( 75 | cartItem: cartItem, 76 | ); 77 | }, 78 | ), 79 | const SizedBox(height: 24.0), 80 | OrderSummaryComponent( 81 | title: 'Total Amount', 82 | value: totalAmount.toString(), 83 | ), 84 | const SizedBox(height: 32.0), 85 | MainButton( 86 | text: 'Checkout', 87 | onTap: () => 88 | Navigator.of(context, rootNavigator: true).pushNamed( 89 | AppRoutes.checkoutPageRoute, 90 | ), 91 | hasCircularBorder: true, 92 | ), 93 | const SizedBox(height: 32.0), 94 | ], 95 | ), 96 | ), 97 | ); 98 | } else if (state is CartError) { 99 | return Center( 100 | child: Text( 101 | state.message, 102 | style: Theme.of(context).textTheme.labelMedium, 103 | ), 104 | ); 105 | } else { 106 | return const SizedBox.shrink(); 107 | } 108 | }, 109 | ), 110 | ); 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /lib/views/pages/checkout/add_shipping_address_page.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_bloc/flutter_bloc.dart'; 3 | import 'package:flutter_ecommerce/controllers/checkout/checkout_cubit.dart'; 4 | import 'package:flutter_ecommerce/controllers/database_controller.dart'; 5 | import 'package:flutter_ecommerce/models/shipping_address.dart'; 6 | import 'package:flutter_ecommerce/utilities/constants.dart'; 7 | import 'package:flutter_ecommerce/views/widgets/main_button.dart'; 8 | import 'package:flutter_ecommerce/views/widgets/main_dialog.dart'; 9 | import 'package:provider/provider.dart'; 10 | 11 | class AddShippingAddressPage extends StatefulWidget { 12 | final ShippingAddress? shippingAddress; 13 | const AddShippingAddressPage({super.key, this.shippingAddress}); 14 | 15 | @override 16 | State createState() => _AddShippingAddressPageState(); 17 | } 18 | 19 | class _AddShippingAddressPageState extends State { 20 | final _formKey = GlobalKey(); 21 | final _fullNameController = TextEditingController(); 22 | final _addressController = TextEditingController(); 23 | final _cityController = TextEditingController(); 24 | final _stateController = TextEditingController(); 25 | final _zipCodeController = TextEditingController(); 26 | final _countryController = TextEditingController(); 27 | ShippingAddress? shippingAddress; 28 | 29 | @override 30 | void initState() { 31 | super.initState(); 32 | shippingAddress = widget.shippingAddress; 33 | if (shippingAddress != null) { 34 | _fullNameController.text = shippingAddress!.fullName; 35 | _addressController.text = shippingAddress!.address; 36 | _cityController.text = shippingAddress!.city; 37 | _stateController.text = shippingAddress!.state; 38 | _zipCodeController.text = shippingAddress!.zipCode; 39 | _countryController.text = shippingAddress!.country; 40 | } 41 | } 42 | 43 | @override 44 | void dispose() { 45 | _fullNameController.dispose(); 46 | _addressController.dispose(); 47 | _cityController.dispose(); 48 | _stateController.dispose(); 49 | _zipCodeController.dispose(); 50 | _countryController.dispose(); 51 | super.dispose(); 52 | } 53 | 54 | Future saveAddress(CheckoutCubit checkoutCubit) async { 55 | try { 56 | if (_formKey.currentState!.validate()) { 57 | final address = ShippingAddress( 58 | id: shippingAddress != null 59 | ? shippingAddress!.id 60 | : documentIdFromLocalData(), 61 | fullName: _fullNameController.text.trim(), 62 | country: _countryController.text.trim(), 63 | address: _addressController.text.trim(), 64 | city: _cityController.text.trim(), 65 | state: _stateController.text.trim(), 66 | zipCode: _zipCodeController.text.trim(), 67 | ); 68 | await checkoutCubit.saveAddress(address); 69 | // await database.saveAddress(address); 70 | if (!mounted) return; 71 | Navigator.of(context).pop(); 72 | } 73 | } catch (e) { 74 | MainDialog( 75 | context: context, 76 | title: 'Error Saving Address', 77 | content: e.toString()) 78 | .showAlertDialog(); 79 | } 80 | } 81 | 82 | @override 83 | Widget build(BuildContext context) { 84 | final checkoutCubit = BlocProvider.of(context); 85 | 86 | return Scaffold( 87 | appBar: AppBar( 88 | title: Text( 89 | shippingAddress != null 90 | ? 'Editing Shipping Address' 91 | : 'Adding Shipping Address', 92 | style: Theme.of(context).textTheme.labelMedium, 93 | ), 94 | centerTitle: true, 95 | ), 96 | body: SingleChildScrollView( 97 | child: Form( 98 | key: _formKey, 99 | child: Padding( 100 | padding: 101 | const EdgeInsets.symmetric(vertical: 24.0, horizontal: 16.0), 102 | child: Column( 103 | children: [ 104 | TextFormField( 105 | controller: _fullNameController, 106 | decoration: const InputDecoration( 107 | labelText: 'Full Name', 108 | fillColor: Colors.white, 109 | filled: true, 110 | ), 111 | validator: (value) => 112 | value!.isNotEmpty ? null : 'Please enter your name', 113 | ), 114 | const SizedBox(height: 16.0), 115 | TextFormField( 116 | controller: _addressController, 117 | decoration: const InputDecoration( 118 | labelText: 'Address', 119 | fillColor: Colors.white, 120 | filled: true, 121 | ), 122 | validator: (value) => 123 | value!.isNotEmpty ? null : 'Please enter your name', 124 | ), 125 | const SizedBox(height: 16.0), 126 | TextFormField( 127 | controller: _cityController, 128 | decoration: const InputDecoration( 129 | labelText: 'City', 130 | fillColor: Colors.white, 131 | filled: true, 132 | ), 133 | validator: (value) => 134 | value!.isNotEmpty ? null : 'Please enter your name', 135 | ), 136 | const SizedBox(height: 16.0), 137 | TextFormField( 138 | controller: _stateController, 139 | decoration: const InputDecoration( 140 | labelText: 'State/Province', 141 | fillColor: Colors.white, 142 | filled: true, 143 | ), 144 | validator: (value) => 145 | value!.isNotEmpty ? null : 'Please enter your name', 146 | ), 147 | const SizedBox(height: 16.0), 148 | TextFormField( 149 | controller: _zipCodeController, 150 | decoration: const InputDecoration( 151 | labelText: 'Zip Code', 152 | fillColor: Colors.white, 153 | filled: true, 154 | ), 155 | validator: (value) => 156 | value!.isNotEmpty ? null : 'Please enter your name', 157 | ), 158 | const SizedBox(height: 16.0), 159 | TextFormField( 160 | controller: _countryController, 161 | decoration: const InputDecoration( 162 | labelText: 'Country', 163 | fillColor: Colors.white, 164 | filled: true, 165 | ), 166 | validator: (value) => 167 | value!.isNotEmpty ? null : 'Please enter your name', 168 | ), 169 | const SizedBox(height: 32.0), 170 | MainButton( 171 | text: 'Save Address', 172 | onTap: () => saveAddress(checkoutCubit), 173 | hasCircularBorder: true, 174 | ), 175 | ], 176 | ), 177 | ), 178 | ), 179 | ), 180 | ); 181 | } 182 | } 183 | -------------------------------------------------------------------------------- /lib/views/pages/checkout/checkout_page.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_bloc/flutter_bloc.dart'; 3 | import 'package:flutter_ecommerce/controllers/checkout/checkout_cubit.dart'; 4 | import 'package:flutter_ecommerce/models/delivery_method.dart'; 5 | import 'package:flutter_ecommerce/models/shipping_address.dart'; 6 | import 'package:flutter_ecommerce/utilities/args_models/add_shipping_address_args.dart'; 7 | import 'package:flutter_ecommerce/utilities/routes.dart'; 8 | import 'package:flutter_ecommerce/views/widgets/checkout/checkout_order_details.dart'; 9 | import 'package:flutter_ecommerce/views/widgets/checkout/delivery_method_item.dart'; 10 | import 'package:flutter_ecommerce/views/widgets/checkout/payment_component.dart'; 11 | import 'package:flutter_ecommerce/views/widgets/checkout/shipping_address_component.dart'; 12 | import 'package:flutter_ecommerce/views/widgets/main_button.dart'; 13 | 14 | class CheckoutPage extends StatelessWidget { 15 | const CheckoutPage({super.key}); 16 | 17 | @override 18 | Widget build(BuildContext context) { 19 | final size = MediaQuery.of(context).size; 20 | final checkoutCubit = BlocProvider.of(context); 21 | 22 | Widget shippingAddressComponent(ShippingAddress? shippingAddress) { 23 | if (shippingAddress == null) { 24 | return Center( 25 | child: Column( 26 | children: [ 27 | const Text('No Shipping Addresses!'), 28 | const SizedBox(height: 6.0), 29 | InkWell( 30 | onTap: () => Navigator.of(context).pushNamed( 31 | AppRoutes.addShippingAddressRoute, 32 | arguments: AddShippingAddressArgs( 33 | checkoutCubit: checkoutCubit, 34 | shippingAddress: shippingAddress, 35 | ) 36 | ), 37 | child: Text( 38 | 'Add new one', 39 | style: Theme.of(context).textTheme.labelSmall!.copyWith( 40 | color: Colors.redAccent, 41 | ), 42 | ), 43 | ), 44 | ], 45 | ), 46 | ); 47 | } else { 48 | return ShippingAddressComponent(shippingAddress: shippingAddress, checkoutCubit: checkoutCubit,); 49 | } 50 | } 51 | 52 | Widget deliveryMethodsComponent(List deliveryMethods) { 53 | if (deliveryMethods.isEmpty) { 54 | return const Center( 55 | child: Text('No delivery methods available!'), 56 | ); 57 | } 58 | return SizedBox( 59 | height: size.height * 0.13, 60 | child: ListView.builder( 61 | itemCount: deliveryMethods.length, 62 | scrollDirection: Axis.horizontal, 63 | itemBuilder: (_, i) => Padding( 64 | padding: const EdgeInsets.all(8.0), 65 | child: DeliveryMethodItem(deliveryMethod: deliveryMethods[i]), 66 | ), 67 | ), 68 | ); 69 | } 70 | 71 | return Scaffold( 72 | appBar: AppBar( 73 | title: Text( 74 | 'Checkout', 75 | style: Theme.of(context).textTheme.labelMedium, 76 | ), 77 | centerTitle: true, 78 | ), 79 | body: BlocBuilder( 80 | bloc: checkoutCubit, 81 | buildWhen: (previous, current) => 82 | current is CheckoutLoading || 83 | current is CheckoutLoaded || 84 | current is CheckoutLoadingFailed, 85 | builder: (context, state) { 86 | if (state is CheckoutLoading) { 87 | return const Center( 88 | child: CircularProgressIndicator.adaptive(), 89 | ); 90 | } else if (state is CheckoutLoadingFailed) { 91 | return Center( 92 | child: Text(state.error), 93 | ); 94 | } else if (state is CheckoutLoaded) { 95 | final shippingAddress = state.shippingAddress; 96 | final deliveryMethods = state.deliveryMethods; 97 | 98 | return Padding( 99 | padding: 100 | const EdgeInsets.symmetric(horizontal: 16.0, vertical: 32.0), 101 | child: SingleChildScrollView( 102 | child: Column( 103 | crossAxisAlignment: CrossAxisAlignment.start, 104 | children: [ 105 | Text( 106 | 'Shipping address', 107 | style: Theme.of(context).textTheme.titleLarge, 108 | ), 109 | const SizedBox(height: 8.0), 110 | shippingAddressComponent(shippingAddress), 111 | const SizedBox(height: 24.0), 112 | Row( 113 | mainAxisAlignment: MainAxisAlignment.spaceBetween, 114 | children: [ 115 | Text( 116 | 'Payment', 117 | style: Theme.of(context).textTheme.titleLarge, 118 | ), 119 | InkWell( 120 | onTap: () { 121 | Navigator.of(context) 122 | .pushNamed(AppRoutes.paymentMethodsRoute); 123 | }, 124 | child: Text( 125 | 'Change', 126 | style: Theme.of(context) 127 | .textTheme 128 | .labelSmall! 129 | .copyWith( 130 | color: Colors.redAccent, 131 | ), 132 | ), 133 | ), 134 | ], 135 | ), 136 | const SizedBox(height: 8.0), 137 | const PaymentComponent(), 138 | const SizedBox(height: 24.0), 139 | Text( 140 | 'Delivery method', 141 | style: Theme.of(context).textTheme.titleLarge, 142 | ), 143 | const SizedBox(height: 8.0), 144 | deliveryMethodsComponent(deliveryMethods), 145 | const SizedBox(height: 32.0), 146 | const CheckoutOrderDetails(), 147 | const SizedBox(height: 64.0), 148 | BlocConsumer( 149 | bloc: checkoutCubit, 150 | listenWhen: (previous, current) => 151 | current is PaymentMakingFailed || 152 | current is PaymentMade, 153 | listener: (context, state) { 154 | if (state is PaymentMakingFailed) { 155 | ScaffoldMessenger.of(context).showSnackBar( 156 | SnackBar( 157 | content: Text(state.error), 158 | backgroundColor: Colors.redAccent, 159 | ), 160 | ); 161 | } else if (state is PaymentMade) { 162 | Navigator.of(context).popUntil( 163 | (route) => route.isFirst, 164 | ); 165 | } 166 | }, 167 | buildWhen: (previous, current) => 168 | current is PaymentMade || 169 | current is PaymentMakingFailed || 170 | current is MakingPayment, 171 | builder: (context, state) { 172 | if (state is MakingPayment) { 173 | return MainButton( 174 | hasCircularBorder: true, 175 | child: const CircularProgressIndicator.adaptive(), 176 | ); 177 | } 178 | return MainButton( 179 | text: 'Submit Order', 180 | onTap: () async => 181 | await checkoutCubit.makePayment(300), 182 | hasCircularBorder: true, 183 | ); 184 | }, 185 | ), 186 | ], 187 | ), 188 | ), 189 | ); 190 | } else { 191 | return const SizedBox.shrink(); 192 | } 193 | }, 194 | ), 195 | ); 196 | } 197 | } 198 | -------------------------------------------------------------------------------- /lib/views/pages/checkout/payment_methods_page.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_bloc/flutter_bloc.dart'; 3 | import 'package:flutter_ecommerce/controllers/checkout/checkout_cubit.dart'; 4 | import 'package:flutter_ecommerce/models/payment_method.dart'; 5 | import 'package:flutter_ecommerce/views/widgets/checkout/add_new_card_bottom_sheet.dart'; 6 | import 'package:flutter_ecommerce/views/widgets/main_button.dart'; 7 | 8 | class PaymentMethodsPage extends StatelessWidget { 9 | const PaymentMethodsPage({super.key}); 10 | 11 | @override 12 | Widget build(BuildContext context) { 13 | final checkoutCubit = BlocProvider.of(context); 14 | 15 | Future showBottomSheet([PaymentMethod? paymentMethod]) async { 16 | showModalBottomSheet( 17 | context: context, 18 | isScrollControlled: true, 19 | builder: (_) { 20 | return BlocProvider.value( 21 | value: checkoutCubit, 22 | child: AddNewCardBottomSheet(paymentMethod: paymentMethod), 23 | ); 24 | }).then((value) => checkoutCubit.fetchCards()); 25 | } 26 | 27 | return Scaffold( 28 | appBar: AppBar( 29 | title: const Text('Payment Methods'), 30 | centerTitle: true, 31 | ), 32 | body: BlocConsumer( 33 | bloc: checkoutCubit, 34 | listenWhen: (previous, current) => 35 | current is PreferredMade || current is PreferredMakingFailed, 36 | listener: (context, state) { 37 | if (state is PreferredMade) { 38 | Navigator.of(context).pop(); 39 | } else if (state is PreferredMakingFailed) { 40 | ScaffoldMessenger.of(context).showSnackBar( 41 | SnackBar( 42 | content: Text(state.error), 43 | backgroundColor: Colors.red, 44 | ), 45 | ); 46 | } 47 | }, 48 | buildWhen: (previous, current) => 49 | current is FetchingCards || 50 | current is CardsFetched || 51 | current is CardsFetchingFailed, 52 | builder: (context, state) { 53 | if (state is FetchingCards) { 54 | return const Center( 55 | child: CircularProgressIndicator.adaptive(), 56 | ); 57 | } else if (state is CardsFetchingFailed) { 58 | return Center( 59 | child: Text(state.error), 60 | ); 61 | } else if (state is CardsFetched) { 62 | final paymentMethods = state.paymentMethods; 63 | 64 | return SingleChildScrollView( 65 | child: Padding( 66 | padding: const EdgeInsets.symmetric( 67 | horizontal: 16.0, 68 | vertical: 24.0, 69 | ), 70 | child: Column( 71 | crossAxisAlignment: CrossAxisAlignment.start, 72 | children: [ 73 | Text( 74 | 'Your payment cards', 75 | style: Theme.of(context).textTheme.titleLarge, 76 | ), 77 | const SizedBox(height: 16.0), 78 | ListView.builder( 79 | shrinkWrap: true, 80 | physics: const NeverScrollableScrollPhysics(), 81 | itemCount: paymentMethods.length, 82 | itemBuilder: (context, index) { 83 | final paymentMethod = paymentMethods[index]; 84 | 85 | return Padding( 86 | padding: const EdgeInsets.only(bottom: 4), 87 | child: InkWell( 88 | onTap: () async { 89 | await checkoutCubit.makePreferred(paymentMethod); 90 | }, 91 | child: Card( 92 | child: Padding( 93 | padding: const EdgeInsets.symmetric( 94 | horizontal: 12.0, 95 | vertical: 8.0, 96 | ), 97 | child: Row( 98 | mainAxisAlignment: 99 | MainAxisAlignment.spaceBetween, 100 | children: [ 101 | Row( 102 | children: [ 103 | const Icon(Icons.credit_card), 104 | const SizedBox(width: 8.0), 105 | Text(paymentMethod.cardNumber), 106 | ], 107 | ), 108 | Row( 109 | children: [ 110 | IconButton( 111 | icon: const Icon(Icons.edit), 112 | onPressed: () { 113 | showBottomSheet(paymentMethod); 114 | }, 115 | ), 116 | BlocBuilder( 118 | bloc: checkoutCubit, 119 | buildWhen: (previous, current) => 120 | (current is DeletingCards && 121 | current.paymentId == 122 | paymentMethod.id) || 123 | current is CardsDeleted || 124 | current is CardsDeletingFailed, 125 | builder: (context, state) { 126 | if (state is DeletingCards) { 127 | return const CircularProgressIndicator 128 | .adaptive(); 129 | } 130 | return IconButton( 131 | icon: const Icon(Icons.delete), 132 | onPressed: () async { 133 | await checkoutCubit 134 | .deleteCard(paymentMethod); 135 | }, 136 | ); 137 | }, 138 | ), 139 | ], 140 | ), 141 | ], 142 | ), 143 | ), 144 | ), 145 | ), 146 | ); 147 | }, 148 | ), 149 | const SizedBox(height: 16.0), 150 | MainButton( 151 | onTap: () { 152 | showBottomSheet(); 153 | }, 154 | text: 'Add New Card', 155 | ), 156 | ], 157 | ), 158 | ), 159 | ); 160 | } else { 161 | return const SizedBox(); 162 | } 163 | }, 164 | ), 165 | ); 166 | } 167 | } 168 | -------------------------------------------------------------------------------- /lib/views/pages/checkout/shipping_addresses_page.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_bloc/flutter_bloc.dart'; 3 | import 'package:flutter_ecommerce/controllers/checkout/checkout_cubit.dart'; 4 | import 'package:flutter_ecommerce/utilities/args_models/add_shipping_address_args.dart'; 5 | import 'package:flutter_ecommerce/utilities/routes.dart'; 6 | import 'package:flutter_ecommerce/views/widgets/checkout/shipping_address_state_item.dart'; 7 | 8 | class ShippingAddressesPage extends StatefulWidget { 9 | const ShippingAddressesPage({super.key}); 10 | 11 | @override 12 | State createState() => _ShippingAddressesPageState(); 13 | } 14 | 15 | class _ShippingAddressesPageState extends State { 16 | @override 17 | void initState() { 18 | super.initState(); 19 | BlocProvider.of(context).getShippingAddresses(); 20 | } 21 | 22 | @override 23 | Widget build(BuildContext context) { 24 | final checkoutCubit = BlocProvider.of(context); 25 | 26 | return Scaffold( 27 | appBar: AppBar( 28 | title: Text( 29 | 'Shipping Addresses', 30 | style: Theme.of(context).textTheme.titleLarge, 31 | ), 32 | centerTitle: true, 33 | ), 34 | body: SingleChildScrollView( 35 | child: Padding( 36 | padding: const EdgeInsets.symmetric(horizontal: 8.0, vertical: 16.0), 37 | child: BlocBuilder( 38 | bloc: checkoutCubit, 39 | buildWhen: (previous, current) => 40 | current is FetchingAddresses || 41 | current is AddressesFetched || 42 | current is AddressesFetchingFailed, 43 | builder: (context, state) { 44 | if (state is FetchingAddresses) { 45 | return const Center( 46 | child: CircularProgressIndicator.adaptive(), 47 | ); 48 | } else if (state is AddressesFetchingFailed) { 49 | return Center( 50 | child: Text(state.error), 51 | ); 52 | } else if (state is AddressesFetched) { 53 | final shippingAddresses = state.shippingAddresses; 54 | 55 | return Column( 56 | children: shippingAddresses 57 | .map( 58 | (shippingAddress) => ShippingAddressStateItem( 59 | shippingAddress: shippingAddress, 60 | ), 61 | ) 62 | .toList(), 63 | ); 64 | } else { 65 | return const SizedBox.shrink(); 66 | } 67 | }, 68 | ), 69 | ), 70 | ), 71 | floatingActionButton: FloatingActionButton( 72 | onPressed: () => Navigator.of(context).pushNamed( 73 | AppRoutes.addShippingAddressRoute, 74 | arguments: AddShippingAddressArgs( 75 | checkoutCubit: checkoutCubit, 76 | ), 77 | ), 78 | backgroundColor: Colors.black, 79 | child: const Icon(Icons.add), 80 | ), 81 | ); 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /lib/views/pages/home_page.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_bloc/flutter_bloc.dart'; 3 | import 'package:flutter_ecommerce/controllers/home/home_cubit.dart'; 4 | import 'package:flutter_ecommerce/utilities/assets.dart'; 5 | import 'package:flutter_ecommerce/views/widgets/header_of_list.dart'; 6 | import 'package:flutter_ecommerce/views/widgets/list_item_home.dart'; 7 | 8 | class HomePage extends StatelessWidget { 9 | const HomePage({super.key}); 10 | 11 | @override 12 | Widget build(BuildContext context) { 13 | final size = MediaQuery.of(context).size; 14 | final homeCubit = BlocProvider.of(context); 15 | 16 | return SafeArea( 17 | top: false, 18 | child: BlocBuilder( 19 | bloc: homeCubit, 20 | buildWhen: (previous, current) => 21 | current is HomeSuccess || 22 | current is HomeLoading || 23 | current is HomeFailed, 24 | builder: (context, state) { 25 | if (state is HomeLoading) { 26 | return const CircularProgressIndicator.adaptive(); 27 | } else if (state is HomeFailed) { 28 | return Center( 29 | child: Text(state.error), 30 | ); 31 | } else if (state is HomeSuccess) { 32 | final salesProducts = state.salesProducts; 33 | final newProducts = state.newProducts; 34 | 35 | return SingleChildScrollView( 36 | child: Column( 37 | crossAxisAlignment: CrossAxisAlignment.start, 38 | children: [ 39 | Stack( 40 | alignment: Alignment.bottomLeft, 41 | children: [ 42 | Image.network( 43 | AppAssets.topBannerHomePageAsset, 44 | width: double.infinity, 45 | height: size.height * 0.3, 46 | fit: BoxFit.cover, 47 | ), 48 | Opacity( 49 | opacity: 0.3, 50 | child: Container( 51 | width: double.infinity, 52 | height: size.height * 0.3, 53 | color: Colors.black, 54 | ), 55 | ), 56 | Padding( 57 | padding: const EdgeInsets.symmetric( 58 | horizontal: 24.0, 59 | vertical: 16.0, 60 | ), 61 | child: Text( 62 | 'Street Clothes', 63 | style: 64 | Theme.of(context).textTheme.titleLarge!.copyWith( 65 | color: Colors.white, 66 | fontWeight: FontWeight.bold, 67 | ), 68 | ), 69 | ), 70 | ], 71 | ), 72 | const SizedBox(height: 24.0), 73 | Padding( 74 | padding: const EdgeInsets.symmetric(horizontal: 24.0), 75 | child: Column( 76 | children: [ 77 | HeaderOfList( 78 | onTap: () {}, 79 | title: 'Sale', 80 | description: 'Super Summer Sale!!', 81 | ), 82 | const SizedBox(height: 8.0), 83 | SizedBox( 84 | height: 330, 85 | child: ListView.builder( 86 | scrollDirection: Axis.horizontal, 87 | itemCount: salesProducts.length, 88 | itemBuilder: (_, int index) => Padding( 89 | padding: const EdgeInsets.all(8.0), 90 | child: ListItemHome( 91 | product: salesProducts[index], 92 | isNew: true, 93 | ), 94 | ), 95 | ), 96 | ), 97 | const SizedBox(height: 12.0), 98 | HeaderOfList( 99 | onTap: () {}, 100 | title: 'New', 101 | description: 'Super New Products!!', 102 | ), 103 | const SizedBox(height: 8.0), 104 | SizedBox( 105 | height: 330, 106 | child: ListView.builder( 107 | scrollDirection: Axis.horizontal, 108 | itemCount: newProducts.length, 109 | itemBuilder: (_, int index) => Padding( 110 | padding: const EdgeInsets.all(8.0), 111 | child: ListItemHome( 112 | product: newProducts[index], 113 | isNew: true, 114 | ), 115 | ), 116 | ), 117 | ), 118 | ], 119 | ), 120 | ), 121 | ], 122 | ), 123 | ); 124 | } else { 125 | return const SizedBox.shrink(); 126 | } 127 | }, 128 | ), 129 | ); 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /lib/views/pages/product_details.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_bloc/flutter_bloc.dart'; 3 | import 'package:flutter_ecommerce/controllers/product_details/product_details_cubit.dart'; 4 | import 'package:flutter_ecommerce/views/widgets/drop_down_menu.dart'; 5 | import 'package:flutter_ecommerce/views/widgets/main_button.dart'; 6 | 7 | class ProductDetails extends StatefulWidget { 8 | const ProductDetails({ 9 | super.key, 10 | }); 11 | 12 | @override 13 | State createState() => _ProductDetailsState(); 14 | } 15 | 16 | class _ProductDetailsState extends State { 17 | bool isFavorite = false; 18 | late String dropdownValue; 19 | 20 | // Future _addToCart(Database database) async { 21 | // try { 22 | // final addToCartProduct = AddToCartModel( 23 | // id: documentIdFromLocalData(), 24 | // title: widget.product.title, 25 | // price: widget.product.price, 26 | // productId: widget.product.id, 27 | // imgUrl: widget.product.imgUrl, 28 | // size: dropdownValue, 29 | // ); 30 | // await database.addToCart(addToCartProduct); 31 | // } catch (e) { 32 | // return MainDialog( 33 | // context: context, 34 | // title: 'Error', 35 | // content: 'Couldn\'t adding to the cart, please try again!', 36 | // ).showAlertDialog(); 37 | // } 38 | // } 39 | 40 | @override 41 | Widget build(BuildContext context) { 42 | final size = MediaQuery.of(context).size; 43 | final productDetailsCubit = BlocProvider.of(context); 44 | 45 | return BlocBuilder( 46 | bloc: productDetailsCubit, 47 | buildWhen: (previous, current) => 48 | current is ProductDetailsLoading || 49 | current is ProductDetailsLoaded || 50 | current is ProductDetailsError, 51 | builder: (context, state) { 52 | if (state is ProductDetailsLoading) { 53 | return const Scaffold( 54 | body: Center( 55 | child: CircularProgressIndicator.adaptive(), 56 | ), 57 | ); 58 | } else if (state is ProductDetailsError) { 59 | return Scaffold( 60 | body: Center( 61 | child: Text(state.error), 62 | ), 63 | ); 64 | } else if (state is ProductDetailsLoaded) { 65 | final product = state.product; 66 | return Scaffold( 67 | appBar: AppBar( 68 | title: Text( 69 | product.title, 70 | style: Theme.of(context).textTheme.titleLarge, 71 | ), 72 | actions: [ 73 | IconButton( 74 | onPressed: () {}, 75 | icon: const Icon( 76 | Icons.share, 77 | ), 78 | ), 79 | ], 80 | ), 81 | body: SingleChildScrollView( 82 | child: Column( 83 | children: [ 84 | Image.network( 85 | product.imgUrl, 86 | width: double.infinity, 87 | height: size.height * 0.55, 88 | fit: BoxFit.cover, 89 | ), 90 | const SizedBox(height: 8.0), 91 | Padding( 92 | padding: const EdgeInsets.symmetric( 93 | horizontal: 16.0, 94 | vertical: 8.0, 95 | ), 96 | child: Column( 97 | crossAxisAlignment: CrossAxisAlignment.start, 98 | children: [ 99 | Row( 100 | mainAxisAlignment: MainAxisAlignment.spaceBetween, 101 | children: [ 102 | Expanded( 103 | child: SizedBox( 104 | height: 60, 105 | child: DropDownMenuComponent( 106 | items: const ['S', 'M', 'L', 'XL', 'XXL'], 107 | hint: 'Size', 108 | onChanged: (String? newValue) => productDetailsCubit.setSize(newValue!), 109 | ), 110 | ), 111 | ), 112 | const Spacer(), 113 | // TODO: Create one component for the favorite button 114 | InkWell( 115 | onTap: () { 116 | setState(() { 117 | isFavorite = !isFavorite; 118 | }); 119 | }, 120 | child: SizedBox( 121 | height: 60, 122 | width: 60, 123 | child: DecoratedBox( 124 | decoration: const BoxDecoration( 125 | shape: BoxShape.circle, 126 | color: Colors.white, 127 | ), 128 | child: Padding( 129 | padding: const EdgeInsets.all(8.0), 130 | child: Icon( 131 | isFavorite 132 | ? Icons.favorite 133 | : Icons.favorite_border_outlined, 134 | color: isFavorite 135 | ? Colors.redAccent 136 | : Colors.black45, 137 | size: 30, 138 | ), 139 | ), 140 | ), 141 | ), 142 | ), 143 | ], 144 | ), 145 | const SizedBox(height: 24.0), 146 | Row( 147 | mainAxisAlignment: MainAxisAlignment.spaceBetween, 148 | children: [ 149 | Text( 150 | product.title, 151 | style: Theme.of(context) 152 | .textTheme 153 | .titleLarge! 154 | .copyWith( 155 | fontWeight: FontWeight.w600, 156 | ), 157 | ), 158 | Text( 159 | '\$${product.price}', 160 | style: Theme.of(context) 161 | .textTheme 162 | .titleLarge! 163 | .copyWith( 164 | fontWeight: FontWeight.w600, 165 | ), 166 | ), 167 | ], 168 | ), 169 | const SizedBox(height: 8.0), 170 | Text( 171 | product.category, 172 | style: 173 | Theme.of(context).textTheme.labelMedium!.copyWith( 174 | color: Colors.black54, 175 | ), 176 | ), 177 | const SizedBox(height: 16.0), 178 | Text( 179 | 'This is a dummy description for this product! I think we will add it in the future! I need to add more lines, so I add these words just to have more than two lines!', 180 | style: Theme.of(context).textTheme.bodyLarge, 181 | ), 182 | const SizedBox(height: 24.0), 183 | BlocConsumer( 184 | bloc: productDetailsCubit, 185 | listenWhen: (previous, current) => 186 | current is AddedToCart || 187 | current is AddToCartError, 188 | listener: (context, state) { 189 | if (state is AddedToCart) { 190 | ScaffoldMessenger.of(context).showSnackBar( 191 | const SnackBar( 192 | content: Text('Product added to the cart!'), 193 | ), 194 | ); 195 | } else if (state is AddToCartError) { 196 | ScaffoldMessenger.of(context).showSnackBar( 197 | SnackBar( 198 | content: Text(state.error), 199 | ), 200 | ); 201 | } 202 | }, 203 | builder: (context, state) { 204 | if (state is AddingToCart) { 205 | return MainButton( 206 | child: const CircularProgressIndicator.adaptive(), 207 | ); 208 | } 209 | return MainButton( 210 | text: 'Add to cart', 211 | onTap: () async => 212 | await productDetailsCubit.addToCart(product), 213 | hasCircularBorder: true, 214 | ); 215 | }, 216 | ), 217 | const SizedBox(height: 32.0), 218 | ], 219 | ), 220 | ), 221 | ], 222 | ), 223 | ), 224 | ); 225 | } else { 226 | return const SizedBox.shrink(); 227 | } 228 | }, 229 | ); 230 | } 231 | } 232 | -------------------------------------------------------------------------------- /lib/views/pages/profle_page.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_bloc/flutter_bloc.dart'; 3 | import 'package:flutter_ecommerce/controllers/auth/auth_cubit.dart'; 4 | import 'package:flutter_ecommerce/utilities/routes.dart'; 5 | import 'package:flutter_ecommerce/views/widgets/main_button.dart'; 6 | 7 | class ProfilePage extends StatelessWidget { 8 | const ProfilePage({super.key}); 9 | 10 | @override 11 | Widget build(BuildContext context) { 12 | final authCubit = BlocProvider.of(context); 13 | 14 | return SafeArea( 15 | child: Column( 16 | children: [ 17 | Padding( 18 | padding: const EdgeInsets.symmetric(horizontal: 10.0, vertical: 20), 19 | child: BlocConsumer( 20 | bloc: authCubit, 21 | listenWhen: (previous, current) => 22 | current is AuthFailed || current is AuthInitial, 23 | listener: (context, state) { 24 | if (state is AuthFailed) { 25 | ScaffoldMessenger.of(context).showSnackBar( 26 | SnackBar( 27 | content: Text(state.error), 28 | ), 29 | ); 30 | } else if (state is AuthInitial) { 31 | Navigator.of(context, rootNavigator: true) 32 | .pushReplacementNamed(AppRoutes.loginPageRoute); 33 | } 34 | }, 35 | buildWhen: (previous, current) => 36 | current is AuthLoading || 37 | current is AuthInitial || 38 | current is AuthFailed, 39 | builder: (context, state) { 40 | if (state is AuthLoading) { 41 | return MainButton( 42 | child: const CircularProgressIndicator.adaptive(), 43 | ); 44 | } 45 | return MainButton( 46 | text: 'Log Out', 47 | onTap: () async { 48 | await authCubit.logout(); 49 | }, 50 | ); 51 | }, 52 | ), 53 | ) 54 | ], 55 | ), 56 | ); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /lib/views/widgets/cart_list_item.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_ecommerce/models/add_to_cart_model.dart'; 3 | 4 | class CartListItem extends StatelessWidget { 5 | final AddToCartModel cartItem; 6 | const CartListItem({ 7 | Key? key, 8 | required this.cartItem, 9 | }) : super(key: key); 10 | 11 | @override 12 | Widget build(BuildContext context) { 13 | return SizedBox( 14 | height: 150, 15 | child: Card( 16 | shape: RoundedRectangleBorder( 17 | borderRadius: BorderRadius.circular(16.0), 18 | ), 19 | child: Row( 20 | crossAxisAlignment: CrossAxisAlignment.start, 21 | mainAxisAlignment: MainAxisAlignment.spaceBetween, 22 | children: [ 23 | ClipRRect( 24 | borderRadius: const BorderRadius.only( 25 | topLeft: Radius.circular(16.0), 26 | bottomLeft: Radius.circular(16.0), 27 | ), 28 | child: Image.network( 29 | cartItem.imgUrl, 30 | fit: BoxFit.cover, 31 | ), 32 | ), 33 | Expanded( 34 | child: Padding( 35 | padding: const EdgeInsets.symmetric(vertical: 12.0), 36 | child: Column( 37 | crossAxisAlignment: CrossAxisAlignment.start, 38 | mainAxisAlignment: MainAxisAlignment.start, 39 | children: [ 40 | Text( 41 | cartItem.title, 42 | style: Theme.of(context).textTheme.titleLarge!.copyWith( 43 | fontWeight: FontWeight.w600, 44 | ), 45 | ), 46 | const SizedBox(height: 4.0), 47 | Row( 48 | children: [ 49 | Text.rich( 50 | TextSpan( 51 | children: [ 52 | TextSpan( 53 | text: 'Color: ', 54 | style: Theme.of(context) 55 | .textTheme 56 | .labelSmall! 57 | .copyWith( 58 | color: Colors.grey, 59 | )), 60 | TextSpan( 61 | text: cartItem.color, 62 | style: Theme.of(context) 63 | .textTheme 64 | .labelSmall! 65 | .copyWith( 66 | color: Colors.black, 67 | ), 68 | ), 69 | ], 70 | ), 71 | ), 72 | const SizedBox(width: 8.0), 73 | Text.rich( 74 | TextSpan( 75 | children: [ 76 | TextSpan( 77 | text: 'Size: ', 78 | style: Theme.of(context) 79 | .textTheme 80 | .labelSmall! 81 | .copyWith( 82 | color: Colors.grey, 83 | )), 84 | TextSpan( 85 | text: cartItem.size, 86 | style: Theme.of(context) 87 | .textTheme 88 | .labelSmall! 89 | .copyWith( 90 | color: Colors.black, 91 | ), 92 | ), 93 | ], 94 | ), 95 | ), 96 | ], 97 | ), 98 | ], 99 | ), 100 | ), 101 | ), 102 | Padding( 103 | padding: 104 | const EdgeInsets.symmetric(vertical: 12.0, horizontal: 8.0), 105 | child: Column( 106 | mainAxisSize: MainAxisSize.max, 107 | mainAxisAlignment: MainAxisAlignment.spaceBetween, 108 | crossAxisAlignment: CrossAxisAlignment.end, 109 | children: [ 110 | const Icon(Icons.more_vert), 111 | Text( 112 | '${cartItem.price}\$', 113 | style: Theme.of(context).textTheme.bodyLarge, 114 | ), 115 | ], 116 | ), 117 | ), 118 | ], 119 | ), 120 | ), 121 | ); 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /lib/views/widgets/checkout/add_new_card_bottom_sheet.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_bloc/flutter_bloc.dart'; 3 | import 'package:flutter_ecommerce/controllers/checkout/checkout_cubit.dart'; 4 | import 'package:flutter_ecommerce/models/payment_method.dart'; 5 | import 'package:flutter_ecommerce/views/widgets/main_button.dart'; 6 | 7 | class AddNewCardBottomSheet extends StatefulWidget { 8 | final PaymentMethod? paymentMethod; 9 | const AddNewCardBottomSheet({super.key, this.paymentMethod,}); 10 | 11 | @override 12 | State createState() => _AddNewCardBottomSheetState(); 13 | } 14 | 15 | class _AddNewCardBottomSheetState extends State { 16 | late final GlobalKey _formKey; 17 | late final TextEditingController _nameOnCardController, 18 | _expireDateController, 19 | _cardNumberController, 20 | _cvvController; 21 | 22 | @override 23 | void initState() { 24 | super.initState(); 25 | _formKey = GlobalKey(); 26 | _nameOnCardController = TextEditingController(); 27 | _expireDateController = TextEditingController(); 28 | _cardNumberController = TextEditingController(); 29 | _cvvController = TextEditingController(); 30 | 31 | if (widget.paymentMethod != null) { 32 | _nameOnCardController.text = widget.paymentMethod!.name; 33 | _expireDateController.text = widget.paymentMethod!.expiryDate; 34 | _cardNumberController.text = widget.paymentMethod!.cardNumber; 35 | _cvvController.text = widget.paymentMethod!.cvv; 36 | } 37 | } 38 | 39 | @override 40 | Widget build(BuildContext context) { 41 | final size = MediaQuery.of(context).size; 42 | final checkoutCubit = BlocProvider.of(context); 43 | 44 | return SizedBox( 45 | height: size.height * 0.7, 46 | child: Form( 47 | key: _formKey, 48 | child: Column( 49 | children: [ 50 | const SizedBox(height: 24.0), 51 | Text( 52 | 'Add New Card', 53 | style: Theme.of(context).textTheme.titleLarge, 54 | ), 55 | const SizedBox(height: 16.0), 56 | Padding( 57 | padding: const EdgeInsets.symmetric( 58 | horizontal: 16.0, 59 | ), 60 | child: TextFormField( 61 | controller: _nameOnCardController, 62 | keyboardType: TextInputType.name, 63 | validator: (value) => value != null && value.isEmpty 64 | ? 'Please enter your name' 65 | : null, 66 | decoration: const InputDecoration( 67 | labelText: 'Name on Card', 68 | border: OutlineInputBorder(), 69 | ), 70 | ), 71 | ), 72 | const SizedBox(height: 16.0), 73 | Padding( 74 | padding: const EdgeInsets.symmetric( 75 | horizontal: 16.0, 76 | ), 77 | child: TextFormField( 78 | controller: _cardNumberController, 79 | validator: (value) => value != null && value.isEmpty 80 | ? 'Please enter your card number' 81 | : null, 82 | keyboardType: TextInputType.number, 83 | onChanged: (value) { 84 | String newValue = value.replaceAll('-', ''); 85 | if (newValue.length % 4 == 0 && newValue.length < 16) { 86 | _cardNumberController.text += '-'; 87 | } 88 | if (value.length >= 20) { 89 | _cardNumberController.text = value.substring(0, 19); 90 | } 91 | }, 92 | decoration: const InputDecoration( 93 | labelText: 'Card Number', 94 | border: OutlineInputBorder(), 95 | ), 96 | ), 97 | ), 98 | const SizedBox(height: 16.0), 99 | Padding( 100 | padding: const EdgeInsets.symmetric( 101 | horizontal: 16.0, 102 | ), 103 | child: TextFormField( 104 | controller: _expireDateController, 105 | validator: (value) => value != null && value.isEmpty 106 | ? 'Please enter your expire date' 107 | : null, 108 | keyboardType: TextInputType.datetime, 109 | onChanged: (value) { 110 | if (value.length == 2 && !value.contains('/')) { 111 | _expireDateController.text += '/'; 112 | } 113 | if (value.length == 6 && value.contains('/')) { 114 | _expireDateController.text = value.substring(0, 5); 115 | } 116 | }, 117 | decoration: const InputDecoration( 118 | labelText: 'Expire Date', 119 | border: OutlineInputBorder(), 120 | ), 121 | ), 122 | ), 123 | const SizedBox(height: 16.0), 124 | Padding( 125 | padding: const EdgeInsets.symmetric( 126 | horizontal: 16.0, 127 | ), 128 | child: TextFormField( 129 | controller: _cvvController, 130 | validator: (value) => value != null && value.isEmpty 131 | ? 'Please enter your CVV' 132 | : null, 133 | onChanged: (value) { 134 | if (value.length >= 3) { 135 | _cvvController.text = value.substring(0, 3); 136 | } 137 | }, 138 | keyboardType: TextInputType.number, 139 | decoration: const InputDecoration( 140 | labelText: 'CVV', 141 | border: OutlineInputBorder(), 142 | ), 143 | ), 144 | ), 145 | const SizedBox(height: 36.0), 146 | Padding( 147 | padding: const EdgeInsets.symmetric( 148 | horizontal: 16.0, 149 | ), 150 | child: BlocConsumer( 151 | bloc: checkoutCubit, 152 | listenWhen: (previous, current) => 153 | current is CardsAdded || current is CardsAddingFailed, 154 | listener: (context, state) { 155 | if (state is CardsAdded) { 156 | Navigator.pop(context); 157 | } else if (state is CardsAddingFailed) { 158 | ScaffoldMessenger.of(context).showSnackBar( 159 | SnackBar( 160 | content: Text(state.error), 161 | ), 162 | ); 163 | } 164 | }, 165 | buildWhen: (previous, current) => 166 | current is AddingCards || 167 | current is CardsAdded || 168 | current is CardsAddingFailed, 169 | builder: (context, state) { 170 | if (state is AddingCards) { 171 | return MainButton( 172 | onTap: null, 173 | child: const CircularProgressIndicator.adaptive(), 174 | ); 175 | } 176 | return MainButton( 177 | onTap: () async { 178 | if (_formKey.currentState!.validate()) { 179 | final paymentMethod = PaymentMethod( 180 | id: widget.paymentMethod != null ? widget.paymentMethod!.id : DateTime.now().toIso8601String(), 181 | name: _nameOnCardController.text, 182 | cardNumber: _cardNumberController.text, 183 | expiryDate: _expireDateController.text, 184 | cvv: _cvvController.text, 185 | ); 186 | await checkoutCubit.addCard(paymentMethod); 187 | } 188 | }, 189 | text: widget.paymentMethod != null ? 'Edit Card' : 'Add Card', 190 | ); 191 | }, 192 | ), 193 | ), 194 | ], 195 | ), 196 | ), 197 | ); 198 | } 199 | } 200 | -------------------------------------------------------------------------------- /lib/views/widgets/checkout/checkout_order_details.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_ecommerce/views/widgets/order_summary_component.dart'; 3 | 4 | class CheckoutOrderDetails extends StatelessWidget { 5 | const CheckoutOrderDetails({Key? key}) : super(key: key); 6 | 7 | @override 8 | Widget build(BuildContext context) { 9 | return Column( 10 | children: const [ 11 | OrderSummaryComponent(title: 'Order', value: '125\$'), 12 | SizedBox(height: 8.0), 13 | OrderSummaryComponent(title: 'Delivery', value: '15\$'), 14 | SizedBox(height: 8.0), 15 | OrderSummaryComponent(title: 'Summary', value: '140\$'), 16 | ], 17 | ); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /lib/views/widgets/checkout/delivery_method_item.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_ecommerce/models/delivery_method.dart'; 3 | 4 | class DeliveryMethodItem extends StatelessWidget { 5 | final DeliveryMethod deliveryMethod; 6 | const DeliveryMethodItem({ 7 | Key? key, 8 | required this.deliveryMethod, 9 | }) : super(key: key); 10 | 11 | @override 12 | Widget build(BuildContext context) { 13 | return DecoratedBox( 14 | decoration: BoxDecoration( 15 | borderRadius: BorderRadius.circular(8.0), 16 | color: Colors.white, 17 | ), 18 | child: Padding( 19 | padding: const EdgeInsets.all(8.0), 20 | child: Column(mainAxisAlignment: MainAxisAlignment.center, children: [ 21 | Image.network( 22 | deliveryMethod.imgUrl, 23 | fit: BoxFit.cover, 24 | height: 30, 25 | ), 26 | const SizedBox(height: 6.0), 27 | Text( 28 | '${deliveryMethod.days} days', 29 | style: Theme.of(context).textTheme.bodyLarge, 30 | ), 31 | ]), 32 | ), 33 | ); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /lib/views/widgets/checkout/payment_component.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_ecommerce/utilities/assets.dart'; 3 | 4 | class PaymentComponent extends StatelessWidget { 5 | const PaymentComponent({Key? key}) : super(key: key); 6 | 7 | @override 8 | Widget build(BuildContext context) { 9 | return Row( 10 | children: [ 11 | DecoratedBox( 12 | decoration: BoxDecoration( 13 | color: Colors.white, borderRadius: BorderRadius.circular(16.0)), 14 | child: Padding( 15 | padding: const EdgeInsets.all(4.0), 16 | child: Image.network( 17 | AppAssets.mastercardIcon, 18 | fit: BoxFit.cover, 19 | height: 30, 20 | ), 21 | ), 22 | ), 23 | const SizedBox(width: 16.0), 24 | Text('**** **** **** 2718'), 25 | ], 26 | ); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /lib/views/widgets/checkout/shipping_address_component.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_ecommerce/controllers/checkout/checkout_cubit.dart'; 3 | import 'package:flutter_ecommerce/controllers/database_controller.dart'; 4 | import 'package:flutter_ecommerce/models/shipping_address.dart'; 5 | import 'package:flutter_ecommerce/utilities/routes.dart'; 6 | 7 | class ShippingAddressComponent extends StatelessWidget { 8 | final ShippingAddress shippingAddress; 9 | final CheckoutCubit checkoutCubit; 10 | const ShippingAddressComponent({ 11 | super.key, 12 | required this.shippingAddress, 13 | required this.checkoutCubit, 14 | }); 15 | 16 | @override 17 | Widget build(BuildContext context) { 18 | return Card( 19 | shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12.0)), 20 | child: Padding( 21 | padding: const EdgeInsets.all(16.0), 22 | child: Column( 23 | crossAxisAlignment: CrossAxisAlignment.start, 24 | children: [ 25 | Row( 26 | mainAxisAlignment: MainAxisAlignment.spaceBetween, 27 | children: [ 28 | Text( 29 | shippingAddress.fullName, 30 | style: Theme.of(context).textTheme.labelMedium!.copyWith( 31 | fontWeight: FontWeight.w600, 32 | ), 33 | ), 34 | InkWell( 35 | onTap: () => Navigator.of(context).pushNamed( 36 | AppRoutes.shippingAddressesRoute, 37 | arguments: checkoutCubit, 38 | ), 39 | child: Text( 40 | 'Change', 41 | style: Theme.of(context).textTheme.labelSmall!.copyWith( 42 | color: Colors.redAccent, 43 | ), 44 | ), 45 | ), 46 | ], 47 | ), 48 | const SizedBox(height: 8.0), 49 | Text( 50 | shippingAddress.address, 51 | style: Theme.of(context).textTheme.labelMedium, 52 | ), 53 | Text( 54 | '${shippingAddress.city}, ${shippingAddress.state}, ${shippingAddress.country}', 55 | style: Theme.of(context).textTheme.labelMedium, 56 | ), 57 | ], 58 | ), 59 | ), 60 | ); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /lib/views/widgets/checkout/shipping_address_state_item.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_bloc/flutter_bloc.dart'; 3 | import 'package:flutter_ecommerce/controllers/checkout/checkout_cubit.dart'; 4 | import 'package:flutter_ecommerce/controllers/database_controller.dart'; 5 | import 'package:flutter_ecommerce/models/shipping_address.dart'; 6 | import 'package:flutter_ecommerce/utilities/args_models/add_shipping_address_args.dart'; 7 | import 'package:flutter_ecommerce/utilities/routes.dart'; 8 | import 'package:provider/provider.dart'; 9 | 10 | class ShippingAddressStateItem extends StatefulWidget { 11 | final ShippingAddress shippingAddress; 12 | const ShippingAddressStateItem({ 13 | super.key, 14 | required this.shippingAddress, 15 | }); 16 | 17 | @override 18 | State createState() => 19 | _ShippingAddressStateItemState(); 20 | } 21 | 22 | class _ShippingAddressStateItemState extends State { 23 | late bool checkedValue; 24 | 25 | @override 26 | void initState() { 27 | super.initState(); 28 | checkedValue = widget.shippingAddress.isDefault; 29 | } 30 | 31 | @override 32 | Widget build(BuildContext context) { 33 | final checkoutCubit = BlocProvider.of(context); 34 | 35 | return Card( 36 | shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12.0)), 37 | child: Padding( 38 | padding: const EdgeInsets.all(16.0), 39 | child: Column( 40 | crossAxisAlignment: CrossAxisAlignment.start, 41 | children: [ 42 | Row( 43 | mainAxisAlignment: MainAxisAlignment.spaceBetween, 44 | children: [ 45 | Text( 46 | widget.shippingAddress.fullName, 47 | style: Theme.of(context).textTheme.labelMedium!.copyWith( 48 | fontWeight: FontWeight.w600, 49 | ), 50 | ), 51 | InkWell( 52 | onTap: () => Navigator.of(context).pushNamed( 53 | AppRoutes.addShippingAddressRoute, 54 | arguments: AddShippingAddressArgs( 55 | shippingAddress: widget.shippingAddress, 56 | checkoutCubit: checkoutCubit 57 | ), 58 | ), 59 | child: Text( 60 | 'Edit', 61 | style: Theme.of(context).textTheme.labelSmall!.copyWith( 62 | color: Colors.redAccent, 63 | ), 64 | ), 65 | ), 66 | ], 67 | ), 68 | const SizedBox(height: 8.0), 69 | Text( 70 | widget.shippingAddress.address, 71 | style: Theme.of(context).textTheme.labelMedium, 72 | ), 73 | Text( 74 | '${widget.shippingAddress.city}, ${widget.shippingAddress.state}, ${widget.shippingAddress.country}', 75 | style: Theme.of(context).textTheme.labelMedium, 76 | ), 77 | CheckboxListTile( 78 | title: const Text("Default shipping address"), 79 | value: checkedValue, 80 | onChanged: (newValue) async { 81 | setState(() { 82 | checkedValue = newValue!; 83 | }); 84 | // TODO: We need to add the business logic of adding the default address (one default) 85 | final newAddress = 86 | widget.shippingAddress.copyWith(isDefault: newValue); 87 | await checkoutCubit.saveAddress(newAddress); 88 | }, 89 | activeColor: Colors.black, 90 | contentPadding: EdgeInsets.zero, 91 | controlAffinity: 92 | ListTileControlAffinity.leading, // <-- leading Checkbox 93 | ) 94 | ], 95 | ), 96 | ), 97 | ); 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /lib/views/widgets/drop_down_menu.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class DropDownMenuComponent extends StatelessWidget { 4 | final void Function(String? value) onChanged; 5 | final List items; 6 | final String hint; 7 | const DropDownMenuComponent({ 8 | Key? key, 9 | required this.onChanged, 10 | required this.items, 11 | required this.hint, 12 | }) : super(key: key); 13 | 14 | @override 15 | Widget build(BuildContext context) { 16 | return DropdownButtonFormField( 17 | value: null, 18 | icon: const Icon(Icons.arrow_drop_down), 19 | isExpanded: true, 20 | elevation: 16, 21 | style: Theme.of(context).textTheme.titleLarge, 22 | hint: FittedBox( 23 | child: Text(hint), 24 | ), 25 | onChanged: onChanged, 26 | items: items.map>((String value) { 27 | return DropdownMenuItem( 28 | value: value, 29 | child: Text(value), 30 | ); 31 | }).toList(), 32 | ); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /lib/views/widgets/header_of_list.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class HeaderOfList extends StatelessWidget { 4 | final String title; 5 | final VoidCallback? onTap; 6 | final String description; 7 | const HeaderOfList({ 8 | Key? key, 9 | required this.title, 10 | required this.description, 11 | this.onTap, 12 | }) : super(key: key); 13 | 14 | @override 15 | Widget build(BuildContext context) { 16 | return Column( 17 | crossAxisAlignment: CrossAxisAlignment.start, 18 | children: [ 19 | Row( 20 | mainAxisAlignment: MainAxisAlignment.spaceBetween, 21 | children: [ 22 | Text( 23 | title, 24 | style: Theme.of(context).textTheme.titleLarge!.copyWith( 25 | fontWeight: FontWeight.bold, 26 | color: Colors.black, 27 | ), 28 | ), 29 | InkWell( 30 | onTap: onTap, 31 | child: Text( 32 | 'View All', 33 | style: Theme.of(context).textTheme.labelMedium, 34 | ), 35 | ), 36 | ], 37 | ), 38 | Text( 39 | description, 40 | style: Theme.of(context).textTheme.labelMedium!.copyWith( 41 | color: Colors.grey, 42 | ), 43 | ), 44 | ], 45 | ); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /lib/views/widgets/list_header.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class ListHeader extends StatelessWidget { 4 | 5 | final String title; 6 | final VoidCallback? onTap; 7 | final String description; 8 | 9 | const ListHeader({ 10 | Key? key, 11 | required this.title, 12 | this.onTap, 13 | required this.description,}) : super(key: key); 14 | 15 | @override 16 | Widget build(BuildContext context) { 17 | return Column( 18 | crossAxisAlignment: CrossAxisAlignment.start, 19 | children: [ 20 | Row( 21 | mainAxisAlignment: MainAxisAlignment.spaceBetween, 22 | children: [ 23 | Text( 24 | title, 25 | style: Theme.of(context).textTheme.titleLarge!.copyWith( 26 | fontWeight: FontWeight.bold, 27 | color: Colors.black, 28 | ), 29 | ), 30 | InkWell( 31 | onTap: onTap, 32 | child: Text( 33 | 'View All', 34 | style: Theme.of(context).textTheme.labelMedium, 35 | ), 36 | ), 37 | ], 38 | ), 39 | Text( 40 | description, 41 | style: Theme.of(context).textTheme.labelMedium!.copyWith( 42 | color: Colors.grey, 43 | ), 44 | ), 45 | ], 46 | ); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /lib/views/widgets/list_item_home.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_ecommerce/controllers/database_controller.dart'; 3 | import 'package:flutter_ecommerce/models/product.dart'; 4 | import 'package:flutter_rating_bar/flutter_rating_bar.dart'; 5 | import 'package:flutter_ecommerce/utilities/assets.dart'; 6 | import 'package:flutter_ecommerce/utilities/routes.dart'; 7 | import 'package:provider/provider.dart'; 8 | 9 | class ListItemHome extends StatelessWidget { 10 | final Product product; 11 | final bool isNew; 12 | final VoidCallback? addToFavorites; 13 | bool isFavorite; 14 | ListItemHome({ 15 | super.key, 16 | required this.product, 17 | required this.isNew, 18 | this.addToFavorites, 19 | this.isFavorite = false, 20 | }); 21 | 22 | @override 23 | Widget build(BuildContext context) { 24 | final size = MediaQuery.of(context).size; 25 | return InkWell( 26 | onTap: () => Navigator.of(context, rootNavigator: true).pushNamed( 27 | AppRoutes.productDetailsRoute, 28 | arguments: product.id, 29 | ), 30 | child: Stack( 31 | children: [ 32 | Stack( 33 | children: [ 34 | ClipRRect( 35 | borderRadius: BorderRadius.circular(12.0), 36 | child: Image.network( 37 | product.imgUrl, 38 | width: 200, 39 | height: 200, 40 | fit: BoxFit.cover, 41 | ), 42 | ), 43 | Padding( 44 | padding: const EdgeInsets.all(8.0), 45 | child: SizedBox( 46 | width: 50, 47 | height: 25, 48 | child: DecoratedBox( 49 | decoration: BoxDecoration( 50 | borderRadius: BorderRadius.circular(16.0), 51 | color: isNew ? Colors.black : Colors.red, 52 | ), 53 | child: Padding( 54 | padding: const EdgeInsets.all(4.0), 55 | child: Center( 56 | child: Text( 57 | isNew ? 'NEW' : '${product.discountValue}%', 58 | style: Theme.of(context).textTheme.labelSmall!.copyWith( 59 | color: Colors.white, 60 | ), 61 | ), 62 | ), 63 | ), 64 | ), 65 | ), 66 | ), 67 | ], 68 | ), 69 | // TODO: Create one component for the favorite button 70 | Positioned( 71 | left: size.width * 0.38, 72 | bottom: size.height * 0.12, 73 | child: Container( 74 | decoration: const BoxDecoration( 75 | shape: BoxShape.circle, 76 | boxShadow: [ 77 | BoxShadow( 78 | blurRadius: 5, 79 | color: Colors.grey, 80 | spreadRadius: 2, 81 | ) 82 | ], 83 | ), 84 | child: CircleAvatar( 85 | backgroundColor: Colors.white, 86 | radius: 20.0, 87 | child: InkWell( 88 | onTap: addToFavorites, 89 | child: Icon( 90 | isFavorite ? Icons.favorite : Icons.favorite_outline, 91 | size: 20.0, 92 | color: isFavorite ? Colors.red : Colors.grey, 93 | ), 94 | ), 95 | ), 96 | ), 97 | ), 98 | Positioned( 99 | bottom: 5, 100 | child: Column( 101 | crossAxisAlignment: CrossAxisAlignment.start, 102 | children: [ 103 | Row( 104 | children: [ 105 | RatingBarIndicator( 106 | itemSize: 25.0, 107 | rating: product.rate?.toDouble() ?? 4.0, 108 | itemBuilder: (context, _) => const Icon( 109 | Icons.star, 110 | color: Colors.amber, 111 | ), 112 | direction: Axis.horizontal, 113 | ), 114 | const SizedBox(width: 4.0), 115 | Text( 116 | '(100)', 117 | style: Theme.of(context).textTheme.labelSmall!.copyWith( 118 | color: Colors.grey, 119 | ), 120 | ), 121 | ], 122 | ), 123 | const SizedBox(height: 8.0), 124 | Text( 125 | product.category, 126 | style: Theme.of(context).textTheme.labelSmall!.copyWith( 127 | color: Colors.grey, 128 | ), 129 | ), 130 | const SizedBox(height: 6.0), 131 | Text( 132 | product.title, 133 | style: Theme.of(context).textTheme.labelMedium!.copyWith( 134 | fontWeight: FontWeight.w600, 135 | ), 136 | ), 137 | const SizedBox(height: 6.0), 138 | isNew 139 | ? Text( 140 | '${product.price}\$', 141 | style: Theme.of(context).textTheme.labelMedium!.copyWith( 142 | color: Colors.grey, 143 | ), 144 | ) 145 | : Text.rich( 146 | TextSpan( 147 | children: [ 148 | TextSpan( 149 | text: '${product.price}\$ ', 150 | style: Theme.of(context) 151 | .textTheme 152 | .labelMedium! 153 | .copyWith( 154 | color: Colors.grey, 155 | decoration: TextDecoration.lineThrough, 156 | ), 157 | ), 158 | TextSpan( 159 | text: 160 | ' ${product.price * (product.discountValue!) / 100}\$', 161 | style: Theme.of(context) 162 | .textTheme 163 | .labelMedium! 164 | .copyWith( 165 | color: Colors.red, 166 | ), 167 | ), 168 | ], 169 | ), 170 | ), 171 | ], 172 | ), 173 | ) 174 | ], 175 | ), 176 | ); 177 | } 178 | } 179 | -------------------------------------------------------------------------------- /lib/views/widgets/main_button.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class MainButton extends StatelessWidget { 4 | final String? text; 5 | final VoidCallback? onTap; 6 | final bool hasCircularBorder; 7 | final Widget? child; 8 | 9 | MainButton({ 10 | super.key, 11 | this.text, 12 | this.onTap, 13 | this.hasCircularBorder = false, 14 | this.child, 15 | }) { 16 | assert(text != null || child != null); 17 | } 18 | 19 | @override 20 | Widget build(BuildContext context) { 21 | return SizedBox( 22 | width: double.infinity, 23 | height: 50, 24 | child: ElevatedButton( 25 | onPressed: onTap, 26 | style: ElevatedButton.styleFrom( 27 | backgroundColor: Theme.of(context).primaryColor, 28 | foregroundColor: Colors.white, 29 | shape: hasCircularBorder 30 | ? RoundedRectangleBorder( 31 | borderRadius: BorderRadius.circular(24.0), 32 | ) 33 | : null, 34 | ), 35 | child: text != null ? Text( 36 | text!, 37 | ) : child, 38 | ), 39 | ); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /lib/views/widgets/main_dialog.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/cupertino.dart'; 2 | import 'package:flutter/material.dart'; 3 | 4 | class MainDialog { 5 | final BuildContext context; 6 | final String title; 7 | final String content; 8 | final List>? actions; 9 | 10 | MainDialog({ 11 | required this.context, 12 | required this.title, 13 | required this.content, 14 | this.actions, 15 | }); 16 | 17 | showAlertDialog() { 18 | showDialog( 19 | context: context, 20 | builder: (_) => AlertDialog( 21 | title: Text( 22 | title, 23 | style: Theme.of(context).textTheme.titleLarge, 24 | ), 25 | content: Text( 26 | content, 27 | style: Theme.of(context).textTheme.labelMedium, 28 | ), 29 | actions: (actions != null) 30 | ? actions! 31 | .map((action) => TextButton( 32 | onPressed: action.values.first, 33 | child: Text(action.keys.first), 34 | )) 35 | .toList() 36 | : [ 37 | TextButton( 38 | onPressed: () => Navigator.of(context).pop(), 39 | child: const Text('OK'), 40 | ), 41 | ], 42 | )); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /lib/views/widgets/order_summary_component.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class OrderSummaryComponent extends StatelessWidget { 4 | final String title; 5 | final String value; 6 | const OrderSummaryComponent({ 7 | Key? key, 8 | required this.title, 9 | required this.value, 10 | }) : super(key: key); 11 | 12 | @override 13 | Widget build(BuildContext context) { 14 | return Row( 15 | mainAxisAlignment: MainAxisAlignment.spaceBetween, 16 | children: [ 17 | Text( 18 | '$title:', 19 | style: Theme.of(context).textTheme.labelMedium!.copyWith( 20 | color: Colors.grey, 21 | ), 22 | ), 23 | Text( 24 | value, 25 | style: Theme.of(context).textTheme.titleLarge, 26 | ), 27 | ], 28 | ); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /lib/views/widgets/social_media_button.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_svg/flutter_svg.dart'; 3 | 4 | class SocialMediaButton extends StatelessWidget { 5 | const SocialMediaButton({ 6 | Key? key, 7 | required this.iconName, 8 | required this.onPress, 9 | }) : super(key: key); 10 | final String iconName; 11 | final VoidCallback onPress; 12 | @override 13 | Widget build(BuildContext context) { 14 | return InkWell( 15 | onTap: onPress, 16 | child: Container( 17 | height: 80, 18 | width: 80, 19 | decoration: BoxDecoration( 20 | borderRadius: BorderRadius.circular(16.0), 21 | color: Colors.white, 22 | ), 23 | child: Center( 24 | child: SvgPicture.asset( 25 | iconName, 26 | width: 40.0, 27 | height: 40.0, 28 | ), 29 | ), 30 | ), 31 | ); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: flutter_ecommerce 2 | description: A new Flutter project. 3 | 4 | publish_to: "none" # Remove this line if you wish to publish to pub.dev 5 | version: 1.0.0+1 6 | 7 | environment: 8 | sdk: ">=3.2.0 <4.0.0" 9 | 10 | dependencies: 11 | cloud_firestore: ^4.13.6 12 | cupertino_icons: ^1.0.2 13 | dio: ^5.6.0 14 | firebase_auth: ^4.15.3 15 | firebase_core: ^2.24.2 16 | flutter: 17 | sdk: flutter 18 | flutter_bloc: ^8.1.5 19 | flutter_rating_bar: ^4.0.1 20 | flutter_stripe: ^11.0.0 21 | flutter_svg: ^2.0.9 22 | persistent_bottom_nav_bar_v2: ^5.3.0 23 | provider: ^6.1.1 24 | 25 | dev_dependencies: 26 | change_app_package_name: ^1.1.0 27 | flutter_lints: ^3.0.1 28 | flutter_test: 29 | sdk: flutter 30 | 31 | flutter: 32 | uses-material-design: true 33 | assets: 34 | - assets/ 35 | -------------------------------------------------------------------------------- /test/widget_test.dart: -------------------------------------------------------------------------------- 1 | // This is a basic Flutter widget test. 2 | // 3 | // To perform an interaction with a widget in your test, use the WidgetTester 4 | // utility in the flutter_test package. For example, you can send tap and scroll 5 | // gestures. You can also use WidgetTester to find child widgets in the widget 6 | // tree, read text, and verify that the values of widget properties are correct. 7 | 8 | import 'package:flutter/material.dart'; 9 | import 'package:flutter_test/flutter_test.dart'; 10 | 11 | import 'package:flutter_ecommerce/main.dart'; 12 | 13 | void main() { 14 | testWidgets('Counter increments smoke test', (WidgetTester tester) async { 15 | // Build our app and trigger a frame. 16 | await tester.pumpWidget(const MyApp()); 17 | 18 | // Verify that our counter starts at 0. 19 | expect(find.text('0'), findsOneWidget); 20 | expect(find.text('1'), findsNothing); 21 | 22 | // Tap the '+' icon and trigger a frame. 23 | await tester.tap(find.byIcon(Icons.add)); 24 | await tester.pump(); 25 | 26 | // Verify that our counter has incremented. 27 | expect(find.text('0'), findsNothing); 28 | expect(find.text('1'), findsOneWidget); 29 | }); 30 | } 31 | --------------------------------------------------------------------------------