├── ios ├── Flutter │ ├── Debug.xcconfig │ ├── Release.xcconfig │ └── AppFrameworkInfo.plist ├── Runner │ ├── Runner-Bridging-Header.h │ ├── Assets.xcassets │ │ ├── LaunchImage.imageset │ │ │ ├── LaunchImage.png │ │ │ ├── LaunchImage@2x.png │ │ │ ├── LaunchImage@3x.png │ │ │ ├── README.md │ │ │ └── Contents.json │ │ └── AppIcon.appiconset │ │ │ ├── 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-1024x1024@1x.png │ │ │ ├── Icon-App-83.5x83.5@2x.png │ │ │ └── Contents.json │ ├── AppDelegate.swift │ ├── Base.lproj │ │ ├── Main.storyboard │ │ └── LaunchScreen.storyboard │ └── Info.plist ├── Runner.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ ├── WorkspaceSettings.xcsettings │ │ └── IDEWorkspaceChecks.plist ├── Runner.xcodeproj │ ├── project.xcworkspace │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata │ │ │ ├── WorkspaceSettings.xcsettings │ │ │ └── IDEWorkspaceChecks.plist │ └── xcshareddata │ │ └── xcschemes │ │ └── Runner.xcscheme └── .gitignore ├── assets └── Images │ ├── cake.jpg │ ├── fish.jpg │ └── logo.png ├── lib ├── constants │ ├── button_text.dart │ ├── payment.dart │ ├── tasks.dart │ ├── screen_ids.dart │ └── screen_titles.dart ├── controllers │ ├── activity_tracker_controller.dart │ ├── shipping_controller.dart │ ├── error_controller.dart │ ├── category_controller.dart │ ├── product_controller.dart │ ├── order_controller.dart │ ├── auth_controller.dart │ └── cart_controller.dart ├── models │ ├── authdata.dart │ ├── cart_item.dart │ ├── category.dart │ ├── shipping_details.dart │ ├── product.dart │ └── order.dart ├── utils │ ├── validator.dart │ └── date_util.dart ├── widgets │ ├── dialog.dart │ ├── global_snackbar.dart │ ├── underlined_text..dart │ ├── round_cart_button.dart │ ├── cart_button.dart │ ├── no_order_history_content.dart │ ├── round_icon_button.dart │ ├── category.dart │ ├── order_history_item.dart │ ├── guest_user_drawer_widget.dart │ ├── drawer.dart │ ├── product_detail_bottomsheet_content.dart │ ├── product_card.dart │ ├── shopping_cart_bottom_sheet.dart │ └── auth_screen_custom_painter.dart ├── services │ ├── category_service.dart │ ├── paypal_service.dart │ ├── product_service.dart │ ├── cart_service.dart │ ├── order_service.dart │ ├── auth_service.dart │ └── stripe_service.dart ├── skeletons │ ├── category_list_skeleton.dart │ ├── product_list_skeleton.dart │ └── product_detail_skeleton.dart ├── application.properties │ └── app_properties.dart ├── screens │ ├── thank_you.dart │ ├── order_history.dart │ ├── single_order.dart │ ├── product_detail.dart │ ├── shipping.dart │ ├── products_list.dart │ └── payment_method.dart └── main.dart ├── android ├── gradle.properties ├── app │ ├── src │ │ ├── main │ │ │ ├── res │ │ │ │ ├── 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 │ │ │ │ ├── xml │ │ │ │ │ └── my_backup_rules.xml │ │ │ │ ├── drawable │ │ │ │ │ └── launch_background.xml │ │ │ │ └── values │ │ │ │ │ └── styles.xml │ │ │ ├── kotlin │ │ │ │ └── com │ │ │ │ │ └── example │ │ │ │ │ └── ecommerceapp │ │ │ │ │ └── MainActivity.kt │ │ │ └── AndroidManifest.xml │ │ ├── debug │ │ │ └── AndroidManifest.xml │ │ └── profile │ │ │ └── AndroidManifest.xml │ └── build.gradle ├── gradle │ └── wrapper │ │ └── gradle-wrapper.properties ├── .gitignore ├── settings.gradle └── build.gradle ├── .metadata ├── .vscode └── launch.json ├── test └── widget_test.dart ├── .gitignore ├── README.md ├── pubspec.yaml └── pubspec.lock /ios/Flutter/Debug.xcconfig: -------------------------------------------------------------------------------- 1 | #include "Generated.xcconfig" 2 | -------------------------------------------------------------------------------- /ios/Flutter/Release.xcconfig: -------------------------------------------------------------------------------- 1 | #include "Generated.xcconfig" 2 | -------------------------------------------------------------------------------- /ios/Runner/Runner-Bridging-Header.h: -------------------------------------------------------------------------------- 1 | #import "GeneratedPluginRegistrant.h" 2 | -------------------------------------------------------------------------------- /assets/Images/cake.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KNehe/Ecommerce_app/HEAD/assets/Images/cake.jpg -------------------------------------------------------------------------------- /assets/Images/fish.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KNehe/Ecommerce_app/HEAD/assets/Images/fish.jpg -------------------------------------------------------------------------------- /assets/Images/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KNehe/Ecommerce_app/HEAD/assets/Images/logo.png -------------------------------------------------------------------------------- /lib/constants/button_text.dart: -------------------------------------------------------------------------------- 1 | const ADD_TO_CART_BUTTON_TEXT = 'ADD'; 2 | const REMOVE_FROM_CART_BUTTON_TEXT = 'REMOVE'; 3 | -------------------------------------------------------------------------------- /android/gradle.properties: -------------------------------------------------------------------------------- 1 | org.gradle.jvmargs=-Xmx1536M 2 | android.enableR8=true 3 | android.useAndroidX=true 4 | android.enableJetifier=true 5 | -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KNehe/Ecommerce_app/HEAD/android/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KNehe/Ecommerce_app/HEAD/android/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KNehe/Ecommerce_app/HEAD/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KNehe/Ecommerce_app/HEAD/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KNehe/Ecommerce_app/HEAD/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KNehe/Ecommerce_app/HEAD/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KNehe/Ecommerce_app/HEAD/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KNehe/Ecommerce_app/HEAD/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KNehe/Ecommerce_app/HEAD/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/KNehe/Ecommerce_app/HEAD/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/KNehe/Ecommerce_app/HEAD/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/KNehe/Ecommerce_app/HEAD/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/KNehe/Ecommerce_app/HEAD/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/KNehe/Ecommerce_app/HEAD/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/KNehe/Ecommerce_app/HEAD/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/KNehe/Ecommerce_app/HEAD/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/KNehe/Ecommerce_app/HEAD/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/KNehe/Ecommerce_app/HEAD/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/KNehe/Ecommerce_app/HEAD/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/KNehe/Ecommerce_app/HEAD/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/KNehe/Ecommerce_app/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KNehe/Ecommerce_app/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KNehe/Ecommerce_app/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png -------------------------------------------------------------------------------- /lib/constants/payment.dart: -------------------------------------------------------------------------------- 1 | const STRIPE_PAYMENT = 'Stripe'; 2 | const PAY_PAL = 'Pay pal'; 3 | 4 | const USER_TYPE_RESGISTERED = 'Registered user'; 5 | const USER_TYPE_GUEST = 'Guest user'; 6 | -------------------------------------------------------------------------------- /android/app/src/main/res/xml/my_backup_rules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /android/app/src/main/kotlin/com/example/ecommerceapp/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package com.example.ecommerceapp 2 | 3 | import io.flutter.embedding.android.FlutterActivity 4 | 5 | class MainActivity: FlutterActivity() { 6 | } 7 | -------------------------------------------------------------------------------- /ios/Runner.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /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-5.6.2-all.zip 7 | -------------------------------------------------------------------------------- /ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PreviewsEnabled 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PreviewsEnabled 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /lib/controllers/activity_tracker_controller.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/foundation.dart'; 2 | 3 | class ActivityTracker extends ChangeNotifier { 4 | String _currentTask = ''; 5 | 6 | setTaskCurrentTask(String task) { 7 | _currentTask = task; 8 | } 9 | 10 | String get currentTask => _currentTask; 11 | } 12 | -------------------------------------------------------------------------------- /lib/models/authdata.dart: -------------------------------------------------------------------------------- 1 | class AuthData { 2 | String name; 3 | String email; 4 | String password; 5 | 6 | AuthData({this.name, this.email, this.password}); 7 | 8 | Map toJson() => { 9 | "name": name, 10 | "email": email, 11 | "password": password, 12 | }; 13 | } 14 | -------------------------------------------------------------------------------- /ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /.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 and should not be manually edited. 5 | 6 | version: 7 | revision: 2ae34518b87dd891355ed6c6ea8cb68c4d52bb9d 8 | channel: stable 9 | 10 | project_type: app 11 | -------------------------------------------------------------------------------- /lib/constants/tasks.dart: -------------------------------------------------------------------------------- 1 | const VIEWING_ORDER_HISTORY = 'Viewing order history'; 2 | const VIEWING_SINGLE_OLD_ORDER_HISTORY = 'Viewing single new order history'; 3 | const VIEWING_SINGLE_NEW_ORDER_HISTORY = 'Viewing single all order history'; 4 | const VIEWING_PROFILE = 'Viewing profile'; 5 | const CHANGING_NAME = 'Changing name'; 6 | const CHANGING_EMAIL = 'Changing email'; 7 | -------------------------------------------------------------------------------- /android/app/src/debug/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /android/app/src/profile/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /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. -------------------------------------------------------------------------------- /.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": "ecommerce_app", 9 | "request": "launch", 10 | "type": "dart" 11 | } 12 | ] 13 | } -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /lib/utils/validator.dart: -------------------------------------------------------------------------------- 1 | class Validator { 2 | static bool isPostalCodeValid(String postalCode) { 3 | return RegExp(r"^(^\d{5}$)|(^\d{5}-\d{4}$)$").hasMatch(postalCode); 4 | } 5 | 6 | static bool isPhoneNumberValid(String phoneNumber) { 7 | return RegExp(r"([\+]?[(]?[0-9]{3}[)]?[-\s\.]?[0-9]{3}[-\s\.]?[0-9]{4,6})") 8 | .hasMatch(phoneNumber); 9 | } 10 | 11 | static bool isEmailValid(String email) { 12 | return RegExp(r"\S+@\S+\.\S+").hasMatch(email); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /android/app/src/main/res/drawable/launch_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /lib/constants/screen_ids.dart: -------------------------------------------------------------------------------- 1 | const ShoppingCart_Screen_Id = 'ShoppingCartScreen'; 2 | const ProductDetail_Screen_Id = 'ProductDetailsScreen'; 3 | const ProductList_Screen_Id = 'ProductListScreen'; 4 | const Shipping_Screen_Id = 'ShippingScreen'; 5 | const PaymentMethod_Screen_Id = 'PaymentMethodScreen'; 6 | const ThankYou_Screen_Id = 'ThaankYouScreen'; 7 | const SingleOrder_Screen_Id = 'SingleOrderScreen'; 8 | const AuthScreen_Id = 'AuthScreen'; 9 | const OrderHistoryScreen_Id = 'OrderHistoryScreen'; 10 | const ProfileScreen_id = 'ProfileScreen'; 11 | -------------------------------------------------------------------------------- /lib/widgets/dialog.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:progress_dialog/progress_dialog.dart'; 3 | 4 | class CDialog { 5 | ProgressDialog dialog; 6 | 7 | CDialog(BuildContext context) { 8 | ProgressDialog pr = ProgressDialog(context); 9 | pr = ProgressDialog( 10 | context, 11 | type: ProgressDialogType.Normal, 12 | isDismissible: false, 13 | showLogs: false, 14 | ); 15 | pr.style( 16 | message: 'Please wait...', 17 | ); 18 | dialog = pr; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /lib/widgets/global_snackbar.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | enum SnackBarType { Error, Success } 4 | 5 | class GlobalSnackBar { 6 | static showSnackbar( 7 | GlobalKey scaffoldKey, String message, SnackBarType type) { 8 | scaffoldKey.currentState.showSnackBar(SnackBar( 9 | backgroundColor: 10 | type == SnackBarType.Error ? Colors.red[900] : Colors.blue[700], 11 | content: Text( 12 | '$message', 13 | style: TextStyle(fontSize: 15), 14 | ), 15 | )); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /lib/services/category_service.dart: -------------------------------------------------------------------------------- 1 | import 'package:ecommerceapp/application.properties/app_properties.dart'; 2 | import 'package:http/http.dart' as http; 3 | 4 | class CategoryService { 5 | static CategoryService _categoryService; 6 | 7 | CategoryService._internal() { 8 | _categoryService = this; 9 | } 10 | 11 | factory CategoryService() => _categoryService ?? CategoryService._internal(); 12 | 13 | static var httpClient = http.Client(); 14 | 15 | Future getCategories() async { 16 | return await httpClient.get(AppProperties.categoryUrl); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /lib/utils/date_util.dart: -------------------------------------------------------------------------------- 1 | import 'package:intl/intl.dart'; 2 | 3 | class DateUtils { 4 | String getFormattedDate(String timestmapWithMillis) { 5 | var dateTime = 6 | new DateFormat("yyyy-MM-dd HH:mm:ss").parse(timestmapWithMillis); 7 | 8 | return dateTime.day.toString() + 9 | "-" + 10 | dateTime.month.toString() + 11 | "-" + 12 | dateTime.year.toString() + 13 | " " + 14 | dateTime.hour.toString() + 15 | ":" + 16 | dateTime.minute.toString() + 17 | ":" + 18 | dateTime.second.toString(); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /lib/constants/screen_titles.dart: -------------------------------------------------------------------------------- 1 | const SignIn_Screen_Title = 'Welcome\n Back'; 2 | 3 | const SignUp_Screen_Title = 'Create\nAccount'; 4 | 5 | const ForgotPassword_Screen_Ttile = 'Forgot\nPasword'; 6 | const OrderHistoryScreen_Title = 'Order History'; 7 | const ProfileScreen_Title = 'Profile'; 8 | const Payment_Screen_Title = 'Order summary & Payment method'; 9 | const ProductList_Screen_Title = 'Store'; 10 | const ShippingDetail_Screen_Title = 'Shipping details'; 11 | const ShoppingCart_Screen_Title = 'Shopping cart'; 12 | const SingleOrder_Screen_Title = 'Order'; 13 | const Thanks_Screen_Title = 'Payment successfull'; 14 | -------------------------------------------------------------------------------- /ios/.gitignore: -------------------------------------------------------------------------------- 1 | *.mode1v3 2 | *.mode2v3 3 | *.moved-aside 4 | *.pbxuser 5 | *.perspectivev3 6 | **/*sync/ 7 | .sconsign.dblite 8 | .tags* 9 | **/.vagrant/ 10 | **/DerivedData/ 11 | Icon? 12 | **/Pods/ 13 | **/.symlinks/ 14 | profile 15 | xcuserdata 16 | **/.generated/ 17 | Flutter/App.framework 18 | Flutter/Flutter.framework 19 | Flutter/Flutter.podspec 20 | Flutter/Generated.xcconfig 21 | Flutter/app.flx 22 | Flutter/app.zip 23 | Flutter/flutter_assets/ 24 | Flutter/flutter_export_environment.sh 25 | ServiceDefinitions.json 26 | Runner/GeneratedPluginRegistrant.* 27 | 28 | # Exceptions to above rules. 29 | !default.mode1v3 30 | !default.mode2v3 31 | !default.pbxuser 32 | !default.perspectivev3 33 | -------------------------------------------------------------------------------- /android/build.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | ext.kotlin_version = '1.3.50' 3 | repositories { 4 | google() 5 | jcenter() 6 | } 7 | 8 | dependencies { 9 | classpath 'com.android.tools.build:gradle:3.5.0' 10 | classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" 11 | } 12 | } 13 | 14 | allprojects { 15 | repositories { 16 | google() 17 | jcenter() 18 | } 19 | } 20 | 21 | rootProject.buildDir = '../build' 22 | subprojects { 23 | project.buildDir = "${rootProject.buildDir}/${project.name}" 24 | } 25 | subprojects { 26 | project.evaluationDependsOn(':app') 27 | } 28 | 29 | task clean(type: Delete) { 30 | delete rootProject.buildDir 31 | } 32 | -------------------------------------------------------------------------------- /lib/widgets/underlined_text..dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class UnderlinedText extends StatelessWidget { 4 | final String text; 5 | final double decorationThickness; 6 | final FontWeight fontWeight; 7 | final double fontSize; 8 | 9 | const UnderlinedText( 10 | {Key key, 11 | @required this.text, 12 | this.decorationThickness, 13 | this.fontWeight, 14 | this.fontSize}) 15 | : super(key: key); 16 | 17 | @override 18 | Widget build(BuildContext context) { 19 | return Text( 20 | text, 21 | style: TextStyle( 22 | decoration: TextDecoration.underline, 23 | decorationThickness: decorationThickness, 24 | fontSize: fontSize, 25 | fontWeight: fontWeight, 26 | ), 27 | ); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /lib/models/cart_item.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | 3 | import 'package:ecommerceapp/models/product.dart'; 4 | 5 | String cartItemToJson(List data) => 6 | json.encode(List.from(data.map((x) => x.toJson()))); 7 | 8 | List cartItemFromJson(String str) => 9 | List.from(json.decode(str).map((x) => CartItem.fromJson(x))); 10 | 11 | class CartItem { 12 | Product product; 13 | int quantity; 14 | 15 | CartItem({this.product, this.quantity, int}); 16 | 17 | Map toJson() => { 18 | "product": product.toJson(), 19 | "quantity": quantity, 20 | }; 21 | 22 | factory CartItem.fromJson(Map json) => CartItem( 23 | product: Product.fromJson(json["product"]), 24 | quantity: json["quantity"], 25 | ); 26 | } 27 | -------------------------------------------------------------------------------- /lib/widgets/round_cart_button.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class RoundCartButton extends StatelessWidget { 4 | final Function onTap; 5 | final IconData icon; 6 | final double width; 7 | const RoundCartButton({ 8 | Key key, 9 | @required this.onTap, 10 | @required this.icon, 11 | @required this.width, 12 | }) : super(key: key); 13 | 14 | @override 15 | Widget build(BuildContext context) { 16 | return GestureDetector( 17 | onTap: onTap, 18 | child: Container( 19 | width: width, 20 | decoration: BoxDecoration( 21 | color: Colors.orange[100], 22 | borderRadius: BorderRadius.circular(50), 23 | ), 24 | child: Icon( 25 | icon, 26 | color: Colors.red, 27 | ), 28 | ), 29 | ); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /lib/controllers/shipping_controller.dart: -------------------------------------------------------------------------------- 1 | import 'package:ecommerceapp/models/shipping_details.dart'; 2 | import 'package:flutter/foundation.dart'; 3 | 4 | class ShippingController extends ChangeNotifier { 5 | var shippingDetails = ShippingDetails( 6 | name: '', 7 | phoneContact: '', 8 | city: '', 9 | addressLine: '', 10 | postalCode: '', 11 | country: '', 12 | ); 13 | 14 | void setShippingDetails({@required ShippingDetails details}) { 15 | shippingDetails = details; 16 | notifyListeners(); 17 | } 18 | 19 | ShippingDetails getShippingDetails() => shippingDetails; 20 | 21 | void reset() { 22 | shippingDetails = ShippingDetails( 23 | name: '', 24 | phoneContact: '', 25 | city: '', 26 | addressLine: '', 27 | postalCode: '', 28 | country: '', 29 | ); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /lib/models/category.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | 3 | List categoryFromJson(String str) => List.from( 4 | json.decode(str).map((x) => CategoryModel.fromJson(x))); 5 | 6 | String categoryToJson(List data) => 7 | json.encode(List.from(data.map((x) => x.toJson()))); 8 | 9 | class CategoryModel { 10 | CategoryModel({ 11 | this.id, 12 | this.category, 13 | this.v, 14 | }); 15 | 16 | String id; 17 | String category; 18 | int v; 19 | 20 | factory CategoryModel.fromJson(Map json) => CategoryModel( 21 | id: json["_id"], 22 | category: json["category"], 23 | v: json["__v"], 24 | ); 25 | 26 | Map toJson() => { 27 | "_id": id, 28 | "category": category, 29 | "__v": v, 30 | }; 31 | } 32 | -------------------------------------------------------------------------------- /test/widget_test.dart: -------------------------------------------------------------------------------- 1 | 2 | //import 'package:flutter/material.dart'; 3 | //import 'package:flutter_test/flutter_test.dart'; 4 | // 5 | //import 'package:ecommerceapp/main.dart'; 6 | // 7 | //void main() { 8 | // testWidgets('Counter increments smoke test', (WidgetTester tester) async { 9 | // // Build our app and trigger a frame. 10 | // await tester.pumpWidget(EcommerceApp()); 11 | // 12 | // // Verify that our counter starts at 0. 13 | // expect(find.text('0'), findsOneWidget); 14 | // expect(find.text('1'), findsNothing); 15 | // 16 | // // Tap the '+' icon and trigger a frame. 17 | // await tester.tap(find.byIcon(Icons.add)); 18 | // await tester.pump(); 19 | // 20 | // // Verify that our counter has incremented. 21 | // expect(find.text('0'), findsNothing); 22 | // expect(find.text('1'), findsOneWidget); 23 | // }); 24 | //} 25 | -------------------------------------------------------------------------------- /lib/widgets/cart_button.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class CartButton extends StatelessWidget { 4 | final String text; 5 | final double width; 6 | 7 | const CartButton({ 8 | Key key, 9 | @required this.text, 10 | @required this.width, 11 | }) : super(key: key); 12 | 13 | @override 14 | Widget build(BuildContext context) { 15 | return Container( 16 | width: width, 17 | decoration: BoxDecoration( 18 | color: Colors.orange, 19 | borderRadius: BorderRadius.circular(50), 20 | ), 21 | child: Padding( 22 | padding: const EdgeInsets.all(8.0), 23 | child: Center( 24 | child: Text( 25 | text, 26 | style: TextStyle( 27 | color: Colors.white, 28 | ), 29 | ), 30 | ), 31 | ), 32 | ); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /lib/services/paypal_service.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/services.dart'; 2 | import 'package:flutter_braintree/flutter_braintree.dart'; 3 | 4 | class PayPalService { 5 | static final String tokenizationKey = 'sandbox_9qqht4sx_2c34stzd8dh7rzxb'; 6 | 7 | static Future processPayment( 8 | String amount, 9 | ) async { 10 | try { 11 | final request = BraintreePayPalRequest(amount: amount); 12 | 13 | BraintreePaymentMethodNonce result = 14 | await Braintree.requestPaypalNonce(tokenizationKey, request); 15 | 16 | if (result != null) { 17 | return result.nonce; 18 | } else { 19 | return null; 20 | } 21 | } on PlatformException catch (e) { 22 | print("Paltform excepion: ${e.toString()}"); 23 | return null; 24 | } catch (e) { 25 | print("ERROR: ${e.toString()} "); 26 | return null; 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /ios/Flutter/AppFrameworkInfo.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 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 | 8.0 25 | 26 | 27 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Miscellaneous 2 | *.class 3 | *.log 4 | *.pyc 5 | *.swp 6 | .DS_Store 7 | .atom/ 8 | .buildlog/ 9 | .history 10 | .svn/ 11 | 12 | # IntelliJ related 13 | *.iml 14 | *.ipr 15 | *.iws 16 | .idea/ 17 | 18 | # The .vscode folder contains launch configuration and tasks you configure in 19 | # VS Code which you may wish to be included in version control, so this line 20 | # is commented out by default. 21 | #.vscode/ 22 | 23 | # Flutter/Dart/Pub related 24 | **/doc/api/ 25 | **/ios/Flutter/.last_build_id 26 | .dart_tool/ 27 | .flutter-plugins 28 | .flutter-plugins-dependencies 29 | .packages 30 | .pub-cache/ 31 | .pub/ 32 | /build/ 33 | 34 | # Web related 35 | lib/generated_plugin_registrant.dart 36 | 37 | # Symbolication related 38 | app.*.symbols 39 | 40 | # Obfuscation related 41 | app.*.map.json 42 | 43 | # Exceptions to above rules. 44 | !/packages/flutter_tools/test/data/dart_dependencies_test/**/.packages 45 | -------------------------------------------------------------------------------- /lib/services/product_service.dart: -------------------------------------------------------------------------------- 1 | import 'package:ecommerceapp/application.properties/app_properties.dart'; 2 | import 'package:http/http.dart' as http; 3 | 4 | class ProductService { 5 | static ProductService _productService; 6 | 7 | ProductService._internal() { 8 | _productService = this; 9 | } 10 | 11 | factory ProductService() => _productService ?? ProductService._internal(); 12 | 13 | static var httpClient = http.Client(); 14 | 15 | Future getAllProducts() async { 16 | return await http.get(AppProperties.productUrl); 17 | } 18 | 19 | Future getProductByCategoryOrName(String value) async { 20 | return await http.get('${AppProperties.searchByCategoryOrNameUrl}$value'); 21 | } 22 | 23 | Future getProductByCategory(String value) async { 24 | return await http.get('${AppProperties.searchByCategoryUrl}$value'); 25 | } 26 | 27 | Future getProductById(String id) async { 28 | return await http.get('${AppProperties.productUrl}$id'); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /lib/widgets/no_order_history_content.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class NoOrderHistoryFoundContent extends StatelessWidget { 4 | const NoOrderHistoryFoundContent({ 5 | Key key, 6 | }) : super(key: key); 7 | 8 | @override 9 | Widget build(BuildContext context) { 10 | return Column( 11 | children: [ 12 | Text( 13 | 'No orders found', 14 | style: TextStyle( 15 | fontSize: 18, 16 | fontWeight: FontWeight.bold, 17 | ), 18 | ), 19 | SizedBox(height: 10), 20 | Text( 21 | 'This could be because you bought products as a guest', 22 | style: TextStyle( 23 | fontSize: 15, 24 | ), 25 | ), 26 | SizedBox(height: 10), 27 | Text( 28 | 'Or you haven\'t bought any item yet', 29 | style: TextStyle( 30 | fontSize: 15, 31 | ), 32 | ), 33 | ], 34 | ); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /lib/skeletons/category_list_skeleton.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class CategoryListSkeleton extends StatelessWidget { 4 | const CategoryListSkeleton({ 5 | Key key, 6 | }) : super(key: key); 7 | 8 | @override 9 | Widget build(BuildContext context) { 10 | return ListView.builder( 11 | itemCount: 5, 12 | scrollDirection: Axis.horizontal, 13 | itemBuilder: (context, index) { 14 | return Padding( 15 | padding: EdgeInsets.all(4.0), 16 | child: Container( 17 | width: 100, 18 | decoration: BoxDecoration( 19 | color: Colors.grey[200], 20 | borderRadius: BorderRadius.circular(50), 21 | ), 22 | child: Text( 23 | '', 24 | style: TextStyle( 25 | color: Colors.black, 26 | fontSize: 5, 27 | ), 28 | ), 29 | ), 30 | ); 31 | }, 32 | ); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /android/app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 15 | 18 | 19 | -------------------------------------------------------------------------------- /lib/models/shipping_details.dart: -------------------------------------------------------------------------------- 1 | class ShippingDetails { 2 | ShippingDetails({ 3 | this.id, 4 | this.name, 5 | this.phoneContact, 6 | this.addressLine, 7 | this.city, 8 | this.postalCode, 9 | this.country, 10 | }); 11 | 12 | String id; 13 | String name; 14 | String phoneContact; 15 | String addressLine; 16 | String city; 17 | String postalCode; 18 | String country; 19 | 20 | factory ShippingDetails.fromJson(Map json) => 21 | ShippingDetails( 22 | id: json["_id"], 23 | name: json["name"], 24 | phoneContact: json["phoneContact"], 25 | addressLine: json["addressLine"], 26 | city: json["city"], 27 | postalCode: json["postalCode"], 28 | country: json["country"], 29 | ); 30 | 31 | Map toJson() => { 32 | "_id": id, 33 | "name": name, 34 | "phoneContact": phoneContact, 35 | "addressLine": addressLine, 36 | "city": city, 37 | "postalCode": postalCode, 38 | "country": country, 39 | }; 40 | } 41 | -------------------------------------------------------------------------------- /lib/widgets/round_icon_button.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class RoundIconButton extends StatelessWidget { 4 | final double width; 5 | final double height; 6 | final Color backgroundColor; 7 | final Color iconColor; 8 | final IconData iconData; 9 | final Function onPressed; 10 | 11 | RoundIconButton({ 12 | @required this.width, 13 | @required this.height, 14 | @required this.backgroundColor, 15 | @required this.iconColor, 16 | @required this.iconData, 17 | @required this.onPressed, 18 | }); 19 | 20 | @override 21 | Widget build(BuildContext context) { 22 | return Container( 23 | width: width, 24 | height: height, 25 | child: Material( 26 | borderRadius: BorderRadius.circular(50), 27 | color: backgroundColor, 28 | child: Center( 29 | child: IconButton( 30 | icon: Icon( 31 | iconData, 32 | color: iconColor, 33 | ), 34 | onPressed: onPressed, 35 | ), 36 | ), 37 | ), 38 | ); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /lib/models/product.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | 3 | List productFromJson(String str) => 4 | List.from(json.decode(str).map((x) => Product.fromJson(x))); 5 | 6 | String productToJson(List data) => 7 | json.encode(List.from(data.map((x) => x.toJson()))); 8 | 9 | class Product { 10 | Product({ 11 | this.id, 12 | this.name, 13 | this.price, 14 | this.imageUrl, 15 | this.category, 16 | this.details, 17 | this.v, 18 | }); 19 | 20 | String id; 21 | String name; 22 | int price; 23 | String imageUrl; 24 | String category; 25 | String details; 26 | int v; 27 | 28 | factory Product.fromJson(Map json) => Product( 29 | id: json["_id"], 30 | name: json["name"], 31 | price: json["price"], 32 | imageUrl: json["imageUrl"], 33 | category: json["category"], 34 | details: json["details"], 35 | v: json["__v"], 36 | ); 37 | 38 | Map toJson() => { 39 | "_id": id, 40 | "name": name, 41 | "price": price, 42 | "imageUrl": imageUrl, 43 | "category": category, 44 | "details": details, 45 | "__v": v, 46 | }; 47 | } 48 | -------------------------------------------------------------------------------- /lib/application.properties/app_properties.dart: -------------------------------------------------------------------------------- 1 | class AppProperties { 2 | //static String _baseApiUrl = 'http://10.0.2.2:3000/api/v1'; 3 | static String _baseApiUrl = 'https://nehe-ecommerce-api.herokuapp.com/api/v1'; 4 | 5 | static String productUrl = '$_baseApiUrl/products/'; 6 | 7 | static String categoryUrl = '$_baseApiUrl/categories/'; 8 | 9 | static String searchByCategoryOrNameUrl = '$_baseApiUrl/products/search/'; 10 | 11 | static String searchByCategoryUrl = '$_baseApiUrl/products/search/category/'; 12 | 13 | static String saveOrderUrl = '$_baseApiUrl/cart/flutter/stripepayment'; 14 | 15 | static String payPalRequestUrl = '$_baseApiUrl/cart/braintree/paypalpayment/'; 16 | 17 | static String signUpUrl = '$_baseApiUrl/users/signup'; 18 | 19 | static String signInUrl = '$_baseApiUrl/users/signin'; 20 | 21 | static String checkTokenExpiryUrl = '$_baseApiUrl/users/checktokenexpiry'; 22 | 23 | static String cartUrl = '$_baseApiUrl/cart/'; 24 | 25 | static String getOrdersUrl = '$_baseApiUrl/cart/orders/user/'; 26 | 27 | static String changenameUrl = '$_baseApiUrl/users/updatename/'; 28 | 29 | static String changeMailUrl = '$_baseApiUrl/users/updatemail/'; 30 | 31 | static String forgotPasswordUrl = '$_baseApiUrl/users/forgotpassword'; 32 | } 33 | -------------------------------------------------------------------------------- /lib/skeletons/product_list_skeleton.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class ProductListSkeleton extends StatelessWidget { 4 | const ProductListSkeleton({ 5 | Key key, 6 | }) : super(key: key); 7 | 8 | @override 9 | Widget build(BuildContext context) { 10 | return GridView.builder( 11 | gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( 12 | crossAxisCount: 2, 13 | crossAxisSpacing: 10, 14 | mainAxisSpacing: 15, 15 | ), 16 | itemCount: 6, 17 | itemBuilder: (context, index) { 18 | return Column(children: [ 19 | Container( 20 | height: 120, 21 | width: double.infinity, 22 | color: Colors.grey, 23 | child: Text(''), 24 | ), 25 | SizedBox( 26 | height: 5.0, 27 | ), 28 | Row( 29 | mainAxisAlignment: MainAxisAlignment.spaceBetween, 30 | children: [ 31 | Container( 32 | height: 10, 33 | width: 80, 34 | color: Colors.grey, 35 | child: Text(''), 36 | ), 37 | Container( 38 | height: 10, 39 | width: 50, 40 | color: Colors.grey, 41 | child: Text(''), 42 | ), 43 | ], 44 | ), 45 | ]); 46 | }, 47 | ); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /lib/widgets/category.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class Category extends StatelessWidget { 4 | final String category; 5 | final int categoryIndex; 6 | final int categorySelectedIndex; 7 | final Function onTapped; 8 | Category({ 9 | @required this.category, 10 | @required this.categorySelectedIndex, 11 | @required this.categoryIndex, 12 | @required this.onTapped, 13 | }); 14 | 15 | @override 16 | Widget build(BuildContext context) { 17 | return GestureDetector( 18 | onTap: onTapped, 19 | child: Padding( 20 | padding: const EdgeInsets.all(4.0), 21 | child: Container( 22 | decoration: BoxDecoration( 23 | color: categoryIndex == categorySelectedIndex 24 | ? Colors.grey[700] 25 | : Colors.grey[300], 26 | borderRadius: BorderRadius.circular(50), 27 | ), 28 | child: Padding( 29 | padding: const EdgeInsets.all(8.0), 30 | child: Center( 31 | child: Text( 32 | '$category', 33 | style: TextStyle( 34 | color: categoryIndex == categorySelectedIndex 35 | ? Colors.white 36 | : Colors.black, 37 | fontSize: 15, 38 | ), 39 | ), 40 | ), 41 | ), 42 | ), 43 | ), 44 | ); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /lib/services/cart_service.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | 3 | import 'package:ecommerceapp/application.properties/app_properties.dart'; 4 | import 'package:http/http.dart' as http; 5 | 6 | class CartService { 7 | static CartService _cartService; 8 | 9 | CartService._internal() { 10 | _cartService = this; 11 | } 12 | 13 | factory CartService() => _cartService ?? CartService._internal(); 14 | 15 | static var httpClient = http.Client(); 16 | 17 | static Map headers = {'Content-Type': 'application/json'}; 18 | 19 | Future saveCart( 20 | String productId, 21 | String userId, 22 | String quantity, 23 | String jwtToken, 24 | ) async { 25 | var bodyObject = Map(); 26 | bodyObject.putIfAbsent('productId', () => productId); 27 | bodyObject.putIfAbsent('userId', () => userId); 28 | bodyObject.putIfAbsent('quantity', () => quantity); 29 | 30 | headers.putIfAbsent('Authorization', () => 'Bearer $jwtToken'); 31 | 32 | return await http.post( 33 | AppProperties.cartUrl, 34 | body: json.encode(bodyObject), 35 | headers: headers, 36 | ); 37 | } 38 | 39 | Future getCart( 40 | String userId, 41 | String jwtToken, 42 | ) async { 43 | headers.putIfAbsent('Authorization', () => 'Bearer $jwtToken'); 44 | return await http.get( 45 | '${AppProperties.cartUrl}/$userId', 46 | headers: headers, 47 | ); 48 | } 49 | 50 | Future deleteCart( 51 | String userId, 52 | ) async { 53 | return await http.delete('${AppProperties.cartUrl}/$userId'); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /lib/services/order_service.dart: -------------------------------------------------------------------------------- 1 | import 'package:ecommerceapp/application.properties/app_properties.dart'; 2 | import 'package:http/http.dart' as http; 3 | 4 | class OrderService { 5 | static OrderService _orderService; 6 | 7 | OrderService._internal() { 8 | _orderService = this; 9 | } 10 | 11 | factory OrderService() => _orderService ?? OrderService._internal(); 12 | 13 | Map headers = {'Content-Type': 'application/json'}; 14 | 15 | static var httpClient = http.Client(); 16 | 17 | Future getShippingCost(String country) async { 18 | //can be used to fetch shipping cost for a particular place from API. 19 | return 0; 20 | } 21 | 22 | Future getTax(String country) async { 23 | //can be used to fetch tax for a particular place from API. 24 | return 100; 25 | } 26 | 27 | //used to save order details after making stripe payment 28 | Future saveOrder(String order) async { 29 | return await httpClient.post(AppProperties.saveOrderUrl, 30 | body: order, headers: headers); 31 | } 32 | 33 | //used to send order details along with paypal nonce to process payment and save the order 34 | Future sendPayPalRequest(String order, String nonce) async { 35 | return await httpClient.post('${AppProperties.payPalRequestUrl}$nonce', 36 | body: order, headers: headers); 37 | } 38 | 39 | Future getOrders(String userId, String jwtToken) async { 40 | headers.putIfAbsent('Authorization', () => 'Bearer $jwtToken'); 41 | return await httpClient.get('${AppProperties.getOrdersUrl}$userId', 42 | headers: headers); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /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 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | ecommerceapp 15 | CFBundlePackageType 16 | APPL 17 | CFBundleShortVersionString 18 | $(FLUTTER_BUILD_NAME) 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | $(FLUTTER_BUILD_NUMBER) 23 | LSRequiresIPhoneOS 24 | 25 | UILaunchStoryboardName 26 | LaunchScreen 27 | UIMainStoryboardFile 28 | Main 29 | UISupportedInterfaceOrientations 30 | 31 | UIInterfaceOrientationPortrait 32 | UIInterfaceOrientationLandscapeLeft 33 | UIInterfaceOrientationLandscapeRight 34 | 35 | UISupportedInterfaceOrientations~ipad 36 | 37 | UIInterfaceOrientationPortrait 38 | UIInterfaceOrientationPortraitUpsideDown 39 | UIInterfaceOrientationLandscapeLeft 40 | UIInterfaceOrientationLandscapeRight 41 | 42 | UIViewControllerBasedStatusBarAppearance 43 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /lib/skeletons/product_detail_skeleton.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class ProductDetailSkeleton extends StatelessWidget { 4 | const ProductDetailSkeleton({ 5 | Key key, 6 | }) : super(key: key); 7 | 8 | @override 9 | Widget build(BuildContext context) { 10 | return Container( 11 | margin: EdgeInsets.only(left: 10, right: 10), 12 | child: Column( 13 | children: [ 14 | Container( 15 | height: MediaQuery.of(context).size.height / 2, 16 | width: MediaQuery.of(context).size.width, 17 | color: Colors.grey, 18 | child: Text(''), 19 | ), 20 | SizedBox( 21 | height: 10, 22 | ), 23 | Row( 24 | mainAxisAlignment: MainAxisAlignment.spaceBetween, 25 | children: [ 26 | Container( 27 | height: MediaQuery.of(context).size.height / 18, 28 | width: MediaQuery.of(context).size.width / 3, 29 | color: Colors.grey, 30 | child: Text(''), 31 | ), 32 | Container( 33 | height: MediaQuery.of(context).size.height / 18, 34 | width: MediaQuery.of(context).size.width / 3, 35 | color: Colors.grey, 36 | child: Text(''), 37 | ), 38 | ], 39 | ), 40 | SizedBox( 41 | height: 10, 42 | ), 43 | Container( 44 | height: MediaQuery.of(context).size.height / 4, 45 | width: MediaQuery.of(context).size.width, 46 | color: Colors.grey, 47 | child: Text(''), 48 | ), 49 | ], 50 | ), 51 | ); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /lib/controllers/error_controller.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | 3 | import 'package:ecommerceapp/widgets/global_snackbar.dart'; 4 | import 'package:flutter/material.dart'; 5 | 6 | class ErrorController { 7 | static showErrorFromApi(GlobalKey scaffoldKey, var response) { 8 | var resBody = json.decode(response.body); 9 | String message = ErrorController()._formatErrorFromApi(resBody['message']); 10 | ErrorController()._showErrorSnackbar(scaffoldKey, message); 11 | } 12 | 13 | static showNoInternetError(GlobalKey scaffoldKey) { 14 | ErrorController()._showErrorSnackbar(scaffoldKey, 'No internet connection'); 15 | } 16 | 17 | static showNoServerError(GlobalKey scaffoldKey) { 18 | ErrorController()._showErrorSnackbar(scaffoldKey, 'Failed to reach server'); 19 | } 20 | 21 | static showFormatExceptionError( 22 | GlobalKey scaffoldKey, 23 | ) { 24 | ErrorController()._showErrorSnackbar(scaffoldKey, 'Bad format error'); 25 | } 26 | 27 | static showUnKownError(GlobalKey scaffoldKey) { 28 | ErrorController()._showErrorSnackbar(scaffoldKey, 'Unknown error'); 29 | } 30 | 31 | static showCustomError(GlobalKey scaffoldKey, String message) { 32 | ErrorController()._showErrorSnackbar(scaffoldKey, message); 33 | } 34 | 35 | _showErrorSnackbar(GlobalKey scaffoldKey, String message) { 36 | GlobalSnackBar.showSnackbar(scaffoldKey, message, SnackBarType.Error); 37 | } 38 | 39 | _formatErrorFromApi(String message) { 40 | switch (message) { 41 | case 'Jwt token is invalid': 42 | return message = 'Authentication failed'; 43 | break; 44 | default: 45 | return message; 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /lib/widgets/order_history_item.dart: -------------------------------------------------------------------------------- 1 | import 'package:ecommerceapp/constants/tasks.dart'; 2 | import 'package:ecommerceapp/controllers/activity_tracker_controller.dart'; 3 | import 'package:ecommerceapp/controllers/order_controller.dart'; 4 | import 'package:ecommerceapp/models/order.dart'; 5 | import 'package:ecommerceapp/screens/single_order.dart'; 6 | import 'package:ecommerceapp/utils/date_util.dart'; 7 | import 'package:flutter/material.dart'; 8 | import 'package:provider/provider.dart'; 9 | 10 | class OrderHistoryItem extends StatelessWidget { 11 | final Order order; 12 | const OrderHistoryItem({ 13 | Key key, 14 | @required this.order, 15 | }) : super(key: key); 16 | 17 | @override 18 | Widget build(BuildContext context) { 19 | return Padding( 20 | padding: const EdgeInsets.only(bottom: 8.0), 21 | child: ListTile( 22 | leading: Icon(Icons.info), 23 | trailing: Icon(Icons.info_outlined), 24 | title: Text( 25 | 'Order ${order.id}', 26 | style: TextStyle( 27 | fontSize: 18, 28 | ), 29 | ), 30 | subtitle: Text( 31 | 'Made on ${DateUtils().getFormattedDate(order.dateOrdered.toString())}', 32 | style: TextStyle( 33 | fontSize: 16, 34 | ), 35 | ), 36 | tileColor: Colors.grey[200], 37 | contentPadding: EdgeInsets.all(10.0), 38 | onTap: () { 39 | Provider.of(context, listen: false) 40 | .setTaskCurrentTask(VIEWING_SINGLE_OLD_ORDER_HISTORY); 41 | 42 | Provider.of(context, listen: false) 43 | .setSingleOrder(order); 44 | 45 | Navigator.pushNamed(context, SingleOrder.id); 46 | }, 47 | ), 48 | ); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /lib/widgets/guest_user_drawer_widget.dart: -------------------------------------------------------------------------------- 1 | import 'package:ecommerceapp/controllers/activity_tracker_controller.dart'; 2 | import 'package:ecommerceapp/screens/auth_screen.dart'; 3 | import 'package:flutter/material.dart'; 4 | import 'package:provider/provider.dart'; 5 | import 'package:flutter/foundation.dart'; 6 | 7 | class GuestUserDrawerWidget extends StatelessWidget { 8 | final String message; 9 | final String currentTask; 10 | const GuestUserDrawerWidget( 11 | {Key key, @required this.message, @required this.currentTask}) 12 | : super(key: key); 13 | 14 | @override 15 | Widget build(BuildContext context) { 16 | return Container( 17 | margin: EdgeInsets.only(top: MediaQuery.of(context).size.height * 0.3), 18 | child: Column( 19 | children: [ 20 | Text( 21 | '$message', 22 | style: TextStyle( 23 | fontSize: 18, 24 | ), 25 | ), 26 | SizedBox( 27 | height: 50, 28 | ), 29 | ButtonTheme( 30 | minWidth: 150, 31 | child: RaisedButton( 32 | elevation: 0, 33 | shape: RoundedRectangleBorder( 34 | borderRadius: BorderRadius.circular( 35 | 10, 36 | ), 37 | ), 38 | color: Colors.orange, 39 | onPressed: () { 40 | Provider.of(context, listen: false) 41 | .setTaskCurrentTask(currentTask); 42 | Navigator.pushReplacementNamed(context, AuthScreen.id); 43 | }, 44 | child: Text( 45 | 'SIGN IN', 46 | style: TextStyle( 47 | color: Colors.white, 48 | fontWeight: FontWeight.bold, 49 | ), 50 | ), 51 | ), 52 | ), 53 | ], 54 | ), 55 | ); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Ecommerce App 2 | 3 | - An ecommerce app built using Flutter 4 | 5 | - To run open terminal and execute ```flutter run``` 6 | 7 | ## Sample Screenshots 8 | 9 | 10 | 11 | 12 | 13 | 14 |
15 | 16 | 17 | 18 | ## Features 19 | 20 | - View products 21 | - View product details 22 | - Add to cart 23 | - Increase or decrease item's quantity 24 | - Save cart if user is logged in 25 | - Payment using paypal and stripe 26 | - Prompt user to continue as guest or sign in - if not logged in or when login session has expired 27 | - Ask user to login in if they want to view order history or profile 28 | and they haven't signed-in or login session has expired 29 | - Can then view single order history 30 | - Product list page reload with refresh indicator 31 | - Log out 32 | - Can view single order history after successful order 33 | - Checkout 34 | - Search by name or category 35 | - View products by category 36 | 37 | ## Note 38 | 39 | - To test payment using stripe(Credit card) You'll have to go to [stripe dashboard](https://dashboard.stripe.com/) and pick your 40 | ```client secret```. 41 | Open ``` lib/services/stripe.service.dart ```. 42 | Include it on line 16 ``` static String secret = 'your-secret'; ```. 43 | If you don't the payment process will always get cancelled. You don't have to do this for paypal as it is handled fully by the API. 44 | It has been tested and works fine with secret added. I removed mine for security reasons. I'm working on a way of including it from backend coupled with encryption so that you don't have to insert your on secret inorder to test the feature. As that is in the pipeline please follow the above mentioned steps. 45 | 46 | - This app has been developed on windows and tested on only android using ```Flutter 1.22.4 • channel stable``` and ``` Dart 2.10.4 ``` 47 | -------------------------------------------------------------------------------- /lib/controllers/category_controller.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | 3 | import 'package:ecommerceapp/controllers/error_controller.dart'; 4 | import 'package:ecommerceapp/models/category.dart'; 5 | import 'package:ecommerceapp/services/category_service.dart'; 6 | import 'dart:convert'; 7 | 8 | import 'package:flutter/foundation.dart'; 9 | import 'package:flutter/material.dart'; 10 | 11 | class CategoryController extends ChangeNotifier { 12 | final _categoryService = CategoryService(); 13 | 14 | var _isLoadingCategories = true; 15 | 16 | var _categoryList = List(); 17 | 18 | List get categoryList => _categoryList; 19 | 20 | bool get isLoadingCategories => _isLoadingCategories; 21 | 22 | setIsLoadingCategories(bool value) { 23 | _isLoadingCategories = value; 24 | notifyListeners(); 25 | } 26 | 27 | void getAllCategories(GlobalKey scaffoldKey) async { 28 | try { 29 | _isLoadingCategories = true; 30 | //important when refresh indicator is called 31 | //to avoid add same items 32 | _categoryList.clear(); 33 | 34 | var response = await _categoryService.getCategories(); 35 | 36 | if (response.statusCode == 200) { 37 | var jsonBody = json.decode(response.body); 38 | var jsonCategories = jsonBody['data']['categories']; 39 | _categoryList.addAll(categoryFromJson(json.encode(jsonCategories))); 40 | _isLoadingCategories = false; 41 | notifyListeners(); 42 | } else { 43 | ErrorController.showErrorFromApi(scaffoldKey, response); 44 | } 45 | } on SocketException catch (_) { 46 | ErrorController.showNoInternetError(scaffoldKey); 47 | } on HttpException catch (_) { 48 | ErrorController.showNoServerError(scaffoldKey); 49 | } on FormatException catch (_) { 50 | ErrorController.showFormatExceptionError(scaffoldKey); 51 | } catch (e) { 52 | print("Error ${e.toString()}"); 53 | ErrorController.showUnKownError(scaffoldKey); 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /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 | 28 | android { 29 | compileSdkVersion 28 30 | 31 | sourceSets { 32 | main.java.srcDirs += 'src/main/kotlin' 33 | } 34 | 35 | lintOptions { 36 | disable 'InvalidPackage' 37 | } 38 | 39 | defaultConfig { 40 | // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). 41 | applicationId "com.example.ecommerceapp" 42 | minSdkVersion 21 43 | targetSdkVersion 28 44 | versionCode flutterVersionCode.toInteger() 45 | versionName flutterVersionName 46 | } 47 | 48 | buildTypes { 49 | release { 50 | // TODO: Add your own signing config for the release build. 51 | // Signing with the debug keys for now, so `flutter run --release` works. 52 | signingConfig signingConfigs.debug 53 | } 54 | } 55 | } 56 | 57 | flutter { 58 | source '../..' 59 | } 60 | 61 | dependencies { 62 | implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" 63 | implementation 'io.card:android-sdk:5.+' 64 | 65 | } 66 | -------------------------------------------------------------------------------- /lib/widgets/drawer.dart: -------------------------------------------------------------------------------- 1 | import 'package:ecommerceapp/screens/order_history.dart'; 2 | import 'package:ecommerceapp/screens/profile.dart'; 3 | import 'package:flutter/material.dart'; 4 | 5 | class CDrawer extends StatefulWidget { 6 | CDrawer({Key key}) : super(key: key); 7 | 8 | @override 9 | _CDrawerState createState() => _CDrawerState(); 10 | } 11 | 12 | class _CDrawerState extends State { 13 | @override 14 | Widget build(BuildContext context) { 15 | return Drawer( 16 | child: ListView( 17 | children: [ 18 | DrawerHeader( 19 | child: Image( 20 | image: AssetImage("assets/images/logo.png"), 21 | fit: BoxFit.fill, 22 | ), 23 | ), 24 | SizedBox(height: 50), 25 | Padding( 26 | padding: const EdgeInsets.only(left: 15.0, right: 10.0), 27 | child: ListTile( 28 | tileColor: Colors.grey[200], 29 | leading: Icon(Icons.person), 30 | trailing: Icon(Icons.people), 31 | title: Text( 32 | 'Profile', 33 | style: TextStyle( 34 | fontSize: 15, 35 | ), 36 | ), 37 | onTap: () { 38 | Navigator.pop(context); 39 | Navigator.pushNamed(context, Profile.id); 40 | }, 41 | ), 42 | ), 43 | SizedBox(height: 50), 44 | Padding( 45 | padding: const EdgeInsets.only(left: 15.0, right: 10.0), 46 | child: ListTile( 47 | tileColor: Colors.grey[200], 48 | leading: Icon(Icons.history), 49 | trailing: Icon(Icons.history_edu), 50 | title: Text( 51 | 'Order history', 52 | style: TextStyle( 53 | fontSize: 15, 54 | ), 55 | ), 56 | onTap: () { 57 | Navigator.pop(context); 58 | Navigator.pushNamed(context, OrderHistroy.id); 59 | }, 60 | ), 61 | ), 62 | ], 63 | ), 64 | ); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /lib/models/order.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | 3 | import 'package:ecommerceapp/models/cart_item.dart'; 4 | import 'package:ecommerceapp/models/shipping_details.dart'; 5 | 6 | Order orderFromJson(String str) => Order.fromJson(json.decode(str)); 7 | 8 | List ordersFromJson(String str) => 9 | List.from(json.decode(str).map((x) => Order.fromJson(x))); 10 | 11 | String orderToJson(Order data) => json.encode(data.toJson()); 12 | 13 | class Order { 14 | Order({ 15 | this.userId, 16 | this.dateOrdered, 17 | this.id, 18 | this.shippingDetails, 19 | this.shippingCost, 20 | this.tax, 21 | this.total, 22 | this.totalItemPrice, 23 | this.paymentMethod, 24 | this.userType, 25 | this.cartItems, 26 | this.v, 27 | }); 28 | 29 | dynamic userId; 30 | DateTime dateOrdered; 31 | String id; 32 | ShippingDetails shippingDetails; 33 | String shippingCost; 34 | String tax; 35 | String total; 36 | String totalItemPrice; 37 | String paymentMethod; 38 | String userType; 39 | List cartItems; 40 | int v; 41 | 42 | factory Order.fromJson(Map jsonData) => Order( 43 | userId: jsonData["userId"], 44 | dateOrdered: DateTime.parse(jsonData["dateOrdered"]), 45 | id: jsonData["_id"], 46 | shippingDetails: ShippingDetails.fromJson(jsonData["shippingDetails"]), 47 | shippingCost: jsonData["shippingCost"], 48 | tax: jsonData["tax"], 49 | total: jsonData["total"], 50 | totalItemPrice: jsonData["totalItemPrice"], 51 | paymentMethod: jsonData["paymentMethod"], 52 | userType: jsonData["userType"], 53 | cartItems: cartItemFromJson(json.encode(jsonData["cartItems"])), 54 | v: jsonData["__v"], 55 | ); 56 | 57 | Map toJson() => { 58 | "userId": userId, 59 | "dateOrdered": dateOrdered.toIso8601String(), 60 | "shippingDetails": shippingDetails.toJson(), 61 | "shippingCost": shippingCost, 62 | "tax": tax, 63 | "total": total, 64 | "totalItemPrice": totalItemPrice, 65 | "paymentMethod": paymentMethod, 66 | "userType": userType, 67 | "cartItems": json.decode(cartItemToJson(cartItems)), 68 | }; 69 | } 70 | -------------------------------------------------------------------------------- /lib/widgets/product_detail_bottomsheet_content.dart: -------------------------------------------------------------------------------- 1 | import 'package:ecommerceapp/screens/shopping_cart.dart'; 2 | import 'package:ecommerceapp/widgets/cart_button.dart'; 3 | import 'package:flutter/material.dart'; 4 | 5 | class ProductDetailBottomSheetContent extends StatelessWidget { 6 | const ProductDetailBottomSheetContent({ 7 | Key key, 8 | @required cartCtlr, 9 | }) : _cartCtlr = cartCtlr, 10 | super(key: key); 11 | 12 | final _cartCtlr; 13 | 14 | @override 15 | Widget build(BuildContext context) { 16 | return Container( 17 | height: 120, 18 | color: Colors.grey[200], 19 | child: Column( 20 | children: [ 21 | SizedBox( 22 | height: 10, 23 | ), 24 | Text( 25 | '${_cartCtlr.selectedItem.product.name} already in cart', 26 | style: TextStyle( 27 | fontSize: 20, 28 | fontWeight: FontWeight.bold, 29 | ), 30 | ), 31 | SizedBox( 32 | height: 10, 33 | ), 34 | Row( 35 | mainAxisAlignment: MainAxisAlignment.spaceAround, 36 | children: [ 37 | RaisedButton( 38 | elevation: 0, 39 | shape: RoundedRectangleBorder( 40 | borderRadius: BorderRadius.circular(18.0), 41 | side: BorderSide(color: Colors.red)), 42 | color: Colors.red, 43 | onPressed: () { 44 | _cartCtlr.removeFromCart(_cartCtlr.selectedItem); 45 | Navigator.pop(context); 46 | }, 47 | child: Text( 48 | 'REMOVE', 49 | style: TextStyle( 50 | color: Colors.white, 51 | ), 52 | ), 53 | ), 54 | Text('OR'), 55 | InkWell( 56 | child: CartButton( 57 | text: 'View cart', 58 | width: MediaQuery.of(context).size.width * 0.25, 59 | ), 60 | onTap: () { 61 | Navigator.pop(context); 62 | Navigator.pushNamed(context, ShoppingCart.id); 63 | }, 64 | ) 65 | ], 66 | ), 67 | ], 68 | ), 69 | ); 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /lib/widgets/product_card.dart: -------------------------------------------------------------------------------- 1 | import 'package:ecommerceapp/models/product.dart'; 2 | import 'package:flutter/material.dart'; 3 | 4 | class ProductCard extends StatelessWidget { 5 | final Product product; 6 | final Function onProductTapped; 7 | 8 | const ProductCard({ 9 | Key key, 10 | @required this.product, 11 | @required this.onProductTapped, 12 | }) : super(key: key); 13 | @override 14 | Widget build(BuildContext context) { 15 | return GestureDetector( 16 | onTap: onProductTapped, 17 | child: Container( 18 | color: Colors.grey[200], 19 | child: Column( 20 | mainAxisAlignment: MainAxisAlignment.spaceBetween, 21 | crossAxisAlignment: CrossAxisAlignment.stretch, 22 | children: [ 23 | Expanded( 24 | child: Image.network( 25 | product.imageUrl, 26 | fit: BoxFit.fill, 27 | errorBuilder: (BuildContext context, Object exception, 28 | StackTrace stackTrace) { 29 | return Center( 30 | child: Icon(Icons.error), 31 | ); 32 | }, 33 | loadingBuilder: (BuildContext context, Widget child, 34 | ImageChunkEvent loadingProgress) { 35 | if (loadingProgress == null) return child; 36 | return Center( 37 | child: CircularProgressIndicator( 38 | strokeWidth: 2.0, 39 | ), 40 | ); 41 | }, 42 | ), 43 | ), 44 | Padding( 45 | padding: const EdgeInsets.only( 46 | top: 10.0, 47 | bottom: 8.0, 48 | left: 8.0, 49 | right: 8.0, 50 | ), 51 | child: Row( 52 | mainAxisAlignment: MainAxisAlignment.spaceBetween, 53 | crossAxisAlignment: CrossAxisAlignment.end, 54 | children: [ 55 | Text( 56 | product.name ?? '', 57 | style: TextStyle(fontWeight: FontWeight.bold), 58 | ), 59 | Text(product.price.toString() ?? ''), 60 | ], 61 | ), 62 | ), 63 | ], 64 | ), 65 | ), 66 | ); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /lib/widgets/shopping_cart_bottom_sheet.dart: -------------------------------------------------------------------------------- 1 | import 'package:ecommerceapp/screens/auth_screen.dart'; 2 | import 'package:ecommerceapp/screens/shipping.dart'; 3 | import 'package:flutter/material.dart'; 4 | 5 | class ShoppingCartBottomSheet extends StatelessWidget { 6 | final String message; 7 | const ShoppingCartBottomSheet({ 8 | this.message, 9 | Key key, 10 | }) : super(key: key); 11 | 12 | @override 13 | Widget build(BuildContext context) { 14 | return Container( 15 | height: 200, 16 | width: MediaQuery.of(context).size.width, 17 | color: Colors.grey[300], 18 | child: Column( 19 | children: [ 20 | SizedBox( 21 | height: 20, 22 | ), 23 | Text( 24 | "${message != null ? message : ''}", 25 | style: TextStyle( 26 | fontWeight: FontWeight.bold, 27 | ), 28 | ), 29 | RaisedButton( 30 | elevation: 0, 31 | shape: RoundedRectangleBorder( 32 | borderRadius: BorderRadius.circular(18.0), 33 | ), 34 | color: Colors.red[900], 35 | onPressed: () { 36 | Navigator.pushNamed(context, AuthScreen.id); 37 | }, 38 | child: Text( 39 | 'Sign In', 40 | style: TextStyle( 41 | color: Colors.white, 42 | fontSize: 18, 43 | ), 44 | ), 45 | ), 46 | SizedBox( 47 | height: 20, 48 | ), 49 | Text( 50 | 'OR', 51 | style: TextStyle(fontWeight: FontWeight.bold), 52 | ), 53 | SizedBox( 54 | height: 20, 55 | ), 56 | RaisedButton( 57 | elevation: 0, 58 | shape: RoundedRectangleBorder( 59 | borderRadius: BorderRadius.circular(18.0), 60 | ), 61 | color: Colors.orange[900], 62 | onPressed: () { 63 | Navigator.pop(context); 64 | Navigator.pushNamed(context, Shipping.id); 65 | }, 66 | child: Text( 67 | 'Continue as Guest', 68 | style: TextStyle( 69 | color: Colors.white, 70 | fontSize: 18, 71 | ), 72 | ), 73 | ), 74 | ], 75 | ), 76 | ); 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /lib/screens/thank_you.dart: -------------------------------------------------------------------------------- 1 | import 'package:ecommerceapp/constants/screen_ids.dart'; 2 | import 'package:ecommerceapp/constants/screen_titles.dart'; 3 | import 'package:ecommerceapp/screens/products_list.dart'; 4 | import 'package:ecommerceapp/screens/single_order.dart'; 5 | import 'package:flutter/material.dart'; 6 | 7 | class Thanks extends StatelessWidget { 8 | const Thanks({Key key}) : super(key: key); 9 | static String id = ThankYou_Screen_Id; 10 | 11 | @override 12 | Widget build(BuildContext context) { 13 | Future _onBackPressed() { 14 | Navigator.pushNamedAndRemoveUntil( 15 | context, ProductList.id, (route) => false); 16 | return Future.value(true); 17 | } 18 | 19 | return WillPopScope( 20 | onWillPop: _onBackPressed, 21 | child: SafeArea( 22 | child: Scaffold( 23 | appBar: AppBar( 24 | title: Text( 25 | '$Thanks_Screen_Title', 26 | style: TextStyle( 27 | color: Colors.black, 28 | ), 29 | ), 30 | iconTheme: IconThemeData(color: Colors.black), 31 | elevation: 1, 32 | backgroundColor: Colors.white, 33 | ), 34 | body: Container( 35 | child: Center( 36 | child: Column( 37 | crossAxisAlignment: CrossAxisAlignment.center, 38 | mainAxisAlignment: MainAxisAlignment.center, 39 | children: [ 40 | Icon( 41 | Icons.star, 42 | size: 40, 43 | ), 44 | SizedBox( 45 | height: 20, 46 | ), 47 | Text( 48 | 'Payment made successfully', 49 | style: TextStyle( 50 | fontSize: 20, 51 | ), 52 | ), 53 | SizedBox( 54 | height: 20, 55 | ), 56 | RaisedButton( 57 | onPressed: () { 58 | Navigator.pushNamed(context, SingleOrder.id); 59 | }, 60 | child: Text( 61 | "VIEW ORDER", 62 | style: TextStyle( 63 | color: Colors.white, 64 | ), 65 | ), 66 | color: Colors.orange, 67 | shape: RoundedRectangleBorder( 68 | borderRadius: BorderRadius.circular(50), 69 | ), 70 | ), 71 | ], 72 | ), 73 | ), 74 | ), 75 | ), 76 | ), 77 | ); 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /lib/services/auth_service.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | 3 | import 'package:ecommerceapp/application.properties/app_properties.dart'; 4 | import 'package:ecommerceapp/models/authdata.dart'; 5 | import 'package:http/http.dart' as http; 6 | 7 | class AuthService { 8 | static AuthService _authService; 9 | 10 | AuthService._internal() { 11 | _authService = this; 12 | } 13 | 14 | factory AuthService() => _authService ?? AuthService._internal(); 15 | 16 | static var httpClient = http.Client(); 17 | 18 | Map headers = {'Content-Type': 'application/json'}; 19 | 20 | Future emailNameAndPasswordSignUp( 21 | String name, 22 | String email, 23 | String password, 24 | ) async { 25 | var authData = AuthData(name: name, email: email, password: password); 26 | 27 | return await http.post( 28 | AppProperties.signUpUrl, 29 | body: json.encode(authData.toJson()), 30 | headers: headers, 31 | ); 32 | } 33 | 34 | Future emailAndPasswordSignIn( 35 | String email, 36 | String password, 37 | ) async { 38 | var authData = AuthData(name: '', email: email, password: password); 39 | 40 | return await http.post( 41 | AppProperties.signInUrl, 42 | body: json.encode(authData.toJson()), 43 | headers: headers, 44 | ); 45 | } 46 | 47 | Future checkTokenExpiry(String token) async { 48 | var tokenObject = Map(); 49 | tokenObject.putIfAbsent('token', () => token); 50 | 51 | return await http.post( 52 | AppProperties.checkTokenExpiryUrl, 53 | body: json.encode(tokenObject), 54 | headers: headers, 55 | ); 56 | } 57 | 58 | Future changeName(String name, String userId, String jwtToken) async { 59 | headers.putIfAbsent('Authorization', () => 'Bearer $jwtToken'); 60 | var bodyObject = Map(); 61 | bodyObject.putIfAbsent('name', () => name); 62 | 63 | return await http.patch( 64 | "${AppProperties.changenameUrl}$userId", 65 | headers: headers, 66 | body: json.encode(bodyObject), 67 | ); 68 | } 69 | 70 | Future changeEmail(String email, String userId, String jwtToken) async { 71 | headers.putIfAbsent('Authorization', () => 'Bearer $jwtToken'); 72 | var bodyObject = Map(); 73 | bodyObject.putIfAbsent('email', () => email); 74 | 75 | return await http.patch( 76 | "${AppProperties.changeMailUrl}$userId", 77 | headers: headers, 78 | body: json.encode(bodyObject), 79 | ); 80 | } 81 | 82 | Future forgotPassword(String email) async { 83 | var bodyObject = Map(); 84 | bodyObject.putIfAbsent('email', () => email); 85 | return await http.post( 86 | "${AppProperties.forgotPasswordUrl}", 87 | headers: headers, 88 | body: json.encode(bodyObject), 89 | ); 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /lib/main.dart: -------------------------------------------------------------------------------- 1 | import 'package:ecommerceapp/controllers/activity_tracker_controller.dart'; 2 | import 'package:ecommerceapp/controllers/cart_controller.dart'; 3 | import 'package:ecommerceapp/controllers/category_controller.dart'; 4 | import 'package:ecommerceapp/controllers/order_controller.dart'; 5 | import 'package:ecommerceapp/controllers/product_controller.dart'; 6 | import 'package:ecommerceapp/controllers/shipping_controller.dart'; 7 | import 'package:ecommerceapp/screens/auth_screen.dart'; 8 | import 'package:ecommerceapp/screens/order_history.dart'; 9 | import 'package:ecommerceapp/screens/payment_method.dart'; 10 | import 'package:ecommerceapp/screens/product_detail.dart'; 11 | import 'package:ecommerceapp/screens/products_list.dart'; 12 | import 'package:ecommerceapp/screens/profile.dart'; 13 | import 'package:ecommerceapp/screens/shipping.dart'; 14 | import 'package:ecommerceapp/screens/shopping_cart.dart'; 15 | import 'package:ecommerceapp/screens/single_order.dart'; 16 | import 'package:ecommerceapp/screens/thank_you.dart'; 17 | import 'package:flutter/material.dart'; 18 | import 'package:flutter/services.dart'; 19 | import 'package:provider/provider.dart'; 20 | 21 | void main() { 22 | runApp(EcommerceApp()); 23 | } 24 | 25 | class EcommerceApp extends StatelessWidget { 26 | @override 27 | Widget build(BuildContext context) { 28 | SystemChrome.setPreferredOrientations( 29 | [DeviceOrientation.portraitUp, DeviceOrientation.portraitDown]); 30 | 31 | SystemChrome.setSystemUIOverlayStyle(SystemUiOverlayStyle( 32 | statusBarColor: Colors.grey, 33 | statusBarIconBrightness: Brightness.dark, 34 | systemNavigationBarColor: Colors.grey, 35 | systemNavigationBarIconBrightness: Brightness.light, 36 | )); 37 | 38 | return MultiProvider( 39 | providers: [ 40 | ChangeNotifierProvider(create: (context) => ProductController()), 41 | ChangeNotifierProvider(create: (context) => CategoryController()), 42 | ChangeNotifierProvider(create: (context) => CartController()), 43 | ChangeNotifierProvider(create: (context) => ShippingController()), 44 | ChangeNotifierProvider(create: (context) => OrderController()), 45 | ChangeNotifierProvider(create: (context) => ActivityTracker()), 46 | ], 47 | child: MaterialApp( 48 | debugShowCheckedModeBanner: false, 49 | title: 'Ecommerce app', 50 | theme: ThemeData( 51 | primarySwatch: Colors.grey, 52 | visualDensity: VisualDensity.adaptivePlatformDensity, 53 | ), 54 | initialRoute: ProductList.id, 55 | routes: { 56 | ProductList.id: (context) => ProductList(), 57 | ShoppingCart.id: (context) => ShoppingCart(), 58 | ProductDetail.id: (context) => ProductDetail(), 59 | Shipping.id: (context) => Shipping(), 60 | PaymentMethod.id: (context) => PaymentMethod(), 61 | Thanks.id: (context) => Thanks(), 62 | SingleOrder.id: (context) => SingleOrder(), 63 | AuthScreen.id: (context) => AuthScreen(), 64 | OrderHistroy.id: (context) => OrderHistroy(), 65 | Profile.id: (context) => Profile() 66 | }, 67 | ), 68 | ); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /lib/services/stripe_service.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | 3 | import 'package:flutter/services.dart'; 4 | import 'package:stripe_payment/stripe_payment.dart'; 5 | import 'package:http/http.dart' as http; 6 | 7 | class StripeTransactionResponse { 8 | String message; 9 | bool success; 10 | StripeTransactionResponse({this.message, this.success}); 11 | } 12 | 13 | class StripeService { 14 | static String apiBase = 'https://api.stripe.com/v1'; 15 | static String paymentApiUrl = '${StripeService.apiBase}/payment_intents'; 16 | static String secret = ''; 17 | static String publishableKey = 'pk_test_nGOzznmrg37WxxREAMw8jNHj00ukQ2wSgi'; 18 | static Map headers = { 19 | 'Authorization': 'Bearer ${StripeService.secret}', 20 | 'Content-Type': 'application/x-www-form-urlencoded' 21 | }; 22 | 23 | static init() { 24 | StripePayment.setOptions( 25 | StripeOptions( 26 | publishableKey: publishableKey, 27 | merchantId: "Test", 28 | androidPayMode: "test"), 29 | ); 30 | } 31 | 32 | static Future processPayment( 33 | String amount, currency) async { 34 | try { 35 | var paymentMethod = await StripePayment.paymentRequestWithCardForm( 36 | CardFormPaymentRequest()); 37 | var paymentIntent = await StripeService.createPaymentIntent( 38 | amount, 39 | currency, 40 | ); 41 | 42 | var response = await StripePayment.confirmPaymentIntent( 43 | PaymentIntent( 44 | clientSecret: paymentIntent['client_secret'], 45 | paymentMethodId: paymentMethod.id, 46 | ), 47 | ); 48 | 49 | if (response.status == 'succeeded') { 50 | return new StripeTransactionResponse( 51 | message: 'Transaction successful', 52 | success: true, 53 | ); 54 | } else { 55 | return new StripeTransactionResponse( 56 | message: 'Transaction failed', 57 | success: false, 58 | ); 59 | } 60 | } on PlatformException catch (err) { 61 | return StripeService.getPlatformExceptionErrorResult(err); 62 | } catch (e) { 63 | return new StripeTransactionResponse( 64 | message: 'Transaction failed: ${e.toString()}', 65 | success: false, 66 | ); 67 | } 68 | } 69 | 70 | static getPlatformExceptionErrorResult(err) { 71 | String message = 'Something went wrong'; 72 | if (err.code == 'cancelled') { 73 | message = 'Transaction cancelled'; 74 | } 75 | 76 | return new StripeTransactionResponse(message: message, success: false); 77 | } 78 | 79 | static Future> createPaymentIntent( 80 | String amount, String currency) async { 81 | try { 82 | Map body = { 83 | 'amount': amount, 84 | 'currency': currency, 85 | 'payment_method_types[]': 'card' 86 | }; 87 | 88 | var response = await http.post( 89 | StripeService.paymentApiUrl, 90 | body: body, 91 | headers: StripeService.headers, 92 | ); 93 | 94 | return jsonDecode(response.body); 95 | } catch (err) { 96 | print('error charging user: ${err.toString()}'); 97 | } 98 | return null; 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: ecommerceapp 2 | description: An ecommerce flutter app 3 | 4 | # The following line prevents the package from being accidentally published to 5 | # pub.dev using `pub publish`. This is preferred for private packages. 6 | publish_to: 'none' # Remove this line if you wish to publish to pub.dev 7 | 8 | # The following defines the version and build number for your application. 9 | # A version number is three numbers separated by dots, like 1.2.43 10 | # followed by an optional build number separated by a +. 11 | # Both the version and the builder number may be overridden in flutter 12 | # build by specifying --build-name and --build-number, respectively. 13 | # In Android, build-name is used as versionName while build-number used as versionCode. 14 | # Read more about Android versioning at https://developer.android.com/studio/publish/versioning 15 | # In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion. 16 | # Read more about iOS versioning at 17 | # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html 18 | version: 1.0.0+1 19 | 20 | environment: 21 | sdk: ">=2.7.0 <3.0.0" 22 | 23 | dependencies: 24 | flutter: 25 | sdk: flutter 26 | http: ^0.12.2 27 | stripe_payment: ^1.0.8 28 | flutter_braintree: ^1.1.0 29 | shimmer: ^1.1.2 30 | provider: ^4.3.2+2 31 | badges: ^1.1.4 32 | flutter_secure_storage: ^3.3.5 33 | progress_dialog: ^1.2.4 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | # The following adds the Cupertino Icons font to your application. 47 | # Use with the CupertinoIcons class for iOS style icons. 48 | cupertino_icons: ^0.1.3 49 | 50 | dev_dependencies: 51 | flutter_test: 52 | sdk: flutter 53 | mockito: ^4.1.3 54 | 55 | 56 | # For information on the generic Dart part of this file, see the 57 | # following page: https://dart.dev/tools/pub/pubspec 58 | 59 | # The following section is specific to Flutter. 60 | flutter: 61 | 62 | # The following line ensures that the Material Icons font is 63 | # included with your application, so that you can use the icons in 64 | # the material Icons class. 65 | uses-material-design: true 66 | 67 | # To add assets to your application, add an assets section, like this: 68 | assets: 69 | - assets/images/ 70 | 71 | # An image asset can refer to one or more resolution-specific "variants", see 72 | # https://flutter.dev/assets-and-images/#resolution-aware. 73 | 74 | # For details regarding adding assets from package dependencies, see 75 | # https://flutter.dev/assets-and-images/#from-packages 76 | 77 | # To add custom fonts to your application, add a fonts section here, 78 | # in this "flutter" section. Each entry in this list should have a 79 | # "family" key with the font family name, and a "fonts" key with a 80 | # list giving the asset and other descriptors for the font. For 81 | # example: 82 | # fonts: 83 | # - family: Schyler 84 | # fonts: 85 | # - asset: fonts/Schyler-Regular.ttf 86 | # - asset: fonts/Schyler-Italic.ttf 87 | # style: italic 88 | # - family: Trajan Pro 89 | # fonts: 90 | # - asset: fonts/TrajanPro.ttf 91 | # - asset: fonts/TrajanPro_Bold.ttf 92 | # weight: 700 93 | # 94 | # For details regarding fonts from package dependencies, 95 | # see https://flutter.dev/custom-fonts/#from-packages 96 | -------------------------------------------------------------------------------- /android/app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 8 | 9 | 16 | 23 | 27 | 31 | 36 | 40 | 41 | 42 | 43 | 44 | 45 | 47 | 50 | 51 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | -------------------------------------------------------------------------------- /ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 32 | 33 | 39 | 40 | 41 | 42 | 43 | 44 | 54 | 56 | 62 | 63 | 64 | 65 | 66 | 67 | 73 | 75 | 81 | 82 | 83 | 84 | 86 | 87 | 90 | 91 | 92 | -------------------------------------------------------------------------------- /lib/controllers/product_controller.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | 3 | import 'package:ecommerceapp/controllers/error_controller.dart'; 4 | import 'package:ecommerceapp/models/product.dart'; 5 | import 'package:ecommerceapp/services/product_service.dart'; 6 | import 'package:flutter/cupertino.dart'; 7 | import 'dart:convert'; 8 | 9 | import 'package:flutter/foundation.dart'; 10 | import 'package:flutter/material.dart'; 11 | 12 | class ProductController extends ChangeNotifier { 13 | final _productService = ProductService(); 14 | 15 | var _productList = List(); 16 | 17 | bool _isLoadingAllProducts = true; 18 | 19 | bool get isLoadingAllProducts => _isLoadingAllProducts; 20 | 21 | setIsLoadingAllProducts(bool value) { 22 | _isLoadingAllProducts = value; 23 | notifyListeners(); 24 | } 25 | 26 | void getAllProducts(GlobalKey scaffoldKey) async { 27 | try { 28 | _isLoadingAllProducts = true; 29 | 30 | //important when refresh indicator is called 31 | //to avoid add same items 32 | _productList.clear(); 33 | 34 | var response = await _productService.getAllProducts(); 35 | 36 | if (response.statusCode == 200) { 37 | var responseJsonStr = json.decode(response.body); 38 | var jsonProd = responseJsonStr['data']['products']; 39 | _productList.addAll(productFromJson(json.encode(jsonProd))); 40 | _isLoadingAllProducts = false; 41 | 42 | notifyListeners(); 43 | } else { 44 | ErrorController.showErrorFromApi(scaffoldKey, response); 45 | } 46 | } on SocketException catch (_) { 47 | ErrorController.showNoInternetError(scaffoldKey); 48 | } on HttpException catch (_) { 49 | ErrorController.showNoServerError(scaffoldKey); 50 | } on FormatException catch (_) { 51 | ErrorController.showFormatExceptionError(scaffoldKey); 52 | } catch (e) { 53 | print("Error ${e.toString()}"); 54 | ErrorController.showUnKownError(scaffoldKey); 55 | } 56 | } 57 | 58 | List get productList => _productList; 59 | 60 | void getProductByCategory( 61 | String value, GlobalKey scaffoldKey) async { 62 | try { 63 | _isLoadingAllProducts = true; 64 | 65 | var response = value == 'All' 66 | ? await _productService.getAllProducts() 67 | : await _productService.getProductByCategory(value); 68 | 69 | if (response.statusCode == 200) { 70 | var responseJsonStr = json.decode(response.body); 71 | var jsonProd = value == 'All' 72 | ? responseJsonStr['data']['products'] 73 | : responseJsonStr['data']['result']; 74 | 75 | _productList.clear(); 76 | _productList.addAll(productFromJson(json.encode(jsonProd))); 77 | _isLoadingAllProducts = false; 78 | 79 | notifyListeners(); 80 | } else { 81 | ErrorController.showErrorFromApi(scaffoldKey, response); 82 | } 83 | } on SocketException catch (_) { 84 | ErrorController.showNoInternetError(scaffoldKey); 85 | } on HttpException catch (_) { 86 | ErrorController.showNoServerError(scaffoldKey); 87 | } on FormatException catch (_) { 88 | ErrorController.showFormatExceptionError(scaffoldKey); 89 | } catch (e) { 90 | print("Error ${e.toString()}"); 91 | ErrorController.showUnKownError(scaffoldKey); 92 | } 93 | } 94 | 95 | void getProductByCategoryOrName(String value) async { 96 | var finalSearchValue = value.trim(); 97 | try { 98 | _isLoadingAllProducts = true; 99 | 100 | var response = finalSearchValue == '' 101 | ? await _productService.getAllProducts() 102 | : await _productService.getProductByCategoryOrName(finalSearchValue); 103 | 104 | if (response.statusCode == 200) { 105 | var responseJsonStr = json.decode(response.body); 106 | var jsonProd = finalSearchValue == '' 107 | ? responseJsonStr['data']['products'] 108 | : responseJsonStr['data']['result']; 109 | 110 | _productList.clear(); 111 | _productList.addAll(productFromJson(json.encode(jsonProd))); 112 | _isLoadingAllProducts = false; 113 | notifyListeners(); 114 | } else { 115 | _isLoadingAllProducts = true; 116 | notifyListeners(); 117 | } 118 | } on SocketException catch (_) { 119 | _isLoadingAllProducts = true; 120 | notifyListeners(); 121 | } on HttpException catch (_) { 122 | _isLoadingAllProducts = true; 123 | notifyListeners(); 124 | } catch (e) { 125 | print("Error ${e.toString()}"); 126 | _isLoadingAllProducts = true; 127 | notifyListeners(); 128 | } 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /lib/screens/order_history.dart: -------------------------------------------------------------------------------- 1 | import 'package:ecommerceapp/constants/screen_ids.dart'; 2 | import 'package:ecommerceapp/constants/screen_titles.dart'; 3 | import 'package:ecommerceapp/constants/tasks.dart'; 4 | import 'package:ecommerceapp/controllers/auth_controller.dart'; 5 | import 'package:ecommerceapp/controllers/order_controller.dart'; 6 | import 'package:ecommerceapp/widgets/guest_user_drawer_widget.dart'; 7 | import 'package:ecommerceapp/widgets/no_order_history_content.dart'; 8 | import 'package:ecommerceapp/widgets/order_history_item.dart'; 9 | import 'package:flutter/material.dart'; 10 | import 'package:provider/provider.dart'; 11 | 12 | class OrderHistroy extends StatefulWidget { 13 | OrderHistroy({Key key}) : super(key: key); 14 | static String id = OrderHistoryScreen_Id; 15 | 16 | @override 17 | _OrderHistroyState createState() => _OrderHistroyState(); 18 | } 19 | 20 | class _OrderHistroyState extends State { 21 | var _authController; 22 | var _orderController; 23 | final _scaffoldKey = GlobalKey(); 24 | 25 | @override 26 | void initState() { 27 | super.initState(); 28 | _authController = AuthController(); 29 | _orderController = Provider.of(context, listen: false); 30 | } 31 | 32 | Future getTokenValidity() async { 33 | return await _authController.isTokenValid(); 34 | } 35 | 36 | Future> getLoginStatus() async { 37 | return await _authController.getUserDataAndLoginStatus(); 38 | } 39 | 40 | @override 41 | Widget build(BuildContext context) { 42 | return Scaffold( 43 | key: _scaffoldKey, 44 | resizeToAvoidBottomInset: false, 45 | appBar: AppBar( 46 | title: Text( 47 | "$OrderHistoryScreen_Title", 48 | style: TextStyle( 49 | color: Colors.black, 50 | ), 51 | ), 52 | iconTheme: IconThemeData(color: Colors.black), 53 | elevation: 0, 54 | backgroundColor: Colors.white, 55 | ), 56 | body: SafeArea( 57 | child: ListView( 58 | children: [ 59 | FutureBuilder( 60 | future: Future.wait([getTokenValidity(), getLoginStatus()]), 61 | builder: (context, snapshot) { 62 | if (!snapshot.hasData) { 63 | return Center( 64 | child: Container( 65 | margin: EdgeInsets.only( 66 | top: MediaQuery.of(context).size.height / 3), 67 | child: CircularProgressIndicator(), 68 | ), 69 | ); 70 | } 71 | 72 | var isLoggedInFlag = snapshot.data[1][1]; 73 | var isTokenValid = snapshot.data[0]; 74 | 75 | //when user is not signed in 76 | if (isLoggedInFlag == null || isLoggedInFlag == '0') { 77 | return GuestUserDrawerWidget( 78 | message: 'Sign in to see order history', 79 | currentTask: VIEWING_ORDER_HISTORY, 80 | ); 81 | } 82 | 83 | //when user token has expired 84 | if (!isTokenValid) { 85 | return GuestUserDrawerWidget( 86 | message: 'Session expired. Sign in to see order history', 87 | currentTask: VIEWING_ORDER_HISTORY, 88 | ); 89 | } 90 | 91 | _orderController.getOrders(_scaffoldKey); 92 | 93 | //user is logged in and token is valid 94 | return Consumer( 95 | builder: (context, orderController, child) { 96 | if (orderController.isLoadingOrders) { 97 | return Center( 98 | child: Container( 99 | margin: EdgeInsets.only( 100 | top: MediaQuery.of(context).size.height / 3), 101 | child: CircularProgressIndicator(), 102 | ), 103 | ); 104 | } 105 | if (orderController.orders.length == 0) { 106 | return Center( 107 | child: Container( 108 | margin: EdgeInsets.only( 109 | top: MediaQuery.of(context).size.height / 3), 110 | child: NoOrderHistoryFoundContent(), 111 | ), 112 | ); 113 | } 114 | return Column(children: [ 115 | ListView.builder( 116 | physics: ScrollPhysics(), 117 | shrinkWrap: true, 118 | itemCount: orderController.orders.length, 119 | itemBuilder: (context, index) { 120 | return OrderHistoryItem( 121 | order: orderController.orders[index], 122 | ); 123 | }) 124 | ]); 125 | }); 126 | }, 127 | ), 128 | ], 129 | ), 130 | ), 131 | ); 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /lib/widgets/auth_screen_custom_painter.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class AuthScreenCustomPainter extends CustomPainter { 4 | @override 5 | void paint(Canvas canvas, Size size) { 6 | Paint paint = Paint(); 7 | Path path = Path(); 8 | 9 | // Path number 1 10 | 11 | paint.color = Color(0xff56bfe5); 12 | path = Path(); 13 | path.lineTo(size.width * 0.76, size.height * 0.68); 14 | path.cubicTo(size.width * 0.71, size.height * 0.67, size.width * 0.66, 15 | size.height * 0.64, size.width * 0.6, size.height * 0.6); 16 | path.cubicTo(size.width * 0.53, size.height * 0.55, size.width * 0.41, 17 | size.height * 0.43, size.width * 0.41, size.height * 0.4); 18 | path.cubicTo(size.width * 0.41, size.height * 0.39, size.width * 0.52, 19 | size.height * 0.27, size.width * 0.76, size.height * 0.18); 20 | path.cubicTo(size.width * 0.84, size.height * 0.15, size.width * 0.94, 21 | size.height * 0.18, size.width * 0.96, size.height * 0.18); 22 | path.cubicTo(size.width * 0.97, size.height * 0.17, size.width, 23 | size.height * 0.16, size.width, size.height * 0.16); 24 | path.cubicTo(size.width, size.height * 0.16, size.width, size.height * 0.57, 25 | size.width, size.height * 0.6); 26 | path.cubicTo(size.width, size.height * 0.63, size.width * 0.96, 27 | size.height * 0.66, size.width * 0.91, size.height * 0.68); 28 | path.cubicTo(size.width * 0.87, size.height * 0.69, size.width * 0.82, 29 | size.height * 0.69, size.width * 0.76, size.height * 0.68); 30 | path.cubicTo(size.width * 0.76, size.height * 0.68, size.width * 0.76, 31 | size.height * 0.68, size.width * 0.76, size.height * 0.68); 32 | canvas.drawPath(path, paint); 33 | 34 | // Path number 2 35 | 36 | paint.color = Color(0xff4b515a); 37 | path = Path(); 38 | path.lineTo(0, size.height * 0.8); 39 | path.cubicTo(0, size.height * 0.8, size.width * 0.13, size.height, 40 | size.width * 0.27, size.height); 41 | path.cubicTo(size.width * 0.41, size.height, size.width * 0.43, 42 | size.height * 0.9, size.width * 0.48, size.height * 0.75); 43 | path.cubicTo(size.width * 0.51, size.height * 0.67, size.width * 0.51, 44 | size.height * 0.61, size.width * 0.63, size.height * 0.51); 45 | path.cubicTo(size.width * 0.77, size.height * 0.38, size.width, 46 | size.height * 0.28, size.width, size.height * 0.23); 47 | path.cubicTo(size.width, size.height * 0.17, size.width, 0, size.width, 0); 48 | path.cubicTo(size.width, 0, size.width * 0.4, 0, size.width * 0.4, 0); 49 | path.cubicTo(size.width * 0.4, 0, 0, size.height / 3, 0, size.height / 3); 50 | path.cubicTo( 51 | 0, size.height / 3, 0, size.height * 0.8, 0, size.height * 0.8); 52 | canvas.drawPath(path, paint); 53 | 54 | // Path number 3 55 | 56 | paint.color = Color(0xfffead40); 57 | path = Path(); 58 | path.lineTo(0, 0); 59 | path.cubicTo(0, 0, size.width * 0.57, 0, size.width * 0.57, 0); 60 | path.cubicTo(size.width * 0.57, 0, size.width * 0.56, size.height * 0.04, 61 | size.width * 0.43, size.height * 0.06); 62 | path.cubicTo(size.width * 0.29, size.height * 0.07, size.width / 4, 63 | size.height * 0.12, size.width * 0.19, size.height * 0.3); 64 | path.cubicTo(size.width * 0.13, size.height * 0.48, 0, size.height / 2, 0, 65 | size.height / 2); 66 | path.cubicTo(0, size.height / 2, 0, 0, 0, 0); 67 | canvas.drawPath(path, paint); 68 | 69 | // // Path number 1 70 | 71 | // paint.color = Color(0xff4b515a).withOpacity(1); 72 | // path = Path(); 73 | // path.lineTo(0, size.height * 0.86); 74 | // path.cubicTo(0, size.height * 0.86, size.width * 0.14, size.height, 75 | // size.width * 0.28, size.height); 76 | // path.cubicTo(size.width * 0.42, size.height, size.width * 0.51, 77 | // size.height * 0.96, size.width * 0.54, size.height * 0.88); 78 | // path.cubicTo(size.width * 0.57, size.height * 0.8, size.width * 0.54, 79 | // size.height * 0.69, size.width * 0.62, size.height * 0.59); 80 | // path.cubicTo(size.width * 0.7, size.height / 2, size.width, size.height / 4, 81 | // size.width, size.height * 0.19); 82 | // path.cubicTo(size.width, size.height * 0.13, size.width, 0, size.width, 0); 83 | // path.cubicTo(size.width, 0, size.width * 0.4, 0, size.width * 0.4, 0); 84 | // path.cubicTo( 85 | // size.width * 0.4, 0, 0, size.height * 0.35, 0, size.height * 0.35); 86 | // path.cubicTo( 87 | // 0, size.height * 0.35, 0, size.height * 0.86, 0, size.height * 0.86); 88 | // canvas.drawPath(path, paint); 89 | 90 | // // Path number 2 91 | 92 | // paint.color = Color(0xfffead40).withOpacity(1); 93 | // path = Path(); 94 | // path.lineTo(0, 0); 95 | // path.cubicTo(0, 0, size.width * 0.57, 0, size.width * 0.57, 0); 96 | // path.cubicTo(size.width * 0.57, 0, size.width * 0.56, size.height * 0.04, 97 | // size.width * 0.43, size.height * 0.06); 98 | // path.cubicTo(size.width * 0.29, size.height * 0.08, size.width / 4, 99 | // size.height * 0.12, size.width * 0.19, size.height * 0.3); 100 | // path.cubicTo(size.width * 0.13, size.height * 0.49, 0, size.height * 0.51, 101 | // 0, size.height * 0.51); 102 | // path.cubicTo(0, size.height * 0.51, 0, 0, 0, 0); 103 | // canvas.drawPath(path, paint); 104 | } 105 | 106 | @override 107 | bool shouldRepaint(CustomPainter oldDelegate) { 108 | return true; 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /pubspec.lock: -------------------------------------------------------------------------------- 1 | # Generated by pub 2 | # See https://dart.dev/tools/pub/glossary#lockfile 3 | packages: 4 | _fe_analyzer_shared: 5 | dependency: transitive 6 | description: 7 | name: _fe_analyzer_shared 8 | url: "https://pub.dartlang.org" 9 | source: hosted 10 | version: "12.0.0" 11 | analyzer: 12 | dependency: transitive 13 | description: 14 | name: analyzer 15 | url: "https://pub.dartlang.org" 16 | source: hosted 17 | version: "0.40.6" 18 | args: 19 | dependency: transitive 20 | description: 21 | name: args 22 | url: "https://pub.dartlang.org" 23 | source: hosted 24 | version: "1.6.0" 25 | async: 26 | dependency: transitive 27 | description: 28 | name: async 29 | url: "https://pub.dartlang.org" 30 | source: hosted 31 | version: "2.5.0-nullsafety.1" 32 | boolean_selector: 33 | dependency: transitive 34 | description: 35 | name: boolean_selector 36 | url: "https://pub.dartlang.org" 37 | source: hosted 38 | version: "2.1.0-nullsafety.1" 39 | characters: 40 | dependency: transitive 41 | description: 42 | name: characters 43 | url: "https://pub.dartlang.org" 44 | source: hosted 45 | version: "1.1.0-nullsafety.3" 46 | charcode: 47 | dependency: transitive 48 | description: 49 | name: charcode 50 | url: "https://pub.dartlang.org" 51 | source: hosted 52 | version: "1.2.0-nullsafety.1" 53 | clock: 54 | dependency: transitive 55 | description: 56 | name: clock 57 | url: "https://pub.dartlang.org" 58 | source: hosted 59 | version: "1.1.0-nullsafety.1" 60 | collection: 61 | dependency: transitive 62 | description: 63 | name: collection 64 | url: "https://pub.dartlang.org" 65 | source: hosted 66 | version: "1.15.0-nullsafety.3" 67 | cupertino_icons: 68 | dependency: "direct main" 69 | description: 70 | name: cupertino_icons 71 | url: "https://pub.dartlang.org" 72 | source: hosted 73 | version: "0.1.3" 74 | dart_style: 75 | dependency: transitive 76 | description: 77 | name: dart_style 78 | url: "https://pub.dartlang.org" 79 | source: hosted 80 | version: "1.3.10" 81 | fake_async: 82 | dependency: transitive 83 | description: 84 | name: fake_async 85 | url: "https://pub.dartlang.org" 86 | source: hosted 87 | version: "1.2.0-nullsafety.1" 88 | flutter: 89 | dependency: "direct main" 90 | description: flutter 91 | source: sdk 92 | version: "0.0.0" 93 | flutter_braintree: 94 | dependency: "direct main" 95 | description: 96 | name: flutter_braintree 97 | url: "https://pub.dartlang.org" 98 | source: hosted 99 | version: "1.1.0" 100 | flutter_secure_storage: 101 | dependency: "direct main" 102 | description: 103 | name: flutter_secure_storage 104 | url: "https://pub.dartlang.org" 105 | source: hosted 106 | version: "3.3.5" 107 | flutter_test: 108 | dependency: "direct dev" 109 | description: flutter 110 | source: sdk 111 | version: "0.0.0" 112 | glob: 113 | dependency: transitive 114 | description: 115 | name: glob 116 | url: "https://pub.dartlang.org" 117 | source: hosted 118 | version: "1.2.0" 119 | http: 120 | dependency: "direct main" 121 | description: 122 | name: http 123 | url: "https://pub.dartlang.org" 124 | source: hosted 125 | version: "0.12.2" 126 | http_parser: 127 | dependency: transitive 128 | description: 129 | name: http_parser 130 | url: "https://pub.dartlang.org" 131 | source: hosted 132 | version: "3.1.4" 133 | intl: 134 | dependency: transitive 135 | description: 136 | name: intl 137 | url: "https://pub.dartlang.org" 138 | source: hosted 139 | version: "0.16.1" 140 | js: 141 | dependency: transitive 142 | description: 143 | name: js 144 | url: "https://pub.dartlang.org" 145 | source: hosted 146 | version: "0.6.2" 147 | logging: 148 | dependency: transitive 149 | description: 150 | name: logging 151 | url: "https://pub.dartlang.org" 152 | source: hosted 153 | version: "0.11.4" 154 | matcher: 155 | dependency: transitive 156 | description: 157 | name: matcher 158 | url: "https://pub.dartlang.org" 159 | source: hosted 160 | version: "0.12.10-nullsafety.1" 161 | meta: 162 | dependency: transitive 163 | description: 164 | name: meta 165 | url: "https://pub.dartlang.org" 166 | source: hosted 167 | version: "1.3.0-nullsafety.3" 168 | path: 169 | dependency: transitive 170 | description: 171 | name: path 172 | url: "https://pub.dartlang.org" 173 | source: hosted 174 | version: "1.8.0-nullsafety.1" 175 | sky_engine: 176 | dependency: transitive 177 | description: flutter 178 | source: sdk 179 | version: "0.0.99" 180 | source_gen: 181 | dependency: transitive 182 | description: 183 | name: source_gen 184 | url: "https://pub.dartlang.org" 185 | source: hosted 186 | version: "0.9.9" 187 | source_span: 188 | dependency: transitive 189 | description: 190 | name: source_span 191 | url: "https://pub.dartlang.org" 192 | source: hosted 193 | version: "1.8.0-nullsafety.2" 194 | stack_trace: 195 | dependency: transitive 196 | description: 197 | name: stack_trace 198 | url: "https://pub.dartlang.org" 199 | source: hosted 200 | version: "1.10.0-nullsafety.1" 201 | stream_channel: 202 | dependency: transitive 203 | description: 204 | name: stream_channel 205 | url: "https://pub.dartlang.org" 206 | source: hosted 207 | version: "2.1.0-nullsafety.1" 208 | string_scanner: 209 | dependency: transitive 210 | description: 211 | name: string_scanner 212 | url: "https://pub.dartlang.org" 213 | source: hosted 214 | version: "1.1.0-nullsafety.1" 215 | term_glyph: 216 | dependency: transitive 217 | description: 218 | name: term_glyph 219 | url: "https://pub.dartlang.org" 220 | source: hosted 221 | version: "1.2.0-nullsafety.1" 222 | test_api: 223 | dependency: transitive 224 | description: 225 | name: test_api 226 | url: "https://pub.dartlang.org" 227 | source: hosted 228 | version: "0.2.19-nullsafety.2" 229 | typed_data: 230 | dependency: transitive 231 | description: 232 | name: typed_data 233 | url: "https://pub.dartlang.org" 234 | source: hosted 235 | version: "1.3.0-nullsafety.3" 236 | vector_math: 237 | dependency: transitive 238 | description: 239 | name: vector_math 240 | url: "https://pub.dartlang.org" 241 | source: hosted 242 | version: "2.1.0-nullsafety.3" 243 | sdks: 244 | dart: ">=2.10.0-110 <2.11.0" 245 | -------------------------------------------------------------------------------- /lib/controllers/order_controller.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | import 'dart:io'; 3 | import 'package:ecommerceapp/constants/payment.dart'; 4 | import 'package:ecommerceapp/controllers/auth_controller.dart'; 5 | import 'package:ecommerceapp/controllers/error_controller.dart'; 6 | import 'package:ecommerceapp/models/cart_item.dart'; 7 | import 'package:ecommerceapp/models/order.dart'; 8 | import 'package:ecommerceapp/models/shipping_details.dart'; 9 | import 'package:ecommerceapp/services/order_service.dart'; 10 | import 'package:flutter/foundation.dart'; 11 | import 'package:flutter/material.dart'; 12 | 13 | class OrderController extends ChangeNotifier { 14 | final _orderService = OrderService(); 15 | 16 | final _authContoller = AuthController(); 17 | 18 | var _shippingCost; 19 | 20 | var _tax; 21 | 22 | Order _singleOrder; 23 | 24 | var _orders = List(); 25 | 26 | bool _isLoadingOrders = true; 27 | 28 | bool get isLoadingOrders => _isLoadingOrders; 29 | 30 | List get orders => _orders; 31 | 32 | Order get singleOrder => _singleOrder; 33 | 34 | get tax => _tax; 35 | 36 | get shippingCost => _shippingCost; 37 | 38 | bool _isProcessingOrder = true; 39 | 40 | get isProcessingOrder => _isProcessingOrder; 41 | 42 | void setShippingCost(String country) async { 43 | try { 44 | _shippingCost = await _orderService.getShippingCost(country); 45 | } catch (e) { 46 | print('Order controller ${e.toString()}'); 47 | } 48 | } 49 | 50 | void setTax(String country) async { 51 | try { 52 | _tax = await _orderService.getTax(country); 53 | } catch (e) { 54 | print('Order controller ${e.toString()}'); 55 | } 56 | } 57 | 58 | void registerOrderWithStripePayment( 59 | ShippingDetails shippingDetails, 60 | String shippingCost, 61 | String tax, 62 | String total, 63 | String totalItemPrice, 64 | String userId, 65 | String paymentMethod, 66 | List cart, 67 | GlobalKey scaffoldKey) async { 68 | try { 69 | var userType = userId != null ? USER_TYPE_RESGISTERED : USER_TYPE_GUEST; 70 | 71 | var order = Order( 72 | shippingDetails: shippingDetails, 73 | shippingCost: shippingCost, 74 | tax: tax, 75 | total: total, 76 | totalItemPrice: totalItemPrice, 77 | userId: userId, 78 | paymentMethod: paymentMethod, 79 | userType: userType, 80 | dateOrdered: DateTime.now(), 81 | cartItems: cart, 82 | ); 83 | 84 | var orderToJson = order.toJson(); 85 | var response = await _orderService.saveOrder(json.encode(orderToJson)); 86 | 87 | if (response.statusCode == 200) { 88 | var jsonD = json.decode(response.body); 89 | _singleOrder = orderFromJson(json.encode(jsonD['data'])); 90 | _isProcessingOrder = false; 91 | notifyListeners(); 92 | } else { 93 | ErrorController.showErrorFromApi(scaffoldKey, response); 94 | } 95 | } on SocketException catch (_) { 96 | ErrorController.showNoInternetError(scaffoldKey); 97 | } on HttpException catch (_) { 98 | ErrorController.showNoServerError(scaffoldKey); 99 | } on FormatException catch (_) { 100 | ErrorController.showFormatExceptionError(scaffoldKey); 101 | } catch (e) { 102 | print("Error ${e.toString()}"); 103 | ErrorController.showUnKownError(scaffoldKey); 104 | } 105 | } 106 | 107 | void processOrderWithPaypal( 108 | ShippingDetails shippingDetails, 109 | String shippingCost, 110 | String tax, 111 | String total, 112 | String totalItemPrice, 113 | String userId, 114 | String paymentMethod, 115 | List cart, 116 | String nonce, 117 | GlobalKey scaffoldKey, 118 | ) async { 119 | try { 120 | var userType = userId != null ? USER_TYPE_RESGISTERED : USER_TYPE_GUEST; 121 | 122 | var order = Order( 123 | shippingDetails: shippingDetails, 124 | shippingCost: shippingCost, 125 | tax: tax, 126 | total: total, 127 | totalItemPrice: totalItemPrice, 128 | userId: userId, 129 | paymentMethod: paymentMethod, 130 | userType: userType, 131 | dateOrdered: DateTime.now(), 132 | cartItems: cart, 133 | ); 134 | 135 | var orderToJson = order.toJson(); 136 | var response = await _orderService.sendPayPalRequest( 137 | json.encode(orderToJson), nonce); 138 | 139 | if (response.statusCode == 200) { 140 | var jsonD = json.decode(response.body); 141 | _singleOrder = orderFromJson(json.encode(jsonD['data'])); 142 | _isProcessingOrder = false; 143 | notifyListeners(); 144 | } else { 145 | ErrorController.showErrorFromApi(scaffoldKey, response); 146 | } 147 | } on SocketException catch (_) { 148 | ErrorController.showNoInternetError(scaffoldKey); 149 | } on HttpException catch (_) { 150 | ErrorController.showNoServerError(scaffoldKey); 151 | } on FormatException catch (_) { 152 | ErrorController.showFormatExceptionError(scaffoldKey); 153 | } catch (e) { 154 | print("Error ${e.toString()}"); 155 | ErrorController.showUnKownError(scaffoldKey); 156 | } 157 | } 158 | 159 | void getOrders(GlobalKey scaffoldKey) async { 160 | try { 161 | _isLoadingOrders = true; 162 | var data = await _authContoller.getUserDataAndLoginStatus(); 163 | var response = await _orderService.getOrders(data[0], data[2]); 164 | if (response.statusCode == 200) { 165 | var decodedResponse = json.decode(response.body); 166 | _orders = 167 | ordersFromJson(json.encode(decodedResponse['data']['orders'])); 168 | _isLoadingOrders = false; 169 | notifyListeners(); 170 | } else { 171 | _isLoadingOrders = true; 172 | notifyListeners(); 173 | ErrorController.showErrorFromApi(scaffoldKey, response); 174 | } 175 | } on SocketException catch (_) { 176 | _isLoadingOrders = true; 177 | notifyListeners(); 178 | ErrorController.showNoInternetError(scaffoldKey); 179 | } on HttpException catch (_) { 180 | _isLoadingOrders = true; 181 | notifyListeners(); 182 | ErrorController.showNoServerError(scaffoldKey); 183 | } on FormatException catch (_) { 184 | _isLoadingOrders = true; 185 | notifyListeners(); 186 | ErrorController.showFormatExceptionError(scaffoldKey); 187 | } catch (e) { 188 | print('error fetching orders ${e.toString()}'); 189 | _isLoadingOrders = true; 190 | notifyListeners(); 191 | ErrorController.showUnKownError(scaffoldKey); 192 | } 193 | } 194 | 195 | void setSingleOrder(Order order) { 196 | this._singleOrder = order; 197 | } 198 | } 199 | -------------------------------------------------------------------------------- /lib/controllers/auth_controller.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | import 'dart:io'; 3 | 4 | import 'package:ecommerceapp/controllers/error_controller.dart'; 5 | import 'package:ecommerceapp/services/auth_service.dart'; 6 | import 'package:ecommerceapp/widgets/global_snackbar.dart'; 7 | import 'package:flutter/material.dart'; 8 | import 'package:flutter_secure_storage/flutter_secure_storage.dart'; 9 | 10 | class AuthController { 11 | final _storage = FlutterSecureStorage(); 12 | final _authService = AuthService(); 13 | 14 | saveUserDataAndLoginStatus( 15 | String userId, 16 | String isLoggedFlag, 17 | String jwt, 18 | String email, 19 | String name, 20 | ) async { 21 | await _storage.write(key: 'UserId', value: userId); 22 | await _storage.write(key: 'IsLoggedFlag', value: isLoggedFlag); 23 | await _storage.write(key: 'jwt', value: jwt); 24 | await _storage.write(key: 'email', value: email); 25 | await _storage.write(key: 'name', value: name); 26 | } 27 | 28 | getUserDataAndLoginStatus() async { 29 | String userId = await _storage.read(key: 'UserId'); 30 | String isLoggedFlag = await _storage.read(key: 'IsLoggedFlag'); 31 | String token = await _storage.read(key: 'jwt'); 32 | String email = await _storage.read(key: 'email'); 33 | String name = await _storage.read(key: 'name'); 34 | return [userId, isLoggedFlag, token, email, name]; 35 | } 36 | 37 | deleteUserDataAndLoginStatus() async { 38 | await _storage.deleteAll(); 39 | } 40 | 41 | Future emailNameAndPasswordSignUp( 42 | String name, 43 | String email, 44 | String password, 45 | GlobalKey scaffoldKey, 46 | ) async { 47 | try { 48 | var response = 49 | await _authService.emailNameAndPasswordSignUp(name, email, password); 50 | 51 | if (response.statusCode == 201) { 52 | var jsonResponse = json.decode(response.body); 53 | var token = jsonResponse['data']['token']; 54 | var userId = jsonResponse['data']['user']['id']; 55 | var email = jsonResponse['data']['user']['email']; 56 | var name = jsonResponse['data']['user']['name']; 57 | 58 | await saveUserDataAndLoginStatus(userId, '1', token, email, name); 59 | return true; 60 | } else { 61 | ErrorController.showErrorFromApi(scaffoldKey, response); 62 | return false; 63 | } 64 | } on SocketException catch (_) { 65 | ErrorController.showNoInternetError(scaffoldKey); 66 | return false; 67 | } on HttpException catch (_) { 68 | ErrorController.showNoServerError(scaffoldKey); 69 | return false; 70 | } on FormatException catch (_) { 71 | ErrorController.showFormatExceptionError(scaffoldKey); 72 | return false; 73 | } catch (e) { 74 | print("Error ${e.toString()}"); 75 | ErrorController.showUnKownError(scaffoldKey); 76 | return false; 77 | } 78 | } 79 | 80 | Future emailAndPasswordSignIn( 81 | String email, 82 | String password, 83 | GlobalKey scaffoldKey, 84 | ) async { 85 | try { 86 | var response = await _authService.emailAndPasswordSignIn(email, password); 87 | 88 | if (response.statusCode == 200) { 89 | var jsonResponse = json.decode(response.body); 90 | var token = jsonResponse['data']['token']; 91 | var userId = jsonResponse['data']['user']['id']; 92 | var email = jsonResponse['data']['user']['email']; 93 | var name = jsonResponse['data']['user']['name']; 94 | 95 | await saveUserDataAndLoginStatus(userId, '1', token, email, name); 96 | return true; 97 | } else { 98 | ErrorController.showErrorFromApi(scaffoldKey, response); 99 | return false; 100 | } 101 | } on SocketException catch (_) { 102 | ErrorController.showNoInternetError(scaffoldKey); 103 | return false; 104 | } on HttpException catch (_) { 105 | ErrorController.showNoServerError(scaffoldKey); 106 | return false; 107 | } on FormatException catch (_) { 108 | ErrorController.showFormatExceptionError(scaffoldKey); 109 | return false; 110 | } catch (e) { 111 | print("Error ${e.toString()}"); 112 | ErrorController.showUnKownError(scaffoldKey); 113 | return false; 114 | } 115 | } 116 | 117 | Future isTokenValid() async { 118 | String token = await _storage.read(key: 'jwt'); 119 | 120 | if (token == null || token.isEmpty) { 121 | return false; 122 | } 123 | var response = await _authService.checkTokenExpiry(token); 124 | 125 | if (response.statusCode == 200) { 126 | return true; 127 | } else { 128 | return false; 129 | } 130 | } 131 | 132 | Future changeName( 133 | String name, GlobalKey scaffoldKey) async { 134 | try { 135 | var data = await getUserDataAndLoginStatus(); 136 | 137 | var response = await _authService.changeName(name, data[0], data[2]); 138 | 139 | if (response.statusCode == 200) { 140 | var responseBody = json.decode(response.body); 141 | await _storage.write(key: 'name', value: responseBody['data']['name']); 142 | 143 | return true; 144 | } else { 145 | ErrorController.showErrorFromApi(scaffoldKey, response); 146 | return false; 147 | } 148 | } on SocketException catch (_) { 149 | ErrorController.showNoInternetError(scaffoldKey); 150 | return false; 151 | } on HttpException catch (_) { 152 | ErrorController.showNoServerError(scaffoldKey); 153 | return false; 154 | } on FormatException catch (_) { 155 | ErrorController.showFormatExceptionError(scaffoldKey); 156 | return false; 157 | } catch (e) { 158 | print("Error ${e.toString()}"); 159 | ErrorController.showUnKownError(scaffoldKey); 160 | return false; 161 | } 162 | } 163 | 164 | Future changeEmail( 165 | String email, GlobalKey scaffoldKey) async { 166 | try { 167 | var data = await getUserDataAndLoginStatus(); 168 | 169 | var response = await _authService.changeEmail(email, data[0], data[2]); 170 | 171 | if (response.statusCode == 200) { 172 | var responseBody = json.decode(response.body); 173 | await _storage.write( 174 | key: 'email', value: responseBody['data']['email']); 175 | return true; 176 | } else { 177 | ErrorController.showErrorFromApi(scaffoldKey, response); 178 | return false; 179 | } 180 | } on SocketException catch (_) { 181 | ErrorController.showNoInternetError(scaffoldKey); 182 | return false; 183 | } on HttpException catch (_) { 184 | ErrorController.showNoServerError(scaffoldKey); 185 | return false; 186 | } on FormatException catch (_) { 187 | ErrorController.showFormatExceptionError(scaffoldKey); 188 | return false; 189 | } catch (e) { 190 | print("Error ${e.toString()}"); 191 | ErrorController.showUnKownError(scaffoldKey); 192 | return false; 193 | } 194 | } 195 | 196 | Future forgotPassword( 197 | String email, GlobalKey scaffoldKey) async { 198 | try { 199 | var response = await _authService.forgotPassword(email); 200 | 201 | if (response.statusCode == 200) { 202 | var responseBody = json.decode(response.body); 203 | 204 | GlobalSnackBar.showSnackbar( 205 | scaffoldKey, 206 | responseBody['message'], 207 | SnackBarType.Success, 208 | ); 209 | 210 | return true; 211 | } else { 212 | ErrorController.showErrorFromApi(scaffoldKey, response); 213 | return false; 214 | } 215 | } on SocketException catch (_) { 216 | ErrorController.showNoInternetError(scaffoldKey); 217 | return false; 218 | } on HttpException catch (_) { 219 | ErrorController.showNoServerError(scaffoldKey); 220 | return false; 221 | } on FormatException catch (_) { 222 | ErrorController.showFormatExceptionError(scaffoldKey); 223 | return false; 224 | } catch (e) { 225 | print("Error ${e.toString()}"); 226 | ErrorController.showUnKownError(scaffoldKey); 227 | return false; 228 | } 229 | } 230 | } 231 | -------------------------------------------------------------------------------- /lib/controllers/cart_controller.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | import 'dart:io'; 3 | 4 | import 'package:ecommerceapp/controllers/auth_controller.dart'; 5 | import 'package:ecommerceapp/controllers/error_controller.dart'; 6 | import 'package:ecommerceapp/models/cart_item.dart'; 7 | import 'package:ecommerceapp/models/product.dart'; 8 | import 'package:ecommerceapp/services/cart_service.dart'; 9 | import 'package:flutter/foundation.dart'; 10 | import 'package:flutter/material.dart'; 11 | 12 | class CartController extends ChangeNotifier { 13 | var _cart = List(); 14 | 15 | bool _isLoadingProduct = true; 16 | 17 | CartItem _selectedItem = CartItem(); 18 | 19 | var _authController = AuthController(); 20 | 21 | var _cartService = CartService(); 22 | 23 | List get cart => _cart; 24 | 25 | bool get isLoadingProduct => _isLoadingProduct; 26 | 27 | CartItem get selectedItem => _selectedItem; 28 | 29 | void setCurrentItem(Product product) async { 30 | _isLoadingProduct = true; 31 | 32 | var item = CartItem(product: product, quantity: 1); 33 | 34 | if (isItemInCart(item)) { 35 | var foundItem = getCartItem(item); 36 | _selectedItem = foundItem; 37 | _isLoadingProduct = false; 38 | notifyListeners(); 39 | } else { 40 | _selectedItem = CartItem(product: product, quantity: 1); 41 | _isLoadingProduct = false; 42 | notifyListeners(); 43 | } 44 | } 45 | 46 | void addToCart(CartItem cartItem) { 47 | cart.add(cartItem); 48 | notifyListeners(); 49 | } 50 | 51 | bool isItemInCart(CartItem cartItem) { 52 | bool isFound = false; 53 | if (cart.length != 0) { 54 | for (CartItem item in cart) { 55 | if (item.product.id == cartItem.product.id) { 56 | isFound = true; 57 | } 58 | } 59 | return isFound; 60 | } 61 | return isFound; 62 | } 63 | 64 | void removeFromCart(CartItem cartItem) { 65 | var foundItem; 66 | if (cart.length != 0) { 67 | for (CartItem item in cart) { 68 | if (item.product.id == cartItem.product.id) { 69 | foundItem = item; 70 | } 71 | } 72 | cart.remove(foundItem); 73 | notifyListeners(); 74 | } 75 | } 76 | 77 | increaseCartItemAndProductDetailItemQuantity() { 78 | if (isItemInCart(_selectedItem)) { 79 | var foundItem = getCartItem(_selectedItem); 80 | if (foundItem != null) { 81 | //this affects both selected item and item in cart's quantity 82 | foundItem.quantity++; 83 | notifyListeners(); 84 | } 85 | } else { 86 | _selectedItem.quantity++; 87 | notifyListeners(); 88 | } 89 | } 90 | 91 | decreaseCartItemAndProductDetailItemQuantity() { 92 | if (isItemInCart(_selectedItem)) { 93 | var foundItem = getCartItem(_selectedItem); 94 | if (foundItem != null && foundItem.quantity > 1) { 95 | //this affects both selected item and item in cart's quantity 96 | foundItem.quantity--; 97 | notifyListeners(); 98 | } 99 | } else { 100 | if (_selectedItem.quantity > 1) { 101 | _selectedItem.quantity--; 102 | notifyListeners(); 103 | } 104 | } 105 | } 106 | 107 | CartItem getCartItem(CartItem cartItem) { 108 | var foundItem; 109 | for (CartItem item in cart) { 110 | if (item.product.id == cartItem.product.id) { 111 | foundItem = item; 112 | } 113 | } 114 | return foundItem; 115 | } 116 | 117 | singleCartItemIncrease(CartItem cartItem) { 118 | var foundItem = getCartItem(cartItem); 119 | if (foundItem != null) { 120 | //this affects both selected item and item in cart's quantity 121 | foundItem.quantity++; 122 | notifyListeners(); 123 | } 124 | } 125 | 126 | singleCartItemDecrease(CartItem cartItem) { 127 | var foundItem = getCartItem(cartItem); 128 | if (foundItem != null && foundItem.quantity > 1) { 129 | //this affects both selected item and item in cart's quantity 130 | foundItem.quantity--; 131 | notifyListeners(); 132 | } 133 | } 134 | 135 | resetCart() { 136 | cart.clear(); 137 | deleteSavedCart(); 138 | } 139 | 140 | saveCart(List cart, GlobalKey scaffoldKey) async { 141 | try { 142 | cart.forEach((cartItem) async { 143 | var productId = cartItem.product.id; 144 | var quantity = cartItem.quantity.toString(); 145 | var authData = await _authController.getUserDataAndLoginStatus(); 146 | await _cartService.saveCart( 147 | productId, authData[0], quantity, authData[2]); 148 | }); 149 | } on SocketException catch (_) { 150 | ErrorController.showNoInternetError(scaffoldKey); 151 | } on HttpException catch (_) { 152 | ErrorController.showNoServerError(scaffoldKey); 153 | } on FormatException catch (_) { 154 | ErrorController.showFormatExceptionError(scaffoldKey); 155 | } catch (e) { 156 | ErrorController.showUnKownError(scaffoldKey); 157 | } 158 | } 159 | 160 | //get cart from db if it was saved at check out stage 161 | //can only be saved if user logged in 162 | //can only be loaded if jwt token didn't expire 163 | getSavedCart() async { 164 | try { 165 | var authData = await _authController.getUserDataAndLoginStatus(); 166 | var userId = authData[0]; 167 | var jwtToken = authData[2]; 168 | 169 | var response = await _cartService.getCart(userId, jwtToken); 170 | 171 | if (response.statusCode == 200) { 172 | var jsonResponse = json.decode(response.body); 173 | var cartObj = jsonResponse['data']['cart']; 174 | cart.addAll(cartItemFromJson(json.encode(cartObj))); 175 | 176 | notifyListeners(); 177 | } else { 178 | //cart isn't updated because it wasn't saved,or jwt expired 179 | //that is okay. User can proceed with using the app 180 | //will be prompted to either login or continue as guest at checkout stage 181 | //else block included for future implementation when required 182 | } 183 | } catch (e) { 184 | print('Get saved cart err ${e.toString()}'); 185 | } 186 | } 187 | 188 | deleteSavedCart() async { 189 | try { 190 | var authData = await _authController.getUserDataAndLoginStatus(); 191 | var userId = authData[0]; 192 | 193 | var response = await _cartService.deleteCart(userId); 194 | 195 | if (response.statusCode == 204) { 196 | //cart is deleted on check out completion 197 | //no need to inform user 198 | print('cart deleted'); 199 | } else { 200 | print('cart not deleted'); 201 | } 202 | } catch (e) { 203 | print('Delete saved cart err ${e.toString()}'); 204 | } 205 | } 206 | 207 | //REQUIRED IF YOU NEED TO FETCH PRODUCT BY ID FROM API AND SET SELECTED ITEM 208 | 209 | // void setCurrentItem( 210 | // String productId, GlobalKey scaffoldKey) async { 211 | // try { 212 | // _isLoadingProduct = true; 213 | 214 | // var response = await _productService.getProductById(productId); 215 | 216 | // if (response.statusCode == 200) { 217 | // var responseJsonStr = json.decode(response.body); 218 | // var jsonProd = responseJsonStr['data']['product']; 219 | // var product = Product.fromJson(jsonProd); 220 | // var item = CartItem(product: product, quantity: 1); 221 | // if (isItemInCart(item)) { 222 | // var foundItem = getCartItem(item); 223 | // _selectedItem = foundItem; 224 | // _isLoadingProduct = false; 225 | // notifyListeners(); 226 | // } else { 227 | // _selectedItem = CartItem(product: product, quantity: 1); 228 | // _isLoadingProduct = false; 229 | // notifyListeners(); 230 | // } 231 | // } else { 232 | // ErrorController.showErrorFromApi(scaffoldKey, response); 233 | // } 234 | // } on SocketException catch (_) { 235 | // ErrorController.showNoInternetError(scaffoldKey); 236 | // } on HttpException catch (_) { 237 | // ErrorController.showNoServerError(scaffoldKey); 238 | // } on FormatException catch (_) { 239 | // ErrorController.showFormatExceptionError(scaffoldKey); 240 | // } catch (e) { 241 | // print("Error ${e.toString()}"); 242 | // ErrorController.showUnKownError(scaffoldKey); 243 | // } 244 | // } 245 | 246 | } 247 | -------------------------------------------------------------------------------- /lib/screens/single_order.dart: -------------------------------------------------------------------------------- 1 | import 'package:ecommerceapp/constants/screen_ids.dart'; 2 | import 'package:ecommerceapp/constants/screen_titles.dart'; 3 | import 'package:ecommerceapp/constants/tasks.dart'; 4 | import 'package:ecommerceapp/controllers/activity_tracker_controller.dart'; 5 | import 'package:ecommerceapp/controllers/order_controller.dart'; 6 | import 'package:ecommerceapp/screens/products_list.dart'; 7 | import 'package:flutter/material.dart'; 8 | import 'package:provider/provider.dart'; 9 | 10 | class SingleOrder extends StatefulWidget { 11 | SingleOrder({Key key}) : super(key: key); 12 | static String id = SingleOrder_Screen_Id; 13 | 14 | @override 15 | _SingleOrderState createState() => _SingleOrderState(); 16 | } 17 | 18 | class _SingleOrderState extends State { 19 | Future _onBackPressed() { 20 | var currentTask = 21 | Provider.of(context, listen: false).currentTask; 22 | if (currentTask == VIEWING_SINGLE_OLD_ORDER_HISTORY) { 23 | Navigator.pop(context); 24 | } else { 25 | Navigator.pushNamedAndRemoveUntil( 26 | context, ProductList.id, (route) => false); 27 | } 28 | return Future.value(true); 29 | } 30 | 31 | @override 32 | Widget build(BuildContext context) { 33 | return WillPopScope( 34 | onWillPop: _onBackPressed, 35 | child: SafeArea( 36 | child: Scaffold( 37 | appBar: AppBar( 38 | title: Text( 39 | '$SingleOrder_Screen_Title ${context.watch().singleOrder.id ?? ''}', 40 | style: TextStyle( 41 | color: Colors.black, 42 | ), 43 | ), 44 | iconTheme: IconThemeData(color: Colors.black), 45 | elevation: 1, 46 | backgroundColor: Colors.white, 47 | ), 48 | body: Container( 49 | margin: EdgeInsets.only(left: 20.0, right: 10.0), 50 | child: ListView( 51 | children: [ 52 | Column( 53 | crossAxisAlignment: CrossAxisAlignment.start, 54 | children: [ 55 | //order details 56 | SizedBox(height: 20.0), 57 | Text( 58 | 'SHIPPING DETAILS', 59 | style: TextStyle( 60 | fontSize: 15, 61 | fontWeight: FontWeight.bold, 62 | ), 63 | ), 64 | SizedBox(height: 20.0), 65 | Text( 66 | 'Date and time ordered: ${context.watch().singleOrder.dateOrdered ?? ''}', 67 | style: TextStyle( 68 | fontSize: 15, 69 | ), 70 | ), 71 | SizedBox(height: 10.0), 72 | Text( 73 | 'Payment method: ${context.watch().singleOrder.paymentMethod ?? ''} ', 74 | style: TextStyle( 75 | fontSize: 15, 76 | ), 77 | ), 78 | SizedBox( 79 | height: 10, 80 | ), 81 | Text( 82 | 'Shipping cost: \$ ${context.watch().singleOrder.shippingCost ?? ''}', 83 | style: TextStyle( 84 | fontSize: 15, 85 | ), 86 | ), 87 | SizedBox( 88 | height: 10, 89 | ), 90 | Text( 91 | 'Tax: \$ ${context.watch().singleOrder.tax ?? ''}', 92 | style: TextStyle( 93 | fontSize: 15, 94 | ), 95 | ), 96 | SizedBox( 97 | height: 10, 98 | ), 99 | Text( 100 | 'Total item price: \$ ${context.watch().singleOrder.totalItemPrice ?? ''}', 101 | style: TextStyle( 102 | fontSize: 15, 103 | ), 104 | ), 105 | SizedBox( 106 | height: 10, 107 | ), 108 | Text( 109 | 'Total: \$ ${context.watch().singleOrder.total ?? ''}', 110 | style: TextStyle( 111 | fontSize: 15, 112 | ), 113 | ), 114 | 115 | //shipping details 116 | SizedBox(height: 20.0), 117 | Text( 118 | 'ORDER DETAILS', 119 | style: TextStyle( 120 | fontSize: 15, 121 | fontWeight: FontWeight.bold, 122 | ), 123 | ), 124 | SizedBox(height: 20.0), 125 | Text( 126 | 'Name: ${context.watch().singleOrder.shippingDetails.name ?? ''}', 127 | style: TextStyle( 128 | fontSize: 15, 129 | ), 130 | ), 131 | SizedBox(height: 10.0), 132 | Text( 133 | 'Phone Contact: ${context.watch().singleOrder.shippingDetails.phoneContact ?? ''}', 134 | style: TextStyle( 135 | fontSize: 15, 136 | ), 137 | ), 138 | SizedBox(height: 10.0), 139 | Text( 140 | 'Address Line: ${context.watch().singleOrder.shippingDetails.addressLine ?? ''}', 141 | style: TextStyle( 142 | fontSize: 15, 143 | ), 144 | ), 145 | SizedBox(height: 10.0), 146 | Text( 147 | 'City: ${context.watch().singleOrder.shippingDetails.city ?? ''}', 148 | style: TextStyle( 149 | fontSize: 15, 150 | ), 151 | ), 152 | SizedBox(height: 10.0), 153 | Text( 154 | 'Postal code: ${context.watch().singleOrder.shippingDetails.postalCode ?? ''}', 155 | style: TextStyle( 156 | fontSize: 15, 157 | ), 158 | ), 159 | SizedBox(height: 10.0), 160 | Text( 161 | 'Country: ${context.watch().singleOrder.shippingDetails.country ?? ''}', 162 | style: TextStyle( 163 | fontSize: 15, 164 | ), 165 | ), 166 | SizedBox(height: 20.0), 167 | 168 | //items 169 | Text( 170 | 'ITEMS', 171 | style: TextStyle( 172 | fontSize: 15, 173 | fontWeight: FontWeight.bold, 174 | ), 175 | ), 176 | SizedBox(height: 20.0), 177 | Consumer( 178 | builder: (context, ctlr, chidl) { 179 | if (ctlr.isProcessingOrder && 180 | ctlr.singleOrder.cartItems == null) { 181 | return Center(child: CircularProgressIndicator()); 182 | } 183 | return ListView.builder( 184 | shrinkWrap: true, 185 | physics: ScrollPhysics(), 186 | itemCount: ctlr.singleOrder.cartItems.length, 187 | itemBuilder: (context, index) { 188 | return Column( 189 | crossAxisAlignment: CrossAxisAlignment.start, 190 | children: [ 191 | Text( 192 | "Name: ${ctlr.singleOrder.cartItems[index].product.name}", 193 | ), 194 | SizedBox(height: 10.0), 195 | Text( 196 | "Price: ${ctlr.singleOrder.cartItems[index].product.price}", 197 | ), 198 | SizedBox(height: 10.0), 199 | Text( 200 | "Quantity: ${ctlr.singleOrder.cartItems[index].quantity}", 201 | ), 202 | Divider( 203 | thickness: 5, 204 | color: Colors.grey, 205 | ), 206 | ], 207 | ); 208 | }); 209 | }), 210 | 211 | SizedBox(height: 10.0), 212 | Center( 213 | child: RaisedButton( 214 | elevation: 0, 215 | onPressed: () {}, 216 | child: Text( 217 | "${context.watch().currentTask == VIEWING_SINGLE_OLD_ORDER_HISTORY ? 'Thank you for the support' : "WE'LL CONTACT YOU SHORTLY"}", 218 | style: TextStyle( 219 | color: Colors.white, 220 | ), 221 | ), 222 | color: Colors.orange[900], 223 | shape: RoundedRectangleBorder( 224 | borderRadius: BorderRadius.circular(50), 225 | ), 226 | ), 227 | ), 228 | SizedBox(height: 10.0), 229 | ], 230 | ), 231 | ], 232 | ), 233 | )), 234 | ), 235 | ); 236 | } 237 | } 238 | -------------------------------------------------------------------------------- /lib/screens/product_detail.dart: -------------------------------------------------------------------------------- 1 | import 'dart:ui'; 2 | import 'package:ecommerceapp/constants/screen_ids.dart'; 3 | import 'package:ecommerceapp/screens/shopping_cart.dart'; 4 | import 'package:badges/badges.dart'; 5 | import 'package:ecommerceapp/controllers/cart_controller.dart'; 6 | import 'package:ecommerceapp/skeletons/product_detail_skeleton.dart'; 7 | import 'package:ecommerceapp/widgets/cart_button.dart'; 8 | import 'package:ecommerceapp/widgets/product_detail_bottomsheet_content.dart'; 9 | import 'package:ecommerceapp/widgets/round_cart_button.dart'; 10 | import 'package:flutter/material.dart'; 11 | import 'package:provider/provider.dart'; 12 | import 'package:shimmer/shimmer.dart'; 13 | 14 | class ProductDetail extends StatefulWidget { 15 | ProductDetail({Key key}) : super(key: key); 16 | static String id = ProductDetail_Screen_Id; 17 | 18 | @override 19 | _ProductDetailState createState() => _ProductDetailState(); 20 | } 21 | 22 | class _ProductDetailState extends State { 23 | var _cartCtlr; 24 | 25 | @override 26 | void initState() { 27 | super.initState(); 28 | _cartCtlr = Provider.of(context, listen: false); 29 | } 30 | 31 | _handleButtonTap(context) { 32 | if (!_cartCtlr.isItemInCart(_cartCtlr.selectedItem)) { 33 | _cartCtlr.addToCart(_cartCtlr.selectedItem); 34 | } else { 35 | showModalBottomSheet( 36 | context: context, 37 | builder: (BuildContext context) => 38 | ProductDetailBottomSheetContent(cartCtlr: _cartCtlr), 39 | ); 40 | } 41 | } 42 | 43 | _handleQuantityIncrease() { 44 | _cartCtlr.increaseCartItemAndProductDetailItemQuantity(); 45 | } 46 | 47 | _handleQuantityDecrease() { 48 | _cartCtlr.decreaseCartItemAndProductDetailItemQuantity(); 49 | } 50 | 51 | @override 52 | Widget build(BuildContext context) { 53 | var size = MediaQuery.of(context).size; 54 | double _leftMargin = 18; 55 | double _rightMargin = 10; 56 | 57 | return Scaffold( 58 | appBar: AppBar( 59 | title: Consumer( 60 | builder: (context, cartCtlr, child) { 61 | if (!cartCtlr.isLoadingProduct) { 62 | return Text( 63 | "${cartCtlr.selectedItem.product.category}", 64 | style: TextStyle( 65 | color: Colors.black, 66 | fontSize: 30, 67 | ), 68 | ); 69 | } 70 | return Text('Loading ...'); 71 | }, 72 | ), 73 | iconTheme: IconThemeData(color: Colors.black), 74 | elevation: 0, 75 | actions: [ 76 | Container( 77 | margin: EdgeInsets.only(right: 20, top: _rightMargin), 78 | child: GestureDetector( 79 | onTap: () { 80 | Navigator.pushNamed(context, ShoppingCart.id); 81 | }, 82 | child: Badge( 83 | padding: EdgeInsets.all(5), 84 | badgeContent: Text( 85 | '${context.watch().cart.length}', 86 | style: TextStyle( 87 | color: Colors.white, 88 | ), 89 | ), 90 | child: Icon( 91 | Icons.shopping_cart, 92 | color: Colors.orange, 93 | ), 94 | ), 95 | ), 96 | ), 97 | ], 98 | backgroundColor: Colors.white, 99 | ), 100 | body: SafeArea( 101 | child: Consumer( 102 | builder: (context, cartCtlr, child) { 103 | if (cartCtlr.isLoadingProduct) { 104 | return Center( 105 | child: Shimmer.fromColors( 106 | child: ProductDetailSkeleton(), 107 | baseColor: Colors.grey[300], 108 | highlightColor: Colors.grey[400], 109 | ), 110 | ); 111 | } 112 | return ListView( 113 | children: [ 114 | Container( 115 | margin: 116 | EdgeInsets.only(left: _leftMargin, right: _rightMargin), 117 | child: Column( 118 | crossAxisAlignment: CrossAxisAlignment.start, 119 | children: [ 120 | //product image 121 | Container( 122 | height: size.width / 2 + 100, 123 | width: size.width, 124 | child: Image.network( 125 | cartCtlr.selectedItem.product.imageUrl, 126 | fit: BoxFit.fill, 127 | errorBuilder: (BuildContext context, Object exception, 128 | StackTrace stackTrace) { 129 | return Center(child: Icon(Icons.error)); 130 | }, 131 | loadingBuilder: (BuildContext context, Widget child, 132 | ImageChunkEvent loadingProgress) { 133 | if (loadingProgress == null) return child; 134 | return Center( 135 | child: CircularProgressIndicator( 136 | strokeWidth: 2.0, 137 | ), 138 | ); 139 | }, 140 | ), 141 | ), 142 | 143 | SizedBox( 144 | height: 30, 145 | ), 146 | 147 | //product name and price 148 | Padding( 149 | padding: const EdgeInsets.all(8.0), 150 | child: Row( 151 | mainAxisAlignment: MainAxisAlignment.spaceBetween, 152 | children: [ 153 | Text( 154 | "${cartCtlr.selectedItem.product.name}", 155 | style: TextStyle( 156 | fontWeight: FontWeight.bold, fontSize: 15), 157 | ), 158 | Text( 159 | "\$${cartCtlr.selectedItem.product.price}", 160 | style: TextStyle(fontSize: 15), 161 | ), 162 | ], 163 | ), 164 | ), 165 | 166 | Divider( 167 | thickness: 3, 168 | ), 169 | // increment and decrement buttons, add to cart button 170 | Container( 171 | color: Colors.grey[200], 172 | child: Padding( 173 | padding: const EdgeInsets.all(8.0), 174 | child: Row( 175 | mainAxisAlignment: MainAxisAlignment.spaceBetween, 176 | children: [ 177 | // quantity ,increment,and decrement buttons 178 | Row( 179 | children: [ 180 | //decrement button 181 | RoundCartButton( 182 | icon: Icons.remove, 183 | width: size.width * 0.1, 184 | onTap: () { 185 | _handleQuantityDecrease(); 186 | }, 187 | ), 188 | Padding( 189 | padding: const EdgeInsets.all(8.0), 190 | // quantity 191 | child: Text( 192 | '${context.watch().selectedItem.quantity}', 193 | style: TextStyle( 194 | fontWeight: FontWeight.bold), 195 | ), 196 | ), 197 | // increment button 198 | RoundCartButton( 199 | icon: Icons.add, 200 | width: size.width * 0.1, 201 | onTap: () { 202 | _handleQuantityIncrease(); 203 | }, 204 | ), 205 | ], 206 | ), 207 | 208 | //add to cart button 209 | InkWell( 210 | onTap: () { 211 | _handleButtonTap(context); 212 | }, 213 | child: CartButton( 214 | text: "ADD", 215 | width: size.width * 0.2, 216 | ), 217 | ) 218 | ], 219 | ), 220 | ), 221 | ), 222 | Divider( 223 | thickness: 3, 224 | ), 225 | 226 | SizedBox( 227 | height: 30, 228 | ), 229 | 230 | //Details 231 | Text( 232 | "Details", 233 | style: TextStyle( 234 | fontWeight: FontWeight.bold, 235 | fontSize: 25, 236 | ), 237 | ), 238 | SizedBox( 239 | height: 10, 240 | ), 241 | Text( 242 | "${cartCtlr.selectedItem.product.details}", 243 | style: TextStyle( 244 | fontWeight: FontWeight.w500, 245 | ), 246 | overflow: TextOverflow.clip, 247 | textAlign: TextAlign.justify, 248 | ), 249 | 250 | SizedBox( 251 | height: 30, 252 | ), 253 | ], 254 | ), 255 | ), 256 | ], 257 | ); 258 | }, 259 | ), 260 | ), 261 | ); 262 | } 263 | } 264 | -------------------------------------------------------------------------------- /lib/screens/shipping.dart: -------------------------------------------------------------------------------- 1 | import 'package:ecommerceapp/constants/screen_ids.dart'; 2 | import 'package:ecommerceapp/constants/screen_titles.dart'; 3 | import 'package:ecommerceapp/constants/tasks.dart'; 4 | import 'package:ecommerceapp/controllers/activity_tracker_controller.dart'; 5 | import 'package:ecommerceapp/controllers/order_controller.dart'; 6 | import 'package:ecommerceapp/controllers/shipping_controller.dart'; 7 | import 'package:ecommerceapp/models/shipping_details.dart'; 8 | import 'package:ecommerceapp/screens/payment_method.dart'; 9 | import 'package:ecommerceapp/screens/shopping_cart.dart'; 10 | import 'package:ecommerceapp/utils/validator.dart'; 11 | import 'package:flutter/material.dart'; 12 | import 'package:provider/provider.dart'; 13 | 14 | class Shipping extends StatefulWidget { 15 | Shipping({Key key}) : super(key: key); 16 | static String id = Shipping_Screen_Id; 17 | 18 | @override 19 | _ShippingState createState() => _ShippingState(); 20 | } 21 | 22 | class _ShippingState extends State { 23 | var _shippingDdetailsFormkey = GlobalKey(); 24 | 25 | //this is to avoid showing bottom sheet again on previous shopping cart screen 26 | Future _onBackPressed() { 27 | Navigator.popUntil(context, ModalRoute.withName(ShoppingCart.id)); 28 | return Future.value(true); 29 | } 30 | 31 | @override 32 | Widget build(BuildContext context) { 33 | String _name; 34 | String _phoneContact; 35 | String _addressLine; 36 | String _city; 37 | String _postalCode; 38 | String _country; 39 | return WillPopScope( 40 | onWillPop: _onBackPressed, 41 | child: SafeArea( 42 | child: Scaffold( 43 | appBar: AppBar( 44 | title: Text( 45 | '$ShippingDetail_Screen_Title', 46 | style: TextStyle( 47 | color: Colors.black, 48 | ), 49 | ), 50 | iconTheme: IconThemeData(color: Colors.black), 51 | elevation: 0, 52 | backgroundColor: Colors.white, 53 | ), 54 | body: Container( 55 | margin: EdgeInsets.all(18), 56 | child: ListView( 57 | children: [ 58 | Form( 59 | key: _shippingDdetailsFormkey, 60 | child: Column( 61 | crossAxisAlignment: CrossAxisAlignment.stretch, 62 | children: [ 63 | SizedBox( 64 | height: 20, 65 | ), 66 | TextFormField( 67 | keyboardType: TextInputType.name, 68 | decoration: InputDecoration( 69 | labelText: "Enter name *", 70 | icon: Icon( 71 | Icons.person, 72 | ), 73 | focusedBorder: UnderlineInputBorder( 74 | borderSide: BorderSide(color: Colors.black), 75 | ), 76 | ), 77 | onSaved: (value) => _name = value, 78 | validator: (value) => value.isEmpty ? 'Required' : null, 79 | initialValue: 80 | '${context.watch().shippingDetails.name}', 81 | ), 82 | TextFormField( 83 | keyboardType: TextInputType.phone, 84 | decoration: InputDecoration( 85 | labelText: "Enter phone number *", 86 | icon: Icon( 87 | Icons.phone, 88 | ), 89 | focusedBorder: UnderlineInputBorder( 90 | borderSide: BorderSide(color: Colors.black), 91 | ), 92 | ), 93 | onSaved: (value) => _phoneContact = value, 94 | validator: (value) { 95 | if (value.isEmpty) { 96 | return 'Required'; 97 | } 98 | if (!Validator.isPhoneNumberValid(value)) { 99 | return "Invalid phone number"; 100 | } 101 | return null; 102 | }, 103 | initialValue: 104 | '${context.watch().shippingDetails.phoneContact}', 105 | ), 106 | TextFormField( 107 | keyboardType: TextInputType.name, 108 | decoration: InputDecoration( 109 | labelText: "Enter address line*", 110 | icon: Icon( 111 | Icons.place, 112 | ), 113 | focusedBorder: UnderlineInputBorder( 114 | borderSide: BorderSide(color: Colors.black), 115 | ), 116 | ), 117 | onSaved: (value) => _addressLine = value, 118 | validator: (value) => value.isEmpty ? 'Required' : null, 119 | initialValue: 120 | '${context.watch().shippingDetails.addressLine}', 121 | ), 122 | TextFormField( 123 | keyboardType: TextInputType.text, 124 | decoration: InputDecoration( 125 | labelText: "Enter City *", 126 | icon: Icon( 127 | Icons.location_city, 128 | ), 129 | focusedBorder: UnderlineInputBorder( 130 | borderSide: BorderSide(color: Colors.black), 131 | ), 132 | ), 133 | onSaved: (value) => _city = value, 134 | validator: (value) => value.isEmpty ? 'Required' : null, 135 | initialValue: 136 | '${context.watch().shippingDetails.city}', 137 | ), 138 | TextFormField( 139 | keyboardType: TextInputType.number, 140 | decoration: InputDecoration( 141 | labelText: "Enter Postal Code *", 142 | icon: Icon( 143 | Icons.local_post_office, 144 | ), 145 | focusedBorder: UnderlineInputBorder( 146 | borderSide: BorderSide(color: Colors.black), 147 | ), 148 | ), 149 | onSaved: (value) => _postalCode = value, 150 | validator: (value) { 151 | if (value.isEmpty) { 152 | return 'Required'; 153 | } 154 | if (!Validator.isPostalCodeValid(value)) { 155 | return 'Invalid postal code'; 156 | } 157 | return null; 158 | }, 159 | initialValue: 160 | '${context.watch().shippingDetails.postalCode}', 161 | ), 162 | TextFormField( 163 | keyboardType: TextInputType.text, 164 | decoration: InputDecoration( 165 | labelText: "Enter Country *", 166 | icon: Icon( 167 | Icons.my_location, 168 | ), 169 | focusedBorder: UnderlineInputBorder( 170 | borderSide: BorderSide(color: Colors.black), 171 | ), 172 | ), 173 | onSaved: (value) => _country = value, 174 | validator: (value) => value.isEmpty ? 'Required' : null, 175 | initialValue: 176 | '${context.watch().shippingDetails.country}', 177 | ), 178 | SizedBox( 179 | height: 20, 180 | ), 181 | RaisedButton( 182 | onPressed: () { 183 | if (_shippingDdetailsFormkey.currentState 184 | .validate()) { 185 | _shippingDdetailsFormkey.currentState.save(); 186 | 187 | var shippingDetails = ShippingDetails( 188 | name: _name, 189 | phoneContact: _phoneContact, 190 | city: _city, 191 | addressLine: _addressLine, 192 | postalCode: _postalCode, 193 | country: _country, 194 | ); 195 | 196 | Provider.of(context, listen: false) 197 | .setTaskCurrentTask( 198 | VIEWING_SINGLE_NEW_ORDER_HISTORY); 199 | 200 | Provider.of(context, 201 | listen: false) 202 | .setShippingDetails(details: shippingDetails); 203 | 204 | Provider.of(context, listen: false) 205 | .setShippingCost(_country); 206 | 207 | Provider.of(context, listen: false) 208 | .setTax(_country); 209 | 210 | Navigator.pushNamed(context, PaymentMethod.id); 211 | } 212 | }, 213 | child: Text( 214 | "CONTINUE", 215 | style: TextStyle( 216 | color: Colors.white, 217 | ), 218 | ), 219 | color: Colors.orange, 220 | shape: RoundedRectangleBorder( 221 | borderRadius: BorderRadius.circular(50), 222 | ), 223 | ), 224 | ], 225 | ), 226 | ), 227 | ], 228 | ), 229 | ), 230 | ), 231 | ), 232 | ); 233 | } 234 | } 235 | -------------------------------------------------------------------------------- /lib/screens/products_list.dart: -------------------------------------------------------------------------------- 1 | import 'package:badges/badges.dart'; 2 | import 'package:ecommerceapp/constants/screen_ids.dart'; 3 | import 'package:ecommerceapp/constants/screen_titles.dart'; 4 | import 'package:ecommerceapp/controllers/cart_controller.dart'; 5 | import 'package:ecommerceapp/controllers/category_controller.dart'; 6 | import 'package:ecommerceapp/controllers/product_controller.dart'; 7 | import 'package:ecommerceapp/screens/product_detail.dart'; 8 | import 'package:ecommerceapp/screens/shopping_cart.dart'; 9 | import 'package:ecommerceapp/skeletons/category_list_skeleton.dart'; 10 | import 'package:ecommerceapp/skeletons/product_list_skeleton.dart'; 11 | import 'package:ecommerceapp/widgets/category.dart'; 12 | import 'package:ecommerceapp/widgets/drawer.dart'; 13 | import 'package:ecommerceapp/widgets/product_card.dart'; 14 | import 'package:flutter/material.dart'; 15 | import 'package:flutter/rendering.dart'; 16 | import 'package:provider/provider.dart'; 17 | import 'package:shimmer/shimmer.dart'; 18 | 19 | class ProductList extends StatefulWidget { 20 | ProductList({Key key}) : super(key: key); 21 | static String id = ProductList_Screen_Id; 22 | 23 | @override 24 | _ProductListState createState() => _ProductListState(); 25 | } 26 | 27 | class _ProductListState extends State { 28 | var _textEditingController = TextEditingController(); 29 | int _categorySelectedIndex; 30 | var _productController; 31 | var _cartController; 32 | var _categoryController; 33 | final _scaffoldKey = GlobalKey(); 34 | 35 | @override 36 | void initState() { 37 | super.initState(); 38 | _productController = Provider.of(context, listen: false); 39 | _productController.getAllProducts(_scaffoldKey); 40 | _categoryController = 41 | Provider.of(context, listen: false); 42 | _categoryController.getAllCategories(_scaffoldKey); 43 | _cartController = Provider.of(context, listen: false); 44 | _cartController.getSavedCart(); 45 | _textEditingController.addListener(_handleSearchField); 46 | _categorySelectedIndex = 0; 47 | } 48 | 49 | @override 50 | void dispose() { 51 | _textEditingController.dispose(); 52 | super.dispose(); 53 | } 54 | 55 | _handleSearchField() { 56 | _productController.getProductByCategoryOrName( 57 | _textEditingController.text, 58 | ); 59 | _categorySelectedIndex = null; 60 | } 61 | 62 | Future _handleRefresh() { 63 | _productController.setIsLoadingAllProducts(true); 64 | _categoryController.setIsLoadingCategories(true); 65 | _categoryController.getAllCategories(_scaffoldKey); 66 | _productController.getAllProducts(_scaffoldKey); 67 | _categorySelectedIndex = 0; 68 | return Future.value(true); 69 | } 70 | 71 | @override 72 | Widget build(BuildContext context) { 73 | var size = MediaQuery.of(context).size; 74 | double _leftMargin = 18; 75 | double _rightMargin = 10; 76 | 77 | return Scaffold( 78 | key: _scaffoldKey, 79 | resizeToAvoidBottomInset: false, 80 | appBar: AppBar( 81 | title: Text( 82 | "$ProductList_Screen_Title", 83 | style: TextStyle( 84 | color: Colors.black, 85 | ), 86 | ), 87 | iconTheme: IconThemeData(color: Colors.black), 88 | elevation: 0, 89 | actions: [ 90 | Container( 91 | margin: EdgeInsets.only(right: 20, top: _rightMargin), 92 | child: GestureDetector( 93 | onTap: () { 94 | Navigator.pushNamed(context, ShoppingCart.id); 95 | }, 96 | child: Badge( 97 | padding: EdgeInsets.all(5), 98 | badgeContent: Text( 99 | '${context.watch().cart.length}', 100 | style: TextStyle( 101 | color: Colors.white, 102 | ), 103 | ), 104 | child: Icon( 105 | Icons.shopping_cart, 106 | color: Colors.orange, 107 | ), 108 | ), 109 | ), 110 | ), 111 | ], 112 | backgroundColor: Colors.white, 113 | ), 114 | body: SafeArea( 115 | child: Column( 116 | crossAxisAlignment: CrossAxisAlignment.start, 117 | children: [ 118 | //search field,title 119 | RefreshIndicator( 120 | onRefresh: _handleRefresh, 121 | child: ListView( 122 | shrinkWrap: true, 123 | children: [ 124 | SizedBox( 125 | height: 10, 126 | ), 127 | //search field 128 | Container( 129 | height: size.height / 15, 130 | margin: EdgeInsets.only( 131 | left: _leftMargin, 132 | right: _rightMargin, 133 | ), 134 | decoration: BoxDecoration( 135 | color: Colors.grey[300], 136 | borderRadius: BorderRadius.circular(50), 137 | ), 138 | child: TextField( 139 | controller: _textEditingController, 140 | decoration: InputDecoration( 141 | hintText: "Search by product name or category", 142 | contentPadding: EdgeInsets.only( 143 | top: 8, 144 | ), 145 | prefixIcon: Icon( 146 | Icons.search, 147 | color: Colors.black, 148 | ), 149 | fillColor: Colors.grey[300], 150 | border: InputBorder.none, 151 | focusedBorder: InputBorder.none, 152 | enabledBorder: InputBorder.none, 153 | errorBorder: InputBorder.none, 154 | disabledBorder: InputBorder.none, 155 | ), 156 | ), 157 | ), 158 | SizedBox( 159 | height: 30, 160 | ), 161 | 162 | //title 163 | Container( 164 | margin: EdgeInsets.only(left: _leftMargin), 165 | child: Text( 166 | "Get The Best Products", 167 | style: 168 | TextStyle(fontWeight: FontWeight.bold, fontSize: 30), 169 | ), 170 | ), 171 | 172 | SizedBox( 173 | height: 30, 174 | ), 175 | ], 176 | ), 177 | ), 178 | 179 | // Horizontal list of categories 180 | Container( 181 | height: 50, 182 | margin: EdgeInsets.only(left: _leftMargin, right: _rightMargin), 183 | child: Consumer( 184 | builder: (context, cateogoryCtlr, child) { 185 | if (cateogoryCtlr.isLoadingCategories) 186 | return Shimmer.fromColors( 187 | child: CategoryListSkeleton(), 188 | baseColor: Colors.grey[200], 189 | highlightColor: Colors.grey[400], 190 | ); 191 | 192 | return ListView.builder( 193 | scrollDirection: Axis.horizontal, 194 | itemCount: cateogoryCtlr.categoryList.length, 195 | itemBuilder: (context, index) { 196 | return Category( 197 | category: cateogoryCtlr.categoryList[index].category, 198 | categoryIndex: index, 199 | categorySelectedIndex: _categorySelectedIndex, 200 | onTapped: () { 201 | if (cateogoryCtlr.categoryList[index].category != 202 | null) { 203 | setState(() { 204 | _categorySelectedIndex = index; 205 | }); 206 | _productController.getProductByCategory( 207 | cateogoryCtlr.categoryList[index].category, 208 | _scaffoldKey, 209 | ); 210 | } 211 | }, 212 | ); 213 | }); 214 | })), 215 | SizedBox( 216 | height: 30, 217 | ), 218 | 219 | // List of products 220 | Expanded( 221 | child: Container( 222 | margin: EdgeInsets.only(left: _leftMargin, right: _rightMargin), 223 | child: Consumer( 224 | builder: (context, productCtlr, child) { 225 | if (productCtlr.isLoadingAllProducts) 226 | return Center( 227 | child: Shimmer.fromColors( 228 | child: ProductListSkeleton(), 229 | baseColor: Colors.grey[200], 230 | highlightColor: Colors.grey[400], 231 | ), 232 | ); 233 | if (!productCtlr.isLoadingAllProducts && 234 | productCtlr.productList.length == 0) 235 | return Center( 236 | child: Text( 237 | 'Results not found ', 238 | style: TextStyle( 239 | fontWeight: FontWeight.bold, 240 | fontSize: 20, 241 | ), 242 | ), 243 | ); 244 | 245 | return GridView.builder( 246 | gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( 247 | crossAxisCount: 2, 248 | crossAxisSpacing: 10, 249 | mainAxisSpacing: 15, 250 | ), 251 | itemCount: productCtlr.productList.length, 252 | itemBuilder: (context, index) { 253 | return ProductCard( 254 | product: productCtlr.productList[index], 255 | onProductTapped: () { 256 | _cartController 257 | .setCurrentItem(productCtlr.productList[index]); 258 | Navigator.pushNamed(context, ProductDetail.id); 259 | }, 260 | ); 261 | }, 262 | ); 263 | }), 264 | ), 265 | ), 266 | 267 | SizedBox( 268 | height: 30, 269 | ), 270 | ], 271 | ), 272 | ), 273 | drawer: Container( 274 | width: size.width * 0.8, 275 | child: CDrawer(), 276 | ), 277 | ); 278 | } 279 | } 280 | -------------------------------------------------------------------------------- /lib/screens/payment_method.dart: -------------------------------------------------------------------------------- 1 | import 'package:ecommerceapp/constants/payment.dart'; 2 | import 'package:ecommerceapp/constants/screen_ids.dart'; 3 | import 'package:ecommerceapp/constants/screen_titles.dart'; 4 | import 'package:ecommerceapp/controllers/auth_controller.dart'; 5 | import 'package:ecommerceapp/controllers/cart_controller.dart'; 6 | import 'package:ecommerceapp/controllers/error_controller.dart'; 7 | import 'package:ecommerceapp/controllers/order_controller.dart'; 8 | import 'package:ecommerceapp/controllers/shipping_controller.dart'; 9 | import 'package:ecommerceapp/screens/thank_you.dart'; 10 | import 'package:ecommerceapp/services/paypal_service.dart'; 11 | import 'package:ecommerceapp/services/stripe_service.dart'; 12 | import 'package:ecommerceapp/widgets/dialog.dart'; 13 | import 'package:ecommerceapp/widgets/global_snackbar.dart'; 14 | import 'package:flutter/material.dart'; 15 | import 'package:provider/provider.dart'; 16 | 17 | class PaymentMethod extends StatefulWidget { 18 | PaymentMethod({Key key}) : super(key: key); 19 | static String id = PaymentMethod_Screen_Id; 20 | 21 | @override 22 | _PaymentMethodState createState() => _PaymentMethodState(); 23 | } 24 | 25 | class _PaymentMethodState extends State { 26 | var _authController; 27 | var _cartController; 28 | var _shippingController; 29 | var _orderController; 30 | final _scaffoldKey = GlobalKey(); 31 | var _progressDialog; 32 | 33 | @override 34 | void initState() { 35 | super.initState(); 36 | _authController = AuthController(); 37 | _cartController = Provider.of(context, listen: false); 38 | _shippingController = 39 | Provider.of(context, listen: false); 40 | _orderController = Provider.of(context, listen: false); 41 | StripeService.init(); 42 | } 43 | 44 | @override 45 | Widget build(BuildContext context) { 46 | var size = MediaQuery.of(context).size; 47 | 48 | var totalItemPrice = _cartController.cart.fold( 49 | 0, 50 | (previousValue, element) => 51 | previousValue + (element.product.price * element.quantity)); 52 | int tax = _orderController.tax; 53 | int shippingCost = _orderController.shippingCost; 54 | 55 | var total = totalItemPrice + tax + shippingCost; 56 | String totalToString = total.toString() + '100'; 57 | 58 | _progressDialog = CDialog(context).dialog; 59 | 60 | _peformStateReset() { 61 | _cartController.resetCart(); 62 | _shippingController.reset(); 63 | } 64 | 65 | _handleStripeSucessPayment() async { 66 | var data = await _authController.getUserDataAndLoginStatus(); 67 | 68 | _orderController.registerOrderWithStripePayment( 69 | _shippingController.getShippingDetails(), 70 | shippingCost.toString(), 71 | tax.toString(), 72 | total.toString(), 73 | totalItemPrice.toString(), 74 | data[0], 75 | STRIPE_PAYMENT, 76 | _cartController.cart, 77 | _scaffoldKey, 78 | ); 79 | 80 | await _progressDialog.hide(); 81 | _peformStateReset(); 82 | Navigator.pushNamed(context, Thanks.id); 83 | } 84 | 85 | _handleStripeFailurePayment() async { 86 | await _progressDialog.hide(); 87 | 88 | GlobalSnackBar.showSnackbar( 89 | _scaffoldKey, 90 | 'Process cancelled', 91 | SnackBarType.Error, 92 | ); 93 | } 94 | 95 | _handlePaypalBrainTree(String nonce) async { 96 | var data = await _authController.getUserDataAndLoginStatus(); 97 | 98 | _orderController.processOrderWithPaypal( 99 | _shippingController.getShippingDetails(), 100 | shippingCost.toString(), 101 | tax.toString(), 102 | total.toString(), 103 | totalItemPrice.toString(), 104 | data[0], 105 | PAY_PAL, 106 | _cartController.cart, 107 | nonce, 108 | _scaffoldKey, 109 | ); 110 | 111 | await _progressDialog.hide(); 112 | _peformStateReset(); 113 | Navigator.pushNamed(context, Thanks.id); 114 | } 115 | 116 | return SafeArea( 117 | child: Scaffold( 118 | key: _scaffoldKey, 119 | appBar: AppBar( 120 | title: Text( 121 | '$Payment_Screen_Title', 122 | style: TextStyle( 123 | color: Colors.black, 124 | ), 125 | ), 126 | iconTheme: IconThemeData(color: Colors.black), 127 | elevation: 1, 128 | backgroundColor: Colors.white, 129 | ), 130 | body: Container( 131 | margin: EdgeInsets.only( 132 | left: 18, 133 | top: 18, 134 | right: 18, 135 | ), 136 | child: ListView( 137 | children: [ 138 | Column( 139 | crossAxisAlignment: CrossAxisAlignment.center, 140 | children: [ 141 | SizedBox( 142 | height: 20, 143 | ), 144 | //title 145 | Text( 146 | "Order summary", 147 | style: TextStyle( 148 | fontWeight: FontWeight.bold, 149 | fontSize: 20, 150 | ), 151 | ), 152 | SizedBox( 153 | height: 25, 154 | ), 155 | Table( 156 | border: TableBorder( 157 | horizontalInside: BorderSide( 158 | width: .5, 159 | ), 160 | bottom: BorderSide( 161 | width: .5, 162 | ), 163 | top: BorderSide( 164 | width: .5, 165 | ), 166 | ), 167 | children: [ 168 | //item 169 | TableRow( 170 | children: [ 171 | Padding( 172 | padding: const EdgeInsets.only( 173 | top: 10.0, 174 | bottom: 10.0, 175 | ), 176 | child: Text( 177 | 'Items', 178 | style: TextStyle( 179 | fontSize: 15, 180 | ), 181 | ), 182 | ), 183 | Padding( 184 | padding: const EdgeInsets.only( 185 | top: 10.0, 186 | bottom: 10.0, 187 | ), 188 | child: Text( 189 | '\$ $totalItemPrice', 190 | style: TextStyle( 191 | fontSize: 15, 192 | ), 193 | ), 194 | ), 195 | ], 196 | ), 197 | //shipping 198 | TableRow( 199 | children: [ 200 | Padding( 201 | padding: EdgeInsets.only( 202 | top: 10.0, 203 | bottom: 10.0, 204 | ), 205 | child: Text( 206 | 'Shipping', 207 | style: TextStyle( 208 | fontSize: 16, 209 | ), 210 | ), 211 | ), 212 | Padding( 213 | padding: EdgeInsets.only( 214 | top: 10.0, 215 | bottom: 10.0, 216 | ), 217 | child: Text( 218 | '\$ $shippingCost', 219 | style: TextStyle( 220 | fontSize: 16, 221 | ), 222 | ), 223 | ), 224 | ], 225 | ), 226 | //tax 227 | TableRow( 228 | children: [ 229 | Padding( 230 | padding: const EdgeInsets.only( 231 | top: 8.0, 232 | bottom: 8.0, 233 | ), 234 | child: Text( 235 | 'Tax', 236 | style: TextStyle( 237 | fontSize: 16, 238 | ), 239 | ), 240 | ), 241 | Padding( 242 | padding: const EdgeInsets.only( 243 | top: 10.0, 244 | bottom: 10.0, 245 | ), 246 | child: Text( 247 | '\$ $tax', 248 | style: TextStyle( 249 | fontSize: 16, 250 | ), 251 | ), 252 | ), 253 | ], 254 | ), 255 | //total 256 | TableRow( 257 | children: [ 258 | Padding( 259 | padding: const EdgeInsets.only( 260 | top: 10.0, 261 | bottom: 10.0, 262 | ), 263 | child: Text( 264 | 'Total', 265 | style: TextStyle( 266 | fontSize: 16, 267 | ), 268 | ), 269 | ), 270 | Padding( 271 | padding: const EdgeInsets.only( 272 | top: 10.0, 273 | bottom: 10.0, 274 | ), 275 | child: Text( 276 | '\$ $total', 277 | style: TextStyle( 278 | fontSize: 16, 279 | ), 280 | ), 281 | ), 282 | ], 283 | ) 284 | ], 285 | ), 286 | 287 | SizedBox( 288 | height: 40, 289 | ), 290 | // Payment 291 | Text( 292 | "Choose Payment method", 293 | style: TextStyle( 294 | fontWeight: FontWeight.bold, 295 | fontSize: 20, 296 | ), 297 | ), 298 | SizedBox( 299 | height: 10, 300 | ), 301 | 302 | //paypal 303 | ButtonTheme( 304 | minWidth: size.width, 305 | child: RaisedButton( 306 | color: Colors.orange[700], 307 | shape: RoundedRectangleBorder( 308 | borderRadius: BorderRadius.circular(20), 309 | ), 310 | onPressed: () async { 311 | await _progressDialog.show(); 312 | var nonce = await PayPalService.processPayment( 313 | total.toString()); 314 | if (nonce != null) { 315 | _handlePaypalBrainTree(nonce); 316 | } else { 317 | await _progressDialog.hide(); 318 | ErrorController.showCustomError( 319 | _scaffoldKey, 'An error occurred'); 320 | } 321 | }, 322 | child: RichText( 323 | text: TextSpan( 324 | text: "Pay", 325 | style: TextStyle( 326 | color: Colors.black, 327 | fontWeight: FontWeight.bold, 328 | ), 329 | children: [ 330 | TextSpan( 331 | text: "Pal", 332 | style: TextStyle( 333 | color: Colors.blue[100], 334 | ), 335 | ), 336 | ], 337 | ), 338 | ), 339 | ), 340 | ), 341 | 342 | Text( 343 | "or", 344 | style: TextStyle( 345 | fontWeight: FontWeight.bold, 346 | fontSize: 20, 347 | ), 348 | ), 349 | 350 | //credit or debit button 351 | RaisedButton( 352 | color: Colors.black, 353 | shape: RoundedRectangleBorder( 354 | borderRadius: BorderRadius.circular(20), 355 | ), 356 | onPressed: () async { 357 | await _progressDialog.show(); 358 | 359 | var result = await StripeService.processPayment( 360 | totalToString, 'usd'); 361 | 362 | if (result.success) { 363 | _handleStripeSucessPayment(); 364 | } else { 365 | _handleStripeFailurePayment(); 366 | } 367 | }, 368 | child: Row( 369 | mainAxisAlignment: MainAxisAlignment.center, 370 | children: [ 371 | Icon( 372 | Icons.credit_card, 373 | color: Colors.white, 374 | ), 375 | SizedBox( 376 | width: 10, 377 | ), 378 | Text( 379 | "Credit or Debit card", 380 | style: TextStyle( 381 | color: Colors.white, 382 | ), 383 | ), 384 | ], 385 | ), 386 | ), 387 | ], 388 | ), 389 | ], 390 | ), 391 | ), 392 | ), 393 | ); 394 | } 395 | } 396 | --------------------------------------------------------------------------------