├── ios ├── 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 │ ├── GoogleService-Info.plist │ ├── Base.lproj │ │ ├── Main.storyboard │ │ └── LaunchScreen.storyboard │ └── Info.plist ├── Flutter │ ├── Debug.xcconfig │ ├── Release.xcconfig │ └── AppFrameworkInfo.plist ├── Runner.xcodeproj │ ├── project.xcworkspace │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata │ │ │ ├── WorkspaceSettings.xcsettings │ │ │ └── IDEWorkspaceChecks.plist │ └── xcshareddata │ │ └── xcschemes │ │ └── Runner.xcscheme ├── Runner.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ ├── WorkspaceSettings.xcsettings │ │ └── IDEWorkspaceChecks.plist └── .gitignore ├── .firebaserc ├── 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 │ │ │ │ ├── values │ │ │ │ │ ├── strings.xml │ │ │ │ │ └── styles.xml │ │ │ │ └── drawable │ │ │ │ │ └── launch_background.xml │ │ │ ├── kotlin │ │ │ │ └── br │ │ │ │ │ └── com │ │ │ │ │ └── ciolfi │ │ │ │ │ └── lojavirtual │ │ │ │ │ └── MainActivity.kt │ │ │ └── AndroidManifest.xml │ │ ├── debug │ │ │ └── AndroidManifest.xml │ │ └── profile │ │ │ └── AndroidManifest.xml │ ├── google-services.json │ └── build.gradle ├── gradle │ └── wrapper │ │ └── gradle-wrapper.properties ├── settings.gradle └── build.gradle ├── functions ├── .gitignore ├── tsconfig.json ├── package.json └── tslint.json ├── firebase.json ├── analysis_options.yaml ├── README.md ├── lib ├── helpers │ ├── extensions.dart │ ├── validators.dart │ └── firebase_errors.dart ├── models │ ├── page_manager.dart │ ├── section_item.dart │ ├── item_size.dart │ ├── credit_card.dart │ ├── stores_manager.dart │ ├── orders_manager.dart │ ├── address.dart │ ├── admin_users_manager.dart │ ├── cepaberto_address.dart │ ├── product_manager.dart │ ├── user.dart │ ├── home_manager.dart │ ├── admin_orders_manager.dart │ ├── cart_product.dart │ ├── store.dart │ ├── order.dart │ ├── section.dart │ ├── user_manager.dart │ ├── checkout_manager.dart │ └── product.dart ├── common │ ├── custom_icon_button.dart │ ├── empty_card.dart │ ├── custom_drawer │ │ ├── drawer_tile.dart │ │ ├── custom_drawer_header.dart │ │ └── custom_drawer.dart │ ├── order │ │ ├── export_address_dialog.dart │ │ ├── cancel_order_dialog.dart │ │ ├── order_product_tile.dart │ │ └── order_tile.dart │ ├── login_card.dart │ └── price_card.dart ├── services │ ├── cepaberto_service.dart │ └── cielo_payment.dart └── screens │ ├── address │ ├── address_screen.dart │ └── components │ │ ├── address_card.dart │ │ └── cep_input_field.dart │ ├── home │ ├── components │ │ ├── add_section_widget.dart │ │ ├── add_tile_widget.dart │ │ ├── section_list.dart │ │ ├── section_staggered.dart │ │ ├── section_header.dart │ │ └── item_tile.dart │ └── home_screen.dart │ ├── stores │ └── stores_screen.dart │ ├── select_product │ └── select_product_screen.dart │ ├── products │ ├── components │ │ ├── search_dialog.dart │ │ └── product_list_tile.dart │ └── products_screen.dart │ ├── orders │ └── orders_screen.dart │ ├── cart │ ├── cart_screen.dart │ └── components │ │ └── cart_tile.dart │ ├── product │ └── components │ │ └── size_widget.dart │ ├── checkout │ ├── components │ │ ├── cpf_field.dart │ │ ├── card_back.dart │ │ ├── credit_card_widget.dart │ │ ├── card_text_field.dart │ │ └── card_front.dart │ └── checkout_screen.dart │ ├── confirmation │ └── confirmation_screen.dart │ ├── admin_users │ └── admin_users_screen.dart │ ├── edit_product │ └── components │ │ ├── edit_item_size.dart │ │ ├── sizes_form.dart │ │ ├── image_source_sheet.dart │ │ └── images_form.dart │ ├── base │ └── base_screen.dart │ └── admin_orders │ └── admin_orders_screen.dart ├── .metadata ├── .gitignore ├── test └── widget_test.dart └── pubspec.yaml /ios/Runner/Runner-Bridging-Header.h: -------------------------------------------------------------------------------- 1 | #import "GeneratedPluginRegistrant.h" 2 | -------------------------------------------------------------------------------- /.firebaserc: -------------------------------------------------------------------------------- 1 | { 2 | "projects": { 3 | "default": "loja-virtual-557ed" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /ios/Flutter/Debug.xcconfig: -------------------------------------------------------------------------------- 1 | #include "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" 2 | #include "Generated.xcconfig" 3 | -------------------------------------------------------------------------------- /android/gradle.properties: -------------------------------------------------------------------------------- 1 | org.gradle.jvmargs=-Xmx1536M 2 | android.enableR8=true 3 | android.useAndroidX=true 4 | android.enableJetifier=true 5 | -------------------------------------------------------------------------------- /ios/Flutter/Release.xcconfig: -------------------------------------------------------------------------------- 1 | #include "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" 2 | #include "Generated.xcconfig" 3 | -------------------------------------------------------------------------------- /android/.gitignore: -------------------------------------------------------------------------------- 1 | gradle-wrapper.jar 2 | /.gradle 3 | /captures/ 4 | /gradlew 5 | /gradlew.bat 6 | /local.properties 7 | GeneratedPluginRegistrant.java 8 | -------------------------------------------------------------------------------- /functions/.gitignore: -------------------------------------------------------------------------------- 1 | ## Compiled JavaScript files 2 | **/*.js 3 | **/*.js.map 4 | 5 | # Typescript v1 declaration files 6 | typings/ 7 | 8 | node_modules/ -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dhciolfi/loja_virtual/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/Dhciolfi/loja_virtual/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/Dhciolfi/loja_virtual/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/Dhciolfi/loja_virtual/HEAD/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dhciolfi/loja_virtual/HEAD/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dhciolfi/loja_virtual/HEAD/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png -------------------------------------------------------------------------------- /firebase.json: -------------------------------------------------------------------------------- 1 | { 2 | "functions": { 3 | "predeploy": [ 4 | "npm --prefix \"$RESOURCE_DIR\" run lint", 5 | "npm --prefix \"$RESOURCE_DIR\" run build" 6 | ] 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dhciolfi/loja_virtual/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/Dhciolfi/loja_virtual/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/Dhciolfi/loja_virtual/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/Dhciolfi/loja_virtual/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/Dhciolfi/loja_virtual/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/Dhciolfi/loja_virtual/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/Dhciolfi/loja_virtual/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/Dhciolfi/loja_virtual/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/Dhciolfi/loja_virtual/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/Dhciolfi/loja_virtual/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/Dhciolfi/loja_virtual/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/Dhciolfi/loja_virtual/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/Dhciolfi/loja_virtual/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dhciolfi/loja_virtual/HEAD/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dhciolfi/loja_virtual/HEAD/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png -------------------------------------------------------------------------------- /analysis_options.yaml: -------------------------------------------------------------------------------- 1 | include: package:lint/analysis_options.yaml 2 | 3 | linter: 4 | rules: 5 | curly_braces_in_flow_control_structures: false 6 | use_setters_to_change_properties: false -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dhciolfi/loja_virtual/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/Dhciolfi/loja_virtual/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png -------------------------------------------------------------------------------- /android/app/src/main/kotlin/br/com/ciolfi/lojavirtual/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package br.com.ciolfi.lojavirtual 2 | 3 | import io.flutter.embedding.android.FlutterActivity 4 | 5 | class MainActivity: FlutterActivity() { 6 | } 7 | -------------------------------------------------------------------------------- /ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Loja Virtual - Flutter 2 | 3 | Aplicativo ensinado passo a passo no curso **Crie uma Loja Virtual Completa - Android e iOS com Flutter**. 4 | 5 | Link para o curso: [Udemy](https://www.udemy.com/course/lojaflutter/?referralCode=B19C11FE71DF8A71D84B) 6 | -------------------------------------------------------------------------------- /lib/helpers/extensions.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | extension Extra on TimeOfDay { 4 | 5 | String formatted(){ 6 | return '${hour}h${minute.toString().padLeft(2, '0')}'; 7 | } 8 | 9 | int toMinutes() => hour*60 + minute; 10 | 11 | } -------------------------------------------------------------------------------- /lib/helpers/validators.dart: -------------------------------------------------------------------------------- 1 | bool emailValid(String email){ 2 | final RegExp regex = RegExp( 3 | r"^(([^<>()[\]\\.,;:\s@\']+(\.[^<>()[\]\\.,;:\s@\']+)*)|(\'.+\'))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$"); 4 | return regex.hasMatch(email); 5 | } -------------------------------------------------------------------------------- /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/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PreviewsEnabled 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /android/app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Loja do Daniel 5 | 6 | 705074733663048 7 | 8 | fb705074733663048 9 | 10 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /functions/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "noImplicitReturns": true, 5 | "noUnusedLocals": true, 6 | "outDir": "lib", 7 | "sourceMap": true, 8 | "strict": true, 9 | "target": "es2017" 10 | }, 11 | "compileOnSave": true, 12 | "include": [ 13 | "src" 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /.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: e6b34c2b5c96bb95325269a29a84e83ed8909b5f 8 | channel: stable 9 | 10 | project_type: app 11 | -------------------------------------------------------------------------------- /lib/models/page_manager.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/cupertino.dart'; 2 | 3 | class PageManager { 4 | 5 | PageManager(this._pageController); 6 | 7 | final PageController _pageController; 8 | 9 | int page = 0; 10 | 11 | void setPage(int value){ 12 | if(value == page) return; 13 | page = value; 14 | _pageController.jumpToPage(value); 15 | } 16 | } -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /android/settings.gradle: -------------------------------------------------------------------------------- 1 | include ':app' 2 | 3 | def flutterProjectRoot = rootProject.projectDir.parentFile.toPath() 4 | 5 | def plugins = new Properties() 6 | def pluginsFile = new File(flutterProjectRoot.toFile(), '.flutter-plugins') 7 | if (pluginsFile.exists()) { 8 | pluginsFile.withReader('UTF-8') { reader -> plugins.load(reader) } 9 | } 10 | 11 | plugins.each { name, path -> 12 | def pluginDirectory = flutterProjectRoot.resolve(path).resolve('android').toFile() 13 | include ":$name" 14 | project(":$name").projectDir = pluginDirectory 15 | } 16 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /lib/models/section_item.dart: -------------------------------------------------------------------------------- 1 | class SectionItem { 2 | 3 | SectionItem({this.image, this.product}); 4 | 5 | SectionItem.fromMap(Map map){ 6 | image = map['image'] as String; 7 | product = map['product'] as String; 8 | } 9 | 10 | dynamic image; 11 | String product; 12 | 13 | SectionItem clone(){ 14 | return SectionItem( 15 | image: image, 16 | product: product, 17 | ); 18 | } 19 | 20 | Map toMap(){ 21 | return { 22 | 'image': image, 23 | 'product': product, 24 | }; 25 | } 26 | 27 | @override 28 | String toString() { 29 | return 'SectionItem{image: $image, product: $product}'; 30 | } 31 | } -------------------------------------------------------------------------------- /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.google.gms:google-services:4.3.3' 10 | classpath 'com.android.tools.build:gradle:3.5.0' 11 | classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" 12 | } 13 | } 14 | 15 | allprojects { 16 | repositories { 17 | google() 18 | jcenter() 19 | } 20 | } 21 | 22 | rootProject.buildDir = '../build' 23 | subprojects { 24 | project.buildDir = "${rootProject.buildDir}/${project.name}" 25 | } 26 | subprojects { 27 | project.evaluationDependsOn(':app') 28 | } 29 | 30 | task clean(type: Delete) { 31 | delete rootProject.buildDir 32 | } 33 | -------------------------------------------------------------------------------- /functions/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "functions", 3 | "scripts": { 4 | "lint": "tslint --project tsconfig.json", 5 | "build": "tsc", 6 | "serve": "npm run build && firebase emulators:start --only functions", 7 | "shell": "npm run build && firebase functions:shell", 8 | "start": "npm run shell", 9 | "deploy": "firebase deploy --only functions", 10 | "logs": "firebase functions:log" 11 | }, 12 | "engines": { 13 | "node": "10" 14 | }, 15 | "main": "lib/index.js", 16 | "dependencies": { 17 | "cielo": "^2.3.0", 18 | "firebase-admin": "^8.10.0", 19 | "firebase-functions": "^3.6.1" 20 | }, 21 | "devDependencies": { 22 | "tslint": "^5.12.0", 23 | "typescript": "^3.8.0", 24 | "firebase-functions-test": "^0.2.0" 25 | }, 26 | "private": true 27 | } 28 | -------------------------------------------------------------------------------- /lib/models/item_size.dart: -------------------------------------------------------------------------------- 1 | class ItemSize { 2 | 3 | ItemSize({this.name, this.price, this.stock}); 4 | 5 | ItemSize.fromMap(Map map){ 6 | name = map['name'] as String; 7 | price = map['price'] as num; 8 | stock = map['stock'] as int; 9 | } 10 | 11 | String name; 12 | num price; 13 | int stock; 14 | 15 | bool get hasStock => stock > 0; 16 | 17 | ItemSize clone(){ 18 | return ItemSize( 19 | name: name, 20 | price: price, 21 | stock: stock, 22 | ); 23 | } 24 | 25 | Map toMap(){ 26 | return { 27 | 'name': name, 28 | 'price': price, 29 | 'stock': stock, 30 | }; 31 | } 32 | 33 | @override 34 | String toString() { 35 | return 'ItemSize{name: $name, price: $price, stock: $stock}'; 36 | } 37 | } -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /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/common/custom_icon_button.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class CustomIconButton extends StatelessWidget { 4 | 5 | const CustomIconButton({this.iconData, this.color, this.onTap, this.size}); 6 | 7 | final IconData iconData; 8 | final Color color; 9 | final VoidCallback onTap; 10 | final double size; 11 | 12 | @override 13 | Widget build(BuildContext context) { 14 | return ClipRRect( 15 | borderRadius: BorderRadius.circular(50), 16 | child: Material( 17 | color: Colors.transparent, 18 | child: InkWell( 19 | onTap: onTap, 20 | child: Padding( 21 | padding: const EdgeInsets.all(5), 22 | child: Icon( 23 | iconData, 24 | color: onTap != null ? color : Colors.grey[400], 25 | size: size ?? 24, 26 | ), 27 | ), 28 | ), 29 | ), 30 | ); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /lib/helpers/firebase_errors.dart: -------------------------------------------------------------------------------- 1 | String getErrorString(String code){ 2 | switch (code) { 3 | case 'ERROR_WEAK_PASSWORD': 4 | return 'Sua senha é muito fraca.'; 5 | case 'ERROR_INVALID_EMAIL': 6 | return 'Seu e-mail é inválido.'; 7 | case 'ERROR_EMAIL_ALREADY_IN_USE': 8 | return 'E-mail já está sendo utilizado em outra conta.'; 9 | case 'ERROR_INVALID_CREDENTIAL': 10 | return 'Seu e-mail é inválido.'; 11 | case 'ERROR_WRONG_PASSWORD': 12 | return 'Sua senha está incorreta.'; 13 | case 'ERROR_USER_NOT_FOUND': 14 | return 'Não há usuário com este e-mail.'; 15 | case 'ERROR_USER_DISABLED': 16 | return 'Este usuário foi desabilitado.'; 17 | case 'ERROR_TOO_MANY_REQUESTS': 18 | return 'Muitas solicitações. Tente novamente mais tarde.'; 19 | case 'ERROR_OPERATION_NOT_ALLOWED': 20 | return 'Operação não permitida.'; 21 | 22 | default: 23 | return 'Um erro indefinido ocorreu.'; 24 | } 25 | } -------------------------------------------------------------------------------- /lib/services/cepaberto_service.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | 3 | import 'package:dio/dio.dart'; 4 | import 'package:lojavirtual/models/cepaberto_address.dart'; 5 | 6 | const token = 'faa665cf0cb302b6a45e6f490110e334'; 7 | 8 | class CepAbertoService { 9 | 10 | Future getAddressFromCep(String cep) async { 11 | final cleanCep = cep.replaceAll('.', '').replaceAll('-', ''); 12 | final endpoint = "https://www.cepaberto.com/api/v3/cep?cep=$cleanCep"; 13 | 14 | final Dio dio = Dio(); 15 | 16 | dio.options.headers[HttpHeaders.authorizationHeader] = 'Token token=$token'; 17 | 18 | try { 19 | final response = await dio.get>(endpoint); 20 | 21 | if(response.data.isEmpty){ 22 | return Future.error('CEP Inválido'); 23 | } 24 | 25 | final CepAbertoAddress address = CepAbertoAddress.fromMap(response.data); 26 | 27 | return address; 28 | } on DioError catch (e){ 29 | return Future.error('Erro ao buscar CEP'); 30 | } 31 | } 32 | } -------------------------------------------------------------------------------- /android/app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 15 | 18 | 19 | -------------------------------------------------------------------------------- /lib/common/empty_card.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class EmptyCard extends StatelessWidget { 4 | 5 | const EmptyCard({this.title, this.iconData}); 6 | 7 | final String title; 8 | final IconData iconData; 9 | 10 | @override 11 | Widget build(BuildContext context) { 12 | return Padding( 13 | padding: const EdgeInsets.all(16.0), 14 | child: Column( 15 | crossAxisAlignment: CrossAxisAlignment.stretch, 16 | mainAxisAlignment: MainAxisAlignment.center, 17 | children: [ 18 | Icon( 19 | iconData, 20 | size: 80.0, 21 | color: Colors.white, 22 | ), 23 | const SizedBox(height: 16.0,), 24 | Text( 25 | title, 26 | style: TextStyle( 27 | fontSize: 20.0, 28 | fontWeight: FontWeight.bold, 29 | color: Colors.white, 30 | ), 31 | textAlign: TextAlign.center, 32 | ), 33 | ], 34 | ), 35 | ); 36 | } 37 | } -------------------------------------------------------------------------------- /lib/models/credit_card.dart: -------------------------------------------------------------------------------- 1 | import 'package:credit_card_type_detector/credit_card_type_detector.dart'; 2 | 3 | class CreditCard { 4 | 5 | String number; 6 | String holder; 7 | String expirationDate; 8 | String securityCode; 9 | String brand; 10 | 11 | void setHolder(String name) => holder = name; 12 | void setExpirationDate(String date) => expirationDate = date; 13 | void setCVV(String cvv) => securityCode = cvv; 14 | void setNumber(String number) { 15 | this.number = number; 16 | brand = detectCCType(number.replaceAll(' ', '')).toString().toUpperCase().split(".").last; 17 | } 18 | 19 | Map toJson() { 20 | return { 21 | 'cardNumber': number.replaceAll(' ', ''), 22 | 'holder': holder, 23 | 'expirationDate': expirationDate, 24 | 'securityCode': securityCode, 25 | 'brand': brand, 26 | }; 27 | } 28 | 29 | @override 30 | String toString() { 31 | return 'CreditCard{number: $number, holder: $holder, expirationDate: $expirationDate, securityCode: $securityCode, brand: $brand}'; 32 | } 33 | } -------------------------------------------------------------------------------- /lib/screens/address/address_screen.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:lojavirtual/common/price_card.dart'; 3 | import 'package:lojavirtual/models/cart_manager.dart'; 4 | import 'package:lojavirtual/screens/address/components/address_card.dart'; 5 | import 'package:provider/provider.dart'; 6 | 7 | class AddressScreen extends StatelessWidget { 8 | @override 9 | Widget build(BuildContext context) { 10 | return Scaffold( 11 | appBar: AppBar( 12 | title: const Text('Entrega'), 13 | centerTitle: true, 14 | ), 15 | body: ListView( 16 | children: [ 17 | AddressCard(), 18 | Consumer( 19 | builder: (_, cartManager, __){ 20 | return PriceCard( 21 | buttonText: 'Continuar para o Pagamento', 22 | onPressed: cartManager.isAddressValid ? (){ 23 | Navigator.of(context).pushNamed('/checkout'); 24 | } : null, 25 | ); 26 | }, 27 | ), 28 | ], 29 | ), 30 | ); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /lib/screens/home/components/add_section_widget.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:lojavirtual/models/home_manager.dart'; 3 | import 'package:lojavirtual/models/section.dart'; 4 | 5 | class AddSectionWidget extends StatelessWidget { 6 | 7 | const AddSectionWidget(this.homeManager); 8 | 9 | final HomeManager homeManager; 10 | 11 | @override 12 | Widget build(BuildContext context) { 13 | return Row( 14 | children: [ 15 | Expanded( 16 | child: FlatButton( 17 | onPressed: (){ 18 | homeManager.addSection(Section(type: 'List')); 19 | }, 20 | textColor: Colors.white, 21 | child: const Text('Adicionar Lista'), 22 | ), 23 | ), 24 | Expanded( 25 | child: FlatButton( 26 | onPressed: (){ 27 | homeManager.addSection(Section(type: 'Staggered')); 28 | }, 29 | textColor: Colors.white, 30 | child: const Text('Adicionar Grade'), 31 | ), 32 | ), 33 | ], 34 | ); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /lib/models/stores_manager.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | import 'package:cloud_firestore/cloud_firestore.dart'; 4 | import 'package:flutter/cupertino.dart'; 5 | import 'package:lojavirtual/models/store.dart'; 6 | 7 | class StoresManager extends ChangeNotifier { 8 | 9 | StoresManager(){ 10 | _loadStoreList(); 11 | _startTimer(); 12 | } 13 | 14 | List stores = []; 15 | 16 | Timer _timer; 17 | 18 | final Firestore firestore = Firestore.instance; 19 | 20 | Future _loadStoreList() async { 21 | final snapshot = await firestore.collection('stores').getDocuments(); 22 | 23 | stores = snapshot.documents.map((e) => Store.fromDocument(e)).toList(); 24 | 25 | notifyListeners(); 26 | } 27 | 28 | void _startTimer(){ 29 | _timer = Timer.periodic(const Duration(minutes: 1), (timer) { 30 | _checkOpening(); 31 | }); 32 | } 33 | 34 | void _checkOpening(){ 35 | for(final store in stores) 36 | store.updateStatus(); 37 | notifyListeners(); 38 | } 39 | 40 | @override 41 | void dispose() { 42 | super.dispose(); 43 | _timer?.cancel(); 44 | } 45 | } -------------------------------------------------------------------------------- /lib/models/orders_manager.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | import 'package:cloud_firestore/cloud_firestore.dart'; 4 | import 'package:flutter/cupertino.dart'; 5 | import 'package:lojavirtual/models/order.dart'; 6 | import 'package:lojavirtual/models/user.dart'; 7 | 8 | class OrdersManager extends ChangeNotifier { 9 | 10 | User user; 11 | 12 | List orders = []; 13 | 14 | final Firestore firestore = Firestore.instance; 15 | 16 | StreamSubscription _subscription; 17 | 18 | void updateUser(User user){ 19 | this.user = user; 20 | orders.clear(); 21 | 22 | _subscription?.cancel(); 23 | if(user != null){ 24 | _listenToOrders(); 25 | } 26 | } 27 | 28 | void _listenToOrders(){ 29 | _subscription = firestore.collection('orders').where('user', isEqualTo: user.id) 30 | .snapshots().listen( 31 | (event) { 32 | orders.clear(); 33 | for(final doc in event.documents){ 34 | orders.add(Order.fromDocument(doc)); 35 | } 36 | notifyListeners(); 37 | }); 38 | } 39 | 40 | @override 41 | void dispose() { 42 | super.dispose(); 43 | _subscription?.cancel(); 44 | } 45 | } -------------------------------------------------------------------------------- /lib/models/address.dart: -------------------------------------------------------------------------------- 1 | class Address { 2 | 3 | Address({this.street, this.number, this.complement, this.district, 4 | this.zipCode, this.city, this.state, this.lat, this.long}); 5 | 6 | String street; 7 | String number; 8 | String complement; 9 | String district; 10 | String zipCode; 11 | String city; 12 | String state; 13 | 14 | double lat; 15 | double long; 16 | 17 | Address.fromMap(Map map) { 18 | street = map['street'] as String; 19 | number = map['number'] as String; 20 | complement = map['complement'] as String; 21 | district = map['district'] as String; 22 | zipCode = map['zipCode'] as String; 23 | city = map['city'] as String; 24 | state = map['state'] as String; 25 | lat = map['lat'] as double; 26 | long = map['long'] as double; 27 | } 28 | 29 | Map toMap() { 30 | return { 31 | 'street': street, 32 | 'number': number, 33 | 'complement': complement, 34 | 'district': district, 35 | 'zipCode': zipCode, 36 | 'city': city, 37 | 'state': state, 38 | 'lat': lat, 39 | 'long': long, 40 | }; 41 | } 42 | 43 | } -------------------------------------------------------------------------------- /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:lojavirtual/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/screens/stores/stores_screen.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:lojavirtual/common/custom_drawer/custom_drawer.dart'; 3 | import 'package:lojavirtual/models/stores_manager.dart'; 4 | import 'package:lojavirtual/screens/stores/components/store_card.dart'; 5 | import 'package:provider/provider.dart'; 6 | 7 | class StoresScreen extends StatelessWidget { 8 | @override 9 | Widget build(BuildContext context) { 10 | return Scaffold( 11 | drawer: CustomDrawer(), 12 | appBar: AppBar( 13 | title: const Text('Lojas'), 14 | centerTitle: true, 15 | ), 16 | body: Consumer( 17 | builder: (_, storesManager, __){ 18 | if(storesManager.stores.isEmpty){ 19 | return LinearProgressIndicator( 20 | valueColor: AlwaysStoppedAnimation(Colors.white), 21 | backgroundColor: Colors.transparent, 22 | ); 23 | } 24 | 25 | return ListView.builder( 26 | itemCount: storesManager.stores.length, 27 | itemBuilder: (_, index){ 28 | return StoreCard(storesManager.stores[index]); 29 | }, 30 | ); 31 | }, 32 | ), 33 | ); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /lib/screens/select_product/select_product_screen.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:lojavirtual/models/product_manager.dart'; 3 | import 'package:provider/provider.dart'; 4 | 5 | class SelectProductScreen extends StatelessWidget { 6 | @override 7 | Widget build(BuildContext context) { 8 | return Scaffold( 9 | appBar: AppBar( 10 | title: const Text('Vincular Produto'), 11 | centerTitle: true, 12 | ), 13 | backgroundColor: Colors.white, 14 | body: Consumer( 15 | builder: (_, productManager, __){ 16 | return ListView.builder( 17 | itemCount: productManager.allProducts.length, 18 | itemBuilder: (_, index){ 19 | final product = productManager.allProducts[index]; 20 | return ListTile( 21 | leading: Image.network(product.images.first), 22 | title: Text(product.name), 23 | subtitle: Text('R\$ ${product.basePrice.toStringAsFixed(2)}'), 24 | onTap: (){ 25 | Navigator.of(context).pop(product); 26 | }, 27 | ); 28 | }, 29 | ); 30 | }, 31 | ), 32 | ); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /android/app/google-services.json: -------------------------------------------------------------------------------- 1 | { 2 | "project_info": { 3 | "project_number": "780684814029", 4 | "firebase_url": "https://loja-virtual-557ed.firebaseio.com", 5 | "project_id": "loja-virtual-557ed", 6 | "storage_bucket": "loja-virtual-557ed.appspot.com" 7 | }, 8 | "client": [ 9 | { 10 | "client_info": { 11 | "mobilesdk_app_id": "1:780684814029:android:78ac8ddb9f920753c76b84", 12 | "android_client_info": { 13 | "package_name": "br.com.ciolfi.lojavirtual" 14 | } 15 | }, 16 | "oauth_client": [ 17 | { 18 | "client_id": "780684814029-md8qgpu70eci2kh4lkupknfe3ivuq6mt.apps.googleusercontent.com", 19 | "client_type": 3 20 | } 21 | ], 22 | "api_key": [ 23 | { 24 | "current_key": "AIzaSyBiAOBtBfdo-8PnBjSvZQm5HxW1cZBjer4" 25 | } 26 | ], 27 | "services": { 28 | "appinvite_service": { 29 | "other_platform_oauth_client": [ 30 | { 31 | "client_id": "780684814029-md8qgpu70eci2kh4lkupknfe3ivuq6mt.apps.googleusercontent.com", 32 | "client_type": 3 33 | } 34 | ] 35 | } 36 | } 37 | } 38 | ], 39 | "configuration_version": "1" 40 | } -------------------------------------------------------------------------------- /lib/models/admin_users_manager.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | import 'package:cloud_firestore/cloud_firestore.dart'; 4 | import 'package:flutter/cupertino.dart'; 5 | import 'package:lojavirtual/models/user.dart'; 6 | import 'package:lojavirtual/models/user_manager.dart'; 7 | 8 | class AdminUsersManager extends ChangeNotifier { 9 | 10 | List users = []; 11 | 12 | final Firestore firestore = Firestore.instance; 13 | 14 | StreamSubscription _subscription; 15 | 16 | void updateUser(UserManager userManager){ 17 | _subscription?.cancel(); 18 | if(userManager.adminEnabled){ 19 | _listenToUsers(); 20 | } else { 21 | users.clear(); 22 | notifyListeners(); 23 | } 24 | } 25 | 26 | void _listenToUsers(){ 27 | _subscription = firestore.collection('users').snapshots() 28 | .listen((snapshot){ 29 | users = snapshot.documents.map((d) => User.fromDocument(d)).toList(); 30 | users.sort((a, b) => 31 | a.name.toLowerCase().compareTo(b.name.toLowerCase())); 32 | notifyListeners(); 33 | }); 34 | } 35 | 36 | List get names => users.map((e) => e.name).toList(); 37 | 38 | @override 39 | void dispose() { 40 | _subscription?.cancel(); 41 | super.dispose(); 42 | } 43 | } -------------------------------------------------------------------------------- /lib/screens/products/components/search_dialog.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class SearchDialog extends StatelessWidget { 4 | 5 | const SearchDialog(this.initialText); 6 | 7 | final String initialText; 8 | 9 | @override 10 | Widget build(BuildContext context) { 11 | return Stack( 12 | children: [ 13 | Positioned( 14 | top: 2, 15 | left: 4, 16 | right: 4, 17 | child: Card( 18 | child: TextFormField( 19 | initialValue: initialText, 20 | textInputAction: TextInputAction.search, 21 | autofocus: true, 22 | decoration: InputDecoration( 23 | border: InputBorder.none, 24 | contentPadding: const EdgeInsets.symmetric(vertical: 15), 25 | prefixIcon: IconButton( 26 | icon: Icon(Icons.arrow_back), 27 | color: Colors.grey[700], 28 | onPressed: (){ 29 | Navigator.of(context).pop(); 30 | }, 31 | ) 32 | ), 33 | onFieldSubmitted: (text){ 34 | Navigator.of(context).pop(text); 35 | }, 36 | ), 37 | ), 38 | ) 39 | ], 40 | ); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /ios/Runner/GoogleService-Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CLIENT_ID 6 | 780684814029-hbh30j59oa00781ciha3seghca58t13n.apps.googleusercontent.com 7 | REVERSED_CLIENT_ID 8 | com.googleusercontent.apps.780684814029-hbh30j59oa00781ciha3seghca58t13n 9 | API_KEY 10 | AIzaSyAl3bQSiKLvSHrBLmAftEEcJAERjz25BS0 11 | GCM_SENDER_ID 12 | 780684814029 13 | PLIST_VERSION 14 | 1 15 | BUNDLE_ID 16 | br.com.ciolfi.lojavirtual 17 | PROJECT_ID 18 | loja-virtual-557ed 19 | STORAGE_BUCKET 20 | loja-virtual-557ed.appspot.com 21 | IS_ADS_ENABLED 22 | 23 | IS_ANALYTICS_ENABLED 24 | 25 | IS_APPINVITE_ENABLED 26 | 27 | IS_GCM_ENABLED 28 | 29 | IS_SIGNIN_ENABLED 30 | 31 | GOOGLE_APP_ID 32 | 1:780684814029:ios:bec68fdaa3c488f0c76b84 33 | DATABASE_URL 34 | https://loja-virtual-557ed.firebaseio.com 35 | 36 | -------------------------------------------------------------------------------- /lib/common/custom_drawer/drawer_tile.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:lojavirtual/models/page_manager.dart'; 3 | import 'package:provider/provider.dart'; 4 | 5 | class DrawerTile extends StatelessWidget { 6 | 7 | const DrawerTile({this.iconData, this.title, this.page}); 8 | 9 | final IconData iconData; 10 | final String title; 11 | final int page; 12 | 13 | @override 14 | Widget build(BuildContext context) { 15 | final int curPage = context.watch().page; 16 | final Color primaryColor = Theme.of(context).primaryColor; 17 | 18 | return InkWell( 19 | onTap: (){ 20 | context.read().setPage(page); 21 | }, 22 | child: SizedBox( 23 | height: 60, 24 | child: Row( 25 | children: [ 26 | Padding( 27 | padding: const EdgeInsets.symmetric(horizontal: 32), 28 | child: Icon( 29 | iconData, 30 | size: 32, 31 | color: curPage == page ? primaryColor : Colors.grey[700], 32 | ), 33 | ), 34 | Text( 35 | title, 36 | style: TextStyle( 37 | fontSize: 16, 38 | color: curPage == page ? primaryColor : Colors.grey[700] 39 | ), 40 | ) 41 | ], 42 | ), 43 | ), 44 | ); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /lib/screens/orders/orders_screen.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:lojavirtual/common/custom_drawer/custom_drawer.dart'; 3 | import 'package:lojavirtual/common/empty_card.dart'; 4 | import 'package:lojavirtual/common/login_card.dart'; 5 | import 'package:lojavirtual/common/order/order_tile.dart'; 6 | import 'package:lojavirtual/models/orders_manager.dart'; 7 | import 'package:provider/provider.dart'; 8 | 9 | class OrdersScreen extends StatelessWidget { 10 | @override 11 | Widget build(BuildContext context) { 12 | return Scaffold( 13 | drawer: CustomDrawer(), 14 | appBar: AppBar( 15 | title: const Text('Meus Pedidos'), 16 | centerTitle: true, 17 | ), 18 | body: Consumer( 19 | builder: (_, ordersManager, __){ 20 | if(ordersManager.user == null){ 21 | return LoginCard(); 22 | } 23 | if(ordersManager.orders.isEmpty){ 24 | return EmptyCard( 25 | title: 'Nenhuma compra encontrada!', 26 | iconData: Icons.border_clear, 27 | ); 28 | } 29 | return ListView.builder( 30 | itemCount: ordersManager.orders.length, 31 | itemBuilder: (_, index){ 32 | return OrderTile( 33 | ordersManager.orders.reversed.toList()[index] 34 | ); 35 | } 36 | ); 37 | }, 38 | ), 39 | ); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /lib/screens/address/components/address_card.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:lojavirtual/models/address.dart'; 3 | import 'package:lojavirtual/models/cart_manager.dart'; 4 | import 'package:lojavirtual/screens/address/components/address_input_field.dart'; 5 | import 'package:lojavirtual/screens/address/components/cep_input_field.dart'; 6 | import 'package:provider/provider.dart'; 7 | 8 | class AddressCard extends StatelessWidget { 9 | 10 | @override 11 | Widget build(BuildContext context) { 12 | return Card( 13 | margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), 14 | child: Padding( 15 | padding: const EdgeInsets.fromLTRB(16, 16, 16, 4), 16 | child: Consumer( 17 | builder: (_, cartManager, __){ 18 | final address = cartManager.address ?? Address(); 19 | 20 | return Form( 21 | child: Column( 22 | crossAxisAlignment: CrossAxisAlignment.start, 23 | children: [ 24 | Text( 25 | 'Endereço de Entrega', 26 | style: TextStyle( 27 | fontWeight: FontWeight.w600, 28 | fontSize: 16, 29 | ), 30 | ), 31 | CepInputField(address), 32 | AddressInputField(address), 33 | ], 34 | ), 35 | ); 36 | }, 37 | ), 38 | ), 39 | ); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /lib/common/order/export_address_dialog.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:gallery_saver/gallery_saver.dart'; 3 | import 'package:lojavirtual/models/address.dart'; 4 | import 'package:screenshot/screenshot.dart'; 5 | 6 | class ExportAddressDialog extends StatelessWidget { 7 | 8 | ExportAddressDialog(this.address); 9 | 10 | final Address address; 11 | 12 | final ScreenshotController screenshotController = ScreenshotController(); 13 | 14 | @override 15 | Widget build(BuildContext context) { 16 | return AlertDialog( 17 | title: const Text('Endereço de Entrega'), 18 | content: Screenshot( 19 | controller: screenshotController, 20 | child: Container( 21 | padding: const EdgeInsets.all(8), 22 | color: Colors.white, 23 | child: Text( 24 | '${address.street}, ${address.number} ${address.complement}\n' 25 | '${address.district}\n' 26 | '${address.city}/${address.state}\n' 27 | '${address.zipCode}', 28 | ), 29 | ), 30 | ), 31 | contentPadding: const EdgeInsets.fromLTRB(16, 16, 16, 0), 32 | actions: [ 33 | FlatButton( 34 | onPressed: () async { 35 | Navigator.of(context).pop(); 36 | final file = await screenshotController.capture(); 37 | await GallerySaver.saveImage(file.path); 38 | }, 39 | textColor: Theme.of(context).primaryColor, 40 | child: const Text('Exportar'), 41 | ) 42 | ], 43 | ); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /lib/screens/home/components/add_tile_widget.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | import 'package:flutter/cupertino.dart'; 3 | import 'package:flutter/material.dart'; 4 | import 'package:lojavirtual/models/section.dart'; 5 | import 'package:lojavirtual/models/section_item.dart'; 6 | import 'package:lojavirtual/screens/edit_product/components/image_source_sheet.dart'; 7 | import 'package:provider/provider.dart'; 8 | 9 | class AddTileWidget extends StatelessWidget { 10 | 11 | @override 12 | Widget build(BuildContext context) { 13 | final section = context.watch
(); 14 | 15 | void onImageSelected(File file){ 16 | section.addItem(SectionItem(image: file)); 17 | Navigator.of(context).pop(); 18 | } 19 | 20 | return AspectRatio( 21 | aspectRatio: 1, 22 | child: GestureDetector( 23 | onTap: (){ 24 | if (Platform.isAndroid) { 25 | showModalBottomSheet( 26 | context: context, 27 | builder: (context) => 28 | ImageSourceSheet(onImageSelected: onImageSelected), 29 | ); 30 | } else { 31 | showCupertinoModalPopup( 32 | context: context, 33 | builder: (context) => 34 | ImageSourceSheet(onImageSelected: onImageSelected), 35 | ); 36 | } 37 | }, 38 | child: Container( 39 | color: Colors.white.withAlpha(30), 40 | child: Icon( 41 | Icons.add, 42 | color: Colors.white, 43 | ), 44 | ), 45 | ), 46 | ); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /lib/common/login_card.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class LoginCard extends StatelessWidget { 4 | @override 5 | Widget build(BuildContext context) { 6 | return Center( 7 | child: Card( 8 | margin: const EdgeInsets.all(16), 9 | child: Padding( 10 | padding: const EdgeInsets.fromLTRB(16, 4, 16, 4), 11 | child: Column( 12 | mainAxisSize: MainAxisSize.min, 13 | crossAxisAlignment: CrossAxisAlignment.stretch, 14 | children: [ 15 | Icon( 16 | Icons.account_circle, 17 | color: Theme.of(context).primaryColor, 18 | size: 100, 19 | ), 20 | Padding( 21 | padding: const EdgeInsets.all(8.0), 22 | child: Text( 23 | 'Faça login para acessar', 24 | textAlign: TextAlign.center, 25 | style: TextStyle( 26 | fontSize: 18, 27 | fontWeight: FontWeight.w800, 28 | color: Theme.of(context).primaryColor, 29 | ), 30 | ), 31 | ), 32 | RaisedButton( 33 | onPressed: (){ 34 | Navigator.of(context).pushNamed('/login'); 35 | }, 36 | color: Theme.of(context).primaryColor, 37 | textColor: Colors.white, 38 | child: const Text( 39 | 'LOGIN' 40 | ), 41 | ), 42 | ], 43 | ), 44 | ), 45 | ), 46 | ); 47 | } 48 | } -------------------------------------------------------------------------------- /lib/models/cepaberto_address.dart: -------------------------------------------------------------------------------- 1 | class CepAbertoAddress { 2 | 3 | final double altitude; 4 | final String cep; 5 | final double latitude; 6 | final double longitude; 7 | final String logradouro; 8 | final String bairro; 9 | final Cidade cidade; 10 | final Estado estado; 11 | 12 | CepAbertoAddress.fromMap(Map map) : 13 | altitude = map['altitude'] as double, 14 | cep = map['cep'] as String, 15 | latitude = double.tryParse(map['latitude'] as String), 16 | longitude = double.tryParse(map['longitude'] as String), 17 | logradouro = map['logradouro'] as String, 18 | bairro = map['bairro'] as String, 19 | cidade = Cidade.fromMap(map['cidade'] as Map), 20 | estado = Estado.fromMap(map['estado'] as Map); 21 | 22 | @override 23 | String toString() { 24 | return 'CepAbertoAddress{altitude: $altitude, cep: $cep, latitude: $latitude, longitude: $longitude, logradouro: $logradouro, bairro: $bairro, cidade: $cidade, estado: $estado}'; 25 | } 26 | } 27 | 28 | class Cidade { 29 | 30 | final int ddd; 31 | final String ibge; 32 | final String nome; 33 | 34 | Cidade.fromMap(Map map) : 35 | ddd = map['ddd'] as int, 36 | ibge = map['ibge'] as String, 37 | nome = map['nome'] as String; 38 | 39 | @override 40 | String toString() { 41 | return 'Cidade{ddd: $ddd, ibge: $ibge, nome: $nome}'; 42 | } 43 | } 44 | 45 | class Estado { 46 | 47 | final String sigla; 48 | 49 | Estado.fromMap(Map map) : 50 | sigla = map['sigla'] as String; 51 | 52 | @override 53 | String toString() { 54 | return 'Estado{sigla: $sigla}'; 55 | } 56 | } -------------------------------------------------------------------------------- /lib/screens/cart/cart_screen.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:lojavirtual/common/empty_card.dart'; 3 | import 'package:lojavirtual/common/login_card.dart'; 4 | import 'package:lojavirtual/common/price_card.dart'; 5 | import 'package:lojavirtual/models/cart_manager.dart'; 6 | import 'package:lojavirtual/screens/cart/components/cart_tile.dart'; 7 | import 'package:provider/provider.dart'; 8 | 9 | class CartScreen extends StatelessWidget { 10 | @override 11 | Widget build(BuildContext context) { 12 | return Scaffold( 13 | appBar: AppBar( 14 | title: const Text('Carrinho'), 15 | centerTitle: true, 16 | ), 17 | body: Consumer( 18 | builder: (_, cartManager, __){ 19 | if(cartManager.user == null){ 20 | return LoginCard(); 21 | } 22 | 23 | if(cartManager.items.isEmpty){ 24 | return EmptyCard( 25 | iconData: Icons.remove_shopping_cart, 26 | title: 'Nenhum produto no carrinho!', 27 | ); 28 | } 29 | 30 | return ListView( 31 | children: [ 32 | Column( 33 | children: cartManager.items.map( 34 | (cartProduct) => CartTile(cartProduct) 35 | ).toList(), 36 | ), 37 | PriceCard( 38 | buttonText: 'Continuar para Entrega', 39 | onPressed: cartManager.isCartValid ? (){ 40 | Navigator.of(context).pushNamed('/address'); 41 | } : null, 42 | ), 43 | ], 44 | ); 45 | }, 46 | ), 47 | ); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /lib/models/product_manager.dart: -------------------------------------------------------------------------------- 1 | import 'package:cloud_firestore/cloud_firestore.dart'; 2 | import 'package:flutter/cupertino.dart'; 3 | import 'package:lojavirtual/models/product.dart'; 4 | 5 | class ProductManager extends ChangeNotifier{ 6 | 7 | ProductManager(){ 8 | _loadAllProducts(); 9 | } 10 | 11 | final Firestore firestore = Firestore.instance; 12 | 13 | List allProducts = []; 14 | 15 | String _search = ''; 16 | 17 | String get search => _search; 18 | set search(String value){ 19 | _search = value; 20 | notifyListeners(); 21 | } 22 | 23 | List get filteredProducts { 24 | final List filteredProducts = []; 25 | 26 | if(search.isEmpty){ 27 | filteredProducts.addAll(allProducts); 28 | } else { 29 | filteredProducts.addAll( 30 | allProducts.where( 31 | (p) => p.name.toLowerCase().contains(search.toLowerCase()) 32 | ) 33 | ); 34 | } 35 | 36 | return filteredProducts; 37 | } 38 | 39 | Future _loadAllProducts() async { 40 | final QuerySnapshot snapProducts = 41 | await firestore.collection('products') 42 | .where('deleted', isEqualTo: false).getDocuments(); 43 | 44 | allProducts = snapProducts.documents.map( 45 | (d) => Product.fromDocument(d)).toList(); 46 | 47 | notifyListeners(); 48 | } 49 | 50 | Product findProductById(String id){ 51 | try { 52 | return allProducts.firstWhere((p) => p.id == id); 53 | } catch (e){ 54 | return null; 55 | } 56 | } 57 | 58 | void update(Product product){ 59 | allProducts.removeWhere((p) => p.id == product.id); 60 | allProducts.add(product); 61 | notifyListeners(); 62 | } 63 | 64 | void delete(Product product){ 65 | product.delete(); 66 | allProducts.removeWhere((p) => p.id == product.id); 67 | notifyListeners(); 68 | } 69 | } -------------------------------------------------------------------------------- /lib/screens/product/components/size_widget.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:lojavirtual/models/item_size.dart'; 3 | import 'package:lojavirtual/models/product.dart'; 4 | import 'package:provider/provider.dart'; 5 | 6 | class SizeWidget extends StatelessWidget { 7 | 8 | const SizeWidget({this.size}); 9 | 10 | final ItemSize size; 11 | 12 | @override 13 | Widget build(BuildContext context) { 14 | final product = context.watch(); 15 | final selected = size == product.selectedSize; 16 | 17 | Color color; 18 | if(!size.hasStock) 19 | color = Colors.red.withAlpha(50); 20 | else if(selected) 21 | color = Theme.of(context).primaryColor; 22 | else 23 | color = Colors.grey; 24 | 25 | return GestureDetector( 26 | onTap: (){ 27 | if(size.hasStock){ 28 | product.selectedSize = size; 29 | } 30 | }, 31 | child: Container( 32 | decoration: BoxDecoration( 33 | border: Border.all( 34 | color: color 35 | ), 36 | ), 37 | child: Row( 38 | mainAxisSize: MainAxisSize.min, 39 | children: [ 40 | Container( 41 | color: color, 42 | padding: const EdgeInsets.symmetric(vertical: 4, horizontal: 8), 43 | child: Text( 44 | size.name, 45 | style: TextStyle(color: Colors.white), 46 | ), 47 | ), 48 | Container( 49 | padding: const EdgeInsets.symmetric(horizontal: 16), 50 | child: Text( 51 | 'R\$ ${size.price.toStringAsFixed(2)}', 52 | style: TextStyle( 53 | color: color, 54 | ), 55 | ), 56 | ) 57 | ], 58 | ), 59 | ), 60 | ); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /lib/screens/checkout/components/cpf_field.dart: -------------------------------------------------------------------------------- 1 | import 'package:brasil_fields/brasil_fields.dart'; 2 | import 'package:cpf_cnpj_validator/cpf_validator.dart'; 3 | import 'package:flutter/material.dart'; 4 | import 'package:flutter/services.dart'; 5 | import 'package:lojavirtual/models/user_manager.dart'; 6 | import 'package:provider/provider.dart'; 7 | 8 | class CpfField extends StatelessWidget { 9 | @override 10 | Widget build(BuildContext context) { 11 | final userManager = context.watch(); 12 | 13 | return Card( 14 | margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 4), 15 | child: Padding( 16 | padding: const EdgeInsets.all(16), 17 | child: Column( 18 | crossAxisAlignment: CrossAxisAlignment.start, 19 | children: [ 20 | Text( 21 | 'CPF', 22 | textAlign: TextAlign.start, 23 | style: TextStyle( 24 | fontWeight: FontWeight.w600, 25 | fontSize: 16, 26 | ), 27 | ), 28 | TextFormField( 29 | initialValue: userManager.user.cpf, 30 | decoration: const InputDecoration( 31 | hintText: '000.000.000-00', 32 | isDense: true 33 | ), 34 | keyboardType: TextInputType.number, 35 | inputFormatters: [ 36 | WhitelistingTextInputFormatter.digitsOnly, 37 | CpfInputFormatter(), 38 | ], 39 | validator: (cpf){ 40 | if(cpf.isEmpty) return 'Campo Obrigatório'; 41 | else if(!CPFValidator.isValid(cpf)) return 'CPF Inválido'; 42 | return null; 43 | }, 44 | onSaved: userManager.user.setCpf, 45 | ) 46 | ], 47 | ), 48 | ), 49 | ); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /lib/screens/confirmation/confirmation_screen.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:lojavirtual/common/order/order_product_tile.dart'; 3 | import 'package:lojavirtual/models/order.dart'; 4 | 5 | class ConfirmationScreen extends StatelessWidget { 6 | 7 | const ConfirmationScreen(this.order); 8 | 9 | final Order order; 10 | 11 | @override 12 | Widget build(BuildContext context) { 13 | return Scaffold( 14 | appBar: AppBar( 15 | title: const Text('Pedido Confirmado'), 16 | centerTitle: true, 17 | ), 18 | body: Center( 19 | child: Card( 20 | margin: const EdgeInsets.all(16), 21 | child: ListView( 22 | shrinkWrap: true, 23 | children: [ 24 | Padding( 25 | padding: const EdgeInsets.all(16), 26 | child: Column( 27 | crossAxisAlignment: CrossAxisAlignment.start, 28 | children: [ 29 | Text( 30 | order.formattedId, 31 | style: TextStyle( 32 | fontWeight: FontWeight.w600, 33 | color: Theme.of(context).primaryColor, 34 | ), 35 | ), 36 | Text( 37 | 'R\$ ${order.price.toStringAsFixed(2)}', 38 | style: TextStyle( 39 | fontWeight: FontWeight.w600, 40 | color: Colors.black, 41 | fontSize: 14, 42 | ), 43 | ), 44 | ], 45 | ), 46 | ), 47 | Column( 48 | children: order.items.map((e){ 49 | return OrderProductTile(e); 50 | }).toList(), 51 | ) 52 | ], 53 | ), 54 | ), 55 | ), 56 | ); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /lib/screens/home/components/section_list.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:lojavirtual/models/home_manager.dart'; 3 | import 'package:lojavirtual/models/section.dart'; 4 | import 'package:lojavirtual/screens/home/components/add_tile_widget.dart'; 5 | import 'package:lojavirtual/screens/home/components/item_tile.dart'; 6 | import 'package:lojavirtual/screens/home/components/section_header.dart'; 7 | import 'package:provider/provider.dart'; 8 | 9 | class SectionList extends StatelessWidget { 10 | 11 | const SectionList(this.section); 12 | 13 | final Section section; 14 | 15 | @override 16 | Widget build(BuildContext context) { 17 | final homeManager = context.watch(); 18 | 19 | return ChangeNotifierProvider.value( 20 | value: section, 21 | child: Container( 22 | margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), 23 | child: Column( 24 | crossAxisAlignment: CrossAxisAlignment.start, 25 | children: [ 26 | SectionHeader(), 27 | SizedBox( 28 | height: 150, 29 | child: Consumer
( 30 | builder: (_, section, __){ 31 | return ListView.separated( 32 | scrollDirection: Axis.horizontal, 33 | itemBuilder: (_, index){ 34 | if(index < section.items.length) 35 | return ItemTile(section.items[index]); 36 | else 37 | return AddTileWidget(); 38 | }, 39 | separatorBuilder: (_, __) => const SizedBox(width: 4,), 40 | itemCount: homeManager.editing 41 | ? section.items.length + 1 42 | : section.items.length, 43 | ); 44 | }, 45 | ), 46 | ) 47 | ], 48 | ), 49 | ), 50 | ); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /lib/models/user.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | 3 | import 'package:cloud_firestore/cloud_firestore.dart'; 4 | import 'package:firebase_messaging/firebase_messaging.dart'; 5 | import 'package:lojavirtual/models/address.dart'; 6 | 7 | class User { 8 | 9 | User({this.email, this.password, this.name, this.id}); 10 | 11 | User.fromDocument(DocumentSnapshot document){ 12 | id = document.documentID; 13 | name = document.data['name'] as String; 14 | email = document.data['email'] as String; 15 | cpf = document.data['cpf'] as String; 16 | if(document.data.containsKey('address')){ 17 | address = Address.fromMap( 18 | document.data['address'] as Map); 19 | } 20 | } 21 | 22 | String id; 23 | String name; 24 | String email; 25 | String cpf; 26 | String password; 27 | 28 | String confirmPassword; 29 | 30 | bool admin = false; 31 | 32 | Address address; 33 | 34 | DocumentReference get firestoreRef => 35 | Firestore.instance.document('users/$id'); 36 | 37 | CollectionReference get cartReference => 38 | firestoreRef.collection('cart'); 39 | 40 | CollectionReference get tokensReference => 41 | firestoreRef.collection('tokens'); 42 | 43 | Future saveData() async { 44 | await firestoreRef.setData(toMap()); 45 | } 46 | 47 | Map toMap(){ 48 | return { 49 | 'name': name, 50 | 'email': email, 51 | if(address != null) 52 | 'address': address.toMap(), 53 | if(cpf != null) 54 | 'cpf': cpf 55 | }; 56 | } 57 | 58 | void setAddress(Address address){ 59 | this.address = address; 60 | saveData(); 61 | } 62 | 63 | void setCpf(String cpf){ 64 | this.cpf = cpf; 65 | saveData(); 66 | } 67 | 68 | Future saveToken() async { 69 | final token = await FirebaseMessaging().getToken(); 70 | await tokensReference.document(token).setData({ 71 | 'token': token, 72 | 'updatedAt': FieldValue.serverTimestamp(), 73 | 'platform': Platform.operatingSystem, 74 | }); 75 | } 76 | } -------------------------------------------------------------------------------- /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: 'com.google.gms.google-services' 26 | apply plugin: 'kotlin-android' 27 | apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" 28 | 29 | android { 30 | compileSdkVersion 29 31 | 32 | sourceSets { 33 | main.java.srcDirs += 'src/main/kotlin' 34 | } 35 | 36 | lintOptions { 37 | disable 'InvalidPackage' 38 | } 39 | 40 | defaultConfig { 41 | // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). 42 | applicationId "br.com.ciolfi.lojavirtual" 43 | minSdkVersion 16 44 | targetSdkVersion 29 45 | multiDexEnabled true 46 | versionCode flutterVersionCode.toInteger() 47 | versionName flutterVersionName 48 | } 49 | 50 | buildTypes { 51 | release { 52 | // TODO: Add your own signing config for the release build. 53 | // Signing with the debug keys for now, so `flutter run --release` works. 54 | signingConfig signingConfigs.debug 55 | } 56 | } 57 | } 58 | 59 | flutter { 60 | source '../..' 61 | } 62 | 63 | dependencies { 64 | implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" 65 | } 66 | -------------------------------------------------------------------------------- /lib/screens/admin_users/admin_users_screen.dart: -------------------------------------------------------------------------------- 1 | import 'package:alphabet_list_scroll_view/alphabet_list_scroll_view.dart'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:lojavirtual/common/custom_drawer/custom_drawer.dart'; 4 | import 'package:lojavirtual/models/admin_orders_manager.dart'; 5 | import 'package:lojavirtual/models/admin_users_manager.dart'; 6 | import 'package:lojavirtual/models/page_manager.dart'; 7 | import 'package:provider/provider.dart'; 8 | 9 | class AdminUsersScreen extends StatelessWidget { 10 | @override 11 | Widget build(BuildContext context) { 12 | return Scaffold( 13 | drawer: CustomDrawer(), 14 | appBar: AppBar( 15 | title: const Text('Usuários'), 16 | centerTitle: true, 17 | ), 18 | body: Consumer( 19 | builder: (_, adminUsersManager, __){ 20 | return AlphabetListScrollView( 21 | itemBuilder: (_, index){ 22 | return ListTile( 23 | title: Text( 24 | adminUsersManager.users[index].name, 25 | style: TextStyle( 26 | fontWeight: FontWeight.w800, 27 | color: Colors.white 28 | ), 29 | ), 30 | subtitle: Text( 31 | adminUsersManager.users[index].email, 32 | style: TextStyle( 33 | color: Colors.white, 34 | ), 35 | ), 36 | onTap: (){ 37 | context.read().setUserFilter( 38 | adminUsersManager.users[index] 39 | ); 40 | context.read().setPage(5); 41 | }, 42 | ); 43 | }, 44 | highlightTextStyle: TextStyle( 45 | color: Colors.white, 46 | fontSize: 20 47 | ), 48 | indexedHeight: (index) => 80, 49 | strList: adminUsersManager.names, 50 | showPreview: true, 51 | ); 52 | }, 53 | ), 54 | ); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /lib/common/custom_drawer/custom_drawer_header.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:lojavirtual/models/page_manager.dart'; 3 | import 'package:lojavirtual/models/user_manager.dart'; 4 | import 'package:provider/provider.dart'; 5 | 6 | class CustomDrawerHeader extends StatelessWidget { 7 | @override 8 | Widget build(BuildContext context) { 9 | return Container( 10 | padding: const EdgeInsets.fromLTRB(32, 24, 16, 8), 11 | height: 180, 12 | child: Consumer( 13 | builder: (_, userManager, __){ 14 | return Column( 15 | crossAxisAlignment: CrossAxisAlignment.start, 16 | mainAxisAlignment: MainAxisAlignment.spaceAround, 17 | children: [ 18 | Text( 19 | 'Loja do\nDaniel', 20 | style: TextStyle( 21 | fontSize: 34, 22 | fontWeight: FontWeight.bold, 23 | ), 24 | ), 25 | Text( 26 | 'Olá, ${userManager.user?.name ?? ''}', 27 | overflow: TextOverflow.ellipsis, 28 | maxLines: 2, 29 | style: TextStyle( 30 | fontSize: 18, 31 | fontWeight: FontWeight.bold, 32 | ), 33 | ), 34 | GestureDetector( 35 | onTap: (){ 36 | if(userManager.isLoggedIn){ 37 | context.read().setPage(0); 38 | userManager.signOut(); 39 | } else { 40 | Navigator.of(context).pushNamed('/login'); 41 | } 42 | }, 43 | child: Text( 44 | userManager.isLoggedIn 45 | ? 'Sair' 46 | : 'Entre ou cadastre-se >', 47 | style: TextStyle( 48 | color: Theme.of(context).primaryColor, 49 | fontSize: 16, 50 | fontWeight: FontWeight.bold, 51 | ), 52 | ), 53 | ), 54 | ], 55 | ); 56 | }, 57 | ), 58 | ); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /lib/models/home_manager.dart: -------------------------------------------------------------------------------- 1 | import 'package:cloud_firestore/cloud_firestore.dart'; 2 | import 'package:flutter/cupertino.dart'; 3 | import 'package:lojavirtual/models/section.dart'; 4 | 5 | class HomeManager extends ChangeNotifier { 6 | 7 | HomeManager(){ 8 | _loadSections(); 9 | } 10 | 11 | final List
_sections = []; 12 | 13 | List
_editingSections = []; 14 | 15 | bool editing = false; 16 | bool loading = false; 17 | 18 | final Firestore firestore = Firestore.instance; 19 | 20 | Future _loadSections() async { 21 | firestore.collection('home').orderBy('pos').snapshots().listen((snapshot) { 22 | _sections.clear(); 23 | for(final DocumentSnapshot document in snapshot.documents){ 24 | _sections.add(Section.fromDocument(document)); 25 | } 26 | notifyListeners(); 27 | }); 28 | } 29 | 30 | void addSection(Section section){ 31 | _editingSections.add(section); 32 | notifyListeners(); 33 | } 34 | 35 | void removeSection(Section section){ 36 | _editingSections.remove(section); 37 | notifyListeners(); 38 | } 39 | 40 | List
get sections { 41 | if(editing) 42 | return _editingSections; 43 | else 44 | return _sections; 45 | } 46 | 47 | void enterEditing(){ 48 | editing = true; 49 | 50 | _editingSections = _sections.map((s) => s.clone()).toList(); 51 | 52 | notifyListeners(); 53 | } 54 | 55 | Future saveEditing() async { 56 | bool valid = true; 57 | for(final section in _editingSections){ 58 | if(!section.valid()) valid = false; 59 | } 60 | if(!valid) return; 61 | 62 | loading = true; 63 | notifyListeners(); 64 | 65 | int pos = 0; 66 | for(final section in _editingSections){ 67 | await section.save(pos); 68 | pos++; 69 | } 70 | 71 | for(final section in List.from(_sections)){ 72 | if(!_editingSections.any((element) => element.id == section.id)){ 73 | await section.delete(); 74 | } 75 | } 76 | 77 | loading = false; 78 | editing = false; 79 | notifyListeners(); 80 | } 81 | 82 | void discardEditing(){ 83 | editing = false; 84 | notifyListeners(); 85 | } 86 | 87 | } -------------------------------------------------------------------------------- /lib/screens/home/components/section_staggered.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_staggered_grid_view/flutter_staggered_grid_view.dart'; 3 | import 'package:lojavirtual/models/home_manager.dart'; 4 | import 'package:lojavirtual/models/section.dart'; 5 | import 'package:lojavirtual/screens/home/components/add_tile_widget.dart'; 6 | import 'package:lojavirtual/screens/home/components/item_tile.dart'; 7 | import 'package:lojavirtual/screens/home/components/section_header.dart'; 8 | import 'package:provider/provider.dart'; 9 | 10 | class SectionStaggered extends StatelessWidget { 11 | 12 | const SectionStaggered(this.section); 13 | 14 | final Section section; 15 | 16 | @override 17 | Widget build(BuildContext context) { 18 | final homeManager = context.watch(); 19 | 20 | return ChangeNotifierProvider.value( 21 | value: section, 22 | child: Container( 23 | margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), 24 | child: Column( 25 | crossAxisAlignment: CrossAxisAlignment.start, 26 | children: [ 27 | SectionHeader(), 28 | Consumer
( 29 | builder: (_, section, __){ 30 | return StaggeredGridView.countBuilder( 31 | padding: EdgeInsets.zero, 32 | shrinkWrap: true, 33 | crossAxisCount: 4, 34 | physics: const NeverScrollableScrollPhysics(), 35 | itemCount: homeManager.editing 36 | ? section.items.length + 1 37 | : section.items.length, 38 | itemBuilder: (_, index){ 39 | if(index < section.items.length) 40 | return ItemTile(section.items[index]); 41 | else 42 | return AddTileWidget(); 43 | }, 44 | staggeredTileBuilder: (index) => 45 | StaggeredTile.count(2, index.isEven ? 2 : 1), 46 | mainAxisSpacing: 4, 47 | crossAxisSpacing: 4, 48 | ); 49 | }, 50 | ) 51 | ], 52 | ), 53 | ), 54 | ); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /lib/common/order/cancel_order_dialog.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:lojavirtual/models/order.dart'; 3 | 4 | class CancelOrderDialog extends StatefulWidget { 5 | 6 | const CancelOrderDialog(this.order); 7 | 8 | final Order order; 9 | 10 | @override 11 | _CancelOrderDialogState createState() => _CancelOrderDialogState(); 12 | } 13 | 14 | class _CancelOrderDialogState extends State { 15 | 16 | bool loading = false; 17 | String error; 18 | 19 | @override 20 | Widget build(BuildContext context) { 21 | return WillPopScope( 22 | onWillPop: () => Future.value(false), 23 | child: AlertDialog( 24 | title: Text('Cancelar ${widget.order.formattedId}?'), 25 | content: Column( 26 | crossAxisAlignment: CrossAxisAlignment.start, 27 | mainAxisSize: MainAxisSize.min, 28 | children: [ 29 | Text( 30 | loading 31 | ? 'Cancelando...' 32 | : 'Esta ação não poderá ser defeita!' 33 | ), 34 | if(error != null) 35 | Padding( 36 | padding: const EdgeInsets.only(top: 8), 37 | child: Text( 38 | error, 39 | style: TextStyle(color: Colors.red), 40 | ), 41 | ) 42 | ], 43 | ), 44 | actions: [ 45 | FlatButton( 46 | onPressed: !loading ? (){ 47 | Navigator.of(context).pop(); 48 | } : null, 49 | child: const Text('Voltar'), 50 | ), 51 | FlatButton( 52 | onPressed: !loading ? () async { 53 | setState(() { 54 | loading = true; 55 | }); 56 | try { 57 | await widget.order.cancel(); 58 | Navigator.of(context).pop(); 59 | } catch (e){ 60 | setState(() { 61 | loading = false; 62 | error = e.toString(); 63 | }); 64 | } 65 | } : null, 66 | textColor: Colors.red, 67 | child: const Text('Cancelar Pedido'), 68 | ), 69 | ], 70 | ), 71 | ); 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /lib/common/order/order_product_tile.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:lojavirtual/models/cart_product.dart'; 3 | 4 | class OrderProductTile extends StatelessWidget { 5 | 6 | const OrderProductTile(this.cartProduct); 7 | 8 | final CartProduct cartProduct; 9 | 10 | @override 11 | Widget build(BuildContext context) { 12 | return GestureDetector( 13 | onTap: (){ 14 | Navigator.of(context).pushNamed( 15 | '/product', arguments: cartProduct.product); 16 | }, 17 | child: Container( 18 | margin: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), 19 | padding: const EdgeInsets.all(8), 20 | child: Row( 21 | children: [ 22 | SizedBox( 23 | height: 60, 24 | width: 60, 25 | child: Image.network(cartProduct.product.images.first), 26 | ), 27 | const SizedBox(width: 8,), 28 | Expanded( 29 | child: Column( 30 | crossAxisAlignment: CrossAxisAlignment.start, 31 | children: [ 32 | Text( 33 | cartProduct.product.name, 34 | style: TextStyle( 35 | fontWeight: FontWeight.w500, 36 | fontSize: 17.0, 37 | ), 38 | ), 39 | Text( 40 | 'Tamanho: ${cartProduct.size}', 41 | style: TextStyle(fontWeight: FontWeight.w300), 42 | ), 43 | Text( 44 | 'R\$ ${(cartProduct.fixedPrice ?? cartProduct.unitPrice) 45 | .toStringAsFixed(2)}', 46 | style: TextStyle( 47 | color: Theme.of(context).primaryColor, 48 | fontSize: 14.0, 49 | fontWeight: FontWeight.bold 50 | ), 51 | ) 52 | ], 53 | ), 54 | ), 55 | Text( 56 | '${cartProduct.quantity}', 57 | style: const TextStyle( 58 | fontSize: 20 59 | ), 60 | ), 61 | ], 62 | ), 63 | ), 64 | ); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /lib/models/admin_orders_manager.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | import 'package:cloud_firestore/cloud_firestore.dart'; 4 | import 'package:flutter/cupertino.dart'; 5 | import 'package:lojavirtual/models/order.dart'; 6 | import 'package:lojavirtual/models/user.dart'; 7 | 8 | class AdminOrdersManager extends ChangeNotifier { 9 | 10 | final List _orders = []; 11 | 12 | User userFilter; 13 | List statusFilter = [Status.preparing]; 14 | 15 | final Firestore firestore = Firestore.instance; 16 | 17 | StreamSubscription _subscription; 18 | 19 | void updateAdmin({bool adminEnabled}){ 20 | _orders.clear(); 21 | 22 | _subscription?.cancel(); 23 | if(adminEnabled){ 24 | _listenToOrders(); 25 | } 26 | } 27 | 28 | List get filteredOrders { 29 | List output = _orders.reversed.toList(); 30 | 31 | if(userFilter != null){ 32 | output = output.where((o) => o.userId == userFilter.id).toList(); 33 | } 34 | 35 | return output.where((o) => statusFilter.contains(o.status)).toList(); 36 | } 37 | 38 | void _listenToOrders(){ 39 | _subscription = firestore.collection('orders').snapshots().listen( 40 | (event) { 41 | for(final change in event.documentChanges){ 42 | switch(change.type){ 43 | case DocumentChangeType.added: 44 | _orders.add( 45 | Order.fromDocument(change.document) 46 | ); 47 | break; 48 | case DocumentChangeType.modified: 49 | final modOrder = _orders.firstWhere( 50 | (o) => o.orderId == change.document.documentID); 51 | modOrder.updateFromDocument(change.document); 52 | break; 53 | case DocumentChangeType.removed: 54 | debugPrint('Deu problema sério!!!'); 55 | break; 56 | } 57 | } 58 | notifyListeners(); 59 | }); 60 | } 61 | 62 | void setUserFilter(User user){ 63 | userFilter = user; 64 | notifyListeners(); 65 | } 66 | 67 | void setStatusFilter({Status status, bool enabled}){ 68 | if(enabled){ 69 | statusFilter.add(status); 70 | } else { 71 | statusFilter.remove(status); 72 | } 73 | notifyListeners(); 74 | } 75 | 76 | @override 77 | void dispose() { 78 | super.dispose(); 79 | _subscription?.cancel(); 80 | } 81 | 82 | } -------------------------------------------------------------------------------- /lib/screens/checkout/components/card_back.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter/services.dart'; 3 | import 'package:lojavirtual/models/credit_card.dart'; 4 | import 'package:lojavirtual/screens/checkout/components/card_text_field.dart'; 5 | 6 | class CardBack extends StatelessWidget { 7 | 8 | const CardBack({this.cvvFocus, this.creditCard}); 9 | 10 | final FocusNode cvvFocus; 11 | final CreditCard creditCard; 12 | 13 | @override 14 | Widget build(BuildContext context) { 15 | return Card( 16 | clipBehavior: Clip.antiAlias, 17 | shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)), 18 | elevation: 16, 19 | child: Container( 20 | height: 200, 21 | color: const Color(0xFF1B4B52), 22 | child: Column( 23 | children: [ 24 | Container( 25 | color: Colors.black, 26 | height: 40, 27 | margin: const EdgeInsets.symmetric(vertical: 16), 28 | ), 29 | Row( 30 | children: [ 31 | Expanded( 32 | flex: 70, 33 | child: Container( 34 | color: Colors.grey[500], 35 | margin: const EdgeInsets.only(left: 12), 36 | padding: const EdgeInsets.symmetric(vertical: 4, horizontal: 8), 37 | child: CardTextField( 38 | initialValue: creditCard.securityCode, 39 | hint: '123', 40 | maxLength: 3, 41 | inputFormatters: [ 42 | WhitelistingTextInputFormatter.digitsOnly, 43 | ], 44 | textAlign: TextAlign.end, 45 | textInputType: TextInputType.number, 46 | validator: (cvv){ 47 | if(cvv.length != 3) return 'Inválido'; 48 | return null; 49 | }, 50 | focusNode: cvvFocus, 51 | onSaved: creditCard.setCVV, 52 | ), 53 | ), 54 | ), 55 | Expanded( 56 | flex: 30, 57 | child: Container(), 58 | ), 59 | ], 60 | ) 61 | ], 62 | ), 63 | ), 64 | ); 65 | } 66 | } -------------------------------------------------------------------------------- /lib/screens/home/components/section_header.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:lojavirtual/common/custom_icon_button.dart'; 3 | import 'package:lojavirtual/models/home_manager.dart'; 4 | import 'package:lojavirtual/models/section.dart'; 5 | import 'package:provider/provider.dart'; 6 | 7 | class SectionHeader extends StatelessWidget { 8 | 9 | @override 10 | Widget build(BuildContext context) { 11 | final homeManager = context.watch(); 12 | final section = context.watch
(); 13 | 14 | if(homeManager.editing){ 15 | return Column( 16 | crossAxisAlignment: CrossAxisAlignment.start, 17 | children: [ 18 | Row( 19 | children: [ 20 | Expanded( 21 | child: TextFormField( 22 | initialValue: section.name, 23 | decoration: const InputDecoration( 24 | hintText: 'Título', 25 | isDense: true, 26 | border: InputBorder.none 27 | ), 28 | style: TextStyle( 29 | color: Colors.white, 30 | fontWeight: FontWeight.w800, 31 | fontSize: 18, 32 | ), 33 | onChanged: (text) => section.name = text, 34 | ), 35 | ), 36 | CustomIconButton( 37 | iconData: Icons.remove, 38 | color: Colors.white, 39 | onTap: (){ 40 | homeManager.removeSection(section); 41 | }, 42 | ), 43 | ], 44 | ), 45 | if(section.error != null) 46 | Padding( 47 | padding: const EdgeInsets.only(bottom: 8), 48 | child: Text( 49 | section.error, 50 | style: const TextStyle( 51 | color: Colors.red 52 | ), 53 | ), 54 | ) 55 | ], 56 | ); 57 | } else { 58 | return Padding( 59 | padding: const EdgeInsets.symmetric(vertical: 8), 60 | child: Text( 61 | section.name ?? "Banana", 62 | style: TextStyle( 63 | color: Colors.white, 64 | fontWeight: FontWeight.w800, 65 | fontSize: 18, 66 | ), 67 | ), 68 | ); 69 | } 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /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/common/custom_drawer/custom_drawer.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:lojavirtual/common/custom_drawer/custom_drawer_header.dart'; 3 | import 'package:lojavirtual/common/custom_drawer/drawer_tile.dart'; 4 | import 'package:lojavirtual/models/user_manager.dart'; 5 | import 'package:provider/provider.dart'; 6 | 7 | class CustomDrawer extends StatelessWidget { 8 | 9 | @override 10 | Widget build(BuildContext context) { 11 | return Drawer( 12 | child: Stack( 13 | children: [ 14 | Container( 15 | decoration: BoxDecoration( 16 | gradient: LinearGradient( 17 | colors: [ 18 | const Color.fromARGB(255, 203, 236, 241), 19 | Colors.white, 20 | ], 21 | begin: Alignment.topCenter, 22 | end: Alignment.bottomCenter, 23 | ) 24 | ), 25 | ), 26 | ListView( 27 | children: [ 28 | CustomDrawerHeader(), 29 | const Divider(), 30 | DrawerTile( 31 | iconData: Icons.home, 32 | title: 'Início', 33 | page: 0, 34 | ), 35 | DrawerTile( 36 | iconData: Icons.list, 37 | title: 'Produtos', 38 | page: 1, 39 | ), 40 | DrawerTile( 41 | iconData: Icons.playlist_add_check, 42 | title: 'Meus Pedidos', 43 | page: 2, 44 | ), 45 | DrawerTile( 46 | iconData: Icons.location_on, 47 | title: 'Lojas', 48 | page: 3, 49 | ), 50 | Consumer( 51 | builder: (_, userManager, __){ 52 | if(userManager.adminEnabled){ 53 | return Column( 54 | children: [ 55 | const Divider(), 56 | DrawerTile( 57 | iconData: Icons.settings, 58 | title: 'Usuários', 59 | page: 4, 60 | ), 61 | DrawerTile( 62 | iconData: Icons.settings, 63 | title: 'Pedidos', 64 | page: 5, 65 | ), 66 | ], 67 | ); 68 | } else { 69 | return Container(); 70 | } 71 | }, 72 | ) 73 | ], 74 | ), 75 | ], 76 | ), 77 | ); 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /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 | Loja do Daniel 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 | NSPhotoLibraryUsageDescription 45 | Acessar galeria para buscar imagem 46 | NSCameraUsageDescription 47 | Acessar camera para adicionar imagem 48 | NSMicrophoneUsageDescription 49 | Acessar camera para adicionar imagem 50 | 51 | CFBundleURLTypes 52 | 53 | CFBundleURLSchemes 54 | 55 | fb705074733663048 56 | 57 | FacebookAppID 58 | 705074733663048 59 | FacebookDisplayName 60 | Loja do Daniel 61 | 62 | LSApplicationQueriesSchemes 63 | 64 | fbapi 65 | fb-messenger-share-api 66 | fbauth2 67 | fbshareextension 68 | comgooglemaps 69 | iosamap 70 | waze 71 | 72 | 73 | 74 | 75 | -------------------------------------------------------------------------------- /lib/models/cart_product.dart: -------------------------------------------------------------------------------- 1 | import 'package:cloud_firestore/cloud_firestore.dart'; 2 | import 'package:flutter/cupertino.dart'; 3 | import 'package:lojavirtual/models/item_size.dart'; 4 | import 'package:lojavirtual/models/product.dart'; 5 | 6 | class CartProduct extends ChangeNotifier { 7 | 8 | CartProduct.fromProduct(this._product){ 9 | productId = product.id; 10 | quantity = 1; 11 | size = product.selectedSize.name; 12 | } 13 | 14 | CartProduct.fromDocument(DocumentSnapshot document){ 15 | id = document.documentID; 16 | productId = document.data['pid'] as String; 17 | quantity = document.data['quantity'] as int; 18 | size = document.data['size'] as String; 19 | 20 | firestore.document('products/$productId').get().then( 21 | (doc) { 22 | product = Product.fromDocument(doc); 23 | } 24 | ); 25 | } 26 | 27 | CartProduct.fromMap(Map map){ 28 | productId = map['pid'] as String; 29 | quantity = map['quantity'] as int; 30 | size = map['size'] as String; 31 | fixedPrice = map['fixedPrice'] as num; 32 | 33 | firestore.document('products/$productId').get().then( 34 | (doc) { 35 | product = Product.fromDocument(doc); 36 | } 37 | ); 38 | } 39 | 40 | final Firestore firestore = Firestore.instance; 41 | 42 | String id; 43 | 44 | String productId; 45 | int quantity; 46 | String size; 47 | 48 | num fixedPrice; 49 | 50 | Product _product; 51 | Product get product => _product; 52 | set product(Product value){ 53 | _product = value; 54 | notifyListeners(); 55 | } 56 | 57 | ItemSize get itemSize { 58 | if(product == null) return null; 59 | return product.findSize(size); 60 | } 61 | 62 | num get unitPrice { 63 | if(product == null) return 0; 64 | return itemSize?.price ?? 0; 65 | } 66 | 67 | num get totalPrice => unitPrice * quantity; 68 | 69 | Map toCartItemMap(){ 70 | return { 71 | 'pid': productId, 72 | 'quantity': quantity, 73 | 'size': size, 74 | }; 75 | } 76 | 77 | Map toOrderItemMap(){ 78 | return { 79 | 'pid': productId, 80 | 'quantity': quantity, 81 | 'size': size, 82 | 'fixedPrice': fixedPrice ?? unitPrice, 83 | }; 84 | } 85 | 86 | bool stackable(Product product){ 87 | return product.id == productId && product.selectedSize.name == size; 88 | } 89 | 90 | void increment(){ 91 | quantity++; 92 | notifyListeners(); 93 | } 94 | 95 | void decrement(){ 96 | quantity--; 97 | notifyListeners(); 98 | } 99 | 100 | bool get hasStock { 101 | if(product != null && product.deleted) return false; 102 | 103 | final size = itemSize; 104 | if(size == null) return false; 105 | return size.stock >= quantity; 106 | } 107 | 108 | } -------------------------------------------------------------------------------- /lib/screens/products/components/product_list_tile.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:lojavirtual/models/product.dart'; 3 | 4 | class ProductListTile extends StatelessWidget { 5 | 6 | const ProductListTile(this.product); 7 | 8 | final Product product; 9 | 10 | @override 11 | Widget build(BuildContext context) { 12 | return GestureDetector( 13 | onTap: (){ 14 | Navigator.of(context).pushNamed('/product', arguments: product); 15 | }, 16 | child: Card( 17 | margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 4), 18 | shape: RoundedRectangleBorder( 19 | borderRadius: BorderRadius.circular(4) 20 | ), 21 | child: Container( 22 | height: 100, 23 | padding: const EdgeInsets.all(8), 24 | child: Row( 25 | children: [ 26 | AspectRatio( 27 | aspectRatio: 1, 28 | child: Image.network(product.images.first), 29 | ), 30 | const SizedBox(width: 16,), 31 | Expanded( 32 | child: Column( 33 | mainAxisAlignment: MainAxisAlignment.spaceEvenly, 34 | crossAxisAlignment: CrossAxisAlignment.start, 35 | children: [ 36 | Text( 37 | product.name, 38 | style: TextStyle( 39 | fontSize: 16, 40 | fontWeight: FontWeight.w800, 41 | ), 42 | ), 43 | Padding( 44 | padding: const EdgeInsets.only(top: 4), 45 | child: Text( 46 | 'A partir de', 47 | style: TextStyle( 48 | color: Colors.grey[400], 49 | fontSize: 12, 50 | ), 51 | ), 52 | ), 53 | Text( 54 | 'R\$ ${product.basePrice.toStringAsFixed(2)}', 55 | style: TextStyle( 56 | fontSize: 15, 57 | fontWeight: FontWeight.w800, 58 | color: Theme.of(context).primaryColor 59 | ), 60 | ), 61 | if(!product.hasStock) 62 | const Padding( 63 | padding: EdgeInsets.only(top: 4), 64 | child: Text( 65 | 'Sem estoque', 66 | style: TextStyle( 67 | color: Colors.red, 68 | fontSize: 10 69 | ), 70 | ), 71 | ) 72 | ], 73 | ), 74 | ) 75 | ], 76 | ), 77 | ), 78 | ), 79 | ); 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /lib/services/cielo_payment.dart: -------------------------------------------------------------------------------- 1 | import 'dart:collection'; 2 | 3 | import 'package:cloud_functions/cloud_functions.dart'; 4 | import 'package:flutter/cupertino.dart'; 5 | import 'package:lojavirtual/models/credit_card.dart'; 6 | import 'package:lojavirtual/models/user.dart'; 7 | 8 | class CieloPayment { 9 | 10 | final CloudFunctions functions = CloudFunctions.instance; 11 | 12 | Future authorize({CreditCard creditCard, num price, 13 | String orderId, User user}) async { 14 | 15 | try { 16 | final Map dataSale = { 17 | 'merchantOrderId': orderId, 18 | 'amount': (price * 100).toInt(), 19 | //'amount': 10 * 100, 20 | 'softDescriptor': 'Loja Daniel', 21 | 'installments': 1, 22 | 'creditCard': creditCard.toJson(), 23 | 'cpf': user.cpf, 24 | 'paymentType': 'CreditCard', 25 | }; 26 | 27 | final HttpsCallable callable = functions.getHttpsCallable( 28 | functionName: 'authorizeCreditCard' 29 | ); 30 | callable.timeout = const Duration(seconds: 60); 31 | final response = await callable.call(dataSale); 32 | final data = Map.from(response.data as LinkedHashMap); 33 | if (data['success'] as bool) { 34 | return data['paymentId'] as String; 35 | } else { 36 | debugPrint('${data['error']['message']}'); 37 | return Future.error(data['error']['message']); 38 | } 39 | } catch (e){ 40 | debugPrint('$e'); 41 | return Future.error('Falha ao processar transação. Tente novamente.'); 42 | } 43 | } 44 | 45 | Future capture(String payId) async { 46 | final Map captureData = { 47 | 'payId': payId 48 | }; 49 | final HttpsCallable callable = functions.getHttpsCallable( 50 | functionName: 'captureCreditCard' 51 | ); 52 | callable.timeout = const Duration(seconds: 60); 53 | final response = await callable.call(captureData); 54 | final data = Map.from(response.data as LinkedHashMap); 55 | 56 | if (data['success'] as bool) { 57 | debugPrint('Captura realizada com sucesso'); 58 | } else { 59 | debugPrint('${data['error']['message']}'); 60 | return Future.error(data['error']['message']); 61 | } 62 | } 63 | 64 | Future cancel(String payId) async { 65 | final Map cancelData = { 66 | 'payId': payId 67 | }; 68 | final HttpsCallable callable = functions.getHttpsCallable( 69 | functionName: 'cancelCreditCard' 70 | ); 71 | callable.timeout = const Duration(seconds: 60); 72 | final response = await callable.call(cancelData); 73 | final data = Map.from(response.data as LinkedHashMap); 74 | 75 | if (data['success'] as bool) { 76 | debugPrint('Cancelamento realizado com sucesso'); 77 | } else { 78 | debugPrint('${data['error']['message']}'); 79 | return Future.error(data['error']['message']); 80 | } 81 | } 82 | 83 | } -------------------------------------------------------------------------------- /lib/screens/edit_product/components/edit_item_size.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:lojavirtual/common/custom_icon_button.dart'; 3 | import 'package:lojavirtual/models/item_size.dart'; 4 | 5 | class EditItemSize extends StatelessWidget { 6 | 7 | const EditItemSize({Key key, this.size, this.onRemove, 8 | this.onMoveUp, this.onMoveDown}) : super(key: key); 9 | 10 | final ItemSize size; 11 | final VoidCallback onRemove; 12 | final VoidCallback onMoveUp; 13 | final VoidCallback onMoveDown; 14 | 15 | @override 16 | Widget build(BuildContext context) { 17 | return Row( 18 | children: [ 19 | Expanded( 20 | flex: 30, 21 | child: TextFormField( 22 | initialValue: size.name, 23 | decoration: const InputDecoration( 24 | labelText: 'Título', 25 | isDense: true, 26 | ), 27 | validator: (name){ 28 | if(name.isEmpty) 29 | return 'Inválido'; 30 | return null; 31 | }, 32 | onChanged: (name) => size.name = name, 33 | ), 34 | ), 35 | const SizedBox(width: 4,), 36 | Expanded( 37 | flex: 30, 38 | child: TextFormField( 39 | initialValue: size.stock?.toString(), 40 | decoration: const InputDecoration( 41 | labelText: 'Estoque', 42 | isDense: true, 43 | ), 44 | keyboardType: TextInputType.number, 45 | validator: (stock){ 46 | if(int.tryParse(stock) == null) 47 | return 'Inválido'; 48 | return null; 49 | }, 50 | onChanged: (stock) => size.stock = int.tryParse(stock), 51 | ), 52 | ), 53 | const SizedBox(width: 4,), 54 | Expanded( 55 | flex: 40, 56 | child: TextFormField( 57 | initialValue: size.price?.toStringAsFixed(2), 58 | decoration: const InputDecoration( 59 | labelText: 'Preço', 60 | isDense: true, 61 | prefixText: 'R\$' 62 | ), 63 | keyboardType: const TextInputType.numberWithOptions(decimal: true), 64 | validator: (price){ 65 | if(num.tryParse(price) == null) 66 | return 'Inválido'; 67 | return null; 68 | }, 69 | onChanged: (price) => size.price = num.tryParse(price), 70 | ), 71 | ), 72 | CustomIconButton( 73 | iconData: Icons.remove, 74 | color: Colors.red, 75 | onTap: onRemove, 76 | ), 77 | CustomIconButton( 78 | iconData: Icons.arrow_drop_up, 79 | color: Colors.black, 80 | onTap: onMoveUp, 81 | ), 82 | CustomIconButton( 83 | iconData: Icons.arrow_drop_down, 84 | color: Colors.black, 85 | onTap: onMoveDown, 86 | ) 87 | ], 88 | ); 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /lib/common/price_card.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:lojavirtual/models/cart_manager.dart'; 3 | import 'package:provider/provider.dart'; 4 | 5 | class PriceCard extends StatelessWidget { 6 | 7 | const PriceCard({this.buttonText, this.onPressed}); 8 | 9 | final String buttonText; 10 | final VoidCallback onPressed; 11 | 12 | @override 13 | Widget build(BuildContext context) { 14 | final cartManager = context.watch(); 15 | final productsPrice = cartManager.productsPrice; 16 | final deliveryPrice = cartManager.deliveryPrice; 17 | final totalPrice = cartManager.totalPrice; 18 | 19 | return Card( 20 | margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), 21 | child: Padding( 22 | padding: const EdgeInsets.fromLTRB(16, 16, 16, 4), 23 | child: Column( 24 | crossAxisAlignment: CrossAxisAlignment.stretch, 25 | children: [ 26 | Text( 27 | 'Resumo do Pedido', 28 | textAlign: TextAlign.start, 29 | style: TextStyle( 30 | fontWeight: FontWeight.w600, 31 | fontSize: 16, 32 | ), 33 | ), 34 | const SizedBox(height: 12,), 35 | Row( 36 | mainAxisAlignment: MainAxisAlignment.spaceBetween, 37 | children: [ 38 | const Text('Subtotal'), 39 | Text('R\$ ${productsPrice.toStringAsFixed(2)}') 40 | ], 41 | ), 42 | const Divider(), 43 | if(deliveryPrice != null) 44 | ...[ 45 | Row( 46 | mainAxisAlignment: MainAxisAlignment.spaceBetween, 47 | children: [ 48 | const Text('Entrega'), 49 | Text('R\$ ${deliveryPrice.toStringAsFixed(2)}') 50 | ], 51 | ), 52 | const Divider(), 53 | ], 54 | const SizedBox(height: 12,), 55 | Row( 56 | mainAxisAlignment: MainAxisAlignment.spaceBetween, 57 | children: [ 58 | Text('Total', 59 | style: TextStyle(fontWeight: FontWeight.w500), 60 | ), 61 | Text( 62 | 'R\$ ${totalPrice.toStringAsFixed(2)}', 63 | style: TextStyle( 64 | color: Theme.of(context).primaryColor, 65 | fontSize: 16, 66 | ), 67 | ) 68 | ], 69 | ), 70 | const SizedBox(height: 8,), 71 | RaisedButton( 72 | color: Theme.of(context).primaryColor, 73 | disabledColor: Theme.of(context).primaryColor.withAlpha(100), 74 | textColor: Colors.white, 75 | onPressed: onPressed, 76 | child: Text(buttonText), 77 | ), 78 | ], 79 | ), 80 | ), 81 | ); 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /lib/screens/edit_product/components/sizes_form.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:lojavirtual/common/custom_icon_button.dart'; 3 | import 'package:lojavirtual/models/item_size.dart'; 4 | import 'package:lojavirtual/models/product.dart'; 5 | import 'package:lojavirtual/screens/edit_product/components/edit_item_size.dart'; 6 | 7 | class SizesForm extends StatelessWidget { 8 | 9 | const SizesForm(this.product); 10 | 11 | final Product product; 12 | 13 | @override 14 | Widget build(BuildContext context) { 15 | return FormField>( 16 | initialValue: product.sizes, 17 | validator: (sizes){ 18 | if(sizes.isEmpty) 19 | return 'Insira um tamanho'; 20 | return null; 21 | }, 22 | builder: (state){ 23 | return Column( 24 | children: [ 25 | Row( 26 | children: [ 27 | Expanded( 28 | child: Text( 29 | 'Tamanhos', 30 | style: TextStyle( 31 | fontSize: 16, 32 | fontWeight: FontWeight.w500 33 | ), 34 | ), 35 | ), 36 | CustomIconButton( 37 | iconData: Icons.add, 38 | color: Colors.black, 39 | onTap: (){ 40 | state.value.add(ItemSize()); 41 | state.didChange(state.value); 42 | }, 43 | ) 44 | ], 45 | ), 46 | Column( 47 | children: state.value.map((size){ 48 | return EditItemSize( 49 | key: ObjectKey(size), 50 | size: size, 51 | onRemove: (){ 52 | state.value.remove(size); 53 | state.didChange(state.value); 54 | }, 55 | onMoveUp: size != state.value.first ? (){ 56 | final index = state.value.indexOf(size); 57 | state.value.remove(size); 58 | state.value.insert(index-1, size); 59 | state.didChange(state.value); 60 | } : null, 61 | onMoveDown: size != state.value.last ? (){ 62 | final index = state.value.indexOf(size); 63 | state.value.remove(size); 64 | state.value.insert(index+1, size); 65 | state.didChange(state.value); 66 | } : null, 67 | ); 68 | }).toList(), 69 | ), 70 | if(state.hasError) 71 | Container( 72 | alignment: Alignment.centerLeft, 73 | child: Text( 74 | state.errorText, 75 | style: const TextStyle( 76 | color: Colors.red, 77 | fontSize: 12, 78 | ), 79 | ), 80 | ) 81 | ], 82 | ); 83 | }, 84 | ); 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /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/screens/edit_product/components/image_source_sheet.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | 3 | import 'package:flutter/cupertino.dart'; 4 | import 'package:flutter/material.dart'; 5 | import 'package:image_cropper/image_cropper.dart'; 6 | import 'package:image_picker/image_picker.dart'; 7 | 8 | class ImageSourceSheet extends StatelessWidget { 9 | 10 | ImageSourceSheet({this.onImageSelected}); 11 | 12 | final Function(File) onImageSelected; 13 | 14 | final ImagePicker picker = ImagePicker(); 15 | 16 | Future editImage(String path, BuildContext context) async { 17 | final File croppedFile = await ImageCropper.cropImage( 18 | sourcePath: path, 19 | aspectRatio: const CropAspectRatio(ratioX: 1.0, ratioY: 1.0), 20 | androidUiSettings: AndroidUiSettings( 21 | toolbarTitle: 'Editar Imagem', 22 | toolbarColor: Theme.of(context).primaryColor, 23 | toolbarWidgetColor: Colors.white, 24 | ), 25 | iosUiSettings: const IOSUiSettings( 26 | title: 'Editar Imagem', 27 | cancelButtonTitle: 'Cancelar', 28 | doneButtonTitle: 'Concluir', 29 | ) 30 | ); 31 | if(croppedFile != null){ 32 | onImageSelected(croppedFile); 33 | } 34 | } 35 | 36 | @override 37 | Widget build(BuildContext context) { 38 | if(Platform.isAndroid) 39 | return BottomSheet( 40 | onClosing: (){}, 41 | builder: (_) => Column( 42 | mainAxisSize: MainAxisSize.min, 43 | crossAxisAlignment: CrossAxisAlignment.stretch, 44 | children: [ 45 | FlatButton( 46 | onPressed: () async { 47 | final PickedFile file = 48 | await picker.getImage(source: ImageSource.camera); 49 | editImage(file.path, context); 50 | }, 51 | child: const Text('Câmera'), 52 | ), 53 | FlatButton( 54 | onPressed: () async { 55 | final PickedFile file = 56 | await picker.getImage(source: ImageSource.gallery); 57 | editImage(file.path, context); 58 | }, 59 | child: const Text('Galeria'), 60 | ), 61 | ], 62 | ), 63 | ); 64 | else 65 | return CupertinoActionSheet( 66 | title: const Text('Selecionar foto para o item'), 67 | message: const Text('Escolha a origem da foto'), 68 | cancelButton: CupertinoActionSheetAction( 69 | onPressed: Navigator.of(context).pop, 70 | child: const Text('Cancelar'), 71 | ), 72 | actions: [ 73 | CupertinoActionSheetAction( 74 | isDefaultAction: true, 75 | onPressed: () async { 76 | final PickedFile file = 77 | await picker.getImage(source: ImageSource.camera); 78 | editImage(file.path, context); 79 | }, 80 | child: const Text('Câmera'), 81 | ), 82 | CupertinoActionSheetAction( 83 | onPressed: () async { 84 | final PickedFile file = 85 | await picker.getImage(source: ImageSource.gallery); 86 | editImage(file.path, context); 87 | }, 88 | child: const Text('Galeria'), 89 | ) 90 | ], 91 | ); 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /lib/screens/home/components/item_tile.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | 3 | import 'package:flutter/material.dart'; 4 | import 'package:lojavirtual/models/home_manager.dart'; 5 | import 'package:lojavirtual/models/product.dart'; 6 | import 'package:lojavirtual/models/product_manager.dart'; 7 | import 'package:lojavirtual/models/section.dart'; 8 | import 'package:lojavirtual/models/section_item.dart'; 9 | import 'package:provider/provider.dart'; 10 | import 'package:transparent_image/transparent_image.dart'; 11 | 12 | class ItemTile extends StatelessWidget { 13 | 14 | const ItemTile(this.item); 15 | 16 | final SectionItem item; 17 | 18 | @override 19 | Widget build(BuildContext context) { 20 | final homeManager = context.watch(); 21 | 22 | return GestureDetector( 23 | onTap: (){ 24 | if(item.product != null){ 25 | final product = context.read() 26 | .findProductById(item.product); 27 | if(product != null){ 28 | Navigator.of(context).pushNamed('/product', arguments: product); 29 | } 30 | } 31 | }, 32 | onLongPress: homeManager.editing ? (){ 33 | showDialog( 34 | context: context, 35 | builder: (_){ 36 | final product = context.read() 37 | .findProductById(item.product); 38 | return AlertDialog( 39 | title: const Text('Editar Item'), 40 | content: product != null 41 | ? ListTile( 42 | contentPadding: EdgeInsets.zero, 43 | leading: Image.network(product.images.first), 44 | title: Text(product.name), 45 | subtitle: Text('R\$ ${product.basePrice.toStringAsFixed(2)}'), 46 | ) 47 | : null, 48 | actions: [ 49 | FlatButton( 50 | onPressed: (){ 51 | context.read
().removeItem(item); 52 | Navigator.of(context).pop(); 53 | }, 54 | textColor: Colors.red, 55 | child: const Text('Excluir'), 56 | ), 57 | FlatButton( 58 | onPressed: () async { 59 | if(product != null){ 60 | item.product = null; 61 | } else { 62 | final Product product = await Navigator.of(context) 63 | .pushNamed('/select_product') as Product; 64 | item.product = product?.id; 65 | } 66 | Navigator.of(context).pop(); 67 | }, 68 | child: Text( 69 | product != null 70 | ? 'Desvincular' 71 | : 'Vincular' 72 | ), 73 | ), 74 | ], 75 | ); 76 | } 77 | ); 78 | } : null, 79 | child: AspectRatio( 80 | aspectRatio: 1, 81 | child: item.image is String 82 | ? FadeInImage.memoryNetwork( 83 | placeholder: kTransparentImage, 84 | image: item.image as String, 85 | fit: BoxFit.cover, 86 | ) 87 | : Image.file(item.image as File, fit: BoxFit.cover,), 88 | ), 89 | ); 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /lib/screens/base/base_screen.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | 3 | import 'package:firebase_messaging/firebase_messaging.dart'; 4 | import 'package:flushbar/flushbar.dart'; 5 | import 'package:flutter/material.dart'; 6 | import 'package:flutter/services.dart'; 7 | import 'package:lojavirtual/models/page_manager.dart'; 8 | import 'package:lojavirtual/models/user_manager.dart'; 9 | import 'package:lojavirtual/screens/admin_orders/admin_orders_screen.dart'; 10 | import 'package:lojavirtual/screens/admin_users/admin_users_screen.dart'; 11 | import 'package:lojavirtual/screens/home/home_screen.dart'; 12 | import 'package:lojavirtual/screens/orders/orders_screen.dart'; 13 | import 'package:lojavirtual/screens/products/products_screen.dart'; 14 | import 'package:lojavirtual/screens/stores/stores_screen.dart'; 15 | import 'package:provider/provider.dart'; 16 | 17 | class BaseScreen extends StatefulWidget { 18 | 19 | @override 20 | _BaseScreenState createState() => _BaseScreenState(); 21 | } 22 | 23 | class _BaseScreenState extends State { 24 | 25 | final PageController pageController = PageController(); 26 | 27 | @override 28 | void initState() { 29 | super.initState(); 30 | 31 | SystemChrome.setPreferredOrientations([ 32 | DeviceOrientation.portraitUp 33 | ]); 34 | 35 | configFCM(); 36 | } 37 | 38 | void configFCM(){ 39 | final fcm = FirebaseMessaging(); 40 | 41 | if(Platform.isIOS){ 42 | fcm.requestNotificationPermissions( 43 | const IosNotificationSettings(provisional: true) 44 | ); 45 | } 46 | 47 | fcm.configure( 48 | onLaunch: (Map message) async { 49 | print('onLaunch $message'); 50 | }, 51 | onResume: (Map message) async { 52 | print('onResume $message'); 53 | }, 54 | onMessage: (Map message) async { 55 | showNotification( 56 | message['notification']['title'] as String, 57 | message['notification']['body'] as String, 58 | ); 59 | } 60 | ); 61 | } 62 | 63 | void showNotification(String title, String message){ 64 | Flushbar( 65 | title: title, 66 | message: message, 67 | flushbarPosition: FlushbarPosition.TOP, 68 | flushbarStyle: FlushbarStyle.GROUNDED, 69 | isDismissible: true, 70 | backgroundColor: Theme.of(context).primaryColor, 71 | duration: const Duration(seconds: 5), 72 | icon: Icon(Icons.shopping_cart, color: Colors.white,), 73 | ).show(context); 74 | } 75 | 76 | @override 77 | Widget build(BuildContext context) { 78 | return Provider( 79 | create: (_) => PageManager(pageController), 80 | child: Consumer( 81 | builder: (_, userManager, __){ 82 | return PageView( 83 | controller: pageController, 84 | physics: const NeverScrollableScrollPhysics(), 85 | children: [ 86 | HomeScreen(), 87 | ProductsScreen(), 88 | OrdersScreen(), 89 | StoresScreen(), 90 | if(userManager.adminEnabled) 91 | ...[ 92 | AdminUsersScreen(), 93 | AdminOrdersScreen(), 94 | ] 95 | ], 96 | ); 97 | }, 98 | ), 99 | ); 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /lib/screens/checkout/components/credit_card_widget.dart: -------------------------------------------------------------------------------- 1 | import 'package:flip_card/flip_card.dart'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:keyboard_actions/keyboard_actions.dart'; 4 | import 'package:lojavirtual/models/credit_card.dart'; 5 | import 'package:lojavirtual/screens/checkout/components/card_back.dart'; 6 | import 'package:lojavirtual/screens/checkout/components/card_front.dart'; 7 | 8 | class CreditCardWidget extends StatefulWidget { 9 | 10 | const CreditCardWidget(this.creditCard); 11 | 12 | final CreditCard creditCard; 13 | 14 | @override 15 | _CreditCardWidgetState createState() => _CreditCardWidgetState(); 16 | } 17 | 18 | class _CreditCardWidgetState extends State { 19 | final GlobalKey cardKey = GlobalKey(); 20 | 21 | final FocusNode numberFocus = FocusNode(); 22 | final FocusNode dateFocus = FocusNode(); 23 | final FocusNode nameFocus = FocusNode(); 24 | final FocusNode cvvFocus = FocusNode(); 25 | 26 | KeyboardActionsConfig _buildConfig(BuildContext context){ 27 | return KeyboardActionsConfig( 28 | keyboardActionsPlatform: KeyboardActionsPlatform.IOS, 29 | keyboardBarColor: Colors.grey[200], 30 | actions: [ 31 | KeyboardAction(focusNode: numberFocus, displayDoneButton: false), 32 | KeyboardAction(focusNode: dateFocus, displayDoneButton: false), 33 | KeyboardAction( 34 | focusNode: nameFocus, 35 | toolbarButtons: [ 36 | (_){ 37 | return GestureDetector( 38 | onTap: (){ 39 | cardKey.currentState.toggleCard(); 40 | cvvFocus.requestFocus(); 41 | }, 42 | child: const Padding( 43 | padding: EdgeInsets.only(right: 8), 44 | child: Text('CONTINUAR'), 45 | ), 46 | ); 47 | } 48 | ] 49 | ), 50 | ] 51 | ); 52 | } 53 | 54 | @override 55 | Widget build(BuildContext context) { 56 | return KeyboardActions( 57 | config: _buildConfig(context), 58 | autoScroll: false, 59 | child: Padding( 60 | padding: const EdgeInsets.fromLTRB(16, 16, 16, 8), 61 | child: Column( 62 | crossAxisAlignment: CrossAxisAlignment.end, 63 | children: [ 64 | FlipCard( 65 | key: cardKey, 66 | direction: FlipDirection.HORIZONTAL, 67 | speed: 700, 68 | flipOnTouch: false, 69 | front: CardFront( 70 | creditCard: widget.creditCard, 71 | numberFocus: numberFocus, 72 | dateFocus: dateFocus, 73 | nameFocus: nameFocus, 74 | finished: (){ 75 | cardKey.currentState.toggleCard(); 76 | cvvFocus.requestFocus(); 77 | }, 78 | ), 79 | back: CardBack( 80 | creditCard: widget.creditCard, 81 | cvvFocus: cvvFocus, 82 | ), 83 | ), 84 | FlatButton( 85 | onPressed: (){ 86 | cardKey.currentState.toggleCard(); 87 | }, 88 | textColor: Colors.white, 89 | padding: EdgeInsets.zero, 90 | child: const Text('Virar cartão'), 91 | ) 92 | ], 93 | ), 94 | ), 95 | ); 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /lib/models/store.dart: -------------------------------------------------------------------------------- 1 | import 'package:cloud_firestore/cloud_firestore.dart'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:lojavirtual/models/address.dart'; 4 | import 'package:lojavirtual/helpers/extensions.dart'; 5 | 6 | enum StoreStatus { closed, open, closing } 7 | 8 | class Store { 9 | 10 | Store.fromDocument(DocumentSnapshot doc){ 11 | name = doc.data['name'] as String; 12 | image = doc.data['image'] as String; 13 | phone = doc.data['phone'] as String; 14 | address = Address.fromMap(doc.data['address'] as Map); 15 | 16 | opening = (doc.data['opening'] as Map).map((key, value) { 17 | final timesString = value as String; 18 | 19 | if(timesString != null && timesString.isNotEmpty){ 20 | final splitted = timesString.split(RegExp("[:-]")); 21 | 22 | return MapEntry( 23 | key, 24 | { 25 | "from": TimeOfDay( 26 | hour: int.parse(splitted[0]), 27 | minute: int.parse(splitted[1]) 28 | ), 29 | "to": TimeOfDay( 30 | hour: int.parse(splitted[2]), 31 | minute: int.parse(splitted[3]) 32 | ), 33 | } 34 | ); 35 | } else { 36 | return MapEntry(key, null); 37 | } 38 | }); 39 | 40 | updateStatus(); 41 | } 42 | 43 | String name; 44 | String image; 45 | String phone; 46 | Address address; 47 | Map> opening; 48 | 49 | StoreStatus status; 50 | 51 | String get addressText => 52 | '${address.street}, ${address.number}${address.complement.isNotEmpty ? ' - ${address.complement}' : ''} - ' 53 | '${address.district}, ${address.city}/${address.state}'; 54 | 55 | String get openingText { 56 | return 57 | 'Seg-Sex: ${formattedPeriod(opening['monfri'])}\n' 58 | 'Sab: ${formattedPeriod(opening['saturday'])}\n' 59 | 'Dom: ${formattedPeriod(opening['sunday'])}'; 60 | } 61 | 62 | String formattedPeriod(Map period){ 63 | if(period == null) return "Fechada"; 64 | return '${period['from'].formatted()} - ${period['to'].formatted()}'; 65 | } 66 | 67 | String get cleanPhone => phone.replaceAll(RegExp(r"[^\d]"), ""); 68 | 69 | void updateStatus(){ 70 | final weekDay = DateTime.now().weekday; 71 | 72 | Map period; 73 | if(weekDay >= 1 && weekDay <= 5){ 74 | period = opening['monfri']; 75 | } else if(weekDay == 6){ 76 | period = opening['saturday']; 77 | } else { 78 | period = opening['sunday']; 79 | } 80 | 81 | final now = TimeOfDay.now(); 82 | 83 | if(period == null){ 84 | status = StoreStatus.closed; 85 | } else if(period['from'].toMinutes() < now.toMinutes() 86 | && period['to'].toMinutes() - 15 > now.toMinutes()){ 87 | status = StoreStatus.open; 88 | } else if(period['from'].toMinutes() < now.toMinutes() 89 | && period['to'].toMinutes() > now.toMinutes()){ 90 | status = StoreStatus.closing; 91 | } else { 92 | status = StoreStatus.closed; 93 | } 94 | } 95 | 96 | String get statusText { 97 | switch(status){ 98 | case StoreStatus.closed: 99 | return 'Fechada'; 100 | case StoreStatus.open: 101 | return 'Aberta'; 102 | case StoreStatus.closing: 103 | return 'Fechando'; 104 | default: 105 | return ''; 106 | } 107 | } 108 | 109 | } -------------------------------------------------------------------------------- /ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 32 | 33 | 39 | 40 | 41 | 42 | 43 | 44 | 54 | 56 | 62 | 63 | 64 | 65 | 66 | 67 | 73 | 75 | 81 | 82 | 83 | 84 | 86 | 87 | 90 | 91 | 92 | -------------------------------------------------------------------------------- /lib/common/order/order_tile.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:lojavirtual/common/order/cancel_order_dialog.dart'; 3 | import 'package:lojavirtual/common/order/export_address_dialog.dart'; 4 | import 'package:lojavirtual/common/order/order_product_tile.dart'; 5 | import 'package:lojavirtual/models/order.dart'; 6 | 7 | class OrderTile extends StatelessWidget { 8 | 9 | const OrderTile(this.order, {this.showControls = false}); 10 | 11 | final Order order; 12 | final bool showControls; 13 | 14 | @override 15 | Widget build(BuildContext context) { 16 | final primaryColor = Theme.of(context).primaryColor; 17 | 18 | return Card( 19 | margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 4), 20 | child: ExpansionTile( 21 | title: Row( 22 | mainAxisAlignment: MainAxisAlignment.spaceBetween, 23 | children: [ 24 | Column( 25 | crossAxisAlignment: CrossAxisAlignment.start, 26 | children: [ 27 | Text( 28 | order.formattedId, 29 | style: TextStyle( 30 | fontWeight: FontWeight.w600, 31 | color: primaryColor, 32 | ), 33 | ), 34 | Text( 35 | 'R\$ ${order.price.toStringAsFixed(2)}', 36 | style: TextStyle( 37 | fontWeight: FontWeight.w600, 38 | color: Colors.black, 39 | fontSize: 14, 40 | ), 41 | ), 42 | ], 43 | ), 44 | Text( 45 | order.statusText, 46 | style: TextStyle( 47 | fontWeight: FontWeight.w400, 48 | color: order.status == Status.canceled ? 49 | Colors.red : primaryColor, 50 | fontSize: 14 51 | ), 52 | ) 53 | ], 54 | ), 55 | children: [ 56 | Column( 57 | children: order.items.map((e){ 58 | return OrderProductTile(e); 59 | }).toList(), 60 | ), 61 | if(showControls && order.status != Status.canceled) 62 | SizedBox( 63 | height: 50, 64 | child: ListView( 65 | scrollDirection: Axis.horizontal, 66 | children: [ 67 | FlatButton( 68 | onPressed: (){ 69 | showDialog( 70 | context: context, 71 | barrierDismissible: false, 72 | builder: (_) => CancelOrderDialog(order) 73 | ); 74 | }, 75 | textColor: Colors.red, 76 | child: const Text('Cancelar'), 77 | ), 78 | FlatButton( 79 | onPressed: order.back, 80 | child: const Text('Recuar'), 81 | ), 82 | FlatButton( 83 | onPressed: order.advance, 84 | child: const Text('Avançar'), 85 | ), 86 | FlatButton( 87 | onPressed: (){ 88 | showDialog(context: context, 89 | builder: (_) => ExportAddressDialog(order.address) 90 | ); 91 | }, 92 | textColor: primaryColor, 93 | child: const Text('Endereço'), 94 | ) 95 | ], 96 | ), 97 | ) 98 | ], 99 | ), 100 | ); 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /lib/screens/address/components/cep_input_field.dart: -------------------------------------------------------------------------------- 1 | import 'package:brasil_fields/brasil_fields.dart'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:flutter/services.dart'; 4 | import 'package:lojavirtual/common/custom_icon_button.dart'; 5 | import 'package:lojavirtual/models/address.dart'; 6 | import 'package:lojavirtual/models/cart_manager.dart'; 7 | import 'package:provider/provider.dart'; 8 | 9 | class CepInputField extends StatefulWidget { 10 | 11 | const CepInputField(this.address); 12 | 13 | final Address address; 14 | 15 | @override 16 | _CepInputFieldState createState() => _CepInputFieldState(); 17 | } 18 | 19 | class _CepInputFieldState extends State { 20 | 21 | final TextEditingController cepController = TextEditingController(); 22 | 23 | @override 24 | Widget build(BuildContext context) { 25 | final cartManager = context.watch(); 26 | final primaryColor = Theme.of(context).primaryColor; 27 | 28 | if(widget.address.zipCode == null) 29 | return Column( 30 | crossAxisAlignment: CrossAxisAlignment.stretch, 31 | children: [ 32 | TextFormField( 33 | enabled: !cartManager.loading, 34 | controller: cepController, 35 | decoration: const InputDecoration( 36 | isDense: true, 37 | labelText: 'CEP', 38 | hintText: '12.345-678' 39 | ), 40 | inputFormatters: [ 41 | WhitelistingTextInputFormatter.digitsOnly, 42 | CepInputFormatter(), 43 | ], 44 | keyboardType: TextInputType.number, 45 | validator: (cep){ 46 | if(cep.isEmpty) 47 | return 'Campo obrigatório'; 48 | else if(cep.length != 10) 49 | return 'CEP Inválido'; 50 | return null; 51 | }, 52 | ), 53 | if(cartManager.loading) 54 | LinearProgressIndicator( 55 | valueColor: AlwaysStoppedAnimation(primaryColor), 56 | backgroundColor: Colors.transparent, 57 | ), 58 | RaisedButton( 59 | onPressed: !cartManager.loading ? () async { 60 | if(Form.of(context).validate()){ 61 | try { 62 | await context.read().getAddress(cepController.text); 63 | } catch (e){ 64 | Scaffold.of(context).showSnackBar( 65 | SnackBar( 66 | content: Text('$e'), 67 | backgroundColor: Colors.red, 68 | ) 69 | ); 70 | } 71 | } 72 | } : null, 73 | textColor: Colors.white, 74 | color: primaryColor, 75 | disabledColor: primaryColor.withAlpha(100), 76 | child: const Text('Buscar CEP'), 77 | ), 78 | ], 79 | ); 80 | else 81 | return Padding( 82 | padding: const EdgeInsets.symmetric(vertical: 4), 83 | child: Row( 84 | children: [ 85 | Expanded( 86 | child: Text( 87 | 'CEP: ${widget.address.zipCode}', 88 | style: TextStyle( 89 | color: primaryColor, 90 | fontWeight: FontWeight.w600 91 | ), 92 | ), 93 | ), 94 | CustomIconButton( 95 | iconData: Icons.edit, 96 | color: primaryColor, 97 | size: 20, 98 | onTap: (){ 99 | context.read().removeAddress(); 100 | }, 101 | ), 102 | ], 103 | ), 104 | ); 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /lib/models/order.dart: -------------------------------------------------------------------------------- 1 | import 'package:cloud_firestore/cloud_firestore.dart'; 2 | import 'package:flutter/cupertino.dart'; 3 | import 'package:lojavirtual/models/address.dart'; 4 | import 'package:lojavirtual/models/cart_manager.dart'; 5 | import 'package:lojavirtual/models/cart_product.dart'; 6 | import 'package:lojavirtual/services/cielo_payment.dart'; 7 | 8 | enum Status { canceled, preparing, transporting, delivered } 9 | 10 | class Order { 11 | 12 | Order.fromCartManager(CartManager cartManager){ 13 | items = List.from(cartManager.items); 14 | price = cartManager.totalPrice; 15 | userId = cartManager.user.id; 16 | address = cartManager.address; 17 | status = Status.preparing; 18 | } 19 | 20 | Order.fromDocument(DocumentSnapshot doc){ 21 | orderId = doc.documentID; 22 | 23 | items = (doc.data['items'] as List).map((e){ 24 | return CartProduct.fromMap(e as Map); 25 | }).toList(); 26 | 27 | price = doc.data['price'] as num; 28 | userId = doc.data['user'] as String; 29 | address = Address.fromMap(doc.data['address'] as Map); 30 | date = doc.data['date'] as Timestamp; 31 | 32 | status = Status.values[doc.data['status'] as int]; 33 | 34 | payId = doc.data['payId'] as String; 35 | } 36 | 37 | final Firestore firestore = Firestore.instance; 38 | 39 | DocumentReference get firestoreRef => 40 | firestore.collection('orders').document(orderId); 41 | 42 | void updateFromDocument(DocumentSnapshot doc){ 43 | status = Status.values[doc.data['status'] as int]; 44 | } 45 | 46 | Future save() async { 47 | firestore.collection('orders').document(orderId).setData( 48 | { 49 | 'items': items.map((e) => e.toOrderItemMap()).toList(), 50 | 'price': price, 51 | 'user': userId, 52 | 'address': address.toMap(), 53 | 'status': status.index, 54 | 'date': Timestamp.now(), 55 | 'payId': payId, 56 | } 57 | ); 58 | } 59 | 60 | Function() get back { 61 | return status.index >= Status.transporting.index ? 62 | (){ 63 | status = Status.values[status.index - 1]; 64 | firestoreRef.updateData({'status': status.index}); 65 | } : null; 66 | } 67 | 68 | Function() get advance { 69 | return status.index <= Status.transporting.index ? 70 | (){ 71 | status = Status.values[status.index + 1]; 72 | firestoreRef.updateData({'status': status.index}); 73 | } : null; 74 | } 75 | 76 | Future cancel() async { 77 | try { 78 | await CieloPayment().cancel(payId); 79 | 80 | status = Status.canceled; 81 | firestoreRef.updateData({'status': status.index}); 82 | } catch (e){ 83 | debugPrint('Erro ao cancelar'); 84 | return Future.error('Falha ao cancelar'); 85 | } 86 | } 87 | 88 | String orderId; 89 | String payId; 90 | 91 | List items; 92 | num price; 93 | 94 | String userId; 95 | 96 | Address address; 97 | 98 | Status status; 99 | 100 | Timestamp date; 101 | 102 | String get formattedId => '#${orderId.padLeft(6, '0')}'; 103 | 104 | String get statusText => getStatusText(status); 105 | 106 | static String getStatusText(Status status) { 107 | switch(status){ 108 | case Status.canceled: 109 | return 'Cancelado'; 110 | case Status.preparing: 111 | return 'Em preparação'; 112 | case Status.transporting: 113 | return 'Em transporte'; 114 | case Status.delivered: 115 | return 'Entregue'; 116 | default: 117 | return ''; 118 | } 119 | } 120 | 121 | @override 122 | String toString() { 123 | return 'Order{firestore: $firestore, orderId: $orderId, items: $items, price: $price, userId: $userId, address: $address, date: $date}'; 124 | } 125 | } -------------------------------------------------------------------------------- /lib/screens/checkout/components/card_text_field.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter/services.dart'; 3 | 4 | class CardTextField extends StatelessWidget { 5 | 6 | const CardTextField({ 7 | this.title, 8 | this.bold = false, 9 | this.hint, 10 | this.textInputType, 11 | this.inputFormatters, 12 | this.validator, 13 | this.maxLength, 14 | this.textAlign = TextAlign.start, 15 | this.focusNode, 16 | this.onSubmitted, 17 | this.onSaved, 18 | this.initialValue, 19 | }) : textInputAction = onSubmitted == null 20 | ? TextInputAction.done 21 | : TextInputAction.next; 22 | 23 | final String title; 24 | final bool bold; 25 | final String hint; 26 | final TextInputType textInputType; 27 | final List inputFormatters; 28 | final FormFieldValidator validator; 29 | final int maxLength; 30 | final TextAlign textAlign; 31 | final FocusNode focusNode; 32 | final Function(String) onSubmitted; 33 | final TextInputAction textInputAction; 34 | final FormFieldSetter onSaved; 35 | final String initialValue; 36 | 37 | @override 38 | Widget build(BuildContext context) { 39 | return FormField( 40 | initialValue: initialValue, 41 | validator: validator, 42 | onSaved: onSaved, 43 | builder: (state){ 44 | return Padding( 45 | padding: const EdgeInsets.symmetric(vertical: 2), 46 | child: Column( 47 | crossAxisAlignment: CrossAxisAlignment.start, 48 | children: [ 49 | if(title != null) 50 | Row( 51 | children: [ 52 | Text( 53 | title, 54 | style: TextStyle( 55 | fontSize: 10, 56 | fontWeight: FontWeight.w400, 57 | color: Colors.white 58 | ), 59 | ), 60 | if(state.hasError) 61 | const Text( 62 | ' Inválido', 63 | style: TextStyle( 64 | color: Colors.red, 65 | fontSize: 9, 66 | ), 67 | ) 68 | ], 69 | ), 70 | TextFormField( 71 | initialValue: initialValue, 72 | style: TextStyle( 73 | color: title == null && state.hasError 74 | ? Colors.red : Colors.white, 75 | fontWeight: bold ? FontWeight.bold : FontWeight.w500, 76 | ), 77 | cursorColor: Colors.white, 78 | decoration: InputDecoration( 79 | hintText: hint, 80 | hintStyle: TextStyle( 81 | color: title == null && state.hasError 82 | ? Colors.red.withAlpha(200) 83 | : Colors.white.withAlpha(100) 84 | ), 85 | border: InputBorder.none, 86 | isDense: true, 87 | contentPadding: const EdgeInsets.symmetric(vertical: 2), 88 | counterText: '', 89 | ), 90 | keyboardType: textInputType, 91 | inputFormatters: inputFormatters, 92 | onChanged: (text){ 93 | state.didChange(text); 94 | }, 95 | maxLength: maxLength, 96 | textAlign: textAlign, 97 | focusNode: focusNode, 98 | onFieldSubmitted: onSubmitted, 99 | textInputAction: textInputAction, 100 | ), 101 | ], 102 | ), 103 | ); 104 | }, 105 | ); 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /lib/models/section.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | 3 | import 'package:cloud_firestore/cloud_firestore.dart'; 4 | import 'package:firebase_storage/firebase_storage.dart'; 5 | import 'package:flutter/cupertino.dart'; 6 | import 'package:lojavirtual/models/section_item.dart'; 7 | import 'package:uuid/uuid.dart'; 8 | 9 | class Section extends ChangeNotifier { 10 | 11 | Section({this.id, this.name, this.type, this.items}){ 12 | items = items ?? []; 13 | originalItems = List.from(items); 14 | } 15 | 16 | Section.fromDocument(DocumentSnapshot document){ 17 | id = document.documentID; 18 | name = document.data['name'] as String; 19 | type = document.data['type'] as String; 20 | items = (document.data['items'] as List).map( 21 | (i) => SectionItem.fromMap(i as Map)).toList(); 22 | } 23 | 24 | final Firestore firestore = Firestore.instance; 25 | final FirebaseStorage storage = FirebaseStorage.instance; 26 | 27 | DocumentReference get firestoreRef => firestore.document('home/$id'); 28 | StorageReference get storageRef => storage.ref().child('home/$id'); 29 | 30 | String id; 31 | String name; 32 | String type; 33 | List items; 34 | List originalItems; 35 | 36 | String _error; 37 | String get error => _error; 38 | set error(String value){ 39 | _error = value; 40 | notifyListeners(); 41 | } 42 | 43 | void addItem(SectionItem item){ 44 | items.add(item); 45 | notifyListeners(); 46 | } 47 | 48 | void removeItem(SectionItem item){ 49 | items.remove(item); 50 | notifyListeners(); 51 | } 52 | 53 | Future save(int pos) async { 54 | final Map data = { 55 | 'name': name, 56 | 'type': type, 57 | 'pos': pos, 58 | }; 59 | 60 | if(id == null){ 61 | final doc = await firestore.collection('home').add(data); 62 | id = doc.documentID; 63 | } else { 64 | await firestoreRef.updateData(data); 65 | } 66 | 67 | for(final item in items){ 68 | if(item.image is File){ 69 | final StorageUploadTask task = storageRef.child(Uuid().v1()) 70 | .putFile(item.image as File); 71 | final StorageTaskSnapshot snapshot = await task.onComplete; 72 | final String url = await snapshot.ref.getDownloadURL() as String; 73 | item.image = url; 74 | } 75 | } 76 | 77 | for(final original in originalItems){ 78 | if(!items.contains(original) 79 | && (original.image as String).contains('firebase')){ 80 | try { 81 | final ref = await storage.getReferenceFromUrl( 82 | original.image as String 83 | ); 84 | await ref.delete(); 85 | // ignore: empty_catches 86 | } catch (e){} 87 | } 88 | } 89 | 90 | final Map itemsData = { 91 | 'items': items.map((e) => e.toMap()).toList() 92 | }; 93 | 94 | await firestoreRef.updateData(itemsData); 95 | } 96 | 97 | Future delete() async { 98 | await firestoreRef.delete(); 99 | for(final item in items){ 100 | if((item.image as String).contains('firebase')){ 101 | try { 102 | final ref = await storage.getReferenceFromUrl( 103 | item.image as String 104 | ); 105 | await ref.delete(); 106 | // ignore: empty_catches 107 | } catch (e){} 108 | } 109 | } 110 | } 111 | 112 | bool valid(){ 113 | if(name == null || name.isEmpty){ 114 | error = 'Título inválido'; 115 | } else if(items.isEmpty){ 116 | error = 'Insira ao menos uma imagem'; 117 | } else { 118 | error = null; 119 | } 120 | return error == null; 121 | } 122 | 123 | Section clone(){ 124 | return Section( 125 | id: id, 126 | name: name, 127 | type: type, 128 | items: items.map((e) => e.clone()).toList(), 129 | ); 130 | } 131 | 132 | @override 133 | String toString() { 134 | return 'Section{name: $name, type: $type, items: $items}'; 135 | } 136 | } -------------------------------------------------------------------------------- /lib/models/user_manager.dart: -------------------------------------------------------------------------------- 1 | import 'package:cloud_firestore/cloud_firestore.dart'; 2 | import 'package:firebase_auth/firebase_auth.dart'; 3 | import 'package:flutter/cupertino.dart'; 4 | import 'package:flutter/services.dart'; 5 | import 'package:flutter_facebook_login/flutter_facebook_login.dart'; 6 | import 'package:lojavirtual/helpers/firebase_errors.dart'; 7 | import 'package:lojavirtual/models/user.dart'; 8 | 9 | class UserManager extends ChangeNotifier { 10 | 11 | UserManager(){ 12 | _loadCurrentUser(); 13 | } 14 | 15 | final FirebaseAuth auth = FirebaseAuth.instance; 16 | final Firestore firestore = Firestore.instance; 17 | 18 | User user; 19 | 20 | bool _loading = false; 21 | bool get loading => _loading; 22 | set loading(bool value){ 23 | _loading = value; 24 | notifyListeners(); 25 | } 26 | 27 | bool _loadingFace = false; 28 | bool get loadingFace => _loadingFace; 29 | set loadingFace(bool value){ 30 | _loadingFace = value; 31 | notifyListeners(); 32 | } 33 | 34 | bool get isLoggedIn => user != null; 35 | 36 | Future signIn({User user, Function onFail, Function onSuccess}) async { 37 | loading = true; 38 | try { 39 | final AuthResult result = await auth.signInWithEmailAndPassword( 40 | email: user.email, password: user.password); 41 | 42 | await _loadCurrentUser(firebaseUser: result.user); 43 | 44 | onSuccess(); 45 | } on PlatformException catch (e){ 46 | onFail(getErrorString(e.code)); 47 | } 48 | loading = false; 49 | } 50 | 51 | Future facebookLogin({Function onFail, Function onSuccess}) async { 52 | loadingFace = true; 53 | 54 | final result = await FacebookLogin().logIn(['email', 'public_profile']); 55 | 56 | switch(result.status){ 57 | case FacebookLoginStatus.loggedIn: 58 | final credential = FacebookAuthProvider.getCredential( 59 | accessToken: result.accessToken.token 60 | ); 61 | 62 | final authResult = await auth.signInWithCredential(credential); 63 | 64 | if(authResult.user != null){ 65 | final firebaseUser = authResult.user; 66 | 67 | user = User( 68 | id: firebaseUser.uid, 69 | name: firebaseUser.displayName, 70 | email: firebaseUser.email 71 | ); 72 | 73 | await user.saveData(); 74 | 75 | user.saveToken(); 76 | 77 | onSuccess(); 78 | } 79 | break; 80 | case FacebookLoginStatus.cancelledByUser: 81 | break; 82 | case FacebookLoginStatus.error: 83 | onFail(result.errorMessage); 84 | break; 85 | } 86 | 87 | loadingFace = false; 88 | } 89 | 90 | Future signUp({User user, Function onFail, Function onSuccess}) async { 91 | loading = true; 92 | try { 93 | final AuthResult result = await auth.createUserWithEmailAndPassword( 94 | email: user.email, password: user.password); 95 | 96 | user.id = result.user.uid; 97 | this.user = user; 98 | 99 | await user.saveData(); 100 | 101 | user.saveToken(); 102 | 103 | onSuccess(); 104 | } on PlatformException catch (e){ 105 | onFail(getErrorString(e.code)); 106 | } 107 | loading = false; 108 | } 109 | 110 | void signOut(){ 111 | auth.signOut(); 112 | user = null; 113 | notifyListeners(); 114 | } 115 | 116 | Future _loadCurrentUser({FirebaseUser firebaseUser}) async { 117 | final FirebaseUser currentUser = firebaseUser ?? await auth.currentUser(); 118 | if(currentUser != null){ 119 | final DocumentSnapshot docUser = await firestore.collection('users') 120 | .document(currentUser.uid).get(); 121 | user = User.fromDocument(docUser); 122 | 123 | user.saveToken(); 124 | 125 | final docAdmin = await firestore.collection('admins').document(user.id).get(); 126 | if(docAdmin.exists){ 127 | user.admin = true; 128 | } 129 | 130 | notifyListeners(); 131 | } 132 | } 133 | 134 | bool get adminEnabled => user != null && user.admin; 135 | } -------------------------------------------------------------------------------- /pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: lojavirtual 2 | description: A new Flutter application. 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 | 27 | 28 | # The following adds the Cupertino Icons font to your application. 29 | # Use with the CupertinoIcons class for iOS style icons. 30 | cupertino_icons: ^0.1.3 31 | cloud_firestore: ^0.13.6 32 | firebase_auth: ^0.16.1 33 | firebase_storage: ^3.1.6 34 | provider: ^4.1.3 35 | carousel_pro: ^1.0.0 36 | flutter_staggered_grid_view: ^0.3.0 37 | transparent_image: ^1.0.0 38 | alphabet_list_scroll_view: ^1.0.6 39 | faker: ^1.2.1 40 | image_picker: ^0.6.7+1 41 | image_cropper: ^1.2.2 42 | uuid: ^2.0.4 43 | dio: ^3.0.9 44 | brasil_fields: ^0.2.0 45 | geolocator: ^5.3.2+2 46 | sliding_up_panel: ^1.0.2 47 | screenshot: ^0.1.1 48 | gallery_saver: ^2.0.1 49 | flutter_signin_button: ^1.0.0 50 | flutter_facebook_login: ^3.0.0 51 | url_launcher: ^5.4.11 52 | map_launcher: ^0.5.0 53 | flip_card: ^0.4.4 54 | mask_text_input_formatter: ^1.0.7 55 | credit_card_type_detector: ^1.1.0 56 | keyboard_actions: ^3.2.1+1 57 | cloud_functions: ^0.5.0 58 | cpf_cnpj_validator: ^1.0.5 59 | firebase_messaging: ^6.0.16 60 | flushbar: ^1.10.4 61 | 62 | dev_dependencies: 63 | flutter_test: 64 | sdk: flutter 65 | 66 | lint: ^1.2.0 67 | 68 | # For information on the generic Dart part of this file, see the 69 | # following page: https://dart.dev/tools/pub/pubspec 70 | 71 | # The following section is specific to Flutter. 72 | flutter: 73 | 74 | # The following line ensures that the Material Icons font is 75 | # included with your application, so that you can use the icons in 76 | # the material Icons class. 77 | uses-material-design: true 78 | 79 | # To add assets to your application, add an assets section, like this: 80 | # assets: 81 | # - images/a_dot_burr.jpeg 82 | # - images/a_dot_ham.jpeg 83 | 84 | # An image asset can refer to one or more resolution-specific "variants", see 85 | # https://flutter.dev/assets-and-images/#resolution-aware. 86 | 87 | # For details regarding adding assets from package dependencies, see 88 | # https://flutter.dev/assets-and-images/#from-packages 89 | 90 | # To add custom fonts to your application, add a fonts section here, 91 | # in this "flutter" section. Each entry in this list should have a 92 | # "family" key with the font family name, and a "fonts" key with a 93 | # list giving the asset and other descriptors for the font. For 94 | # example: 95 | # fonts: 96 | # - family: Schyler 97 | # fonts: 98 | # - asset: fonts/Schyler-Regular.ttf 99 | # - asset: fonts/Schyler-Italic.ttf 100 | # style: italic 101 | # - family: Trajan Pro 102 | # fonts: 103 | # - asset: fonts/TrajanPro.ttf 104 | # - asset: fonts/TrajanPro_Bold.ttf 105 | # weight: 700 106 | # 107 | # For details regarding fonts from package dependencies, 108 | # see https://flutter.dev/custom-fonts/#from-packages 109 | -------------------------------------------------------------------------------- /lib/screens/edit_product/components/images_form.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | 3 | import 'package:carousel_pro/carousel_pro.dart'; 4 | import 'package:flutter/cupertino.dart'; 5 | import 'package:flutter/material.dart'; 6 | import 'package:lojavirtual/models/product.dart'; 7 | import 'package:lojavirtual/screens/edit_product/components/image_source_sheet.dart'; 8 | 9 | class ImagesForm extends StatelessWidget { 10 | 11 | const ImagesForm(this.product); 12 | 13 | final Product product; 14 | 15 | @override 16 | Widget build(BuildContext context) { 17 | return FormField>( 18 | initialValue: List.from(product.images), 19 | validator: (images){ 20 | if(images.isEmpty) 21 | return 'Insira ao menos uma imagem'; 22 | return null; 23 | }, 24 | onSaved: (images) => product.newImages = images, 25 | builder: (state){ 26 | void onImageSelected(File file){ 27 | state.value.add(file); 28 | state.didChange(state.value); 29 | Navigator.of(context).pop(); 30 | } 31 | 32 | return Column( 33 | children: [ 34 | AspectRatio( 35 | aspectRatio: 1, 36 | child: Carousel( 37 | images: state.value.map((image){ 38 | return Stack( 39 | fit: StackFit.expand, 40 | children: [ 41 | if(image is String) 42 | Image.network(image, fit: BoxFit.cover,) 43 | else 44 | Image.file(image as File, fit: BoxFit.cover,), 45 | Align( 46 | alignment: Alignment.topRight, 47 | child: IconButton( 48 | icon: Icon(Icons.remove), 49 | color: Colors.red, 50 | onPressed: (){ 51 | state.value.remove(image); 52 | state.didChange(state.value); 53 | }, 54 | ), 55 | ) 56 | ], 57 | ); 58 | }).toList()..add( 59 | Material( 60 | color: Colors.grey[100], 61 | child: IconButton( 62 | icon: Icon(Icons.add_a_photo), 63 | color: Theme.of(context).primaryColor, 64 | iconSize: 50, 65 | onPressed: (){ 66 | if(Platform.isAndroid) 67 | showModalBottomSheet( 68 | context: context, 69 | builder: (_) => ImageSourceSheet( 70 | onImageSelected: onImageSelected, 71 | ) 72 | ); 73 | else 74 | showCupertinoModalPopup( 75 | context: context, 76 | builder: (_) => ImageSourceSheet( 77 | onImageSelected: onImageSelected, 78 | ) 79 | ); 80 | }, 81 | ), 82 | ) 83 | ), 84 | dotSize: 4, 85 | dotSpacing: 15, 86 | dotBgColor: Colors.transparent, 87 | dotColor: Theme.of(context).primaryColor, 88 | autoplay: false, 89 | ), 90 | ), 91 | if(state.hasError) 92 | Container( 93 | margin: const EdgeInsets.only(top: 16, left: 16), 94 | alignment: Alignment.centerLeft, 95 | child: Text( 96 | state.errorText, 97 | style: const TextStyle( 98 | color: Colors.red, 99 | fontSize: 12, 100 | ), 101 | ), 102 | ) 103 | ], 104 | ); 105 | }, 106 | ); 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /lib/screens/products/products_screen.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:lojavirtual/common/custom_drawer/custom_drawer.dart'; 3 | import 'package:lojavirtual/models/product_manager.dart'; 4 | import 'package:lojavirtual/models/user_manager.dart'; 5 | import 'package:lojavirtual/screens/products/components/product_list_tile.dart'; 6 | import 'package:lojavirtual/screens/products/components/search_dialog.dart'; 7 | import 'package:provider/provider.dart'; 8 | 9 | class ProductsScreen extends StatelessWidget { 10 | @override 11 | Widget build(BuildContext context) { 12 | return Scaffold( 13 | drawer: CustomDrawer(), 14 | appBar: AppBar( 15 | title: Consumer( 16 | builder: (_, productManager, __){ 17 | if(productManager.search.isEmpty){ 18 | return const Text('Produtos'); 19 | } else { 20 | return LayoutBuilder( 21 | builder: (_, constraints){ 22 | return GestureDetector( 23 | onTap: () async { 24 | final search = await showDialog(context: context, 25 | builder: (_) => SearchDialog(productManager.search)); 26 | if(search != null){ 27 | productManager.search = search; 28 | } 29 | }, 30 | child: Container( 31 | width: constraints.biggest.width, 32 | child: Text( 33 | productManager.search, 34 | textAlign: TextAlign.center, 35 | ) 36 | ), 37 | ); 38 | }, 39 | ); 40 | } 41 | }, 42 | ), 43 | centerTitle: true, 44 | actions: [ 45 | Consumer( 46 | builder: (_, productManager, __){ 47 | if(productManager.search.isEmpty){ 48 | return IconButton( 49 | icon: Icon(Icons.search), 50 | onPressed: () async { 51 | final search = await showDialog(context: context, 52 | builder: (_) => SearchDialog(productManager.search)); 53 | if(search != null){ 54 | productManager.search = search; 55 | } 56 | }, 57 | ); 58 | } else { 59 | return IconButton( 60 | icon: Icon(Icons.close), 61 | onPressed: () async { 62 | productManager.search = ''; 63 | }, 64 | ); 65 | } 66 | }, 67 | ), 68 | Consumer( 69 | builder: (_, userManager, __){ 70 | if(userManager.adminEnabled){ 71 | return IconButton( 72 | icon: Icon(Icons.add), 73 | onPressed: (){ 74 | Navigator.of(context).pushNamed( 75 | '/edit_product', 76 | ); 77 | }, 78 | ); 79 | } else { 80 | return Container(); 81 | } 82 | }, 83 | ) 84 | ], 85 | ), 86 | body: Consumer( 87 | builder: (_, productManager, __){ 88 | final filteredProducts = productManager.filteredProducts; 89 | return ListView.builder( 90 | itemCount: filteredProducts.length, 91 | itemBuilder: (_, index){ 92 | return ProductListTile(filteredProducts[index]); 93 | } 94 | ); 95 | }, 96 | ), 97 | floatingActionButton: FloatingActionButton( 98 | backgroundColor: Colors.white, 99 | foregroundColor: Theme.of(context).primaryColor, 100 | onPressed: (){ 101 | Navigator.of(context).pushNamed('/cart'); 102 | }, 103 | child: Icon(Icons.shopping_cart), 104 | ), 105 | ); 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /lib/screens/checkout/components/card_front.dart: -------------------------------------------------------------------------------- 1 | import 'package:brasil_fields/brasil_fields.dart'; 2 | import 'package:credit_card_type_detector/credit_card_type_detector.dart'; 3 | import 'package:flutter/material.dart'; 4 | import 'package:flutter/services.dart'; 5 | import 'package:lojavirtual/models/credit_card.dart'; 6 | import 'package:lojavirtual/screens/checkout/components/card_text_field.dart'; 7 | import 'package:mask_text_input_formatter/mask_text_input_formatter.dart'; 8 | 9 | class CardFront extends StatelessWidget { 10 | 11 | CardFront({this.numberFocus, this.dateFocus, this.nameFocus, this.finished, this.creditCard}); 12 | 13 | final MaskTextInputFormatter dateFormatter = MaskTextInputFormatter( 14 | mask: '!#/####', filter: {'#': RegExp('[0-9]'), '!': RegExp('[0-1]')} 15 | ); 16 | 17 | final VoidCallback finished; 18 | 19 | final FocusNode numberFocus; 20 | final FocusNode dateFocus; 21 | final FocusNode nameFocus; 22 | 23 | final CreditCard creditCard; 24 | 25 | @override 26 | Widget build(BuildContext context) { 27 | return Card( 28 | clipBehavior: Clip.antiAlias, 29 | shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)), 30 | elevation: 16, 31 | child: Container( 32 | height: 200, 33 | color: const Color(0xFF1B4B52), 34 | padding: const EdgeInsets.all(24), 35 | child: Row( 36 | children: [ 37 | Expanded( 38 | child: Column( 39 | mainAxisAlignment: MainAxisAlignment.end, 40 | children: [ 41 | CardTextField( 42 | initialValue: creditCard.number, 43 | title: 'Número', 44 | hint: '0000 0000 0000 0000', 45 | textInputType: TextInputType.number, 46 | bold: true, 47 | inputFormatters: [ 48 | WhitelistingTextInputFormatter.digitsOnly, 49 | CartaoBancarioInputFormatter() 50 | ], 51 | validator: (number){ 52 | if(number.length != 19) return 'Inválido'; 53 | else if(detectCCType(number) == CreditCardType.unknown) 54 | return 'Inválido'; 55 | return null; 56 | }, 57 | onSubmitted: (_){ 58 | dateFocus.requestFocus(); 59 | }, 60 | focusNode: numberFocus, 61 | onSaved: creditCard.setNumber, 62 | ), 63 | CardTextField( 64 | initialValue: creditCard.expirationDate, 65 | title: 'Validade', 66 | hint: '11/2020', 67 | textInputType: TextInputType.number, 68 | inputFormatters: [dateFormatter], 69 | validator: (date){ 70 | if(date.length != 7) return 'Inválido'; 71 | return null; 72 | }, 73 | onSubmitted: (_){ 74 | nameFocus.requestFocus(); 75 | }, 76 | focusNode: dateFocus, 77 | onSaved: creditCard.setExpirationDate, 78 | ), 79 | CardTextField( 80 | initialValue: creditCard.holder, 81 | title: 'Títular', 82 | hint: 'João da Silva', 83 | textInputType: TextInputType.text, 84 | bold: true, 85 | validator: (name){ 86 | if(name.isEmpty) return 'Inválido'; 87 | return null; 88 | }, 89 | onSubmitted: (_){ 90 | finished(); 91 | }, 92 | focusNode: nameFocus, 93 | onSaved: creditCard.setHolder, 94 | ), 95 | ], 96 | ), 97 | ), 98 | ], 99 | ), 100 | ), 101 | ); 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /android/app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 8 | 9 | 10 | 11 | 12 | 16 | 23 | 27 | 31 | 36 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 52 | 55 | 56 | 60 | 61 | 63 | 64 | 68 | 69 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | -------------------------------------------------------------------------------- /lib/screens/checkout/checkout_screen.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:lojavirtual/common/price_card.dart'; 3 | import 'package:lojavirtual/models/cart_manager.dart'; 4 | import 'package:lojavirtual/models/checkout_manager.dart'; 5 | import 'package:lojavirtual/models/credit_card.dart'; 6 | import 'package:lojavirtual/screens/checkout/components/cpf_field.dart'; 7 | import 'package:lojavirtual/screens/checkout/components/credit_card_widget.dart'; 8 | import 'package:provider/provider.dart'; 9 | 10 | class CheckoutScreen extends StatelessWidget { 11 | 12 | final GlobalKey scaffoldKey = GlobalKey(); 13 | final GlobalKey formKey = GlobalKey(); 14 | 15 | final CreditCard creditCard = CreditCard(); 16 | 17 | @override 18 | Widget build(BuildContext context) { 19 | return ChangeNotifierProxyProvider( 20 | create: (_) => CheckoutManager(), 21 | update: (_, cartManager, checkoutManager) => 22 | checkoutManager..updateCart(cartManager), 23 | lazy: false, 24 | child: Scaffold( 25 | key: scaffoldKey, 26 | appBar: AppBar( 27 | title: const Text('Pagamento'), 28 | centerTitle: true, 29 | ), 30 | body: GestureDetector( 31 | onTap: (){ 32 | FocusScope.of(context).unfocus(); 33 | }, 34 | child: Consumer( 35 | builder: (_, checkoutManager, __){ 36 | if(checkoutManager.loading){ 37 | return Center( 38 | child: Column( 39 | mainAxisAlignment: MainAxisAlignment.center, 40 | children: [ 41 | CircularProgressIndicator( 42 | valueColor: AlwaysStoppedAnimation(Colors.white), 43 | ), 44 | const SizedBox(height: 16,), 45 | Text( 46 | 'Processando seu pagamento...', 47 | style: TextStyle( 48 | color: Colors.white, 49 | fontWeight: FontWeight.w800, 50 | fontSize: 16 51 | ), 52 | ) 53 | ], 54 | ), 55 | ); 56 | } 57 | 58 | return Form( 59 | key: formKey, 60 | child: ListView( 61 | children: [ 62 | CreditCardWidget(creditCard), 63 | CpfField(), 64 | PriceCard( 65 | buttonText: 'Finalizar Pedido', 66 | onPressed: (){ 67 | if(formKey.currentState.validate()){ 68 | formKey.currentState.save(); 69 | 70 | checkoutManager.checkout( 71 | creditCard: creditCard, 72 | onStockFail: (e){ 73 | Navigator.of(context).popUntil( 74 | (route) => route.settings.name == '/cart'); 75 | }, 76 | onPayFail: (e){ 77 | scaffoldKey.currentState.showSnackBar( 78 | SnackBar( 79 | content: Text('$e'), 80 | backgroundColor: Colors.red, 81 | ) 82 | ); 83 | }, 84 | onSuccess: (order){ 85 | Navigator.of(context).popUntil( 86 | (route) => route.settings.name == '/'); 87 | Navigator.of(context).pushNamed( 88 | '/confirmation', 89 | arguments: order 90 | ); 91 | } 92 | ); 93 | } 94 | }, 95 | ) 96 | ], 97 | ), 98 | ); 99 | }, 100 | ), 101 | ), 102 | ), 103 | ); 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /lib/models/checkout_manager.dart: -------------------------------------------------------------------------------- 1 | import 'package:cloud_firestore/cloud_firestore.dart'; 2 | import 'package:flutter/cupertino.dart'; 3 | import 'package:lojavirtual/models/cart_manager.dart'; 4 | import 'package:lojavirtual/models/credit_card.dart'; 5 | import 'package:lojavirtual/models/order.dart'; 6 | import 'package:lojavirtual/models/product.dart'; 7 | import 'package:lojavirtual/services/cielo_payment.dart'; 8 | 9 | class CheckoutManager extends ChangeNotifier { 10 | 11 | CartManager cartManager; 12 | 13 | bool _loading = false; 14 | bool get loading => _loading; 15 | set loading(bool value){ 16 | _loading = value; 17 | notifyListeners(); 18 | } 19 | 20 | final Firestore firestore = Firestore.instance; 21 | 22 | final CieloPayment cieloPayment = CieloPayment(); 23 | 24 | // ignore: use_setters_to_change_properties 25 | void updateCart(CartManager cartManager){ 26 | this.cartManager = cartManager; 27 | } 28 | 29 | Future checkout({CreditCard creditCard, 30 | Function onStockFail, 31 | Function onSuccess, 32 | Function onPayFail}) async { 33 | 34 | loading = true; 35 | 36 | final orderId = await _getOrderId(); 37 | 38 | String payId; 39 | try { 40 | payId = await cieloPayment.authorize( 41 | creditCard: creditCard, 42 | price: cartManager.totalPrice, 43 | orderId: orderId.toString(), 44 | user: cartManager.user, 45 | ); 46 | debugPrint('success $payId'); 47 | } catch (e){ 48 | onPayFail(e); 49 | loading = false; 50 | return; 51 | } 52 | 53 | try { 54 | await _decrementStock(); 55 | } catch (e){ 56 | cieloPayment.cancel(payId); 57 | onStockFail(e); 58 | loading = false; 59 | return; 60 | } 61 | 62 | try { 63 | await cieloPayment.capture(payId); 64 | } catch (e){ 65 | onPayFail(e); 66 | loading = false; 67 | return; 68 | } 69 | 70 | final order = Order.fromCartManager(cartManager); 71 | order.orderId = orderId.toString(); 72 | order.payId = payId; 73 | 74 | await order.save(); 75 | 76 | cartManager.clear(); 77 | 78 | onSuccess(order); 79 | loading = false; 80 | } 81 | 82 | Future _getOrderId() async { 83 | final ref = firestore.document('aux/ordercounter'); 84 | 85 | try { 86 | final result = await firestore.runTransaction((tx) async { 87 | final doc = await tx.get(ref); 88 | final orderId = doc.data['current'] as int; 89 | await tx.update(ref, {'current': orderId + 1}); 90 | return {'orderId': orderId}; 91 | }); 92 | return result['orderId'] as int; 93 | } catch (e){ 94 | debugPrint(e.toString()); 95 | return Future.error('Falha ao gerar número do pedido'); 96 | } 97 | } 98 | 99 | Future _decrementStock(){ 100 | // 1. Ler todos os estoques 3xM 101 | // 2. Decremento localmente os estoques 2xM 102 | // 3. Salvar os estoques no firebase 2xM 103 | 104 | return firestore.runTransaction((tx) async { 105 | final List productsToUpdate = []; 106 | final List productsWithoutStock = []; 107 | 108 | for(final cartProduct in cartManager.items){ 109 | Product product; 110 | 111 | if(productsToUpdate.any((p) => p.id == cartProduct.productId)){ 112 | product = productsToUpdate.firstWhere( 113 | (p) => p.id == cartProduct.productId); 114 | } else { 115 | final doc = await tx.get( 116 | firestore.document('products/${cartProduct.productId}') 117 | ); 118 | product = Product.fromDocument(doc); 119 | } 120 | 121 | cartProduct.product = product; 122 | 123 | final size = product.findSize(cartProduct.size); 124 | if(size.stock - cartProduct.quantity < 0){ 125 | productsWithoutStock.add(product); 126 | } else { 127 | size.stock -= cartProduct.quantity; 128 | productsToUpdate.add(product); 129 | } 130 | } 131 | 132 | if(productsWithoutStock.isNotEmpty){ 133 | return Future.error( 134 | '${productsWithoutStock.length} produtos sem estoque'); 135 | } 136 | 137 | for(final product in productsToUpdate){ 138 | tx.update(firestore.document('products/${product.id}'), 139 | {'sizes': product.exportSizeList()}); 140 | } 141 | }); 142 | } 143 | 144 | } -------------------------------------------------------------------------------- /lib/screens/cart/components/cart_tile.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:lojavirtual/common/custom_icon_button.dart'; 3 | import 'package:lojavirtual/models/cart_product.dart'; 4 | import 'package:provider/provider.dart'; 5 | 6 | class CartTile extends StatelessWidget { 7 | 8 | const CartTile(this.cartProduct); 9 | 10 | final CartProduct cartProduct; 11 | 12 | @override 13 | Widget build(BuildContext context) { 14 | return ChangeNotifierProvider.value( 15 | value: cartProduct, 16 | child: GestureDetector( 17 | onTap: (){ 18 | Navigator.of(context).pushNamed( 19 | '/product', 20 | arguments: cartProduct.product); 21 | }, 22 | child: Card( 23 | margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 4), 24 | child: Padding( 25 | padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), 26 | child: Row( 27 | children: [ 28 | SizedBox( 29 | height: 80, 30 | width: 80, 31 | child: Image.network(cartProduct.product.images.first), 32 | ), 33 | Expanded( 34 | child: Padding( 35 | padding: const EdgeInsets.only(left: 16), 36 | child: Column( 37 | crossAxisAlignment: CrossAxisAlignment.start, 38 | children: [ 39 | Text( 40 | cartProduct.product.name, 41 | style: TextStyle( 42 | fontWeight: FontWeight.w500, 43 | fontSize: 17.0, 44 | ), 45 | ), 46 | Padding( 47 | padding: const EdgeInsets.symmetric(vertical: 8), 48 | child: Text( 49 | 'Tamanho: ${cartProduct.size}', 50 | style: TextStyle(fontWeight: FontWeight.w300), 51 | ), 52 | ), 53 | Consumer( 54 | builder: (_, cartProduct, __){ 55 | if(cartProduct.hasStock) 56 | return Text( 57 | 'R\$ ${cartProduct.unitPrice.toStringAsFixed(2)}', 58 | style: TextStyle( 59 | color: Theme.of(context).primaryColor, 60 | fontSize: 16.0, 61 | fontWeight: FontWeight.bold 62 | ), 63 | ); 64 | else 65 | return const Text( 66 | 'Sem estoque suficiente', 67 | style: TextStyle( 68 | color: Colors.red, 69 | fontSize: 12, 70 | ), 71 | ); 72 | }, 73 | ) 74 | ], 75 | ), 76 | ), 77 | ), 78 | Consumer( 79 | builder: (_, cartProduct, __){ 80 | return Column( 81 | children: [ 82 | CustomIconButton( 83 | iconData: Icons.add, 84 | color: Theme.of(context).primaryColor, 85 | onTap: cartProduct.increment, 86 | ), 87 | Text( 88 | '${cartProduct.quantity}', 89 | style: const TextStyle(fontSize: 20), 90 | ), 91 | CustomIconButton( 92 | iconData: Icons.remove, 93 | color: cartProduct.quantity > 1 ? 94 | Theme.of(context).primaryColor : Colors.red, 95 | onTap: cartProduct.decrement, 96 | ), 97 | ], 98 | ); 99 | }, 100 | ) 101 | ], 102 | ), 103 | ), 104 | ), 105 | ), 106 | ); 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /lib/models/product.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | 3 | import 'package:cloud_firestore/cloud_firestore.dart'; 4 | import 'package:firebase_storage/firebase_storage.dart'; 5 | import 'package:flutter/cupertino.dart'; 6 | import 'package:lojavirtual/models/item_size.dart'; 7 | import 'package:uuid/uuid.dart'; 8 | 9 | class Product extends ChangeNotifier { 10 | 11 | Product({this.id, this.name, this.description, this.images, this.sizes, 12 | this.deleted = false}){ 13 | images = images ?? []; 14 | sizes = sizes ?? []; 15 | } 16 | 17 | Product.fromDocument(DocumentSnapshot document){ 18 | id = document.documentID; 19 | name = document['name'] as String; 20 | description = document['description'] as String; 21 | images = List.from(document.data['images'] as List); 22 | deleted = (document.data['deleted'] ?? false) as bool; 23 | sizes = (document.data['sizes'] as List ?? []).map( 24 | (s) => ItemSize.fromMap(s as Map)).toList(); 25 | } 26 | 27 | final Firestore firestore = Firestore.instance; 28 | final FirebaseStorage storage = FirebaseStorage.instance; 29 | 30 | DocumentReference get firestoreRef => firestore.document('products/$id'); 31 | StorageReference get storageRef => storage.ref().child('products').child(id); 32 | 33 | String id; 34 | String name; 35 | String description; 36 | List images; 37 | List sizes; 38 | 39 | List newImages; 40 | 41 | bool deleted; 42 | 43 | bool _loading = false; 44 | bool get loading => _loading; 45 | set loading(bool value){ 46 | _loading = value; 47 | notifyListeners(); 48 | } 49 | 50 | ItemSize _selectedSize; 51 | ItemSize get selectedSize => _selectedSize; 52 | set selectedSize(ItemSize value){ 53 | _selectedSize = value; 54 | notifyListeners(); 55 | } 56 | 57 | int get totalStock { 58 | int stock = 0; 59 | for(final size in sizes){ 60 | stock += size.stock; 61 | } 62 | return stock; 63 | } 64 | 65 | bool get hasStock { 66 | return totalStock > 0 && !deleted; 67 | } 68 | 69 | num get basePrice { 70 | num lowest = double.infinity; 71 | for(final size in sizes){ 72 | if(size.price < lowest) 73 | lowest = size.price; 74 | } 75 | return lowest; 76 | } 77 | 78 | ItemSize findSize(String name){ 79 | try { 80 | return sizes.firstWhere((s) => s.name == name); 81 | } catch (e){ 82 | return null; 83 | } 84 | } 85 | 86 | List> exportSizeList(){ 87 | return sizes.map((size) => size.toMap()).toList(); 88 | } 89 | 90 | Future save() async { 91 | loading = true; 92 | 93 | final Map data = { 94 | 'name': name, 95 | 'description': description, 96 | 'sizes': exportSizeList(), 97 | 'deleted': deleted 98 | }; 99 | 100 | if(id == null){ 101 | final doc = await firestore.collection('products').add(data); 102 | id = doc.documentID; 103 | } else { 104 | await firestoreRef.updateData(data); 105 | } 106 | 107 | final List updateImages = []; 108 | 109 | for(final newImage in newImages){ 110 | if(images.contains(newImage)){ 111 | updateImages.add(newImage as String); 112 | } else { 113 | final StorageUploadTask task = storageRef.child(Uuid().v1()).putFile(newImage as File); 114 | final StorageTaskSnapshot snapshot = await task.onComplete; 115 | final String url = await snapshot.ref.getDownloadURL() as String; 116 | updateImages.add(url); 117 | } 118 | } 119 | 120 | for(final image in images){ 121 | if(!newImages.contains(image) && image.contains('firebase')){ 122 | try { 123 | final ref = await storage.getReferenceFromUrl(image); 124 | await ref.delete(); 125 | } catch (e){ 126 | debugPrint('Falha ao deletar $image'); 127 | } 128 | } 129 | } 130 | 131 | await firestoreRef.updateData({'images': updateImages}); 132 | 133 | images = updateImages; 134 | 135 | loading = false; 136 | } 137 | 138 | void delete(){ 139 | firestoreRef.updateData({'deleted': true}); 140 | } 141 | 142 | Product clone(){ 143 | return Product( 144 | id: id, 145 | name: name, 146 | description: description, 147 | images: List.from(images), 148 | sizes: sizes.map((size) => size.clone()).toList(), 149 | deleted: deleted, 150 | ); 151 | } 152 | 153 | @override 154 | String toString() { 155 | return 'Product{id: $id, name: $name, description: $description, images: $images, sizes: $sizes, newImages: $newImages}'; 156 | } 157 | } -------------------------------------------------------------------------------- /lib/screens/home/home_screen.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:lojavirtual/common/custom_drawer/custom_drawer.dart'; 3 | import 'package:lojavirtual/models/home_manager.dart'; 4 | import 'package:lojavirtual/models/user_manager.dart'; 5 | import 'package:lojavirtual/screens/home/components/add_section_widget.dart'; 6 | import 'package:lojavirtual/screens/home/components/section_list.dart'; 7 | import 'package:lojavirtual/screens/home/components/section_staggered.dart'; 8 | import 'package:provider/provider.dart'; 9 | 10 | class HomeScreen extends StatelessWidget { 11 | @override 12 | Widget build(BuildContext context) { 13 | return Scaffold( 14 | drawer: CustomDrawer(), 15 | body: Stack( 16 | children: [ 17 | Container( 18 | decoration: BoxDecoration( 19 | gradient: LinearGradient( 20 | colors: const [ 21 | Color.fromARGB(255, 211, 118, 130), 22 | Color.fromARGB(255, 253, 181, 168) 23 | ], 24 | begin: Alignment.topCenter, 25 | end: Alignment.bottomCenter 26 | ) 27 | ), 28 | ), 29 | CustomScrollView( 30 | slivers: [ 31 | SliverAppBar( 32 | snap: true, 33 | floating: true, 34 | elevation: 0, 35 | backgroundColor: Colors.transparent, 36 | flexibleSpace: const FlexibleSpaceBar( 37 | title: Text('Loja do Daniel'), 38 | centerTitle: true, 39 | ), 40 | actions: [ 41 | IconButton( 42 | icon: Icon(Icons.shopping_cart), 43 | color: Colors.white, 44 | onPressed: () => Navigator.of(context).pushNamed('/cart'), 45 | ), 46 | Consumer2( 47 | builder: (_, userManager, homeManager, __){ 48 | if(userManager.adminEnabled && !homeManager.loading) { 49 | if(homeManager.editing){ 50 | return PopupMenuButton( 51 | onSelected: (e){ 52 | if(e == 'Salvar'){ 53 | homeManager.saveEditing(); 54 | } else { 55 | homeManager.discardEditing(); 56 | } 57 | }, 58 | itemBuilder: (_){ 59 | return ['Salvar', 'Descartar'].map((e){ 60 | return PopupMenuItem( 61 | value: e, 62 | child: Text(e), 63 | ); 64 | }).toList(); 65 | }, 66 | ); 67 | } else { 68 | return IconButton( 69 | icon: Icon(Icons.edit), 70 | onPressed: homeManager.enterEditing, 71 | ); 72 | } 73 | } else return Container(); 74 | }, 75 | ), 76 | ], 77 | ), 78 | Consumer( 79 | builder: (_, homeManager, __){ 80 | if(homeManager.loading){ 81 | return SliverToBoxAdapter( 82 | child: LinearProgressIndicator( 83 | valueColor: AlwaysStoppedAnimation(Colors.white), 84 | backgroundColor: Colors.transparent, 85 | ), 86 | ); 87 | } 88 | 89 | final List children = homeManager.sections.map( 90 | (section) { 91 | switch(section.type){ 92 | case 'List': 93 | return SectionList(section); 94 | case 'Staggered': 95 | return SectionStaggered(section); 96 | default: 97 | return Container(); 98 | } 99 | } 100 | ).toList(); 101 | 102 | if(homeManager.editing) 103 | children.add(AddSectionWidget(homeManager)); 104 | 105 | return SliverList( 106 | delegate: SliverChildListDelegate(children), 107 | ); 108 | }, 109 | ) 110 | ], 111 | ), 112 | ], 113 | ), 114 | ); 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /lib/screens/admin_orders/admin_orders_screen.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:lojavirtual/common/custom_drawer/custom_drawer.dart'; 3 | import 'package:lojavirtual/common/custom_icon_button.dart'; 4 | import 'package:lojavirtual/common/empty_card.dart'; 5 | import 'package:lojavirtual/common/order/order_tile.dart'; 6 | import 'package:lojavirtual/models/admin_orders_manager.dart'; 7 | import 'package:lojavirtual/models/order.dart'; 8 | import 'package:provider/provider.dart'; 9 | import 'package:sliding_up_panel/sliding_up_panel.dart'; 10 | 11 | class AdminOrdersScreen extends StatefulWidget { 12 | 13 | @override 14 | _AdminOrdersScreenState createState() => _AdminOrdersScreenState(); 15 | } 16 | 17 | class _AdminOrdersScreenState extends State { 18 | final PanelController panelController = PanelController(); 19 | 20 | @override 21 | Widget build(BuildContext context) { 22 | return Scaffold( 23 | drawer: CustomDrawer(), 24 | appBar: AppBar( 25 | title: const Text('Todos os Pedidos'), 26 | centerTitle: true, 27 | ), 28 | body: Consumer( 29 | builder: (_, ordersManager, __){ 30 | final filteredOrders = ordersManager.filteredOrders; 31 | 32 | return SlidingUpPanel( 33 | controller: panelController, 34 | body: Column( 35 | children: [ 36 | if(ordersManager.userFilter != null) 37 | Padding( 38 | padding: const EdgeInsets.fromLTRB(16, 0, 16, 2), 39 | child: Row( 40 | children: [ 41 | Expanded( 42 | child: Text( 43 | 'Pedidos de ${ordersManager.userFilter.name}', 44 | style: TextStyle( 45 | fontWeight: FontWeight.w800, 46 | color: Colors.white, 47 | ), 48 | ), 49 | ), 50 | CustomIconButton( 51 | iconData: Icons.close, 52 | color: Colors.white, 53 | onTap: (){ 54 | ordersManager.setUserFilter(null); 55 | }, 56 | ) 57 | ], 58 | ), 59 | ), 60 | if(filteredOrders.isEmpty) 61 | Expanded( 62 | child: EmptyCard( 63 | title: 'Nenhuma venda realizada!', 64 | iconData: Icons.border_clear, 65 | ), 66 | ) 67 | else 68 | Expanded( 69 | child: ListView.builder( 70 | itemCount: filteredOrders.length, 71 | itemBuilder: (_, index){ 72 | return OrderTile( 73 | filteredOrders[index], 74 | showControls: true, 75 | ); 76 | } 77 | ), 78 | ), 79 | const SizedBox(height: 120,), 80 | ], 81 | ), 82 | minHeight: 40, 83 | maxHeight: 250, 84 | panel: Column( 85 | crossAxisAlignment: CrossAxisAlignment.stretch, 86 | children: [ 87 | GestureDetector( 88 | onTap: (){ 89 | if(panelController.isPanelClosed){ 90 | panelController.open(); 91 | } else { 92 | panelController.close(); 93 | } 94 | }, 95 | child: Container( 96 | height: 40, 97 | color: Colors.white, 98 | alignment: Alignment.center, 99 | child: Text( 100 | 'Filtros', 101 | style: TextStyle( 102 | color: Theme.of(context).primaryColor, 103 | fontWeight: FontWeight.w800, 104 | fontSize: 16, 105 | ), 106 | ), 107 | ), 108 | ), 109 | Expanded( 110 | child: Column( 111 | mainAxisAlignment: MainAxisAlignment.spaceBetween, 112 | children: Status.values.map((s){ 113 | return CheckboxListTile( 114 | title: Text(Order.getStatusText(s)), 115 | dense: true, 116 | activeColor: Theme.of(context).primaryColor, 117 | value: ordersManager.statusFilter.contains(s), 118 | onChanged: (v){ 119 | ordersManager.setStatusFilter( 120 | status: s, 121 | enabled: v 122 | ); 123 | }, 124 | ); 125 | }).toList(), 126 | ), 127 | ), 128 | ], 129 | ), 130 | ); 131 | }, 132 | ), 133 | ); 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /functions/tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "rules": { 3 | // -- Strict errors -- 4 | // These lint rules are likely always a good idea. 5 | 6 | // Force function overloads to be declared together. This ensures readers understand APIs. 7 | "adjacent-overload-signatures": true, 8 | 9 | // Do not allow the subtle/obscure comma operator. 10 | "ban-comma-operator": true, 11 | 12 | // Do not allow internal modules or namespaces . These are deprecated in favor of ES6 modules. 13 | "no-namespace": true, 14 | 15 | // Do not allow parameters to be reassigned. To avoid bugs, developers should instead assign new values to new vars. 16 | "no-parameter-reassignment": true, 17 | 18 | // Force the use of ES6-style imports instead of /// imports. 19 | "no-reference": true, 20 | 21 | // Do not allow type assertions that do nothing. This is a big warning that the developer may not understand the 22 | // code currently being edited (they may be incorrectly handling a different type case that does not exist). 23 | "no-unnecessary-type-assertion": true, 24 | 25 | // Disallow nonsensical label usage. 26 | "label-position": true, 27 | 28 | // Disallows the (often typo) syntax if (var1 = var2). Replace with if (var2) { var1 = var2 }. 29 | "no-conditional-assignment": true, 30 | 31 | // Disallows constructors for primitive types (e.g. new Number('123'), though Number('123') is still allowed). 32 | "no-construct": true, 33 | 34 | // Do not allow super() to be called twice in a constructor. 35 | "no-duplicate-super": true, 36 | 37 | // Do not allow the same case to appear more than once in a switch block. 38 | "no-duplicate-switch-case": true, 39 | 40 | // Do not allow a variable to be declared more than once in the same block. Consider function parameters in this 41 | // rule. 42 | "no-duplicate-variable": [true, "check-parameters"], 43 | 44 | // Disallows a variable definition in an inner scope from shadowing a variable in an outer scope. Developers should 45 | // instead use a separate variable name. 46 | "no-shadowed-variable": true, 47 | 48 | // Empty blocks are almost never needed. Allow the one general exception: empty catch blocks. 49 | "no-empty": [true, "allow-empty-catch"], 50 | 51 | // Functions must either be handled directly (e.g. with a catch() handler) or returned to another function. 52 | // This is a major source of errors in Cloud Functions and the team strongly recommends leaving this rule on. 53 | "no-floating-promises": true, 54 | 55 | // Do not allow any imports for modules that are not in package.json. These will almost certainly fail when 56 | // deployed. 57 | "no-implicit-dependencies": true, 58 | 59 | // The 'this' keyword can only be used inside of classes. 60 | "no-invalid-this": true, 61 | 62 | // Do not allow strings to be thrown because they will not include stack traces. Throw Errors instead. 63 | "no-string-throw": true, 64 | 65 | // Disallow control flow statements, such as return, continue, break, and throw in finally blocks. 66 | "no-unsafe-finally": true, 67 | 68 | // Expressions must always return a value. Avoids common errors like const myValue = functionReturningVoid(); 69 | "no-void-expression": [true, "ignore-arrow-function-shorthand"], 70 | 71 | // Disallow duplicate imports in the same file. 72 | "no-duplicate-imports": true, 73 | 74 | 75 | // -- Strong Warnings -- 76 | // These rules should almost never be needed, but may be included due to legacy code. 77 | // They are left as a warning to avoid frustration with blocked deploys when the developer 78 | // understand the warning and wants to deploy anyway. 79 | 80 | // Warn when an empty interface is defined. These are generally not useful. 81 | "no-empty-interface": {"severity": "warning"}, 82 | 83 | // Warn when an import will have side effects. 84 | "no-import-side-effect": {"severity": "warning"}, 85 | 86 | // Warn when variables are defined with var. Var has subtle meaning that can lead to bugs. Strongly prefer const for 87 | // most values and let for values that will change. 88 | "no-var-keyword": {"severity": "warning"}, 89 | 90 | // Prefer === and !== over == and !=. The latter operators support overloads that are often accidental. 91 | "triple-equals": {"severity": "warning"}, 92 | 93 | // Warn when using deprecated APIs. 94 | "deprecation": {"severity": "warning"}, 95 | 96 | // -- Light Warnings -- 97 | // These rules are intended to help developers use better style. Simpler code has fewer bugs. These would be "info" 98 | // if TSLint supported such a level. 99 | 100 | // prefer for( ... of ... ) to an index loop when the index is only used to fetch an object from an array. 101 | // (Even better: check out utils like .map if transforming an array!) 102 | "prefer-for-of": {"severity": "warning"}, 103 | 104 | // Warns if function overloads could be unified into a single function with optional or rest parameters. 105 | "unified-signatures": {"severity": "warning"}, 106 | 107 | // Prefer const for values that will not change. This better documents code. 108 | "prefer-const": {"severity": "warning"}, 109 | 110 | // Multi-line object literals and function calls should have a trailing comma. This helps avoid merge conflicts. 111 | "trailing-comma": {"severity": "warning"} 112 | }, 113 | 114 | "defaultSeverity": "error" 115 | } 116 | --------------------------------------------------------------------------------