├── 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 ├── screenshots ├── cart.jpg ├── home1.jpg ├── home2.jpg ├── login.jpg ├── account.jpg ├── drawer1.jpg ├── drawer2.jpg ├── latest.jpg ├── order1.jpg ├── order2.jpg ├── orders.jpg ├── product1.jpg ├── product2.jpg ├── products.jpg ├── search.jpg ├── categories1.jpg ├── categories2.jpg └── new account.jpg ├── assets ├── images │ ├── Banner1.jpg │ ├── Banner2.jpg │ ├── grodudes_logo.png │ └── grodudes_logo_svg.svg └── fonts │ ├── OpenSans-Bold.ttf │ ├── OpenSans-Light.ttf │ ├── OpenSans-Regular.ttf │ ├── OpenSans-SemiBold.ttf │ └── LICENSE.txt ├── android ├── gradle.properties ├── .gitignore ├── 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 │ │ │ │ ├── drawable │ │ │ │ │ └── launch_background.xml │ │ │ │ └── values │ │ │ │ │ └── styles.xml │ │ │ ├── kotlin │ │ │ │ └── com │ │ │ │ │ └── example │ │ │ │ │ └── grodudes │ │ │ │ │ └── MainActivity.kt │ │ │ └── AndroidManifest.xml │ │ ├── debug │ │ │ └── AndroidManifest.xml │ │ └── profile │ │ │ └── AndroidManifest.xml │ └── build.gradle ├── gradle │ └── wrapper │ │ └── gradle-wrapper.properties ├── settings.gradle └── build.gradle ├── lib ├── models │ ├── Category.dart │ └── Product.dart ├── components │ ├── ProductsList.dart │ ├── ProductDescriptionText.dart │ ├── StyledProductPrice.dart │ ├── QuantityToggle.dart │ ├── ProductCard.dart │ ├── ProductListTile.dart │ ├── CartListItem.dart │ ├── CategoryCard.dart │ └── RootDrawer.dart ├── helper │ ├── Constants.dart │ ├── ImageFetcher.dart │ └── WooCommerceAPI.dart ├── pages │ ├── account │ │ ├── AccountRoot.dart │ │ ├── AccountDetails.dart │ │ ├── OrderDetails.dart │ │ ├── LogInPage.dart │ │ └── AddressDetails.dart │ ├── AllCategoriesPage.dart │ ├── AllProductsPage.dart │ ├── HomeSpecialProductsPage.dart │ ├── CartItems.dart │ ├── SearchResultsPage.dart │ ├── checkout │ │ ├── OrderPlacementPage.dart │ │ ├── AddressUpdatePage.dart │ │ └── ConfirmationPage.dart │ └── ProductDetailsPage.dart ├── state │ ├── cart_state.dart │ ├── products_state.dart │ └── user_state.dart └── Root.dart ├── .metadata ├── .gitignore ├── LICENSE.md ├── test └── widget_test.dart ├── README.md └── pubspec.yaml /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 | -------------------------------------------------------------------------------- /screenshots/cart.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Reshav-Paul/grodudes_app/HEAD/screenshots/cart.jpg -------------------------------------------------------------------------------- /screenshots/home1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Reshav-Paul/grodudes_app/HEAD/screenshots/home1.jpg -------------------------------------------------------------------------------- /screenshots/home2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Reshav-Paul/grodudes_app/HEAD/screenshots/home2.jpg -------------------------------------------------------------------------------- /screenshots/login.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Reshav-Paul/grodudes_app/HEAD/screenshots/login.jpg -------------------------------------------------------------------------------- /screenshots/account.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Reshav-Paul/grodudes_app/HEAD/screenshots/account.jpg -------------------------------------------------------------------------------- /screenshots/drawer1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Reshav-Paul/grodudes_app/HEAD/screenshots/drawer1.jpg -------------------------------------------------------------------------------- /screenshots/drawer2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Reshav-Paul/grodudes_app/HEAD/screenshots/drawer2.jpg -------------------------------------------------------------------------------- /screenshots/latest.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Reshav-Paul/grodudes_app/HEAD/screenshots/latest.jpg -------------------------------------------------------------------------------- /screenshots/order1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Reshav-Paul/grodudes_app/HEAD/screenshots/order1.jpg -------------------------------------------------------------------------------- /screenshots/order2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Reshav-Paul/grodudes_app/HEAD/screenshots/order2.jpg -------------------------------------------------------------------------------- /screenshots/orders.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Reshav-Paul/grodudes_app/HEAD/screenshots/orders.jpg -------------------------------------------------------------------------------- /screenshots/product1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Reshav-Paul/grodudes_app/HEAD/screenshots/product1.jpg -------------------------------------------------------------------------------- /screenshots/product2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Reshav-Paul/grodudes_app/HEAD/screenshots/product2.jpg -------------------------------------------------------------------------------- /screenshots/products.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Reshav-Paul/grodudes_app/HEAD/screenshots/products.jpg -------------------------------------------------------------------------------- /screenshots/search.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Reshav-Paul/grodudes_app/HEAD/screenshots/search.jpg -------------------------------------------------------------------------------- /assets/images/Banner1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Reshav-Paul/grodudes_app/HEAD/assets/images/Banner1.jpg -------------------------------------------------------------------------------- /assets/images/Banner2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Reshav-Paul/grodudes_app/HEAD/assets/images/Banner2.jpg -------------------------------------------------------------------------------- /screenshots/categories1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Reshav-Paul/grodudes_app/HEAD/screenshots/categories1.jpg -------------------------------------------------------------------------------- /screenshots/categories2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Reshav-Paul/grodudes_app/HEAD/screenshots/categories2.jpg -------------------------------------------------------------------------------- /screenshots/new account.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Reshav-Paul/grodudes_app/HEAD/screenshots/new account.jpg -------------------------------------------------------------------------------- /assets/fonts/OpenSans-Bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Reshav-Paul/grodudes_app/HEAD/assets/fonts/OpenSans-Bold.ttf -------------------------------------------------------------------------------- /assets/fonts/OpenSans-Light.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Reshav-Paul/grodudes_app/HEAD/assets/fonts/OpenSans-Light.ttf -------------------------------------------------------------------------------- /assets/images/grodudes_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Reshav-Paul/grodudes_app/HEAD/assets/images/grodudes_logo.png -------------------------------------------------------------------------------- /assets/fonts/OpenSans-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Reshav-Paul/grodudes_app/HEAD/assets/fonts/OpenSans-Regular.ttf -------------------------------------------------------------------------------- /assets/fonts/OpenSans-SemiBold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Reshav-Paul/grodudes_app/HEAD/assets/fonts/OpenSans-SemiBold.ttf -------------------------------------------------------------------------------- /android/gradle.properties: -------------------------------------------------------------------------------- 1 | org.gradle.jvmargs=-Xmx1536M 2 | android.enableR8=true 3 | android.useAndroidX=true 4 | android.enableJetifier=true 5 | -------------------------------------------------------------------------------- /android/.gitignore: -------------------------------------------------------------------------------- 1 | gradle-wrapper.jar 2 | /.gradle 3 | /captures/ 4 | /gradlew 5 | /gradlew.bat 6 | /local.properties 7 | GeneratedPluginRegistrant.java 8 | -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Reshav-Paul/grodudes_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/Reshav-Paul/grodudes_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/Reshav-Paul/grodudes_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/Reshav-Paul/grodudes_app/HEAD/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /lib/models/Category.dart: -------------------------------------------------------------------------------- 1 | class Category { 2 | Map data; 3 | 4 | Category(Map data) { 5 | this.data = data; 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Reshav-Paul/grodudes_app/HEAD/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Reshav-Paul/grodudes_app/HEAD/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Reshav-Paul/grodudes_app/HEAD/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Reshav-Paul/grodudes_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/Reshav-Paul/grodudes_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/Reshav-Paul/grodudes_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/Reshav-Paul/grodudes_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/Reshav-Paul/grodudes_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/Reshav-Paul/grodudes_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/Reshav-Paul/grodudes_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/Reshav-Paul/grodudes_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/Reshav-Paul/grodudes_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/Reshav-Paul/grodudes_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/Reshav-Paul/grodudes_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/Reshav-Paul/grodudes_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/Reshav-Paul/grodudes_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/Reshav-Paul/grodudes_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/Reshav-Paul/grodudes_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/Reshav-Paul/grodudes_app/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png -------------------------------------------------------------------------------- /android/app/src/main/kotlin/com/example/grodudes/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package com.example.grodudes 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 | -------------------------------------------------------------------------------- /ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PreviewsEnabled 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /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: 1ad9baa8b99a2897c20f9e6e54d3b9b359ade314 8 | channel: stable 9 | 10 | project_type: app 11 | -------------------------------------------------------------------------------- /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. -------------------------------------------------------------------------------- /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/models/Product.dart: -------------------------------------------------------------------------------- 1 | class Product { 2 | Map data; 3 | int quantity; 4 | 5 | Product(Map data) { 6 | this.data = data; 7 | quantity = 1; 8 | } 9 | 10 | @override 11 | bool operator ==(other) { 12 | return other is Product && this.data['id'] == other.data['id']; 13 | } 14 | 15 | bool isSameAs(Product item) { 16 | return this.data['id'] == item.data['id']; 17 | } 18 | 19 | @override 20 | int get hashCode => this.data['id'].hashCode; 21 | } 22 | -------------------------------------------------------------------------------- /android/app/src/main/res/drawable/launch_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 12 | 13 | -------------------------------------------------------------------------------- /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/components/ProductsList.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:grodudes/components/ProductListTile.dart'; 3 | import 'package:grodudes/models/Product.dart'; 4 | 5 | class ProductsList extends StatelessWidget { 6 | final List items; 7 | ProductsList(this.items); 8 | @override 9 | Widget build(BuildContext context) { 10 | if (this.items == null || this.items.length == 0) { 11 | return SizedBox(height: 0); 12 | } 13 | return ListView.builder( 14 | padding: EdgeInsets.all(8), 15 | itemCount: items.length, 16 | itemBuilder: (context, index) => ProductListTile(items[index]), 17 | ); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /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/settings.gradle: -------------------------------------------------------------------------------- 1 | // Copyright 2014 The Flutter Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style license that can be 3 | // found in the LICENSE file. 4 | 5 | include ':app' 6 | 7 | def localPropertiesFile = new File(rootProject.projectDir, "local.properties") 8 | def properties = new Properties() 9 | 10 | assert localPropertiesFile.exists() 11 | localPropertiesFile.withReader("UTF-8") { reader -> properties.load(reader) } 12 | 13 | def flutterSdkPath = properties.getProperty("flutter.sdk") 14 | assert flutterSdkPath != null, "flutter.sdk not set in local.properties" 15 | apply from: "$flutterSdkPath/packages/flutter_tools/gradle/app_plugin_loader.gradle" 16 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /.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 | .dart_tool/ 26 | .flutter-plugins 27 | .flutter-plugins-dependencies 28 | .packages 29 | .pub-cache/ 30 | .pub/ 31 | /build/ 32 | 33 | # Web related 34 | lib/generated_plugin_registrant.dart 35 | 36 | # Symbolication related 37 | app.*.symbols 38 | 39 | # Obfuscation related 40 | app.*.map.json 41 | 42 | # Exceptions to above rules. 43 | !/packages/flutter_tools/test/data/dart_dependencies_test/**/.packages 44 | secrets.dart -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /lib/helper/Constants.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class GrodudesPrimaryColor { 4 | static Map primaryColor = { 5 | 50: Color.fromRGBO(0, 4, 40, 0.1), 6 | 100: Color.fromRGBO(0, 4, 40, 0.2), 7 | 200: Color.fromRGBO(0, 4, 40, 0.3), 8 | 300: Color.fromRGBO(0, 4, 40, 0.4), 9 | 400: Color.fromRGBO(0, 4, 40, 0.5), 10 | 500: Color.fromRGBO(0, 4, 40, 0.6), 11 | 600: Color.fromRGBO(0, 4, 40, 0.7), 12 | 700: Color.fromRGBO(0, 4, 40, 0.8), 13 | 800: Color.fromRGBO(0, 4, 40, 0.9), 14 | 900: Color.fromRGBO(0, 4, 40, 1), 15 | }; 16 | 17 | static MaterialColor customSwatch = MaterialColor(0xFF000428, primaryColor); 18 | } 19 | 20 | final List pincodes = [ 21 | '712201', 22 | '712202', 23 | '712203', 24 | '712204', 25 | '712205', 26 | '712234', 27 | '712235', 28 | '712246', 29 | '712247', 30 | '712248', 31 | '712249', 32 | '712250', 33 | ]; 34 | -------------------------------------------------------------------------------- /lib/components/ProductDescriptionText.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_html/flutter_html.dart'; 3 | // import 'package:flutter_html/html_parser.dart'; 4 | import 'package:flutter_html/style.dart'; 5 | 6 | class ProductDescriptionText extends StatelessWidget { 7 | final String title; 8 | final String htmlText; 9 | ProductDescriptionText(this.title, this.htmlText); 10 | @override 11 | Widget build(BuildContext context) { 12 | return Column( 13 | crossAxisAlignment: CrossAxisAlignment.start, 14 | children: [ 15 | Text( 16 | title ?? '', 17 | style: TextStyle(fontWeight: FontWeight.bold, fontSize: 20), 18 | ), 19 | Html( 20 | data: htmlText ?? '', 21 | style: { 22 | 'p': Style(fontSize: FontSize(16), margin: EdgeInsets.all(0)) 23 | }, 24 | ), 25 | ], 26 | ); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /android/app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 15 | 18 | 19 | -------------------------------------------------------------------------------- /lib/helper/ImageFetcher.dart: -------------------------------------------------------------------------------- 1 | import 'package:cached_network_image/cached_network_image.dart'; 2 | import 'package:flutter/material.dart'; 3 | 4 | class ImageFetcher { 5 | static Widget getImage(url, 6 | {BoxFit fit = BoxFit.contain, 7 | Duration fadeInDuration = const Duration(milliseconds: 500)}) { 8 | return CachedNetworkImage( 9 | imageUrl: url ?? '', 10 | alignment: Alignment.center, 11 | errorWidget: (context, url, error) => Align( 12 | alignment: Alignment.center, 13 | child: Icon( 14 | Icons.error, 15 | size: 40, 16 | color: Colors.red[400], 17 | ), 18 | ), 19 | filterQuality: FilterQuality.low, 20 | fit: fit, 21 | fadeInDuration: fadeInDuration, 22 | progressIndicatorBuilder: (context, url, downloadProgress) => Align( 23 | alignment: Alignment.center, 24 | child: LimitedBox( 25 | maxHeight: 6, 26 | maxWidth: 6, 27 | child: CircularProgressIndicator( 28 | value: downloadProgress.progress, 29 | ), 30 | ), 31 | ), 32 | ); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Reshav Paul 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /test/widget_test.dart: -------------------------------------------------------------------------------- 1 | // This is a basic Flutter widget test. 2 | // 3 | // To perform an interaction with a widget in your test, use the WidgetTester 4 | // utility that Flutter provides. For example, you can send tap and scroll 5 | // gestures. You can also use WidgetTester to find child widgets in the widget 6 | // tree, read text, and verify that the values of widget properties are correct. 7 | 8 | import 'package:flutter/material.dart'; 9 | import 'package:flutter_test/flutter_test.dart'; 10 | 11 | import 'package:grodudes/main.dart'; 12 | 13 | void main() { 14 | testWidgets('Counter increments smoke test', (WidgetTester tester) async { 15 | // Build our app and trigger a frame. 16 | await tester.pumpWidget(MyApp()); 17 | 18 | // Verify that our counter starts at 0. 19 | expect(find.text('0'), findsOneWidget); 20 | expect(find.text('1'), findsNothing); 21 | 22 | // Tap the '+' icon and trigger a frame. 23 | await tester.tap(find.byIcon(Icons.add)); 24 | await tester.pump(); 25 | 26 | // Verify that our counter has incremented. 27 | expect(find.text('0'), findsNothing); 28 | expect(find.text('1'), findsOneWidget); 29 | }); 30 | } 31 | -------------------------------------------------------------------------------- /lib/pages/account/AccountRoot.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:grodudes/pages/account/AccountDetails.dart'; 3 | import 'package:grodudes/pages/account/LogInPage.dart'; 4 | import 'package:grodudes/state/user_state.dart'; 5 | import 'package:provider/provider.dart'; 6 | 7 | class AccountRoot extends StatelessWidget { 8 | @override 9 | Widget build(BuildContext context) { 10 | return Scaffold( 11 | appBar: AppBar(), 12 | body: Consumer( 13 | builder: (context, user, child) { 14 | var logInStatus = user.getLogInStatus(); 15 | return Container( 16 | child: Stack( 17 | children: [ 18 | logInStatus == logInStates.loggedIn 19 | ? AccountDetails() 20 | : LoginPage(), 21 | logInStatus == logInStates.pending 22 | ? Container( 23 | color: Colors.white, 24 | child: Center(child: CircularProgressIndicator()), 25 | ) 26 | : SizedBox(height: 0, width: 0) 27 | ], 28 | ), 29 | ); 30 | }, 31 | ), 32 | ); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /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 | grodudes 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/components/StyledProductPrice.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class StyledProductPrice extends StatelessWidget { 4 | final String price; 5 | final String regularPrice; 6 | final double priceFontSize; 7 | final double regularPriceFontSize; 8 | StyledProductPrice(this.price, this.regularPrice, 9 | {this.priceFontSize = 14, this.regularPriceFontSize = 12}); 10 | 11 | bool shouldDisplayRegularPrice() { 12 | if (this.regularPrice == null || this.regularPrice.length == 0) 13 | return false; 14 | 15 | try { 16 | int regularPrice = double.parse(this.regularPrice).round(); 17 | int currentPrice = double.parse(this.price).round(); 18 | if (regularPrice == currentPrice) return false; 19 | } catch (err) { 20 | print(err); 21 | return false; 22 | } 23 | return true; 24 | } 25 | 26 | @override 27 | Widget build(BuildContext context) { 28 | return Wrap( 29 | crossAxisAlignment: WrapCrossAlignment.center, 30 | children: [ 31 | Text( 32 | '₹ ${this.price}', 33 | style: TextStyle( 34 | fontWeight: FontWeight.bold, 35 | color: Colors.green[500], 36 | fontSize: this.priceFontSize, 37 | ), 38 | ), 39 | SizedBox(width: 4), 40 | shouldDisplayRegularPrice() == true 41 | ? Text( 42 | '₹ ${this.regularPrice}', 43 | style: TextStyle( 44 | color: Colors.black54, 45 | decoration: TextDecoration.lineThrough, 46 | fontSize: this.regularPriceFontSize, 47 | ), 48 | overflow: TextOverflow.ellipsis, 49 | ) 50 | : SizedBox(width: 0), 51 | ], 52 | ); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /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.grodudes" 42 | minSdkVersion 18 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 | } 64 | -------------------------------------------------------------------------------- /lib/components/QuantityToggle.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:grodudes/helper/Constants.dart'; 3 | import 'package:grodudes/models/Product.dart'; 4 | import 'package:grodudes/state/cart_state.dart'; 5 | import 'package:provider/provider.dart'; 6 | 7 | class QuantityToggle extends StatelessWidget { 8 | final Product item; 9 | final EdgeInsetsGeometry margin; 10 | final double iconSize; 11 | QuantityToggle(this.item, 12 | {this.margin = const EdgeInsets.symmetric(horizontal: 4), 13 | this.iconSize = 26}); 14 | @override 15 | Widget build(BuildContext context) { 16 | if (this.item == null || this.item.data['id'] == null) { 17 | return SizedBox(height: 0); 18 | } 19 | return Consumer( 20 | builder: (context, cart, child) { 21 | if (cart.isPresentInCart(item)) { 22 | return Container( 23 | decoration: BoxDecoration( 24 | color: GrodudesPrimaryColor.primaryColor[700], 25 | borderRadius: BorderRadius.circular(14)), 26 | padding: EdgeInsets.all(2), 27 | margin: this.margin, 28 | child: Row( 29 | mainAxisSize: MainAxisSize.min, 30 | children: [ 31 | GestureDetector( 32 | child: Icon(Icons.remove_circle, 33 | size: this.iconSize, color: Colors.white), 34 | onTap: () => cart.decrementQuantityOfProduct(item), 35 | ), 36 | SizedBox( 37 | width: 20, 38 | child: Text( 39 | '${this.item.quantity}', 40 | style: TextStyle( 41 | color: Colors.white, fontWeight: FontWeight.bold), 42 | textAlign: TextAlign.center, 43 | ), 44 | ), 45 | GestureDetector( 46 | child: Icon(Icons.add_circle, 47 | size: this.iconSize, color: Colors.white), 48 | onTap: () => cart.incrementQuantityOfProduct(item), 49 | ), 50 | ], 51 | ), 52 | ); 53 | } else { 54 | return GestureDetector( 55 | child: Icon(Icons.add_shopping_cart, size: this.iconSize), 56 | onTap: () => cart.addCartItem(item), 57 | ); 58 | } 59 | }, 60 | ); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /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/pages/AllCategoriesPage.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/cupertino.dart'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:grodudes/components/CategoryCard.dart'; 4 | import 'package:grodudes/state/products_state.dart'; 5 | import 'package:provider/provider.dart'; 6 | 7 | class AllCategoriesPage extends StatefulWidget { 8 | @override 9 | _AllCategoriesPageState createState() => _AllCategoriesPageState(); 10 | } 11 | 12 | class _AllCategoriesPageState extends State 13 | with AutomaticKeepAliveClientMixin { 14 | @override 15 | bool get wantKeepAlive => true; 16 | 17 | @override 18 | Widget build(BuildContext context) { 19 | super.build(context); 20 | return Consumer( 21 | builder: (context, productsManager, child) { 22 | return FutureBuilder( 23 | future: productsManager.fetchAllParentCategories(), 24 | builder: (context, snapshot) { 25 | if (snapshot.connectionState != ConnectionState.done) 26 | return Center(child: CircularProgressIndicator()); 27 | if (snapshot.hasData) { 28 | return GridView.builder( 29 | gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( 30 | crossAxisCount: 2, 31 | childAspectRatio: 1, 32 | crossAxisSpacing: 20, 33 | mainAxisSpacing: 20, 34 | ), 35 | padding: EdgeInsets.all(16), 36 | itemCount: productsManager.categories.length, 37 | itemBuilder: (context, index) { 38 | return CategoryCard( 39 | productsManager.categories.values.elementAt(index), 40 | ); 41 | }, 42 | ); 43 | } 44 | if (snapshot.hasError) { 45 | print(snapshot.error); 46 | return Column( 47 | mainAxisAlignment: MainAxisAlignment.center, 48 | children: [ 49 | Text('Failed to fetch categories'), 50 | RaisedButton( 51 | child: Text( 52 | 'Retry', 53 | style: TextStyle(color: Colors.white), 54 | ), 55 | onPressed: () => setState(() {}), 56 | ) 57 | ], 58 | ); 59 | } 60 | return Center(child: CircularProgressIndicator()); 61 | }, 62 | ); 63 | }, 64 | ); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /lib/pages/AllProductsPage.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:grodudes/components/ProductListTile.dart'; 3 | import 'package:grodudes/state/products_state.dart'; 4 | import 'package:provider/provider.dart'; 5 | 6 | class AllProductsPage extends StatefulWidget { 7 | @override 8 | _AllProductsPageState createState() => _AllProductsPageState(); 9 | } 10 | 11 | class _AllProductsPageState extends State 12 | with AutomaticKeepAliveClientMixin { 13 | @override 14 | bool get wantKeepAlive => true; 15 | bool _isLoading; 16 | 17 | ScrollController _scrollController; 18 | @override 19 | void initState() { 20 | this._isLoading = false; 21 | this._scrollController = ScrollController(); 22 | this._scrollController.addListener(_scrollHandler); 23 | super.initState(); 24 | } 25 | 26 | Future _scrollHandler() async { 27 | if (_isLoading) return; 28 | 29 | try { 30 | setState(() => this._isLoading = true); 31 | if (this._scrollController.position.pixels == 32 | this._scrollController.position.maxScrollExtent) { 33 | await Provider.of(context, listen: false) 34 | .fetchNextProductsPage(); 35 | } 36 | } catch (err) { 37 | print(err); 38 | } finally { 39 | setState(() => this._isLoading = false); 40 | } 41 | } 42 | 43 | @override 44 | void dispose() { 45 | this._scrollController.dispose(); 46 | super.dispose(); 47 | } 48 | 49 | @override 50 | Widget build(BuildContext context) { 51 | super.build(context); 52 | return Consumer( 53 | builder: (context, productsManager, child) { 54 | if (productsManager.products.isEmpty) { 55 | Provider.of(context, listen: false) 56 | .fetchNextProductsPage(); 57 | } 58 | return ListView.builder( 59 | padding: EdgeInsets.all(8), 60 | controller: this._scrollController, 61 | itemCount: productsManager.products.length + 1, 62 | itemBuilder: (context, index) { 63 | if (index == productsManager.products.length && 64 | productsManager.allProductPagesFetched == false) { 65 | if (!this._isLoading) return SizedBox(height: 0); 66 | return Center(child: CircularProgressIndicator()); 67 | } 68 | if (index == productsManager.products.length && 69 | productsManager.allProductPagesFetched == true) { 70 | return SizedBox(height: 0); 71 | } 72 | return ProductListTile( 73 | productsManager.products.values.elementAt(index), 74 | ); 75 | }, 76 | ); 77 | }, 78 | ); 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /lib/pages/HomeSpecialProductsPage.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:grodudes/components/ProductListTile.dart'; 3 | import 'package:grodudes/models/Product.dart'; 4 | import 'package:grodudes/state/products_state.dart'; 5 | import 'package:provider/provider.dart'; 6 | 7 | class HomeSpecialProductsPage extends StatefulWidget { 8 | final String title; 9 | final Set productIds; 10 | final Future Function() cb; 11 | HomeSpecialProductsPage(this.title, this.productIds, this.cb); 12 | @override 13 | _HomeSpecialProductsPageState createState() => 14 | _HomeSpecialProductsPageState(); 15 | } 16 | 17 | class _HomeSpecialProductsPageState extends State { 18 | bool _isLoading; 19 | bool requestedOnce; 20 | @override 21 | void initState() { 22 | this._isLoading = false; 23 | this.requestedOnce = false; 24 | super.initState(); 25 | } 26 | 27 | Future _getData() async { 28 | setState(() { 29 | this._isLoading = true; 30 | }); 31 | await widget.cb().catchError((err) { 32 | print('loading error: ' + err.toString()); 33 | }); 34 | setState(() { 35 | this.requestedOnce = true; 36 | this._isLoading = false; 37 | }); 38 | } 39 | 40 | @override 41 | Widget build(BuildContext context) { 42 | if ((widget.productIds == null || this.widget.productIds.length == 0) && 43 | !this.requestedOnce) { 44 | _getData(); 45 | } 46 | Map products = 47 | Provider.of(context, listen: false).products; 48 | return Scaffold( 49 | appBar: AppBar(title: Text(widget.title)), 50 | body: _isLoading 51 | ? Container( 52 | color: Colors.white, 53 | child: Center( 54 | child: CircularProgressIndicator(), 55 | ), 56 | ) 57 | : this.widget.productIds.length == 0 && this.requestedOnce 58 | ? Center( 59 | child: Column( 60 | mainAxisAlignment: MainAxisAlignment.center, 61 | children: [ 62 | Text('Failed to Load Products'), 63 | RaisedButton( 64 | onPressed: _getData, 65 | child: Text( 66 | 'Retry', 67 | style: TextStyle(color: Colors.white), 68 | ), 69 | ) 70 | ], 71 | ), 72 | ) 73 | : ListView.builder( 74 | padding: EdgeInsets.all(8), 75 | itemCount: widget.productIds.length, 76 | itemBuilder: (context, index) => ProductListTile( 77 | products[widget.productIds.elementAt(index)]), 78 | ), 79 | ); 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /android/app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 8 | 9 | 13 | 21 | 25 | 29 | 34 | 38 | 39 | 40 | 41 | 42 | 43 | 45 | 48 | 49 | 50 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Grodudes App 2 | A grocery app made with flutter and wordpress/woocommerce backend 3 | 4 | ## Screenshots 5 | 6 | 7 |

Home Page

8 | 9 | 10 | 11 | 12 | 13 | 14 |
15 | 16 | 17 |

Product Pages

18 | 19 | 20 | 21 | 22 | 23 | 24 |
25 | 26 | 27 |

Category Pages

28 | 29 | 30 | 31 | 32 |
33 | 34 | 35 |

Account Pages

36 | 37 | 38 | 39 | 40 | 41 |
42 | 43 | 44 |

Cart and Search Page

45 | 46 | 47 | 48 | 49 |
50 | 51 | 52 |

Customer Order Pages

53 | 54 | 55 | 56 | 57 | 58 |
59 | -------------------------------------------------------------------------------- /lib/pages/CartItems.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/cupertino.dart'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:grodudes/components/CartListItem.dart'; 4 | import 'package:grodudes/models/Product.dart'; 5 | import 'package:grodudes/pages/checkout/ConfirmationPage.dart'; 6 | import 'package:grodudes/state/cart_state.dart'; 7 | import 'package:provider/provider.dart'; 8 | 9 | class CartItems extends StatefulWidget { 10 | @override 11 | _CartItemsState createState() => _CartItemsState(); 12 | } 13 | 14 | class _CartItemsState extends State 15 | with AutomaticKeepAliveClientMixin { 16 | @override 17 | bool get wantKeepAlive => true; 18 | 19 | String _calculateCartTotal(List items) { 20 | double total = 0; 21 | items.forEach( 22 | (item) => total += double.parse(item.data['price']) * item.quantity); 23 | return total.toStringAsFixed(2); 24 | } 25 | 26 | @override 27 | Widget build(BuildContext context) { 28 | super.build(context); 29 | return Scaffold( 30 | appBar: AppBar(title: Text('Your Cart')), 31 | body: Consumer( 32 | builder: (context, value, child) => ListView.builder( 33 | itemCount: value.cartItems.length, 34 | itemBuilder: (context, index) => CartListItem(value.cartItems[index]), 35 | ), 36 | ), 37 | bottomNavigationBar: Consumer( 38 | builder: (context, cart, child) => Container( 39 | height: 60, 40 | decoration: BoxDecoration( 41 | color: Colors.white, 42 | boxShadow: [ 43 | BoxShadow( 44 | blurRadius: 2, 45 | spreadRadius: 2, 46 | color: Color(0xffcccccc), 47 | ) 48 | ], 49 | ), 50 | child: Row( 51 | children: [ 52 | SizedBox(width: 8), 53 | Column( 54 | crossAxisAlignment: CrossAxisAlignment.start, 55 | mainAxisAlignment: MainAxisAlignment.center, 56 | children: [ 57 | Text( 58 | 'Total ₹${_calculateCartTotal(cart.cartItems)}', 59 | style: TextStyle( 60 | color: Colors.green[700], 61 | fontSize: 18, 62 | fontWeight: FontWeight.w900, 63 | ), 64 | ), 65 | Text( 66 | '${cart.cartItems.length} ${cart.cartItems.length == 1 ? 'item' : 'items'}', 67 | style: TextStyle(fontSize: 15), 68 | ), 69 | ], 70 | ), 71 | Expanded(child: Container()), 72 | RaisedButton( 73 | onPressed: () => Navigator.push( 74 | context, 75 | CupertinoPageRoute(builder: (context) => ConfirmationPage()), 76 | ), 77 | child: Text('Checkout', style: TextStyle(color: Colors.white)), 78 | ), 79 | SizedBox(width: 8), 80 | ], 81 | ), 82 | ), 83 | ), 84 | ); 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /lib/components/ProductCard.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/cupertino.dart'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:grodudes/components/QuantityToggle.dart'; 4 | import 'package:grodudes/components/StyledProductPrice.dart'; 5 | import 'package:grodudes/helper/ImageFetcher.dart'; 6 | import 'package:grodudes/models/Product.dart'; 7 | import 'package:grodudes/pages/ProductDetailsPage.dart'; 8 | 9 | class ProductCard extends StatelessWidget { 10 | final Product item; 11 | ProductCard(this.item); 12 | 13 | @override 14 | Widget build(BuildContext context) { 15 | if (this.item == null || this.item.data['id'] == null) { 16 | return SizedBox(height: 0); 17 | } 18 | return GestureDetector( 19 | onTap: () { 20 | WidgetsBinding.instance.focusManager.primaryFocus?.unfocus(); 21 | Navigator.push( 22 | context, 23 | CupertinoPageRoute( 24 | builder: (context) => ProductDetailsPage(this.item)), 25 | ); 26 | }, 27 | child: Container( 28 | margin: EdgeInsets.symmetric(horizontal: 8, vertical: 6), 29 | padding: EdgeInsets.all(8), 30 | decoration: BoxDecoration( 31 | color: Colors.white, 32 | boxShadow: [ 33 | BoxShadow(color: Color(0xffe5e5e5), blurRadius: 3, spreadRadius: 1) 34 | ], 35 | borderRadius: BorderRadius.circular(16), 36 | ), 37 | child: Stack( 38 | children: [ 39 | Column( 40 | crossAxisAlignment: CrossAxisAlignment.start, 41 | children: [ 42 | Expanded( 43 | child: ImageFetcher.getImage(item.data['images'][0]['src']), 44 | ), 45 | Text( 46 | item.data['name'], 47 | maxLines: 2, 48 | overflow: TextOverflow.ellipsis, 49 | style: TextStyle(fontSize: 14, fontWeight: FontWeight.bold), 50 | ), 51 | SizedBox(height: 2), 52 | item.data['purchasable'] != null && item.data['purchasable'] 53 | ? item.data['in_stock'] 54 | ? StyledProductPrice(this.item.data['price'], 55 | this.item.data['regular_price']) 56 | : Text( 57 | 'Out of Stock', 58 | style: TextStyle(color: Colors.red[600]), 59 | ) 60 | : SizedBox(height: 0), 61 | SizedBox(height: 4) 62 | ], 63 | ), 64 | item.data['purchasable'] != null && 65 | item.data['purchasable'] && 66 | item.data['in_stock'] 67 | ? Align( 68 | alignment: Alignment.topRight, 69 | child: QuantityToggle( 70 | this.item, 71 | margin: EdgeInsets.all(0), 72 | iconSize: 24, 73 | ), 74 | ) 75 | : SizedBox(height: 0) 76 | ], 77 | ), 78 | ), 79 | ); 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /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/state/cart_state.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | 3 | import 'package:flutter/cupertino.dart'; 4 | import 'package:grodudes/models/Product.dart'; 5 | import 'package:shared_preferences/shared_preferences.dart'; 6 | 7 | const String localCartStorageKey = 'grodudes_cart_data'; 8 | 9 | class CartManager with ChangeNotifier { 10 | List cartItems; 11 | Future _prefs; 12 | CartManager() { 13 | cartItems = []; 14 | this._prefs = SharedPreferences.getInstance(); 15 | } 16 | 17 | setCartItemsFromLocalData(List products) { 18 | if (this.cartItems.length > 0) return; 19 | products.forEach((item) { 20 | if (!isPresentInCart(item)) { 21 | this.cartItems.add(item); 22 | } 23 | }); 24 | } 25 | 26 | addCartItem(Product item, {int quantity = 1}) { 27 | if (isPresentInCart(item) == true) return; 28 | try { 29 | bool inStock = item.data['in_stock']; 30 | bool isPurchasable = item.data['purchasable']; 31 | if (inStock == null || isPurchasable == null) return; 32 | if (!inStock || !isPurchasable) return; 33 | double price = double.parse(item.data['price']); 34 | if (price <= 0) return; 35 | } catch (err) { 36 | print(err); 37 | return; 38 | } 39 | item.quantity = quantity; 40 | this.cartItems.add(item); 41 | notifyListeners(); 42 | _storeCartLocally(); 43 | } 44 | 45 | removeCartItem(Product item) { 46 | this.cartItems.remove(item); 47 | notifyListeners(); 48 | _storeCartLocally(); 49 | } 50 | 51 | clearCart() { 52 | cartItems.clear(); 53 | notifyListeners(); 54 | _storeCartLocally(); 55 | } 56 | 57 | isPresentInCart(Product item) { 58 | for (final cartItem in cartItems) { 59 | if (cartItem.isSameAs(item)) return true; 60 | } 61 | return false; 62 | } 63 | 64 | incrementQuantityOfProduct(Product item) { 65 | for (final cartItem in cartItems) { 66 | if (cartItem.isSameAs(item)) { 67 | cartItem.quantity++; 68 | notifyListeners(); 69 | _storeCartLocally(); 70 | return; 71 | } 72 | } 73 | } 74 | 75 | decrementQuantityOfProduct(Product item) { 76 | for (final cartItem in cartItems) { 77 | if (cartItem.isSameAs(item)) { 78 | if (cartItem.quantity == 1) { 79 | removeCartItem(item); 80 | return; 81 | } 82 | cartItem.quantity--; 83 | notifyListeners(); 84 | _storeCartLocally(); 85 | return; 86 | } 87 | } 88 | } 89 | 90 | Future _storeCartLocally() async { 91 | try { 92 | final SharedPreferences prefs = await _prefs; 93 | await prefs.setString(localCartStorageKey, _getCartDataAsString()); 94 | } catch (err) { 95 | print(err); 96 | } 97 | } 98 | 99 | String _getCartDataAsString() { 100 | List productsInCart = []; 101 | this.cartItems.forEach( 102 | (item) => productsInCart.add({ 103 | 'id': item.data['id'], 104 | 'quantity': item.quantity, 105 | }), 106 | ); 107 | return json.encode(productsInCart); 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /lib/helper/WooCommerceAPI.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | import 'dart:io'; 3 | 4 | import 'package:flutter/material.dart'; 5 | import 'package:http/http.dart' as http; 6 | 7 | class WooCommerceAPI { 8 | String url; 9 | String consumerKey; 10 | String consumerSecret; 11 | 12 | WooCommerceAPI( 13 | {@required this.url, 14 | @required this.consumerKey, 15 | @required this.consumerSecret}); 16 | 17 | String _getUrl(String endpoint, {String apiVersion = 'v2'}) { 18 | String type = endpoint.contains('users') ? 'wp' : 'wc'; 19 | String wcApiBase = this.url + '/wp-json/$type/$apiVersion/' + endpoint; 20 | if (endpoint.contains('?')) { 21 | return wcApiBase + 22 | '&consumer_key=${this.consumerKey}&consumer_secret=${this.consumerSecret}'; 23 | } else { 24 | return wcApiBase + 25 | '?consumer_key=${this.consumerKey}&consumer_secret=${this.consumerSecret}'; 26 | } 27 | } 28 | 29 | Future getAsync(String endPoint, {String apiVersion = 'v2'}) async { 30 | String reqUrl = this._getUrl(endPoint, apiVersion: apiVersion); 31 | 32 | try { 33 | final http.Response response = await http.get(reqUrl); 34 | if (response.statusCode == 200) { 35 | return json.decode(response.body); 36 | } 37 | } on SocketException { 38 | throw Exception('No Internet connection.'); 39 | } 40 | } 41 | 42 | Future postAsync(String endPoint, Map data, 43 | {Map userHeaders, String apiVersion: 'v2'}) async { 44 | String reqUrl = this._getUrl(endPoint, apiVersion: apiVersion); 45 | Map headers = { 46 | HttpHeaders.contentTypeHeader: 'application/json' 47 | }; 48 | if (userHeaders != null) headers = {...headers, ...userHeaders}; 49 | var response = await http.post( 50 | reqUrl, 51 | headers: headers, 52 | body: json.encode(data), 53 | encoding: Encoding.getByName('utf-8'), 54 | ); 55 | 56 | var dataResponse = await json.decode(response.body); 57 | return dataResponse; 58 | } 59 | 60 | Future putAsync(String endPoint, Map data, 61 | {Map userHeaders, String apiVersion: 'v2'}) async { 62 | String reqUrl = this._getUrl(endPoint, apiVersion: apiVersion); 63 | Map headers = { 64 | HttpHeaders.contentTypeHeader: 'application/json' 65 | }; 66 | if (userHeaders != null) headers = {...headers, ...userHeaders}; 67 | var response = await http.put( 68 | reqUrl, 69 | headers: headers, 70 | body: json.encode(data), 71 | encoding: Encoding.getByName('utf-8'), 72 | ); 73 | 74 | var dataResponse = await json.decode(response.body); 75 | return dataResponse; 76 | } 77 | 78 | Future getAuthToken(String username, String password) async { 79 | final body = { 80 | 'username': username, 81 | 'password': password, 82 | }; 83 | final response = await http.post( 84 | '${this.url}/wp-json/jwt-auth/v1/token', 85 | body: body, 86 | ); 87 | return json.decode(response.body); 88 | } 89 | 90 | Future getLoggedInUserId(String token) async { 91 | final response = await http.post( 92 | '${this.url}/wp-json/wp/v2/users/me', 93 | headers: {HttpHeaders.authorizationHeader: 'Bearer $token'}, 94 | ); 95 | return json.decode(response.body)['id']; 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /lib/components/ProductListTile.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/cupertino.dart'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:grodudes/components/QuantityToggle.dart'; 4 | import 'package:grodudes/components/StyledProductPrice.dart'; 5 | import 'package:grodudes/helper/ImageFetcher.dart'; 6 | import 'package:grodudes/models/Product.dart'; 7 | import 'package:grodudes/pages/ProductDetailsPage.dart'; 8 | 9 | class ProductListTile extends StatelessWidget { 10 | final Product item; 11 | ProductListTile(this.item); 12 | 13 | @override 14 | Widget build(BuildContext context) { 15 | if (this.item == null || this.item.data['id'] == null) { 16 | return SizedBox(height: 0); 17 | } 18 | return GestureDetector( 19 | onTap: () { 20 | WidgetsBinding.instance.focusManager.primaryFocus?.unfocus(); 21 | Navigator.push( 22 | context, 23 | CupertinoPageRoute( 24 | builder: (context) => ProductDetailsPage(item), 25 | ), 26 | ); 27 | }, 28 | child: Container( 29 | height: 100, 30 | padding: EdgeInsets.symmetric(vertical: 8), 31 | color: Colors.white, 32 | child: Row( 33 | crossAxisAlignment: CrossAxisAlignment.center, 34 | children: [ 35 | SizedBox( 36 | height: 100, 37 | width: 100, 38 | child: ImageFetcher.getImage(item.data['images'][0]['src']), 39 | ), 40 | Expanded( 41 | child: Column( 42 | crossAxisAlignment: CrossAxisAlignment.start, 43 | mainAxisAlignment: MainAxisAlignment.center, 44 | children: [ 45 | Text( 46 | item.data['name'], 47 | style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold), 48 | overflow: TextOverflow.ellipsis, 49 | ), 50 | item.data['purchasable'] != null && item.data['purchasable'] 51 | ? Padding( 52 | padding: const EdgeInsets.symmetric(vertical: 2), 53 | child: item.data['in_stock'] == true 54 | ? StyledProductPrice( 55 | this.item.data['price'], 56 | this.item.data['regular_price'], 57 | ) 58 | : Text( 59 | 'Out of Stock', 60 | style: TextStyle(color: Colors.red[600]), 61 | ), 62 | ) 63 | : Padding( 64 | padding: const EdgeInsets.only(top: 4), 65 | child: Text( 66 | 'Currently Not Purchasable', 67 | textAlign: TextAlign.center, 68 | style: TextStyle(color: Colors.red[600]), 69 | ), 70 | ), 71 | ], 72 | ), 73 | ), 74 | item.data['purchasable'] != null && item.data['purchasable'] 75 | ? item.data['in_stock'] == true 76 | ? QuantityToggle(item) 77 | : SizedBox(width: 25) 78 | : SizedBox(width: 26), 79 | ], 80 | ), 81 | ), 82 | ); 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: grodudes 2 | description: A new Flutter project. 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 | # woocommerce_api: ^0.0.9 27 | provider: ^4.2.0 28 | flutter_html: ^1.0.0 29 | flutter_secure_storage: ^3.3.3 30 | shared_preferences: ^0.5.10 31 | flutter_svg: ^0.18.0 32 | cached_network_image: ^2.2.0+1 33 | carousel_slider: ^2.2.1 34 | url_launcher: ^5.5.0 35 | http: ^0.12.1 36 | 37 | 38 | # The following adds the Cupertino Icons font to your application. 39 | # Use with the CupertinoIcons class for iOS style icons. 40 | cupertino_icons: ^0.1.3 41 | 42 | dev_dependencies: 43 | flutter_test: 44 | sdk: flutter 45 | 46 | # For information on the generic Dart part of this file, see the 47 | # following page: https://dart.dev/tools/pub/pubspec 48 | 49 | # The following section is specific to Flutter. 50 | flutter: 51 | 52 | # The following line ensures that the Material Icons font is 53 | # included with your application, so that you can use the icons in 54 | # the material Icons class. 55 | uses-material-design: true 56 | 57 | # To add assets to your application, add an assets section, like this: 58 | assets: 59 | - assets/images/grodudes_logo.png 60 | - assets/images/grodudes_logo_svg.svg 61 | - assets/images/Banner1.jpg 62 | - assets/images/Banner2.jpg 63 | # - images/a_dot_ham.jpeg 64 | 65 | # An image asset can refer to one or more resolution-specific "variants", see 66 | # https://flutter.dev/assets-and-images/#resolution-aware. 67 | 68 | # For details regarding adding assets from package dependencies, see 69 | # https://flutter.dev/assets-and-images/#from-packages 70 | 71 | # To add custom fonts to your application, add a fonts section here, 72 | # in this "flutter" section. Each entry in this list should have a 73 | # "family" key with the font family name, and a "fonts" key with a 74 | # list giving the asset and other descriptors for the font. For 75 | # example: 76 | fonts: 77 | - family: 'Open Sans' 78 | fonts: 79 | - asset: assets/fonts/OpenSans-Regular.ttf 80 | weight: 400 81 | - asset: assets/fonts/OpenSans-Bold.ttf 82 | weight: 700 83 | - asset: assets/fonts/OpenSans-Light.ttf 84 | weight: 300 85 | - asset: assets/fonts/OpenSans-SemiBold.ttf 86 | weight: 600 87 | # - family: Trajan Pro 88 | # fonts: 89 | # - asset: fonts/TrajanPro.ttf 90 | # - asset: fonts/TrajanPro_Bold.ttf 91 | # weight: 700 92 | # 93 | # For details regarding fonts from package dependencies, 94 | # see https://flutter.dev/custom-fonts/#from-packages 95 | -------------------------------------------------------------------------------- /lib/pages/SearchResultsPage.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:grodudes/components/ProductListTile.dart'; 3 | import 'package:grodudes/helper/Constants.dart'; 4 | import 'package:grodudes/models/Product.dart'; 5 | import 'package:grodudes/state/products_state.dart'; 6 | import 'package:provider/provider.dart'; 7 | 8 | class SearchResultsPage extends StatefulWidget { 9 | final String searchQuery; 10 | SearchResultsPage(this.searchQuery); 11 | @override 12 | _SearchResultsPageState createState() => _SearchResultsPageState(); 13 | } 14 | 15 | class _SearchResultsPageState extends State { 16 | TextEditingController _searchController; 17 | 18 | @override 19 | void initState() { 20 | this._searchController = TextEditingController(text: widget.searchQuery); 21 | super.initState(); 22 | } 23 | 24 | void loadSearch() { 25 | setState(() { 26 | WidgetsBinding.instance.focusManager.primaryFocus?.unfocus(); 27 | }); 28 | } 29 | 30 | @override 31 | Widget build(BuildContext context) { 32 | return Scaffold( 33 | appBar: AppBar( 34 | title: Container( 35 | decoration: BoxDecoration( 36 | color: Colors.white, 37 | borderRadius: BorderRadius.circular(16), 38 | border: Border.all( 39 | color: GrodudesPrimaryColor.primaryColor[400], width: 2), 40 | ), 41 | child: Row( 42 | children: [ 43 | Expanded( 44 | child: TextField( 45 | controller: this._searchController, 46 | style: TextStyle(fontSize: 18), 47 | decoration: InputDecoration( 48 | hintText: 'Search Grodudes...', 49 | hintStyle: TextStyle( 50 | fontSize: 16, 51 | fontWeight: FontWeight.w500, 52 | color: Colors.black38), 53 | isDense: true, 54 | contentPadding: 55 | EdgeInsets.symmetric(horizontal: 10, vertical: 8), 56 | border: InputBorder.none, 57 | ), 58 | cursorColor: GrodudesPrimaryColor.primaryColor[600], 59 | onSubmitted: (value) => loadSearch(), 60 | ), 61 | ), 62 | GestureDetector( 63 | onTap: loadSearch, 64 | child: Icon(Icons.search, 65 | size: 24, color: GrodudesPrimaryColor.primaryColor[700]), 66 | ), 67 | SizedBox(width: 6), 68 | ], 69 | ), 70 | ), 71 | ), 72 | body: FutureBuilder( 73 | future: Provider.of(context, listen: false) 74 | .searchProducts(this._searchController.text), 75 | builder: (context, snapshot) { 76 | if (snapshot.connectionState != ConnectionState.done) 77 | return Center(child: CircularProgressIndicator()); 78 | if (snapshot.hasError) { 79 | return Center( 80 | child: Container( 81 | padding: EdgeInsets.all(16), 82 | child: Text( 83 | 'An Error Occured. Please Try Again', 84 | textAlign: TextAlign.center, 85 | style: TextStyle(fontSize: 18), 86 | ), 87 | ), 88 | ); 89 | } 90 | if (snapshot.hasData) { 91 | List items = snapshot.data; 92 | return ListView.builder( 93 | padding: EdgeInsets.all(8), 94 | itemCount: items.length, 95 | itemBuilder: (context, index) => ProductListTile(items[index]), 96 | ); 97 | } 98 | return Center(child: CircularProgressIndicator()); 99 | }, 100 | ), 101 | ); 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /lib/components/CartListItem.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:grodudes/components/StyledProductPrice.dart'; 3 | import 'package:grodudes/helper/Constants.dart'; 4 | import 'package:grodudes/helper/ImageFetcher.dart'; 5 | import 'package:grodudes/models/Product.dart'; 6 | import 'package:grodudes/state/cart_state.dart'; 7 | import 'package:provider/provider.dart'; 8 | 9 | class CartListItem extends StatelessWidget { 10 | final Product item; 11 | CartListItem(this.item); 12 | 13 | String getTotalPrice() { 14 | return (double.parse(item.data['price']) * item.quantity) 15 | .toStringAsFixed(2); 16 | } 17 | 18 | @override 19 | Widget build(BuildContext context) { 20 | if (this.item == null || this.item.data['id'] == null) { 21 | return SizedBox(height: 0); 22 | } 23 | return Column( 24 | children: [ 25 | ListTile( 26 | onTap: () => 27 | WidgetsBinding.instance.focusManager.primaryFocus?.unfocus(), 28 | leading: ImageFetcher.getImage(this.item.data['images'][0]['src']), 29 | title: Text( 30 | item.data['name'], 31 | style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold), 32 | ), 33 | subtitle: Padding( 34 | padding: const EdgeInsets.symmetric(vertical: 2), 35 | child: StyledProductPrice( 36 | this.item.data['price'], 37 | this.item.data['regular_price'], 38 | ), 39 | ), 40 | trailing: Container( 41 | decoration: BoxDecoration( 42 | color: GrodudesPrimaryColor.primaryColor[700], 43 | borderRadius: BorderRadius.circular(14)), 44 | padding: EdgeInsets.all(2), 45 | child: Row( 46 | mainAxisSize: MainAxisSize.min, 47 | children: [ 48 | GestureDetector( 49 | child: 50 | Icon(Icons.remove_circle, size: 26, color: Colors.white), 51 | onTap: () { 52 | Provider.of(context, listen: false) 53 | .decrementQuantityOfProduct(item); 54 | }, 55 | ), 56 | SizedBox( 57 | width: 20, 58 | child: Text( 59 | '${this.item.quantity}', 60 | style: TextStyle( 61 | color: Colors.white, 62 | fontWeight: FontWeight.bold, 63 | ), 64 | textAlign: TextAlign.center, 65 | ), 66 | ), 67 | GestureDetector( 68 | child: Icon(Icons.add_circle, size: 26, color: Colors.white), 69 | onTap: () { 70 | Provider.of(context, listen: false) 71 | .incrementQuantityOfProduct(item); 72 | }, 73 | ), 74 | ], 75 | ), 76 | ), 77 | ), 78 | Row( 79 | children: [ 80 | SizedBox(width: 8), 81 | Expanded( 82 | child: Text( 83 | '₹ ${getTotalPrice()}', 84 | textAlign: TextAlign.center, 85 | style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold), 86 | ), 87 | ), 88 | Expanded( 89 | child: RaisedButton( 90 | color: Colors.white, 91 | elevation: 0, 92 | child: Text('Remove', style: TextStyle(color: Colors.red[700])), 93 | shape: RoundedRectangleBorder( 94 | side: BorderSide(color: Color(0xffdddddd), width: 2), 95 | borderRadius: BorderRadius.circular(6), 96 | ), 97 | onPressed: () => 98 | Provider.of(context, listen: false) 99 | .removeCartItem(item), 100 | ), 101 | ), 102 | SizedBox(width: 8), 103 | ], 104 | ) 105 | ], 106 | ); 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /lib/components/CategoryCard.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/cupertino.dart'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:grodudes/components/ProductsList.dart'; 4 | import 'package:grodudes/helper/ImageFetcher.dart'; 5 | import 'package:grodudes/models/Category.dart'; 6 | import 'package:grodudes/state/products_state.dart'; 7 | import 'package:provider/provider.dart'; 8 | 9 | class CategoryCard extends StatelessWidget { 10 | final Category category; 11 | CategoryCard(this.category); 12 | 13 | String _formatName(String name) { 14 | return name.replaceAll('&', '&'); 15 | } 16 | 17 | @override 18 | Widget build(BuildContext context) { 19 | if (this.category == null || this.category.data['id'] == null) { 20 | return SizedBox(width: 0, height: 0); 21 | } 22 | return GestureDetector( 23 | onTap: () { 24 | Navigator.push( 25 | context, 26 | CupertinoPageRoute( 27 | builder: (context) { 28 | return CategoryContents(category: category); 29 | }, 30 | ), 31 | ); 32 | }, 33 | child: Container( 34 | // margin: EdgeInsets.only(top: 16, bottom: 6, left: 8, right: 8), 35 | decoration: BoxDecoration( 36 | color: Colors.white, 37 | boxShadow: [ 38 | BoxShadow(color: Color(0xffd5d5d5), blurRadius: 3, spreadRadius: 1) 39 | ], 40 | borderRadius: BorderRadius.circular(8), 41 | ), 42 | child: Column( 43 | children: [ 44 | Expanded( 45 | child: category.data['image'] != null 46 | ? ImageFetcher.getImage(category.data['image']['src']) 47 | : Icon(Icons.image, size: 70), 48 | ), 49 | Padding( 50 | padding: const EdgeInsets.all(4), 51 | child: Text( 52 | _formatName(category.data['name']), 53 | overflow: TextOverflow.ellipsis, 54 | style: TextStyle( 55 | fontWeight: FontWeight.bold, 56 | fontSize: 15, 57 | ), 58 | ), 59 | ), 60 | ], 61 | ), 62 | ), 63 | ); 64 | } 65 | } 66 | 67 | class CategoryContents extends StatefulWidget { 68 | const CategoryContents({ 69 | Key key, 70 | @required this.category, 71 | }) : super(key: key); 72 | 73 | final Category category; 74 | 75 | @override 76 | _CategoryContentsState createState() => _CategoryContentsState(); 77 | } 78 | 79 | class _CategoryContentsState extends State { 80 | String _formatName(String name) { 81 | return name.replaceAll('&', '&'); 82 | } 83 | 84 | @override 85 | Widget build(BuildContext context) { 86 | return Scaffold( 87 | appBar: AppBar( 88 | title: Text(_formatName(this.widget.category.data['name'])), 89 | elevation: 0, 90 | ), 91 | body: FutureBuilder( 92 | future: Provider.of(context) 93 | .fetchCategoryDetails(widget.category), 94 | builder: (context, snapshot) { 95 | if (snapshot.hasError) { 96 | return Center( 97 | child: Column( 98 | mainAxisAlignment: MainAxisAlignment.center, 99 | crossAxisAlignment: CrossAxisAlignment.center, 100 | children: [ 101 | Icon(Icons.error, color: Colors.red[600]), 102 | Text( 103 | 'An error occured while loading', 104 | style: TextStyle(fontSize: 16), 105 | ), 106 | RaisedButton( 107 | child: Text('Retry', style: TextStyle(color: Colors.white)), 108 | onPressed: () => setState(() {}), 109 | ) 110 | ], 111 | ), 112 | ); 113 | } 114 | if (snapshot.hasData) { 115 | if (snapshot.data['products'] != null) { 116 | return ProductsList(snapshot.data['products']); 117 | } 118 | return GridView.builder( 119 | gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( 120 | crossAxisCount: 2, 121 | childAspectRatio: 1, 122 | crossAxisSpacing: 20, 123 | mainAxisSpacing: 20, 124 | ), 125 | padding: EdgeInsets.all(16), 126 | itemCount: snapshot.data['categories'].length, 127 | itemBuilder: (context, index) => 128 | CategoryCard(snapshot.data['categories'][index]), 129 | ); 130 | } 131 | return Center(child: CircularProgressIndicator()); 132 | }, 133 | ), 134 | ); 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /lib/Root.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/cupertino.dart'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:grodudes/components/RootDrawer.dart'; 4 | import 'package:grodudes/helper/Constants.dart'; 5 | import 'package:grodudes/pages/AllCategoriesPage.dart'; 6 | import 'package:grodudes/pages/AllProductsPage.dart'; 7 | import 'package:grodudes/pages/CartItems.dart'; 8 | import 'package:grodudes/pages/Home.dart'; 9 | import 'package:grodudes/pages/SearchResultsPage.dart'; 10 | import 'package:grodudes/pages/account/AccountRoot.dart'; 11 | 12 | class Root extends StatefulWidget { 13 | @override 14 | _RootState createState() => _RootState(); 15 | } 16 | 17 | class _RootState extends State with SingleTickerProviderStateMixin { 18 | var currentPage; 19 | TabController _tabController; 20 | TextEditingController _searchController; 21 | 22 | @override 23 | initState() { 24 | this._tabController = 25 | TabController(initialIndex: 0, length: 3, vsync: this); 26 | this._searchController = TextEditingController(); 27 | super.initState(); 28 | } 29 | 30 | @override 31 | void dispose() { 32 | this._tabController.dispose(); 33 | this._searchController.dispose(); 34 | super.dispose(); 35 | } 36 | 37 | openSearchPage(String searchQuery) { 38 | WidgetsBinding.instance.focusManager.primaryFocus?.unfocus(); 39 | this._searchController.clear(); 40 | Navigator.push( 41 | context, 42 | CupertinoPageRoute( 43 | builder: (context) => SearchResultsPage(searchQuery), 44 | ), 45 | ); 46 | } 47 | 48 | @override 49 | Widget build(BuildContext context) { 50 | return Scaffold( 51 | backgroundColor: Colors.white, 52 | appBar: AppBar( 53 | titleSpacing: 2, 54 | iconTheme: IconThemeData(color: Colors.black87, size: 36), 55 | actionsIconTheme: 56 | IconThemeData(color: GrodudesPrimaryColor.primaryColor[700]), 57 | title: Container( 58 | decoration: BoxDecoration( 59 | color: Colors.white, 60 | boxShadow: [BoxShadow(color: Colors.black38, blurRadius: 3)], 61 | borderRadius: BorderRadius.circular(16), 62 | ), 63 | child: Row( 64 | children: [ 65 | Expanded( 66 | child: TextField( 67 | controller: this._searchController, 68 | style: TextStyle(fontSize: 18), 69 | decoration: InputDecoration( 70 | hintText: 'Search Grodudes...', 71 | hintStyle: TextStyle( 72 | fontSize: 16, 73 | fontWeight: FontWeight.w500, 74 | color: Colors.black38), 75 | isDense: true, 76 | contentPadding: 77 | EdgeInsets.symmetric(horizontal: 10, vertical: 7), 78 | border: InputBorder.none, 79 | ), 80 | onSubmitted: (value) => openSearchPage(value), 81 | ), 82 | ), 83 | GestureDetector( 84 | onTap: () => openSearchPage(this._searchController.text), 85 | child: Icon( 86 | Icons.search, 87 | size: 24, 88 | color: GrodudesPrimaryColor.primaryColor[700], 89 | ), 90 | ), 91 | SizedBox(width: 6), 92 | ], 93 | ), 94 | ), 95 | backgroundColor: Colors.white, 96 | actions: [ 97 | IconButton( 98 | icon: Icon(Icons.shopping_cart), 99 | constraints: BoxConstraints(maxHeight: 38, maxWidth: 38), 100 | onPressed: () => Navigator.push( 101 | context, 102 | CupertinoPageRoute(builder: (context) => CartItems()), 103 | ), 104 | ), 105 | IconButton( 106 | icon: Icon(Icons.person), 107 | constraints: BoxConstraints(maxHeight: 38, maxWidth: 38), 108 | onPressed: () => Navigator.push( 109 | context, 110 | CupertinoPageRoute(builder: (context) => AccountRoot()), 111 | ), 112 | ), 113 | ], 114 | ), 115 | drawer: RootDrawer(), 116 | body: Column( 117 | children: [ 118 | Align( 119 | alignment: Alignment.centerLeft, 120 | child: TabBar( 121 | controller: this._tabController, 122 | // isScrollable: true, 123 | indicatorColor: GrodudesPrimaryColor.primaryColor[600], 124 | indicatorWeight: 3, 125 | indicatorSize: TabBarIndicatorSize.tab, 126 | labelColor: GrodudesPrimaryColor.primaryColor[600], 127 | labelStyle: TextStyle(fontSize: 15, fontWeight: FontWeight.bold), 128 | unselectedLabelColor: Colors.black87, 129 | tabs: [ 130 | Tab(child: Text('Home')), 131 | Tab(child: Text('Products')), 132 | Tab(child: Text('Categories')), 133 | ], 134 | ), 135 | ), 136 | Expanded( 137 | child: TabBarView( 138 | controller: this._tabController, 139 | children: [ 140 | Home(() => this._tabController.animateTo(2)), 141 | AllProductsPage(), 142 | AllCategoriesPage(), 143 | ], 144 | ), 145 | ) 146 | ], 147 | ), 148 | ); 149 | } 150 | } 151 | -------------------------------------------------------------------------------- /assets/images/grodudes_logo_svg.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 7 | 8 | 10 | 38 | 53 | 55 | 56 | 58 | 59 | 61 | 63 | 65 | 66 | 68 | 69 | 71 | 73 | 75 | 77 | 79 | 81 | 83 | 85 | 87 | 89 | 91 | 93 | 95 | 97 | 98 | 99 | -------------------------------------------------------------------------------- /lib/pages/checkout/OrderPlacementPage.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:grodudes/helper/WooCommerceAPI.dart'; 3 | import 'package:grodudes/models/Product.dart'; 4 | import 'package:grodudes/state/cart_state.dart'; 5 | import 'package:provider/provider.dart'; 6 | import '../../secrets.dart'; 7 | 8 | class OrderPlacementPage extends StatefulWidget { 9 | final int customerId; 10 | final Map billingAddress; 11 | final Map shippingAddress; 12 | final List items; 13 | OrderPlacementPage({ 14 | @required this.customerId, 15 | @required this.billingAddress, 16 | @required this.shippingAddress, 17 | @required this.items, 18 | }); 19 | 20 | @override 21 | _OrderPlacementPageState createState() => _OrderPlacementPageState(); 22 | } 23 | 24 | class _OrderPlacementPageState extends State { 25 | Map orderDetails; 26 | 27 | @override 28 | initState() { 29 | this.orderDetails = {}; 30 | super.initState(); 31 | } 32 | 33 | Future _placeOrder() async { 34 | if (this.orderDetails['id'] != null) return this.orderDetails; 35 | 36 | List lineItems = []; 37 | for (Product item in this.widget.items) { 38 | lineItems.add({ 39 | "product_id": item.data['id'], 40 | "quantity": item.quantity > 0 ? item.quantity : 1, 41 | }); 42 | } 43 | 44 | Map orderBody = { 45 | "payment_method": "Cod", 46 | "payment_method_title": "Cash on delivery", 47 | "customer_id": this.widget.customerId, 48 | "billing": this.widget.billingAddress, 49 | "shipping": this.widget.shippingAddress, 50 | "line_items": lineItems, 51 | }; 52 | 53 | WooCommerceAPI wooCommerceAPI = WooCommerceAPI( 54 | url: 'https://www.grodudes.com', 55 | consumerKey: secret['consumerKey'], 56 | consumerSecret: secret['consumerSecret'], 57 | ); 58 | 59 | this.orderDetails = await wooCommerceAPI.postAsync('orders', orderBody); 60 | return this.orderDetails; 61 | } 62 | 63 | @override 64 | Widget build(BuildContext context) { 65 | if (widget.customerId == null || 66 | widget.billingAddress == null || 67 | widget.shippingAddress == null) { 68 | return Scaffold( 69 | appBar: AppBar(title: Text('Order Placement')), 70 | body: Center( 71 | child: Text( 72 | 'Sorry there was an error', 73 | style: TextStyle(fontSize: 18), 74 | ), 75 | ), 76 | ); 77 | } 78 | return Scaffold( 79 | appBar: AppBar(title: Text('Order Placement')), 80 | body: FutureBuilder( 81 | future: _placeOrder(), 82 | builder: (context, snapshot) { 83 | if (snapshot.connectionState != ConnectionState.done) 84 | return Center(child: CircularProgressIndicator()); 85 | if (snapshot.hasError) { 86 | return Center( 87 | child: Column( 88 | mainAxisAlignment: MainAxisAlignment.center, 89 | children: [ 90 | Text('Failed to place order - Connection problem'), 91 | MaterialButton( 92 | child: Text('Retry'), 93 | onPressed: () => setState(() {}), 94 | ) 95 | ], 96 | ), 97 | ); 98 | } 99 | if (snapshot.hasData) { 100 | if (snapshot.data['id'] == null) { 101 | return Center( 102 | child: Column( 103 | mainAxisAlignment: MainAxisAlignment.center, 104 | children: [ 105 | Text( 106 | 'Failed to place order. Something went wrong on our side', 107 | ), 108 | MaterialButton( 109 | child: Text('Retry'), 110 | onPressed: () => setState(() {}), 111 | ) 112 | ], 113 | ), 114 | ); 115 | } else { 116 | return Center( 117 | child: ListView( 118 | shrinkWrap: true, 119 | children: [ 120 | Row( 121 | mainAxisAlignment: MainAxisAlignment.center, 122 | children: [ 123 | Icon(Icons.check_circle, color: Colors.green[600]), 124 | Text( 125 | 'Thank You For Shopping with Us!', 126 | style: TextStyle(fontSize: 20), 127 | textAlign: TextAlign.center, 128 | ), 129 | ], 130 | ), 131 | SizedBox(height: 16), 132 | Text( 133 | 'Your Order ID is ${snapshot.data['id']}', 134 | textAlign: TextAlign.center, 135 | style: TextStyle( 136 | fontSize: 18, 137 | fontWeight: FontWeight.bold, 138 | ), 139 | ), 140 | SizedBox(height: 8), 141 | Text( 142 | 'Your Order is due to be paid through Cash on Delivery (CoD)', 143 | style: TextStyle(fontSize: 16), 144 | textAlign: TextAlign.center, 145 | ), 146 | SizedBox(height: 16), 147 | Center( 148 | child: RaisedButton( 149 | child: Text( 150 | "Finish", 151 | style: TextStyle(color: Colors.white), 152 | ), 153 | onPressed: () { 154 | Provider.of(context, listen: false) 155 | .clearCart(); 156 | Navigator.pop(context); 157 | }, 158 | ), 159 | ) 160 | ], 161 | ), 162 | ); 163 | } 164 | } 165 | return Center(child: CircularProgressIndicator()); 166 | }, 167 | ), 168 | ); 169 | } 170 | } 171 | -------------------------------------------------------------------------------- /lib/pages/account/AccountDetails.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/cupertino.dart'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:grodudes/helper/Constants.dart'; 4 | import 'package:grodudes/pages/account/AddressDetails.dart'; 5 | import 'package:grodudes/pages/account/Orders.dart'; 6 | import 'package:grodudes/state/user_state.dart'; 7 | import 'package:provider/provider.dart'; 8 | 9 | class AccountDetails extends StatelessWidget { 10 | // final Map wpUserInfo; 11 | // final Map wcUserInfo; 12 | final listTileTextStyle = TextStyle(fontSize: 16); 13 | 14 | // AccountDetails(this.wpUserInfo, this.wcUserInfo); 15 | 16 | Map _filterData(Map data) { 17 | Map filteredData = {}; 18 | for (final String key in data.keys) { 19 | if (key == 'email' && data[key] == null || data[key].length == 0) 20 | continue; 21 | filteredData[key] = data[key]; 22 | } 23 | return filteredData; 24 | } 25 | 26 | @override 27 | Widget build(BuildContext context) { 28 | final screenWidth = MediaQuery.of(context).size.width; 29 | final dataProvider = Provider.of(context, listen: false); 30 | if (dataProvider.wcUserInfo == null || 31 | dataProvider.wcUserInfo['id'] == null) 32 | return _LoginErrorDescriptionWidget(dataProvider.wpUserInfo); 33 | if (dataProvider.wpUserInfo == null || 34 | dataProvider.wpUserInfo['user_email'] == null) 35 | return _LoginErrorDescriptionWidget(dataProvider.wpUserInfo); 36 | 37 | return ListView( 38 | scrollDirection: Axis.vertical, 39 | children: [ 40 | SizedBox(height: 24), 41 | Text( 42 | 'Account Details', 43 | style: TextStyle( 44 | fontSize: 20, 45 | fontWeight: FontWeight.bold, 46 | ), 47 | textAlign: TextAlign.center, 48 | ), 49 | SizedBox(height: 12), 50 | Padding( 51 | padding: const EdgeInsets.all(16), 52 | child: Image.network( 53 | dataProvider.wcUserInfo['avatar_url'] ?? '', 54 | height: 80, 55 | width: 80, 56 | errorBuilder: (context, error, stackTrace) => 57 | Icon(Icons.person, size: 80), 58 | ), 59 | ), 60 | Text( 61 | dataProvider.wcUserInfo['username'], 62 | style: TextStyle( 63 | fontSize: 18, 64 | fontWeight: FontWeight.bold, 65 | ), 66 | textAlign: TextAlign.center, 67 | ), 68 | Text( 69 | dataProvider.wcUserInfo['email'], 70 | style: TextStyle( 71 | fontSize: 14, 72 | ), 73 | textAlign: TextAlign.center, 74 | ), 75 | ListTile( 76 | title: Text('Orders', style: listTileTextStyle), 77 | contentPadding: EdgeInsets.symmetric(horizontal: screenWidth * 0.1), 78 | trailing: Icon( 79 | Icons.chevron_right, 80 | size: 28, 81 | color: GrodudesPrimaryColor.primaryColor[500], 82 | ), 83 | onTap: () => Navigator.push( 84 | context, CupertinoPageRoute(builder: (context) => Orders())), 85 | ), 86 | ListTile( 87 | title: Text('Billing Address', style: listTileTextStyle), 88 | contentPadding: EdgeInsets.symmetric(horizontal: screenWidth * 0.1), 89 | trailing: Icon( 90 | Icons.chevron_right, 91 | size: 28, 92 | color: GrodudesPrimaryColor.primaryColor[500], 93 | ), 94 | onTap: () => Navigator.push( 95 | context, 96 | CupertinoPageRoute( 97 | builder: (context) => AddressDetails( 98 | title: 'Billing Address', 99 | type: 'billing', 100 | updateCb: (Map newAddress) async { 101 | String msg = 102 | await Provider.of(context, listen: false) 103 | .updateUser({'billing': _filterData(newAddress)}); 104 | return msg; 105 | }, 106 | ), 107 | )), 108 | ), 109 | ListTile( 110 | title: Text('Shipping Address', style: listTileTextStyle), 111 | contentPadding: EdgeInsets.symmetric(horizontal: screenWidth * 0.1), 112 | trailing: Icon( 113 | Icons.chevron_right, 114 | size: 28, 115 | color: GrodudesPrimaryColor.primaryColor[500], 116 | ), 117 | onTap: () => Navigator.push( 118 | context, 119 | CupertinoPageRoute( 120 | builder: (context) => AddressDetails( 121 | title: 'Shipping Address', 122 | type: 'shipping', 123 | updateCb: (Map newAddress) async { 124 | String msg = 125 | await Provider.of(context, listen: false) 126 | .updateUser({'shipping': _filterData(newAddress)}); 127 | return msg; 128 | }, 129 | ), 130 | )), 131 | ), 132 | Center( 133 | child: RaisedButton( 134 | onPressed: () { 135 | dataProvider.logOut(); 136 | }, 137 | textColor: Colors.white, 138 | child: Text( 139 | 'Logout', 140 | style: TextStyle(fontWeight: FontWeight.bold), 141 | ), 142 | ), 143 | ), 144 | ], 145 | ); 146 | } 147 | } 148 | 149 | class _LoginErrorDescriptionWidget extends StatelessWidget { 150 | final Map errorData; 151 | _LoginErrorDescriptionWidget(this.errorData); 152 | @override 153 | Widget build(BuildContext context) { 154 | return Center( 155 | child: Column( 156 | mainAxisAlignment: MainAxisAlignment.center, 157 | children: [ 158 | Text( 159 | errorData == null 160 | ? 'An Unknown error has occured. Please retry Login' 161 | : errorData['errMsg'] ?? 162 | 'An Unknown error has occured. Please retry Login', 163 | style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16), 164 | textAlign: TextAlign.center, 165 | ), 166 | SizedBox(height: 24), 167 | RaisedButton( 168 | onPressed: () {}, 169 | child: Text('Retry Login'), 170 | ) 171 | ], 172 | ), 173 | ); 174 | } 175 | } 176 | -------------------------------------------------------------------------------- /lib/pages/checkout/AddressUpdatePage.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:grodudes/helper/Constants.dart'; 3 | 4 | class AddressUpdatePage extends StatelessWidget { 5 | final Map address; 6 | final Function updateCb; 7 | final bool shouldDisplayPostcodeDropdown; 8 | AddressUpdatePage(this.address, this.updateCb, 9 | {@required this.shouldDisplayPostcodeDropdown}); 10 | 11 | final List _textEditingControllers = []; 12 | 13 | _capitalize(String str) { 14 | return '${str[0].toUpperCase()}${str.substring(1)}'; 15 | } 16 | 17 | List> _getMenuItems(String value) { 18 | List> menuItems = pincodes 19 | .map( 20 | (e) => DropdownMenuItem( 21 | child: Text(e), 22 | value: e, 23 | ), 24 | ) 25 | .toList(); 26 | 27 | if (pincodes.contains(value)) return menuItems; 28 | menuItems.add(DropdownMenuItem( 29 | child: Text('Not Specified'), 30 | value: value, 31 | )); 32 | return menuItems; 33 | } 34 | 35 | @override 36 | Widget build(BuildContext context) { 37 | if (address == null || address.length == 0) { 38 | return Scaffold( 39 | appBar: AppBar(title: Text('Update your address details')), 40 | body: Center(child: Text('Address not found')), 41 | ); 42 | } 43 | return Scaffold( 44 | appBar: AppBar(title: Text('Update your address details')), 45 | body: ListView.builder( 46 | padding: EdgeInsets.all(8), 47 | itemCount: address.length, 48 | itemBuilder: (context, index) { 49 | String key = address.keys.elementAt(index); 50 | String keyText = key.split('_').map((e) => _capitalize(e)).join(' '); 51 | TextEditingController _controller = 52 | TextEditingController(text: address[key] ?? ''); 53 | this._textEditingControllers.add(_controller); 54 | if (shouldDisplayPostcodeDropdown && key == 'postcode') { 55 | return Row( 56 | crossAxisAlignment: CrossAxisAlignment.start, 57 | children: [ 58 | Expanded( 59 | flex: 1, 60 | child: Padding( 61 | padding: const EdgeInsets.only(top: 8.0), 62 | child: Text( 63 | keyText, 64 | style: 65 | TextStyle(fontSize: 18, fontWeight: FontWeight.bold), 66 | ), 67 | ), 68 | ), 69 | ButtonTheme( 70 | alignedDropdown: true, 71 | child: Expanded( 72 | flex: 2, 73 | child: DropdownButtonFormField( 74 | value: this.address[key], 75 | dropdownColor: Color.fromRGBO(230, 230, 230, 1), 76 | items: >[ 77 | ..._getMenuItems(this.address[key]) 78 | ], 79 | onChanged: (value) => this.address[key] = value, 80 | decoration: InputDecoration( 81 | focusedBorder: OutlineInputBorder( 82 | borderSide: BorderSide( 83 | color: GrodudesPrimaryColor.primaryColor[600], 84 | width: 2), 85 | ), 86 | border: OutlineInputBorder( 87 | borderSide: BorderSide(color: Colors.blue)), 88 | contentPadding: 89 | EdgeInsets.symmetric(horizontal: 6, vertical: 2), 90 | ), 91 | style: TextStyle(fontSize: 16, color: Colors.black), 92 | ), 93 | ), 94 | ) 95 | ], 96 | ); 97 | } 98 | return Column( 99 | crossAxisAlignment: CrossAxisAlignment.start, 100 | children: [ 101 | Text( 102 | keyText, 103 | style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold), 104 | ), 105 | SizedBox(height: 4), 106 | TextField( 107 | controller: _controller, 108 | decoration: InputDecoration( 109 | focusedBorder: OutlineInputBorder( 110 | borderSide: BorderSide( 111 | color: GrodudesPrimaryColor.primaryColor[500], 112 | width: 2)), 113 | border: OutlineInputBorder( 114 | borderSide: BorderSide(color: Colors.blue)), 115 | contentPadding: 116 | EdgeInsets.symmetric(horizontal: 6, vertical: 2), 117 | ), 118 | onChanged: (value) => this.address[key] = value, 119 | style: TextStyle(fontSize: 16), 120 | ), 121 | SizedBox(height: 16), 122 | ], 123 | ); 124 | }, 125 | ), 126 | bottomNavigationBar: Container( 127 | height: 45, 128 | child: Row( 129 | crossAxisAlignment: CrossAxisAlignment.stretch, 130 | children: [ 131 | Expanded( 132 | child: RaisedButton( 133 | shape: RoundedRectangleBorder( 134 | borderRadius: BorderRadius.zero, 135 | side: BorderSide( 136 | color: GrodudesPrimaryColor.primaryColor[700], 137 | width: 2, 138 | ), 139 | ), 140 | color: Colors.white, 141 | child: Text( 142 | 'Cancel', 143 | style: TextStyle(color: Colors.black87, fontSize: 16), 144 | ), 145 | onPressed: () => Navigator.pop(context), 146 | ), 147 | ), 148 | Expanded( 149 | child: RaisedButton( 150 | shape: RoundedRectangleBorder( 151 | borderRadius: BorderRadius.zero, 152 | ), 153 | child: Text( 154 | 'Update', 155 | style: TextStyle(color: Colors.white, fontSize: 16), 156 | ), 157 | onPressed: () { 158 | this.updateCb(this.address); 159 | Navigator.pop(context); 160 | }, 161 | ), 162 | ), 163 | ], 164 | ), 165 | ), 166 | ); 167 | } 168 | } 169 | -------------------------------------------------------------------------------- /lib/state/products_state.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/cupertino.dart'; 2 | import 'package:grodudes/helper/WooCommerceAPI.dart'; 3 | import 'package:grodudes/models/Category.dart'; 4 | import 'package:grodudes/models/Product.dart'; 5 | import '../secrets.dart'; 6 | 7 | const productsPerPage = 50; 8 | 9 | class ProductsManager with ChangeNotifier { 10 | Map products; 11 | Map categories; 12 | bool isFetchingCategories = false; 13 | Set popularProductIds; 14 | Set topRatedProductIds; 15 | Set latestProductIds; 16 | int nextProductPage; 17 | bool allProductPagesFetched; 18 | WooCommerceAPI wooCommerceAPI; 19 | 20 | ProductsManager() { 21 | products = {}; 22 | categories = {}; 23 | nextProductPage = 1; 24 | allProductPagesFetched = false; 25 | popularProductIds = {}; 26 | topRatedProductIds = {}; 27 | latestProductIds = {}; 28 | wooCommerceAPI = WooCommerceAPI( 29 | url: 'https://www.grodudes.com', 30 | consumerKey: secret['consumerKey'], 31 | consumerSecret: secret['consumerSecret'], 32 | ); 33 | } 34 | 35 | setProductsFromLocalCart(List localCartProducts) { 36 | localCartProducts.forEach((item) => this._addProduct(item)); 37 | } 38 | 39 | _addProduct(Product item) { 40 | if (this.products.containsKey(item.data['id'])) { 41 | return this.products[item.data['id']]; 42 | } 43 | this.products[item.data['id']] = item; 44 | return item; 45 | } 46 | 47 | Future fetchAllParentCategories() async { 48 | if (this.categories.values.length > 0) return true; 49 | List fetchedCategories = []; 50 | 51 | fetchedCategories = await wooCommerceAPI 52 | .getAsync('products/categories?parent=0&per_page=50&page=1'); 53 | if (fetchedCategories != null && fetchedCategories.length > 0) { 54 | fetchedCategories 55 | .where((element) => 56 | element['name'].toString().toLowerCase() != 'uncategorized') 57 | .forEach( 58 | (category) => categories[category['id']] = Category(category)); 59 | notifyListeners(); 60 | return true; 61 | } 62 | } 63 | 64 | Future fetchNextProductsPage() async { 65 | if (allProductPagesFetched) return; 66 | List fetchedProducts = []; 67 | fetchedProducts = await wooCommerceAPI 68 | .getAsync('products?per_page=$productsPerPage&page=$nextProductPage') 69 | .timeout(Duration(seconds: 60)); 70 | if (fetchedProducts == null) return; 71 | if (fetchedProducts.length > 0) nextProductPage++; 72 | if (fetchedProducts.length < productsPerPage) allProductPagesFetched = true; 73 | fetchedProducts.forEach((item) => _addProduct(Product(item))); 74 | notifyListeners(); 75 | } 76 | 77 | Future fetchCategoryDetails(Category category) async { 78 | //fetch sub categories 79 | List data = await wooCommerceAPI 80 | .getAsync('products/categories?parent=${category.data['id']}') 81 | .catchError((err) => print(err)); 82 | List categories = []; 83 | data.forEach((item) => categories.add(Category(item))); 84 | if (categories.length > 0) return {'categories': categories}; 85 | 86 | // fetch products if there are no sub categories 87 | List productData = await wooCommerceAPI 88 | .getAsync('products?category=${category.data['id']}') 89 | .catchError((err) => print(err)); 90 | List fetchedProducts = []; 91 | productData.forEach((product) { 92 | _addProduct(Product(product)); 93 | fetchedProducts.add(this.products[product['id']]); 94 | }); 95 | return { 96 | 'categories': categories, 97 | 'products': fetchedProducts, 98 | }; 99 | } 100 | 101 | Future fetchLatestProducts({bool shouldNotify = false}) async { 102 | if (this.latestProductIds.length > 0) { 103 | if (shouldNotify) notifyListeners(); 104 | return true; 105 | } 106 | List productsData = await this 107 | .wooCommerceAPI 108 | .getAsync('products?per_page=20&orderby=date&order=desc', 109 | apiVersion: 'v3') 110 | .timeout(Duration(seconds: 60)) 111 | .catchError((err) { 112 | print('fetching latest products: $err'); 113 | throw err; 114 | }); 115 | if (productsData == null) throw Exception('Failed to fetch products'); 116 | productsData.forEach((item) { 117 | item['in_stock'] = item['stock_status'] == 'instock'; 118 | this._addProduct(Product(item)); 119 | this.latestProductIds.add(item['id']); 120 | }); 121 | if (shouldNotify) notifyListeners(); 122 | return true; 123 | } 124 | 125 | Future fetchRatedProducts({bool shouldNotify = false}) async { 126 | if (this.topRatedProductIds.length > 0) { 127 | if (shouldNotify) notifyListeners(); 128 | return true; 129 | } 130 | List productsData = await this 131 | .wooCommerceAPI 132 | .getAsync('products?per_page=20&orderby=rating&order=desc', 133 | apiVersion: 'v3') 134 | .timeout(Duration(seconds: 60)) 135 | .catchError((err) { 136 | print('fetching top rated products: $err'); 137 | throw err; 138 | }); 139 | if (productsData == null) throw FlutterError('Failed to fetch products'); 140 | productsData.forEach((item) { 141 | item['in_stock'] = item['stock_status'] == 'instock'; 142 | this._addProduct(Product(item)); 143 | this.topRatedProductIds.add(item['id']); 144 | }); 145 | if (shouldNotify) notifyListeners(); 146 | return true; 147 | } 148 | 149 | Future fetchPopularProducts({bool shouldNotify = false}) async { 150 | if (this.popularProductIds.length > 0) { 151 | if (shouldNotify) notifyListeners(); 152 | return true; 153 | } 154 | List productsData = await this 155 | .wooCommerceAPI 156 | .getAsync('products?per_page=20&orderby=popularity&order=desc', 157 | apiVersion: 'v3') 158 | .timeout(Duration(seconds: 60)) 159 | .catchError((err) { 160 | print('fetching popular products: $err'); 161 | throw err; 162 | }); 163 | if (productsData == null) throw FlutterError('Failed to fetch products'); 164 | productsData.forEach((item) { 165 | item['in_stock'] = item['stock_status'] == 'instock'; 166 | this._addProduct(Product(item)); 167 | this.popularProductIds.add(item['id']); 168 | }); 169 | if (shouldNotify) notifyListeners(); 170 | return true; 171 | } 172 | 173 | Future> searchProducts(String searchQuery) async { 174 | List searchedProductsData = 175 | await wooCommerceAPI.getAsync('products?search=$searchQuery'); 176 | 177 | List searchedProducts = []; 178 | searchedProductsData.forEach((productData) { 179 | Product item = _addProduct(Product(productData)); 180 | searchedProducts.add(item); 181 | }); 182 | return searchedProducts; 183 | } 184 | 185 | Future> getProductsInOrder(Map order) async { 186 | List productsIds = []; 187 | order['line_items'].forEach((item) { 188 | productsIds.add(item['product_id']); 189 | }); 190 | String includeString = productsIds.join(','); 191 | List response = 192 | await wooCommerceAPI.getAsync('products?include=$includeString'); 193 | List orderProducts = []; 194 | response.forEach((item) => orderProducts.add(_addProduct(Product(item)))); 195 | return orderProducts; 196 | } 197 | } 198 | -------------------------------------------------------------------------------- /lib/pages/account/OrderDetails.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:grodudes/components/ProductListTile.dart'; 3 | import 'package:grodudes/models/Product.dart'; 4 | import 'package:grodudes/state/products_state.dart'; 5 | import 'package:provider/provider.dart'; 6 | 7 | class OrderDetails extends StatelessWidget { 8 | final Map order; 9 | final TextStyle labelTextStyle = TextStyle( 10 | fontSize: 18, 11 | fontWeight: FontWeight.bold, 12 | ); 13 | OrderDetails(this.order); 14 | 15 | String _getQuantityById(int id) { 16 | List lineItems = this.order['line_items']; 17 | if (lineItems == null) return 'Not Found'; 18 | String quantity = 'Not Found'; 19 | lineItems.forEach((item) { 20 | if (item['product_id'] == id) { 21 | quantity = item['quantity'].toString(); 22 | } 23 | }); 24 | return quantity; 25 | } 26 | 27 | @override 28 | Widget build(BuildContext context) { 29 | if (order == null || order['id'] == null) { 30 | return Scaffold( 31 | appBar: AppBar(title: Text('Order Details')), 32 | body: Container( 33 | alignment: Alignment.center, 34 | child: Text( 35 | 'Something went Wrong!', 36 | style: TextStyle(fontSize: 18), 37 | textAlign: TextAlign.center, 38 | ), 39 | ), 40 | ); 41 | } 42 | // print(order); 43 | final String date = order['date_created'].toString().substring(0, 10); 44 | final Map shippingAddress = this.order['shipping']; 45 | final Map billingAddress = this.order['billing']; 46 | return Scaffold( 47 | appBar: AppBar(title: Text('Order Details')), 48 | body: ListView( 49 | padding: EdgeInsets.all(16), 50 | scrollDirection: Axis.vertical, 51 | children: [ 52 | SizedBox(height: 15), 53 | Text( 54 | 'Order ID ${order['id']}', 55 | textAlign: TextAlign.center, 56 | style: TextStyle(fontSize: 26, fontWeight: FontWeight.bold), 57 | ), 58 | SizedBox(height: 16), 59 | Row( 60 | children: [ 61 | Text('Order Total', style: labelTextStyle), 62 | SizedBox(width: 16), 63 | Expanded( 64 | child: Text( 65 | '₹${order['total']}', 66 | style: TextStyle( 67 | fontSize: 20, 68 | color: Colors.green[600], 69 | fontWeight: FontWeight.bold, 70 | ), 71 | ), 72 | ), 73 | ], 74 | ), 75 | Row( 76 | children: [ 77 | Text('Order Status', style: labelTextStyle), 78 | SizedBox(width: 16), 79 | Text( 80 | order['status'].split('-').join(' '), 81 | style: TextStyle( 82 | color: Colors.green[800], 83 | fontSize: 16, 84 | fontWeight: FontWeight.bold, 85 | ), 86 | ), 87 | ], 88 | ), 89 | SizedBox(height: 8), 90 | Text( 91 | '${order['line_items'].length} ${order['line_items'].length > 1 ? 'items' : 'item'}', 92 | style: TextStyle( 93 | fontSize: 16, 94 | color: Colors.black87, 95 | ), 96 | ), 97 | Text( 98 | 'Placed on ' + date.split('-').reversed.join('-'), 99 | style: TextStyle( 100 | fontSize: 16, 101 | color: Colors.black87, 102 | ), 103 | ), 104 | Text( 105 | 'Payment Method - ' + 106 | (order['payment_method_title'] ?? 'Not Found'), 107 | style: TextStyle( 108 | fontSize: 16, 109 | color: Colors.black87, 110 | ), 111 | ), 112 | SizedBox(height: 20), 113 | Text('Shipping Address', style: labelTextStyle), 114 | SizedBox(height: 8), 115 | Column( 116 | children: [ 117 | ...(shippingAddress ?? {'error': 'Something went wrong'}) 118 | .entries 119 | .map((e) => _OrderAddressRow(e.key, e.value)), 120 | ], 121 | ), 122 | SizedBox(height: 20), 123 | Text('Billing Address', style: labelTextStyle), 124 | SizedBox(height: 8), 125 | Column( 126 | children: [ 127 | ...(billingAddress ?? {'error': 'Something went wrong'}) 128 | .entries 129 | .map((e) => _OrderAddressRow(e.key, e.value)), 130 | ], 131 | ), 132 | SizedBox(height: 20), 133 | Text('Ordered Products', style: labelTextStyle), 134 | SizedBox(height: 8), 135 | FutureBuilder( 136 | future: 137 | Provider.of(context).getProductsInOrder(order), 138 | builder: (context, snapshot) { 139 | if (snapshot.hasError) { 140 | return Column( 141 | children: [ 142 | SizedBox(height: 20), 143 | Icon(Icons.error, color: Colors.red[600], size: 36), 144 | Text( 145 | 'Failed to fetch products', 146 | style: TextStyle(fontSize: 16), 147 | ), 148 | SizedBox(height: 20), 149 | ], 150 | ); 151 | } 152 | if (snapshot.hasData) { 153 | return ListView.builder( 154 | shrinkWrap: true, 155 | scrollDirection: Axis.vertical, 156 | physics: NeverScrollableScrollPhysics(), 157 | itemCount: snapshot.data.length, 158 | itemBuilder: (context, index) => _OrderedProductListTile( 159 | snapshot.data[index], 160 | _getQuantityById(snapshot.data[index].data['id'])), 161 | ); 162 | } 163 | return Container( 164 | padding: EdgeInsets.symmetric(vertical: 16), 165 | alignment: Alignment.center, 166 | child: CircularProgressIndicator(), 167 | ); 168 | }, 169 | ) 170 | ], 171 | ), 172 | ); 173 | } 174 | } 175 | 176 | class _OrderedProductListTile extends StatelessWidget { 177 | final Product item; 178 | final String orderQuantity; 179 | _OrderedProductListTile(this.item, this.orderQuantity); 180 | @override 181 | Widget build(BuildContext context) { 182 | return Column( 183 | crossAxisAlignment: CrossAxisAlignment.end, 184 | children: [ 185 | ProductListTile(item), 186 | Text( 187 | "Order Quantity - " + orderQuantity ?? 'Not Found', 188 | style: TextStyle(fontSize: 16), 189 | ), 190 | SizedBox(height: 8), 191 | ], 192 | ); 193 | } 194 | } 195 | 196 | class _OrderAddressRow extends StatelessWidget { 197 | final String addressKey; 198 | final String addressValue; 199 | _OrderAddressRow(this.addressKey, this.addressValue); 200 | 201 | _capitalize(String str) { 202 | return '${str[0].toUpperCase()}${str.substring(1)}'; 203 | } 204 | 205 | String _formatName(String name) { 206 | return name.split('_').map((e) => _capitalize(e)).join(' '); 207 | } 208 | 209 | final TextStyle labelTextStyle = TextStyle( 210 | fontSize: 16, 211 | fontWeight: FontWeight.bold, 212 | ); 213 | 214 | @override 215 | Widget build(BuildContext context) { 216 | return Row( 217 | children: [ 218 | Expanded( 219 | flex: 1, 220 | child: Text(_formatName(this.addressKey), style: labelTextStyle), 221 | ), 222 | Expanded( 223 | flex: 2, 224 | child: Container( 225 | child: Text(this.addressValue, style: TextStyle(fontSize: 16)), 226 | ), 227 | ), 228 | ], 229 | ); 230 | } 231 | } 232 | -------------------------------------------------------------------------------- /lib/components/RootDrawer.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/cupertino.dart'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:provider/provider.dart'; 4 | import 'package:url_launcher/url_launcher.dart'; 5 | 6 | import 'package:grodudes/components/CategoryCard.dart'; 7 | import 'package:grodudes/models/Category.dart'; 8 | import 'package:grodudes/pages/CartItems.dart'; 9 | import 'package:grodudes/pages/account/AccountRoot.dart'; 10 | import 'package:grodudes/pages/account/Orders.dart'; 11 | import 'package:grodudes/state/products_state.dart'; 12 | import 'package:grodudes/state/user_state.dart'; 13 | 14 | class RootDrawer extends StatelessWidget { 15 | final TextStyle _whiteText = TextStyle(color: Colors.white, fontSize: 15); 16 | 17 | String _formatName(String name) { 18 | return name.replaceAll('&', '&'); 19 | } 20 | 21 | Future _openSnapPage() async { 22 | String url = "https://www.grodudes.com/take-a-snap"; 23 | try { 24 | if (await canLaunch(url)) { 25 | await launch(url, enableJavaScript: true); 26 | } 27 | } catch (err) { 28 | print(err); 29 | } 30 | } 31 | 32 | @override 33 | Widget build(BuildContext context) { 34 | return Consumer( 35 | builder: (context, userManager, child) { 36 | bool isLoggedIn = userManager.isLoggedIn(); 37 | List categories = 38 | Provider.of(context, listen: false) 39 | .categories 40 | .values 41 | .toList(); 42 | return Drawer( 43 | child: ListView( 44 | children: [ 45 | Container( 46 | decoration: BoxDecoration(color: Color(0xFF3a3a3a)), 47 | padding: EdgeInsets.symmetric(horizontal: 16, vertical: 36), 48 | child: GestureDetector( 49 | onTap: () { 50 | Navigator.pop(context); 51 | Navigator.push( 52 | context, 53 | CupertinoPageRoute(builder: (context) => AccountRoot()), 54 | ); 55 | }, 56 | child: Row( 57 | mainAxisSize: MainAxisSize.max, 58 | children: [ 59 | isLoggedIn 60 | ? Image.network( 61 | userManager.wcUserInfo['avatar_url'] ?? '', 62 | height: 52, 63 | width: 52, 64 | errorBuilder: (context, error, stackTrace) => 65 | Icon( 66 | Icons.person, 67 | size: 52, 68 | color: Colors.grey, 69 | ), 70 | ) 71 | : Icon(Icons.person, size: 52, color: Colors.white70), 72 | SizedBox(width: 12), 73 | isLoggedIn 74 | ? Expanded( 75 | child: Column( 76 | crossAxisAlignment: CrossAxisAlignment.start, 77 | children: [ 78 | Text( 79 | userManager.wcUserInfo['username'] ?? 80 | 'Username not found', 81 | style: _whiteText), 82 | Text( 83 | userManager.wcUserInfo['email'] ?? 84 | 'Email not found', 85 | style: _whiteText) 86 | ], 87 | ), 88 | ) 89 | : Text('Guest', style: _whiteText) 90 | ], 91 | ), 92 | ), 93 | ), 94 | ListTile( 95 | title: Text('Your Orders'), 96 | leading: Icon(Icons.card_giftcard, color: Colors.orange[700]), 97 | trailing: Icon( 98 | Icons.keyboard_arrow_right, 99 | color: Colors.green, 100 | ), 101 | onTap: () { 102 | if (!isLoggedIn) { 103 | Scaffold.of(context).showSnackBar( 104 | SnackBar( 105 | content: Text('Please Login to track your Orders'), 106 | ), 107 | ); 108 | Navigator.pop(context); 109 | return; 110 | } 111 | Navigator.pop(context); 112 | Navigator.push( 113 | context, 114 | CupertinoPageRoute( 115 | builder: (context) => Orders(), 116 | ), 117 | ); 118 | }, 119 | ), 120 | ListTile( 121 | title: Text('Cart'), 122 | leading: Icon(Icons.shopping_cart, color: Colors.blue[700]), 123 | trailing: Icon( 124 | Icons.keyboard_arrow_right, 125 | color: Colors.green, 126 | ), 127 | onTap: () { 128 | Navigator.pop(context); 129 | Navigator.push( 130 | context, 131 | CupertinoPageRoute(builder: (context) => CartItems()), 132 | ); 133 | }, 134 | ), 135 | categories != null && categories is List 136 | ? Theme( 137 | data: Theme.of(context) 138 | .copyWith(unselectedWidgetColor: Colors.green), 139 | child: ExpansionTile( 140 | title: Text('Categories'), 141 | leading: Icon(Icons.category, color: Colors.amber[700]), 142 | children: [ 143 | ...categories 144 | .map( 145 | (e) => ListTile( 146 | title: Text(_formatName(e.data['name'])), 147 | onTap: () { 148 | Navigator.pop(context); 149 | Navigator.push( 150 | context, 151 | CupertinoPageRoute( 152 | builder: (context) => 153 | CategoryContents(category: e), 154 | ), 155 | ); 156 | }, 157 | ), 158 | ) 159 | .toList() 160 | ], 161 | ), 162 | ) 163 | : SizedBox(height: 0), 164 | ListTile( 165 | title: Text('Account'), 166 | leading: Icon(Icons.person, color: Colors.green[700]), 167 | trailing: Icon( 168 | Icons.keyboard_arrow_right, 169 | color: Colors.green, 170 | ), 171 | onTap: () { 172 | Navigator.pop(context); 173 | Navigator.push( 174 | context, 175 | CupertinoPageRoute(builder: (context) => AccountRoot()), 176 | ); 177 | }, 178 | ), 179 | ListTile( 180 | title: Text('Order through Snap'), 181 | leading: Icon(Icons.camera_alt, color: Colors.brown), 182 | trailing: Icon( 183 | Icons.keyboard_arrow_right, 184 | color: Colors.green, 185 | ), 186 | onTap: () { 187 | Navigator.pop(context); 188 | _openSnapPage(); 189 | }, 190 | ), 191 | isLoggedIn 192 | ? ListTile( 193 | title: Text('Logout'), 194 | leading: 195 | Icon(Icons.power_settings_new, color: Colors.red), 196 | onTap: () { 197 | Navigator.pop(context); 198 | userManager.logOut(); 199 | }, 200 | ) 201 | : SizedBox(height: 0), 202 | ], 203 | ), 204 | ); 205 | }, 206 | ); 207 | } 208 | } 209 | -------------------------------------------------------------------------------- /lib/pages/account/LogInPage.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:grodudes/helper/Constants.dart'; 3 | import 'package:grodudes/state/user_state.dart'; 4 | import 'package:provider/provider.dart'; 5 | 6 | final InputDecoration defaultTextFieldDecoration = InputDecoration( 7 | labelStyle: TextStyle(color: Colors.grey[600]), 8 | alignLabelWithHint: true, 9 | focusedBorder: OutlineInputBorder( 10 | borderSide: BorderSide( 11 | color: GrodudesPrimaryColor.primaryColor[600], 12 | width: 2, 13 | )), 14 | border: OutlineInputBorder(borderSide: BorderSide(color: Colors.blue)), 15 | contentPadding: EdgeInsets.symmetric(horizontal: 6, vertical: 2), 16 | ); 17 | 18 | class LoginPage extends StatefulWidget { 19 | @override 20 | _LoginPageState createState() => _LoginPageState(); 21 | } 22 | 23 | class _LoginPageState extends State { 24 | TextEditingController _usernameController; 25 | TextEditingController _passwordController; 26 | TextEditingController _emailController; 27 | String action; 28 | 29 | @override 30 | void initState() { 31 | this._usernameController = new TextEditingController(); 32 | this._passwordController = new TextEditingController(); 33 | this._emailController = new TextEditingController(); 34 | this.action = 'login'; 35 | super.initState(); 36 | } 37 | 38 | @override 39 | void dispose() { 40 | this._usernameController.dispose(); 41 | this._passwordController.dispose(); 42 | this._emailController.dispose(); 43 | super.dispose(); 44 | } 45 | 46 | Widget getFormInputField( 47 | String title, TextEditingController inputController) { 48 | double screenWidth = MediaQuery.of(context).size.width; 49 | return Container( 50 | width: screenWidth * 0.8, 51 | child: TextFormField( 52 | decoration: defaultTextFieldDecoration.copyWith( 53 | labelText: title, 54 | hintText: title, 55 | prefixIcon: title == 'Username' 56 | ? Icon(Icons.account_circle, color: Colors.green[700]) 57 | : title == 'Email' 58 | ? Icon(Icons.email, color: Colors.green[700]) 59 | : Icon(Icons.security, color: Colors.green[700]), 60 | ), 61 | controller: inputController, 62 | style: TextStyle(fontSize: 16), 63 | ), 64 | ); 65 | } 66 | 67 | _handleFormSubmit() { 68 | bool valuesAvailable = true; 69 | WidgetsBinding.instance.focusManager.primaryFocus?.unfocus(); 70 | if (this._usernameController.text.length == 0 || 71 | this._passwordController.text.length == 0) { 72 | valuesAvailable = false; 73 | } 74 | if (this.action == 'register' && this._emailController.text.length == 0) { 75 | valuesAvailable = false; 76 | } 77 | if (valuesAvailable == false) { 78 | Scaffold.of(context).showSnackBar( 79 | SnackBar( 80 | content: Text('Please fill all the feilds'), 81 | ), 82 | ); 83 | return; 84 | } 85 | if (this.action == 'login') { 86 | Provider.of(context, listen: false) 87 | .logInToWordpress( 88 | this._usernameController.text, this._passwordController.text) 89 | .catchError((err) { 90 | print(err); 91 | }); 92 | } else if (this.action == 'register') { 93 | Provider.of(context, listen: false) 94 | .registerNewUser( 95 | this._usernameController.text, 96 | this._emailController.text, 97 | this._passwordController.text, 98 | ) 99 | .catchError((err) { 100 | print(err); 101 | }); 102 | } 103 | } 104 | 105 | Widget _getErrorPrompt() { 106 | Map wpUserInfo = 107 | Provider.of(context, listen: false).wpUserInfo; 108 | return Container( 109 | padding: EdgeInsets.symmetric(vertical: 16, horizontal: 10), 110 | margin: EdgeInsets.symmetric(vertical: 4, horizontal: 16), 111 | decoration: BoxDecoration( 112 | color: Color(0x39ff0000), 113 | borderRadius: BorderRadius.circular(8), 114 | border: Border.all(color: Colors.red[300], width: 2), 115 | ), 116 | child: Text(wpUserInfo['errMsg'] ?? 'Login Failed. Please try again.'), 117 | ); 118 | } 119 | 120 | @override 121 | Widget build(BuildContext context) { 122 | return ListView( 123 | scrollDirection: Axis.vertical, 124 | children: [ 125 | SizedBox(height: MediaQuery.of(context).size.height * 0.1), 126 | Form( 127 | child: Column( 128 | mainAxisAlignment: MainAxisAlignment.center, 129 | children: [ 130 | this.action == 'login' 131 | ? Icon(Icons.person, size: 60, color: Colors.black54) 132 | : Icon(Icons.person_add, size: 60, color: Colors.black54), 133 | Text( 134 | this.action == 'login' 135 | ? 'Login to your Account' 136 | : 'Create a new Account', 137 | style: TextStyle(fontWeight: FontWeight.bold, fontSize: 20), 138 | ), 139 | SizedBox(height: 40), 140 | getFormInputField('Username', this._usernameController), 141 | SizedBox(height: 14), 142 | _PasswordFromInputRow(this._passwordController), 143 | this.action == 'login' 144 | ? SizedBox(height: 0) 145 | : SizedBox(height: 14), 146 | this.action == 'login' 147 | ? SizedBox(height: 0) 148 | : getFormInputField('Email', this._emailController), 149 | SizedBox(height: 12), 150 | RaisedButton( 151 | onPressed: _handleFormSubmit, 152 | textColor: Colors.white, 153 | shape: RoundedRectangleBorder( 154 | borderRadius: BorderRadius.circular(8)), 155 | child: Text( 156 | this.action == 'login' ? 'Login' : 'Register', 157 | style: TextStyle(fontWeight: FontWeight.bold), 158 | ), 159 | ), 160 | Row( 161 | mainAxisAlignment: MainAxisAlignment.center, 162 | children: [ 163 | Text( 164 | this.action == 'login' ? 'New User?' : 'Existing User?', 165 | style: TextStyle(fontSize: 16), 166 | ), 167 | MaterialButton( 168 | child: Text( 169 | this.action == 'login' ? 'Register' : 'Login', 170 | style: TextStyle(fontSize: 16), 171 | ), 172 | onPressed: () => setState(() { 173 | this.action = 174 | this.action == 'login' ? 'register' : 'login'; 175 | }), 176 | ) 177 | ], 178 | ), 179 | SizedBox(height: 8), 180 | Provider.of(context).getLogInStatus() == 181 | logInStates.logInFailed 182 | ? _getErrorPrompt() 183 | : SizedBox(height: 0), 184 | SizedBox(height: 16), 185 | ], 186 | ), 187 | ), 188 | ], 189 | ); 190 | } 191 | } 192 | 193 | class _PasswordFromInputRow extends StatefulWidget { 194 | final TextEditingController inputController; 195 | _PasswordFromInputRow(this.inputController); 196 | @override 197 | __PasswordFromInputRowState createState() => __PasswordFromInputRowState(); 198 | } 199 | 200 | class __PasswordFromInputRowState extends State<_PasswordFromInputRow> { 201 | final String title = 'Password'; 202 | bool isObscured; 203 | @override 204 | void initState() { 205 | this.isObscured = true; 206 | super.initState(); 207 | } 208 | 209 | @override 210 | Widget build(BuildContext context) { 211 | double screenWidth = MediaQuery.of(context).size.width; 212 | return Container( 213 | width: screenWidth * 0.8, 214 | child: TextFormField( 215 | obscureText: this.isObscured, 216 | autocorrect: false, 217 | decoration: defaultTextFieldDecoration.copyWith( 218 | labelText: title, 219 | hintText: title, 220 | prefixIcon: Icon(Icons.security, color: Colors.green[700]), 221 | suffixIcon: GestureDetector( 222 | child: Icon(Icons.remove_red_eye, 223 | color: GrodudesPrimaryColor.primaryColor[600]), 224 | onTap: () => setState(() => this.isObscured = !this.isObscured), 225 | ), 226 | ), 227 | controller: widget.inputController, 228 | style: TextStyle(fontSize: 16), 229 | ), 230 | ); 231 | } 232 | } 233 | -------------------------------------------------------------------------------- /lib/state/user_state.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | import 'dart:io'; 3 | 4 | import 'package:flutter/cupertino.dart'; 5 | import 'package:flutter_secure_storage/flutter_secure_storage.dart'; 6 | import 'package:grodudes/helper/WooCommerceAPI.dart'; 7 | import '../secrets.dart'; 8 | 9 | enum logInStates { loggedIn, loggedOut, pending, logInFailed } 10 | 11 | class UserManager with ChangeNotifier { 12 | Map wcUserInfo; 13 | Map wpUserInfo; 14 | FlutterSecureStorage _secureStorage; 15 | var _logInStatus; 16 | WooCommerceAPI _wooCommApi; 17 | 18 | UserManager() { 19 | this.wcUserInfo = {}; 20 | this.wpUserInfo = {}; 21 | this._secureStorage = FlutterSecureStorage(); 22 | this._logInStatus = logInStates.loggedOut; 23 | this._wooCommApi = WooCommerceAPI( 24 | url: 'https://www.grodudes.com', 25 | consumerKey: secret['consumerKey'], 26 | consumerSecret: secret['consumerSecret'], 27 | ); 28 | } 29 | 30 | bool isLoggedIn() => this._logInStatus == logInStates.loggedIn; 31 | getLogInStatus() => this._logInStatus; 32 | 33 | initializeUser( 34 | Map wpUserInfo, Map wcUserInfo) { 35 | if (wcUserInfo == null || wcUserInfo['id'] == null) return; 36 | if (this.wcUserInfo != null && this.wcUserInfo['id'] != null) return; 37 | this.wpUserInfo = wpUserInfo; 38 | this.wcUserInfo = wcUserInfo; 39 | this._logInStatus = logInStates.loggedIn; 40 | } 41 | 42 | Future logInToWordpress(String username, String password) async { 43 | if (isLoggedIn()) return; 44 | if (this._logInStatus != logInStates.pending) { 45 | this._logInStatus = logInStates.pending; 46 | notifyListeners(); 47 | } 48 | //authorize user through wordpress 49 | Map message; 50 | try { 51 | message = await this._wooCommApi.getAuthToken(username, password); 52 | if (message == null || message['token'] == null) { 53 | _createLoginErrorResponse(message ?? {'code': 'token_error'}); 54 | this._logInStatus = logInStates.logInFailed; 55 | notifyListeners(); 56 | return; 57 | } 58 | } catch (err) { 59 | _createLoginErrorResponse({'code': 'token_error'}); 60 | this._logInStatus = logInStates.logInFailed; 61 | notifyListeners(); 62 | print('error logging in to wordpress: $err'); 63 | return; 64 | } 65 | this.wpUserInfo = message; 66 | String token = this.wpUserInfo['token']; 67 | bool success = await _fetchLoggedInUserData(token); 68 | if (success != null && success) { 69 | this._logInStatus = logInStates.loggedIn; 70 | await _storeUserDataLocally().catchError((err) => print(err)); 71 | } else { 72 | this._logInStatus = logInStates.logInFailed; 73 | _createLoginErrorResponse({}); 74 | } 75 | notifyListeners(); 76 | return success; 77 | } 78 | 79 | Future _storeUserDataLocally() async { 80 | try { 81 | await this._secureStorage.write( 82 | key: 'grodudes_login_status', 83 | value: 'true', 84 | ); 85 | await this._secureStorage.write( 86 | key: 'grodudes_wp_info', 87 | value: json.encode(this.wpUserInfo), 88 | ); 89 | return true; 90 | } catch (err) { 91 | print(err); 92 | return false; 93 | } 94 | } 95 | 96 | Future _fetchLoggedInUserData(String token) async { 97 | try { 98 | int id = await this._wooCommApi.getLoggedInUserId(token); 99 | if (id == null) return false; 100 | var response = await this._wooCommApi.getAsync('customers/$id'); 101 | this.wcUserInfo = response; 102 | return true; 103 | } catch (err) { 104 | print('_fetchLoggedInUserData: $err'); 105 | return false; 106 | } 107 | } 108 | 109 | Future registerNewUser(String username, String email, String password) async { 110 | this._logInStatus = logInStates.pending; 111 | notifyListeners(); 112 | 113 | Map payload = { 114 | 'username': username, 115 | 'password': password, 116 | 'email': email, 117 | 'roles': 'customer' 118 | }; 119 | 120 | try { 121 | var response = await _wooCommApi.postAsync('customers', payload); 122 | if (response == null || response['id'] == null) { 123 | this._logInStatus = logInStates.logInFailed; 124 | _createRegistrationErrorResponse(response); 125 | notifyListeners(); 126 | return; 127 | } 128 | this.wcUserInfo = response; 129 | this.wpUserInfo = { 130 | 'user_email': email, 131 | 'user_display_name': username, 132 | }; 133 | 134 | this._logInStatus = logInStates.loggedIn; 135 | notifyListeners(); 136 | completeAuthOnRegistration(username, password); 137 | } catch (err) { 138 | this._logInStatus = logInStates.logInFailed; 139 | _createRegistrationErrorResponse({'code': 'registration_failed'}); 140 | notifyListeners(); 141 | } 142 | } 143 | 144 | Future completeAuthOnRegistration(String username, String password) async { 145 | try { 146 | var response = await this._wooCommApi.getAuthToken(username, password); 147 | if (response != null && response['token'] != null) { 148 | this.wpUserInfo = response; 149 | this._logInStatus = logInStates.loggedIn; 150 | notifyListeners(); 151 | } 152 | await _storeUserDataLocally(); 153 | } catch (err) { 154 | print(err); 155 | } 156 | } 157 | 158 | logOut() async { 159 | this.wpUserInfo = null; 160 | this.wcUserInfo = null; 161 | this._logInStatus = logInStates.loggedOut; 162 | notifyListeners(); 163 | await this 164 | ._secureStorage 165 | .write(key: 'grodudes_login_status', value: 'false'); 166 | } 167 | 168 | Future getOrders(int page) async { 169 | int customerId = this.wcUserInfo['id']; 170 | if (customerId == null) return false; 171 | try { 172 | List orders = await this 173 | ._wooCommApi 174 | .getAsync('orders?customer=$customerId&&per_page=10&&page=$page'); 175 | return orders; 176 | } catch (err) { 177 | print('orders not found $err'); 178 | return false; 179 | } 180 | } 181 | 182 | Future updateUser(Map user) async { 183 | try { 184 | var response = await this._wooCommApi.putAsync( 185 | 'customers/${this.wcUserInfo['id']}', 186 | user, 187 | userHeaders: {HttpHeaders.contentTypeHeader: 'application/json'}, 188 | ); 189 | if (response['id'] != null) { 190 | this.wcUserInfo = response; 191 | notifyListeners(); 192 | return "Updated"; 193 | } 194 | Map errorMap = response['data']['params']; 195 | if (errorMap == null) return 'Update Failed'; 196 | String errorMsg = errorMap.values.elementAt(0); 197 | return errorMsg != null ? 'Update Failed: $errorMsg' : 'Update Failed'; 198 | } catch (err) { 199 | print('could not get wcomm details $err'); 200 | return 'Update Failed'; 201 | } 202 | } 203 | 204 | Future cancelOrder(int id) async { 205 | Map body = { 206 | 'id': id, 207 | 'status': 'cancel-request', 208 | }; 209 | var response = await this._wooCommApi.putAsync( 210 | 'orders/$id', 211 | body, 212 | userHeaders: {HttpHeaders.contentTypeHeader: 'application/json'}, 213 | ); 214 | if (response['id'] != null) 215 | return response; 216 | else 217 | return false; 218 | } 219 | 220 | _createLoginErrorResponse(Map response) { 221 | final code = response['code']; 222 | 223 | if (code == '[jwt_auth] invalid_username') { 224 | this.wpUserInfo = { 225 | 'code': 'Invalid Username', 226 | 'errMsg': 'Unknown username. Check again or try your email address.' 227 | }; 228 | } else if (code == '[jwt_auth] incorrect_password') { 229 | this.wpUserInfo = { 230 | 'code': 'Wrong Password', 231 | 'errMsg': 'Please retry with the correct password' 232 | }; 233 | } else if (code == 'token_error') { 234 | this.wpUserInfo = { 235 | 'code': 'Authorization Problem', 236 | 'errMsg': 'Could not get authorization token' 237 | }; 238 | } else { 239 | this.wpUserInfo = { 240 | 'code': 'Unknown Error', 241 | 'errMsg': 'An Error Occured while trying to login' 242 | }; 243 | } 244 | this.wcUserInfo = this.wpUserInfo; 245 | } 246 | 247 | _createRegistrationErrorResponse(Map response) { 248 | final code = response['code']; 249 | 250 | if (code == 'registration_failed') { 251 | this.wpUserInfo = { 252 | 'code': code, 253 | 'errMsg': 'Failed to create User! Please try again.', 254 | }; 255 | } 256 | this.wpUserInfo = { 257 | 'code': response['code'] ?? 'registration_failed', 258 | 'errMsg': response['message'] ?? 'Registration Failed', 259 | }; 260 | this.wcUserInfo = this.wpUserInfo; 261 | } 262 | } 263 | -------------------------------------------------------------------------------- /lib/pages/ProductDetailsPage.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:grodudes/components/ProductDescriptionText.dart'; 3 | import 'package:grodudes/components/StyledProductPrice.dart'; 4 | import 'package:grodudes/helper/ImageFetcher.dart'; 5 | 6 | import 'package:grodudes/models/Product.dart'; 7 | import 'package:grodudes/state/cart_state.dart'; 8 | import 'package:provider/provider.dart'; 9 | 10 | class ProductDetailsPage extends StatelessWidget { 11 | final Product item; 12 | ProductDetailsPage(this.item); 13 | 14 | @override 15 | Widget build(BuildContext context) { 16 | return Scaffold( 17 | appBar: AppBar(title: Text('Product Details')), 18 | body: this.item == null 19 | ? Container( 20 | alignment: Alignment.center, 21 | child: Text( 22 | 'Data Not Found', 23 | style: TextStyle(fontSize: 18), 24 | textAlign: TextAlign.center, 25 | ), 26 | ) 27 | : ListView( 28 | children: [ 29 | SizedBox(height: 8), 30 | LimitedBox( 31 | maxHeight: 250, 32 | child: 33 | ImageFetcher.getImage(this.item.data['images'][0]['src']), 34 | ), 35 | SizedBox(height: 16), 36 | Padding( 37 | padding: 38 | const EdgeInsets.symmetric(vertical: 4.0, horizontal: 24), 39 | child: Text( 40 | this.item.data['name'], 41 | style: TextStyle( 42 | fontSize: 24, 43 | fontWeight: FontWeight.bold, 44 | ), 45 | ), 46 | ), 47 | this.item.data['purchasable'] != null && 48 | this.item.data['purchasable'] 49 | ? this.item.data['in_stock'] 50 | ? Padding( 51 | padding: const EdgeInsets.symmetric(horizontal: 24), 52 | child: StyledProductPrice( 53 | this.item.data['price'], 54 | this.item.data['regular_price'], 55 | priceFontSize: 18, 56 | regularPriceFontSize: 14, 57 | ), 58 | ) 59 | : Padding( 60 | padding: const EdgeInsets.only(top: 8), 61 | child: Text( 62 | 'Out of Stock', 63 | textAlign: TextAlign.center, 64 | style: TextStyle( 65 | color: Colors.red[600], 66 | fontSize: 18, 67 | fontWeight: FontWeight.bold, 68 | ), 69 | ), 70 | ) 71 | : Padding( 72 | padding: const EdgeInsets.only(top: 8), 73 | child: Text( 74 | 'Currently Not Purchasable', 75 | textAlign: TextAlign.center, 76 | style: TextStyle( 77 | color: Colors.red[600], 78 | fontSize: 16, 79 | fontWeight: FontWeight.bold, 80 | ), 81 | ), 82 | ), 83 | SizedBox(height: 8), 84 | this.item.data['purchasable'] != null && 85 | this.item.data['purchasable'] && 86 | this.item.data['in_stock'] 87 | ? _CartHandler(this.item) 88 | : SizedBox(height: 0), 89 | this.item.data['short_description'].length > 0 90 | ? Padding( 91 | padding: const EdgeInsets.symmetric( 92 | horizontal: 16, vertical: 24), 93 | child: ProductDescriptionText( 94 | 'About', 95 | this.item.data['short_description'], 96 | ), 97 | ) 98 | : SizedBox(height: 16), 99 | Padding( 100 | padding: const EdgeInsets.symmetric(horizontal: 16), 101 | child: ProductDescriptionText( 102 | 'Description', 103 | this.item.data['description'], 104 | ), 105 | ), 106 | SizedBox(height: 16), 107 | ], 108 | ), 109 | ); 110 | } 111 | } 112 | 113 | class _CartHandler extends StatefulWidget { 114 | final Product item; 115 | _CartHandler(this.item); 116 | @override 117 | __CartHandlerState createState() => __CartHandlerState(); 118 | } 119 | 120 | class __CartHandlerState extends State<_CartHandler> { 121 | int quantity; 122 | 123 | @override 124 | initState() { 125 | quantity = 1; 126 | super.initState(); 127 | } 128 | 129 | double getTotalPrice() { 130 | try { 131 | return this.quantity * double.parse(widget.item.data['price']) ?? 0; 132 | } catch (err) { 133 | print('error getting price'); 134 | return 0; 135 | } 136 | } 137 | 138 | @override 139 | Widget build(BuildContext context) { 140 | return Consumer( 141 | builder: (context, cart, child) { 142 | bool isInCart = cart.isPresentInCart(widget.item); 143 | if (isInCart) this.quantity = widget.item.quantity; 144 | return Column( 145 | children: [ 146 | Row( 147 | mainAxisAlignment: MainAxisAlignment.spaceEvenly, 148 | children: [ 149 | Container( 150 | child: Row( 151 | children: [ 152 | IconButton( 153 | iconSize: 28, 154 | icon: Icon(Icons.remove), 155 | onPressed: () => isInCart 156 | ? cart.decrementQuantityOfProduct(widget.item) 157 | : setState(() => this.quantity = 158 | this.quantity > 1 ? --this.quantity : 1), 159 | ), 160 | Text( 161 | '${isInCart ? widget.item.quantity : this.quantity}', 162 | style: TextStyle( 163 | fontSize: 18, fontWeight: FontWeight.bold), 164 | ), 165 | IconButton( 166 | icon: Icon(Icons.add), 167 | iconSize: 28, 168 | onPressed: () => isInCart 169 | ? cart.incrementQuantityOfProduct(widget.item) 170 | : setState(() => this.quantity++), 171 | ) 172 | ], 173 | ), 174 | ), 175 | !isInCart 176 | ? RaisedButton( 177 | onPressed: () { 178 | cart.addCartItem(widget.item, 179 | quantity: this.quantity); 180 | Scaffold.of(context).showSnackBar( 181 | SnackBar(content: Text('Item added to cart'))); 182 | }, 183 | child: Text( 184 | 'Add to Cart', 185 | style: TextStyle(color: Colors.white, fontSize: 15), 186 | ), 187 | ) 188 | : RaisedButton( 189 | color: Colors.red[400], 190 | onPressed: () { 191 | cart.removeCartItem(widget.item); 192 | Scaffold.of(context).showSnackBar(SnackBar( 193 | content: Text('Item removed from cart'))); 194 | }, 195 | child: Text( 196 | 'Remove from Cart', 197 | style: TextStyle(color: Colors.white, fontSize: 15), 198 | ), 199 | ), 200 | ], 201 | ), 202 | SizedBox(height: 10), 203 | Row( 204 | mainAxisAlignment: MainAxisAlignment.center, 205 | crossAxisAlignment: CrossAxisAlignment.end, 206 | children: [ 207 | Text( 208 | 'Total Amount - ', 209 | style: TextStyle(fontWeight: FontWeight.bold, fontSize: 18), 210 | ), 211 | Text( 212 | '₹ ${getTotalPrice()}', 213 | style: TextStyle( 214 | fontWeight: FontWeight.bold, 215 | fontSize: 16, 216 | color: Colors.green[700], 217 | ), 218 | ), 219 | ], 220 | ) 221 | ], 222 | ); 223 | }, 224 | ); 225 | } 226 | } 227 | -------------------------------------------------------------------------------- /lib/pages/account/AddressDetails.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:grodudes/helper/Constants.dart'; 3 | import 'package:grodudes/state/user_state.dart'; 4 | import 'package:provider/provider.dart'; 5 | 6 | class AddressDetails extends StatefulWidget { 7 | final String title; 8 | final String type; 9 | final Future Function(Map) updateCb; 10 | AddressDetails({ 11 | @required this.title, 12 | @required this.type, 13 | @required this.updateCb, 14 | }); 15 | @override 16 | _AddressDetailsState createState() => _AddressDetailsState(); 17 | } 18 | 19 | class _AddressDetailsState extends State { 20 | Map _textControllers; 21 | bool _isLoading; 22 | 23 | @override 24 | void initState() { 25 | this._textControllers = {}; 26 | _isLoading = false; 27 | super.initState(); 28 | } 29 | 30 | @override 31 | void dispose() { 32 | this._textControllers.values.forEach((element) { 33 | element.dispose(); 34 | }); 35 | super.dispose(); 36 | } 37 | 38 | capitalize(String str) { 39 | return '${str[0].toUpperCase()}${str.substring(1)}'; 40 | } 41 | 42 | Map formatData() { 43 | Map data = {}; 44 | this._textControllers.keys.forEach((key) { 45 | data[key] = this._textControllers[key].text; 46 | }); 47 | return data; 48 | } 49 | 50 | String _formatName(String name) { 51 | return name.split('_').map((e) => capitalize(e)).join(' '); 52 | } 53 | 54 | List> _getMenuItems(String value) { 55 | List> menuItems = pincodes 56 | .map( 57 | (e) => DropdownMenuItem( 58 | child: Text(e), 59 | value: e, 60 | ), 61 | ) 62 | .toList(); 63 | 64 | if (pincodes.contains(value)) return menuItems; 65 | menuItems.add(DropdownMenuItem( 66 | child: Text('Not Specified'), 67 | value: value, 68 | )); 69 | return menuItems; 70 | } 71 | 72 | final headerTextStyle = TextStyle(fontSize: 18, fontWeight: FontWeight.bold); 73 | @override 74 | Widget build(BuildContext context) { 75 | return Consumer( 76 | builder: (context, user, child) { 77 | Map address = user.wcUserInfo[widget.type]; 78 | // print(address); 79 | if (address == null || address.length == 0) { 80 | return Scaffold( 81 | appBar: AppBar(title: Text(widget.title)), 82 | body: Center(child: Text('Address not found')), 83 | ); 84 | } 85 | return Scaffold( 86 | appBar: AppBar( 87 | title: Text(this.widget.title), 88 | centerTitle: true, 89 | ), 90 | body: Stack( 91 | children: [ 92 | ListView.builder( 93 | itemCount: address.keys.length, 94 | scrollDirection: Axis.vertical, 95 | itemBuilder: (context, index) { 96 | final key = address.keys.elementAt(index); 97 | final text = address[key]; 98 | 99 | TextEditingController _controller; 100 | _controller = TextEditingController(text: text); 101 | this._textControllers[key] = _controller; 102 | 103 | return Container( 104 | padding: EdgeInsets.symmetric(vertical: 10, horizontal: 4), 105 | child: Row( 106 | children: [ 107 | SizedBox(width: 6), 108 | Expanded( 109 | flex: 3, 110 | child: Text( 111 | _formatName(address.keys.elementAt(index)), 112 | style: headerTextStyle, 113 | ), 114 | ), 115 | SizedBox(width: 12), 116 | Expanded( 117 | flex: 7, 118 | child: address.keys.elementAt(index) == 'postcode' && 119 | widget.type == 'shipping' 120 | ? DropdownButtonFormField( 121 | value: _controller.text, 122 | items: >[ 123 | ..._getMenuItems(_controller.text) 124 | ], 125 | onChanged: (value) => 126 | _controller.text = value, 127 | decoration: InputDecoration( 128 | focusedBorder: OutlineInputBorder( 129 | borderSide: BorderSide( 130 | color: GrodudesPrimaryColor 131 | .primaryColor[600], 132 | width: 2)), 133 | border: OutlineInputBorder( 134 | borderSide: 135 | BorderSide(color: Colors.blue)), 136 | contentPadding: EdgeInsets.symmetric( 137 | horizontal: 6, vertical: 2), 138 | ), 139 | style: TextStyle( 140 | fontSize: 16, color: Colors.black), 141 | ) 142 | : TextField( 143 | controller: _controller, 144 | decoration: InputDecoration( 145 | focusedBorder: OutlineInputBorder( 146 | borderSide: BorderSide( 147 | color: GrodudesPrimaryColor 148 | .primaryColor[600], 149 | width: 2)), 150 | border: OutlineInputBorder( 151 | borderSide: 152 | BorderSide(color: Colors.blue)), 153 | contentPadding: EdgeInsets.symmetric( 154 | horizontal: 6, vertical: 2), 155 | ), 156 | style: TextStyle(fontSize: 16), 157 | ), 158 | ), 159 | SizedBox(width: 6), 160 | ], 161 | ), 162 | ); 163 | }, 164 | ), 165 | _isLoading 166 | ? Container( 167 | color: Colors.white, 168 | child: Center(child: CircularProgressIndicator())) 169 | : SizedBox(height: 0, width: 0) 170 | ], 171 | ), 172 | bottomNavigationBar: Builder(builder: (context) { 173 | return Container( 174 | height: 45, 175 | child: Row( 176 | mainAxisAlignment: MainAxisAlignment.end, 177 | crossAxisAlignment: CrossAxisAlignment.stretch, 178 | children: [ 179 | Expanded( 180 | child: RaisedButton( 181 | shape: RoundedRectangleBorder( 182 | borderRadius: BorderRadius.zero, 183 | side: BorderSide( 184 | color: GrodudesPrimaryColor.primaryColor[700], 185 | width: 2, 186 | ), 187 | ), 188 | color: Colors.white, 189 | child: Text( 190 | 'Cancel', 191 | style: TextStyle(color: Colors.black87, fontSize: 16), 192 | ), 193 | onPressed: () => Navigator.pop(context), 194 | ), 195 | ), 196 | Expanded( 197 | child: RaisedButton( 198 | textColor: Colors.white, 199 | shape: RoundedRectangleBorder( 200 | borderRadius: BorderRadius.circular(0)), 201 | child: Text( 202 | 'Update', 203 | style: TextStyle( 204 | fontWeight: FontWeight.bold, fontSize: 16), 205 | ), 206 | onPressed: () async { 207 | this.setState(() { 208 | _isLoading = true; 209 | }); 210 | String msg; 211 | msg = await this 212 | .widget 213 | .updateCb(formatData()) 214 | .catchError((err) { 215 | print(err); 216 | msg = null; 217 | }); 218 | this.setState(() { 219 | _isLoading = false; 220 | }); 221 | Scaffold.of(context).showSnackBar( 222 | SnackBar(content: Text(msg ?? 'An error occured'))); 223 | }, 224 | ), 225 | ), 226 | ], 227 | ), 228 | ); 229 | }), 230 | ); 231 | }, 232 | ); 233 | } 234 | } 235 | -------------------------------------------------------------------------------- /lib/pages/checkout/ConfirmationPage.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/cupertino.dart'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:grodudes/helper/Constants.dart'; 4 | import 'package:grodudes/pages/checkout/AddressUpdatePage.dart'; 5 | import 'package:grodudes/pages/checkout/OrderPlacementPage.dart'; 6 | import 'package:grodudes/state/cart_state.dart'; 7 | import 'package:grodudes/state/user_state.dart'; 8 | import 'package:provider/provider.dart'; 9 | 10 | class ConfirmationPage extends StatefulWidget { 11 | @override 12 | _ConfirmationPageState createState() => _ConfirmationPageState(); 13 | } 14 | 15 | class _ConfirmationPageState extends State { 16 | Map shippingAddress; 17 | Map billingAddress; 18 | UserManager userManager; 19 | TextEditingController _emailController; 20 | TextEditingController _phoneController; 21 | 22 | //styles 23 | final _textFieldDecoration = InputDecoration( 24 | labelStyle: TextStyle(color: GrodudesPrimaryColor.primaryColor[600]), 25 | alignLabelWithHint: true, 26 | focusedBorder: OutlineInputBorder( 27 | borderSide: BorderSide( 28 | color: GrodudesPrimaryColor.primaryColor[600], width: 2)), 29 | border: OutlineInputBorder(borderSide: BorderSide(color: Colors.blue)), 30 | contentPadding: EdgeInsets.symmetric(horizontal: 6, vertical: 2), 31 | ); 32 | final _headerTextStyle = TextStyle(fontSize: 17, fontWeight: FontWeight.bold); 33 | 34 | @override 35 | void initState() { 36 | this.userManager = Provider.of(context, listen: false); 37 | if (userManager.isLoggedIn() && this.userManager.wcUserInfo['id'] != null) { 38 | this.shippingAddress = new Map.from(userManager.wcUserInfo['shipping']); 39 | this.billingAddress = new Map.from(userManager.wcUserInfo['billing']); 40 | 41 | String wordpressEmail = userManager.wpUserInfo['user_email'] ?? ''; 42 | String email = wordpressEmail; 43 | String woocommerceEmail = billingAddress['email']; 44 | String phone = billingAddress['phone'] ?? ''; 45 | 46 | if (woocommerceEmail != null && woocommerceEmail.length > 0) { 47 | email = woocommerceEmail; 48 | } 49 | 50 | this._emailController = TextEditingController(text: email); 51 | this._phoneController = TextEditingController(text: phone); 52 | } 53 | 54 | super.initState(); 55 | } 56 | 57 | @override 58 | void dispose() { 59 | if (this._emailController != null) this._emailController.dispose(); 60 | if (this._phoneController != null) this._phoneController.dispose(); 61 | super.dispose(); 62 | } 63 | 64 | _capitalize(String str) { 65 | return '${str[0].toUpperCase()}${str.substring(1)}'; 66 | } 67 | 68 | @override 69 | Widget build(BuildContext context) { 70 | if (this.userManager.isLoggedIn() == false || 71 | this.userManager.wcUserInfo['id'] == null) { 72 | return Scaffold( 73 | backgroundColor: Colors.white, 74 | appBar: AppBar( 75 | title: Text('Confirm your details'), 76 | centerTitle: true, 77 | ), 78 | body: Center( 79 | child: Text('Please login to proceed to checkout'), 80 | ), 81 | ); 82 | } 83 | return Scaffold( 84 | backgroundColor: Colors.white, 85 | appBar: AppBar( 86 | title: Text('Confirm your details'), 87 | centerTitle: true, 88 | ), 89 | body: ListView( 90 | padding: EdgeInsets.all(8), 91 | children: [ 92 | SizedBox(height: 6), 93 | TextField( 94 | controller: this._emailController, 95 | keyboardType: TextInputType.emailAddress, 96 | decoration: this._textFieldDecoration.copyWith( 97 | labelText: 'Email', 98 | hintText: 'Enter your email address', 99 | prefixIcon: Icon(Icons.email, color: Colors.green[700]), 100 | ), 101 | ), 102 | SizedBox(height: 16), 103 | SizedBox(height: 6), 104 | TextField( 105 | controller: this._phoneController, 106 | keyboardType: TextInputType.numberWithOptions( 107 | decimal: false, 108 | signed: false, 109 | ), 110 | decoration: this._textFieldDecoration.copyWith( 111 | labelText: 'Phone Number', 112 | hintText: 'Enter your phone number', 113 | prefixIcon: 114 | Icon(Icons.phone_android, color: Colors.green[700]), 115 | ), 116 | ), 117 | SizedBox(height: 16), 118 | Row( 119 | children: [ 120 | Text('Shipping Address', style: this._headerTextStyle), 121 | IconButton( 122 | icon: Icon(Icons.edit, color: Colors.green), 123 | onPressed: () => Navigator.push( 124 | context, 125 | CupertinoPageRoute( 126 | builder: (context) => AddressUpdatePage( 127 | this.shippingAddress, 128 | (newAddress) { 129 | setState(() => this.shippingAddress = newAddress); 130 | // print(this.shippingAddress); 131 | }, 132 | shouldDisplayPostcodeDropdown: true, 133 | ), 134 | ), 135 | ), 136 | ) 137 | ], 138 | ), 139 | // SizedBox(height: 8), 140 | ...this.shippingAddress.entries.map( 141 | (e) => Row( 142 | children: [ 143 | Expanded( 144 | flex: 3, 145 | child: Text( 146 | e.key.split('_').map((e) => _capitalize(e)).join(' '), 147 | style: TextStyle( 148 | fontWeight: FontWeight.bold, fontSize: 15), 149 | ), 150 | ), 151 | SizedBox(width: 8), 152 | Expanded( 153 | flex: 7, 154 | child: Text( 155 | e.value, 156 | style: TextStyle(fontSize: 15), 157 | ), 158 | ), 159 | ], 160 | ), 161 | ), 162 | SizedBox(height: 16), 163 | Row( 164 | children: [ 165 | Text('Billing Address', style: this._headerTextStyle), 166 | IconButton( 167 | icon: Icon(Icons.edit, color: Colors.green), 168 | onPressed: () => Navigator.push( 169 | context, 170 | CupertinoPageRoute( 171 | builder: (context) => AddressUpdatePage( 172 | this.billingAddress, 173 | (newAddress) { 174 | setState(() => this.billingAddress = newAddress); 175 | // print(this.billingAddress); 176 | }, 177 | shouldDisplayPostcodeDropdown: false, 178 | ), 179 | ), 180 | ), 181 | ) 182 | ], 183 | ), 184 | // SizedBox(height: 8), 185 | ...this.billingAddress.entries.map( 186 | (e) => Row( 187 | children: [ 188 | Expanded( 189 | flex: 3, 190 | child: Text( 191 | e.key.split('_').map((e) => _capitalize(e)).join(' '), 192 | style: TextStyle( 193 | fontWeight: FontWeight.bold, fontSize: 15), 194 | ), 195 | ), 196 | SizedBox(width: 8), 197 | Expanded( 198 | flex: 7, 199 | child: Text( 200 | e.value, 201 | style: TextStyle(fontSize: 15), 202 | ), 203 | ), 204 | ], 205 | ), 206 | ), 207 | Builder(builder: (context) { 208 | return Center( 209 | child: RaisedButton( 210 | child: 211 | Text('Place Order', style: TextStyle(color: Colors.white)), 212 | shape: RoundedRectangleBorder( 213 | borderRadius: BorderRadius.circular(4)), 214 | padding: EdgeInsets.symmetric(vertical: 12, horizontal: 32), 215 | onPressed: () { 216 | if (this._emailController.text.length == 0 || 217 | this._phoneController.text.length == 0) { 218 | Scaffold.of(context).showSnackBar(SnackBar( 219 | content: Text( 220 | 'Please fill out your Email and Phone number'))); 221 | return; 222 | } 223 | if (this._emailController.text.contains('@') && 224 | billingAddress['email'] != this._emailController.text) { 225 | billingAddress['email'] = this._emailController.text; 226 | } 227 | if (this._phoneController.text.indexOf(RegExp(r'[,. ]')) == 228 | -1 && 229 | billingAddress['phone'] != this._phoneController.text) { 230 | billingAddress['phone'] = this._phoneController.text; 231 | } 232 | Navigator.pushReplacement( 233 | context, 234 | CupertinoPageRoute( 235 | builder: (context) => OrderPlacementPage( 236 | customerId: userManager.wcUserInfo['id'], 237 | billingAddress: this.billingAddress, 238 | shippingAddress: this.shippingAddress, 239 | items: Provider.of(context).cartItems, 240 | ), 241 | ), 242 | ); 243 | }, 244 | ), 245 | ); 246 | }) 247 | ], 248 | ), 249 | ); 250 | } 251 | } 252 | -------------------------------------------------------------------------------- /assets/fonts/LICENSE.txt: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | --------------------------------------------------------------------------------