├── .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 |
42 |
--------------------------------------------------------------------------------
/assets/google-svgrepo-com.svg:
--------------------------------------------------------------------------------
1 |
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