├── res └── values │ └── strings_en.arb ├── android ├── settings_aar.gradle ├── key.properties ├── gradle.properties ├── app │ ├── src │ │ ├── main │ │ │ ├── res │ │ │ │ ├── drawable │ │ │ │ │ ├── launch_screen.png │ │ │ │ │ └── launch_background.xml │ │ │ │ ├── mipmap-hdpi │ │ │ │ │ └── ic_launcher.png │ │ │ │ ├── mipmap-mdpi │ │ │ │ │ └── ic_launcher.png │ │ │ │ ├── mipmap-xhdpi │ │ │ │ │ └── ic_launcher.png │ │ │ │ ├── mipmap-xxhdpi │ │ │ │ │ └── ic_launcher.png │ │ │ │ ├── mipmap-xxxhdpi │ │ │ │ │ └── ic_launcher.png │ │ │ │ ├── values │ │ │ │ │ ├── colors.xml │ │ │ │ │ └── styles.xml │ │ │ │ ├── drawable-hdpi │ │ │ │ │ └── ic_launcher_foreground.png │ │ │ │ ├── drawable-mdpi │ │ │ │ │ └── ic_launcher_foreground.png │ │ │ │ ├── drawable-xhdpi │ │ │ │ │ └── ic_launcher_foreground.png │ │ │ │ ├── drawable-xxhdpi │ │ │ │ │ └── ic_launcher_foreground.png │ │ │ │ ├── drawable-xxxhdpi │ │ │ │ │ └── ic_launcher_foreground.png │ │ │ │ └── mipmap-anydpi-v26 │ │ │ │ │ └── ic_launcher.xml │ │ │ ├── kotlin │ │ │ │ └── com │ │ │ │ │ └── a1manstartup │ │ │ │ │ └── travel_budget │ │ │ │ │ └── MainActivity.kt │ │ │ └── AndroidManifest.xml │ │ ├── debug │ │ │ └── AndroidManifest.xml │ │ └── profile │ │ │ └── AndroidManifest.xml │ └── build.gradle ├── gradle │ └── wrapper │ │ └── gradle-wrapper.properties ├── settings.gradle └── build.gradle ├── ios ├── Flutter │ ├── .last_build_id │ ├── Debug.xcconfig │ ├── Release.xcconfig │ ├── Flutter.podspec │ ├── flutter_export_environment.sh │ └── AppFrameworkInfo.plist ├── Runner │ ├── Runner-Bridging-Header.h │ ├── Assets.xcassets │ │ ├── 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-50x50@1x.png │ │ │ ├── Icon-App-50x50@2x.png │ │ │ ├── Icon-App-57x57@1x.png │ │ │ ├── Icon-App-57x57@2x.png │ │ │ ├── Icon-App-60x60@2x.png │ │ │ ├── Icon-App-60x60@3x.png │ │ │ ├── Icon-App-72x72@1x.png │ │ │ ├── Icon-App-72x72@2x.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 │ │ └── LaunchImage.imageset │ │ │ ├── launch_screen.png │ │ │ ├── launch_screen@2x.png │ │ │ ├── launch_screen@3x.png │ │ │ ├── README.md │ │ │ └── Contents.json │ ├── Runner.entitlements │ ├── AppDelegate.swift │ ├── Base.lproj │ │ ├── Main.storyboard │ │ └── LaunchScreen.storyboard │ └── Info.plist ├── Runner.xcodeproj │ ├── project.xcworkspace │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata │ │ │ └── IDEWorkspaceChecks.plist │ └── xcshareddata │ │ └── xcschemes │ │ └── Runner.xcscheme ├── Runner.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ └── IDEWorkspaceChecks.plist └── Podfile ├── assets ├── sun_clouds.flr └── icons │ ├── main_logo.png │ └── foreground_logo.png ├── lib ├── models │ ├── Place.dart │ ├── User.dart │ └── Trip.dart ├── services │ ├── custom_colors.dart │ ├── firebase_service.dart │ ├── admob_service.dart │ └── auth_service.dart ├── widgets │ ├── provider_widget.dart │ ├── divider_with_text_widget.dart │ ├── rounded_button.dart │ ├── money_text_field.dart │ ├── trip_card.dart │ ├── custom_dialog.dart │ └── calculator_widget.dart ├── views │ ├── home_widgets │ │ ├── home_header.dart │ │ ├── days_until_trip.dart │ │ ├── current_daily_budget.dart │ │ ├── travel_type.dart │ │ ├── notes.dart │ │ ├── percent_saved.dart │ │ ├── trip_details_card.dart │ │ └── saved_vs_needed.dart │ ├── home_view.dart │ ├── new_trips │ │ ├── summary_view.dart │ │ ├── budget_view.dart │ │ ├── location_view.dart │ │ └── date_view.dart │ ├── navigation_view.dart │ ├── past_trips_view.dart │ ├── first_view.dart │ ├── edit_notes_view.dart │ ├── deposit_view.dart │ ├── profile_view.dart │ ├── detail_trip_view.dart │ └── sign_up_view.dart ├── classes │ └── progress_painter.dart ├── main.dart └── generated │ └── i18n.dart ├── .metadata ├── .github └── FUNDING.yml ├── LICENSE ├── test └── widget_test.dart ├── README.md ├── .gitignore ├── pubspec.yaml ├── .flutter-plugins-dependencies └── pubspec.lock /res/values/strings_en.arb: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /android/settings_aar.gradle: -------------------------------------------------------------------------------- 1 | include ':app' 2 | -------------------------------------------------------------------------------- /ios/Flutter/.last_build_id: -------------------------------------------------------------------------------- 1 | ff43a8078c18a80d1d983aec4895d6ad -------------------------------------------------------------------------------- /ios/Runner/Runner-Bridging-Header.h: -------------------------------------------------------------------------------- 1 | #import "GeneratedPluginRegistrant.h" -------------------------------------------------------------------------------- /assets/sun_clouds.flr: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/davefaliskie/travel_treasury/HEAD/assets/sun_clouds.flr -------------------------------------------------------------------------------- /assets/icons/main_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/davefaliskie/travel_treasury/HEAD/assets/icons/main_logo.png -------------------------------------------------------------------------------- /assets/icons/foreground_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/davefaliskie/travel_treasury/HEAD/assets/icons/foreground_logo.png -------------------------------------------------------------------------------- /android/key.properties: -------------------------------------------------------------------------------- 1 | storePassword=password123 2 | keyPassword=password123 3 | keyAlias=key 4 | storeFile=/Users/davefaliskie/key.jks 5 | -------------------------------------------------------------------------------- /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.useAndroidX=true 3 | android.enableJetifier=true 4 | android.enableR8=true 5 | -------------------------------------------------------------------------------- /ios/Flutter/Release.xcconfig: -------------------------------------------------------------------------------- 1 | #include "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" 2 | #include "Generated.xcconfig" 3 | -------------------------------------------------------------------------------- /android/app/src/main/res/drawable/launch_screen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/davefaliskie/travel_treasury/HEAD/android/app/src/main/res/drawable/launch_screen.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/davefaliskie/travel_treasury/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/davefaliskie/travel_treasury/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/davefaliskie/travel_treasury/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/davefaliskie/travel_treasury/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/davefaliskie/travel_treasury/HEAD/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #57AEAF 4 | -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-hdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/davefaliskie/travel_treasury/HEAD/android/app/src/main/res/drawable-hdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-mdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/davefaliskie/travel_treasury/HEAD/android/app/src/main/res/drawable-mdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-xhdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/davefaliskie/travel_treasury/HEAD/android/app/src/main/res/drawable-xhdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-xxhdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/davefaliskie/travel_treasury/HEAD/android/app/src/main/res/drawable-xxhdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/davefaliskie/travel_treasury/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/davefaliskie/travel_treasury/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/davefaliskie/travel_treasury/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/davefaliskie/travel_treasury/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/davefaliskie/travel_treasury/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/davefaliskie/travel_treasury/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/davefaliskie/travel_treasury/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/davefaliskie/travel_treasury/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/davefaliskie/travel_treasury/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-50x50@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/davefaliskie/travel_treasury/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-50x50@1x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-50x50@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/davefaliskie/travel_treasury/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-50x50@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-57x57@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/davefaliskie/travel_treasury/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-57x57@1x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-57x57@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/davefaliskie/travel_treasury/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-57x57@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/davefaliskie/travel_treasury/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/davefaliskie/travel_treasury/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-72x72@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/davefaliskie/travel_treasury/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-72x72@1x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-72x72@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/davefaliskie/travel_treasury/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-72x72@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/davefaliskie/travel_treasury/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/davefaliskie/travel_treasury/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/launch_screen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/davefaliskie/travel_treasury/HEAD/ios/Runner/Assets.xcassets/LaunchImage.imageset/launch_screen.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-xxxhdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/davefaliskie/travel_treasury/HEAD/android/app/src/main/res/drawable-xxxhdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/launch_screen@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/davefaliskie/travel_treasury/HEAD/ios/Runner/Assets.xcassets/LaunchImage.imageset/launch_screen@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/launch_screen@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/davefaliskie/travel_treasury/HEAD/ios/Runner/Assets.xcassets/LaunchImage.imageset/launch_screen@3x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/davefaliskie/travel_treasury/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/davefaliskie/travel_treasury/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png -------------------------------------------------------------------------------- /lib/models/Place.dart: -------------------------------------------------------------------------------- 1 | class Place { 2 | String name; 3 | double averageBudget; 4 | String placeId; 5 | 6 | Place( 7 | this.name, 8 | this.averageBudget, 9 | this.placeId, 10 | ); 11 | } -------------------------------------------------------------------------------- /lib/models/User.dart: -------------------------------------------------------------------------------- 1 | class User { 2 | String homeCountry; 3 | bool admin; 4 | 5 | User(this.homeCountry); 6 | 7 | Map toJson() => { 8 | 'homeCountry': homeCountry, 9 | 'admin': admin, 10 | }; 11 | } -------------------------------------------------------------------------------- /ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /android/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Fri Jun 23 08:50:38 CEST 2017 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | distributionUrl=https\://services.gradle.org/distributions/gradle-5.6.2-all.zip 7 | -------------------------------------------------------------------------------- /ios/Runner.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /ios/Runner/Runner.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.developer.applesignin 6 | 7 | Default 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /.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: 7a4c33425ddd78c54aba07d86f3f9a4a0051769b 8 | channel: stable 9 | 10 | project_type: app 11 | -------------------------------------------------------------------------------- /android/app/src/debug/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /android/app/src/profile/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md: -------------------------------------------------------------------------------- 1 | # Launch Screen Assets 2 | 3 | You can customize the launch screen with your own desired assets by replacing the image files in this directory. 4 | 5 | You can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images. -------------------------------------------------------------------------------- /android/app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | 9 | -------------------------------------------------------------------------------- /android/app/src/main/kotlin/com/a1manstartup/travel_budget/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package com.a1manstartup.travel_budget 2 | 3 | import android.os.Bundle 4 | 5 | import io.flutter.app.FlutterActivity 6 | import io.flutter.plugins.GeneratedPluginRegistrant 7 | 8 | class MainActivity: FlutterActivity() { 9 | override fun onCreate(savedInstanceState: Bundle?) { 10 | super.onCreate(savedInstanceState) 11 | GeneratedPluginRegistrant.registerWith(this) 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /android/app/src/main/res/drawable/launch_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /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: [UIApplicationLaunchOptionsKey: Any]? 9 | ) -> Bool { 10 | GeneratedPluginRegistrant.register(with: self) 11 | return super.application(application, didFinishLaunchingWithOptions: launchOptions) 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /lib/services/custom_colors.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class CustomColors { 4 | Brightness brightness; 5 | Color text1, primary; 6 | 7 | CustomColors(Brightness brightness) { 8 | this.brightness = brightness; 9 | 10 | if(brightness == Brightness.dark) { 11 | this.text1 = Color(0xff252223); 12 | this.primary = Color(0xff3c7778); 13 | 14 | } else { 15 | this.text1 = Colors.white; 16 | this.primary = Color(0xff57AEAF); 17 | } 18 | } 19 | 20 | } -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "launch_screen.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "filename" : "launch_screen@2x.png", 10 | "idiom" : "universal", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "filename" : "launch_screen@3x.png", 15 | "idiom" : "universal", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "author" : "xcode", 21 | "version" : 1 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 | -------------------------------------------------------------------------------- /lib/widgets/provider_widget.dart: -------------------------------------------------------------------------------- 1 | import 'package:travel_budget/services/auth_service.dart'; 2 | import 'package:flutter/material.dart'; 3 | 4 | class Provider extends InheritedWidget { 5 | final AuthService auth; 6 | final db; 7 | final colors; 8 | 9 | Provider({Key key, Widget child, this.auth, this.db, this.colors}) : super(key: key, child: child); 10 | 11 | @override 12 | bool updateShouldNotify(InheritedWidget oldWidget) { 13 | return true; 14 | } 15 | 16 | static Provider of(BuildContext context) => 17 | (context.dependOnInheritedWidgetOfExactType()); 18 | } -------------------------------------------------------------------------------- /lib/widgets/divider_with_text_widget.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class DividerWithText extends StatelessWidget { 4 | final String dividerText; 5 | const DividerWithText({Key key, @required this.dividerText}) : super(key: key); 6 | 7 | @override 8 | Widget build(BuildContext context) { 9 | return Row( 10 | children: [ 11 | Expanded(child: Padding( 12 | padding: const EdgeInsets.only(right:8.0), 13 | child: Divider(), 14 | )), 15 | Text(dividerText), 16 | Expanded(child: Padding( 17 | padding: const EdgeInsets.only(left: 8.0), 18 | child: Divider(), 19 | )), 20 | ], 21 | ); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /lib/widgets/rounded_button.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class RoundedButton extends RaisedButton { 4 | final VoidCallback onPressed; 5 | final Widget child; 6 | final Color color; 7 | 8 | const RoundedButton({@required this.onPressed, this.child, this.color}) : super(onPressed: onPressed, child: child); 9 | 10 | @override 11 | Widget build(BuildContext context) { 12 | return Theme( 13 | data: Theme.of(context).copyWith( 14 | buttonTheme: Theme.of(context).buttonTheme.copyWith( 15 | shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(30.0)), 16 | buttonColor: color, 17 | ) 18 | ), 19 | child: Builder(builder: super.build), 20 | ); 21 | } 22 | } -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] 4 | patreon: # Replace with a single Patreon username 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | otechie: # Replace with a single Otechie username 12 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 13 | custom: ['https://1manstartup.com/donate'] 14 | -------------------------------------------------------------------------------- /ios/Flutter/Flutter.podspec: -------------------------------------------------------------------------------- 1 | # 2 | # NOTE: This podspec is NOT to be published. It is only used as a local source! 3 | # 4 | 5 | Pod::Spec.new do |s| 6 | s.name = 'Flutter' 7 | s.version = '1.0.0' 8 | s.summary = 'High-performance, high-fidelity mobile apps.' 9 | s.description = <<-DESC 10 | Flutter provides an easy and productive way to build and deploy high-performance mobile apps for Android and iOS. 11 | DESC 12 | s.homepage = 'https://flutter.io' 13 | s.license = { :type => 'MIT' } 14 | s.author = { 'Flutter Dev Team' => 'flutter-dev@googlegroups.com' } 15 | s.source = { :git => 'https://github.com/flutter/engine', :tag => s.version.to_s } 16 | s.ios.deployment_target = '8.0' 17 | s.vendored_frameworks = 'Flutter.framework' 18 | end 19 | -------------------------------------------------------------------------------- /ios/Flutter/flutter_export_environment.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # This is a generated file; do not edit or check into version control. 3 | export "FLUTTER_ROOT=/Users/davefaliskie/flutter" 4 | export "FLUTTER_APPLICATION_PATH=/Users/davefaliskie/Code/travel_budget" 5 | export "FLUTTER_TARGET=/Users/davefaliskie/Code/travel_budget/lib/main.dart" 6 | export "FLUTTER_BUILD_DIR=build" 7 | export "SYMROOT=${SOURCE_ROOT}/../build/ios" 8 | export "OTHER_LDFLAGS=$(inherited) -framework Flutter" 9 | export "FLUTTER_FRAMEWORK_DIR=/Users/davefaliskie/flutter/bin/cache/artifacts/engine/ios" 10 | export "FLUTTER_BUILD_NAME=1.0.0" 11 | export "FLUTTER_BUILD_NUMBER=1" 12 | export "DART_DEFINES=flutter.inspector.structuredErrors%3Dtrue" 13 | export "DART_OBFUSCATION=false" 14 | export "TRACK_WIDGET_CREATION=true" 15 | export "TREE_SHAKE_ICONS=false" 16 | export "PACKAGE_CONFIG=.packages" 17 | -------------------------------------------------------------------------------- /android/build.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | ext.kotlin_version = '1.3.21' 3 | repositories { 4 | google() 5 | jcenter() 6 | } 7 | 8 | dependencies { 9 | classpath 'com.android.tools.build:gradle:3.3.1' 10 | classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" 11 | classpath 'com.google.gms:google-services:4.3.3' 12 | classpath 'com.google.firebase:firebase-crashlytics-gradle:2.2.0' 13 | } 14 | } 15 | 16 | allprojects { 17 | repositories { 18 | google() 19 | jcenter() 20 | } 21 | } 22 | 23 | rootProject.buildDir = '../build' 24 | subprojects { 25 | project.buildDir = "${rootProject.buildDir}/${project.name}" 26 | } 27 | subprojects { 28 | project.evaluationDependsOn(':app') 29 | } 30 | 31 | task clean(type: Delete) { 32 | delete rootProject.buildDir 33 | } 34 | -------------------------------------------------------------------------------- /ios/Flutter/AppFrameworkInfo.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | App 9 | CFBundleIdentifier 10 | io.flutter.flutter.app 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | App 15 | CFBundlePackageType 16 | FMWK 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1.0 23 | MinimumOSVersion 24 | 8.0 25 | 26 | 27 | -------------------------------------------------------------------------------- /lib/services/firebase_service.dart: -------------------------------------------------------------------------------- 1 | import 'package:cloud_firestore/cloud_firestore.dart'; 2 | import 'package:travel_budget/models/Trip.dart'; 3 | import 'package:travel_budget/widgets/provider_widget.dart'; 4 | 5 | class FirebaseService { 6 | 7 | static Future getNextTrip(context) async { 8 | final uid = Provider.of(context).auth.getCurrentUID(); 9 | var snapshot = await FirebaseFirestore.instance 10 | .collection('userData') 11 | .doc(uid) 12 | .collection('trips') 13 | .orderBy('startDate') 14 | .limit(1) 15 | .get(); 16 | return Trip.fromSnapshot(snapshot.docs.first); 17 | } 18 | 19 | static void addToLedger(context, documentId, item) async { 20 | await Provider.of(context).db 21 | .collection('userData') 22 | .doc(Provider.of(context).auth.getCurrentUID()) 23 | .collection('trips') 24 | .doc(documentId) 25 | .update(item); 26 | } 27 | } -------------------------------------------------------------------------------- /lib/widgets/money_text_field.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter/services.dart'; 3 | 4 | class MoneyTextField extends StatelessWidget { 5 | final TextEditingController controller; 6 | final String helperText; 7 | 8 | const MoneyTextField({Key key, @required this.controller, this.helperText}) 9 | : super(key: key); 10 | 11 | @override 12 | Widget build(BuildContext context) { 13 | return Padding( 14 | padding: const EdgeInsets.all(30.0), 15 | child: TextField( 16 | controller: controller, 17 | maxLines: 1, 18 | decoration: InputDecoration( 19 | prefixIcon: Icon(Icons.attach_money), 20 | helperText: helperText, 21 | ), 22 | keyboardType: TextInputType.numberWithOptions(decimal: false), 23 | inputFormatters: [ 24 | WhitelistingTextInputFormatter.digitsOnly, 25 | ], 26 | autofocus: false, 27 | ), 28 | ); 29 | } 30 | } 31 | 32 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Dave Faliskie 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /test/widget_test.dart: -------------------------------------------------------------------------------- 1 | // This is a basic Flutter widget test. 2 | // 3 | // To perform an interaction with a widget in your test, use the WidgetTester 4 | // utility that Flutter provides. For example, you can send tap and scroll 5 | // gestures. You can also use WidgetTester to find child widgets in the widget 6 | // tree, read text, and verify that the values of widget properties are correct. 7 | 8 | import 'package:flutter/material.dart'; 9 | import 'package:flutter_test/flutter_test.dart'; 10 | 11 | import 'package:travel_budget/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/views/home_widgets/home_header.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:travel_budget/models/Trip.dart'; 3 | 4 | class HomeHeader extends StatelessWidget { 5 | HomeHeader(this.trip); 6 | final Trip trip; 7 | 8 | @override 9 | Widget build(BuildContext context) { 10 | return Container( 11 | width: MediaQuery.of(context).size.width, 12 | height: MediaQuery.of(context).size.height * 0.25, 13 | decoration: BoxDecoration( 14 | gradient: LinearGradient( 15 | colors: [Colors.deepPurple, Colors.blueAccent], 16 | begin: Alignment.topCenter, 17 | end: Alignment.bottomCenter, 18 | ) 19 | ), 20 | 21 | child: SafeArea( 22 | child: Column( 23 | mainAxisAlignment: MainAxisAlignment.center, 24 | children: [ 25 | Padding( 26 | padding: const EdgeInsets.all(8.0), 27 | child: FittedBox( 28 | fit: BoxFit.fitWidth, 29 | child: Text("\$${(trip.saved ?? 0.0).floor()}", style: TextStyle(color: Colors.white, fontSize: 65)), 30 | ), 31 | ), 32 | Text("Total Saved", style: TextStyle(color: Colors.white)), 33 | ], 34 | ), 35 | ), 36 | ); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Travel Treasury 2 | [Download The Live App](https://traveltreasury.app) 3 | 4 | ![alt text](https://travel-treasury.s3.amazonaws.com/background_3phones.png) 5 | 6 | This repository contains all the code written throughout the 1ManStartup YouTube tutorials for building a travel budget app using Flutter 7 | 8 | [View the channel on YouTube](https://www.youtube.com/channel/UC8xcnxN4CyXdPCeUN1eURPg) 9 | 10 | ## How To Use This Resource 11 | 12 | Each episode where code is created or modified will have an associated branch 13 | in this repo. The code in each episode's branch will contain the completed code from that episode 14 | and the branch will remain in that state. 15 | 16 | The master branch will contain the most recent version of code, and be considered the "production" version. 17 | This means the master branch will always be the most up to date. 18 | 19 | 20 | Any questions should be asked in the comments of the relevant video on YouTube. 21 | 22 | 23 | ## Setup Firebase Database 24 | 25 | After Episode 15 you will need to configure your own Firebase project. Most importantly you will need to generate and include your own google-services.json file for Android and GoogleServices-Info.plist file for iOS. Full instructions on how to configure Firebase for this project can be found in [Episode 15 on YouTube](https://youtu.be/8L0YWmVYIqU) 26 | -------------------------------------------------------------------------------- /lib/views/home_widgets/days_until_trip.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:travel_budget/models/Trip.dart'; 3 | 4 | class DaysUntilTrip extends StatelessWidget { 5 | DaysUntilTrip(this.trip); 6 | final Trip trip; 7 | 8 | @override 9 | Widget build(BuildContext context) { 10 | return Card( 11 | child: Container( 12 | decoration: BoxDecoration( 13 | borderRadius: BorderRadius.circular(4.0), 14 | gradient: LinearGradient( 15 | colors: [Colors.lightBlue, Colors.blueAccent], 16 | begin: Alignment.topLeft, 17 | end: Alignment.bottomRight, 18 | ), 19 | ), 20 | child: Padding( 21 | padding: const EdgeInsets.all(20.0), 22 | child: Column( 23 | mainAxisAlignment: MainAxisAlignment.spaceEvenly, 24 | children: [ 25 | FittedBox( 26 | fit: BoxFit.fitWidth, 27 | child: Text("${trip.getDaysUntilTrip()}", style: TextStyle(fontSize: 60, color: Colors.white)), 28 | ), 29 | FittedBox( 30 | fit: BoxFit.fitWidth, 31 | child: Text("days until your trip", style: TextStyle(color: Colors.white)), 32 | ), 33 | ], 34 | ), 35 | ), 36 | ), 37 | ); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /lib/views/home_widgets/current_daily_budget.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:travel_budget/models/Trip.dart'; 3 | 4 | class CurrentDailyBudget extends StatelessWidget { 5 | CurrentDailyBudget(this.trip); 6 | final Trip trip; 7 | 8 | @override 9 | Widget build(BuildContext context) { 10 | return Card( 11 | child: Container( 12 | decoration: BoxDecoration( 13 | borderRadius: BorderRadius.circular(4.0), 14 | gradient: LinearGradient( 15 | colors: [Colors.blueAccent, Colors.blue], 16 | begin: Alignment.topLeft, 17 | end: Alignment.bottomRight, 18 | ), 19 | ), 20 | child: Padding( 21 | padding: const EdgeInsets.all(20.0), 22 | child: Column( 23 | mainAxisAlignment: MainAxisAlignment.spaceEvenly, 24 | children: [ 25 | FittedBox( 26 | fit: BoxFit.fitWidth, 27 | child: Text("\$${trip.getCurrentDailyBudget()}", style: TextStyle(fontSize: 60, color: Colors.white)), 28 | ), 29 | FittedBox( 30 | fit: BoxFit.fitWidth, 31 | child: Text("current daily budget", style: TextStyle(color: Colors.white)), 32 | ), 33 | ], 34 | ), 35 | ), 36 | ), 37 | ); 38 | } 39 | } 40 | 41 | -------------------------------------------------------------------------------- /lib/classes/progress_painter.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'dart:math'; 3 | 4 | class ProgressPainter extends CustomPainter { 5 | Color defaultCircleColor; 6 | Color percentageCompletedCircleColor; 7 | double completedPercentage; 8 | double circleWidth; 9 | 10 | ProgressPainter( 11 | {this.defaultCircleColor, 12 | this.percentageCompletedCircleColor, 13 | this.completedPercentage, 14 | this.circleWidth}); 15 | 16 | getPaint(Color color) { 17 | return Paint() 18 | ..color = color 19 | ..strokeCap = StrokeCap.round 20 | ..style = PaintingStyle.stroke 21 | ..strokeWidth = circleWidth; 22 | } 23 | 24 | @override 25 | void paint(Canvas canvas, Size size) { 26 | Paint defaultCirclePaint = getPaint(defaultCircleColor); 27 | Paint progressCirclePaint = getPaint(percentageCompletedCircleColor); 28 | 29 | Offset center = Offset(size.width / 2, size.height / 2); 30 | double radius = min(size.width / 2, size.height / 2); 31 | canvas.drawCircle(center, radius, defaultCirclePaint); 32 | 33 | double arcAngle = 2 * pi * (completedPercentage / 100); 34 | canvas.drawArc(Rect.fromCircle(center: center, radius: radius), -pi / 2, 35 | arcAngle, false, progressCirclePaint); 36 | } 37 | 38 | @override 39 | bool shouldRepaint(CustomPainter painter) { 40 | return true; 41 | } 42 | } -------------------------------------------------------------------------------- /ios/Podfile: -------------------------------------------------------------------------------- 1 | # Uncomment this line to define a global platform for your project 2 | # platform :ios, '9.0' 3 | 4 | # CocoaPods analytics sends network stats synchronously affecting flutter build latency. 5 | ENV['COCOAPODS_DISABLE_STATS'] = 'true' 6 | 7 | project 'Runner', { 8 | 'Debug' => :debug, 9 | 'Profile' => :release, 10 | 'Release' => :release, 11 | } 12 | 13 | def flutter_root 14 | generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__) 15 | unless File.exist?(generated_xcode_build_settings_path) 16 | raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first" 17 | end 18 | 19 | File.foreach(generated_xcode_build_settings_path) do |line| 20 | matches = line.match(/FLUTTER_ROOT\=(.*)/) 21 | return matches[1].strip if matches 22 | end 23 | raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get" 24 | end 25 | 26 | require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) 27 | 28 | flutter_ios_podfile_setup 29 | 30 | target 'Runner' do 31 | use_frameworks! 32 | use_modular_headers! 33 | 34 | flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__)) 35 | end 36 | 37 | post_install do |installer| 38 | installer.pods_project.targets.each do |target| 39 | flutter_additional_ios_build_settings(target) 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /ios/Runner/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 | -------------------------------------------------------------------------------- /.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 | # Visual Studio Code related 19 | .vscode/ 20 | 21 | # Flutter/Dart/Pub related 22 | **/doc/api/ 23 | .dart_tool/ 24 | .flutter-plugins 25 | .flutter-plugins-dependencies 26 | .packages 27 | .pub-cache/ 28 | .pub/ 29 | /build/ 30 | 31 | # Android related 32 | **/android/**/gradle-wrapper.jar 33 | **/android/.gradle 34 | **/android/captures/ 35 | **/android/gradlew 36 | **/android/gradlew.bat 37 | **/android/local.properties 38 | **/android/**/GeneratedPluginRegistrant.java 39 | **/android/key.properties 40 | 41 | # iOS/XCode related 42 | **/ios/**/*.mode1v3 43 | **/ios/**/*.mode2v3 44 | **/ios/**/*.moved-aside 45 | **/ios/**/*.pbxuser 46 | **/ios/**/*.perspectivev3 47 | **/ios/**/*sync/ 48 | **/ios/**/.sconsign.dblite 49 | **/ios/**/.tags* 50 | **/ios/**/.vagrant/ 51 | **/ios/**/DerivedData/ 52 | **/ios/**/Icon? 53 | **/ios/**/Pods/ 54 | **/ios/**/.symlinks/ 55 | **/ios/**/profile 56 | **/ios/**/xcuserdata 57 | **/ios/.generated/ 58 | **/ios/Flutter/App.framework 59 | **/ios/Flutter/Flutter.framework 60 | **/ios/Flutter/Generated.xcconfig 61 | **/ios/Flutter/app.flx 62 | **/ios/Flutter/app.zip 63 | **/ios/Flutter/flutter_assets/ 64 | **/ios/ServiceDefinitions.json 65 | **/ios/Runner/GeneratedPluginRegistrant.* 66 | 67 | # Exceptions to above rules. 68 | !**/ios/**/default.mode1v3 69 | !**/ios/**/default.mode2v3 70 | !**/ios/**/default.pbxuser 71 | !**/ios/**/default.perspectivev3 72 | !/packages/flutter_tools/test/data/dart_dependencies_test/**/.packages 73 | 74 | 75 | *google-services.json 76 | *GoogleService-Info.plist 77 | *lib/credentials.dart -------------------------------------------------------------------------------- /lib/views/home_widgets/travel_type.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:travel_budget/models/Trip.dart'; 3 | 4 | class TravelType extends StatelessWidget { 5 | TravelType(this.trip); 6 | final Trip trip; 7 | 8 | Widget getTypeIcon() { 9 | if (trip.types().containsKey(trip.travelType)) { 10 | return trip.types(color: Colors.white)[trip.travelType]; 11 | } else { 12 | return Icon(Icons.directions, size: 40, color: Colors.white); 13 | } 14 | } 15 | 16 | @override 17 | Widget build(BuildContext context) { 18 | return Card( 19 | child: Container( 20 | decoration: BoxDecoration( 21 | borderRadius: BorderRadius.circular(4.0), 22 | gradient: LinearGradient( 23 | colors: [Colors.lightBlue, Colors.blueAccent], 24 | begin: Alignment.topLeft, 25 | end: Alignment.bottomRight, 26 | ), 27 | ), 28 | child: Padding( 29 | padding: const EdgeInsets.all(20.0), 30 | child: Column( 31 | mainAxisAlignment: MainAxisAlignment.spaceEvenly, 32 | children: [ 33 | FittedBox( 34 | fit: BoxFit.fitWidth, 35 | child: Padding( 36 | padding: const EdgeInsets.only(top: 6.0), 37 | child: Text( 38 | "transport", 39 | style: TextStyle(color: Colors.white), 40 | ), 41 | ), 42 | ), 43 | Expanded( 44 | child: getTypeIcon(), 45 | ), 46 | Text(trip.travelType, style: TextStyle(color: Colors.white)), 47 | ], 48 | ), 49 | ), 50 | ), 51 | 52 | ); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /lib/services/admob_service.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | import 'package:firebase_admob/firebase_admob.dart'; 3 | import 'package:flutter/material.dart'; 4 | 5 | class AdMobService { 6 | 7 | String getAdMobAppId() { 8 | if (Platform.isIOS) { 9 | return 'ca-app-pub-2334510780816542~6726672523'; 10 | } else if (Platform.isAndroid) { 11 | return 'ca-app-pub-2334510780816542~7385148076'; 12 | } 13 | return null; 14 | } 15 | 16 | static String _getBannerAdId() { 17 | if (Platform.isIOS) { 18 | // return 'ca-app-pub-2334510780816542/6833456062'; 19 | return 'ca-app-pub-3940256099942544/2934735716'; 20 | } else if (Platform.isAndroid) { 21 | // return 'ca-app-pub-2334510780816542/2993163849'; 22 | return "ca-app-pub-3940256099942544/6300978111"; 23 | } 24 | return null; 25 | } 26 | 27 | String getInterstitialAdId() { 28 | if (Platform.isIOS) { 29 | // return ''; 30 | return 'ca-app-pub-3940256099942544/4411468910'; 31 | } else if (Platform.isAndroid) { 32 | // return ''; 33 | return "ca-app-pub-3940256099942544/1033173712"; 34 | } 35 | return null; 36 | } 37 | 38 | 39 | InterstitialAd getNewTripInterstitial() { 40 | return InterstitialAd( 41 | adUnitId: getInterstitialAdId(), 42 | listener: (MobileAdEvent event) { 43 | print("InterstitialAd event is $event"); 44 | }, 45 | ); 46 | } 47 | 48 | static BannerAd _homeBannerAd; 49 | 50 | static BannerAd _getHomePageBannerAd() { 51 | return BannerAd( 52 | adUnitId: _getBannerAdId(), 53 | size: AdSize.smartBanner 54 | ); 55 | } 56 | 57 | static void showHomeBannerAd() { 58 | if ( _homeBannerAd == null ) _homeBannerAd = _getHomePageBannerAd(); 59 | _homeBannerAd 60 | ..load() 61 | ..show(anchorType: AnchorType.bottom, anchorOffset: kBottomNavigationBarHeight); 62 | } 63 | 64 | static void hideHomeBannerAd() async { 65 | await _homeBannerAd.dispose(); 66 | _homeBannerAd = null; 67 | } 68 | 69 | 70 | } -------------------------------------------------------------------------------- /android/app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 9 | 13 | 20 | 24 | 27 | 28 | 29 | 30 | 31 | 32 | 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /lib/views/home_widgets/notes.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:travel_budget/models/Trip.dart'; 3 | import 'package:travel_budget/views/edit_notes_view.dart'; 4 | 5 | class Notes extends StatelessWidget { 6 | final Trip trip; 7 | 8 | Notes({@required this.trip}); 9 | 10 | @override 11 | Widget build(BuildContext context) { 12 | return Hero( 13 | tag: "TripNotes-${trip.title}", 14 | transitionOnUserGestures: true, 15 | child: Card( 16 | color: Colors.amberAccent, 17 | child: InkWell( 18 | child: Column( 19 | children: [ 20 | Padding( 21 | padding: const EdgeInsets.only(left: 10.0, top: 8.0), 22 | child: Row( 23 | children: [ 24 | Text("Notes", style: TextStyle(fontSize: 24, color: Colors.black)), 25 | ], 26 | ), 27 | ), 28 | Padding( 29 | padding: const EdgeInsets.all(8.0), 30 | child: Row( 31 | children: setNoteText(), 32 | ), 33 | ) 34 | ], 35 | ), 36 | onTap: () { 37 | Navigator.push( 38 | context, 39 | MaterialPageRoute(builder: (context) => EditNotesView(trip: trip)), 40 | ); 41 | }, 42 | ), 43 | ), 44 | ); 45 | } 46 | 47 | List setNoteText() { 48 | if (trip.notes == null) { 49 | return [ 50 | Padding( 51 | padding: const EdgeInsets.only(right: 8.0), 52 | child: Icon(Icons.add_circle_outline, color: Colors.grey), 53 | ), 54 | Text("Click To Add Notes", style: TextStyle(color: Colors.black)), 55 | ]; 56 | } else { 57 | return [ 58 | Flexible( 59 | child: Padding( 60 | padding: const EdgeInsets.only(left: 8.0, right: 8.0), 61 | child: Text( 62 | trip.notes, 63 | style: TextStyle(color: Colors.black), 64 | overflow: TextOverflow.fade, 65 | maxLines: 5, 66 | ), 67 | ), 68 | ) 69 | ]; 70 | } 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /ios/Runner/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | Travel Treasury 15 | CFBundlePackageType 16 | APPL 17 | CFBundleShortVersionString 18 | $(FLUTTER_BUILD_NAME) 19 | CFBundleSignature 20 | ???? 21 | CFBundleURLTypes 22 | 23 | 24 | CFBundleTypeRole 25 | Editor 26 | CFBundleURLSchemes 27 | 28 | com.googleusercontent.apps.635382246089-feadgcif5jb704jtq2nir0d6cnh8fu02 29 | 30 | 31 | 32 | CFBundleTypeRole 33 | Editor 34 | CFBundleURLSchemes 35 | 36 | com.googleusercontent.apps.635382246089-egv2iuv19s1e0gjiq7rqvngu4c7e7su0 37 | 38 | 39 | 40 | CFBundleVersion 41 | $(FLUTTER_BUILD_NUMBER) 42 | GADApplicationIdentifier 43 | ca-app-pub-2334510780816542~6726672523 44 | LSApplicationCategoryType 45 | 46 | LSRequiresIPhoneOS 47 | 48 | UIBackgroundModes 49 | 50 | fetch 51 | remote-notification 52 | 53 | UILaunchStoryboardName 54 | LaunchScreen 55 | UIMainStoryboardFile 56 | Main 57 | UISupportedInterfaceOrientations 58 | 59 | UIInterfaceOrientationPortrait 60 | 61 | UISupportedInterfaceOrientations~ipad 62 | 63 | UIInterfaceOrientationPortrait 64 | UIInterfaceOrientationPortraitUpsideDown 65 | UIInterfaceOrientationLandscapeLeft 66 | UIInterfaceOrientationLandscapeRight 67 | 68 | UIViewControllerBasedStatusBarAppearance 69 | 70 | io.flutter.embedded_views_preview 71 | 72 | 73 | 74 | -------------------------------------------------------------------------------- /lib/views/home_widgets/percent_saved.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:travel_budget/classes/progress_painter.dart'; 3 | import 'package:travel_budget/models/Trip.dart'; 4 | 5 | class PercentSaved extends StatelessWidget { 6 | PercentSaved(this.trip); 7 | final Trip trip; 8 | 9 | @override 10 | Widget build(BuildContext context) { 11 | final totalBudget = trip.budget.floor() * trip.getTotalTripDays(); 12 | final saved = (trip.saved ?? 0).floor(); 13 | final percentComplete = (saved / totalBudget) * 100; 14 | 15 | return Card( 16 | child: Container( 17 | decoration: BoxDecoration( 18 | borderRadius: BorderRadius.circular(4.0), 19 | gradient: LinearGradient( 20 | colors: [Colors.indigoAccent, Colors.indigo], 21 | begin: Alignment.topLeft, 22 | end: Alignment.bottomRight, 23 | ), 24 | ), 25 | child: Padding( 26 | padding: const EdgeInsets.all(25.0), 27 | child: Column( 28 | children: [ 29 | Container( 30 | height: 200, 31 | width: 200, 32 | decoration: BoxDecoration( 33 | shape: BoxShape.circle, 34 | ), 35 | child: Padding( 36 | padding: const EdgeInsets.all(20.0), 37 | child: Stack( 38 | children: [ 39 | Center( 40 | child: Text("${percentComplete.toStringAsFixed(0)}%", 41 | style: TextStyle(color: Colors.white, fontSize: 30))), 42 | CustomPaint( 43 | child: Center(), 44 | painter: ProgressPainter( 45 | circleWidth: 40, 46 | completedPercentage: percentComplete, 47 | defaultCircleColor: Colors.white30, 48 | percentageCompletedCircleColor: Colors.greenAccent, 49 | ), 50 | ), 51 | ], 52 | ), 53 | ), 54 | ), 55 | Container( 56 | child: Padding( 57 | padding: const EdgeInsets.only(top: 15.0), 58 | child: Text("percent saved", style: TextStyle(color: Colors.white)), 59 | ), 60 | ), 61 | ], 62 | ), 63 | ), 64 | ), 65 | ); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /lib/widgets/trip_card.dart: -------------------------------------------------------------------------------- 1 | import 'package:cloud_firestore/cloud_firestore.dart'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:google_fonts/google_fonts.dart'; 4 | import 'package:intl/intl.dart'; 5 | import 'package:travel_budget/models/Trip.dart'; 6 | import 'package:travel_budget/services/admob_service.dart'; 7 | import 'package:travel_budget/views/detail_trip_view.dart'; 8 | 9 | Widget buildTripCard(BuildContext context, DocumentSnapshot document, [bool loadBannerAd]) { 10 | final trip = Trip.fromSnapshot(document); 11 | final tripType = trip.types(); 12 | 13 | return new Container( 14 | child: Card( 15 | child: InkWell( 16 | child: Padding( 17 | padding: const EdgeInsets.all(16.0), 18 | child: Column( 19 | children: [ 20 | Padding( 21 | padding: const EdgeInsets.only(top: 8.0, bottom: 4.0), 22 | child: Row(children: [ 23 | Text( 24 | trip.title, 25 | style: GoogleFonts.seymourOne(fontSize: 20.0), 26 | ), 27 | Spacer(), 28 | ]), 29 | ), 30 | Padding( 31 | padding: const EdgeInsets.only(top: 4.0, bottom: 80.0), 32 | child: Row(children: [ 33 | Text( 34 | "${DateFormat('MM/dd/yyyy').format(trip.startDate).toString()} - ${DateFormat('MM/dd/yyyy').format(trip.endDate).toString()}"), 35 | Spacer(), 36 | ]), 37 | ), 38 | Padding( 39 | padding: const EdgeInsets.only(top: 8.0, bottom: 8.0), 40 | child: Row( 41 | children: [ 42 | Text( 43 | "\$${(trip.budget == null) ? "n/a" : trip.budget.toStringAsFixed(2)}", 44 | style: new TextStyle(fontSize: 35.0), 45 | ), 46 | Spacer(), 47 | (tripType.containsKey(trip.travelType)) ? tripType[trip.travelType] : tripType["other"], 48 | ], 49 | ), 50 | ) 51 | ], 52 | ), 53 | ), 54 | onTap: () { 55 | if (loadBannerAd == true) { 56 | Navigator.push(context, MaterialPageRoute(builder: (context) => DetailTripView(trip: trip))).then((value) { 57 | AdMobService.showHomeBannerAd(); 58 | }); 59 | } else { 60 | Navigator.push(context, MaterialPageRoute(builder: (context) => DetailTripView(trip: trip))); 61 | } 62 | }, 63 | ), 64 | ), 65 | ); 66 | } 67 | -------------------------------------------------------------------------------- /lib/views/home_view.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:travel_budget/models/Trip.dart'; 3 | import 'package:travel_budget/views/home_widgets/home_header.dart'; 4 | import 'package:travel_budget/views/home_widgets/percent_saved.dart'; 5 | import 'package:travel_budget/views/home_widgets/saved_vs_needed.dart'; 6 | import 'package:travel_budget/views/home_widgets/travel_type.dart'; 7 | import 'package:travel_budget/views/home_widgets/trip_details_card.dart'; 8 | 9 | import 'home_widgets/current_daily_budget.dart'; 10 | import 'home_widgets/days_until_trip.dart'; 11 | import 'home_widgets/notes.dart'; 12 | 13 | class HomeView extends StatefulWidget { 14 | final Trip trip; 15 | 16 | HomeView({ 17 | @required this.trip, 18 | }); 19 | 20 | @override 21 | _HomeViewState createState() => _HomeViewState(); 22 | } 23 | 24 | class _HomeViewState extends State { 25 | @override 26 | Widget build(BuildContext context) { 27 | return SingleChildScrollView( 28 | child: Column( 29 | children: [ 30 | HomeHeader(widget.trip), 31 | TripDetailsCard(widget.trip), 32 | Padding( 33 | padding: const EdgeInsets.only(left: 15.0, right: 15.0), 34 | child: IntrinsicHeight( 35 | child: Row( 36 | children: [ 37 | Expanded( 38 | flex: 12, 39 | child: DaysUntilTrip(widget.trip), 40 | ), 41 | Spacer(), 42 | Expanded( 43 | flex: 12, 44 | child: CurrentDailyBudget(widget.trip), 45 | ) 46 | ], 47 | ), 48 | ), 49 | ), 50 | Padding( 51 | padding: const EdgeInsets.all(15.0), 52 | child: SavedVsNeeded(widget.trip), 53 | ), 54 | 55 | Padding( 56 | padding: const EdgeInsets.only(left: 15.0, right: 15.0), 57 | child: IntrinsicHeight( 58 | child: Row( 59 | children: [ 60 | Expanded( 61 | flex: 5, 62 | child: TravelType(widget.trip), 63 | ), 64 | Spacer(), 65 | Expanded( 66 | flex: 12, 67 | child: PercentSaved(widget.trip), 68 | ) 69 | ], 70 | ), 71 | ), 72 | ), 73 | 74 | Padding( 75 | padding: const EdgeInsets.all(15.0), 76 | child: Notes(trip: widget.trip), 77 | ), 78 | 79 | Container(height: 40) 80 | ], 81 | ), 82 | ); 83 | } 84 | } 85 | 86 | -------------------------------------------------------------------------------- /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 | 39 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /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 FileNotFoundException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.") 12 | } 13 | 14 | def flutterVersionCode = localProperties.getProperty('flutter.versionCode') 15 | if (flutterVersionCode == null) { 16 | flutterVersionCode = '1' 17 | } 18 | 19 | def flutterVersionName = localProperties.getProperty('flutter.versionName') 20 | if (flutterVersionName == null) { 21 | flutterVersionName = '1.0' 22 | } 23 | 24 | apply plugin: 'com.android.application' 25 | apply plugin: 'kotlin-android' 26 | apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" 27 | apply plugin: 'com.google.firebase.crashlytics' 28 | 29 | def keystoreProperties = new Properties() 30 | def keystorePropertiesFile = rootProject.file('key.properties') 31 | if (keystorePropertiesFile.exists()) { 32 | keystoreProperties.load(new FileInputStream(keystorePropertiesFile)) 33 | } 34 | 35 | android { 36 | compileSdkVersion 28 37 | 38 | sourceSets { 39 | main.java.srcDirs += 'src/main/kotlin' 40 | } 41 | 42 | lintOptions { 43 | disable 'InvalidPackage' 44 | } 45 | 46 | defaultConfig { 47 | applicationId "com.a1manstartup.travel_budget" 48 | minSdkVersion 21 49 | targetSdkVersion 29 50 | versionCode flutterVersionCode.toInteger() 51 | versionName flutterVersionName 52 | testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" 53 | // TODO: find a better solution to the DEX reference limit https://developer.android.com/studio/build/multidex 54 | multiDexEnabled true 55 | } 56 | 57 | signingConfigs { 58 | release { 59 | keyAlias keystoreProperties['keyAlias'] 60 | keyPassword keystoreProperties['keyPassword'] 61 | storeFile keystoreProperties['storeFile'] ? file(keystoreProperties['storeFile']) : null 62 | storePassword keystoreProperties['storePassword'] 63 | } 64 | } 65 | 66 | buildTypes { 67 | release { 68 | signingConfig signingConfigs.release 69 | } 70 | } 71 | } 72 | 73 | flutter { 74 | source '../..' 75 | } 76 | 77 | dependencies { 78 | implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" 79 | testImplementation 'junit:junit:4.12' 80 | androidTestImplementation 'com.android.support.test:runner:1.0.2' 81 | androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2' 82 | implementation 'com.android.support:multidex:1.0.3' 83 | } 84 | 85 | 86 | apply plugin: 'com.google.gms.google-services' -------------------------------------------------------------------------------- /lib/views/home_widgets/trip_details_card.dart: -------------------------------------------------------------------------------- 1 | import 'package:auto_size_text/auto_size_text.dart'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:intl/intl.dart'; 4 | import 'package:travel_budget/models/Trip.dart'; 5 | 6 | class TripDetailsCard extends StatelessWidget { 7 | TripDetailsCard(this.trip); 8 | final Trip trip; 9 | 10 | @override 11 | Widget build(BuildContext context) { 12 | return Container( 13 | child: Stack( 14 | overflow: Overflow.visible, 15 | alignment: Alignment.bottomCenter, 16 | children: [ 17 | Container( 18 | child: Column( 19 | children: [ 20 | SizedBox( 21 | width: double.infinity, 22 | height: 250, 23 | child: trip.getLocationImage(), 24 | ), 25 | SizedBox( 26 | height: 50, 27 | width: double.infinity, 28 | ) 29 | ], 30 | ), 31 | ), 32 | Positioned( 33 | top: 150, 34 | left: 15, 35 | right: 15, 36 | child: Card( 37 | child: Padding( 38 | padding: const EdgeInsets.all(16.0), 39 | child: Column( 40 | children: [ 41 | Row( 42 | children: [ 43 | Expanded( 44 | child: Container( 45 | height: 40, 46 | child: AutoSizeText( 47 | trip.title, 48 | style: TextStyle(fontSize: 30.0), 49 | maxLines: 2, 50 | ), 51 | ), 52 | ) 53 | ], 54 | ), 55 | Row( 56 | children: [ 57 | Padding( 58 | padding: const EdgeInsets.only(bottom: 8.0), 59 | child: Text( 60 | "${DateFormat('MMM dd, yyyy').format(trip.startDate).toString()} - ${DateFormat('MMM dd, yyyy').format(trip.endDate).toString()}"), 61 | ), 62 | ], 63 | ), 64 | Row( 65 | children: [ 66 | Text("\$${(trip.budget.floor() * trip.getTotalTripDays()).toString()}", 67 | style: TextStyle(fontSize: 25.0, fontWeight: FontWeight.bold)), 68 | Padding( 69 | padding: const EdgeInsets.only(top: 8.0, left: 4.0), 70 | child: Text("total budget"), 71 | ), 72 | ], 73 | ) 74 | ], 75 | ), 76 | ), 77 | ), 78 | ) 79 | ], 80 | ), 81 | ); 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /lib/views/new_trips/summary_view.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:travel_budget/models/Trip.dart'; 3 | import 'package:cloud_firestore/cloud_firestore.dart'; 4 | import 'package:travel_budget/widgets/provider_widget.dart'; 5 | import 'package:intl/intl.dart'; 6 | import 'package:travel_budget/services/admob_service.dart'; 7 | import 'package:firebase_admob/firebase_admob.dart'; 8 | 9 | 10 | class NewTripSummaryView extends StatelessWidget { 11 | final db = FirebaseFirestore.instance; 12 | final Trip trip; 13 | final ams = AdMobService(); 14 | 15 | NewTripSummaryView({Key key, @required this.trip}) : super(key: key); 16 | 17 | @override 18 | Widget build(BuildContext context) { 19 | InterstitialAd newTripAd = ams.getNewTripInterstitial(); 20 | newTripAd.load(); 21 | final tripTypes = trip.types(); 22 | var tripKeys = tripTypes.keys.toList(); 23 | return Scaffold( 24 | appBar: AppBar( 25 | title: Text('Trip Summary'), 26 | ), 27 | body: Center( 28 | child: Column( 29 | mainAxisAlignment: MainAxisAlignment.center, 30 | children: [ 31 | Padding( 32 | padding: const EdgeInsets.all(12.0), 33 | child: Text("Submit", style: TextStyle(fontSize: 20, color: Colors.green),), 34 | ), 35 | Text("${trip.title}"), 36 | Text("${DateFormat('dd/MM/yyyy').format(trip.startDate).toString()} - ${DateFormat('dd/MM/yyyy').format(trip.endDate).toString()}"), 37 | Text("\$${trip.budget.toStringAsFixed(2)}"), 38 | 39 | Expanded( 40 | child: GridView.count( 41 | crossAxisCount: 3, 42 | scrollDirection: Axis.vertical, 43 | primary: false, 44 | children: List.generate(tripTypes.length, (index) { 45 | return FlatButton( 46 | child: Column( 47 | mainAxisAlignment: MainAxisAlignment.center, 48 | children: [ 49 | tripTypes[tripKeys[index]], 50 | Text(tripKeys[index]), 51 | ], 52 | ), 53 | onPressed: () async { 54 | trip.travelType = tripKeys[index]; 55 | final uid = await Provider.of(context).auth.getCurrentUID(); 56 | await db.collection("userData").doc(uid).collection("trips").add(trip.toJson()); 57 | newTripAd.show( 58 | anchorType: AnchorType.bottom, 59 | anchorOffset: 0.0, 60 | horizontalCenterOffset: 0.0, 61 | ); 62 | Navigator.of(context).popUntil((route) => route.isFirst); 63 | }, 64 | ); 65 | }), 66 | ), 67 | ), 68 | ], 69 | ) 70 | ) 71 | ); 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /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/views/navigation_view.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:travel_budget/services/firebase_service.dart'; 3 | import 'package:travel_budget/views/home_view.dart'; 4 | import 'deposit_view.dart'; 5 | import 'profile_view.dart'; 6 | import 'package:travel_budget/models/Trip.dart'; 7 | 8 | class NavigationView extends StatefulWidget { 9 | @override 10 | State createState() { 11 | return _NavigationViewState(); 12 | } 13 | } 14 | 15 | class _NavigationViewState extends State { 16 | Future _nextTrip; 17 | Trip _trip; 18 | int _currentIndex = 0; 19 | 20 | @override 21 | void didChangeDependencies() { 22 | super.didChangeDependencies(); 23 | _nextTrip = FirebaseService.getNextTrip(context); 24 | } 25 | 26 | @override 27 | Widget build(BuildContext context) { 28 | return FutureBuilder( 29 | future: _nextTrip, 30 | builder: (context, snapshot) { 31 | if (snapshot.connectionState == ConnectionState.done) { 32 | if(snapshot.hasData) { 33 | _trip = snapshot.data; 34 | return _buildView(); 35 | } else { 36 | // TODO update when user has no trips 37 | return Container( 38 | color: Colors.white, 39 | child: Center( 40 | child: CircularProgressIndicator(), 41 | ), 42 | ); 43 | } 44 | } else { 45 | return Container( 46 | color: Colors.white, 47 | child: Center( 48 | child: CircularProgressIndicator(), 49 | ), 50 | ); 51 | } 52 | }, 53 | ); 54 | } 55 | 56 | _buildView() { 57 | final List _children = [ 58 | HomeView(trip: _trip), 59 | DepositView(trip: _trip), 60 | ProfileView(), 61 | ]; 62 | 63 | return Scaffold( 64 | body: _children[_currentIndex], 65 | floatingActionButton: FloatingActionButton( 66 | onPressed: () { 67 | onTabTapped(1); 68 | }, 69 | tooltip: "Add Savings", 70 | child: Icon(Icons.attach_money, color: Colors.indigo), 71 | elevation: 4.0, 72 | backgroundColor: Colors.white, 73 | ), 74 | floatingActionButtonLocation: FloatingActionButtonLocation.centerDocked, 75 | bottomNavigationBar: BottomNavigationBar( 76 | onTap: onTabTapped, 77 | currentIndex: _currentIndex, 78 | items: [ 79 | BottomNavigationBarItem( 80 | icon: new Icon(Icons.home), 81 | title: new Text("Home"), 82 | ), 83 | BottomNavigationBarItem( 84 | icon: new Icon(Icons.attach_money), 85 | title: new Text("Save"), 86 | ), 87 | BottomNavigationBarItem( 88 | icon: new Icon(Icons.account_circle), 89 | title: new Text("Profile"), 90 | ), 91 | ] 92 | ), 93 | ); 94 | } 95 | 96 | void onTabTapped(int index) { 97 | setState(() { 98 | _currentIndex = index; 99 | }); 100 | } 101 | 102 | } 103 | -------------------------------------------------------------------------------- /lib/views/home_widgets/saved_vs_needed.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:travel_budget/models/Trip.dart'; 3 | 4 | class SavedVsNeeded extends StatelessWidget { 5 | SavedVsNeeded(this.trip); 6 | final Trip trip; 7 | 8 | @override 9 | Widget build(BuildContext context) { 10 | final saved = (trip.saved ?? 0.0).floor(); 11 | final totalBudget = trip.budget.floor() * trip.getTotalTripDays(); 12 | final needed = (totalBudget - saved).floor(); 13 | 14 | return Card( 15 | child: Container ( 16 | height: MediaQuery.of(context).size.height * 0.30, 17 | decoration: BoxDecoration( 18 | borderRadius: BorderRadius.circular(4.0), 19 | gradient: LinearGradient( 20 | colors: [Colors.blueAccent, Colors.indigo], 21 | begin: Alignment.topLeft, 22 | end: Alignment.bottomRight, 23 | ), 24 | ), 25 | 26 | child: Row( 27 | crossAxisAlignment: CrossAxisAlignment.end, 28 | mainAxisAlignment: MainAxisAlignment.spaceAround, 29 | children: [ 30 | Column( 31 | children: [ 32 | Padding( 33 | padding: const EdgeInsets.only(top: 15.0, bottom: 15.0), 34 | child: Text("saved", style: TextStyle(color: Colors.white)), 35 | ), 36 | Expanded( 37 | child: Container( 38 | alignment: Alignment.bottomCenter, 39 | child: FractionallySizedBox( 40 | heightFactor: (saved > totalBudget) ? 1 : (saved/totalBudget), 41 | child: Container( 42 | width: 50, 43 | color: Colors.white, 44 | ), 45 | ), 46 | ), 47 | ), 48 | Padding( 49 | padding: const EdgeInsets.only(top: 15.0, bottom: 15.0), 50 | child: Text("\$$saved", style: TextStyle(color: Colors.white)), 51 | ), 52 | ], 53 | ), 54 | Column( 55 | children: [ 56 | Padding( 57 | padding: const EdgeInsets.only(top: 15.0, bottom: 15.0), 58 | child: Text("needed", style: TextStyle(color: Colors.white)), 59 | ), 60 | Expanded( 61 | child: Container( 62 | alignment: Alignment.bottomCenter, 63 | child: FractionallySizedBox( 64 | heightFactor: (needed <= 0) ? 0 : (needed/totalBudget), 65 | child: Container( 66 | width: 50, 67 | color: Colors.white, 68 | ), 69 | ), 70 | ), 71 | ), 72 | Padding( 73 | padding: const EdgeInsets.only(top: 15.0, bottom: 15.0), 74 | child: Text("\$${(needed <= 0) ? 0 : needed}", style: TextStyle(color: Colors.white)), 75 | ), 76 | ], 77 | ) 78 | ], 79 | ), 80 | ), 81 | ); 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /lib/views/past_trips_view.dart: -------------------------------------------------------------------------------- 1 | import 'package:cloud_firestore/cloud_firestore.dart'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:travel_budget/models/Trip.dart'; 4 | import 'package:travel_budget/widgets/provider_widget.dart'; 5 | import 'package:travel_budget/widgets/trip_card.dart'; 6 | 7 | 8 | class PastTripsView extends StatefulWidget { 9 | @override 10 | _PastTripsViewState createState() => _PastTripsViewState(); 11 | } 12 | 13 | class _PastTripsViewState extends State { 14 | TextEditingController _searchController = TextEditingController(); 15 | 16 | Future resultsLoaded; 17 | List _allResults = []; 18 | List _resultsList = []; 19 | 20 | @override 21 | void initState() { 22 | super.initState(); 23 | _searchController.addListener(_onSearchChanged); 24 | } 25 | 26 | @override 27 | void dispose() { 28 | _searchController.removeListener(_onSearchChanged); 29 | _searchController.dispose(); 30 | super.dispose(); 31 | } 32 | 33 | @override 34 | void didChangeDependencies() { 35 | super.didChangeDependencies(); 36 | resultsLoaded = getUsersPastTripsStreamSnapshots(); 37 | } 38 | 39 | 40 | _onSearchChanged() { 41 | searchResultsList(); 42 | } 43 | 44 | searchResultsList() { 45 | var showResults = []; 46 | 47 | if(_searchController.text != "") { 48 | for(var tripSnapshot in _allResults){ 49 | var title = Trip.fromSnapshot(tripSnapshot).title.toLowerCase(); 50 | 51 | if(title.contains(_searchController.text.toLowerCase())) { 52 | showResults.add(tripSnapshot); 53 | } 54 | } 55 | 56 | } else { 57 | showResults = List.from(_allResults); 58 | } 59 | setState(() { 60 | _resultsList = showResults; 61 | }); 62 | } 63 | 64 | getUsersPastTripsStreamSnapshots() async { 65 | final uid = await Provider.of(context).auth.getCurrentUID(); 66 | var data = await FirebaseFirestore.instance 67 | .collection('userData') 68 | .doc(uid) 69 | .collection('trips') 70 | .where("endDate", isLessThanOrEqualTo: DateTime.now()) 71 | .orderBy('endDate') 72 | .get(); 73 | setState(() { 74 | _allResults = data.docs; 75 | }); 76 | searchResultsList(); 77 | return "complete"; 78 | } 79 | 80 | Widget build(BuildContext context) { 81 | return Container( 82 | child: Column( 83 | children: [ 84 | Text("Past Trips", style: TextStyle(fontSize: 20)), 85 | Padding( 86 | padding: const EdgeInsets.only(left: 30.0, right: 30.0, bottom: 30.0), 87 | child: TextField( 88 | controller: _searchController, 89 | decoration: InputDecoration( 90 | prefixIcon: Icon(Icons.search) 91 | ), 92 | ), 93 | ), 94 | Expanded( 95 | child: ListView.builder( 96 | itemCount: _resultsList.length, 97 | itemBuilder: (BuildContext context, int index) => 98 | buildTripCard(context, _resultsList[index]), 99 | ) 100 | 101 | ), 102 | ], 103 | ), 104 | ); 105 | } 106 | } -------------------------------------------------------------------------------- /lib/views/first_view.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:auto_size_text/auto_size_text.dart'; 3 | import 'package:travel_budget/widgets/custom_dialog.dart'; 4 | 5 | class FirstView extends StatelessWidget { 6 | final primaryColor = const Color(0xFF75A2EA); 7 | 8 | @override 9 | Widget build(BuildContext context) { 10 | final _width = MediaQuery.of(context).size.width; 11 | final _height = MediaQuery.of(context).size.height; 12 | 13 | return Scaffold( 14 | body: Container( 15 | width: _width, 16 | height: _height, 17 | color: primaryColor, 18 | child: SafeArea( 19 | child: Padding( 20 | padding: const EdgeInsets.all(16.0), 21 | child: Column( 22 | children: [ 23 | SizedBox(height: _height * 0.10), 24 | Text( 25 | "Welcome", 26 | style: TextStyle(fontSize: 44, color: Colors.white), 27 | ), 28 | SizedBox(height: _height * 0.10), 29 | AutoSizeText( 30 | "Let’s start planning your next trip", 31 | maxLines: 2, 32 | textAlign: TextAlign.center, 33 | style: TextStyle( 34 | fontSize: 40, 35 | color: Colors.white, 36 | ), 37 | ), 38 | SizedBox(height: _height * 0.15), 39 | RaisedButton( 40 | color: Colors.white, 41 | shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(30.0)), 42 | child: Padding( 43 | padding: const EdgeInsets.only(top: 10.0, bottom: 10.0, left: 30.0, right: 30.0), 44 | child: Text( 45 | "Get Started", 46 | style: TextStyle( 47 | color: primaryColor, 48 | fontSize: 28, 49 | fontWeight: FontWeight.w300, 50 | ), 51 | ), 52 | ), 53 | onPressed: () { 54 | showDialog( 55 | context: context, 56 | builder: (BuildContext context) => CustomDialog( 57 | title: "Would you like to create a free account?", 58 | description: 59 | "With an account, your data will be securely saved, allowing you to access it from multiple devices.", 60 | primaryButtonText: "Create My Account", 61 | primaryButtonRoute: "/signUp", 62 | secondaryButtonText: "Maybe Later", 63 | secondaryButtonRoute: "/anonymousSignIn", 64 | ), 65 | ); 66 | }, 67 | ), 68 | SizedBox(height: _height * 0.05), 69 | FlatButton( 70 | child: Text( 71 | "Sign In", 72 | style: TextStyle(color: Colors.white, fontSize: 25), 73 | ), 74 | onPressed: () { 75 | Navigator.of(context).pushReplacementNamed('/signIn'); 76 | }, 77 | ) 78 | ], 79 | ), 80 | ), 81 | ), 82 | ), 83 | ); 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /lib/models/Trip.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:cloud_firestore/cloud_firestore.dart'; 3 | import 'package:travel_budget/credentials.dart'; 4 | 5 | 6 | class Trip { 7 | String title; 8 | DateTime startDate; 9 | DateTime endDate; 10 | double budget; 11 | Map budgetTypes; 12 | String travelType; 13 | String photoReference; 14 | String notes; 15 | String documentId; 16 | double saved; 17 | List ledger; 18 | 19 | 20 | Trip( 21 | this.title, 22 | this.startDate, 23 | this.endDate, 24 | this.budget, 25 | this.budgetTypes, 26 | this.travelType 27 | ); 28 | 29 | // formatting for upload to Firbase when creating the trip 30 | Map toJson() => { 31 | 'title': title, 32 | 'startDate': startDate, 33 | 'endDate': endDate, 34 | 'budget': budget, 35 | 'budgetTypes': budgetTypes, 36 | 'travelType': travelType, 37 | 'photoReference': photoReference, 38 | }; 39 | 40 | // creating a Trip object from a firebase snapshot 41 | Trip.fromSnapshot(DocumentSnapshot snapshot) : 42 | title = snapshot.data()['title'], 43 | startDate = snapshot.data()['startDate'].toDate(), 44 | endDate = snapshot.data()['endDate'].toDate(), 45 | budget = snapshot.data()['budget'], 46 | budgetTypes = snapshot.data()['budgetTypes'], 47 | travelType = snapshot.data()['travelType'], 48 | photoReference = snapshot.data()['photoReference'], 49 | notes = snapshot.data()['notes'], 50 | documentId = snapshot.id, 51 | saved = snapshot.data()['saved'], 52 | ledger = snapshot.data()['ledger']; 53 | 54 | 55 | 56 | Map types({color = Colors.black}) => { 57 | "car": Icon(Icons.directions_car, size: 50, color: color), 58 | "bus": Icon(Icons.directions_bus, size: 50, color: color), 59 | "train": Icon(Icons.train, size: 50, color: color), 60 | "plane": Icon(Icons.airplanemode_active, size: 50, color: color), 61 | "ship": Icon(Icons.directions_boat, size: 50, color: color), 62 | "other": Icon(Icons.directions, size: 50, color: color), 63 | }; 64 | 65 | // return the google places image 66 | Image getLocationImage() { 67 | final baseUrl = "https://maps.googleapis.com/maps/api/place/photo"; 68 | final maxWidth = "1000"; 69 | final url = "$baseUrl?maxwidth=$maxWidth&photoreference=$photoReference&key=$PLACES_API_KEY"; 70 | return Image.network(url, fit: BoxFit.cover); 71 | } 72 | 73 | int getTotalTripDays() { 74 | int total = endDate.difference(startDate).inDays; 75 | if (total < 1) { 76 | total = 1; 77 | } 78 | return total; 79 | } 80 | 81 | int getDaysUntilTrip() { 82 | int diff = startDate.difference(DateTime.now()).inDays; 83 | if (diff < 0) { 84 | diff = 0; 85 | } 86 | return diff; 87 | } 88 | 89 | int getCurrentDailyBudget() { 90 | if (saved == 0 || saved == null) { 91 | return 0; 92 | } else { 93 | return (saved / getTotalTripDays()).floor(); 94 | } 95 | } 96 | 97 | Map ledgerItem(String amount, String type) { 98 | var amountDouble = double.parse(amount); 99 | if (type == "spent") { 100 | amountDouble = double.parse("-" + amount); 101 | } 102 | return { 103 | 'ledger': FieldValue.arrayUnion([ 104 | { 105 | "date": DateTime.now(), 106 | "amount": amountDouble, 107 | }, 108 | ]), 109 | 'saved': FieldValue.increment(amountDouble) 110 | }; 111 | } 112 | } 113 | 114 | -------------------------------------------------------------------------------- /lib/main.dart: -------------------------------------------------------------------------------- 1 | import 'package:cloud_firestore/cloud_firestore.dart'; 2 | import 'package:firebase_core/firebase_core.dart'; 3 | import 'package:flutter/material.dart'; 4 | import 'package:google_fonts/google_fonts.dart'; 5 | import 'package:travel_budget/services/custom_colors.dart'; 6 | import 'package:travel_budget/views/navigation_view.dart'; 7 | import 'package:travel_budget/views/first_view.dart'; 8 | import 'package:travel_budget/views/sign_up_view.dart'; 9 | import 'package:travel_budget/widgets/provider_widget.dart'; 10 | import 'package:travel_budget/services/auth_service.dart'; 11 | import 'package:firebase_admob/firebase_admob.dart'; 12 | import 'package:travel_budget/services/admob_service.dart'; 13 | import 'package:firebase_crashlytics/firebase_crashlytics.dart'; 14 | 15 | void main() async { 16 | WidgetsFlutterBinding.ensureInitialized(); 17 | await Firebase.initializeApp(); 18 | FirebaseAdMob.instance.initialize(appId: AdMobService().getAdMobAppId()); 19 | FlutterError.onError = FirebaseCrashlytics.instance.recordFlutterError; 20 | runApp(MyApp()); 21 | } 22 | 23 | class MyApp extends StatefulWidget { 24 | @override 25 | _MyAppState createState() => _MyAppState(); 26 | } 27 | 28 | class _MyAppState extends State with WidgetsBindingObserver { 29 | var colors = CustomColors(WidgetsBinding.instance.window.platformBrightness); 30 | 31 | @override 32 | void initState() { 33 | super.initState(); 34 | WidgetsBinding.instance.addObserver(this); 35 | } 36 | 37 | @override 38 | void dispose() { 39 | WidgetsBinding.instance.removeObserver(this); 40 | super.dispose(); 41 | } 42 | 43 | @override 44 | void didChangePlatformBrightness() { 45 | setState(() { 46 | colors = CustomColors(WidgetsBinding.instance.window.platformBrightness); 47 | }); 48 | } 49 | 50 | @override 51 | Widget build(BuildContext context) { 52 | return Provider( 53 | auth: AuthService(), 54 | db: FirebaseFirestore.instance, 55 | colors: colors, 56 | child: MaterialApp( 57 | debugShowCheckedModeBanner: false, 58 | title: "Travel Budget App", 59 | theme: ThemeData( 60 | brightness: Brightness.light, 61 | primarySwatch: Colors.blue, 62 | textTheme: TextTheme(bodyText2: GoogleFonts.quicksand(fontSize: 14.0))), 63 | darkTheme: ThemeData( 64 | brightness: Brightness.dark, 65 | primarySwatch: Colors.blue, 66 | textTheme: TextTheme(bodyText2: GoogleFonts.bitter(fontSize: 14.0))), 67 | home: HomeController(), 68 | routes: { 69 | '/home': (BuildContext context) => HomeController(), 70 | '/signUp': (BuildContext context) => SignUpView(authFormType: AuthFormType.signUp), 71 | '/signIn': (BuildContext context) => SignUpView(authFormType: AuthFormType.signIn), 72 | '/anonymousSignIn': (BuildContext context) => SignUpView(authFormType: AuthFormType.anonymous), 73 | '/convertUser': (BuildContext context) => SignUpView(authFormType: AuthFormType.convert), 74 | }, 75 | ), 76 | ); 77 | } 78 | } 79 | 80 | class HomeController extends StatelessWidget { 81 | @override 82 | Widget build(BuildContext context) { 83 | final AuthService auth = Provider.of(context).auth; 84 | return StreamBuilder( 85 | stream: auth.onAuthStateChanged, 86 | builder: (context, AsyncSnapshot snapshot) { 87 | if (snapshot.connectionState == ConnectionState.active) { 88 | final bool signedIn = snapshot.hasData; 89 | return signedIn ? NavigationView() : FirstView(); 90 | } 91 | return Container(); 92 | }, 93 | ); 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 31 | 32 | 33 | 34 | 40 | 41 | 42 | 43 | 44 | 45 | 56 | 58 | 64 | 65 | 66 | 67 | 68 | 69 | 75 | 77 | 83 | 84 | 85 | 86 | 88 | 89 | 92 | 93 | 94 | -------------------------------------------------------------------------------- /lib/views/edit_notes_view.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:travel_budget/models/Trip.dart'; 3 | import 'package:cloud_firestore/cloud_firestore.dart'; 4 | import 'package:travel_budget/widgets/provider_widget.dart'; 5 | import 'package:travel_budget/widgets/rounded_button.dart'; 6 | 7 | class EditNotesView extends StatefulWidget { 8 | final Trip trip; 9 | 10 | EditNotesView({Key key, @required this.trip}) : super(key: key); 11 | 12 | @override 13 | _EditNotesViewState createState() => _EditNotesViewState(); 14 | } 15 | 16 | class _EditNotesViewState extends State { 17 | TextEditingController _notesController = new TextEditingController(); 18 | 19 | final db = FirebaseFirestore.instance; 20 | 21 | @override 22 | void initState() { 23 | super.initState(); 24 | _notesController.text = widget.trip.notes; 25 | } 26 | 27 | @override 28 | Widget build(BuildContext context) { 29 | return Scaffold( 30 | body: Container( 31 | height: MediaQuery.of(context).size.height, 32 | width: MediaQuery.of(context).size.width, 33 | color: Colors.amberAccent, 34 | child: Hero( 35 | tag: "TripNotes-${widget.trip.title}", 36 | transitionOnUserGestures: true, 37 | child: SafeArea( 38 | child: SingleChildScrollView( 39 | child: Column( 40 | children: [ 41 | buildHeading(context), 42 | buildNotesText(), 43 | buildSubmitButton(context), 44 | ], 45 | ), 46 | ), 47 | ), 48 | ), 49 | ), 50 | ); 51 | } 52 | 53 | Widget buildHeading(context) { 54 | return Material( 55 | color: Colors.amberAccent, 56 | child: Padding( 57 | padding: const EdgeInsets.only(left: 20.0, top: 10.0), 58 | child: Row( 59 | children: [ 60 | Expanded( 61 | child: Text( 62 | "Trip Notes", 63 | style: TextStyle(fontSize: 24, color: Colors.black), 64 | ), 65 | ), 66 | FlatButton( 67 | child: Icon(Icons.close, color: Colors.black, size: 30), 68 | onPressed: () { 69 | Navigator.of(context).pop(); 70 | }, 71 | ) 72 | ], 73 | ), 74 | ), 75 | ); 76 | } 77 | 78 | Widget buildNotesText() { 79 | return Material( 80 | color: Colors.amberAccent, 81 | child: Padding( 82 | padding: const EdgeInsets.all(20.0), 83 | child: TextField( 84 | maxLines: null, 85 | controller: _notesController, 86 | decoration: InputDecoration( 87 | border: InputBorder.none, 88 | ), 89 | cursorColor: Colors.black, 90 | autofocus: true, 91 | style: TextStyle(color: Colors.black), 92 | ), 93 | ), 94 | ); 95 | } 96 | 97 | Widget buildSubmitButton(context) { 98 | return Material( 99 | color: Colors.amberAccent, 100 | child: RoundedButton( 101 | child: Text("Save", style: TextStyle(color: Colors.white)), 102 | color: Colors.indigo, 103 | onPressed: () async { 104 | widget.trip.notes = _notesController.text; 105 | 106 | final uid = Provider.of(context).auth.getCurrentUID(); 107 | 108 | await db.collection("userData") 109 | .doc(uid) 110 | .collection("trips") 111 | .doc(widget.trip.documentId) 112 | .update({'notes': _notesController.text}); 113 | 114 | Navigator.of(context).pop(); 115 | 116 | }, 117 | ), 118 | ); 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: travel_budget 2 | description: A new Flutter application. 3 | 4 | # The following defines the version and build number for your application. 5 | # A version number is three numbers separated by dots, like 1.2.43 6 | # followed by an optional build number separated by a +. 7 | # Both the version and the builder number may be overridden in flutter 8 | # build by specifying --build-name and --build-number, respectively. 9 | # In Android, build-name is used as versionName while build-number used as versionCode. 10 | # Read more about Android versioning at https://developer.android.com/studio/publish/versioning 11 | # In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion. 12 | # Read more about iOS versioning at 13 | # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html 14 | version: 1.0.0+1 15 | 16 | environment: 17 | sdk: ">=2.1.0 <3.0.0" 18 | 19 | dependencies: 20 | flutter: 21 | sdk: flutter 22 | 23 | firebase_core: ^0.5.3 24 | cloud_firestore: ^0.14.0 25 | firebase_auth: ^0.18.0 26 | google_sign_in: ^4.0.4 27 | apple_sign_in: ^0.1.0 28 | 29 | # The following adds the Cupertino Icons font to your application. 30 | # Use with the CupertinoIcons class for iOS style icons. 31 | cupertino_icons: ^0.1.2 32 | intl: 0.16.1 33 | date_range_picker: ^1.0.5 34 | auto_size_text: ^2.0.2 35 | flutter_spinkit: ^3.1.0 36 | flutter_auth_buttons: ^0.5.0 37 | dio: ^3.0.10 38 | uuid: 2.0.1 39 | device_info: ^0.4.1+4 40 | google_fonts: ^0.3.2 41 | firebase_admob: ^0.10.0-dev.1 42 | flare_flutter: ^2.0.3 43 | international_phone_input: ^1.0.4 44 | flutter_launcher_icons: "^0.7.3" 45 | firebase_crashlytics: "^0.2.4" 46 | 47 | 48 | # flutter pub run flutter_launcher_icons:main 49 | flutter_icons: 50 | image_path: "assets/icons/main_logo.png" 51 | adaptive_icon_foreground: "assets/icons/foreground_logo.png" 52 | adaptive_icon_background: "#57AEAF" 53 | android: true 54 | ios: true 55 | 56 | 57 | 58 | dev_dependencies: 59 | flutter_test: 60 | sdk: flutter 61 | 62 | 63 | # For information on the generic Dart part of this file, see the 64 | # following page: https://www.dartlang.org/tools/pub/pubspec 65 | 66 | # The following section is specific to Flutter. 67 | flutter: 68 | 69 | # The following line ensures that the Material Icons font is 70 | # included with your application, so that you can use the icons in 71 | # the material Icons class. 72 | uses-material-design: true 73 | 74 | # To add assets to your application, add an assets section, like this: 75 | assets: 76 | - assets/ 77 | # - images/a_dot_burr.jpeg 78 | # - images/a_dot_ham.jpeg 79 | 80 | # An image asset can refer to one or more resolution-specific "variants", see 81 | # https://flutter.dev/assets-and-images/#resolution-aware. 82 | 83 | # For details regarding adding assets from package dependencies, see 84 | # https://flutter.dev/assets-and-images/#from-packages 85 | 86 | # To add custom fonts to your application, add a fonts section here, 87 | # in this "flutter" section. Each entry in this list should have a 88 | # "family" key with the font family name, and a "fonts" key with a 89 | # list giving the asset and other descriptors for the font. For 90 | # example: 91 | # fonts: 92 | # - family: Schyler 93 | # fonts: 94 | # - asset: fonts/Schyler-Regular.ttf 95 | # - asset: fonts/Schyler-Italic.ttf 96 | # style: italic 97 | # - family: Trajan Pro 98 | # fonts: 99 | # - asset: fonts/TrajanPro.ttf 100 | # - asset: fonts/TrajanPro_Bold.ttf 101 | # weight: 700 102 | # 103 | # For details regarding fonts from package dependencies, 104 | # see https://flutter.dev/custom-fonts/#from-packages 105 | -------------------------------------------------------------------------------- /lib/generated/i18n.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | import 'package:flutter/foundation.dart'; 4 | import 'package:flutter/material.dart'; 5 | 6 | // ignore_for_file: non_constant_identifier_names 7 | // ignore_for_file: camel_case_types 8 | // ignore_for_file: prefer_single_quotes 9 | 10 | // This file is automatically generated. DO NOT EDIT, all your changes would be lost. 11 | class S implements WidgetsLocalizations { 12 | const S(); 13 | 14 | static S current; 15 | 16 | static const GeneratedLocalizationsDelegate delegate = 17 | GeneratedLocalizationsDelegate(); 18 | 19 | static S of(BuildContext context) => Localizations.of(context, S); 20 | 21 | @override 22 | TextDirection get textDirection => TextDirection.ltr; 23 | 24 | } 25 | 26 | class $en extends S { 27 | const $en(); 28 | } 29 | 30 | class GeneratedLocalizationsDelegate extends LocalizationsDelegate { 31 | const GeneratedLocalizationsDelegate(); 32 | 33 | List get supportedLocales { 34 | return const [ 35 | Locale("en", ""), 36 | ]; 37 | } 38 | 39 | LocaleListResolutionCallback listResolution({Locale fallback, bool withCountry = true}) { 40 | return (List locales, Iterable supported) { 41 | if (locales == null || locales.isEmpty) { 42 | return fallback ?? supported.first; 43 | } else { 44 | return _resolve(locales.first, fallback, supported, withCountry); 45 | } 46 | }; 47 | } 48 | 49 | LocaleResolutionCallback resolution({Locale fallback, bool withCountry = true}) { 50 | return (Locale locale, Iterable supported) { 51 | return _resolve(locale, fallback, supported, withCountry); 52 | }; 53 | } 54 | 55 | @override 56 | Future load(Locale locale) { 57 | final String lang = getLang(locale); 58 | if (lang != null) { 59 | switch (lang) { 60 | case "en": 61 | S.current = const $en(); 62 | return SynchronousFuture(S.current); 63 | default: 64 | // NO-OP. 65 | } 66 | } 67 | S.current = const S(); 68 | return SynchronousFuture(S.current); 69 | } 70 | 71 | @override 72 | bool isSupported(Locale locale) => _isSupported(locale, true); 73 | 74 | @override 75 | bool shouldReload(GeneratedLocalizationsDelegate old) => false; 76 | 77 | /// 78 | /// Internal method to resolve a locale from a list of locales. 79 | /// 80 | Locale _resolve(Locale locale, Locale fallback, Iterable supported, bool withCountry) { 81 | if (locale == null || !_isSupported(locale, withCountry)) { 82 | return fallback ?? supported.first; 83 | } 84 | 85 | final Locale languageLocale = Locale(locale.languageCode, ""); 86 | if (supported.contains(locale)) { 87 | return locale; 88 | } else if (supported.contains(languageLocale)) { 89 | return languageLocale; 90 | } else { 91 | final Locale fallbackLocale = fallback ?? supported.first; 92 | return fallbackLocale; 93 | } 94 | } 95 | 96 | /// 97 | /// Returns true if the specified locale is supported, false otherwise. 98 | /// 99 | bool _isSupported(Locale locale, bool withCountry) { 100 | if (locale != null) { 101 | for (Locale supportedLocale in supportedLocales) { 102 | // Language must always match both locales. 103 | if (supportedLocale.languageCode != locale.languageCode) { 104 | continue; 105 | } 106 | 107 | // If country code matches, return this locale. 108 | if (supportedLocale.countryCode == locale.countryCode) { 109 | return true; 110 | } 111 | 112 | // If no country requirement is requested, check if this locale has no country. 113 | if (true != withCountry && (supportedLocale.countryCode == null || supportedLocale.countryCode.isEmpty)) { 114 | return true; 115 | } 116 | } 117 | } 118 | return false; 119 | } 120 | } 121 | 122 | String getLang(Locale l) => l == null 123 | ? null 124 | : l.countryCode != null && l.countryCode.isEmpty 125 | ? l.languageCode 126 | : l.toString(); 127 | -------------------------------------------------------------------------------- /lib/widgets/custom_dialog.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:auto_size_text/auto_size_text.dart'; 3 | 4 | class CustomDialog extends StatelessWidget { 5 | final primaryColor = const Color(0xFF75A2EA); 6 | final grayColor = const Color(0xFF939393); 7 | 8 | final String title, 9 | description, 10 | primaryButtonText, 11 | primaryButtonRoute, 12 | secondaryButtonText, 13 | secondaryButtonRoute; 14 | 15 | CustomDialog( 16 | {@required this.title, 17 | @required this.description, 18 | @required this.primaryButtonText, 19 | @required this.primaryButtonRoute, 20 | this.secondaryButtonText, 21 | this.secondaryButtonRoute}); 22 | 23 | static const double padding = 20.0; 24 | 25 | @override 26 | Widget build(BuildContext context) { 27 | return Dialog( 28 | shape: RoundedRectangleBorder( 29 | borderRadius: BorderRadius.circular(padding), 30 | ), 31 | child: Stack( 32 | children: [ 33 | Container( 34 | padding: EdgeInsets.all(padding), 35 | decoration: BoxDecoration( 36 | color: Colors.white, 37 | shape: BoxShape.rectangle, 38 | borderRadius: BorderRadius.circular(padding), 39 | boxShadow: [ 40 | BoxShadow( 41 | color: Colors.black, 42 | blurRadius: 10.0, 43 | offset: const Offset(0.0, 10.0), 44 | ), 45 | ]), 46 | child: Column( 47 | mainAxisSize: MainAxisSize.min, 48 | children: [ 49 | SizedBox(height: 24.0), 50 | AutoSizeText( 51 | title, 52 | maxLines: 2, 53 | textAlign: TextAlign.center, 54 | style: TextStyle( 55 | color: primaryColor, 56 | fontSize: 25.0, 57 | ), 58 | ), 59 | SizedBox(height: 24.0), 60 | AutoSizeText( 61 | description, 62 | maxLines: 4, 63 | textAlign: TextAlign.center, 64 | style: TextStyle( 65 | color: grayColor, 66 | fontSize: 18.0, 67 | ), 68 | ), 69 | SizedBox(height: 24.0), 70 | RaisedButton( 71 | color: primaryColor, 72 | shape: RoundedRectangleBorder( 73 | borderRadius: BorderRadius.circular(30.0)), 74 | child: Padding( 75 | padding: const EdgeInsets.fromLTRB(20, 10, 20, 10), 76 | child: AutoSizeText( 77 | primaryButtonText, 78 | maxLines: 1, 79 | style: TextStyle( 80 | fontSize: 18, 81 | fontWeight: FontWeight.w200, 82 | color: Colors.white, 83 | ), 84 | ), 85 | ), 86 | onPressed: () { 87 | Navigator.of(context).pop(); 88 | Navigator.of(context) 89 | .pushReplacementNamed(primaryButtonRoute); 90 | }, 91 | ), 92 | SizedBox(height: 10.0), 93 | showSecondaryButton(context), 94 | ], 95 | ), 96 | ) 97 | ], 98 | ), 99 | ); 100 | } 101 | 102 | showSecondaryButton(BuildContext context) { 103 | if (secondaryButtonRoute != null && secondaryButtonText != null ){ 104 | return FlatButton( 105 | child: AutoSizeText( 106 | secondaryButtonText, 107 | maxLines: 1, 108 | style: TextStyle( 109 | fontSize: 18, 110 | color: primaryColor, 111 | fontWeight: FontWeight.w400, 112 | ), 113 | ), 114 | onPressed: () { 115 | Navigator.of(context).pop(); 116 | Navigator.of(context).pushReplacementNamed(secondaryButtonRoute); 117 | }, 118 | ); 119 | } else { 120 | return SizedBox(height: 10.0); 121 | } 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /lib/views/deposit_view.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter/services.dart'; 3 | import 'package:travel_budget/models/Trip.dart'; 4 | import 'package:travel_budget/services/firebase_service.dart'; 5 | import 'package:travel_budget/views/navigation_view.dart'; 6 | import 'package:travel_budget/widgets/provider_widget.dart'; 7 | import 'package:travel_budget/widgets/rounded_button.dart'; 8 | 9 | class DepositView extends StatefulWidget { 10 | final Trip trip; 11 | 12 | DepositView({ 13 | @required this.trip, 14 | }); 15 | 16 | @override 17 | _DepositViewState createState() => _DepositViewState(); 18 | } 19 | 20 | class _DepositViewState extends State { 21 | String _amount = "0"; 22 | String _error; 23 | 24 | @override 25 | Widget build(BuildContext context) { 26 | return Container( 27 | color: Colors.indigo, 28 | width: MediaQuery.of(context).size.width, 29 | height: MediaQuery.of(context).size.height, 30 | child: SafeArea( 31 | child: Column( 32 | children: [ 33 | Spacer(), 34 | FittedBox( 35 | fit: BoxFit.fitWidth, 36 | child: Text( 37 | "\$$_amount", 38 | style: TextStyle(fontSize: 100, fontWeight: FontWeight.bold, color: Colors.white), 39 | ), 40 | ), 41 | Padding( 42 | padding: const EdgeInsets.only(top: 15.0), 43 | child: Text("${_error ?? ''}", style: TextStyle(color: Colors.white)), 44 | ), 45 | Container( 46 | child: Padding( 47 | padding: const EdgeInsets.symmetric(horizontal: 25.0), 48 | child: GridView.count( 49 | crossAxisCount: 3, 50 | shrinkWrap: true, 51 | physics: NeverScrollableScrollPhysics(), 52 | childAspectRatio: 1.4, 53 | children: setKeyboard(), 54 | ), 55 | ), 56 | ), 57 | Spacer(), 58 | Padding( 59 | padding: const EdgeInsets.symmetric(horizontal: 15.0), 60 | child: Row( 61 | mainAxisAlignment: MainAxisAlignment.spaceEvenly, 62 | children: [ 63 | _actionBtn("spent"), 64 | _actionBtn("saved"), 65 | ], 66 | ), 67 | ), 68 | Spacer() 69 | ], 70 | ), 71 | ), 72 | ); 73 | } 74 | 75 | Widget _numberBtn(String number) { 76 | return FlatButton( 77 | child: Text( 78 | "$number", 79 | style: TextStyle(fontSize: 40, color: Colors.white), 80 | ), 81 | onPressed: () { 82 | setState(() { 83 | if (_amount == "0") { 84 | _amount = "$number"; 85 | } else if (_amount.length == 5) { 86 | _amount = _amount; 87 | HapticFeedback.heavyImpact(); 88 | } else { 89 | _amount += "$number"; 90 | } 91 | }); 92 | }, 93 | ); 94 | } 95 | 96 | Widget _deleteBtn() { 97 | return FlatButton( 98 | child: Text( 99 | "<", 100 | style: TextStyle(fontSize: 40, color: Colors.white), 101 | ), 102 | onPressed: () { 103 | setState(() { 104 | if (_amount.length <= 1) { 105 | _amount = "0"; 106 | } else { 107 | _amount = _amount.substring(0, _amount.length - 1); 108 | } 109 | }); 110 | }, 111 | ); 112 | } 113 | 114 | Widget _actionBtn(String type) { 115 | return Expanded( 116 | child: Padding( 117 | padding: const EdgeInsets.symmetric(vertical: 5.0, horizontal: 10.0), 118 | child: RoundedButton( 119 | color: Colors.indigoAccent, 120 | child: 121 | Text("${type[0].toUpperCase()}${type.substring(1)}", style: TextStyle(color: Colors.white, fontSize: 20)), 122 | onPressed: () async { 123 | if (_amount == "0") { 124 | setState(() { 125 | _error = "Enter an amount"; 126 | }); 127 | } else if (type == "spent" && double.parse(_amount) > widget.trip.saved) { 128 | setState(() { 129 | _error = "Yove've only saved \$${widget.trip.saved.floor()}"; 130 | }); 131 | } else { 132 | FirebaseService.addToLedger(context, widget.trip.documentId, widget.trip.ledgerItem(_amount, type)); 133 | Navigator.pushReplacement( 134 | context, 135 | PageRouteBuilder( 136 | pageBuilder: (_, __, ___) => NavigationView(), 137 | transitionDuration: Duration(seconds: 0), 138 | ), 139 | ); 140 | } 141 | }, 142 | ), 143 | ), 144 | ); 145 | } 146 | 147 | setKeyboard() { 148 | List keyboard = []; 149 | 150 | // numbers 1-9 151 | List.generate(9, (index) { 152 | keyboard.add(_numberBtn("${index + 1}")); 153 | }); 154 | 155 | keyboard.add(Text("")); 156 | keyboard.add(_numberBtn("0")); 157 | keyboard.add(_deleteBtn()); 158 | 159 | return keyboard; 160 | } 161 | } 162 | -------------------------------------------------------------------------------- /.flutter-plugins-dependencies: -------------------------------------------------------------------------------- 1 | {"info":"This is a generated file; do not edit or check into version control.","plugins":{"ios":[{"name":"apple_sign_in","path":"/Users/davefaliskie/flutter/.pub-cache/hosted/pub.dartlang.org/apple_sign_in-0.1.0/","dependencies":[]},{"name":"cloud_firestore","path":"/Users/davefaliskie/flutter/.pub-cache/hosted/pub.dartlang.org/cloud_firestore-0.14.4/","dependencies":["firebase_core"]},{"name":"device_info","path":"/Users/davefaliskie/flutter/.pub-cache/hosted/pub.dartlang.org/device_info-0.4.2+7/","dependencies":[]},{"name":"firebase_admob","path":"/Users/davefaliskie/flutter/.pub-cache/hosted/pub.dartlang.org/firebase_admob-0.10.0-dev.1/","dependencies":["firebase_core"]},{"name":"firebase_auth","path":"/Users/davefaliskie/flutter/.pub-cache/hosted/pub.dartlang.org/firebase_auth-0.18.4+1/","dependencies":["firebase_core"]},{"name":"firebase_core","path":"/Users/davefaliskie/flutter/.pub-cache/hosted/pub.dartlang.org/firebase_core-0.5.3/","dependencies":[]},{"name":"firebase_crashlytics","path":"/Users/davefaliskie/flutter/.pub-cache/hosted/pub.dartlang.org/firebase_crashlytics-0.2.4/","dependencies":["firebase_core"]},{"name":"google_sign_in","path":"/Users/davefaliskie/flutter/.pub-cache/hosted/pub.dartlang.org/google_sign_in-4.5.3/","dependencies":[]},{"name":"libphonenumber","path":"/Users/davefaliskie/flutter/.pub-cache/hosted/pub.dartlang.org/libphonenumber-1.0.1/","dependencies":[]},{"name":"path_provider","path":"/Users/davefaliskie/flutter/.pub-cache/hosted/pub.dartlang.org/path_provider-1.6.14/","dependencies":[]}],"android":[{"name":"apple_sign_in","path":"/Users/davefaliskie/flutter/.pub-cache/hosted/pub.dartlang.org/apple_sign_in-0.1.0/","dependencies":[]},{"name":"cloud_firestore","path":"/Users/davefaliskie/flutter/.pub-cache/hosted/pub.dartlang.org/cloud_firestore-0.14.4/","dependencies":["firebase_core"]},{"name":"device_info","path":"/Users/davefaliskie/flutter/.pub-cache/hosted/pub.dartlang.org/device_info-0.4.2+7/","dependencies":[]},{"name":"firebase_admob","path":"/Users/davefaliskie/flutter/.pub-cache/hosted/pub.dartlang.org/firebase_admob-0.10.0-dev.1/","dependencies":["firebase_core"]},{"name":"firebase_auth","path":"/Users/davefaliskie/flutter/.pub-cache/hosted/pub.dartlang.org/firebase_auth-0.18.4+1/","dependencies":["firebase_core"]},{"name":"firebase_core","path":"/Users/davefaliskie/flutter/.pub-cache/hosted/pub.dartlang.org/firebase_core-0.5.3/","dependencies":[]},{"name":"firebase_crashlytics","path":"/Users/davefaliskie/flutter/.pub-cache/hosted/pub.dartlang.org/firebase_crashlytics-0.2.4/","dependencies":["firebase_core"]},{"name":"google_sign_in","path":"/Users/davefaliskie/flutter/.pub-cache/hosted/pub.dartlang.org/google_sign_in-4.5.3/","dependencies":[]},{"name":"libphonenumber","path":"/Users/davefaliskie/flutter/.pub-cache/hosted/pub.dartlang.org/libphonenumber-1.0.1/","dependencies":[]},{"name":"path_provider","path":"/Users/davefaliskie/flutter/.pub-cache/hosted/pub.dartlang.org/path_provider-1.6.14/","dependencies":[]}],"macos":[{"name":"cloud_firestore","path":"/Users/davefaliskie/flutter/.pub-cache/hosted/pub.dartlang.org/cloud_firestore-0.14.4/","dependencies":["firebase_core"]},{"name":"firebase_auth","path":"/Users/davefaliskie/flutter/.pub-cache/hosted/pub.dartlang.org/firebase_auth-0.18.4+1/","dependencies":["firebase_core"]},{"name":"firebase_core","path":"/Users/davefaliskie/flutter/.pub-cache/hosted/pub.dartlang.org/firebase_core-0.5.3/","dependencies":[]},{"name":"firebase_crashlytics","path":"/Users/davefaliskie/flutter/.pub-cache/hosted/pub.dartlang.org/firebase_crashlytics-0.2.4/","dependencies":["firebase_core"]},{"name":"path_provider_macos","path":"/Users/davefaliskie/flutter/.pub-cache/hosted/pub.dartlang.org/path_provider_macos-0.0.4+3/","dependencies":[]}],"linux":[{"name":"path_provider_linux","path":"/Users/davefaliskie/flutter/.pub-cache/hosted/pub.dartlang.org/path_provider_linux-0.0.1+2/","dependencies":[]}],"windows":[],"web":[{"name":"cloud_firestore_web","path":"/Users/davefaliskie/flutter/.pub-cache/hosted/pub.dartlang.org/cloud_firestore_web-0.2.1+2/","dependencies":["firebase_core_web"]},{"name":"firebase_auth_web","path":"/Users/davefaliskie/flutter/.pub-cache/hosted/pub.dartlang.org/firebase_auth_web-0.3.2+3/","dependencies":["firebase_core_web"]},{"name":"firebase_core_web","path":"/Users/davefaliskie/flutter/.pub-cache/hosted/pub.dartlang.org/firebase_core_web-0.2.1+1/","dependencies":[]},{"name":"google_sign_in_web","path":"/Users/davefaliskie/flutter/.pub-cache/hosted/pub.dartlang.org/google_sign_in_web-0.9.1+1/","dependencies":[]}]},"dependencyGraph":[{"name":"apple_sign_in","dependencies":[]},{"name":"cloud_firestore","dependencies":["firebase_core","cloud_firestore_web"]},{"name":"cloud_firestore_web","dependencies":["firebase_core","firebase_core_web"]},{"name":"device_info","dependencies":[]},{"name":"firebase_admob","dependencies":["firebase_core"]},{"name":"firebase_auth","dependencies":["firebase_core","firebase_auth_web"]},{"name":"firebase_auth_web","dependencies":["firebase_core","firebase_core_web"]},{"name":"firebase_core","dependencies":["firebase_core_web"]},{"name":"firebase_core_web","dependencies":[]},{"name":"firebase_crashlytics","dependencies":["firebase_core"]},{"name":"google_sign_in","dependencies":["google_sign_in_web"]},{"name":"google_sign_in_web","dependencies":[]},{"name":"libphonenumber","dependencies":[]},{"name":"path_provider","dependencies":["path_provider_macos","path_provider_linux"]},{"name":"path_provider_linux","dependencies":[]},{"name":"path_provider_macos","dependencies":[]}],"date_created":"2021-03-07 14:57:33.645272","version":"1.20.3"} -------------------------------------------------------------------------------- /lib/views/new_trips/budget_view.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:travel_budget/models/Trip.dart'; 3 | import 'package:travel_budget/widgets/divider_with_text_widget.dart'; 4 | import 'package:travel_budget/widgets/money_text_field.dart'; 5 | import 'summary_view.dart'; 6 | 7 | enum budgetType { simple, complex } 8 | 9 | class NewTripBudgetView extends StatefulWidget { 10 | final Trip trip; 11 | 12 | NewTripBudgetView({Key key, @required this.trip}) : super(key: key); 13 | 14 | @override 15 | _NewTripBudgetViewState createState() => _NewTripBudgetViewState(); 16 | } 17 | 18 | class _NewTripBudgetViewState extends State { 19 | var _budgetState = budgetType.simple; 20 | var _switchButtonText = "Build Budget"; 21 | var _budgetTotal = 0; 22 | 23 | TextEditingController _budgetController = new TextEditingController(); 24 | TextEditingController _transportationController = new TextEditingController(); 25 | TextEditingController _foodController = new TextEditingController(); 26 | TextEditingController _lodgingController = new TextEditingController(); 27 | TextEditingController _entertainmentController = new TextEditingController(); 28 | 29 | @override 30 | void initState() { 31 | super.initState(); 32 | _budgetController.addListener(_setBudgetTotal); 33 | _transportationController.addListener(_setTotalBudget); 34 | _foodController.addListener(_setTotalBudget); 35 | _lodgingController.addListener(_setTotalBudget); 36 | _entertainmentController.addListener(_setTotalBudget); 37 | } 38 | 39 | 40 | _setTotalBudget() { 41 | var total = 0; 42 | total = (_transportationController.text == "") ? 0 : int.parse(_transportationController.text); 43 | total += (_foodController.text == "") ? 0 : int.parse(_foodController.text); 44 | total += (_lodgingController.text== "") ? 0 : int.parse(_lodgingController.text); 45 | total += (_entertainmentController.text == "") ? 0 : int.parse(_entertainmentController.text); 46 | setState(() { 47 | _budgetTotal = total; 48 | }); 49 | } 50 | 51 | _setBudgetTotal() { 52 | setState(() { 53 | _budgetTotal = (_budgetController.text == "") ? 0 : int.parse(_budgetController.text); 54 | }); 55 | } 56 | 57 | 58 | List setBudgetFields(_budgetController) { 59 | List fields = []; 60 | 61 | if (_budgetState == budgetType.simple) { 62 | _switchButtonText = "Build Budget"; 63 | fields.add(Padding( 64 | padding: const EdgeInsets.all(12.0), 65 | child: Text("Enter a Trip Budget"), 66 | )); 67 | fields.add(MoneyTextField(controller: _budgetController, helperText: "Daily estimated budget")); 68 | } else { 69 | // assumes complex budget 70 | _switchButtonText = "Simple Budget"; 71 | fields.add(Padding( 72 | padding: const EdgeInsets.all(12.0), 73 | child: Text("Enter How much you want to spend in each area"), 74 | )); 75 | fields.add(MoneyTextField(controller: _transportationController, helperText: "Daily Estimated Transportation Budget")); 76 | fields.add(MoneyTextField(controller: _foodController, helperText: "Daily Estimated Food Budget")); 77 | fields.add(MoneyTextField(controller: _lodgingController, helperText: "Daily Estimated Lodging Budget")); 78 | fields.add(MoneyTextField(controller: _entertainmentController, helperText: "Daily Estimated Entertainment Budget")); 79 | fields.add(Text("Total: \$$_budgetTotal")); 80 | } 81 | 82 | fields.add(FlatButton( 83 | child: Text( 84 | "Continue", 85 | style: TextStyle(fontSize: 25, color: Colors.blue), 86 | ), 87 | onPressed: () async { 88 | widget.trip.budget = _budgetTotal.toDouble(); 89 | widget.trip.budgetTypes = { 90 | 'transportation': (_transportationController.text == "") ? 0.0 : double.parse(_transportationController.text), 91 | 'food': (_foodController.text == "") ? 0.0 : double.parse(_foodController.text), 92 | 'lodging': (_lodgingController.text== "") ? 0.0 : double.parse(_lodgingController.text), 93 | 'entertainment': (_entertainmentController.text == "") ? 0.0 : double.parse(_entertainmentController.text), 94 | }; 95 | 96 | Navigator.push( 97 | context, 98 | MaterialPageRoute( 99 | builder: (context) => NewTripSummaryView(trip: widget.trip)), 100 | ); 101 | }, 102 | )); 103 | fields.add(DividerWithText(dividerText: "or")); 104 | fields.add(FlatButton( 105 | child: Text( 106 | _switchButtonText, 107 | style: TextStyle(fontSize: 25, color: Colors.blue), 108 | ), 109 | onPressed: () { 110 | setState(() { 111 | _budgetState = (_budgetState == budgetType.simple) 112 | ? budgetType.complex 113 | : budgetType.simple; 114 | }); 115 | }, 116 | )); 117 | 118 | return fields; 119 | } 120 | 121 | @override 122 | Widget build(BuildContext context) { 123 | _budgetController.text = (_budgetController.text == "") ? "" : _budgetTotal.toString(); 124 | _budgetController.selection = TextSelection.collapsed(offset: _budgetController.text.length); 125 | 126 | return Scaffold( 127 | appBar: AppBar( 128 | title: Text('Create Trip - Budget'), 129 | ), 130 | body: SingleChildScrollView( 131 | child: Center( 132 | child: Column( 133 | mainAxisAlignment: MainAxisAlignment.center, 134 | children: setBudgetFields(_budgetController), 135 | ), 136 | ), 137 | ), 138 | ); 139 | } 140 | } 141 | -------------------------------------------------------------------------------- /lib/widgets/calculator_widget.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:travel_budget/widgets/money_text_field.dart'; 3 | import 'package:travel_budget/models/Trip.dart'; 4 | import 'package:travel_budget/widgets/provider_widget.dart'; 5 | import 'package:cloud_firestore/cloud_firestore.dart'; 6 | 7 | class CalculatorWidget extends StatefulWidget { 8 | final Trip trip; 9 | 10 | CalculatorWidget({ 11 | @required this.trip, 12 | }); 13 | 14 | @override 15 | _CalculatorWidgetState createState() => _CalculatorWidgetState(); 16 | } 17 | 18 | class _CalculatorWidgetState extends State { 19 | TextEditingController _moneyController = TextEditingController(); 20 | int _saved; 21 | int _needed; 22 | 23 | @override 24 | void initState() { 25 | super.initState(); 26 | _saved = (widget.trip.saved ?? 0.0).floor(); 27 | _needed = (widget.trip.budget.floor() * widget.trip.getTotalTripDays()) - _saved; 28 | } 29 | 30 | @override 31 | Widget build(BuildContext context) { 32 | return Card( 33 | child: Column( 34 | children: [ 35 | Container( 36 | color: Colors.cyan, 37 | child: Padding( 38 | padding: const EdgeInsets.only(top: 12.0, bottom: 12.0), 39 | child: Row( 40 | mainAxisAlignment: MainAxisAlignment.spaceEvenly, 41 | children: [ 42 | Column( 43 | children: [ 44 | Text("\$$_saved", style: TextStyle(fontSize: 60)), 45 | Text("Saved", style: TextStyle(fontSize: 20)), 46 | ], 47 | ), 48 | Container( 49 | height: 80, 50 | child: VerticalDivider( 51 | color: Colors.white, 52 | thickness: 5, 53 | ), 54 | ), 55 | Column( 56 | children: [ 57 | Text("\$$_needed", style: TextStyle(fontSize: 60)), 58 | Text("Needed", style: TextStyle(fontSize: 20)), 59 | ], 60 | ), 61 | ], 62 | ), 63 | ), 64 | ), 65 | Container( 66 | color: Colors.orangeAccent, 67 | child: Padding( 68 | padding: const EdgeInsets.only(left: 20.0, right: 40.0), 69 | child: Row( 70 | children: [ 71 | Expanded( 72 | child: MoneyTextField( 73 | controller: _moneyController, 74 | helperText: "Save Additional", 75 | ), 76 | ), 77 | IconButton( 78 | icon: Icon(Icons.add_circle), 79 | color: Colors.green, 80 | iconSize: 50, 81 | onPressed: () async { 82 | setState(() { 83 | _saved = _saved + int.parse(_moneyController.text); 84 | _needed = _needed - int.parse(_moneyController.text); 85 | }); 86 | final uid = await Provider.of(context).auth.getCurrentUID(); 87 | await FirebaseFirestore.instance.collection('userData') 88 | .doc(uid) 89 | .collection('trips') 90 | .doc(widget.trip.documentId) 91 | .update({'saved': _saved.toDouble()}); 92 | }, 93 | ), 94 | IconButton( 95 | icon: Icon(Icons.remove_circle), 96 | color: Colors.red, 97 | iconSize: 50, 98 | onPressed: () async { 99 | setState(() { 100 | _saved = _saved - int.parse(_moneyController.text); 101 | _needed = _needed + int.parse(_moneyController.text); 102 | }); 103 | final uid = await Provider.of(context).auth.getCurrentUID(); 104 | await FirebaseFirestore.instance.collection('userData') 105 | .doc(uid) 106 | .collection('trips') 107 | .doc(widget.trip.documentId) 108 | .update({'saved': _saved.toDouble()}); 109 | }, 110 | ) 111 | ], 112 | ), 113 | ), 114 | ), 115 | Container( 116 | color: Colors.orangeAccent, 117 | child: Padding( 118 | padding: const EdgeInsets.only(bottom: 20.0), 119 | child: Row( 120 | mainAxisAlignment: MainAxisAlignment.spaceEvenly, 121 | children: [ 122 | generateAddMoneyBtn(25), 123 | generateAddMoneyBtn(50), 124 | generateAddMoneyBtn(100), 125 | ], 126 | ), 127 | ), 128 | ) 129 | ], 130 | ), 131 | ); 132 | } 133 | 134 | 135 | RaisedButton generateAddMoneyBtn(int amount) { 136 | return RaisedButton( 137 | child: Text("\$$amount"), 138 | color: Colors.white, 139 | textColor: Colors.deepOrange, 140 | onPressed: () async { 141 | setState(() { 142 | _saved = _saved + amount; 143 | _needed = _needed - amount; 144 | }); 145 | final uid = await Provider.of(context).auth.getCurrentUID(); 146 | await FirebaseFirestore.instance.collection('userData') 147 | .doc(uid) 148 | .collection('trips') 149 | .doc(widget.trip.documentId) 150 | .update({'saved': _saved.toDouble()}); 151 | }, 152 | ); 153 | } 154 | } 155 | -------------------------------------------------------------------------------- /lib/views/new_trips/location_view.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:travel_budget/models/Trip.dart'; 3 | import 'package:travel_budget/models/Place.dart'; 4 | import 'package:travel_budget/widgets/divider_with_text_widget.dart'; 5 | import 'date_view.dart'; 6 | import 'package:travel_budget/credentials.dart'; 7 | import 'package:dio/dio.dart'; 8 | import 'package:auto_size_text/auto_size_text.dart'; 9 | import 'dart:async'; 10 | import 'package:uuid/uuid.dart'; 11 | 12 | 13 | class NewTripLocationView extends StatefulWidget { 14 | final Trip trip; 15 | 16 | NewTripLocationView({Key key, @required this.trip}) : super(key: key); 17 | 18 | @override 19 | _NewTripLocationViewState createState() => _NewTripLocationViewState(); 20 | } 21 | 22 | class _NewTripLocationViewState extends State { 23 | TextEditingController _searchController = new TextEditingController(); 24 | // Timer _throttle; 25 | var uuid = new Uuid(); 26 | String _sessionToken; 27 | 28 | String _heading; 29 | List _placesList; 30 | final List _suggestedList = [ 31 | // Place("New York", 320.00), 32 | // Place("Austin", 250.00), 33 | // Place("Boston", 290.00), 34 | // Place("Florence", 300.00), 35 | // Place("Washington D.C.", 190.00), 36 | ]; 37 | 38 | @override 39 | void initState() { 40 | super.initState(); 41 | _heading = "Suggestions"; 42 | _placesList = _suggestedList; 43 | _searchController.addListener(_onSearchChanged); 44 | } 45 | 46 | @override 47 | void dispose() { 48 | _searchController.removeListener(_onSearchChanged); 49 | _searchController.dispose(); 50 | super.dispose(); 51 | } 52 | 53 | _onSearchChanged() { 54 | if(_sessionToken == null) { 55 | setState(() { 56 | _sessionToken = uuid.v4(); 57 | }); 58 | } 59 | getLocationResults(_searchController.text); 60 | } 61 | 62 | void getLocationResults(String input) async { 63 | if (input.isEmpty) { 64 | setState(() { 65 | _heading = "Suggestions"; 66 | }); 67 | return; 68 | } 69 | 70 | String baseURL = 'https://maps.googleapis.com/maps/api/place/autocomplete/json'; 71 | String type = '(regions)'; 72 | String request = '$baseURL?input=$input&key=$PLACES_API_KEY&type=$type&sessiontoken=$_sessionToken'; 73 | Response response = await Dio().get(request); 74 | 75 | final predictions = response.data['predictions']; 76 | 77 | List _displayResults = []; 78 | 79 | for (var i=0; i < predictions.length; i++) { 80 | String name = predictions[i]['description']; 81 | String placeId = predictions[i]['place_id']; 82 | 83 | // TODO figure out the budget 84 | double averageBudget = 200.0; 85 | _displayResults.add(Place(name, averageBudget, placeId)); 86 | } 87 | 88 | setState(() { 89 | _heading = "Results"; 90 | _placesList = _displayResults; 91 | }); 92 | } 93 | 94 | Future getLocationPhotoRef(placeId) async { 95 | String placeImgRequest = 'https://maps.googleapis.com/maps/api/place/details/json?place_id=$placeId&fields=photo,geometry&key=$PLACES_API_KEY&sessiontoken=$_sessionToken'; 96 | Response placeDetails = await Dio().get(placeImgRequest); 97 | return placeDetails.data["result"]["photos"][0]["photo_reference"]; 98 | } 99 | 100 | @override 101 | Widget build(BuildContext context) { 102 | return Scaffold( 103 | appBar: AppBar( 104 | title: Text('Create Trip - Location'), 105 | ), 106 | body: Center( 107 | child: Column( 108 | children: [ 109 | Padding( 110 | padding: const EdgeInsets.all(30.0), 111 | child: TextField( 112 | controller: _searchController, 113 | decoration: InputDecoration( 114 | prefixIcon: Icon(Icons.search), 115 | ), 116 | ), 117 | ), 118 | Padding( 119 | padding: const EdgeInsets.only(left: 10.0, right: 10.0), 120 | child: new DividerWithText( 121 | dividerText: _heading, 122 | ), 123 | ), 124 | Expanded( 125 | child: ListView.builder( 126 | itemCount: _placesList.length, 127 | itemBuilder: (BuildContext context, int index) => 128 | buildPlaceCard(context, index), 129 | ), 130 | ), 131 | ], 132 | ), 133 | ), 134 | ); 135 | } 136 | 137 | Widget buildPlaceCard(BuildContext context, int index) { 138 | return Hero( 139 | tag: "SelectedTrip-${_placesList[index].name}", 140 | transitionOnUserGestures: true, 141 | child: Container( 142 | child: Padding( 143 | padding: const EdgeInsets.only( 144 | left: 8.0, 145 | right: 8.0, 146 | ), 147 | child: Card( 148 | child: InkWell( 149 | child: Row( 150 | children: [ 151 | Expanded( 152 | child: Padding( 153 | padding: const EdgeInsets.all(16.0), 154 | child: Column( 155 | children: [ 156 | Row( 157 | children: [ 158 | Flexible( 159 | child: AutoSizeText(_placesList[index].name, 160 | maxLines: 3, 161 | style: TextStyle(fontSize: 25.0)), 162 | ), 163 | ], 164 | ), 165 | Row( 166 | children: [ 167 | Text( 168 | "Average Budget \$${_placesList[index].averageBudget.toStringAsFixed(2)}"), 169 | ], 170 | ), 171 | ], 172 | ), 173 | ), 174 | ), 175 | 176 | ], 177 | ), 178 | onTap: () async { 179 | String photoReference = await getLocationPhotoRef(_placesList[index].placeId); 180 | widget.trip.title = _placesList[index].name; 181 | widget.trip.photoReference = photoReference; 182 | setState(() { 183 | _sessionToken = null; 184 | }); 185 | // TODO maybe pass the trip average budget through here too... 186 | // that would need to be added to the Trip object 187 | Navigator.push( 188 | context, 189 | MaterialPageRoute( 190 | builder: (context) => NewTripDateView(trip: widget.trip)), 191 | ); 192 | }, 193 | ), 194 | ), 195 | ), 196 | ), 197 | ); 198 | } 199 | } 200 | -------------------------------------------------------------------------------- /lib/views/new_trips/date_view.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:travel_budget/models/Trip.dart'; 3 | import 'package:date_range_picker/date_range_picker.dart' as DateRagePicker; 4 | import 'package:intl/intl.dart'; 5 | import 'package:auto_size_text/auto_size_text.dart'; 6 | import 'dart:async'; 7 | import 'budget_view.dart'; 8 | 9 | class NewTripDateView extends StatefulWidget { 10 | final Trip trip; 11 | 12 | NewTripDateView({Key key, @required this.trip}) : super(key: key); 13 | 14 | @override 15 | _NewTripDateViewState createState() => _NewTripDateViewState(); 16 | } 17 | 18 | class _NewTripDateViewState extends State { 19 | DateTime _startDate = DateTime.now(); 20 | DateTime _endDate = DateTime.now().add(Duration(days: 7)); 21 | 22 | Future displayDateRangePicker(BuildContext context) async { 23 | final List picked = await DateRagePicker.showDatePicker( 24 | context: context, 25 | initialFirstDate: _startDate, 26 | initialLastDate: _endDate, 27 | firstDate: new DateTime(DateTime.now().year - 50), 28 | lastDate: new DateTime(DateTime.now().year + 50)); 29 | if (picked != null && picked.length == 2) { 30 | setState(() { 31 | _startDate = picked[0]; 32 | _endDate = picked[1]; 33 | }); 34 | } 35 | } 36 | 37 | @override 38 | Widget build(BuildContext context) { 39 | return Scaffold( 40 | body: Center( 41 | child: CustomScrollView( 42 | slivers: [ 43 | SliverAppBar( 44 | title: Text(''), 45 | backgroundColor: Colors.green, 46 | expandedHeight: 350.0, 47 | flexibleSpace: FlexibleSpaceBar( 48 | background: widget.trip.getLocationImage(), 49 | ), 50 | ), 51 | SliverFixedExtentList( 52 | itemExtent: 200.00, 53 | delegate: SliverChildListDelegate([ 54 | buildSelectedDetails(context, widget.trip), 55 | buildButtons(), 56 | ]), 57 | ) 58 | ], 59 | ), 60 | ), 61 | ); 62 | } 63 | 64 | Widget buildButtons() { 65 | return Column( 66 | mainAxisAlignment: MainAxisAlignment.spaceEvenly, 67 | children: [ 68 | SizedBox( 69 | width: MediaQuery.of(context).size.width * 0.60, 70 | child: RaisedButton( 71 | child: Text("Change Date Range"), 72 | color: Colors.deepPurpleAccent, 73 | textColor: Colors.white, 74 | onPressed: () async { 75 | await displayDateRangePicker(context); 76 | }, 77 | ), 78 | ), 79 | SizedBox( 80 | width: MediaQuery.of(context).size.width * 0.60, 81 | child: RaisedButton( 82 | child: Text('Continue'), 83 | color: Colors.amberAccent, 84 | onPressed: () { 85 | widget.trip.startDate = _startDate; 86 | widget.trip.endDate = _endDate; 87 | Navigator.push( 88 | context, 89 | MaterialPageRoute( 90 | builder: (context) => NewTripBudgetView( 91 | trip: widget.trip, 92 | ), 93 | ), 94 | ); 95 | }, 96 | ), 97 | ), 98 | ], 99 | ); 100 | } 101 | 102 | Widget buildingSelectedDates() { 103 | return Padding( 104 | padding: const EdgeInsets.only(top: 15.0), 105 | child: Container( 106 | child: Row( 107 | mainAxisAlignment: MainAxisAlignment.spaceEvenly, 108 | children: [ 109 | Column( 110 | mainAxisAlignment: MainAxisAlignment.center, 111 | crossAxisAlignment: CrossAxisAlignment.center, 112 | children: [ 113 | Text("Start Date"), 114 | Padding( 115 | padding: const EdgeInsets.only(top: 8.0), 116 | child: Text( 117 | "${DateFormat('MM-dd').format(_startDate).toString()}", 118 | style: TextStyle(fontSize: 35, color: Colors.deepPurple), 119 | ), 120 | ), 121 | Text( 122 | "${DateFormat('yyyy').format(_startDate).toString()}", 123 | style: TextStyle(color: Colors.deepPurple), 124 | ), 125 | ], 126 | ), 127 | Container( 128 | child: Icon( 129 | Icons.arrow_forward, 130 | color: Colors.deepOrange, 131 | size: 45, 132 | )), 133 | Column( 134 | mainAxisAlignment: MainAxisAlignment.center, 135 | crossAxisAlignment: CrossAxisAlignment.center, 136 | children: [ 137 | Text("End Date"), 138 | Padding( 139 | padding: const EdgeInsets.only(top: 8.0), 140 | child: Text( 141 | "${DateFormat('MM-dd').format(_endDate).toString()}", 142 | style: TextStyle(fontSize: 35, color: Colors.deepPurple), 143 | ), 144 | ), 145 | Text( 146 | "${DateFormat('yyyy').format(_endDate).toString()}", 147 | style: TextStyle(color: Colors.deepPurple), 148 | ), 149 | ], 150 | ), 151 | ], 152 | ), 153 | ), 154 | ); 155 | } 156 | 157 | Widget buildSelectedDetails(BuildContext context, Trip trip) { 158 | return Hero( 159 | tag: "SelectedTrip-${trip.title}", 160 | transitionOnUserGestures: true, 161 | child: Container( 162 | child: Padding( 163 | padding: const EdgeInsets.only( 164 | left: 8.0, 165 | right: 8.0, 166 | ), 167 | child: SingleChildScrollView( 168 | child: Card( 169 | child: Row( 170 | children: [ 171 | Expanded( 172 | child: Padding( 173 | padding: const EdgeInsets.only( 174 | top: 16.0, left: 16.0, bottom: 16.0), 175 | child: Column( 176 | children: [ 177 | Row( 178 | children: [ 179 | Flexible( 180 | child: AutoSizeText(trip.title, 181 | maxLines: 3, 182 | style: TextStyle(fontSize: 25.0)), 183 | ), 184 | ], 185 | ), 186 | buildingSelectedDates(), 187 | ], 188 | ), 189 | ), 190 | ), 191 | ], 192 | ), 193 | ), 194 | ), 195 | ), 196 | ), 197 | ); 198 | } 199 | } 200 | -------------------------------------------------------------------------------- /lib/views/profile_view.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:travel_budget/widgets/provider_widget.dart'; 3 | import 'package:intl/intl.dart'; 4 | import 'package:travel_budget/models/User.dart'; 5 | 6 | class ProfileView extends StatefulWidget { 7 | @override 8 | _ProfileViewState createState() => _ProfileViewState(); 9 | } 10 | 11 | class _ProfileViewState extends State { 12 | User user = User(""); 13 | bool _isAdmin = false; 14 | TextEditingController _userCountryController = TextEditingController(); 15 | 16 | @override 17 | Widget build(BuildContext context) { 18 | return SingleChildScrollView( 19 | child: Container( 20 | width: MediaQuery.of(context).size.width, 21 | child: Column( 22 | children: [ 23 | FutureBuilder( 24 | future: Provider.of(context).auth.getCurrentUser(), 25 | builder: (context, snapshot) { 26 | if (snapshot.connectionState == ConnectionState.done) { 27 | return displayUserInformation(context, snapshot); 28 | } else { 29 | return CircularProgressIndicator(); 30 | } 31 | }, 32 | ) 33 | ], 34 | ), 35 | ), 36 | ); 37 | } 38 | 39 | Widget displayUserInformation(context, snapshot) { 40 | final authData = snapshot.data; 41 | 42 | return Column( 43 | children: [ 44 | Padding( 45 | padding: const EdgeInsets.only(top: 10.0), 46 | child: Provider.of(context).auth.getProfileImage(), 47 | ), 48 | Padding( 49 | padding: const EdgeInsets.all(8.0), 50 | child: Text( 51 | "Name: ${authData.displayName ?? 'Anonymous'}", 52 | style: TextStyle(fontSize: 20), 53 | ), 54 | ), 55 | Padding( 56 | padding: const EdgeInsets.all(8.0), 57 | child: Text( 58 | "Email: ${authData.email ?? 'Anonymous'}", 59 | style: TextStyle(fontSize: 20), 60 | ), 61 | ), 62 | Padding( 63 | padding: const EdgeInsets.all(8.0), 64 | child: Text( 65 | "Created: ${DateFormat('MM/dd/yyyy').format(authData.metadata.creationTime)}", 66 | style: TextStyle(fontSize: 20), 67 | ), 68 | ), 69 | FutureBuilder( 70 | future: _getProfileData(), 71 | builder: (context, snapshot) { 72 | if (snapshot.connectionState == ConnectionState.done) { 73 | _userCountryController.text = user.homeCountry; 74 | _isAdmin = user.admin ?? false; 75 | } 76 | return Container( 77 | child: Column( 78 | children: [ 79 | Padding( 80 | padding: const EdgeInsets.all(8.0), 81 | child: Text( 82 | "Home Country: ${_userCountryController.text}", 83 | style: TextStyle(fontSize: 20), 84 | ), 85 | ), 86 | adminFeature(), 87 | ], 88 | ), 89 | ); 90 | } 91 | ), 92 | showSignOut(context, authData.isAnonymous), 93 | RaisedButton( 94 | child: Text("Edit User"), 95 | onPressed: () { 96 | _userEditBottomSheet(context); 97 | }, 98 | ) 99 | ], 100 | ); 101 | } 102 | 103 | _getProfileData() async { 104 | final uid = await Provider.of(context).auth.getCurrentUID(); 105 | await Provider.of(context) 106 | .db 107 | .collection('userData') 108 | .document(uid) 109 | .get().then((result) { 110 | user.homeCountry = result.data['homeCountry']; 111 | user.admin = result.data['admin']; 112 | }); 113 | } 114 | 115 | Widget showSignOut(context, bool isAnonymous) { 116 | if (isAnonymous == true) { 117 | return RaisedButton( 118 | child: Text("Sign In To Save Your Data"), 119 | onPressed: () { 120 | Navigator.of(context).pushNamed('/convertUser'); 121 | }, 122 | ); 123 | } else { 124 | return RaisedButton( 125 | child: Text("Sign Out"), 126 | onPressed: () { 127 | try { 128 | Provider.of(context).auth.signOut(); 129 | } catch (e) { 130 | print(e); 131 | } 132 | }, 133 | ); 134 | } 135 | } 136 | 137 | Widget adminFeature() { 138 | if(_isAdmin == true) { 139 | return Text("You are an admin"); 140 | } else { 141 | return Container(); 142 | } 143 | } 144 | 145 | void _userEditBottomSheet(BuildContext context) { 146 | showModalBottomSheet( 147 | context: context, 148 | builder: (BuildContext bc) { 149 | return Container( 150 | height: MediaQuery.of(context).size.height * .60, 151 | child: Padding( 152 | padding: const EdgeInsets.only(left: 15.0, top: 15.0), 153 | child: Column( 154 | children: [ 155 | Row( 156 | children: [ 157 | Text("Update Profile"), 158 | Spacer(), 159 | IconButton( 160 | icon: Icon(Icons.cancel), 161 | color: Colors.orange, 162 | iconSize: 25, 163 | onPressed: () { 164 | Navigator.of(context).pop(); 165 | }, 166 | ), 167 | ], 168 | ), 169 | Row( 170 | children: [ 171 | Expanded( 172 | child: Padding( 173 | padding: const EdgeInsets.only(right: 15.0), 174 | child: TextField( 175 | controller: _userCountryController, 176 | decoration: InputDecoration( 177 | helperText: "Home Country", 178 | ), 179 | ), 180 | ), 181 | ) 182 | ], 183 | ), 184 | Row( 185 | mainAxisAlignment: MainAxisAlignment.center, 186 | children: [ 187 | RaisedButton( 188 | child: Text('Save'), 189 | color: Colors.green, 190 | textColor: Colors.white, 191 | onPressed: () async { 192 | user.homeCountry = _userCountryController.text; 193 | setState(() { 194 | _userCountryController.text = user.homeCountry; 195 | }); 196 | final uid = 197 | await Provider.of(context).auth.getCurrentUID(); 198 | await Provider.of(context) 199 | .db 200 | .collection('userData') 201 | .document(uid) 202 | .setData(user.toJson()); 203 | Navigator.of(context).pop(); 204 | }, 205 | ) 206 | ], 207 | ), 208 | ], 209 | ), 210 | ), 211 | ); 212 | }, 213 | ); 214 | } 215 | } 216 | -------------------------------------------------------------------------------- /lib/services/auth_service.dart: -------------------------------------------------------------------------------- 1 | import 'package:apple_sign_in/apple_sign_in.dart'; 2 | import 'package:firebase_auth/firebase_auth.dart'; 3 | import 'package:flutter/cupertino.dart'; 4 | import 'package:flutter/material.dart'; 5 | import 'package:google_sign_in/google_sign_in.dart'; 6 | 7 | class AuthService { 8 | final FirebaseAuth _firebaseAuth = FirebaseAuth.instance; 9 | final GoogleSignIn _googleSignIn = GoogleSignIn(); 10 | 11 | Stream get onAuthStateChanged => _firebaseAuth.authStateChanges().map( 12 | (User user) => user?.uid, 13 | ); 14 | 15 | // GET UID 16 | String getCurrentUID() { 17 | return _firebaseAuth.currentUser.uid; 18 | } 19 | 20 | // GET CURRENT USER 21 | Future getCurrentUser() async { 22 | return _firebaseAuth.currentUser; 23 | } 24 | 25 | getProfileImage() { 26 | if(_firebaseAuth.currentUser.photoURL != null) { 27 | return Image.network(_firebaseAuth.currentUser.photoURL, height: 100, width: 100); 28 | } else { 29 | return Icon(Icons.account_circle, size: 100); 30 | } 31 | } 32 | 33 | // Email & Password Sign Up 34 | Future createUserWithEmailAndPassword( 35 | String email, String password, String name) async { 36 | final authResult = await _firebaseAuth.createUserWithEmailAndPassword( 37 | email: email, 38 | password: password, 39 | ); 40 | 41 | // Update the username 42 | await updateUserName(name, authResult.user); 43 | return authResult.user.uid; 44 | } 45 | 46 | Future updateUserName(String name, User currentUser) async { 47 | await currentUser.updateProfile(displayName: name); 48 | await currentUser.reload(); 49 | } 50 | 51 | // Email & Password Sign In 52 | Future signInWithEmailAndPassword( 53 | String email, String password) async { 54 | return (await _firebaseAuth.signInWithEmailAndPassword( 55 | email: email, password: password)) 56 | .user 57 | .uid; 58 | } 59 | 60 | // Sign Out 61 | signOut() async { 62 | return await _firebaseAuth.signOut(); 63 | } 64 | 65 | // Reset Password 66 | Future sendPasswordResetEmail(String email) async { 67 | return _firebaseAuth.sendPasswordResetEmail(email: email); 68 | } 69 | 70 | // Create Anonymous User 71 | Future singInAnonymously() { 72 | return _firebaseAuth.signInAnonymously(); 73 | } 74 | 75 | Future convertUserWithEmail( 76 | String email, String password, String name) async { 77 | final currentUser = _firebaseAuth.currentUser; 78 | 79 | final credential = 80 | EmailAuthProvider.credential(email: email, password: password); 81 | await currentUser.linkWithCredential(credential); 82 | await updateUserName(name, currentUser); 83 | } 84 | 85 | Future convertWithGoogle() async { 86 | final currentUser = _firebaseAuth.currentUser; 87 | final GoogleSignInAccount account = await _googleSignIn.signIn(); 88 | final GoogleSignInAuthentication _googleAuth = await account.authentication; 89 | final AuthCredential credential = GoogleAuthProvider.credential( 90 | idToken: _googleAuth.idToken, 91 | accessToken: _googleAuth.accessToken, 92 | ); 93 | await currentUser.linkWithCredential(credential); 94 | await updateUserName(_googleSignIn.currentUser.displayName, currentUser); 95 | } 96 | 97 | // GOOGLE 98 | Future signInWithGoogle() async { 99 | final GoogleSignInAccount account = await _googleSignIn.signIn(); 100 | final GoogleSignInAuthentication _googleAuth = await account.authentication; 101 | final AuthCredential credential = GoogleAuthProvider.credential( 102 | idToken: _googleAuth.idToken, 103 | accessToken: _googleAuth.accessToken, 104 | ); 105 | return (await _firebaseAuth.signInWithCredential(credential)).user.uid; 106 | } 107 | 108 | // APPLE 109 | Future signInWithApple() async { 110 | final AuthorizationResult result = await AppleSignIn.performRequests([ 111 | AppleIdRequest(requestedScopes: [Scope.email, Scope.fullName]) 112 | ]); 113 | 114 | switch (result.status) { 115 | case AuthorizationStatus.authorized: 116 | final AppleIdCredential _auth = result.credential; 117 | final OAuthProvider oAuthProvider = 118 | new OAuthProvider("apple.com"); 119 | 120 | final AuthCredential credential = oAuthProvider.credential( 121 | idToken: String.fromCharCodes(_auth.identityToken), 122 | accessToken: String.fromCharCodes(_auth.authorizationCode), 123 | ); 124 | 125 | await _firebaseAuth.signInWithCredential(credential); 126 | 127 | // update the user information 128 | if (_auth.fullName != null) { 129 | await _firebaseAuth.currentUser.updateProfile(displayName: "${_auth.fullName.givenName} ${_auth.fullName.familyName}"); 130 | } 131 | 132 | break; 133 | 134 | case AuthorizationStatus.error: 135 | print("Sign In Failed ${result.error.localizedDescription}"); 136 | break; 137 | 138 | case AuthorizationStatus.cancelled: 139 | print("User Cancled"); 140 | break; 141 | } 142 | } 143 | 144 | Future createUserWithPhone(String phone, BuildContext context) async { 145 | _firebaseAuth.verifyPhoneNumber( 146 | phoneNumber: phone, 147 | timeout: Duration(seconds: 0), 148 | verificationCompleted: (AuthCredential authCredential) { 149 | _firebaseAuth.signInWithCredential(authCredential).then((UserCredential result){ 150 | Navigator.of(context).pop(); // to pop the dialog box 151 | Navigator.of(context).pushReplacementNamed('/home'); 152 | }).catchError((e) { 153 | return "error"; 154 | }); 155 | }, 156 | verificationFailed: (FirebaseAuthException exception) { 157 | return "error"; 158 | }, 159 | codeSent: (String verificationId, [int forceResendingToken]) { 160 | final _codeController = TextEditingController(); 161 | showDialog( 162 | context: context, 163 | barrierDismissible: false, 164 | builder: (context) => AlertDialog( 165 | title: Text("Enter Verification Code From Text Message"), 166 | content: Column( 167 | mainAxisSize: MainAxisSize.min, 168 | children: [TextField(controller: _codeController)], 169 | ), 170 | actions: [ 171 | FlatButton( 172 | child: Text("submit"), 173 | textColor: Colors.white, 174 | color: Colors.green, 175 | onPressed: () { 176 | var _credential = PhoneAuthProvider.credential(verificationId: verificationId, 177 | smsCode: _codeController.text.trim()); 178 | _firebaseAuth.signInWithCredential(_credential).then((UserCredential result){ 179 | Navigator.of(context).pop(); // to pop the dialog box 180 | Navigator.of(context).pushReplacementNamed('/home'); 181 | }).catchError((e) { 182 | return "error"; 183 | }); 184 | }, 185 | ) 186 | ], 187 | ), 188 | ); 189 | }, 190 | codeAutoRetrievalTimeout: (String verificationId) { 191 | verificationId = verificationId; 192 | }); 193 | } 194 | } 195 | 196 | class NameValidator { 197 | static String validate(String value) { 198 | if (value.isEmpty) { 199 | return "Name can't be empty"; 200 | } 201 | if (value.length < 2) { 202 | return "Name must be at least 2 characters long"; 203 | } 204 | if (value.length > 50) { 205 | return "Name must be less than 50 characters long"; 206 | } 207 | return null; 208 | } 209 | } 210 | 211 | class EmailValidator { 212 | static String validate(String value) { 213 | if (value.isEmpty) { 214 | return "Email can't be empty"; 215 | } 216 | return null; 217 | } 218 | } 219 | 220 | class PasswordValidator { 221 | static String validate(String value) { 222 | if (value.isEmpty) { 223 | return "Password can't be empty"; 224 | } 225 | return null; 226 | } 227 | } 228 | -------------------------------------------------------------------------------- /lib/views/detail_trip_view.dart: -------------------------------------------------------------------------------- 1 | import 'package:cloud_firestore/cloud_firestore.dart'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:travel_budget/models/Trip.dart'; 4 | import 'package:travel_budget/services/admob_service.dart'; 5 | import 'package:travel_budget/widgets/provider_widget.dart'; 6 | import 'edit_notes_view.dart'; 7 | import 'package:intl/intl.dart'; 8 | import 'package:auto_size_text/auto_size_text.dart'; 9 | import 'package:travel_budget/widgets/money_text_field.dart'; 10 | import 'package:travel_budget/widgets/calculator_widget.dart'; 11 | 12 | class DetailTripView extends StatefulWidget { 13 | final Trip trip; 14 | 15 | DetailTripView({Key key, @required this.trip}) : super(key: key); 16 | 17 | @override 18 | _DetailTripViewState createState() => _DetailTripViewState(); 19 | } 20 | 21 | class _DetailTripViewState extends State { 22 | TextEditingController _budgetController = TextEditingController(); 23 | var _budget; 24 | 25 | void initState() { 26 | super.initState(); 27 | AdMobService.hideHomeBannerAd(); 28 | _budgetController.text = widget.trip.budget.toStringAsFixed(0); 29 | _budget = widget.trip.budget.floor(); 30 | } 31 | 32 | @override 33 | Widget build(BuildContext context) { 34 | return Scaffold( 35 | body: Center( 36 | child: CustomScrollView( 37 | slivers: [ 38 | SliverAppBar( 39 | title: Text('Trip Details'), 40 | backgroundColor: Colors.green, 41 | expandedHeight: 350.0, 42 | flexibleSpace: FlexibleSpaceBar( 43 | background: widget.trip.getLocationImage(), 44 | ), 45 | actions: [ 46 | IconButton( 47 | icon: Icon( 48 | Icons.settings, 49 | color: Colors.white, 50 | size: 30, 51 | ), 52 | padding: const EdgeInsets.only(right: 15), 53 | onPressed: () { 54 | _tripEditModalBottomSheet(context); 55 | }, 56 | ), 57 | ], 58 | ), 59 | SliverList( 60 | delegate: SliverChildListDelegate([ 61 | tripDetails(), 62 | CalculatorWidget(trip: widget.trip), 63 | totalBudgetCard(), 64 | daysOutCard(), 65 | notesCard(context), 66 | Container( 67 | height: 200, 68 | ) 69 | ]), 70 | ) 71 | ], 72 | ), 73 | ), 74 | ); 75 | } 76 | 77 | Widget daysOutCard() { 78 | return Card( 79 | color: Colors.amberAccent, 80 | child: Padding( 81 | padding: const EdgeInsets.all(8.0), 82 | child: Row( 83 | mainAxisAlignment: MainAxisAlignment.spaceEvenly, 84 | children: [ 85 | Text("${widget.trip.getDaysUntilTrip()}", style: TextStyle(fontSize: 75)), 86 | Text("days until your trip", style: TextStyle(fontSize: 25)) 87 | ], 88 | ), 89 | ), 90 | ); 91 | } 92 | 93 | Widget tripDetails() { 94 | return Card( 95 | child: Column( 96 | children: [ 97 | Row( 98 | children: [ 99 | Padding( 100 | padding: const EdgeInsets.all(8.0), 101 | child: Text( 102 | widget.trip.title, 103 | style: TextStyle(fontSize: 30, color: Colors.green[900]), 104 | ), 105 | ), 106 | ], 107 | ), 108 | Row( 109 | children: [ 110 | Padding( 111 | padding: const EdgeInsets.only(left: 8.0, bottom: 8.0), 112 | child: Text( 113 | "${DateFormat('MM/dd/yyyy').format(widget.trip.startDate).toString()} - ${DateFormat('MM/dd/yyyy').format(widget.trip.endDate).toString()}"), 114 | ), 115 | ], 116 | ), 117 | ], 118 | ), 119 | ); 120 | } 121 | 122 | Widget totalBudgetCard() { 123 | return Card( 124 | color: Colors.blue, 125 | child: Column( 126 | children: [ 127 | Padding( 128 | padding: const EdgeInsets.all(8.0), 129 | child: Row( 130 | mainAxisAlignment: MainAxisAlignment.center, 131 | children: [ 132 | Text("Daily Budget", 133 | style: TextStyle(fontSize: 15, color: Colors.white)), 134 | ], 135 | ), 136 | ), 137 | Padding( 138 | padding: const EdgeInsets.all(8.0), 139 | child: Row( 140 | mainAxisAlignment: MainAxisAlignment.center, 141 | children: [ 142 | Flexible( 143 | child: AutoSizeText( 144 | "\$$_budget", 145 | style: TextStyle(fontSize: 100), 146 | maxLines: 1, 147 | ), 148 | ), 149 | ], 150 | ), 151 | ), 152 | Row( 153 | children: [ 154 | Expanded( 155 | child: Container( 156 | color: Colors.blue[900], 157 | child: Padding( 158 | padding: const EdgeInsets.symmetric( 159 | vertical: 10.0, horizontal: 20), 160 | child: Text( 161 | "\$${_budget * widget.trip.getTotalTripDays()} total", 162 | style: TextStyle(color: Colors.white, fontSize: 20), 163 | ), 164 | ), 165 | ), 166 | ) 167 | ], 168 | ) 169 | ], 170 | ), 171 | ); 172 | } 173 | 174 | Widget notesCard(context) { 175 | return Hero( 176 | tag: "TripNotes-${widget.trip.title}", 177 | transitionOnUserGestures: true, 178 | child: Card( 179 | color: Colors.deepPurpleAccent, 180 | child: InkWell( 181 | child: Column( 182 | children: [ 183 | Padding( 184 | padding: const EdgeInsets.only(top: 10.0, left: 10.0), 185 | child: Row( 186 | children: [ 187 | Text("Trip Notes", 188 | style: TextStyle(fontSize: 24, color: Colors.white)), 189 | ], 190 | ), 191 | ), 192 | Padding( 193 | padding: const EdgeInsets.all(8.0), 194 | child: Row( 195 | children: setNoteText(), 196 | ), 197 | ) 198 | ], 199 | ), 200 | onTap: () { 201 | Navigator.push( 202 | context, 203 | MaterialPageRoute( 204 | builder: (context) => EditNotesView(trip: widget.trip))); 205 | }, 206 | ), 207 | ), 208 | ); 209 | } 210 | 211 | List setNoteText() { 212 | if (widget.trip.notes == null) { 213 | return [ 214 | Padding( 215 | padding: const EdgeInsets.only(right: 8.0), 216 | child: Icon(Icons.add_circle_outline, color: Colors.grey[300]), 217 | ), 218 | Text("Click To Add Notes", style: TextStyle(color: Colors.grey[300])), 219 | ]; 220 | } else { 221 | return [ 222 | Text(widget.trip.notes, style: TextStyle(color: Colors.grey[300])) 223 | ]; 224 | } 225 | } 226 | 227 | 228 | void _tripEditModalBottomSheet(context) { 229 | showModalBottomSheet( 230 | context: context, 231 | builder: (BuildContext bc) { 232 | return Container( 233 | height: MediaQuery.of(context).size.height * .60, 234 | child: Padding( 235 | padding: const EdgeInsets.all(8.0), 236 | child: Column( 237 | children: [ 238 | Row( 239 | children: [ 240 | Text("Edit Trip"), 241 | Spacer(), 242 | IconButton( 243 | icon: Icon( 244 | Icons.cancel, 245 | color: Colors.orange, 246 | size: 25, 247 | ), 248 | onPressed: () { 249 | Navigator.of(context).pop(); 250 | }, 251 | ) 252 | ], 253 | ), 254 | Row( 255 | children: [ 256 | Text( 257 | widget.trip.title, 258 | style: TextStyle(fontSize: 30, color: Colors.green[900]), 259 | ), 260 | ], 261 | ), 262 | Row( 263 | children: [ 264 | Expanded( 265 | child: MoneyTextField( 266 | controller: _budgetController, 267 | helperText: "Budget", 268 | ), 269 | ) 270 | ], 271 | ), 272 | Row( 273 | mainAxisAlignment: MainAxisAlignment.center, 274 | children: [ 275 | RaisedButton( 276 | child: Text('Submit'), 277 | color: Colors.deepPurple, 278 | textColor: Colors.white, 279 | onPressed: () async { 280 | widget.trip.budget = double.parse(_budgetController.text); 281 | setState(() { 282 | _budget = widget.trip.budget.floor(); 283 | }); 284 | await updateTrip(context); 285 | Navigator.of(context).pop(); 286 | }, 287 | ) 288 | ], 289 | ), 290 | Row( 291 | mainAxisAlignment: MainAxisAlignment.center, 292 | children: [ 293 | RaisedButton( 294 | child: Text('Delete'), 295 | color: Colors.red, 296 | textColor: Colors.white, 297 | onPressed: () async { 298 | await deleteTrip(context); 299 | Navigator.of(context).pushNamedAndRemoveUntil('/home', (Route route) => false); 300 | }, 301 | ) 302 | ], 303 | ) 304 | ], 305 | ), 306 | ), 307 | ); 308 | }, 309 | ); 310 | } 311 | 312 | Future updateTrip(context) async { 313 | var uid = await Provider.of(context).auth.getCurrentUID(); 314 | final doc = FirebaseFirestore.instance 315 | .collection('userData') 316 | .doc(uid) 317 | .collection("trips") 318 | .doc(widget.trip.documentId); 319 | 320 | return await doc.set(widget.trip.toJson()); 321 | } 322 | 323 | Future deleteTrip(context) async { 324 | var uid = await Provider.of(context).auth.getCurrentUID(); 325 | final doc = FirebaseFirestore.instance 326 | .collection('userData') 327 | .doc(uid) 328 | .collection("trips") 329 | .doc(widget.trip.documentId); 330 | 331 | return await doc.delete(); 332 | } 333 | } 334 | -------------------------------------------------------------------------------- /lib/views/sign_up_view.dart: -------------------------------------------------------------------------------- 1 | import 'package:apple_sign_in/apple_sign_in.dart'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:travel_budget/services/auth_service.dart'; 4 | import 'package:auto_size_text/auto_size_text.dart'; 5 | import 'package:travel_budget/widgets/provider_widget.dart'; 6 | import 'package:flutter_spinkit/flutter_spinkit.dart'; 7 | import 'package:flutter_auth_buttons/flutter_auth_buttons.dart'; 8 | import 'package:international_phone_input/international_phone_input.dart'; 9 | 10 | // TODO move this to tone location 11 | final primaryColor = const Color(0xFF75A2EA); 12 | 13 | enum AuthFormType { signIn, signUp, reset, anonymous, convert, phone } 14 | 15 | class SignUpView extends StatefulWidget { 16 | final AuthFormType authFormType; 17 | 18 | SignUpView({Key key, @required this.authFormType}) : super(key: key); 19 | 20 | @override 21 | _SignUpViewState createState() => 22 | _SignUpViewState(authFormType: this.authFormType); 23 | } 24 | 25 | class _SignUpViewState extends State { 26 | AuthFormType authFormType; 27 | bool _showAppleSignIn = false; 28 | 29 | @override 30 | void initState() { 31 | super.initState(); 32 | 33 | _useAppleSignIn(); 34 | } 35 | 36 | _useAppleSignIn() async { 37 | final isAvailable = await AppleSignIn.isAvailable(); 38 | setState(() { 39 | _showAppleSignIn = isAvailable; 40 | }); 41 | } 42 | 43 | _SignUpViewState({this.authFormType}); 44 | 45 | final formKey = GlobalKey(); 46 | String _email, _password, _name, _warning, _phone; 47 | 48 | void switchFormState(String state) { 49 | formKey.currentState.reset(); 50 | if (state == "signUp") { 51 | setState(() { 52 | authFormType = AuthFormType.signUp; 53 | }); 54 | } else if (state == 'home') { 55 | Navigator.of(context).pop(); 56 | } else { 57 | setState(() { 58 | authFormType = AuthFormType.signIn; 59 | }); 60 | } 61 | } 62 | 63 | bool validate() { 64 | final form = formKey.currentState; 65 | if (authFormType == AuthFormType.anonymous) { 66 | return true; 67 | } 68 | form.save(); 69 | if (form.validate()) { 70 | form.save(); 71 | return true; 72 | } else { 73 | return false; 74 | } 75 | } 76 | 77 | void submit() async { 78 | if (validate()) { 79 | try { 80 | final auth = Provider.of(context).auth; 81 | switch (authFormType) { 82 | case AuthFormType.signIn: 83 | await auth.signInWithEmailAndPassword(_email, _password); 84 | Navigator.of(context).pushReplacementNamed('/home'); 85 | break; 86 | case AuthFormType.signUp: 87 | await auth.createUserWithEmailAndPassword(_email, _password, _name); 88 | Navigator.of(context).pushReplacementNamed('/home'); 89 | break; 90 | case AuthFormType.reset: 91 | await auth.sendPasswordResetEmail(_email); 92 | setState(() { 93 | _warning = "A password reset link has been sent to $_email"; 94 | authFormType = AuthFormType.signIn; 95 | }); 96 | break; 97 | case AuthFormType.anonymous: 98 | await auth.singInAnonymously(); 99 | Navigator.of(context).pushReplacementNamed('/home'); 100 | break; 101 | case AuthFormType.convert: 102 | await auth.convertUserWithEmail(_email, _password, _name); 103 | Navigator.of(context).pop(); 104 | break; 105 | case AuthFormType.phone: 106 | var result = await auth.createUserWithPhone(_phone, context); 107 | if (_phone == "" || result == "error") { 108 | setState(() { 109 | _warning = "Your phone number could not be validated"; 110 | }); 111 | } 112 | break; 113 | } 114 | } catch (e) { 115 | setState(() { 116 | _warning = e.message; 117 | }); 118 | } 119 | } 120 | } 121 | 122 | @override 123 | Widget build(BuildContext context) { 124 | final _width = MediaQuery.of(context).size.width; 125 | final _height = MediaQuery.of(context).size.height; 126 | 127 | if (authFormType == AuthFormType.anonymous) { 128 | submit(); 129 | return Scaffold( 130 | backgroundColor: primaryColor, 131 | body: SingleChildScrollView( 132 | child: Column( 133 | mainAxisAlignment: MainAxisAlignment.center, 134 | children: [ 135 | SpinKitDoubleBounce( 136 | color: Colors.white, 137 | ), 138 | Text( 139 | "Loading", 140 | style: TextStyle(color: Colors.white), 141 | ), 142 | ], 143 | ), 144 | )); 145 | } else { 146 | return Scaffold( 147 | body: SingleChildScrollView( 148 | child: Container( 149 | color: primaryColor, 150 | height: _height, 151 | width: _width, 152 | child: SafeArea( 153 | child: Column( 154 | children: [ 155 | SizedBox(height: _height * 0.025), 156 | showAlert(), 157 | SizedBox(height: _height * 0.025), 158 | buildHeaderText(), 159 | SizedBox(height: _height * 0.05), 160 | Padding( 161 | padding: const EdgeInsets.all(20.0), 162 | child: Form( 163 | key: formKey, 164 | child: Column( 165 | children: buildInputs() + buildButtons(), 166 | ), 167 | ), 168 | ), 169 | ], 170 | ), 171 | ), 172 | ), 173 | ), 174 | ); 175 | } 176 | } 177 | 178 | Widget showAlert() { 179 | if (_warning != null) { 180 | return Container( 181 | color: Colors.amberAccent, 182 | width: double.infinity, 183 | padding: EdgeInsets.all(8.0), 184 | child: Row( 185 | children: [ 186 | Padding( 187 | padding: const EdgeInsets.only(right: 8.0), 188 | child: Icon(Icons.error_outline), 189 | ), 190 | Expanded( 191 | child: AutoSizeText( 192 | _warning, 193 | maxLines: 3, 194 | ), 195 | ), 196 | Padding( 197 | padding: const EdgeInsets.only(left: 8.0), 198 | child: IconButton( 199 | icon: Icon(Icons.close), 200 | onPressed: () { 201 | setState(() { 202 | _warning = null; 203 | }); 204 | }, 205 | ), 206 | ) 207 | ], 208 | ), 209 | ); 210 | } 211 | return SizedBox( 212 | height: 0, 213 | ); 214 | } 215 | 216 | AutoSizeText buildHeaderText() { 217 | String _headerText; 218 | if (authFormType == AuthFormType.signIn) { 219 | _headerText = "Sign In"; 220 | } else if (authFormType == AuthFormType.reset) { 221 | _headerText = "Reset Password"; 222 | } else if (authFormType == AuthFormType.phone) { 223 | _headerText = "Phone Sign In"; 224 | } else { 225 | _headerText = "Create New Account"; 226 | } 227 | return AutoSizeText( 228 | _headerText, 229 | maxLines: 1, 230 | textAlign: TextAlign.center, 231 | style: TextStyle( 232 | fontSize: 35, 233 | color: Colors.white, 234 | ), 235 | ); 236 | } 237 | 238 | void onPhoneNumberChange( 239 | String number, String internationalizedPhoneNumber, String isoCode) { 240 | setState(() { 241 | _phone = internationalizedPhoneNumber; 242 | }); 243 | } 244 | 245 | List buildInputs() { 246 | List textFields = []; 247 | 248 | // if were in the sign up state add name 249 | if ([AuthFormType.signUp, AuthFormType.convert].contains(authFormType)) { 250 | textFields.add( 251 | TextFormField( 252 | validator: NameValidator.validate, 253 | style: TextStyle(fontSize: 22.0), 254 | decoration: buildSignUpInputDecoration("Name"), 255 | onSaved: (value) => _name = value, 256 | ), 257 | ); 258 | textFields.add(SizedBox(height: 20)); 259 | } 260 | 261 | // add email & password 262 | if ([ 263 | AuthFormType.signUp, 264 | AuthFormType.convert, 265 | AuthFormType.reset, 266 | AuthFormType.signIn 267 | ].contains(authFormType)) { 268 | textFields.add( 269 | TextFormField( 270 | validator: EmailValidator.validate, 271 | style: TextStyle(fontSize: 22.0), 272 | decoration: buildSignUpInputDecoration("Email"), 273 | onSaved: (value) => _email = value, 274 | ), 275 | ); 276 | textFields.add(SizedBox(height: 20)); 277 | } 278 | 279 | if (authFormType != AuthFormType.reset && 280 | authFormType != AuthFormType.phone) { 281 | textFields.add( 282 | TextFormField( 283 | validator: PasswordValidator.validate, 284 | style: TextStyle(fontSize: 22.0), 285 | decoration: buildSignUpInputDecoration("Password"), 286 | obscureText: true, 287 | onSaved: (value) => _password = value, 288 | ), 289 | ); 290 | textFields.add(SizedBox(height: 20)); 291 | } 292 | 293 | if (authFormType == AuthFormType.phone) { 294 | textFields.add( 295 | InternationalPhoneInput( 296 | decoration: buildSignUpInputDecoration("Enter Phone Number"), 297 | onPhoneNumberChange: onPhoneNumberChange, 298 | initialPhoneNumber: _phone, 299 | initialSelection: 'US', 300 | showCountryCodes: true), 301 | ); 302 | textFields.add(SizedBox(height: 20)); 303 | } 304 | 305 | return textFields; 306 | } 307 | 308 | InputDecoration buildSignUpInputDecoration(String hint) { 309 | return InputDecoration( 310 | hintText: hint, 311 | filled: true, 312 | fillColor: Colors.white, 313 | focusColor: Colors.white, 314 | enabledBorder: OutlineInputBorder( 315 | borderSide: BorderSide(color: Colors.white, width: 0.0)), 316 | contentPadding: 317 | const EdgeInsets.only(left: 14.0, bottom: 10.0, top: 10.0), 318 | ); 319 | } 320 | 321 | List buildButtons() { 322 | String _switchButtonText, _newFormState, _submitButtonText; 323 | bool _showForgotPassword = false; 324 | bool _showSocial = true; 325 | 326 | if (authFormType == AuthFormType.signIn) { 327 | _switchButtonText = "Create New Account"; 328 | _newFormState = "signUp"; 329 | _submitButtonText = "Sign In"; 330 | _showForgotPassword = true; 331 | } else if (authFormType == AuthFormType.reset) { 332 | _switchButtonText = "Return to Sign In"; 333 | _newFormState = "signIn"; 334 | _submitButtonText = "Submit"; 335 | _showSocial = false; 336 | } else if (authFormType == AuthFormType.convert) { 337 | _switchButtonText = "Cancel"; 338 | _newFormState = "home"; 339 | _submitButtonText = "Sign Up"; 340 | } else if (authFormType == AuthFormType.phone) { 341 | _switchButtonText = "Cancel"; 342 | _newFormState = "signIn"; 343 | _submitButtonText = "Continue"; 344 | _showSocial = false; 345 | } else { 346 | _switchButtonText = "Have an Account? Sign In"; 347 | _newFormState = "signIn"; 348 | _submitButtonText = "Sign Up"; 349 | } 350 | 351 | return [ 352 | Container( 353 | width: MediaQuery.of(context).size.width * 0.7, 354 | child: RaisedButton( 355 | shape: 356 | RoundedRectangleBorder(borderRadius: BorderRadius.circular(30.0)), 357 | color: Colors.white, 358 | textColor: primaryColor, 359 | child: Padding( 360 | padding: const EdgeInsets.all(8.0), 361 | child: Text( 362 | _submitButtonText, 363 | style: TextStyle(fontSize: 20.0, fontWeight: FontWeight.w300), 364 | ), 365 | ), 366 | onPressed: submit, 367 | ), 368 | ), 369 | showForgotPassword(_showForgotPassword), 370 | FlatButton( 371 | child: Text( 372 | _switchButtonText, 373 | style: TextStyle(color: Colors.white), 374 | ), 375 | onPressed: () { 376 | switchFormState(_newFormState); 377 | }, 378 | ), 379 | buildSocialIcons(_showSocial), 380 | ]; 381 | } 382 | 383 | Widget showForgotPassword(bool visible) { 384 | return Visibility( 385 | child: FlatButton( 386 | child: Text( 387 | "Forgot Password?", 388 | style: TextStyle(color: Colors.white), 389 | ), 390 | onPressed: () { 391 | setState(() { 392 | authFormType = AuthFormType.reset; 393 | }); 394 | }, 395 | ), 396 | visible: visible, 397 | ); 398 | } 399 | 400 | Widget buildSocialIcons(bool visible) { 401 | final _auth = Provider.of(context).auth; 402 | return Visibility( 403 | child: Column( 404 | crossAxisAlignment: CrossAxisAlignment.stretch, 405 | children: [ 406 | Divider( 407 | color: Colors.white, 408 | ), 409 | SizedBox(height: 10), 410 | buildAppleSignIn(_auth), 411 | SizedBox(height: 10), 412 | GoogleSignInButton( 413 | onPressed: () async { 414 | try { 415 | if (authFormType == AuthFormType.convert) { 416 | await _auth.convertWithGoogle(); 417 | Navigator.of(context).pop(); 418 | } else { 419 | await _auth.signInWithGoogle(); 420 | Navigator.of(context).pushReplacementNamed('/home'); 421 | } 422 | } catch (e) { 423 | setState(() { 424 | _warning = e.message; 425 | }); 426 | } 427 | }, 428 | ), 429 | RaisedButton( 430 | color: Colors.green, 431 | textColor: Colors.white, 432 | child: Row( 433 | children: [ 434 | Icon(Icons.phone), 435 | Padding( 436 | padding: const EdgeInsets.only( 437 | left: 14.0, top: 10.0, bottom: 10.0), 438 | child: Text("Sign in with Phone", 439 | style: TextStyle(fontSize: 18)), 440 | ) 441 | ], 442 | ), 443 | onPressed: () { 444 | setState(() { 445 | authFormType = AuthFormType.phone; 446 | }); 447 | }, 448 | ), 449 | ], 450 | ), 451 | visible: visible, 452 | ); 453 | } 454 | 455 | Widget buildAppleSignIn(_auth) { 456 | if (authFormType != AuthFormType.convert && _showAppleSignIn == true) { 457 | return AppleSignInButton( 458 | onPressed: () async { 459 | await _auth.signInWithApple(); 460 | Navigator.of(context).pushReplacementNamed('/home'); 461 | }, 462 | style: ButtonStyle.black, 463 | ); 464 | } else { 465 | return Container(); 466 | } 467 | } 468 | } 469 | -------------------------------------------------------------------------------- /pubspec.lock: -------------------------------------------------------------------------------- 1 | # Generated by pub 2 | # See https://dart.dev/tools/pub/glossary#lockfile 3 | packages: 4 | apple_sign_in: 5 | dependency: "direct main" 6 | description: 7 | name: apple_sign_in 8 | url: "https://pub.dartlang.org" 9 | source: hosted 10 | version: "0.1.0" 11 | archive: 12 | dependency: transitive 13 | description: 14 | name: archive 15 | url: "https://pub.dartlang.org" 16 | source: hosted 17 | version: "2.0.13" 18 | args: 19 | dependency: transitive 20 | description: 21 | name: args 22 | url: "https://pub.dartlang.org" 23 | source: hosted 24 | version: "1.6.0" 25 | async: 26 | dependency: transitive 27 | description: 28 | name: async 29 | url: "https://pub.dartlang.org" 30 | source: hosted 31 | version: "2.4.2" 32 | auto_size_text: 33 | dependency: "direct main" 34 | description: 35 | name: auto_size_text 36 | url: "https://pub.dartlang.org" 37 | source: hosted 38 | version: "2.1.0" 39 | boolean_selector: 40 | dependency: transitive 41 | description: 42 | name: boolean_selector 43 | url: "https://pub.dartlang.org" 44 | source: hosted 45 | version: "2.0.0" 46 | built_collection: 47 | dependency: transitive 48 | description: 49 | name: built_collection 50 | url: "https://pub.dartlang.org" 51 | source: hosted 52 | version: "4.3.2" 53 | built_value: 54 | dependency: transitive 55 | description: 56 | name: built_value 57 | url: "https://pub.dartlang.org" 58 | source: hosted 59 | version: "7.1.0" 60 | characters: 61 | dependency: transitive 62 | description: 63 | name: characters 64 | url: "https://pub.dartlang.org" 65 | source: hosted 66 | version: "1.0.0" 67 | charcode: 68 | dependency: transitive 69 | description: 70 | name: charcode 71 | url: "https://pub.dartlang.org" 72 | source: hosted 73 | version: "1.1.3" 74 | clock: 75 | dependency: transitive 76 | description: 77 | name: clock 78 | url: "https://pub.dartlang.org" 79 | source: hosted 80 | version: "1.0.1" 81 | cloud_firestore: 82 | dependency: "direct main" 83 | description: 84 | name: cloud_firestore 85 | url: "https://pub.dartlang.org" 86 | source: hosted 87 | version: "0.14.4" 88 | cloud_firestore_platform_interface: 89 | dependency: transitive 90 | description: 91 | name: cloud_firestore_platform_interface 92 | url: "https://pub.dartlang.org" 93 | source: hosted 94 | version: "2.2.1" 95 | cloud_firestore_web: 96 | dependency: transitive 97 | description: 98 | name: cloud_firestore_web 99 | url: "https://pub.dartlang.org" 100 | source: hosted 101 | version: "0.2.1+2" 102 | collection: 103 | dependency: transitive 104 | description: 105 | name: collection 106 | url: "https://pub.dartlang.org" 107 | source: hosted 108 | version: "1.14.13" 109 | convert: 110 | dependency: transitive 111 | description: 112 | name: convert 113 | url: "https://pub.dartlang.org" 114 | source: hosted 115 | version: "2.1.1" 116 | crypto: 117 | dependency: transitive 118 | description: 119 | name: crypto 120 | url: "https://pub.dartlang.org" 121 | source: hosted 122 | version: "2.1.5" 123 | cupertino_icons: 124 | dependency: "direct main" 125 | description: 126 | name: cupertino_icons 127 | url: "https://pub.dartlang.org" 128 | source: hosted 129 | version: "0.1.3" 130 | date_range_picker: 131 | dependency: "direct main" 132 | description: 133 | name: date_range_picker 134 | url: "https://pub.dartlang.org" 135 | source: hosted 136 | version: "1.0.6" 137 | device_info: 138 | dependency: "direct main" 139 | description: 140 | name: device_info 141 | url: "https://pub.dartlang.org" 142 | source: hosted 143 | version: "0.4.2+7" 144 | device_info_platform_interface: 145 | dependency: transitive 146 | description: 147 | name: device_info_platform_interface 148 | url: "https://pub.dartlang.org" 149 | source: hosted 150 | version: "1.0.1" 151 | dio: 152 | dependency: "direct main" 153 | description: 154 | name: dio 155 | url: "https://pub.dartlang.org" 156 | source: hosted 157 | version: "3.0.10" 158 | fake_async: 159 | dependency: transitive 160 | description: 161 | name: fake_async 162 | url: "https://pub.dartlang.org" 163 | source: hosted 164 | version: "1.1.0" 165 | file: 166 | dependency: transitive 167 | description: 168 | name: file 169 | url: "https://pub.dartlang.org" 170 | source: hosted 171 | version: "5.2.1" 172 | firebase_admob: 173 | dependency: "direct main" 174 | description: 175 | name: firebase_admob 176 | url: "https://pub.dartlang.org" 177 | source: hosted 178 | version: "0.10.0-dev.1" 179 | firebase_auth: 180 | dependency: "direct main" 181 | description: 182 | name: firebase_auth 183 | url: "https://pub.dartlang.org" 184 | source: hosted 185 | version: "0.18.4+1" 186 | firebase_auth_platform_interface: 187 | dependency: transitive 188 | description: 189 | name: firebase_auth_platform_interface 190 | url: "https://pub.dartlang.org" 191 | source: hosted 192 | version: "2.1.4" 193 | firebase_auth_web: 194 | dependency: transitive 195 | description: 196 | name: firebase_auth_web 197 | url: "https://pub.dartlang.org" 198 | source: hosted 199 | version: "0.3.2+3" 200 | firebase_core: 201 | dependency: "direct main" 202 | description: 203 | name: firebase_core 204 | url: "https://pub.dartlang.org" 205 | source: hosted 206 | version: "0.5.3" 207 | firebase_core_platform_interface: 208 | dependency: transitive 209 | description: 210 | name: firebase_core_platform_interface 211 | url: "https://pub.dartlang.org" 212 | source: hosted 213 | version: "2.1.0" 214 | firebase_core_web: 215 | dependency: transitive 216 | description: 217 | name: firebase_core_web 218 | url: "https://pub.dartlang.org" 219 | source: hosted 220 | version: "0.2.1+1" 221 | firebase_crashlytics: 222 | dependency: "direct main" 223 | description: 224 | name: firebase_crashlytics 225 | url: "https://pub.dartlang.org" 226 | source: hosted 227 | version: "0.2.4" 228 | firebase_crashlytics_platform_interface: 229 | dependency: transitive 230 | description: 231 | name: firebase_crashlytics_platform_interface 232 | url: "https://pub.dartlang.org" 233 | source: hosted 234 | version: "1.1.4" 235 | fixnum: 236 | dependency: transitive 237 | description: 238 | name: fixnum 239 | url: "https://pub.dartlang.org" 240 | source: hosted 241 | version: "0.10.11" 242 | flare_dart: 243 | dependency: transitive 244 | description: 245 | name: flare_dart 246 | url: "https://pub.dartlang.org" 247 | source: hosted 248 | version: "2.3.4" 249 | flare_flutter: 250 | dependency: "direct main" 251 | description: 252 | name: flare_flutter 253 | url: "https://pub.dartlang.org" 254 | source: hosted 255 | version: "2.0.6" 256 | flutter: 257 | dependency: "direct main" 258 | description: flutter 259 | source: sdk 260 | version: "0.0.0" 261 | flutter_auth_buttons: 262 | dependency: "direct main" 263 | description: 264 | name: flutter_auth_buttons 265 | url: "https://pub.dartlang.org" 266 | source: hosted 267 | version: "0.5.0" 268 | flutter_launcher_icons: 269 | dependency: "direct main" 270 | description: 271 | name: flutter_launcher_icons 272 | url: "https://pub.dartlang.org" 273 | source: hosted 274 | version: "0.7.5" 275 | flutter_spinkit: 276 | dependency: "direct main" 277 | description: 278 | name: flutter_spinkit 279 | url: "https://pub.dartlang.org" 280 | source: hosted 281 | version: "3.1.0" 282 | flutter_test: 283 | dependency: "direct dev" 284 | description: flutter 285 | source: sdk 286 | version: "0.0.0" 287 | flutter_web_plugins: 288 | dependency: transitive 289 | description: flutter 290 | source: sdk 291 | version: "0.0.0" 292 | google_fonts: 293 | dependency: "direct main" 294 | description: 295 | name: google_fonts 296 | url: "https://pub.dartlang.org" 297 | source: hosted 298 | version: "0.3.10" 299 | google_sign_in: 300 | dependency: "direct main" 301 | description: 302 | name: google_sign_in 303 | url: "https://pub.dartlang.org" 304 | source: hosted 305 | version: "4.5.3" 306 | google_sign_in_platform_interface: 307 | dependency: transitive 308 | description: 309 | name: google_sign_in_platform_interface 310 | url: "https://pub.dartlang.org" 311 | source: hosted 312 | version: "1.1.2" 313 | google_sign_in_web: 314 | dependency: transitive 315 | description: 316 | name: google_sign_in_web 317 | url: "https://pub.dartlang.org" 318 | source: hosted 319 | version: "0.9.1+1" 320 | http: 321 | dependency: transitive 322 | description: 323 | name: http 324 | url: "https://pub.dartlang.org" 325 | source: hosted 326 | version: "0.12.2" 327 | http_parser: 328 | dependency: transitive 329 | description: 330 | name: http_parser 331 | url: "https://pub.dartlang.org" 332 | source: hosted 333 | version: "3.1.4" 334 | image: 335 | dependency: transitive 336 | description: 337 | name: image 338 | url: "https://pub.dartlang.org" 339 | source: hosted 340 | version: "2.1.14" 341 | international_phone_input: 342 | dependency: "direct main" 343 | description: 344 | name: international_phone_input 345 | url: "https://pub.dartlang.org" 346 | source: hosted 347 | version: "1.0.4" 348 | intl: 349 | dependency: "direct main" 350 | description: 351 | name: intl 352 | url: "https://pub.dartlang.org" 353 | source: hosted 354 | version: "0.16.1" 355 | js: 356 | dependency: transitive 357 | description: 358 | name: js 359 | url: "https://pub.dartlang.org" 360 | source: hosted 361 | version: "0.6.2" 362 | libphonenumber: 363 | dependency: transitive 364 | description: 365 | name: libphonenumber 366 | url: "https://pub.dartlang.org" 367 | source: hosted 368 | version: "1.0.1" 369 | matcher: 370 | dependency: transitive 371 | description: 372 | name: matcher 373 | url: "https://pub.dartlang.org" 374 | source: hosted 375 | version: "0.12.8" 376 | meta: 377 | dependency: transitive 378 | description: 379 | name: meta 380 | url: "https://pub.dartlang.org" 381 | source: hosted 382 | version: "1.1.8" 383 | path: 384 | dependency: transitive 385 | description: 386 | name: path 387 | url: "https://pub.dartlang.org" 388 | source: hosted 389 | version: "1.7.0" 390 | path_provider: 391 | dependency: transitive 392 | description: 393 | name: path_provider 394 | url: "https://pub.dartlang.org" 395 | source: hosted 396 | version: "1.6.14" 397 | path_provider_linux: 398 | dependency: transitive 399 | description: 400 | name: path_provider_linux 401 | url: "https://pub.dartlang.org" 402 | source: hosted 403 | version: "0.0.1+2" 404 | path_provider_macos: 405 | dependency: transitive 406 | description: 407 | name: path_provider_macos 408 | url: "https://pub.dartlang.org" 409 | source: hosted 410 | version: "0.0.4+3" 411 | path_provider_platform_interface: 412 | dependency: transitive 413 | description: 414 | name: path_provider_platform_interface 415 | url: "https://pub.dartlang.org" 416 | source: hosted 417 | version: "1.0.3" 418 | pedantic: 419 | dependency: transitive 420 | description: 421 | name: pedantic 422 | url: "https://pub.dartlang.org" 423 | source: hosted 424 | version: "1.9.0" 425 | petitparser: 426 | dependency: transitive 427 | description: 428 | name: petitparser 429 | url: "https://pub.dartlang.org" 430 | source: hosted 431 | version: "3.0.4" 432 | platform: 433 | dependency: transitive 434 | description: 435 | name: platform 436 | url: "https://pub.dartlang.org" 437 | source: hosted 438 | version: "2.2.1" 439 | plugin_platform_interface: 440 | dependency: transitive 441 | description: 442 | name: plugin_platform_interface 443 | url: "https://pub.dartlang.org" 444 | source: hosted 445 | version: "1.0.2" 446 | process: 447 | dependency: transitive 448 | description: 449 | name: process 450 | url: "https://pub.dartlang.org" 451 | source: hosted 452 | version: "3.0.13" 453 | quiver: 454 | dependency: transitive 455 | description: 456 | name: quiver 457 | url: "https://pub.dartlang.org" 458 | source: hosted 459 | version: "2.1.3" 460 | sky_engine: 461 | dependency: transitive 462 | description: flutter 463 | source: sdk 464 | version: "0.0.99" 465 | source_span: 466 | dependency: transitive 467 | description: 468 | name: source_span 469 | url: "https://pub.dartlang.org" 470 | source: hosted 471 | version: "1.7.0" 472 | stack_trace: 473 | dependency: transitive 474 | description: 475 | name: stack_trace 476 | url: "https://pub.dartlang.org" 477 | source: hosted 478 | version: "1.9.5" 479 | stream_channel: 480 | dependency: transitive 481 | description: 482 | name: stream_channel 483 | url: "https://pub.dartlang.org" 484 | source: hosted 485 | version: "2.0.0" 486 | string_scanner: 487 | dependency: transitive 488 | description: 489 | name: string_scanner 490 | url: "https://pub.dartlang.org" 491 | source: hosted 492 | version: "1.0.5" 493 | term_glyph: 494 | dependency: transitive 495 | description: 496 | name: term_glyph 497 | url: "https://pub.dartlang.org" 498 | source: hosted 499 | version: "1.1.0" 500 | test_api: 501 | dependency: transitive 502 | description: 503 | name: test_api 504 | url: "https://pub.dartlang.org" 505 | source: hosted 506 | version: "0.2.17" 507 | typed_data: 508 | dependency: transitive 509 | description: 510 | name: typed_data 511 | url: "https://pub.dartlang.org" 512 | source: hosted 513 | version: "1.2.0" 514 | uuid: 515 | dependency: "direct main" 516 | description: 517 | name: uuid 518 | url: "https://pub.dartlang.org" 519 | source: hosted 520 | version: "2.0.1" 521 | vector_math: 522 | dependency: transitive 523 | description: 524 | name: vector_math 525 | url: "https://pub.dartlang.org" 526 | source: hosted 527 | version: "2.0.8" 528 | xdg_directories: 529 | dependency: transitive 530 | description: 531 | name: xdg_directories 532 | url: "https://pub.dartlang.org" 533 | source: hosted 534 | version: "0.1.0" 535 | xml: 536 | dependency: transitive 537 | description: 538 | name: xml 539 | url: "https://pub.dartlang.org" 540 | source: hosted 541 | version: "4.2.0" 542 | yaml: 543 | dependency: transitive 544 | description: 545 | name: yaml 546 | url: "https://pub.dartlang.org" 547 | source: hosted 548 | version: "2.2.1" 549 | sdks: 550 | dart: ">=2.9.0-14.0.dev <3.0.0" 551 | flutter: ">=1.12.13+hotfix.5 <2.0.0" 552 | --------------------------------------------------------------------------------