├── .gitignore ├── .metadata ├── LICENSE ├── README.md ├── SCREENSHOTS.md ├── android ├── .gitignore ├── app │ ├── build.gradle │ └── src │ │ └── main │ │ ├── AndroidManifest.xml │ │ ├── java │ │ └── com │ │ │ └── example │ │ │ └── flutterauthstarter │ │ │ └── MainActivity.java │ │ └── res │ │ ├── drawable │ │ └── launch_background.xml │ │ ├── mipmap-hdpi │ │ └── ic_launcher.png │ │ ├── mipmap-mdpi │ │ └── ic_launcher.png │ │ ├── mipmap-xhdpi │ │ └── ic_launcher.png │ │ ├── mipmap-xxhdpi │ │ └── ic_launcher.png │ │ ├── mipmap-xxxhdpi │ │ └── ic_launcher.png │ │ └── values │ │ └── styles.xml ├── build.gradle ├── gradle.properties ├── gradle │ └── wrapper │ │ ├── gradle-wrapper.jar │ │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat └── settings.gradle ├── assets └── icons │ ├── appIcon.jpg │ ├── profileIcon.png │ └── transparent.png ├── flutter_auth_starter.iml ├── flutter_auth_starter_android.iml ├── images ├── about-ios.png ├── changedisplayname-android.png ├── changeemail-android.png ├── changepassword-android.png ├── draweraccount-android.png ├── drawerinfo-android.png ├── linkaccounts-ios.png ├── linkemail-ios.png ├── profileaccount-ios.png ├── profileinfo-ios.png ├── resetpassword-android.png ├── resetpassword-ios.png ├── resetpasswordsent-android.png ├── signin-android.png ├── signin-ios.png ├── signup-android.png ├── splash.png └── termsaccept-ios.png ├── ios ├── .gitignore ├── Flutter │ ├── AppFrameworkInfo.plist │ ├── Debug.xcconfig │ └── Release.xcconfig ├── Podfile ├── Podfile.lock ├── Runner.xcodeproj │ ├── project.pbxproj │ ├── project.xcworkspace │ │ └── contents.xcworkspacedata │ └── xcshareddata │ │ └── xcschemes │ │ └── Runner.xcscheme ├── Runner.xcworkspace │ └── contents.xcworkspacedata └── Runner │ ├── AppDelegate.h │ ├── AppDelegate.m │ ├── Assets.xcassets │ ├── AppIcon.appiconset │ │ ├── Contents.json │ │ ├── Icon-App-1024x1024@1x.png │ │ ├── Icon-App-20x20@1x.png │ │ ├── Icon-App-20x20@2x.png │ │ ├── Icon-App-20x20@3x.png │ │ ├── Icon-App-29x29@1x.png │ │ ├── Icon-App-29x29@2x.png │ │ ├── Icon-App-29x29@3x.png │ │ ├── Icon-App-40x40@1x.png │ │ ├── Icon-App-40x40@2x.png │ │ ├── Icon-App-40x40@3x.png │ │ ├── Icon-App-60x60@2x.png │ │ ├── Icon-App-60x60@3x.png │ │ ├── Icon-App-76x76@1x.png │ │ ├── Icon-App-76x76@2x.png │ │ └── Icon-App-83.5x83.5@2x.png │ └── LaunchImage.imageset │ │ ├── Contents.json │ │ ├── LaunchImage.png │ │ ├── LaunchImage@2x.png │ │ ├── LaunchImage@3x.png │ │ └── README.md │ ├── Base.lproj │ ├── LaunchScreen.storyboard │ └── Main.storyboard │ ├── Info.plist │ └── main.m ├── lib ├── core │ ├── app_info.dart │ ├── app_model.dart │ ├── auth │ │ ├── handlers │ │ │ ├── email │ │ │ │ ├── change │ │ │ │ │ ├── change_email_page.dart │ │ │ │ │ ├── change_email_view_model.dart │ │ │ │ │ ├── change_password_page.dart │ │ │ │ │ └── change_password_view_model.dart │ │ │ │ ├── forgotPassword │ │ │ │ │ ├── forgot_password_page.dart │ │ │ │ │ └── forgot_password_view_model.dart │ │ │ │ ├── icon.dart │ │ │ │ ├── link │ │ │ │ │ ├── link_account.dart │ │ │ │ │ └── link_account_view_model.dart │ │ │ │ ├── signIn │ │ │ │ │ ├── sign_in_page.dart │ │ │ │ │ └── sign_in_view_model.dart │ │ │ │ ├── signUp │ │ │ │ │ ├── sign_up_page.dart │ │ │ │ │ └── sign_up_view_model.dart │ │ │ │ ├── sign_in_button.dart │ │ │ │ └── sign_up_button.dart │ │ │ ├── google │ │ │ │ ├── icon.dart │ │ │ │ └── sign_in_button.dart │ │ │ ├── link │ │ │ │ └── linkAccounts │ │ │ │ │ ├── link_accounts_page.dart │ │ │ │ │ ├── link_accounts_view_model.dart │ │ │ │ │ └── link_card.dart │ │ │ ├── signin_accounts │ │ │ │ ├── signin_picker.dart │ │ │ │ └── signin_picker_dialog.dart │ │ │ └── user │ │ │ │ ├── closeAccount │ │ │ │ └── close_account_page.dart │ │ │ │ ├── displayName │ │ │ │ ├── change_display_name_page.dart │ │ │ │ └── change_display_name_view_model.dart │ │ │ │ └── termsAcceptance │ │ │ │ └── terms_accept_modal.dart │ │ └── mock │ │ │ ├── mock_email_provider.dart │ │ │ ├── mock_google_provider.dart │ │ │ ├── mock_service.dart │ │ │ ├── mock_user.dart │ │ │ └── mock_user_account.dart │ ├── common │ │ ├── actionable.dart │ │ ├── app_exception.dart │ │ ├── dialog.dart │ │ ├── future_action_callback.dart │ │ ├── md5.dart │ │ └── throttle.dart │ ├── dialogs │ │ ├── app_info_dialog.dart │ │ ├── show_error_dialog.dart │ │ ├── show_info_dialog.dart │ │ └── show_ok_cancel_dialog.dart │ ├── imageProviders │ │ ├── combined_image_provider.dart │ │ ├── gravatar_provider.dart │ │ └── user_photo_url_provider.dart │ ├── pages │ │ ├── drawer_page.dart │ │ ├── license_page.dart │ │ ├── profile_base_state.dart │ │ ├── profile_page.dart │ │ └── splash_page.dart │ ├── validators │ │ ├── display_name_validator.dart │ │ ├── email_validator.dart │ │ ├── password_validator.dart │ │ ├── validate_if.dart │ │ ├── validator.dart │ │ └── words_match_validator.dart │ └── widgets │ │ ├── about_dialog.dart │ │ ├── email_image_circle_avatar.dart │ │ ├── form_progress_actionable_state.dart │ │ ├── header_button.dart │ │ ├── modalAppBar.dart │ │ ├── progress_actionable_state.dart │ │ ├── progressable_state.dart │ │ ├── screen_aware_padding.dart │ │ ├── screen_logo.dart │ │ ├── tablet_aware_layout_builder.dart │ │ ├── tablet_aware_scaffold.dart │ │ └── throttled_text_editing_controller.dart ├── main.dart ├── mock_auth_service.dart ├── routes.dart ├── src │ └── pages │ │ └── home_page.dart └── theme.dart ├── pubspec.lock └── pubspec.yaml /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .atom/ 3 | .idea 4 | .vscode/ 5 | .packages 6 | .pub/ 7 | build/ 8 | ios/.generated/ 9 | packages 10 | .flutter-plugins 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: 12bbaba9ae044d0ea77da4dd5e4db15eed403f09 8 | channel: beta 9 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Lance Johnstone 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 | -------------------------------------------------------------------------------- /SCREENSHOTS.md: -------------------------------------------------------------------------------- 1 | Instead of having all these images on the README file, I have decided to link to here to showcase what the starter project looks like... 2 | 3 | ![alt text](images/splash.png "Splash") 4 | 5 | Splash Screen 6 | 7 | ![alt text](images/signin-ios.png "Signin ios") 8 | 9 | Sign In - ios 10 | 11 | ![alt text](images/signin-android.png "Signin android") 12 | 13 | Sign In - android 14 | 15 | ![alt text](images/signup-android.png "Signup android") 16 | 17 | Sign up - android 18 | 19 | ![alt text](images/resetpassword-ios.png "Reset password ios") 20 | 21 | Reset password - ios 22 | 23 | ![alt text](images/resetpassword-android.png "Reset password android") 24 | 25 | Reset password - android 26 | 27 | ![alt text](images/resetpasswordsent-android.png "Reset password sent android") 28 | 29 | Reset password sent - android 30 | 31 | ![alt text](images/termsaccept-ios.png "Terms accept ios") 32 | 33 | Terms Acceptance - ios 34 | 35 | ![alt text](images/profileaccount-ios.png "Profile account ios") 36 | 37 | Profile account - ios 38 | 39 | ![alt text](images/profileinfo-ios.png "Profile info ios") 40 | 41 | Profile info - ios 42 | 43 | ![alt text](images/about-ios.png "About ios") 44 | 45 | About - ios 46 | 47 | ![alt text](images/linkaccounts-ios.png "Link Accounts ios") 48 | 49 | Link / Connect accounts - ios 50 | 51 | ![alt text](images/linkemail-ios.png "Link Email ios") 52 | 53 | Link email - ios 54 | 55 | ![alt text](images/drawerinfo-android.png "Drawer info android") 56 | 57 | Drawer info - android 58 | 59 | ![alt text](images/draweraccount-android.png "Drawer account android") 60 | 61 | Drawer account - android 62 | 63 | ![alt text](images/changedisplayname-android.png "Change display name android") 64 | 65 | Change display name - android 66 | 67 | ![alt text](images/changeemail-android.png "Change email android") 68 | 69 | Change email - android 70 | 71 | ![alt text](images/changepassword-android.png "Change password android") 72 | 73 | Change password - android 74 | -------------------------------------------------------------------------------- /android/.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | *.class 3 | .gradle 4 | /local.properties 5 | /.idea/workspace.xml 6 | /.idea/libraries 7 | .DS_Store 8 | /build 9 | /captures 10 | GeneratedPluginRegistrant.java 11 | -------------------------------------------------------------------------------- /android/app/build.gradle: -------------------------------------------------------------------------------- 1 | def localProperties = new Properties() 2 | def localPropertiesFile = rootProject.file('local.properties') 3 | if (localPropertiesFile.exists()) { 4 | localPropertiesFile.withReader('UTF-8') { reader -> 5 | localProperties.load(reader) 6 | } 7 | } 8 | 9 | def flutterRoot = localProperties.getProperty('flutter.sdk') 10 | if (flutterRoot == null) { 11 | throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.") 12 | } 13 | 14 | apply plugin: 'com.android.application' 15 | apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" 16 | 17 | android { 18 | compileSdkVersion 27 19 | 20 | lintOptions { 21 | disable 'InvalidPackage' 22 | } 23 | 24 | defaultConfig { 25 | // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). 26 | applicationId "com.example.flutterauthstarter" 27 | minSdkVersion 16 28 | targetSdkVersion 27 29 | versionCode 1 30 | versionName "1.0" 31 | testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" 32 | } 33 | 34 | buildTypes { 35 | release { 36 | // TODO: Add your own signing config for the release build. 37 | // Signing with the debug keys for now, so `flutter run --release` works. 38 | signingConfig signingConfigs.debug 39 | } 40 | } 41 | } 42 | 43 | flutter { 44 | source '../..' 45 | } 46 | 47 | dependencies { 48 | testImplementation 'junit:junit:4.12' 49 | androidTestImplementation 'com.android.support.test:runner:1.0.1' 50 | androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.1' 51 | } 52 | -------------------------------------------------------------------------------- /android/app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 8 | 9 | 10 | 15 | 19 | 26 | 30 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /android/app/src/main/java/com/example/flutterauthstarter/MainActivity.java: -------------------------------------------------------------------------------- 1 | package com.example.flutterauthstarter; 2 | 3 | import android.os.Bundle; 4 | 5 | import io.flutter.app.FlutterActivity; 6 | import io.flutter.plugins.GeneratedPluginRegistrant; 7 | 8 | public class MainActivity extends FlutterActivity { 9 | @Override 10 | protected void onCreate(Bundle savedInstanceState) { 11 | super.onCreate(savedInstanceState); 12 | GeneratedPluginRegistrant.registerWith(this); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /android/app/src/main/res/drawable/launch_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 12 | 13 | -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aqwert/flutter_auth_starter/993146f4d2ae2472bacaa6e26ad97fab04fba415/android/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aqwert/flutter_auth_starter/993146f4d2ae2472bacaa6e26ad97fab04fba415/android/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aqwert/flutter_auth_starter/993146f4d2ae2472bacaa6e26ad97fab04fba415/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aqwert/flutter_auth_starter/993146f4d2ae2472bacaa6e26ad97fab04fba415/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aqwert/flutter_auth_starter/993146f4d2ae2472bacaa6e26ad97fab04fba415/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | 9 | -------------------------------------------------------------------------------- /android/build.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | repositories { 3 | google() 4 | jcenter() 5 | } 6 | 7 | dependencies { 8 | classpath 'com.android.tools.build:gradle:3.0.1' 9 | } 10 | } 11 | 12 | allprojects { 13 | repositories { 14 | google() 15 | jcenter() 16 | } 17 | } 18 | 19 | rootProject.buildDir = '../build' 20 | subprojects { 21 | project.buildDir = "${rootProject.buildDir}/${project.name}" 22 | } 23 | subprojects { 24 | project.evaluationDependsOn(':app') 25 | } 26 | 27 | task clean(type: Delete) { 28 | delete rootProject.buildDir 29 | } 30 | -------------------------------------------------------------------------------- /android/gradle.properties: -------------------------------------------------------------------------------- 1 | org.gradle.jvmargs=-Xmx1536M 2 | -------------------------------------------------------------------------------- /android/gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aqwert/flutter_auth_starter/993146f4d2ae2472bacaa6e26ad97fab04fba415/android/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /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-4.1-all.zip 7 | -------------------------------------------------------------------------------- /android/gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 10 | DEFAULT_JVM_OPTS="" 11 | 12 | APP_NAME="Gradle" 13 | APP_BASE_NAME=`basename "$0"` 14 | 15 | # Use the maximum available, or set MAX_FD != -1 to use that value. 16 | MAX_FD="maximum" 17 | 18 | warn ( ) { 19 | echo "$*" 20 | } 21 | 22 | die ( ) { 23 | echo 24 | echo "$*" 25 | echo 26 | exit 1 27 | } 28 | 29 | # OS specific support (must be 'true' or 'false'). 30 | cygwin=false 31 | msys=false 32 | darwin=false 33 | case "`uname`" in 34 | CYGWIN* ) 35 | cygwin=true 36 | ;; 37 | Darwin* ) 38 | darwin=true 39 | ;; 40 | MINGW* ) 41 | msys=true 42 | ;; 43 | esac 44 | 45 | # Attempt to set APP_HOME 46 | # Resolve links: $0 may be a link 47 | PRG="$0" 48 | # Need this for relative symlinks. 49 | while [ -h "$PRG" ] ; do 50 | ls=`ls -ld "$PRG"` 51 | link=`expr "$ls" : '.*-> \(.*\)$'` 52 | if expr "$link" : '/.*' > /dev/null; then 53 | PRG="$link" 54 | else 55 | PRG=`dirname "$PRG"`"/$link" 56 | fi 57 | done 58 | SAVED="`pwd`" 59 | cd "`dirname \"$PRG\"`/" >/dev/null 60 | APP_HOME="`pwd -P`" 61 | cd "$SAVED" >/dev/null 62 | 63 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 64 | 65 | # Determine the Java command to use to start the JVM. 66 | if [ -n "$JAVA_HOME" ] ; then 67 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 68 | # IBM's JDK on AIX uses strange locations for the executables 69 | JAVACMD="$JAVA_HOME/jre/sh/java" 70 | else 71 | JAVACMD="$JAVA_HOME/bin/java" 72 | fi 73 | if [ ! -x "$JAVACMD" ] ; then 74 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 75 | 76 | Please set the JAVA_HOME variable in your environment to match the 77 | location of your Java installation." 78 | fi 79 | else 80 | JAVACMD="java" 81 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 82 | 83 | Please set the JAVA_HOME variable in your environment to match the 84 | location of your Java installation." 85 | fi 86 | 87 | # Increase the maximum file descriptors if we can. 88 | if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then 89 | MAX_FD_LIMIT=`ulimit -H -n` 90 | if [ $? -eq 0 ] ; then 91 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 92 | MAX_FD="$MAX_FD_LIMIT" 93 | fi 94 | ulimit -n $MAX_FD 95 | if [ $? -ne 0 ] ; then 96 | warn "Could not set maximum file descriptor limit: $MAX_FD" 97 | fi 98 | else 99 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 100 | fi 101 | fi 102 | 103 | # For Darwin, add options to specify how the application appears in the dock 104 | if $darwin; then 105 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 106 | fi 107 | 108 | # For Cygwin, switch paths to Windows format before running java 109 | if $cygwin ; then 110 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 111 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 112 | JAVACMD=`cygpath --unix "$JAVACMD"` 113 | 114 | # We build the pattern for arguments to be converted via cygpath 115 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 116 | SEP="" 117 | for dir in $ROOTDIRSRAW ; do 118 | ROOTDIRS="$ROOTDIRS$SEP$dir" 119 | SEP="|" 120 | done 121 | OURCYGPATTERN="(^($ROOTDIRS))" 122 | # Add a user-defined pattern to the cygpath arguments 123 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 124 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 125 | fi 126 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 127 | i=0 128 | for arg in "$@" ; do 129 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 130 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 131 | 132 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 133 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 134 | else 135 | eval `echo args$i`="\"$arg\"" 136 | fi 137 | i=$((i+1)) 138 | done 139 | case $i in 140 | (0) set -- ;; 141 | (1) set -- "$args0" ;; 142 | (2) set -- "$args0" "$args1" ;; 143 | (3) set -- "$args0" "$args1" "$args2" ;; 144 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 145 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 146 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 147 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 148 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 149 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 150 | esac 151 | fi 152 | 153 | # Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules 154 | function splitJvmOpts() { 155 | JVM_OPTS=("$@") 156 | } 157 | eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS 158 | JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" 159 | 160 | exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" 161 | -------------------------------------------------------------------------------- /android/gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 12 | set DEFAULT_JVM_OPTS= 13 | 14 | set DIRNAME=%~dp0 15 | if "%DIRNAME%" == "" set DIRNAME=. 16 | set APP_BASE_NAME=%~n0 17 | set APP_HOME=%DIRNAME% 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windowz variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | if "%@eval[2+2]" == "4" goto 4NT_args 53 | 54 | :win9xME_args 55 | @rem Slurp the command line arguments. 56 | set CMD_LINE_ARGS= 57 | set _SKIP=2 58 | 59 | :win9xME_args_slurp 60 | if "x%~1" == "x" goto execute 61 | 62 | set CMD_LINE_ARGS=%* 63 | goto execute 64 | 65 | :4NT_args 66 | @rem Get arguments from the 4NT Shell from JP Software 67 | set CMD_LINE_ARGS=%$ 68 | 69 | :execute 70 | @rem Setup the command line 71 | 72 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 73 | 74 | @rem Execute Gradle 75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 76 | 77 | :end 78 | @rem End local scope for the variables with windows NT shell 79 | if "%ERRORLEVEL%"=="0" goto mainEnd 80 | 81 | :fail 82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 83 | rem the _cmd.exe /c_ return code! 84 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 85 | exit /b 1 86 | 87 | :mainEnd 88 | if "%OS%"=="Windows_NT" endlocal 89 | 90 | :omega 91 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /assets/icons/appIcon.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aqwert/flutter_auth_starter/993146f4d2ae2472bacaa6e26ad97fab04fba415/assets/icons/appIcon.jpg -------------------------------------------------------------------------------- /assets/icons/profileIcon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aqwert/flutter_auth_starter/993146f4d2ae2472bacaa6e26ad97fab04fba415/assets/icons/profileIcon.png -------------------------------------------------------------------------------- /assets/icons/transparent.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aqwert/flutter_auth_starter/993146f4d2ae2472bacaa6e26ad97fab04fba415/assets/icons/transparent.png -------------------------------------------------------------------------------- /flutter_auth_starter.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /flutter_auth_starter_android.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /images/about-ios.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aqwert/flutter_auth_starter/993146f4d2ae2472bacaa6e26ad97fab04fba415/images/about-ios.png -------------------------------------------------------------------------------- /images/changedisplayname-android.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aqwert/flutter_auth_starter/993146f4d2ae2472bacaa6e26ad97fab04fba415/images/changedisplayname-android.png -------------------------------------------------------------------------------- /images/changeemail-android.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aqwert/flutter_auth_starter/993146f4d2ae2472bacaa6e26ad97fab04fba415/images/changeemail-android.png -------------------------------------------------------------------------------- /images/changepassword-android.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aqwert/flutter_auth_starter/993146f4d2ae2472bacaa6e26ad97fab04fba415/images/changepassword-android.png -------------------------------------------------------------------------------- /images/draweraccount-android.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aqwert/flutter_auth_starter/993146f4d2ae2472bacaa6e26ad97fab04fba415/images/draweraccount-android.png -------------------------------------------------------------------------------- /images/drawerinfo-android.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aqwert/flutter_auth_starter/993146f4d2ae2472bacaa6e26ad97fab04fba415/images/drawerinfo-android.png -------------------------------------------------------------------------------- /images/linkaccounts-ios.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aqwert/flutter_auth_starter/993146f4d2ae2472bacaa6e26ad97fab04fba415/images/linkaccounts-ios.png -------------------------------------------------------------------------------- /images/linkemail-ios.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aqwert/flutter_auth_starter/993146f4d2ae2472bacaa6e26ad97fab04fba415/images/linkemail-ios.png -------------------------------------------------------------------------------- /images/profileaccount-ios.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aqwert/flutter_auth_starter/993146f4d2ae2472bacaa6e26ad97fab04fba415/images/profileaccount-ios.png -------------------------------------------------------------------------------- /images/profileinfo-ios.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aqwert/flutter_auth_starter/993146f4d2ae2472bacaa6e26ad97fab04fba415/images/profileinfo-ios.png -------------------------------------------------------------------------------- /images/resetpassword-android.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aqwert/flutter_auth_starter/993146f4d2ae2472bacaa6e26ad97fab04fba415/images/resetpassword-android.png -------------------------------------------------------------------------------- /images/resetpassword-ios.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aqwert/flutter_auth_starter/993146f4d2ae2472bacaa6e26ad97fab04fba415/images/resetpassword-ios.png -------------------------------------------------------------------------------- /images/resetpasswordsent-android.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aqwert/flutter_auth_starter/993146f4d2ae2472bacaa6e26ad97fab04fba415/images/resetpasswordsent-android.png -------------------------------------------------------------------------------- /images/signin-android.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aqwert/flutter_auth_starter/993146f4d2ae2472bacaa6e26ad97fab04fba415/images/signin-android.png -------------------------------------------------------------------------------- /images/signin-ios.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aqwert/flutter_auth_starter/993146f4d2ae2472bacaa6e26ad97fab04fba415/images/signin-ios.png -------------------------------------------------------------------------------- /images/signup-android.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aqwert/flutter_auth_starter/993146f4d2ae2472bacaa6e26ad97fab04fba415/images/signup-android.png -------------------------------------------------------------------------------- /images/splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aqwert/flutter_auth_starter/993146f4d2ae2472bacaa6e26ad97fab04fba415/images/splash.png -------------------------------------------------------------------------------- /images/termsaccept-ios.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aqwert/flutter_auth_starter/993146f4d2ae2472bacaa6e26ad97fab04fba415/images/termsaccept-ios.png -------------------------------------------------------------------------------- /ios/.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | .vagrant/ 3 | .sconsign.dblite 4 | .svn/ 5 | 6 | .DS_Store 7 | *.swp 8 | profile 9 | 10 | DerivedData/ 11 | build/ 12 | GeneratedPluginRegistrant.h 13 | GeneratedPluginRegistrant.m 14 | 15 | .generated/ 16 | 17 | *.pbxuser 18 | *.mode1v3 19 | *.mode2v3 20 | *.perspectivev3 21 | 22 | !default.pbxuser 23 | !default.mode1v3 24 | !default.mode2v3 25 | !default.perspectivev3 26 | 27 | xcuserdata 28 | 29 | *.moved-aside 30 | 31 | *.pyc 32 | *sync/ 33 | Icon? 34 | .tags* 35 | 36 | /Flutter/app.flx 37 | /Flutter/app.zip 38 | /Flutter/flutter_assets/ 39 | /Flutter/App.framework 40 | /Flutter/Flutter.framework 41 | /Flutter/Generated.xcconfig 42 | /ServiceDefinitions.json 43 | 44 | Pods/ 45 | -------------------------------------------------------------------------------- /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 | UIRequiredDeviceCapabilities 24 | 25 | arm64 26 | 27 | MinimumOSVersion 28 | 8.0 29 | 30 | 31 | -------------------------------------------------------------------------------- /ios/Flutter/Debug.xcconfig: -------------------------------------------------------------------------------- 1 | #include "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" 2 | #include "Generated.xcconfig" 3 | -------------------------------------------------------------------------------- /ios/Flutter/Release.xcconfig: -------------------------------------------------------------------------------- 1 | #include "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" 2 | #include "Generated.xcconfig" 3 | -------------------------------------------------------------------------------- /ios/Podfile: -------------------------------------------------------------------------------- 1 | # Uncomment this line to define a global platform for your project 2 | # platform :ios, '9.0' 3 | 4 | # CocoaPods analytics sends network stats synchronously affecting flutter build latency. 5 | ENV['COCOAPODS_DISABLE_STATS'] = 'true' 6 | 7 | def parse_KV_file(file, separator='=') 8 | file_abs_path = File.expand_path(file) 9 | if !File.exists? file_abs_path 10 | return []; 11 | end 12 | pods_ary = [] 13 | skip_line_start_symbols = ["#", "/"] 14 | File.foreach(file_abs_path) { |line| 15 | next if skip_line_start_symbols.any? { |symbol| line =~ /^\s*#{symbol}/ } 16 | plugin = line.split(pattern=separator) 17 | if plugin.length == 2 18 | podname = plugin[0].strip() 19 | path = plugin[1].strip() 20 | podpath = File.expand_path("#{path}", file_abs_path) 21 | pods_ary.push({:name => podname, :path => podpath}); 22 | else 23 | puts "Invalid plugin specification: #{line}" 24 | end 25 | } 26 | return pods_ary 27 | end 28 | 29 | target 'Runner' do 30 | # Prepare symlinks folder. We use symlinks to avoid having Podfile.lock 31 | # referring to absolute paths on developers' machines. 32 | system('rm -rf Pods/.symlinks') 33 | system('mkdir -p Pods/.symlinks/plugins') 34 | 35 | # Flutter Pods 36 | generated_xcode_build_settings = parse_KV_file('./Flutter/Generated.xcconfig') 37 | if generated_xcode_build_settings.empty? 38 | puts "Generated.xcconfig must exist. If you're running pod install manually, make sure flutter packages get is executed first." 39 | end 40 | generated_xcode_build_settings.map { |p| 41 | if p[:name] == 'FLUTTER_FRAMEWORK_DIR' 42 | symlink = File.join('Pods', '.symlinks', 'flutter') 43 | File.symlink(File.dirname(p[:path]), symlink) 44 | pod 'Flutter', :path => File.join(symlink, File.basename(p[:path])) 45 | end 46 | } 47 | 48 | # Plugin Pods 49 | plugin_pods = parse_KV_file('../.flutter-plugins') 50 | plugin_pods.map { |p| 51 | symlink = File.join('Pods', '.symlinks', 'plugins', p[:name]) 52 | File.symlink(p[:path], symlink) 53 | pod p[:name], :path => File.join(symlink, 'ios') 54 | } 55 | end 56 | 57 | post_install do |installer| 58 | installer.pods_project.targets.each do |target| 59 | target.build_configurations.each do |config| 60 | config.build_settings['ENABLE_BITCODE'] = 'NO' 61 | end 62 | end 63 | end 64 | -------------------------------------------------------------------------------- /ios/Podfile.lock: -------------------------------------------------------------------------------- 1 | PODS: 2 | - Flutter (1.0.0) 3 | - url_launcher (0.0.1): 4 | - Flutter 5 | 6 | DEPENDENCIES: 7 | - Flutter (from `Pods/.symlinks/flutter/ios`) 8 | - url_launcher (from `Pods/.symlinks/plugins/url_launcher/ios`) 9 | 10 | EXTERNAL SOURCES: 11 | Flutter: 12 | :path: Pods/.symlinks/flutter/ios 13 | url_launcher: 14 | :path: Pods/.symlinks/plugins/url_launcher/ios 15 | 16 | SPEC CHECKSUMS: 17 | Flutter: 9d0fac939486c9aba2809b7982dfdbb47a7b0296 18 | url_launcher: 92b89c1029a0373879933c21642958c874539095 19 | 20 | PODFILE CHECKSUM: 13dcf421f4da2e937a57e8ba760ed880beae536f 21 | 22 | COCOAPODS: 1.4.0 23 | -------------------------------------------------------------------------------- /ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /ios/Runner.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /ios/Runner/AppDelegate.h: -------------------------------------------------------------------------------- 1 | #import 2 | #import 3 | 4 | @interface AppDelegate : FlutterAppDelegate 5 | 6 | @end 7 | -------------------------------------------------------------------------------- /ios/Runner/AppDelegate.m: -------------------------------------------------------------------------------- 1 | #include "AppDelegate.h" 2 | #include "GeneratedPluginRegistrant.h" 3 | 4 | @implementation AppDelegate 5 | 6 | - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { 7 | [GeneratedPluginRegistrant registerWithRegistry:self]; 8 | // Override point for customization after application launch. 9 | return [super application:application didFinishLaunchingWithOptions:launchOptions]; 10 | } 11 | 12 | @end 13 | -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "size" : "20x20", 5 | "idiom" : "iphone", 6 | "filename" : "Icon-App-20x20@2x.png", 7 | "scale" : "2x" 8 | }, 9 | { 10 | "size" : "20x20", 11 | "idiom" : "iphone", 12 | "filename" : "Icon-App-20x20@3x.png", 13 | "scale" : "3x" 14 | }, 15 | { 16 | "size" : "29x29", 17 | "idiom" : "iphone", 18 | "filename" : "Icon-App-29x29@1x.png", 19 | "scale" : "1x" 20 | }, 21 | { 22 | "size" : "29x29", 23 | "idiom" : "iphone", 24 | "filename" : "Icon-App-29x29@2x.png", 25 | "scale" : "2x" 26 | }, 27 | { 28 | "size" : "29x29", 29 | "idiom" : "iphone", 30 | "filename" : "Icon-App-29x29@3x.png", 31 | "scale" : "3x" 32 | }, 33 | { 34 | "size" : "40x40", 35 | "idiom" : "iphone", 36 | "filename" : "Icon-App-40x40@2x.png", 37 | "scale" : "2x" 38 | }, 39 | { 40 | "size" : "40x40", 41 | "idiom" : "iphone", 42 | "filename" : "Icon-App-40x40@3x.png", 43 | "scale" : "3x" 44 | }, 45 | { 46 | "size" : "60x60", 47 | "idiom" : "iphone", 48 | "filename" : "Icon-App-60x60@2x.png", 49 | "scale" : "2x" 50 | }, 51 | { 52 | "size" : "60x60", 53 | "idiom" : "iphone", 54 | "filename" : "Icon-App-60x60@3x.png", 55 | "scale" : "3x" 56 | }, 57 | { 58 | "size" : "20x20", 59 | "idiom" : "ipad", 60 | "filename" : "Icon-App-20x20@1x.png", 61 | "scale" : "1x" 62 | }, 63 | { 64 | "size" : "20x20", 65 | "idiom" : "ipad", 66 | "filename" : "Icon-App-20x20@2x.png", 67 | "scale" : "2x" 68 | }, 69 | { 70 | "size" : "29x29", 71 | "idiom" : "ipad", 72 | "filename" : "Icon-App-29x29@1x.png", 73 | "scale" : "1x" 74 | }, 75 | { 76 | "size" : "29x29", 77 | "idiom" : "ipad", 78 | "filename" : "Icon-App-29x29@2x.png", 79 | "scale" : "2x" 80 | }, 81 | { 82 | "size" : "40x40", 83 | "idiom" : "ipad", 84 | "filename" : "Icon-App-40x40@1x.png", 85 | "scale" : "1x" 86 | }, 87 | { 88 | "size" : "40x40", 89 | "idiom" : "ipad", 90 | "filename" : "Icon-App-40x40@2x.png", 91 | "scale" : "2x" 92 | }, 93 | { 94 | "size" : "76x76", 95 | "idiom" : "ipad", 96 | "filename" : "Icon-App-76x76@1x.png", 97 | "scale" : "1x" 98 | }, 99 | { 100 | "size" : "76x76", 101 | "idiom" : "ipad", 102 | "filename" : "Icon-App-76x76@2x.png", 103 | "scale" : "2x" 104 | }, 105 | { 106 | "size" : "83.5x83.5", 107 | "idiom" : "ipad", 108 | "filename" : "Icon-App-83.5x83.5@2x.png", 109 | "scale" : "2x" 110 | }, 111 | { 112 | "size" : "1024x1024", 113 | "idiom" : "ios-marketing", 114 | "filename" : "Icon-App-1024x1024@1x.png", 115 | "scale" : "1x" 116 | } 117 | ], 118 | "info" : { 119 | "version" : 1, 120 | "author" : "xcode" 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aqwert/flutter_auth_starter/993146f4d2ae2472bacaa6e26ad97fab04fba415/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aqwert/flutter_auth_starter/993146f4d2ae2472bacaa6e26ad97fab04fba415/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/aqwert/flutter_auth_starter/993146f4d2ae2472bacaa6e26ad97fab04fba415/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/aqwert/flutter_auth_starter/993146f4d2ae2472bacaa6e26ad97fab04fba415/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/aqwert/flutter_auth_starter/993146f4d2ae2472bacaa6e26ad97fab04fba415/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/aqwert/flutter_auth_starter/993146f4d2ae2472bacaa6e26ad97fab04fba415/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/aqwert/flutter_auth_starter/993146f4d2ae2472bacaa6e26ad97fab04fba415/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/aqwert/flutter_auth_starter/993146f4d2ae2472bacaa6e26ad97fab04fba415/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/aqwert/flutter_auth_starter/993146f4d2ae2472bacaa6e26ad97fab04fba415/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/aqwert/flutter_auth_starter/993146f4d2ae2472bacaa6e26ad97fab04fba415/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aqwert/flutter_auth_starter/993146f4d2ae2472bacaa6e26ad97fab04fba415/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/aqwert/flutter_auth_starter/993146f4d2ae2472bacaa6e26ad97fab04fba415/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aqwert/flutter_auth_starter/993146f4d2ae2472bacaa6e26ad97fab04fba415/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/aqwert/flutter_auth_starter/993146f4d2ae2472bacaa6e26ad97fab04fba415/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aqwert/flutter_auth_starter/993146f4d2ae2472bacaa6e26ad97fab04fba415/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "LaunchImage.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "filename" : "LaunchImage@2x.png", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "filename" : "LaunchImage@3x.png", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "version" : 1, 21 | "author" : "xcode" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aqwert/flutter_auth_starter/993146f4d2ae2472bacaa6e26ad97fab04fba415/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aqwert/flutter_auth_starter/993146f4d2ae2472bacaa6e26ad97fab04fba415/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aqwert/flutter_auth_starter/993146f4d2ae2472bacaa6e26ad97fab04fba415/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md: -------------------------------------------------------------------------------- 1 | # Launch Screen Assets 2 | 3 | You can customize the launch screen with your own desired assets by replacing the image files in this directory. 4 | 5 | You can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images. -------------------------------------------------------------------------------- /ios/Runner/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /ios/Runner/Base.lproj/Main.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /ios/Runner/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | flutter_auth_starter 15 | CFBundlePackageType 16 | APPL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1 23 | LSRequiresIPhoneOS 24 | 25 | UILaunchStoryboardName 26 | LaunchScreen 27 | UIMainStoryboardFile 28 | Main 29 | UIRequiredDeviceCapabilities 30 | 31 | arm64 32 | 33 | UISupportedInterfaceOrientations 34 | 35 | UIInterfaceOrientationPortrait 36 | UIInterfaceOrientationLandscapeLeft 37 | UIInterfaceOrientationLandscapeRight 38 | 39 | UISupportedInterfaceOrientations~ipad 40 | 41 | UIInterfaceOrientationPortrait 42 | UIInterfaceOrientationPortraitUpsideDown 43 | UIInterfaceOrientationLandscapeLeft 44 | UIInterfaceOrientationLandscapeRight 45 | 46 | UIViewControllerBasedStatusBarAppearance 47 | 48 | 49 | 50 | -------------------------------------------------------------------------------- /ios/Runner/main.m: -------------------------------------------------------------------------------- 1 | #import 2 | #import 3 | #import "AppDelegate.h" 4 | 5 | int main(int argc, char * argv[]) { 6 | @autoreleasepool { 7 | return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class])); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /lib/core/app_info.dart: -------------------------------------------------------------------------------- 1 | import 'package:meta/meta.dart'; 2 | 3 | class AppInfo { 4 | AppInfo( 5 | {@required this.appName, 6 | @required this.appVersion, 7 | @required this.appIconPath, 8 | @required this.avatarDefaultAppIconPath, 9 | this.termsOfServiceUrl, 10 | this.privacyPolicyUrl, 11 | this.applicationLegalese = ''}); 12 | 13 | final String appName; 14 | final String appVersion; 15 | final String applicationLegalese; 16 | 17 | final String privacyPolicyUrl; 18 | final String termsOfServiceUrl; 19 | 20 | final String appIconPath; 21 | final String avatarDefaultAppIconPath; 22 | } 23 | -------------------------------------------------------------------------------- /lib/core/app_model.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | import 'package:flutter_auth_base/flutter_auth_base.dart'; 4 | import 'package:meta/meta.dart'; 5 | import 'package:scoped_model/scoped_model.dart'; 6 | 7 | import 'app_info.dart'; 8 | 9 | class AuthUserState { 10 | AuthUserState({this.user, this.hasChanged}); 11 | 12 | final AuthUser user; 13 | final bool hasChanged; 14 | bool get isValidUser => user != null && user.isValid; 15 | } 16 | 17 | class AppModel extends Model { 18 | AppModel({@required this.authService, @required this.appInfo}); 19 | 20 | AuthUser _user; 21 | String _authToken; 22 | 23 | AuthUser get user => _user; 24 | String get token => _authToken; 25 | 26 | final AppInfo appInfo; 27 | final AuthService authService; 28 | 29 | Future refreshAuthUser() async { 30 | var prevUser = _user; 31 | 32 | _user = await authService.currentUser(); 33 | _authToken = await authService.currentUserToken(); 34 | 35 | bool changed = false; 36 | if (prevUser == null) { 37 | if (_user != null) { 38 | changed = true; 39 | } 40 | } else { 41 | if (_user != null) { 42 | if (prevUser.isValid != _user.isValid) { 43 | changed = true; 44 | } else { 45 | if (prevUser.uid != _user.uid) { 46 | changed = true; 47 | } 48 | } 49 | } else { 50 | changed = true; 51 | } 52 | } 53 | 54 | notifyListeners(); 55 | 56 | return new AuthUserState(user: _user, hasChanged: changed); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /lib/core/auth/handlers/email/change/change_email_page.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:flutter_auth_base/flutter_auth_base.dart'; 4 | import 'package:scoped_model/scoped_model.dart'; 5 | import 'package:flutter_platform_widgets/flutter_platform_widgets.dart'; 6 | 7 | import '../../../../widgets/form_progress_actionable_state.dart'; 8 | import '../../../../app_model.dart'; 9 | import 'change_email_view_model.dart'; 10 | import '../icon.dart'; 11 | import '../../../../widgets/modalAppBar.dart'; 12 | 13 | class ChangeEmail extends StatefulWidget { 14 | @override 15 | createState() => new ChangeEmailState(); 16 | } 17 | 18 | class ChangeEmailState extends FormProgressActionableState { 19 | @override 20 | void initState() { 21 | super.initState(); 22 | 23 | _viewModel = new ViewModel(); 24 | } 25 | 26 | ViewModel _viewModel; 27 | 28 | AuthProvider _getPasswordProvider(AuthService authService) { 29 | return authService.authProviders.firstWhere( 30 | (prov) => prov.providerName == 'password', 31 | orElse: () => null); 32 | } 33 | 34 | Future _changeEmail(AuthService authService) async { 35 | var provider = _getPasswordProvider(authService); 36 | 37 | var user = await authService.currentUser(); 38 | await provider?.changePrimaryIdentifier( 39 | { 40 | 'currentEmail': user.email, 41 | 'newEmail': _viewModel.email, 42 | 'password': _viewModel.password 43 | }, 44 | ); 45 | 46 | Navigator.pop(context); 47 | } 48 | 49 | Widget _emailField() { 50 | return ListTile( 51 | leading: Icon( 52 | providerIcon, 53 | ), 54 | title: TextFormField( 55 | enabled: !super.showProgress, 56 | keyboardType: TextInputType.emailAddress, 57 | autocorrect: false, 58 | decoration: InputDecoration(labelText: 'New Email'), 59 | validator: _viewModel.validateEmail, 60 | onSaved: (val) => _viewModel.email = val), 61 | ); 62 | } 63 | 64 | Widget _passwordField() { 65 | return ListTile( 66 | leading: Icon( 67 | Icons.lock, 68 | ), 69 | title: TextFormField( 70 | enabled: !super.showProgress, 71 | autocorrect: false, 72 | obscureText: true, 73 | decoration: InputDecoration(labelText: 'Current Password'), 74 | validator: _viewModel.validatePassword, 75 | onSaved: (val) => _viewModel.password = val), 76 | ); 77 | } 78 | 79 | Widget _progressIndicator() { 80 | return super.showProgress 81 | ? Padding( 82 | padding: EdgeInsets.all(16.0), 83 | child: PlatformCircularProgressIndicator()) 84 | : Container(); 85 | } 86 | 87 | Widget _build() { 88 | return SingleChildScrollView( 89 | child: Column( 90 | children: [ 91 | Padding( 92 | padding: EdgeInsets.all(16.0), 93 | child: Text( 94 | 'Please enter you current password to change your primary email address'), 95 | ), 96 | _passwordField(), 97 | Padding( 98 | padding: EdgeInsets.symmetric(vertical: 8.0), 99 | child: Divider(), 100 | ), 101 | _emailField(), 102 | _progressIndicator(), 103 | ], 104 | ), 105 | ); 106 | } 107 | 108 | Form _asForm(Widget widget) { 109 | return Form(autovalidate: true, key: super.formKey, child: widget); 110 | } 111 | 112 | @override 113 | Widget build(BuildContext context) { 114 | return ScopedModelDescendant( 115 | rebuildOnChange: false, 116 | builder: (_, child, model) => PlatformScaffold( 117 | appBar: ModalAppBar( 118 | title: Text('Change Email'), 119 | acceptText: 'Apply', 120 | acceptAction: super.showProgress 121 | ? null 122 | : () => super.validateAndSubmit( 123 | (_) async => await _changeEmail(model.authService), 124 | ), 125 | closeAction: 126 | super.showProgress ? null : () => Navigator.maybePop(context), 127 | ), 128 | body: Padding( 129 | padding: const EdgeInsets.symmetric(horizontal: 16.0), 130 | child: Material( 131 | color: isMaterial ? null : Theme.of(context).cardColor, 132 | child: _asForm( 133 | _build(), 134 | ), 135 | ), 136 | ), 137 | ), 138 | ); 139 | } 140 | } 141 | -------------------------------------------------------------------------------- /lib/core/auth/handlers/email/change/change_email_view_model.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_auth_base/flutter_auth_base.dart'; 2 | 3 | import '../../../../validators/validator.dart'; 4 | import '../../../../validators/email_validator.dart' as emailValidator; 5 | import '../../../../validators/password_validator.dart' as passwordValidator; 6 | 7 | import '../../../../common/app_exception.dart'; 8 | import '../../../../validators/validate_if.dart'; 9 | 10 | class ViewModel { 11 | ViewModel() { 12 | _validator = new Validator(); 13 | _validator.validations.add(() => validateEmail(email)); 14 | _validator.validations.add(() => validatePassword(password)); 15 | } 16 | 17 | //only want to validate field if either there is some text or 18 | // the submit button clicked and the field is empty 19 | bool emptyTextValidation = false; 20 | 21 | String currentEmail = ''; 22 | String email = ''; 23 | String password = ''; 24 | 25 | Validator _validator; 26 | String validateEmail(String value) => 27 | validateIfNotEmpty(emptyTextValidation, value, emailValidator.validate); 28 | String validatePassword(String value) => validateIfNotEmpty( 29 | emptyTextValidation, value, passwordValidator.validate); 30 | 31 | void validateAll() { 32 | emptyTextValidation = true; 33 | 34 | var errors = _validator.validate(); 35 | if (errors != null && errors.length > 0) { 36 | throw new AppException(errors); 37 | } 38 | } 39 | 40 | void init(AuthService authService) async { 41 | var user = await authService.currentUser(); 42 | currentEmail = user.email; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /lib/core/auth/handlers/email/change/change_password_page.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:flutter_auth_base/flutter_auth_base.dart'; 4 | import 'package:scoped_model/scoped_model.dart'; 5 | import 'package:flutter_platform_widgets/flutter_platform_widgets.dart'; 6 | 7 | import '../../../../widgets/form_progress_actionable_state.dart'; 8 | import '../../../../app_model.dart'; 9 | import 'change_password_view_model.dart'; 10 | import '../../../../widgets/modalAppBar.dart'; 11 | 12 | class ChangePassword extends StatefulWidget { 13 | @override 14 | createState() => new ChangePasswordState(); 15 | } 16 | 17 | class ChangePasswordState extends FormProgressActionableState { 18 | @override 19 | void initState() { 20 | super.initState(); 21 | 22 | _viewModel = new ViewModel(); 23 | } 24 | 25 | ViewModel _viewModel; 26 | 27 | AuthProvider _getPasswordProvider(AuthService authService) { 28 | return authService.authProviders.firstWhere( 29 | (prov) => prov.providerName == 'password', 30 | orElse: () => null); 31 | } 32 | 33 | Future _changePassword(AuthService authService) async { 34 | var provider = _getPasswordProvider(authService); 35 | 36 | var user = await authService.currentUser(); 37 | await provider?.changePassword({ 38 | 'currentEmail': user.email, 39 | 'currentPassword': _viewModel.currentPassword, 40 | 'newPassword': _viewModel.newPassword 41 | }); 42 | 43 | Navigator.pop(context); 44 | } 45 | 46 | Widget _currentPasswordField() { 47 | return ListTile( 48 | leading: Icon( 49 | Icons.lock, 50 | ), 51 | title: TextFormField( 52 | obscureText: true, 53 | decoration: InputDecoration(labelText: 'Current Password'), 54 | validator: _viewModel.validatePassword, 55 | onSaved: (val) => _viewModel.currentPassword = val), 56 | ); 57 | } 58 | 59 | Widget _newPasswordField() { 60 | return ListTile( 61 | leading: Icon( 62 | Icons.lock, 63 | ), 64 | title: TextFormField( 65 | enabled: !super.showProgress, 66 | autocorrect: false, 67 | obscureText: true, 68 | decoration: InputDecoration(labelText: 'New Password'), 69 | validator: _viewModel.validatePassword, 70 | onSaved: (val) => _viewModel.newPassword = val), 71 | ); 72 | } 73 | 74 | Widget _newPasswordConfirmField() { 75 | return ListTile( 76 | leading: Icon( 77 | Icons.lock, 78 | ), 79 | title: TextFormField( 80 | enabled: !super.showProgress, 81 | autocorrect: false, 82 | obscureText: true, 83 | decoration: InputDecoration(labelText: 'Re-type New Password'), 84 | validator: _viewModel.validatePassword, 85 | onSaved: (val) => _viewModel.newPasswordConfirm = val), 86 | ); 87 | } 88 | 89 | Widget _progressIndicator() { 90 | return super.showProgress 91 | ? Padding( 92 | padding: EdgeInsets.all(16.0), 93 | child: PlatformCircularProgressIndicator()) 94 | : Container(); 95 | } 96 | 97 | Widget _build() { 98 | return SingleChildScrollView( 99 | child: Column(children: [ 100 | Padding( 101 | padding: EdgeInsets.all(16.0), 102 | child: Text( 103 | 'Please enter you current password to be able change to a new password'), 104 | ), 105 | _currentPasswordField(), 106 | Padding( 107 | padding: EdgeInsets.symmetric(vertical: 8.0), 108 | child: Divider(), 109 | ), 110 | _newPasswordField(), 111 | _newPasswordConfirmField(), 112 | _progressIndicator(), 113 | ])); 114 | } 115 | 116 | Form _asForm(Widget widget) { 117 | return Form(autovalidate: true, key: super.formKey, child: widget); 118 | } 119 | 120 | @override 121 | Widget build(BuildContext context) { 122 | return ScopedModelDescendant( 123 | rebuildOnChange: false, 124 | builder: (_, child, model) => PlatformScaffold( 125 | appBar: ModalAppBar( 126 | title: Text('Change Passwrord'), 127 | acceptText: 'Apply', 128 | acceptAction: super.showProgress 129 | ? null 130 | : () => super.validateAndSubmit( 131 | (_) async => await _changePassword(model.authService), 132 | ), 133 | closeAction: 134 | super.showProgress ? null : () => Navigator.maybePop(context), 135 | ), 136 | body: Padding( 137 | padding: const EdgeInsets.symmetric(horizontal: 16.0), 138 | child: Material( 139 | color: isMaterial ? null : Theme.of(context).cardColor, 140 | child: _asForm( 141 | _build(), 142 | ), 143 | ), 144 | ), 145 | ), 146 | ); 147 | } 148 | } 149 | -------------------------------------------------------------------------------- /lib/core/auth/handlers/email/change/change_password_view_model.dart: -------------------------------------------------------------------------------- 1 | import '../../../../validators/validator.dart'; 2 | import '../../../../validators/password_validator.dart' as passwordValidator; 3 | import '../../../../validators/words_match_validator.dart' 4 | as passwordsMatchValidator; 5 | 6 | import '../../../../common/app_exception.dart'; 7 | import '../../../../validators/validate_if.dart'; 8 | 9 | class ViewModel { 10 | ViewModel() { 11 | _validator = new Validator(); 12 | 13 | _validator.validations.add(() => validatePassword(currentPassword)); 14 | _validator.validations.add(() => validatePassword(newPassword)); 15 | _validator.validations.add(() => validatePassword(newPasswordConfirm)); 16 | _validator.validations 17 | .add(() => validatePasswordsMatch(newPassword, newPasswordConfirm)); 18 | } 19 | 20 | //only want to validate field if either there is some text or 21 | // the submit button clicked and the field is empty 22 | bool emptyTextValidation = false; 23 | 24 | String currentPassword = ''; 25 | String newPassword = ''; 26 | String newPasswordConfirm = ''; 27 | 28 | Validator _validator; 29 | 30 | String validatePassword(String value) => validateIfNotEmpty( 31 | emptyTextValidation, value, passwordValidator.validate); 32 | 33 | String validatePasswordsMatch(String value1, String value2) => 34 | passwordsMatchValidator.validate(value1, value2, 35 | customOnError: "Passwords do not match"); 36 | 37 | void validateAll() { 38 | var errors = _validator.validate(); 39 | if (errors != null && errors.length > 0) { 40 | throw new AppException(errors); 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /lib/core/auth/handlers/email/forgotPassword/forgot_password_page.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | import 'package:flutter/material.dart'; 4 | import 'package:flutter_auth_base/flutter_auth_base.dart'; 5 | import 'package:scoped_model/scoped_model.dart'; 6 | import 'package:flutter_platform_widgets/flutter_platform_widgets.dart'; 7 | 8 | import '../../../../widgets/modalAppBar.dart'; 9 | import '../../../../widgets/form_progress_actionable_state.dart'; 10 | import '../../../../app_model.dart'; 11 | 12 | import 'forgot_password_view_model.dart'; 13 | 14 | class ForgotPassword extends StatefulWidget { 15 | @override 16 | createState() => new ForgotPasswordState(); 17 | } 18 | 19 | class ForgotPasswordState extends FormProgressActionableState { 20 | @override 21 | void initState() { 22 | super.initState(); 23 | 24 | _viewModel = new ViewModel(); 25 | } 26 | 27 | ViewModel _viewModel; 28 | bool _showSendMessage = false; 29 | 30 | AuthProvider _getPasswordProvider(AuthService authService) { 31 | return authService.authProviders.firstWhere( 32 | (prov) => prov.providerName == 'password', 33 | orElse: () => null); 34 | } 35 | 36 | Future _sendPasswordReset(AuthService authService) async { 37 | var provider = _getPasswordProvider(authService); 38 | 39 | if (provider != null) { 40 | await provider.sendPasswordReset({'email': _viewModel.email}); 41 | setState(() { 42 | _showSendMessage = true; 43 | }); 44 | } 45 | } 46 | 47 | Widget _emailField() { 48 | return TextFormField( 49 | enabled: !super.showProgress, 50 | keyboardType: TextInputType.emailAddress, 51 | autocorrect: false, 52 | decoration: InputDecoration(labelText: 'Email'), 53 | validator: _viewModel.validateEmail, 54 | onSaved: (val) => _viewModel.email = val, 55 | ); 56 | } 57 | 58 | Widget _forgotButton(AuthService authService) { 59 | return Padding( 60 | padding: EdgeInsets.all(32.0), 61 | child: PlatformButton( 62 | child: Text('Send Email'), 63 | onPressed: super.showProgress 64 | ? null 65 | : () => super.validateAndSubmit( 66 | (_) async => await _sendPasswordReset(authService))), 67 | ); 68 | } 69 | 70 | Widget _progressIndicator() { 71 | return super.showProgress 72 | ? Padding( 73 | padding: EdgeInsets.all(16.0), 74 | child: PlatformCircularProgressIndicator()) 75 | : Container(); 76 | } 77 | 78 | Widget _buildForm(AuthService authService) { 79 | return SingleChildScrollView( 80 | child: new Padding( 81 | padding: const EdgeInsets.all(16.0), 82 | child: Column(children: [ 83 | Padding( 84 | padding: EdgeInsets.all(16.0), 85 | child: Text( 86 | 'Please enter your email address and we will send you a reset password email.', 87 | ), 88 | ), 89 | _emailField(), 90 | _forgotButton(authService), 91 | _showSendMessage 92 | ? Padding( 93 | padding: const EdgeInsets.all(8.0), 94 | child: Text( 95 | 'An email has been sent to this address. Please open the email and follow the instructions to reset your password', 96 | textAlign: TextAlign.center, 97 | ), 98 | ) 99 | : Container(), 100 | _progressIndicator() 101 | ]), 102 | )); 103 | } 104 | 105 | Form _asForm(Widget widget) { 106 | return Form(autovalidate: true, key: super.formKey, child: widget); 107 | } 108 | 109 | @override 110 | Widget build(BuildContext context) { 111 | return PlatformScaffold( 112 | appBar: ModalAppBar( 113 | title: Text('Reset Password'), 114 | hideAccept: true, 115 | closeAction: 116 | super.showProgress ? null : () => Navigator.maybePop(context), 117 | ), 118 | body: Material( 119 | color: isMaterial ? null : Theme.of(context).cardColor, 120 | child: ScopedModelDescendant( 121 | rebuildOnChange: false, 122 | builder: (_, child, model) => Padding( 123 | padding: const EdgeInsets.symmetric(horizontal: 16.0), 124 | child: _asForm(_buildForm(model.authService)), 125 | ), 126 | ), 127 | ), 128 | ); 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /lib/core/auth/handlers/email/forgotPassword/forgot_password_view_model.dart: -------------------------------------------------------------------------------- 1 | import '../../../../validators/validator.dart'; 2 | import '../../../../validators/email_validator.dart' as emailValidator; 3 | 4 | import '../../../../common/app_exception.dart'; 5 | import '../../../../validators/validate_if.dart'; 6 | 7 | class ViewModel { 8 | ViewModel({this.email}) { 9 | _validator = new Validator(); 10 | _validator.validations.add(() => validateEmail(email)); 11 | } 12 | 13 | //only want to validate field if either there is some text or 14 | // the submit button clicked and the field is empty 15 | bool emptyTextValidation = false; 16 | 17 | String email = ''; 18 | 19 | Validator _validator; 20 | String validateEmail(String value) => 21 | validateIfNotEmpty(emptyTextValidation, value, emailValidator.validate); 22 | 23 | void validateAll() { 24 | emptyTextValidation = true; 25 | 26 | var errors = _validator.validate(); 27 | if (errors != null && errors.length > 0) { 28 | throw new AppException(errors); 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /lib/core/auth/handlers/email/icon.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | IconData providerIcon = Icons.email; 4 | -------------------------------------------------------------------------------- /lib/core/auth/handlers/email/link/link_account.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:flutter_auth_base/flutter_auth_base.dart'; 4 | import 'package:flutter_platform_widgets/flutter_platform_widgets.dart'; 5 | 6 | import '../../../../widgets/form_progress_actionable_state.dart'; 7 | import 'link_account_view_model.dart'; 8 | 9 | import '../../../../widgets/modalAppBar.dart'; 10 | 11 | class LinkEmailAccount extends StatefulWidget { 12 | LinkEmailAccount(this.linkProvider, {this.onAuthRequired}); 13 | 14 | final LinkableProvider linkProvider; 15 | final VoidCallback onAuthRequired; 16 | 17 | @override 18 | createState() => new LinkEmailAccountState(); 19 | } 20 | 21 | class LinkEmailAccountState 22 | extends FormProgressActionableState { 23 | @override 24 | void initState() { 25 | super.initState(); 26 | 27 | _viewModel = new ViewModel(); 28 | } 29 | 30 | ViewModel _viewModel; 31 | 32 | Future doLinkAccount() { 33 | _viewModel.validateAll(); 34 | 35 | return widget.linkProvider.linkAccount( 36 | {'email': _viewModel.email, 'password': _viewModel.password}); 37 | } 38 | 39 | Widget _linkButton() { 40 | return Padding( 41 | padding: EdgeInsets.symmetric(vertical: 32.0), 42 | child: PlatformButton( 43 | child: Text('Sign In'), 44 | onPressed: super.showProgress 45 | ? null 46 | : () => super.validateAndSubmit( 47 | (_) async { 48 | try { 49 | await doLinkAccount(); 50 | Navigator.pop(context); 51 | } on AuthRequiredException catch (error) { 52 | if (widget.onAuthRequired != null) { 53 | print('widget.onAuthRequired'); 54 | widget.onAuthRequired(); 55 | } else { 56 | throw error; 57 | } 58 | return null; 59 | } 60 | }, 61 | ), 62 | ), 63 | ); 64 | } 65 | 66 | Widget _emailField() { 67 | return ListTile( 68 | leading: Icon( 69 | Icons.email, 70 | ), 71 | title: TextFormField( 72 | enabled: !super.showProgress, 73 | keyboardType: TextInputType.emailAddress, 74 | autocorrect: false, 75 | decoration: InputDecoration(labelText: 'Email'), 76 | validator: _viewModel.validateEmail, 77 | onSaved: (val) => _viewModel.email = val), 78 | ); 79 | } 80 | 81 | Widget _passwordField() { 82 | return ListTile( 83 | leading: Icon( 84 | Icons.lock, 85 | ), 86 | title: TextFormField( 87 | enabled: !super.showProgress, 88 | autocorrect: false, 89 | obscureText: true, 90 | decoration: InputDecoration(labelText: 'Password'), 91 | validator: _viewModel.validatePassword, 92 | onSaved: (val) => _viewModel.password = val), 93 | ); 94 | } 95 | 96 | Widget _progressIndicator() { 97 | return super.showProgress 98 | ? Padding( 99 | padding: EdgeInsets.all(16.0), 100 | child: PlatformCircularProgressIndicator()) 101 | : Container(); 102 | } 103 | 104 | Widget _buildPage() { 105 | return Padding( 106 | padding: const EdgeInsets.all(8.0), 107 | child: SingleChildScrollView( 108 | child: Column(children: [ 109 | _emailField(), 110 | _passwordField(), 111 | _linkButton(), 112 | _progressIndicator(), 113 | ])), 114 | ); 115 | } 116 | 117 | Form _asForm(Widget widget) { 118 | return Form(autovalidate: true, key: super.formKey, child: widget); 119 | } 120 | 121 | @override 122 | Widget build(BuildContext context) { 123 | return PlatformScaffold( 124 | appBar: ModalAppBar( 125 | hideAccept: true, 126 | closeAction: 127 | super.showProgress ? null : () => Navigator.maybePop(context), 128 | title: Text('Link Email Account'), 129 | ), 130 | body: Material( 131 | color: isMaterial ? null : Theme.of(context).cardColor, 132 | child: _asForm( 133 | _buildPage(), 134 | ), 135 | ), 136 | ); 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /lib/core/auth/handlers/email/link/link_account_view_model.dart: -------------------------------------------------------------------------------- 1 | import '../../../../validators/validator.dart'; 2 | import '../../../../validators/email_validator.dart' as emailValidator; 3 | import '../../../../validators/password_validator.dart' as passwordValidator; 4 | 5 | import '../../../../common/app_exception.dart'; 6 | import '../../../../validators/validate_if.dart'; 7 | 8 | class ViewModel { 9 | ViewModel({this.email, this.password}) { 10 | _validator = new Validator(); 11 | _validator.validations.add(() => validateEmail(email)); 12 | _validator.validations.add(() => validatePassword(password)); 13 | } 14 | 15 | //only want to validate field if either there is some text or 16 | // the submit button clicked and the field is empty 17 | bool emptyTextValidation = false; 18 | 19 | String email = ''; 20 | String password = ''; 21 | 22 | Validator _validator; 23 | String validateEmail(String value) => 24 | validateIfNotEmpty(emptyTextValidation, value, emailValidator.validate); 25 | String validatePassword(String value) => validateIfNotEmpty( 26 | emptyTextValidation, value, passwordValidator.validate); 27 | 28 | void validateAll() { 29 | emptyTextValidation = true; 30 | 31 | var errors = _validator.validate(); 32 | if (errors != null && errors.length > 0) { 33 | throw new AppException(errors); 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /lib/core/auth/handlers/email/signIn/sign_in_page.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | import 'package:flutter/material.dart'; 4 | import 'package:flutter_auth_base/flutter_auth_base.dart'; 5 | import 'package:scoped_model/scoped_model.dart'; 6 | import 'package:flutter_platform_widgets/flutter_platform_widgets.dart'; 7 | 8 | import '../../../../common/dialog.dart'; 9 | import '../../../../widgets/form_progress_actionable_state.dart'; 10 | import '../../../../widgets/tablet_aware_layout_builder.dart'; 11 | import '../../../../app_info.dart'; 12 | import '../../../../app_model.dart'; 13 | 14 | import '../../../../widgets/header_button.dart'; 15 | import '../../../../widgets/throttled_text_editing_controller.dart'; 16 | import '../../../../widgets/email_image_circle_avatar.dart'; 17 | 18 | import '../signUp/sign_up_page.dart'; 19 | import '../forgotPassword/forgot_password_page.dart'; 20 | import '../../user/termsAcceptance/terms_accept_modal.dart'; 21 | 22 | import 'sign_in_view_model.dart'; 23 | 24 | class SignInPassword extends StatefulWidget { 25 | SignInPassword( 26 | {this.displaySignInButton = true, this.popRouteOnSignin = false}); 27 | 28 | final bool displaySignInButton; 29 | final bool popRouteOnSignin; 30 | @override 31 | createState() => new SignInPasswordState(); 32 | } 33 | 34 | class SignInPasswordState extends FormProgressActionableState { 35 | @override 36 | void initState() { 37 | super.initState(); 38 | 39 | _viewModel = new ViewModel(); 40 | 41 | _emailController = ThrottledTextEditingController( 42 | throttleDurationMilliseconds: 1500, 43 | onUpdate: (value) => avatarKey.currentState.performUpdate(value)); 44 | } 45 | 46 | ViewModel _viewModel; 47 | TextEditingController _emailController; 48 | 49 | final avatarKey = GlobalKey(); 50 | 51 | AuthProvider _getPasswordProvider(AuthService authService) { 52 | return authService.authProviders.firstWhere( 53 | (prov) => prov.providerName == 'password', 54 | orElse: () => null); 55 | } 56 | 57 | void _signUp() { 58 | Navigator.push( 59 | context, MaterialPageRoute(builder: (_) => new SignUpPassword())); 60 | } 61 | 62 | Widget _logoGravatar(AppInfo appInfo, AuthService authService) { 63 | return Padding( 64 | padding: const EdgeInsets.all(8.0), 65 | child: EmailImageCircleAvatar( 66 | checkIfImageExists: true, 67 | key: avatarKey, 68 | imageSize: 150, 69 | backgroundColor: Colors.white70, 70 | defaultImage: AssetImage(appInfo.appIconPath), 71 | imageProvider: authService.preAuthPhotoProvider), 72 | ); 73 | } 74 | 75 | Widget _emailField() { 76 | return TextFormField( 77 | enabled: !super.showProgress, 78 | keyboardType: TextInputType.emailAddress, 79 | autocorrect: false, 80 | controller: _emailController, 81 | decoration: InputDecoration(labelText: 'Email'), 82 | validator: _viewModel.validateEmail, 83 | onSaved: (val) => _viewModel.email = val, 84 | ); 85 | } 86 | 87 | Widget _passwordField() { 88 | return TextFormField( 89 | enabled: !super.showProgress, 90 | autocorrect: false, 91 | obscureText: true, 92 | decoration: InputDecoration(labelText: 'Password'), 93 | validator: _viewModel.validatePassword, 94 | onSaved: (val) => _viewModel.password = val); 95 | } 96 | 97 | Future _signInWithEmailPassword(AuthService authService) async { 98 | _viewModel.validateAll(); 99 | 100 | var provider = _getPasswordProvider(authService); 101 | try { 102 | await provider?.signIn( 103 | {'email': _viewModel.email, 'password': _viewModel.password}); 104 | } on UserAcceptanceRequiredException { 105 | bool accepted = await _handleAcceptanceRequired(); 106 | 107 | if (accepted) 108 | await provider?.create( 109 | {'email': _viewModel.email, 'password': _viewModel.password}, 110 | termsAccepted: true); 111 | } 112 | 113 | if (widget.popRouteOnSignin) Navigator.pop(context); 114 | } 115 | 116 | Future _handleAcceptanceRequired() async { 117 | var accepted = await openDialog( 118 | context: context, 119 | builder: (_) => TermsAcceptModal(), 120 | ); 121 | return accepted; 122 | } 123 | 124 | Widget _signInButton(AuthService authService) { 125 | final ThemeData themeData = Theme.of(context); 126 | final bool isDark = Brightness.dark == themeData.primaryColorBrightness; 127 | 128 | return Padding( 129 | padding: EdgeInsets.symmetric(vertical: 32.0), 130 | child: PlatformButton( 131 | android: (_) => MaterialRaisedButtonData( 132 | textColor: isDark ? Colors.white : Colors.black87, 133 | color: Theme.of(context).primaryColor), 134 | ios: (_) => 135 | CupertinoButtonData(color: Theme.of(context).primaryColor), 136 | child: Text('Sign In'), 137 | onPressed: super.showProgress 138 | ? null 139 | : () => super.validateAndSubmit( 140 | (_) async => await _signInWithEmailPassword(authService))), 141 | ); 142 | } 143 | 144 | Widget _forgotPasswordButton(AuthService authService) { 145 | return authService.options.canSendForgotEmail 146 | ? Padding( 147 | padding: const EdgeInsets.only( 148 | bottom: 16.0, top: 16.0, left: 32.0, right: 32.0), 149 | child: PlatformButton( 150 | child: Text('Forgot password'), 151 | onPressed: super.showProgress 152 | ? null 153 | : () async => await openDialog( 154 | context: context, builder: (_) => ForgotPassword()))) 155 | : Container(); 156 | } 157 | 158 | Widget _progressIndicator() { 159 | return super.showProgress 160 | ? Padding( 161 | padding: EdgeInsets.all(16.0), 162 | child: PlatformCircularProgressIndicator()) 163 | : Container(); 164 | } 165 | 166 | Widget _buildMobileForm(AppInfo appInfo, AuthService authService) { 167 | return Padding( 168 | padding: const EdgeInsets.symmetric(horizontal: 16.0), 169 | child: SingleChildScrollView( 170 | child: Column( 171 | crossAxisAlignment: CrossAxisAlignment.stretch, 172 | children: [ 173 | Row( 174 | mainAxisAlignment: MainAxisAlignment.center, 175 | children: [ 176 | _logoGravatar(appInfo, authService), 177 | ], 178 | ), 179 | _emailField(), 180 | _passwordField(), 181 | _signInButton(authService), 182 | _forgotPasswordButton(authService), 183 | Row( 184 | mainAxisAlignment: MainAxisAlignment.center, 185 | children: [_progressIndicator()], 186 | ), 187 | ], 188 | ), 189 | ), 190 | ); 191 | } 192 | 193 | Widget _buildTabletForm(AppInfo appInfo, AuthService authService) { 194 | return Row( 195 | children: [ 196 | Expanded( 197 | flex: 1, 198 | child: Container( 199 | color: Colors.white, 200 | //elevation: 4.0, 201 | child: Column( 202 | mainAxisAlignment: MainAxisAlignment.center, 203 | children: [ 204 | _logoGravatar(appInfo, authService), 205 | _progressIndicator() 206 | ], 207 | ), 208 | ), 209 | ), 210 | Expanded( 211 | flex: 1, 212 | child: SingleChildScrollView( 213 | child: Padding( 214 | padding: EdgeInsets.symmetric(horizontal: 36.0), 215 | child: Column( 216 | children: [ 217 | _emailField(), 218 | _passwordField(), 219 | _signInButton(authService), 220 | _forgotPasswordButton(authService), 221 | ], 222 | ), 223 | ), 224 | ), 225 | ), 226 | ], 227 | ); 228 | } 229 | 230 | Form _asForm(Widget widget) { 231 | return Form(autovalidate: true, key: super.formKey, child: widget); 232 | } 233 | 234 | @override 235 | Widget build(BuildContext context) { 236 | return PlatformScaffold( 237 | appBar: PlatformAppBar( 238 | title: Text('Login'), 239 | trailingActions: [ 240 | widget.displaySignInButton 241 | ? HeaderButton( 242 | text: 'Sign Up', 243 | onPressed: () => _signUp(), 244 | ) 245 | : Container(), 246 | ], 247 | ), 248 | body: Material( 249 | color: isMaterial ? null : Theme.of(context).cardColor, // Colors.white, 250 | child: ScopedModelDescendant( 251 | builder: (_, child, model) => TabletAwareLayoutBuilder( 252 | mobileView: (_) => _asForm( 253 | _buildMobileForm(model.appInfo, model.authService), 254 | ), 255 | tabletView: (_) => _asForm( 256 | _buildTabletForm(model.appInfo, model.authService), 257 | ), 258 | ), 259 | ), 260 | ), 261 | ); 262 | } 263 | } 264 | -------------------------------------------------------------------------------- /lib/core/auth/handlers/email/signIn/sign_in_view_model.dart: -------------------------------------------------------------------------------- 1 | import '../../../../validators/validator.dart'; 2 | import '../../../../validators/email_validator.dart' as emailValidator; 3 | import '../../../../validators/password_validator.dart' as passwordValidator; 4 | 5 | import '../../../../common/app_exception.dart'; 6 | 7 | import '../../../../validators/validate_if.dart'; 8 | 9 | class ViewModel { 10 | ViewModel({this.email, this.password}) { 11 | _validator = new Validator(); 12 | _validator.validations.add(() => validateEmail(email)); 13 | _validator.validations.add(() => validatePassword(password)); 14 | } 15 | 16 | //only want to validate field if either there is some text or 17 | // the submit button clicked and the field is empty 18 | bool emptyTextValidation = false; 19 | 20 | String email = ''; 21 | String password = ''; 22 | 23 | Validator _validator; 24 | String validateEmail(String value) => 25 | validateIfNotEmpty(emptyTextValidation, value, emailValidator.validate); 26 | String validatePassword(String value) => validateIfNotEmpty( 27 | emptyTextValidation, value, passwordValidator.validate); 28 | 29 | void validateAll() { 30 | emptyTextValidation = true; 31 | 32 | var errors = _validator.validate(); 33 | if (errors != null && errors.length > 0) { 34 | throw new AppException(errors); 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /lib/core/auth/handlers/email/signUp/sign_up_page.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:flutter_auth_base/flutter_auth_base.dart'; 4 | import 'package:scoped_model/scoped_model.dart'; 5 | import 'package:flutter_platform_widgets/flutter_platform_widgets.dart'; 6 | 7 | import '../../../../common/dialog.dart'; 8 | import '../../../../widgets/form_progress_actionable_state.dart'; 9 | import '../../../../widgets/tablet_aware_layout_builder.dart'; 10 | import '../../../../app_info.dart'; 11 | import '../../../../app_model.dart'; 12 | 13 | import '../../../../widgets/throttled_text_editing_controller.dart'; 14 | import '../../../../widgets/email_image_circle_avatar.dart'; 15 | 16 | import '../../user/termsAcceptance/terms_accept_modal.dart'; 17 | 18 | import 'sign_up_view_model.dart'; 19 | 20 | class SignUpPassword extends StatefulWidget { 21 | @override 22 | createState() => new SignUpPasswordState(); 23 | } 24 | 25 | class SignUpPasswordState extends FormProgressActionableState { 26 | @override 27 | void initState() { 28 | super.initState(); 29 | 30 | _viewModel = new ViewModel(); 31 | 32 | _emailController = ThrottledTextEditingController( 33 | throttleDurationMilliseconds: 1500, 34 | onUpdate: (value) => avatarKey.currentState?.performUpdate(value)); 35 | } 36 | 37 | ViewModel _viewModel; 38 | TextEditingController _emailController; 39 | 40 | final avatarKey = GlobalKey(); 41 | 42 | AuthProvider _getPasswordProvider(AuthService authService) { 43 | return authService.authProviders.firstWhere( 44 | (prov) => prov.providerName == 'password', 45 | orElse: () => null); 46 | } 47 | 48 | Widget _logoGravatar(AppInfo appInfo, AuthService authService) { 49 | return Padding( 50 | padding: const EdgeInsets.all(8.0), 51 | child: EmailImageCircleAvatar( 52 | checkIfImageExists: true, 53 | key: avatarKey, 54 | imageSize: 150, 55 | backgroundColor: Colors.white70, 56 | defaultImage: AssetImage(appInfo.appIconPath), 57 | imageProvider: authService.preAuthPhotoProvider), 58 | ); 59 | } 60 | 61 | Widget _displayNameField() { 62 | return TextFormField( 63 | enabled: !super.showProgress, 64 | keyboardType: TextInputType.text, 65 | autocorrect: false, 66 | decoration: InputDecoration(labelText: 'Display Name (optional)'), 67 | validator: _viewModel.validateDisplayName, 68 | onSaved: (val) => _viewModel.displayName = val, 69 | ); 70 | } 71 | 72 | Widget _emailField() { 73 | return TextFormField( 74 | enabled: !super.showProgress, 75 | keyboardType: TextInputType.emailAddress, 76 | autocorrect: false, 77 | controller: _emailController, 78 | decoration: InputDecoration(labelText: 'Email'), 79 | validator: _viewModel.validateEmail, 80 | onSaved: (val) => _viewModel.email = val, 81 | ); 82 | } 83 | 84 | Widget _passwordField() { 85 | return TextFormField( 86 | enabled: !super.showProgress, 87 | autocorrect: false, 88 | obscureText: true, 89 | decoration: InputDecoration(labelText: 'Password'), 90 | validator: _viewModel.validatePassword, 91 | onSaved: (val) => _viewModel.password = val); 92 | } 93 | 94 | Widget _passwordConfirmField() { 95 | return TextFormField( 96 | enabled: !super.showProgress, 97 | autocorrect: false, 98 | obscureText: true, 99 | decoration: InputDecoration(labelText: 'Re-type Password'), 100 | validator: _viewModel.validatePassword, 101 | onSaved: (val) => _viewModel.passwordConfirm = val); 102 | } 103 | 104 | Future _signUpWithEmailPassword(AuthService authService) async { 105 | _viewModel.validateAll(); 106 | 107 | var provider = _getPasswordProvider(authService); 108 | try { 109 | await provider?.create( 110 | {'email': _viewModel.email, 'password': _viewModel.password}); 111 | } on UserAcceptanceRequiredException { 112 | bool accepted = await _handleAcceptanceRequired(); 113 | 114 | if (accepted) 115 | await provider?.create( 116 | {'email': _viewModel.email, 'password': _viewModel.password}, 117 | termsAccepted: true); 118 | } 119 | } 120 | 121 | Future _handleAcceptanceRequired() async { 122 | var accepted = await openDialog( 123 | context: context, 124 | builder: (_) => TermsAcceptModal(), 125 | ); 126 | return accepted; 127 | } 128 | 129 | Widget _signUpButton(AuthService authService) { 130 | final ThemeData themeData = Theme.of(context); 131 | final bool isDark = Brightness.dark == themeData.primaryColorBrightness; 132 | 133 | return Padding( 134 | padding: EdgeInsets.symmetric(vertical: 32.0), 135 | child: PlatformButton( 136 | android: (_) => MaterialRaisedButtonData( 137 | textColor: isDark ? Colors.white : Colors.black87, 138 | color: Theme.of(context).primaryColor), 139 | ios: (_) => CupertinoButtonData(color: Theme.of(context).primaryColor), 140 | child: Text('Sign Up'), 141 | onPressed: super.showProgress 142 | ? null 143 | : () => super.validateAndSubmit( 144 | (_) async => await _signUpWithEmailPassword(authService), 145 | ), 146 | ), 147 | ); 148 | } 149 | 150 | Widget _progressIndicator() { 151 | return super.showProgress 152 | ? Padding( 153 | padding: EdgeInsets.all(16.0), 154 | child: PlatformCircularProgressIndicator()) 155 | : Container(); 156 | } 157 | 158 | Widget _buildMobileForm(AppInfo appInfo, AuthService authService) { 159 | return Padding( 160 | padding: const EdgeInsets.symmetric(horizontal: 16.0), 161 | child: SingleChildScrollView( 162 | child: Column( 163 | crossAxisAlignment: CrossAxisAlignment.stretch, 164 | children: [ 165 | Row( 166 | mainAxisAlignment: MainAxisAlignment.center, 167 | children: [ 168 | _logoGravatar(appInfo, authService), 169 | ], 170 | ), 171 | _displayNameField(), 172 | _emailField(), 173 | _passwordField(), 174 | _passwordConfirmField(), 175 | _signUpButton(authService), 176 | Row( 177 | mainAxisAlignment: MainAxisAlignment.center, 178 | children: [_progressIndicator()], 179 | ), 180 | ], 181 | ), 182 | ), 183 | ); 184 | } 185 | 186 | Widget _buildTabletForm(AppInfo appInfo, AuthService authService) { 187 | return Row( 188 | children: [ 189 | Expanded( 190 | flex: 1, 191 | child: Container( 192 | color: Colors.white, 193 | //elevation: 4.0, 194 | child: Column( 195 | mainAxisAlignment: MainAxisAlignment.center, 196 | children: [ 197 | _logoGravatar(appInfo, authService), 198 | _progressIndicator() 199 | ], 200 | ), 201 | ), 202 | ), 203 | Expanded( 204 | flex: 1, 205 | child: SingleChildScrollView( 206 | child: Padding( 207 | padding: EdgeInsets.symmetric(horizontal: 36.0), 208 | child: Column( 209 | children: [ 210 | _displayNameField(), 211 | _emailField(), 212 | _passwordField(), 213 | _passwordConfirmField(), 214 | _signUpButton(authService), 215 | ], 216 | ), 217 | ), 218 | ), 219 | ), 220 | ], 221 | ); 222 | } 223 | 224 | Form _asForm(Widget widget) { 225 | return Form(autovalidate: true, key: super.formKey, child: widget); 226 | } 227 | 228 | @override 229 | Widget build(BuildContext context) { 230 | return PlatformScaffold( 231 | appBar: PlatformAppBar( 232 | title: Text('Create New Account'), 233 | ), 234 | backgroundColor: Colors.white, 235 | body: Material( 236 | color: isMaterial ? null : Theme.of(context).cardColor, 237 | child: ScopedModelDescendant( 238 | builder: (_, child, model) => TabletAwareLayoutBuilder( 239 | mobileView: (_) => _asForm( 240 | _buildMobileForm(model.appInfo, model.authService), 241 | ), 242 | tabletView: (_) => _asForm( 243 | _buildTabletForm(model.appInfo, model.authService), 244 | ), 245 | ), 246 | ), 247 | ), 248 | ); 249 | } 250 | } 251 | -------------------------------------------------------------------------------- /lib/core/auth/handlers/email/signUp/sign_up_view_model.dart: -------------------------------------------------------------------------------- 1 | import '../../../../validators/validator.dart'; 2 | import '../../../../validators/email_validator.dart' as emailValidator; 3 | import '../../../../validators/password_validator.dart' as passwordValidator; 4 | import '../../../../validators/display_name_validator.dart' 5 | as displayNameValidator; 6 | import '../../../../validators/words_match_validator.dart' 7 | as passwordsMatchValidator; 8 | 9 | import '../../../../validators/validate_if.dart'; 10 | 11 | import '../../../../common/app_exception.dart'; 12 | 13 | class ViewModel { 14 | ViewModel({this.email, this.password}) { 15 | _validator = new Validator(); 16 | _validator.validations.add(() => validateDisplayName(displayName)); 17 | _validator.validations.add(() => validateEmail(email)); 18 | _validator.validations.add(() => validatePassword(password)); 19 | _validator.validations.add(() => validatePassword(passwordConfirm)); 20 | _validator.validations 21 | .add(() => validatePasswordsMatch(password, passwordConfirm)); 22 | } 23 | 24 | //only want to validate field if either there is some text or 25 | // the submit button clicked and the field is empty 26 | bool emptyTextValidation = false; 27 | 28 | String displayName = ''; 29 | String email = ''; 30 | String password = ''; 31 | String passwordConfirm = ''; 32 | 33 | Validator _validator; 34 | 35 | String validateDisplayName(String value) => validateIfNotEmpty( 36 | emptyTextValidation, value, displayNameValidator.validate); 37 | 38 | String validateEmail(String value) => 39 | validateIfNotEmpty(emptyTextValidation, value, emailValidator.validate); 40 | 41 | String validatePassword(String value) => validateIfNotEmpty( 42 | emptyTextValidation, value, passwordValidator.validate); 43 | 44 | String validatePasswordsMatch(String value1, String value2) => 45 | passwordsMatchValidator.validate(value1, value2, 46 | customOnError: "Passwords do not match"); 47 | 48 | void validateAll() { 49 | emptyTextValidation = true; 50 | 51 | var errors = _validator.validate(); 52 | if (errors != null && errors.length > 0) { 53 | throw new AppException(errors); 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /lib/core/auth/handlers/email/sign_in_button.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | import 'package:flutter/material.dart'; 4 | import 'signIn/sign_in_page.dart'; 5 | import 'icon.dart'; 6 | 7 | import '../../../common/future_action_callback.dart'; 8 | 9 | Future signIn(BuildContext context) { 10 | return Navigator.push( 11 | context, MaterialPageRoute(builder: (_) => SignInPassword())); 12 | } 13 | 14 | class SignInButton extends StatelessWidget { 15 | SignInButton({this.action = signIn}); 16 | 17 | final FutureActionCallback action; 18 | 19 | @override 20 | Widget build(BuildContext context) { 21 | final ThemeData theme = Theme.of(context); 22 | final bool isDark = Brightness.dark == theme.primaryColorBrightness; 23 | var color = isDark ? Colors.white : Colors.black87; 24 | 25 | return RaisedButton( 26 | color: theme.primaryColor, 27 | padding: EdgeInsets.all(8.0), 28 | onPressed: () => action(context), 29 | child: Row( 30 | mainAxisAlignment: MainAxisAlignment.center, 31 | children: [ 32 | Container( 33 | alignment: Alignment.centerRight, 34 | width: 70.0, 35 | child: Icon( 36 | providerIcon, 37 | color: color, 38 | ), 39 | ), 40 | Expanded( 41 | child: new Center( 42 | child: Text('Email Sign in', style: TextStyle(color: color))), 43 | ), 44 | Container( 45 | width: 70.0, 46 | ), 47 | ], 48 | ), 49 | ); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /lib/core/auth/handlers/email/sign_up_button.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'signUp/sign_up_page.dart'; 3 | 4 | void signUp(BuildContext context) { 5 | Navigator.push(context, MaterialPageRoute(builder: (_) => SignUpPassword())); 6 | } 7 | 8 | class SignUpButton extends StatelessWidget { 9 | @override 10 | Widget build(BuildContext context) { 11 | var theme = Theme.of(context); 12 | 13 | return Column(children: [ 14 | Padding( 15 | padding: EdgeInsets.symmetric(vertical: 8.0), 16 | child: Text("Not registered?", 17 | style: TextStyle(color: theme.textTheme.body1.color)), 18 | ), 19 | OutlineButton( 20 | padding: EdgeInsets.symmetric(horizontal: 32.0), 21 | textColor: theme.primaryColor, 22 | onPressed: () => signUp(context), 23 | child: Text("Sign Up")) 24 | ]); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /lib/core/auth/handlers/google/icon.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:font_awesome_flutter/font_awesome_flutter.dart'; 3 | 4 | IconData providerIcon = FontAwesomeIcons.google; 5 | -------------------------------------------------------------------------------- /lib/core/auth/handlers/google/sign_in_button.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:meta/meta.dart'; 3 | 4 | import 'icon.dart'; 5 | 6 | import '../../../common/future_action_callback.dart'; 7 | 8 | class SignInButton extends StatelessWidget { 9 | SignInButton({@required this.action}); 10 | 11 | final FutureActionCallback action; 12 | 13 | @override 14 | Widget build(BuildContext context) { 15 | return RaisedButton( 16 | color: Colors.red, 17 | padding: EdgeInsets.all(8.0), 18 | onPressed: () async => await action(context), 19 | child: Row( 20 | mainAxisAlignment: MainAxisAlignment.center, 21 | children: [ 22 | Container( 23 | alignment: Alignment.centerRight, 24 | width: 70.0, 25 | child: Icon( 26 | providerIcon, 27 | color: Colors.white, 28 | ), 29 | ), 30 | Expanded( 31 | child: new Center( 32 | child: Text('Google Sign in', 33 | style: TextStyle(color: Colors.white))), 34 | ), 35 | Container( 36 | width: 70.0, 37 | ), 38 | ], 39 | ), 40 | ); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /lib/core/auth/handlers/link/linkAccounts/link_accounts_page.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_auth_base/flutter_auth_base.dart'; 3 | import 'package:scoped_model/scoped_model.dart'; 4 | import 'package:flutter_platform_widgets/flutter_platform_widgets.dart'; 5 | 6 | import '../../../../app_model.dart'; 7 | 8 | import '../../../../common/dialog.dart'; 9 | import 'link_accounts_view_model.dart'; 10 | import '../../email/link/link_account.dart' as email; 11 | import '../../email/icon.dart' as email; 12 | import '../../google/icon.dart' as google; 13 | import 'link_card.dart'; 14 | 15 | import '../../../../dialogs/show_ok_cancel_dialog.dart'; 16 | import '../../signin_accounts/signin_picker_dialog.dart'; 17 | 18 | class LinkAccounts extends StatefulWidget { 19 | @override 20 | createState() => new LinkAccountsState(); 21 | } 22 | 23 | class LinkAccountsState extends State { 24 | @override 25 | void initState() { 26 | super.initState(); 27 | 28 | _viewModel = new ViewModel(); 29 | } 30 | 31 | ViewModel _viewModel; 32 | 33 | final Map _icons = { 34 | 'google': google.providerIcon, 35 | 'password': email.providerIcon 36 | }; 37 | 38 | bool isEmpty(String str) { 39 | return str == null || str.length == 0; 40 | } 41 | 42 | Widget _buildActiveCard({ViewModelItem viewModel, IconData icon}) { 43 | var theme = Theme.of(context); 44 | return Column( 45 | children: [ 46 | ListTile( 47 | leading: Icon(icon), 48 | title: Text(viewModel.title), 49 | subtitle: (isEmpty(viewModel.subTitle) 50 | ? null 51 | : Text( 52 | viewModel.subTitle, 53 | style: theme.textTheme.body1, 54 | )), 55 | trailing: Text('Connected', style: theme.textTheme.body2), 56 | ), 57 | Divider(), 58 | ], 59 | ); 60 | } 61 | 62 | Widget _buildLinkableCard( 63 | {ViewModel parentViewModel, ViewModelItem viewModel, IconData icon}) { 64 | switch (viewModel.providerName) { 65 | case 'password': 66 | return LinkCard( 67 | icon: email.providerIcon, 68 | title: viewModel.title, 69 | subTitle: viewModel.subTitle, 70 | linkAction: (context) async => await openDialog( 71 | context: context, 72 | builder: (_) => email.LinkEmailAccount( 73 | viewModel.linkableProvider, 74 | onAuthRequired: () => 75 | handleAuthenticationRequired(parentViewModel), 76 | ), 77 | ), 78 | ); 79 | case 'google': 80 | return LinkCard( 81 | icon: google.providerIcon, 82 | title: viewModel.title, 83 | subTitle: viewModel.subTitle, 84 | linkAction: (context) async { 85 | try { 86 | await viewModel.linkableProvider.linkAccount({}); 87 | } on AuthRequiredException { 88 | handleAuthenticationRequired(parentViewModel); 89 | } 90 | }, 91 | ); 92 | } 93 | return Container(); 94 | } 95 | 96 | void handleAuthenticationRequired(ViewModel viewModel) { 97 | showOkCancelDialog(() { 98 | Navigator.pop(context); 99 | 100 | showSigninPickerDialog(context, viewModel.signInProviders); 101 | }, () { 102 | Navigator.pop(context); 103 | }, 104 | context: context, 105 | caption: "Authentication Required", 106 | message: 107 | "You need to re-authenticate to be able to connect this account"); 108 | } 109 | 110 | Widget _handleCompleted(ViewModel viewModel) { 111 | return Column( 112 | crossAxisAlignment: CrossAxisAlignment.stretch, 113 | children: [ 114 | Material( 115 | color: isMaterial ? null : Theme.of(context).cardColor, 116 | elevation: isMaterial ? 4.0 : 0.5, 117 | child: Padding( 118 | padding: const EdgeInsets.all(8.0), 119 | child: Text( 120 | 'Access the same data with multiple accounts.', 121 | textAlign: TextAlign.center, 122 | ), 123 | ), 124 | ), 125 | Expanded( 126 | child: ListView.builder( 127 | padding: EdgeInsets.all(0.0), 128 | itemCount: viewModel.items.length, 129 | itemBuilder: (BuildContext context, int index) { 130 | var vm = viewModel.items.elementAt(index); 131 | if (vm.isActive) { 132 | return _buildActiveCard( 133 | viewModel: vm, icon: _icons[vm.providerName]); 134 | } else { 135 | return _buildLinkableCard( 136 | parentViewModel: viewModel, 137 | viewModel: vm, 138 | icon: _icons[vm.providerName]); 139 | } 140 | }, 141 | ), 142 | ), 143 | ], 144 | ); 145 | } 146 | 147 | Widget _handleError(error) { 148 | return Center(child: Text('An error occrued')); 149 | } 150 | 151 | Widget _handleSnapshot( 152 | BuildContext context, AsyncSnapshot snapshot) { 153 | if (snapshot.hasData) { 154 | return _handleCompleted(snapshot.data); 155 | } else if (snapshot.hasError) { 156 | return _handleError(snapshot.error); 157 | } else { 158 | return Padding( 159 | padding: const EdgeInsets.all(16.0), 160 | child: PlatformCircularProgressIndicator(), 161 | ); 162 | } 163 | } 164 | 165 | Widget _buildPage(AuthService authService) { 166 | FutureBuilder _loader; 167 | _loader = FutureBuilder( 168 | future: _viewModel.loadItems(authService), 169 | builder: (BuildContext context, AsyncSnapshot snapshot) { 170 | return _handleSnapshot(context, snapshot); 171 | }, 172 | ); 173 | return _loader; 174 | } 175 | 176 | @override 177 | Widget build(BuildContext context) { 178 | return PlatformScaffold( 179 | appBar: PlatformAppBar( 180 | title: Text('Accounts'), 181 | ), 182 | body: ScopedModelDescendant( 183 | //rebuildOnChange: false, 184 | builder: (_, child, model) => Material( 185 | color: isMaterial ? null : Theme.of(context).cardColor, 186 | child: _buildPage(model.authService), 187 | ), 188 | ), 189 | ); 190 | } 191 | } 192 | -------------------------------------------------------------------------------- /lib/core/auth/handlers/link/linkAccounts/link_accounts_view_model.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | import 'package:flutter_auth_base/flutter_auth_base.dart'; 4 | import 'package:meta/meta.dart'; 5 | 6 | class ViewModelItem { 7 | ViewModelItem( 8 | {@required this.isActive, 9 | @required this.providerName, 10 | @required this.title, 11 | this.subTitle, 12 | this.linkableProvider}); 13 | 14 | bool isActive; 15 | 16 | String providerName; 17 | String title; 18 | String subTitle; 19 | 20 | LinkableProvider linkableProvider; 21 | } 22 | 23 | class ViewModel { 24 | List items; 25 | 26 | List signInProviders = []; 27 | 28 | Future loadItems(AuthService authService) async { 29 | var viewModels = new List(); 30 | var user = await authService.currentUser(); 31 | 32 | //have the active accounts displayed first 33 | for (var authProv in user.providerAccounts) { 34 | viewModels.add(new ViewModelItem( 35 | isActive: true, 36 | providerName: authProv.providerName, 37 | title: authProv.email)); 38 | } 39 | 40 | // and then the available ones after 41 | for (var linkProv in authService.linkProviders) { 42 | if (!viewModels.any((vm) => vm.providerName == linkProv.providerName)) { 43 | viewModels.add(new ViewModelItem( 44 | isActive: false, 45 | providerName: linkProv.providerName, 46 | linkableProvider: linkProv, 47 | title: 'Connect ${linkProv.providerDisplayName}')); 48 | } 49 | } 50 | 51 | signInProviders = []; 52 | for (var acc in user.providerAccounts) { 53 | var authProvider = authService.authProviders.firstWhere( 54 | (p) => p.providerName == acc.providerName, 55 | orElse: () => null); 56 | if (authProvider != null) { 57 | signInProviders.add(authProvider); 58 | } 59 | } 60 | 61 | items = viewModels; 62 | 63 | return this; 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /lib/core/auth/handlers/link/linkAccounts/link_card.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | import 'package:meta/meta.dart'; 4 | import 'package:flutter_platform_widgets/flutter_platform_widgets.dart'; 5 | 6 | import '../../../../common/future_action_callback.dart'; 7 | import '../../../../widgets/progress_actionable_state.dart'; 8 | 9 | class LinkCard extends StatefulWidget { 10 | LinkCard( 11 | {@required this.linkAction, 12 | @required this.icon, 13 | @required this.title, 14 | this.subTitle}); 15 | 16 | final IconData icon; 17 | final String title; 18 | final String subTitle; 19 | final FutureActionCallback linkAction; 20 | 21 | @override 22 | createState() => new LinkCardState(); 23 | } 24 | 25 | class LinkCardState extends ProgressActionableState { 26 | @override 27 | void initState() { 28 | super.initState(); 29 | } 30 | 31 | bool isEmpty(String str) { 32 | return str == null || str.length == 0; 33 | } 34 | 35 | @override 36 | Widget build(BuildContext context) { 37 | var theme = Theme.of(context); 38 | return Column( 39 | children: [ 40 | ListTile( 41 | leading: Icon(widget.icon), 42 | title: isEmpty(widget.title) 43 | ? null 44 | : Text(widget.title, style: theme.textTheme.body1), 45 | subtitle: isEmpty(widget.subTitle) 46 | ? null 47 | : Text(widget.subTitle, style: theme.textTheme.caption), 48 | trailing: super.showProgress 49 | ? Padding( 50 | padding: const EdgeInsets.all(8.0), 51 | child: PlatformCircularProgressIndicator(), 52 | ) 53 | : PlatformButton( 54 | onPressed: super.showProgress 55 | ? null 56 | : () async { 57 | await super.performAction(widget.linkAction); 58 | }, 59 | child: Text('Connect'), 60 | ), 61 | ), 62 | Divider(), 63 | ], 64 | ); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /lib/core/auth/handlers/signin_accounts/signin_picker.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter/widgets.dart'; 3 | import 'package:flutter_auth_base/flutter_auth_base.dart'; 4 | import 'package:flutter_platform_widgets/flutter_platform_widgets.dart'; 5 | 6 | import '../../../widgets/progress_actionable_state.dart'; 7 | 8 | import '../email/signIn/sign_in_page.dart'; 9 | import '../email/sign_in_button.dart' as email; 10 | import '../google/sign_in_button.dart' as google; 11 | 12 | class SignInPicker extends StatefulWidget { 13 | SignInPicker({this.authProviders}); 14 | 15 | final List authProviders; 16 | 17 | @override 18 | createState() => new SignInPickerState(); 19 | } 20 | 21 | class SignInPickerState extends ProgressActionableState { 22 | @override 23 | void initState() { 24 | super.initState(); 25 | } 26 | 27 | @override 28 | Widget build(BuildContext context) { 29 | var buttons = 30 | widget.authProviders.map((prov) => _getProviderButton(prov)).toList(); 31 | 32 | return Column( 33 | mainAxisSize: MainAxisSize.min, 34 | crossAxisAlignment: CrossAxisAlignment.center, 35 | children: [ 36 | super.showProgress 37 | ? Padding( 38 | padding: const EdgeInsets.all(16.0), 39 | child: PlatformCircularProgressIndicator(), 40 | ) 41 | : SingleChildScrollView( 42 | child: ListBody( 43 | children: buttons, 44 | ), 45 | ), 46 | ], 47 | ); 48 | } 49 | 50 | Widget _getProviderButton(AuthProvider prov) { 51 | if (prov.providerName == 'google') { 52 | return new Padding( 53 | padding: const EdgeInsets.all(8.0), 54 | child: google.SignInButton(action: (_) async { 55 | await performAction((BuildContext context) async { 56 | await prov.signIn({}); 57 | Navigator.pop(context); 58 | }); 59 | }), 60 | ); 61 | } else if (prov.providerName == 'password') { 62 | return new Padding( 63 | padding: const EdgeInsets.all(8.0), 64 | child: email.SignInButton(action: (context) { 65 | Navigator.pop(context); 66 | Navigator.push( 67 | context, 68 | MaterialPageRoute( 69 | builder: (_) => SignInPassword( 70 | popRouteOnSignin: true, displaySignInButton: false), 71 | ), 72 | ); 73 | return null; 74 | }), 75 | ); 76 | } else { 77 | return Container(); 78 | } 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /lib/core/auth/handlers/signin_accounts/signin_picker_dialog.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | import 'package:flutter_auth_base/flutter_auth_base.dart'; 4 | import 'package:flutter_platform_widgets/flutter_platform_widgets.dart'; 5 | import 'package:flutter/material.dart'; 6 | 7 | import 'signin_picker.dart'; 8 | 9 | Future showSigninPickerDialog( 10 | BuildContext context, List authProviders) async { 11 | return showDialog( 12 | context: context, 13 | barrierDismissible: true, 14 | builder: (BuildContext context) { 15 | return PlatformAlertDialog( 16 | title: Text('Pick Sign-in Account'), 17 | content: SignInPicker(authProviders: authProviders), 18 | actions: [ 19 | PlatformDialogAction( 20 | child: PlatformText('Cancel'), 21 | onPressed: () => Navigator.pop(context), 22 | ), 23 | ], 24 | ); 25 | }, 26 | ); 27 | } 28 | -------------------------------------------------------------------------------- /lib/core/auth/handlers/user/closeAccount/close_account_page.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:flutter_auth_base/flutter_auth_base.dart'; 4 | import 'package:scoped_model/scoped_model.dart'; 5 | import 'package:flutter_platform_widgets/flutter_platform_widgets.dart'; 6 | 7 | import '../../../../widgets/progress_actionable_state.dart'; 8 | import '../../../../app_model.dart'; 9 | import '../../../../widgets/modalAppBar.dart'; 10 | import '../../../../dialogs/show_ok_cancel_dialog.dart'; 11 | 12 | import '../../signin_accounts/signin_picker_dialog.dart'; 13 | 14 | class CloseAccount extends StatefulWidget { 15 | @override 16 | createState() => new CloseAccountState(); 17 | } 18 | 19 | class CloseAccountState extends ProgressActionableState { 20 | @override 21 | void initState() { 22 | super.initState(); 23 | } 24 | 25 | Future _closeAccount(AuthService authService) async { 26 | var user = await authService.currentUser(); 27 | 28 | try { 29 | await authService.closeAccount({}); 30 | } on AuthRequiredException { 31 | var signInProviders = _getSignInProviders(authService, user); 32 | handleAuthenticationRequired(signInProviders); 33 | } 34 | } 35 | 36 | List _getSignInProviders( 37 | AuthService authService, AuthUser user) { 38 | List signInProviders = []; 39 | for (var acc in user.providerAccounts) { 40 | var authProvider = authService.authProviders.firstWhere( 41 | (p) => p.providerName == acc.providerName, 42 | orElse: () => null); 43 | if (authProvider != null) { 44 | signInProviders.add(authProvider); 45 | } 46 | } 47 | 48 | return signInProviders; 49 | } 50 | 51 | void handleAuthenticationRequired(List signInProviders) { 52 | showOkCancelDialog(() { 53 | Navigator.pop(context); 54 | 55 | showSigninPickerDialog(context, signInProviders); 56 | }, () { 57 | Navigator.pop(context); 58 | }, 59 | context: context, 60 | caption: "Authentication Required", 61 | message: 62 | "You need to re-authenticate to be able to remove this account"); 63 | } 64 | 65 | Widget _progressIndicator() { 66 | return super.showProgress 67 | ? Padding( 68 | padding: EdgeInsets.all(16.0), 69 | child: PlatformCircularProgressIndicator()) 70 | : Container(); 71 | } 72 | 73 | void _confirmAndActionClosingAccount(AuthService authService) { 74 | super.performAction((_) async => await showOkCancelDialog(() { 75 | Navigator.pop(context); 76 | _closeAccount(authService); 77 | }, () => Navigator.pop(context), 78 | caption: 'Confirm', 79 | message: 80 | 'Are you sure you want to permanently close your account and delete all data you created? You may be prompted to reauthenticate.', 81 | context: context)); 82 | } 83 | 84 | Widget _build(AuthService authService) { 85 | return SingleChildScrollView( 86 | child: Column(children: [ 87 | Padding( 88 | padding: const EdgeInsets.all(16.0), 89 | child: Text( 90 | 'Closing your account will permanently delete any data you created. This cannot be undone'), 91 | ), 92 | Padding( 93 | padding: const EdgeInsets.only(top: 32.0), 94 | child: Text( 95 | 'Do you wish to continue?', 96 | style: TextStyle(fontWeight: FontWeight.bold), 97 | ), 98 | ), 99 | Padding( 100 | padding: const EdgeInsets.all(16.0), 101 | child: PlatformButton( 102 | child: Text('Yes, close my account'), 103 | onPressed: super.showProgress 104 | ? null 105 | : () => _confirmAndActionClosingAccount(authService)), 106 | ), 107 | _progressIndicator(), 108 | ])); 109 | } 110 | 111 | // Form _asForm(Widget widget) { 112 | // return Form(autovalidate: true, key: super.formKey, child: widget); 113 | // } 114 | 115 | @override 116 | Widget build(BuildContext context) { 117 | return ScopedModelDescendant( 118 | rebuildOnChange: false, 119 | builder: (_, child, model) => PlatformScaffold( 120 | appBar: ModalAppBar( 121 | title: Text('Close Account'), 122 | hideAccept: true, 123 | closeAction: () => Navigator.maybePop(context), 124 | ), 125 | body: Padding( 126 | padding: const EdgeInsets.symmetric(horizontal: 16.0), 127 | child: Material( 128 | color: isMaterial ? null : Theme.of(context).cardColor, 129 | child: _build(model.authService), 130 | ), 131 | ), 132 | ), 133 | ); 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /lib/core/auth/handlers/user/displayName/change_display_name_page.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:flutter_auth_base/flutter_auth_base.dart'; 4 | import 'package:scoped_model/scoped_model.dart'; 5 | import 'package:flutter_platform_widgets/flutter_platform_widgets.dart'; 6 | 7 | import 'change_display_name_view_model.dart'; 8 | import '../../../../widgets/form_progress_actionable_state.dart'; 9 | import '../../../../app_model.dart'; 10 | import '../../../../widgets/modalAppBar.dart'; 11 | 12 | class ChangeDisplayName extends StatefulWidget { 13 | ChangeDisplayName({this.displayName}); 14 | 15 | final String displayName; 16 | //final AuthService authService; 17 | 18 | @override 19 | createState() => new ChangeDisplayNameState(); 20 | } 21 | 22 | class ChangeDisplayNameState 23 | extends FormProgressActionableState { 24 | @override 25 | void initState() { 26 | super.initState(); 27 | 28 | _viewModel = new ViewModel(displayName: widget.displayName); 29 | } 30 | 31 | ViewModel _viewModel; 32 | 33 | Future _setDisplayName(AuthService authService) async { 34 | _viewModel.validateAll(); 35 | await authService.setUserDisplayName(_viewModel.displayName); 36 | 37 | Navigator.pop(context); 38 | } 39 | 40 | Widget _displayNameField() { 41 | return ListTile( 42 | leading: Icon( 43 | Icons.person, 44 | ), 45 | title: TextFormField( 46 | initialValue: _viewModel.displayName, 47 | decoration: new InputDecoration(labelText: 'Display Name'), 48 | validator: _viewModel.validateDisplayName, 49 | onSaved: (val) => _viewModel.displayName = val), 50 | ); 51 | } 52 | 53 | Widget _progressIndicator() { 54 | return super.showProgress 55 | ? Padding( 56 | padding: EdgeInsets.all(16.0), 57 | child: PlatformCircularProgressIndicator()) 58 | : Container(); 59 | } 60 | 61 | Widget _build() { 62 | return SingleChildScrollView( 63 | child: Column(children: [ 64 | _displayNameField(), 65 | _progressIndicator(), 66 | ])); 67 | } 68 | 69 | Form _asForm(Widget widget) { 70 | return Form(autovalidate: true, key: super.formKey, child: widget); 71 | } 72 | 73 | @override 74 | Widget build(BuildContext context) { 75 | return ScopedModelDescendant( 76 | rebuildOnChange: false, 77 | builder: (_, child, model) => PlatformScaffold( 78 | appBar: ModalAppBar( 79 | title: Text('Change Display Name'), 80 | acceptAction: super.showProgress 81 | ? null 82 | : () => super.validateAndSubmit( 83 | (_) async => await _setDisplayName(model.authService), 84 | ), 85 | closeAction: 86 | super.showProgress ? null : () => Navigator.maybePop(context), 87 | ), 88 | body: Padding( 89 | padding: const EdgeInsets.symmetric(horizontal: 16.0), 90 | child: Material( 91 | color: isMaterial ? null : Theme.of(context).cardColor, 92 | child: _asForm( 93 | _build(), 94 | ), 95 | ), 96 | ), 97 | ), 98 | ); 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /lib/core/auth/handlers/user/displayName/change_display_name_view_model.dart: -------------------------------------------------------------------------------- 1 | import '../../../../validators/validator.dart'; 2 | import '../../../../validators/display_name_validator.dart' 3 | as displayNameValidator; 4 | 5 | import '../../../../common/app_exception.dart'; 6 | 7 | class ViewModel { 8 | ViewModel({this.displayName}) { 9 | _validator = new Validator(); 10 | _validator.validations.add(() => validateDisplayName(displayName)); 11 | } 12 | 13 | String displayName; 14 | 15 | Validator _validator; 16 | 17 | String validateDisplayName(String value) => 18 | displayNameValidator.validate(value); 19 | 20 | void validateAll() { 21 | var errors = _validator.validate(); 22 | if (errors != null && errors.length > 0) { 23 | throw new AppException(errors); 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /lib/core/auth/handlers/user/termsAcceptance/terms_accept_modal.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:flutter_auth_base/flutter_auth_base.dart'; 4 | import 'package:scoped_model/scoped_model.dart'; 5 | import 'package:flutter_platform_widgets/flutter_platform_widgets.dart'; 6 | import 'package:url_launcher/url_launcher.dart'; 7 | 8 | import '../../../../dialogs/show_error_dialog.dart'; 9 | import '../../../../widgets/email_image_circle_avatar.dart'; 10 | import '../../../../widgets/modalAppBar.dart'; 11 | import '../../../../app_model.dart'; 12 | import '../../../../app_info.dart'; 13 | 14 | class TermsAcceptModal extends StatefulWidget { 15 | @override 16 | createState() => new TermsAcceptModalState(); 17 | } 18 | 19 | class TermsAcceptModalState extends State { 20 | @override 21 | void initState() { 22 | super.initState(); 23 | } 24 | 25 | bool termsAccepted = false; 26 | final avatarKey = GlobalKey(); 27 | 28 | Future _launchURL(String url) async { 29 | try { 30 | if (await canLaunch(url)) { 31 | await launch(url); 32 | } else { 33 | throw 'Could not launch $url'; 34 | } 35 | } catch (error) { 36 | showErrorDialog(context, 'An error occured opening that link'); 37 | } 38 | } 39 | 40 | Widget _logoGravatar(AppInfo appInfo, AuthService authService) { 41 | return Padding( 42 | padding: const EdgeInsets.all(8.0), 43 | child: EmailImageCircleAvatar( 44 | checkIfImageExists: true, 45 | key: avatarKey, 46 | imageSize: 150, 47 | backgroundColor: Colors.white70, 48 | defaultImage: AssetImage(appInfo.appIconPath), 49 | imageProvider: authService.preAuthPhotoProvider), 50 | ); 51 | } 52 | 53 | Widget _termsAcceptance(AppInfo appInfo) { 54 | return Container( 55 | padding: const EdgeInsets.all(8.0), 56 | child: Row( 57 | children: [ 58 | Checkbox( 59 | value: termsAccepted, 60 | onChanged: (val) => setState(() { 61 | termsAccepted = val; 62 | })), 63 | Expanded( 64 | child: Text('By checking this box you agree to the following:'), 65 | ), 66 | ], 67 | ), 68 | ); 69 | } 70 | 71 | Widget _termsButtons(AppInfo appInfo) { 72 | return Column( 73 | children: [ 74 | new Padding( 75 | padding: const EdgeInsets.all(8.0), 76 | child: PlatformButton( 77 | child: Text('Terms of Service'), 78 | onPressed: () async => await _launchURL(appInfo.termsOfServiceUrl), 79 | ), 80 | ), 81 | new Padding( 82 | padding: const EdgeInsets.all(8.0), 83 | child: PlatformButton( 84 | child: Text('Privacy Policy'), 85 | onPressed: () async => await _launchURL(appInfo.privacyPolicyUrl), 86 | ), 87 | ), 88 | ], 89 | ); 90 | } 91 | 92 | void _accept() { 93 | if (!termsAccepted) { 94 | showErrorDialog(context, 'You need to agree first.'); 95 | } else { 96 | Navigator.maybePop(context, termsAccepted); 97 | } 98 | } 99 | 100 | Widget _buildPage(AppInfo appInfo, AuthService authService) { 101 | return Center( 102 | child: Column( 103 | children: [ 104 | _logoGravatar(appInfo, authService), 105 | _termsAcceptance(appInfo), 106 | _termsButtons(appInfo), 107 | ], 108 | ), 109 | ); 110 | } 111 | 112 | @override 113 | Widget build(BuildContext context) { 114 | return PlatformScaffold( 115 | appBar: ModalAppBar( 116 | title: Text('T & C'), 117 | closeAction: () => Navigator.maybePop(context, false), 118 | acceptText: 'Accept', 119 | acceptAction: () => _accept(), 120 | android: (_) => MaterialAppBarData( 121 | actions: [ 122 | FlatButton( 123 | child: Text( 124 | 'I Accept', 125 | style: TextStyle( 126 | fontWeight: FontWeight.bold, 127 | color: 128 | Theme.of(context).primaryTextTheme.headline.color), 129 | ), 130 | onPressed: () => _accept(), 131 | ) 132 | ], 133 | ), 134 | ), 135 | body: Material( 136 | color: isMaterial ? null : Theme.of(context).cardColor, 137 | child: ScopedModelDescendant( 138 | rebuildOnChange: false, 139 | builder: (_, child, model) => Padding( 140 | padding: const EdgeInsets.symmetric(horizontal: 16.0), 141 | child: _buildPage(model.appInfo, model.authService)), 142 | ), 143 | ), 144 | ); 145 | } 146 | } 147 | -------------------------------------------------------------------------------- /lib/core/auth/mock/mock_email_provider.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | import 'package:flutter_auth_base/flutter_auth_base.dart'; 3 | 4 | import 'mock_user.dart'; 5 | import 'mock_user_account.dart'; 6 | 7 | class MockEmailProvider extends AuthProvider implements LinkableProvider { 8 | MockEmailProvider(this.service); 9 | 10 | AuthService service; 11 | 12 | @override 13 | String get providerName => 'password'; 14 | 15 | @override 16 | String get providerDisplayName => "Email with Password"; 17 | 18 | @override 19 | Future create(Map args, 20 | {termsAccepted = false}) async { 21 | await new Future.delayed(const Duration(milliseconds: 1000), () => {}); 22 | 23 | //We are meant to display a confirmation of terms and privacy policy 24 | //for all newly added users. Therefore this is mocking that intent. 25 | //The called need to set the accepted flag to not raise this exception 26 | if (!termsAccepted) { 27 | throw new UserAcceptanceRequiredException(args); 28 | } 29 | 30 | await new Future.delayed(const Duration(milliseconds: 1000), () => {}); 31 | 32 | var user = new MockUser() 33 | ..email = args['email'] 34 | ..displayName = 'Mocked'; 35 | 36 | service.authUserChanged.value = user; 37 | return user; 38 | } 39 | 40 | @override 41 | Future signIn(Map args, 42 | {termsAccepted = false}) async { 43 | await new Future.delayed(const Duration(milliseconds: 1000), () => {}); 44 | 45 | var user = new MockUser() 46 | ..email = args['email'] 47 | ..displayName = 'Mocked'; 48 | 49 | service.authUserChanged.value = user; 50 | return user; 51 | } 52 | 53 | @override 54 | Future sendPasswordReset(Map args) async { 55 | await new Future.delayed(const Duration(milliseconds: 1000), () => {}); 56 | 57 | return service.authUserChanged.value; 58 | } 59 | 60 | @override 61 | Future changePassword(Map args) async { 62 | await new Future.delayed(const Duration(milliseconds: 1000), () => {}); 63 | 64 | return service.authUserChanged.value; 65 | } 66 | 67 | @override 68 | Future changePrimaryIdentifier(Map args) async { 69 | var currentUser = service.authUserChanged.value; 70 | 71 | await new Future.delayed(const Duration(milliseconds: 1000), () => {}); 72 | 73 | return service.authUserChanged.value = new MockUser() 74 | ..email = args['newEmail'] 75 | ..displayName = currentUser.displayName 76 | ..photoUrl = currentUser.photoUrl 77 | ..isEmailVerified = 78 | false //pretend that changing email also requires validation 79 | ..providerAccounts = 80 | new List.from(currentUser.providerAccounts); 81 | } 82 | 83 | @override 84 | Future sendVerification(Map args) async { 85 | var currentUser = service.authUserChanged.value; 86 | 87 | await new Future.delayed(const Duration(milliseconds: 1000), () => {}); 88 | 89 | return service.authUserChanged.value = new MockUser() 90 | ..email = currentUser.email 91 | ..displayName = currentUser.displayName 92 | ..photoUrl = currentUser.photoUrl 93 | ..isEmailVerified = true 94 | ..providerAccounts = 95 | new List.from(currentUser.providerAccounts); 96 | } 97 | 98 | @override 99 | Future linkAccount(Map args) async { 100 | if (args['email'] == 'auth@required') { 101 | await new Future.delayed(const Duration(milliseconds: 1000), () => {}); 102 | throw new AuthRequiredException(); 103 | } 104 | 105 | var currentUser = service.authUserChanged.value; 106 | 107 | await new Future.delayed(const Duration(milliseconds: 1000), () => {}); 108 | 109 | var providers = 110 | new List.from(currentUser.providerAccounts); 111 | providers.add(new MockUserPasswordAccount()); 112 | 113 | service.authUserChanged.value = new MockUser() 114 | ..email = currentUser.email 115 | ..displayName = currentUser.displayName 116 | ..photoUrl = currentUser.photoUrl 117 | ..isEmailVerified = true 118 | ..providerAccounts = providers; 119 | return service.authUserChanged.value; 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /lib/core/auth/mock/mock_google_provider.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_auth_base/flutter_auth_base.dart'; 2 | import 'dart:async'; 3 | 4 | import 'mock_user.dart'; 5 | import 'mock_user_account.dart'; 6 | 7 | class MockGoogleProvider extends AuthProvider implements LinkableProvider { 8 | MockGoogleProvider(this.service); 9 | 10 | AuthService service; 11 | 12 | @override 13 | String get providerName => 'google'; 14 | 15 | @override 16 | String get providerDisplayName => "Google"; 17 | 18 | bool _linkAccountRequiredAuth = true; 19 | @override 20 | Future linkAccount(Map args) async { 21 | if (_linkAccountRequiredAuth) { 22 | await new Future.delayed(const Duration(milliseconds: 1000), () => {}); 23 | _linkAccountRequiredAuth = false; 24 | throw new AuthRequiredException(); 25 | } 26 | 27 | var currentUser = service.authUserChanged.value; 28 | 29 | await new Future.delayed(const Duration(milliseconds: 1000), () => {}); 30 | 31 | var providers = 32 | new List.from(currentUser.providerAccounts); 33 | providers.add(new MockUserGoogleAccount()); 34 | 35 | service.authUserChanged.value = new MockUser() 36 | ..email = currentUser.email 37 | ..displayName = currentUser.displayName 38 | ..photoUrl = currentUser.photoUrl 39 | ..isEmailVerified = true 40 | ..providerAccounts = providers; 41 | return service.authUserChanged.value; 42 | } 43 | 44 | @override 45 | Future signIn(Map args, 46 | {termsAccepted = false}) async { 47 | await new Future.delayed(const Duration(milliseconds: 1000), () => {}); 48 | 49 | //We are meant to display a confirmation of terms and privacy policy 50 | //for all newly added users. Therefore this is mocking that intent. 51 | //The called need to set the accepted flag to not raise this exception 52 | if (!termsAccepted) { 53 | throw new UserAcceptanceRequiredException( 54 | {'accessId': '1234', 'uid': 'abcd'}); 55 | } 56 | 57 | await new Future.delayed(const Duration(milliseconds: 1000), () => {}); 58 | 59 | print('******** Google sign in ********'); 60 | var google = new MockUserGoogleAccount(); 61 | var providers = new List()..add(google); 62 | 63 | return service.authUserChanged.value = new MockUser() 64 | ..email = google.email 65 | ..displayName = google.displayName 66 | ..photoUrl = google.photoUrl 67 | ..isEmailVerified = true 68 | ..providerAccounts = providers; 69 | } 70 | 71 | @override 72 | Future changePassword(Map args) async { 73 | throw new UnsupportedError('Cannot change Google password '); 74 | } 75 | 76 | @override 77 | Future changePrimaryIdentifier(Map args) async { 78 | throw new UnsupportedError('Cannot change Google email '); 79 | } 80 | 81 | @override 82 | Future create(Map args, 83 | {termsAccepted = false}) async { 84 | throw new UnsupportedError('Cannot create Google password '); 85 | } 86 | 87 | @override 88 | Future sendPasswordReset(Map args) async { 89 | throw new UnsupportedError('Cannot reset Google password '); 90 | } 91 | 92 | @override 93 | Future sendVerification(Map args) async { 94 | throw new UnsupportedError('Cannot send Google verification email '); 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /lib/core/auth/mock/mock_service.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | import 'package:flutter/foundation.dart'; 3 | import 'package:flutter_auth_base/flutter_auth_base.dart'; 4 | 5 | import 'mock_user.dart'; 6 | import 'mock_email_provider.dart'; 7 | import 'mock_google_provider.dart'; 8 | 9 | class MockService extends AuthService { 10 | MockService() { 11 | var email = new MockEmailProvider(this); 12 | var google = new MockGoogleProvider(this); 13 | authProviders.add(email); 14 | authProviders.add(google); 15 | linkProviders.add(email); 16 | linkProviders.add(google); 17 | } 18 | 19 | @override 20 | AuthOptions options = new AuthOptions(); 21 | 22 | final ValueNotifier _authChangeNotifier = 23 | new ValueNotifier( 24 | new MockUser()); //..email = 'mocked@mocked.com'); 25 | 26 | @override 27 | ValueNotifier get authUserChanged => _authChangeNotifier; 28 | 29 | @override 30 | List authProviders = new List(); 31 | 32 | @override 33 | List linkProviders = new List(); 34 | 35 | @override 36 | PhotoUrlProvider preAuthPhotoProvider; 37 | 38 | @override 39 | PhotoUrlProvider postAuthPhotoProvider; 40 | 41 | @override 42 | Future currentUser() async { 43 | return _authChangeNotifier.value; 44 | } 45 | 46 | @override 47 | Future currentUserToken({bool refresh = false}) async { 48 | if (_authChangeNotifier.value.isValid) { 49 | return _authChangeNotifier.value.email; 50 | } 51 | return null; 52 | } 53 | 54 | @override 55 | Future refreshUser() async { 56 | await new Future.delayed(const Duration(milliseconds: 1000), () => {}); 57 | 58 | return _authChangeNotifier.value; 59 | } 60 | 61 | @override 62 | Future setUserDisplayName(String name) async { 63 | var user = _authChangeNotifier.value; 64 | 65 | await new Future.delayed(const Duration(milliseconds: 1000), () => {}); 66 | 67 | return _authChangeNotifier.value = new MockUser() 68 | ..displayName = name 69 | ..email = user.email 70 | ..isEmailVerified = user.isEmailVerified 71 | ..photoUrl = user.photoUrl 72 | ..providerAccounts = 73 | new List.from(user.providerAccounts); 74 | } 75 | 76 | @override 77 | Future signOut() async { 78 | await new Future.delayed(const Duration(milliseconds: 1000), () => {}); 79 | 80 | _authChangeNotifier.value = new MockUser()..email = null; 81 | } 82 | 83 | @override 84 | Future closeAccount(Map reauthenticationArgs) async { 85 | if (DateTime.now().minute % 2 == 0) { 86 | //if even minute 87 | throw new AuthRequiredException(); 88 | } 89 | 90 | await new Future.delayed(const Duration(milliseconds: 1000), () => {}); 91 | 92 | _authChangeNotifier.value = new MockUser()..email = null; 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /lib/core/auth/mock/mock_user.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_auth_base/flutter_auth_base.dart'; 2 | 3 | import 'mock_user_account.dart'; 4 | 5 | class MockUser extends AuthUser { 6 | @override 7 | String displayName = "Mocked"; 8 | 9 | @override 10 | String email; 11 | 12 | @override 13 | bool isEmailVerified = false; 14 | 15 | @override 16 | bool get isValid => email != null; 17 | 18 | @override 19 | String get uid => 'mockedid'; 20 | 21 | @override 22 | String photoUrl; 23 | 24 | @override 25 | List providerAccounts = new List() 26 | ..add(new MockUserPasswordAccount()); 27 | } 28 | -------------------------------------------------------------------------------- /lib/core/auth/mock/mock_user_account.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_auth_base/flutter_auth_base.dart'; 2 | 3 | class MockUserPasswordAccount extends AuthUserAccount { 4 | @override 5 | String get displayName => 'Mocked Name'; 6 | 7 | @override 8 | String get email => 'some@some.com'; 9 | 10 | @override 11 | String get providerName => 'password'; 12 | 13 | @override 14 | String get photoUrl => null; 15 | } 16 | 17 | class MockUserGoogleAccount extends AuthUserAccount { 18 | MockUserGoogleAccount() 19 | : super( 20 | canChangeDisplayName: false, 21 | canChangeEmail: false, 22 | canChangePassword: false); 23 | 24 | @override 25 | String get displayName => 'Mocked Google Name'; 26 | 27 | @override 28 | String get email => 'some@gmail.com'; 29 | 30 | @override 31 | String get providerName => 'google'; 32 | 33 | @override 34 | String get photoUrl => null; 35 | } 36 | -------------------------------------------------------------------------------- /lib/core/common/actionable.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | import 'package:flutter/material.dart'; 4 | 5 | import 'future_action_callback.dart'; 6 | 7 | //typedef Future FutureContextCallback(BuildContext context); 8 | 9 | abstract class Actionable { 10 | Future performAction(FutureActionCallback action); 11 | } 12 | -------------------------------------------------------------------------------- /lib/core/common/app_exception.dart: -------------------------------------------------------------------------------- 1 | class AppException implements Exception { 2 | AppException(this.message); 3 | 4 | final String message; 5 | 6 | @override 7 | String toString() { 8 | return message; 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /lib/core/common/dialog.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | import 'package:flutter/material.dart'; 4 | import 'package:flutter/widgets.dart'; 5 | import 'package:meta/meta.dart'; 6 | 7 | import '../widgets/screen_aware_padding.dart'; 8 | 9 | Future openDialog( 10 | {@required BuildContext context, @required WidgetBuilder builder}) { 11 | var size = MediaQuery.of(context).size; 12 | 13 | bool isMobile = size.width < 660 || size.height < 660; 14 | 15 | if (isMobile) { 16 | //mobile view we use PageRoute 17 | return Navigator.push( 18 | context, MaterialPageRoute(fullscreenDialog: true, builder: builder)); 19 | } else { 20 | //tablet we use showDialog with Screen padding 21 | return showDialog( 22 | context: context, 23 | builder: (ctx) => ScreenAwarePadding(child: builder(ctx)), 24 | barrierDismissible: false); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /lib/core/common/future_action_callback.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | typedef Future FutureActionCallback(T value); 4 | -------------------------------------------------------------------------------- /lib/core/common/md5.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | import 'package:crypto/crypto.dart' as crypto; 3 | import 'package:convert/convert.dart'; 4 | 5 | String md5(String input) { 6 | var bytes = utf8.encode(input); 7 | var digest = crypto.md5.convert(bytes); 8 | var hash = hex.encode(digest.bytes); 9 | 10 | return hash; 11 | } 12 | -------------------------------------------------------------------------------- /lib/core/common/throttle.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | typedef void ValueCallback(dynamic value); 4 | typedef dynamic ResultCallback(); 5 | 6 | class Throttler { 7 | Duration delay; 8 | ValueCallback callback; 9 | ResultCallback argCallback; 10 | bool noTrailing; 11 | 12 | Timer timer; 13 | 14 | Throttler( 15 | {this.delay, this.callback, this.argCallback, this.noTrailing = false}); 16 | 17 | DateTime now = new DateTime.now(); 18 | 19 | void throttle() { 20 | Duration elapsed = new DateTime.now().difference(now); 21 | 22 | void exec() { 23 | now = new DateTime.now(); 24 | callback(argCallback()); 25 | } 26 | 27 | if (elapsed.compareTo(delay) >= 0) { 28 | exec(); 29 | } 30 | 31 | if (timer != null) timer.cancel(); 32 | 33 | if (noTrailing == false) { 34 | timer = new Timer(delay, exec); 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /lib/core/dialogs/app_info_dialog.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart' hide AboutDialog; 2 | import 'package:meta/meta.dart'; 3 | 4 | import '../widgets/about_dialog.dart'; 5 | 6 | void showAppInfoDialog( 7 | {@required BuildContext context, 8 | @required String appName, 9 | @required String appVersion, 10 | @required String appIconPath, 11 | String applicationLegalese, 12 | List addtionalWidgets}) async { 13 | var icon = new AssetImage(appIconPath); 14 | 15 | showDialog( 16 | context: context, 17 | builder: (BuildContext context) { 18 | return new AboutDialog( 19 | applicationName: appName, 20 | applicationVersion: "version $appVersion", 21 | applicationIcon: new Image( 22 | image: icon, 23 | height: 64.0, 24 | width: 64.0, 25 | ), 26 | applicationLegalese: applicationLegalese, 27 | children: addtionalWidgets ?? []); 28 | }, 29 | ); 30 | } 31 | -------------------------------------------------------------------------------- /lib/core/dialogs/show_error_dialog.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | import 'package:flutter_platform_widgets/flutter_platform_widgets.dart'; 4 | import 'package:flutter/material.dart'; 5 | 6 | Future showErrorDialog(BuildContext context, String message) async { 7 | return showDialog( 8 | context: context, 9 | barrierDismissible: true, 10 | builder: (BuildContext context) { 11 | return PlatformAlertDialog( 12 | title: Text('Oops'), 13 | content: SingleChildScrollView( 14 | child: ListBody( 15 | children: [ 16 | Text(message ?? 'An unknown error occured'), 17 | ], 18 | ), 19 | ), 20 | actions: [ 21 | PlatformDialogAction( 22 | child: Text('OK'), 23 | onPressed: () { 24 | Navigator.pop(context); 25 | }, 26 | ), 27 | ], 28 | ); 29 | }, 30 | ); 31 | } 32 | -------------------------------------------------------------------------------- /lib/core/dialogs/show_info_dialog.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | import 'package:flutter_platform_widgets/flutter_platform_widgets.dart'; 3 | 4 | import 'package:flutter/material.dart'; 5 | import 'package:meta/meta.dart'; 6 | 7 | Future showInfoDialog(VoidCallback onOk, 8 | {String okText = 'OK', 9 | @required String message, 10 | @required String caption, 11 | @required BuildContext context}) async { 12 | return showDialog( 13 | context: context, 14 | barrierDismissible: true, 15 | builder: (BuildContext context) { 16 | return PlatformAlertDialog( 17 | title: Text(caption), 18 | content: SingleChildScrollView( 19 | child: ListBody( 20 | children: [ 21 | Text(message), 22 | ], 23 | ), 24 | ), 25 | actions: [ 26 | PlatformDialogAction(child: Text(okText), onPressed: () => onOk()), 27 | ], 28 | ); 29 | }, 30 | ); 31 | } 32 | -------------------------------------------------------------------------------- /lib/core/dialogs/show_ok_cancel_dialog.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | import 'package:flutter_platform_widgets/flutter_platform_widgets.dart'; 3 | 4 | import 'package:flutter/material.dart'; 5 | import 'package:meta/meta.dart'; 6 | 7 | Future showOkCancelDialog(VoidCallback onOk, VoidCallback onCancel, 8 | {@required String message, 9 | @required String caption, 10 | @required BuildContext context}) async { 11 | return showDialog( 12 | context: context, 13 | barrierDismissible: true, 14 | builder: (BuildContext context) { 15 | return PlatformAlertDialog( 16 | title: Text(caption), 17 | content: SingleChildScrollView( 18 | child: ListBody( 19 | children: [ 20 | Text(message), 21 | ], 22 | ), 23 | ), 24 | actions: [ 25 | PlatformDialogAction( 26 | child: PlatformText('Cancel'), 27 | onPressed: () => onCancel(), 28 | ), 29 | PlatformDialogAction(child: Text('OK'), onPressed: () => onOk()), 30 | ], 31 | ); 32 | }, 33 | ); 34 | } 35 | -------------------------------------------------------------------------------- /lib/core/imageProviders/combined_image_provider.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | import 'package:flutter_auth_base/flutter_auth_base.dart'; 4 | 5 | class CombinedPhotoProvider extends PhotoUrlProvider { 6 | List _providers = new List(); 7 | 8 | void add(PhotoUrlProvider provider) { 9 | _providers.add(provider); 10 | } 11 | 12 | @override 13 | Future emailToPhotoUrl(String email, 14 | {int size: 100, bool checkIfImageExists}) async { 15 | for (var prov in _providers) { 16 | try { 17 | var info = await prov.emailToPhotoUrl(email, 18 | size: size, checkIfImageExists: checkIfImageExists); 19 | 20 | if (info.isValid) { 21 | return info; 22 | } 23 | } catch (error) { 24 | //dont let exceptions destory a chance to get te image from another provider 25 | } 26 | } 27 | 28 | return new PhotoUrlInfo(); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /lib/core/imageProviders/gravatar_provider.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | import 'dart:io'; 3 | 4 | import 'package:flutter_auth_base/flutter_auth_base.dart'; 5 | import '../common/md5.dart'; 6 | 7 | enum ImageType { 8 | None, 9 | MysteryMan, 10 | Identicon, 11 | MonsterId, 12 | Wavatar, 13 | Retro, 14 | Robohash, 15 | Error 16 | } 17 | 18 | class GravatarProvider extends PhotoUrlProvider { 19 | GravatarProvider({this.missingImageType = ImageType.None}); 20 | 21 | final ImageType missingImageType; 22 | 23 | @override 24 | Future emailToPhotoUrl(String email, 25 | {int size = 100, bool checkIfImageExists = false}) async { 26 | if (email != null) { 27 | var hash = md5(email.trim().toLowerCase()); 28 | 29 | if (checkIfImageExists) { 30 | //we use http error code 404 to get the http response 31 | var url = 32 | 'https://www.gravatar.com/avatar/$hash?d=404&s=${size.toString()}&r=g'; 33 | var hasImage = await _haveValidImage(url); 34 | if (hasImage) { 35 | return new PhotoUrlInfo(url: url); 36 | } else { 37 | return new PhotoUrlInfo(); 38 | } 39 | } else { 40 | var url; 41 | switch (missingImageType) { 42 | case ImageType.None: 43 | url = 44 | 'https://www.gravatar.com/avatar/$hash?s=${size.toString()}&r=g&d=blank'; 45 | break; 46 | case ImageType.MysteryMan: 47 | url = 48 | 'https://www.gravatar.com/avatar/$hash?s=${size.toString()}&r=g&d=mm'; 49 | break; 50 | case ImageType.Identicon: 51 | url = 52 | 'https://www.gravatar.com/avatar/$hash?s=${size.toString()}&r=g&d=identicon'; 53 | break; 54 | case ImageType.MonsterId: 55 | url = 56 | 'https://www.gravatar.com/avatar/$hash?s=${size.toString()}&r=g&d=monsterid'; 57 | break; 58 | case ImageType.Wavatar: 59 | url = 60 | 'https://www.gravatar.com/avatar/$hash?s=${size.toString()}&r=g&d=wavatar'; 61 | break; 62 | case ImageType.Retro: 63 | url = 64 | 'https://www.gravatar.com/avatar/$hash?s=${size.toString()}&r=g&d=retro'; 65 | break; 66 | case ImageType.Robohash: 67 | url = 68 | 'https://www.gravatar.com/avatar/$hash?s=${size.toString()}&r=g&d=robohash'; 69 | break; 70 | case ImageType.Error: 71 | url = 72 | 'https://www.gravatar.com/avatar/$hash?s=${size.toString()}&r=g&d=404'; 73 | break; 74 | } 75 | 76 | return new PhotoUrlInfo(url: url); 77 | } 78 | } 79 | return new PhotoUrlInfo(); 80 | } 81 | 82 | Future _haveValidImage(String url) async { 83 | var httpClient = new HttpClient(); 84 | try { 85 | var req = await httpClient.getUrl(Uri.parse(url)); 86 | 87 | var resp = await req.close(); 88 | 89 | return resp.statusCode == HttpStatus.OK; 90 | } finally { 91 | httpClient.close(); 92 | } 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /lib/core/imageProviders/user_photo_url_provider.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_auth_base/flutter_auth_base.dart'; 2 | import 'package:meta/meta.dart'; 3 | import 'dart:async'; 4 | 5 | class AuthUserImageProvider extends PhotoUrlProvider { 6 | AuthUserImageProvider({@required this.service}); 7 | 8 | final AuthService service; 9 | 10 | @override 11 | Future emailToPhotoUrl(String email, 12 | {int size: 100, bool checkIfImageExists}) async { 13 | var user = await service.currentUser(); 14 | 15 | if (user != null) { 16 | return new PhotoUrlInfo(url: user.photoUrl); 17 | } 18 | return new PhotoUrlInfo(); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /lib/core/pages/drawer_page.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | import 'package:async_loader/async_loader.dart'; 4 | import 'package:flutter/material.dart'; 5 | import 'package:flutter_auth_base/flutter_auth_base.dart'; 6 | import 'package:scoped_model/scoped_model.dart'; 7 | 8 | import '../app_info.dart'; 9 | import '../app_model.dart'; 10 | import 'profile_base_state.dart'; 11 | 12 | import '../dialogs/app_info_dialog.dart' as app; 13 | 14 | class DrawerPage extends StatefulWidget { 15 | DrawerPage({this.licenceAdditionalWidgets}); 16 | 17 | final List licenceAdditionalWidgets; 18 | 19 | @override 20 | createState() => new DrawerPageState(); 21 | } 22 | 23 | class DrawerPageState extends ProfileBaseState { 24 | @override 25 | Widget aboutItem(AppInfo appInfo) { 26 | return super.listItem( 27 | 'About', 28 | () => app.showAppInfoDialog( 29 | appIconPath: appInfo.appIconPath, 30 | appName: appInfo.appName, 31 | appVersion: appInfo.appVersion, 32 | applicationLegalese: appInfo.applicationLegalese, 33 | context: context, 34 | addtionalWidgets: widget.licenceAdditionalWidgets), 35 | ); 36 | } 37 | 38 | Widget _profileHeader( 39 | AppInfo appInfo, AuthService authService, AuthUser userInfo) { 40 | var theme = Theme.of(context); 41 | return UserAccountsDrawerHeader( 42 | onDetailsPressed: () => setState(() { 43 | _showInfo = !_showInfo; 44 | }), 45 | accountName: Text(userInfo.displayName), 46 | accountEmail: Text(userInfo.email), 47 | currentAccountPicture: 48 | super.userPhotoImage(appInfo, authService, userInfo), 49 | decoration: BoxDecoration(color: theme.accentColor), 50 | ); 51 | } 52 | 53 | bool _showInfo = false; 54 | 55 | Widget _authView( 56 | AppInfo appInfo, AuthService authService, AuthUser userInfo) { 57 | return ListView( 58 | // Important: Remove any padding from the ListView. 59 | padding: EdgeInsets.zero, 60 | children: [ 61 | _profileHeader(appInfo, authService, userInfo), 62 | _showInfo 63 | ? super.userList(appInfo, authService, userInfo) 64 | : super.standardList(appInfo, authService, userInfo), 65 | ], 66 | ); 67 | } 68 | 69 | @override 70 | Widget build(BuildContext context) { 71 | return ScopedModelDescendant(builder: (context, child, model) { 72 | if (model.user == null || !model.user.isValid) { 73 | return super.notAuthView(model.authService); 74 | } else { 75 | return _authView(model.appInfo, model.authService, model.user); 76 | } 77 | }); 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /lib/core/pages/license_page.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | import 'dart:developer' show Timeline, Flow; 3 | import 'dart:io' show Platform; 4 | 5 | import 'package:flutter/foundation.dart'; 6 | import 'package:flutter/material.dart' hide Flow; 7 | import 'package:flutter/scheduler.dart'; 8 | import 'package:flutter/widgets.dart' hide Flow; 9 | 10 | import 'package:flutter_platform_widgets/flutter_platform_widgets.dart'; 11 | 12 | /// NOTE: this is taken from flutter/lib/src/material/about.dart 13 | /// So it can support both Cupertino and Material design 14 | 15 | /// A page that shows licenses for software used by the application. 16 | /// 17 | /// To show a [LicensePage], use [showLicensePage]. 18 | /// 19 | /// The [AboutDialog] shown by [showAboutDialog] and [AboutListTile] includes 20 | /// a button that calls [showLicensePage]. 21 | /// 22 | /// The licenses shown on the [LicensePage] are those returned by the 23 | /// [LicenseRegistry] API, which can be used to add more licenses to the list. 24 | class LicensePage extends StatefulWidget { 25 | /// Creates a page that shows licenses for software used by the application. 26 | /// 27 | /// The arguments are all optional. The application name, if omitted, will be 28 | /// derived from the nearest [Title] widget. The version and legalese values 29 | /// default to the empty string. 30 | /// 31 | /// The licenses shown on the [LicensePage] are those returned by the 32 | /// [LicenseRegistry] API, which can be used to add more licenses to the list. 33 | const LicensePage( 34 | {Key key, 35 | this.applicationName, 36 | this.applicationVersion, 37 | this.applicationLegalese}) 38 | : super(key: key); 39 | 40 | /// The name of the application. 41 | /// 42 | /// Defaults to the value of [Title.title], if a [Title] widget can be found. 43 | /// Otherwise, defaults to [Platform.resolvedExecutable]. 44 | final String applicationName; 45 | 46 | /// The version of this build of the application. 47 | /// 48 | /// This string is shown under the application name. 49 | /// 50 | /// Defaults to the empty string. 51 | final String applicationVersion; 52 | 53 | /// A string to show in small print. 54 | /// 55 | /// Typically this is a copyright notice. 56 | /// 57 | /// Defaults to the empty string. 58 | final String applicationLegalese; 59 | 60 | @override 61 | _LicensePageState createState() => new _LicensePageState(); 62 | } 63 | 64 | class _LicensePageState extends State { 65 | @override 66 | void initState() { 67 | super.initState(); 68 | _initLicenses(); 69 | } 70 | 71 | final List _licenses = []; 72 | bool _loaded = false; 73 | 74 | Future _initLicenses() async { 75 | final Flow flow = Flow.begin(); 76 | Timeline.timeSync('_initLicenses()', () {}, flow: flow); 77 | await for (LicenseEntry license in LicenseRegistry.licenses) { 78 | if (!mounted) return; 79 | Timeline.timeSync('_initLicenses()', () {}, flow: Flow.step(flow.id)); 80 | final List paragraphs = 81 | await SchedulerBinding.instance.scheduleTask>( 82 | () => license.paragraphs.toList(), 83 | Priority.animation, 84 | debugLabel: 'License', 85 | flow: flow, 86 | ); 87 | setState(() { 88 | _licenses.add(const Padding( 89 | padding: const EdgeInsets.symmetric(vertical: 18.0), 90 | child: const Text( 91 | '🍀‬', // That's U+1F340. Could also use U+2766 (❦) if U+1F340 doesn't work everywhere. 92 | textAlign: TextAlign.center))); 93 | _licenses.add(new Container( 94 | decoration: const BoxDecoration( 95 | border: const Border(bottom: const BorderSide(width: 0.0))), 96 | child: new Text(license.packages.join(', '), 97 | style: const TextStyle(fontWeight: FontWeight.bold), 98 | textAlign: TextAlign.center))); 99 | for (LicenseParagraph paragraph in paragraphs) { 100 | if (paragraph.indent == LicenseParagraph.centeredIndent) { 101 | _licenses.add(new Padding( 102 | padding: const EdgeInsets.only(top: 16.0), 103 | child: new Text(paragraph.text, 104 | style: const TextStyle(fontWeight: FontWeight.bold), 105 | textAlign: TextAlign.center))); 106 | } else { 107 | assert(paragraph.indent >= 0); 108 | _licenses.add(new Padding( 109 | padding: new EdgeInsetsDirectional.only( 110 | top: 8.0, start: 16.0 * paragraph.indent), 111 | child: new Text(paragraph.text))); 112 | } 113 | } 114 | }); 115 | } 116 | setState(() { 117 | _loaded = true; 118 | }); 119 | Timeline.timeSync('Build scheduled', () {}, flow: Flow.end(flow.id)); 120 | } 121 | 122 | @override 123 | Widget build(BuildContext context) { 124 | final String name = 125 | widget.applicationName ?? _defaultApplicationName(context); 126 | final String version = 127 | widget.applicationVersion ?? _defaultApplicationVersion(context); 128 | final MaterialLocalizations localizations = 129 | MaterialLocalizations.of(context); 130 | final List contents = [ 131 | new Text(name, 132 | style: Theme.of(context).textTheme.headline, 133 | textAlign: TextAlign.center), 134 | new Text(version, 135 | style: Theme.of(context).textTheme.body1, 136 | textAlign: TextAlign.center), 137 | new Container(height: 18.0), 138 | new Text(widget.applicationLegalese ?? '', 139 | style: Theme.of(context).textTheme.caption, 140 | textAlign: TextAlign.center), 141 | new Container(height: 18.0), 142 | new Text('Powered by Flutter', 143 | style: Theme.of(context).textTheme.body1, 144 | textAlign: TextAlign.center), 145 | new Container(height: 24.0), 146 | ]; 147 | contents.addAll(_licenses); 148 | if (!_loaded) { 149 | contents.add(const Padding( 150 | padding: const EdgeInsets.symmetric(vertical: 24.0), 151 | child: const Center(child: const CircularProgressIndicator()))); 152 | } 153 | return new PlatformScaffold( 154 | appBar: new PlatformAppBar( 155 | title: new Text(localizations.licensesPageTitle), 156 | ), 157 | // All of the licenses page text is English. We don't want localized text 158 | // or text direction. 159 | body: new Localizations.override( 160 | locale: const Locale('en', 'US'), 161 | context: context, 162 | child: new DefaultTextStyle( 163 | style: Theme.of(context).textTheme.caption, 164 | child: new Scrollbar( 165 | child: new ListView( 166 | padding: 167 | const EdgeInsets.symmetric(horizontal: 8.0, vertical: 12.0), 168 | children: contents, 169 | ), 170 | ), 171 | ), 172 | ), 173 | ); 174 | } 175 | } 176 | 177 | String _defaultApplicationName(BuildContext context) { 178 | final Title ancestorTitle = context.ancestorWidgetOfExactType(Title); 179 | return ancestorTitle?.title ?? 180 | Platform.resolvedExecutable.split(Platform.pathSeparator).last; 181 | } 182 | 183 | String _defaultApplicationVersion(BuildContext context) { 184 | // TODO(ianh): Get this from the embedder somehow. 185 | return ''; 186 | } 187 | 188 | // Widget _defaultApplicationIcon(BuildContext context) { 189 | // // TODO(ianh): Get this from the embedder somehow. 190 | // return null; 191 | // } 192 | -------------------------------------------------------------------------------- /lib/core/pages/profile_page.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter/widgets.dart'; 3 | import 'package:flutter_auth_base/flutter_auth_base.dart'; 4 | import 'package:flutter_platform_widgets/flutter_platform_widgets.dart'; 5 | import 'package:scoped_model/scoped_model.dart'; 6 | 7 | import '../app_info.dart'; 8 | import '../app_model.dart'; 9 | 10 | import 'profile_base_state.dart'; 11 | import '../widgets/header_button.dart'; 12 | import '../widgets/tablet_aware_layout_builder.dart'; 13 | 14 | import '../dialogs/app_info_dialog.dart' as app; 15 | 16 | class ProfilePage extends StatefulWidget { 17 | ProfilePage({this.licenceAdditionalWidgets}); 18 | 19 | final List licenceAdditionalWidgets; 20 | 21 | @override 22 | State createState() => ProfilePageState(); 23 | } 24 | 25 | class ProfilePageState extends ProfileBaseState { 26 | @override 27 | Widget aboutItem(AppInfo appInfo) { 28 | return super.listItem( 29 | 'About', 30 | () => app.showAppInfoDialog( 31 | appIconPath: appInfo.appIconPath, 32 | appName: appInfo.appName, 33 | appVersion: appInfo.appVersion, 34 | applicationLegalese: appInfo.applicationLegalese, 35 | context: context, 36 | addtionalWidgets: widget.licenceAdditionalWidgets), 37 | ); 38 | } 39 | 40 | Widget _profileHeader( 41 | AppInfo appInfo, AuthService authService, AuthUser user) { 42 | return Container( 43 | color: Color(0xffeeeeee), 44 | child: Center( 45 | child: Column( 46 | children: [ 47 | super.userPhotoImage(appInfo, authService, user), 48 | Padding( 49 | padding: const EdgeInsets.all(0.0), 50 | child: Text( 51 | user.displayName, 52 | style: Theme 53 | .of(context) 54 | .primaryTextTheme 55 | .body1 56 | .copyWith(color: Colors.black), 57 | ), 58 | ), 59 | Padding( 60 | padding: const EdgeInsets.all(8.0), 61 | child: Text(user.email, 62 | style: Theme 63 | .of(context) 64 | .primaryTextTheme 65 | .body2 66 | .copyWith(color: Colors.black54)), 67 | ), 68 | //Divider(), 69 | ], 70 | ), 71 | ), 72 | ); 73 | } 74 | 75 | Widget _authTabletView( 76 | AppInfo appInfo, AuthService authService, AuthUser userInfo) { 77 | return Row( 78 | children: [ 79 | Expanded( 80 | flex: 1, 81 | child: Container( 82 | color: Color(0xffeeeeee), 83 | //elevation: 4.0, 84 | child: Column( 85 | mainAxisAlignment: MainAxisAlignment.center, 86 | children: [ 87 | _profileHeader(appInfo, authService, userInfo), 88 | ], 89 | ), 90 | ), 91 | ), 92 | Expanded( 93 | flex: 1, 94 | child: ListView( 95 | // Important: Remove any padding from the ListView. 96 | padding: EdgeInsets.zero, 97 | children: [ 98 | _showInfo 99 | ? super.userList(appInfo, authService, userInfo) 100 | : super.standardList(appInfo, authService, userInfo), 101 | ], 102 | ), 103 | ), 104 | ], 105 | ); 106 | } 107 | 108 | Widget _authMobileView( 109 | AppInfo appInfo, AuthService authService, AuthUser userInfo) { 110 | return ListView( 111 | // Important: Remove any padding from the ListView. 112 | padding: EdgeInsets.zero, 113 | children: [ 114 | _profileHeader(appInfo, authService, userInfo), 115 | _showInfo 116 | ? super.userList(appInfo, authService, userInfo) 117 | : super.standardList(appInfo, authService, userInfo), 118 | ], 119 | ); 120 | } 121 | 122 | bool _showInfo = false; 123 | void _toggleList() { 124 | setState(() { 125 | _showInfo = !_showInfo; 126 | }); 127 | } 128 | 129 | @override 130 | Widget build(BuildContext context) { 131 | return PlatformScaffold( 132 | appBar: PlatformAppBar( 133 | title: new Text("Profile"), 134 | trailingActions: [ 135 | HeaderButton( 136 | text: _showInfo ? 'Info' : 'Account', 137 | onPressed: () => _toggleList(), 138 | ), 139 | ], 140 | ), 141 | body: ScopedModelDescendant( 142 | builder: (context, child, model) { 143 | if (model.user == null || !model.user.isValid) { 144 | return super.notAuthView(model.authService); 145 | } else { 146 | return TabletAwareLayoutBuilder( 147 | mobileView: (_) => 148 | _authMobileView(model.appInfo, model.authService, model.user), 149 | tabletView: (_) => 150 | _authTabletView(model.appInfo, model.authService, model.user), 151 | ); 152 | } 153 | }, 154 | ), 155 | ); 156 | } 157 | } 158 | -------------------------------------------------------------------------------- /lib/core/pages/splash_page.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | import 'package:async_loader/async_loader.dart'; 4 | import 'package:flutter/material.dart'; 5 | import 'package:flutter_auth_base/flutter_auth_base.dart'; 6 | import 'package:scoped_model/scoped_model.dart'; 7 | import 'package:flutter_platform_widgets/flutter_platform_widgets.dart'; 8 | 9 | import '../common/dialog.dart'; 10 | import '../widgets/tablet_aware_scaffold.dart'; 11 | import '../widgets/screen_logo.dart'; 12 | import '../widgets/progress_actionable_state.dart'; 13 | 14 | import '../auth/handlers/email/sign_in_button.dart' as email; 15 | import '../auth/handlers/email/sign_up_button.dart' as email; 16 | import '../auth/handlers/google/sign_in_button.dart' as google; 17 | 18 | import '../app_model.dart'; 19 | import '../app_info.dart'; 20 | 21 | import '../auth/handlers/user/termsAcceptance/terms_accept_modal.dart'; 22 | 23 | enum LoginState { 24 | LoginSuccessful, 25 | LoginRequired, 26 | } 27 | 28 | class Splash extends StatefulWidget { 29 | @override 30 | State createState() => SplashState(); 31 | } 32 | 33 | class SplashState extends ProgressActionableState { 34 | @override 35 | void initState() { 36 | super.initState(); 37 | 38 | _loader = _buildLoader(); 39 | } 40 | 41 | Widget _loader; 42 | Widget _loginLoader; 43 | 44 | final GlobalKey _loaderKey = GlobalKey(); 45 | 46 | AuthProvider _getPasswordProvider(AuthService authService) { 47 | return authService.authProviders.firstWhere( 48 | (prov) => prov.providerName == 'password', 49 | orElse: () => null); 50 | } 51 | 52 | AuthProvider _getGoogleProvider(AuthService authService) { 53 | return authService.authProviders.firstWhere( 54 | (prov) => prov.providerName == 'google', 55 | orElse: () => null); 56 | } 57 | 58 | Future _initAppState(AppModel model) async { 59 | await model.refreshAuthUser(); 60 | if (model.user != null && model.user.isValid) { 61 | return LoginState.LoginSuccessful; 62 | } else { 63 | return LoginState.LoginRequired; 64 | } 65 | } 66 | 67 | Widget _handleCompleted( 68 | AppInfo appInfo, AuthService authService, LoginState state) { 69 | if (state == LoginState.LoginRequired) { 70 | return _buttons(appInfo, authService); 71 | } else if (state == LoginState.LoginSuccessful) { 72 | //_navigateToHome(); 73 | } 74 | return Container(); 75 | } 76 | 77 | Widget _handleError(AppInfo appInfo, AuthService authService, Object error) { 78 | return _buttons(appInfo, authService, errorMessage: error.toString()); 79 | } 80 | 81 | Widget _progressIndicator() { 82 | return Padding( 83 | padding: EdgeInsets.all(16.0), 84 | child: PlatformCircularProgressIndicator( 85 | android: (_) => MaterialProgressIndicatorData( 86 | valueColor: AlwaysStoppedAnimation(Colors.black45), 87 | ), 88 | ), 89 | ); 90 | } 91 | 92 | Widget _handleSnapshot(AppModel model, BuildContext context, 93 | AsyncSnapshot snapshot) { 94 | if (snapshot.hasData) { 95 | return _handleCompleted(model.appInfo, model.authService, snapshot.data); 96 | } else if (snapshot.hasError) { 97 | return _handleError(model.appInfo, model.authService, snapshot.error); 98 | } else { 99 | return _progressIndicator(); 100 | } 101 | } 102 | 103 | Widget _buildLoader() { 104 | return ScopedModelDescendant(builder: (_, child, model) { 105 | if (_loginLoader == null) { 106 | _loginLoader = FutureBuilder( 107 | key: _loaderKey, 108 | future: _initAppState(model), 109 | builder: (_, AsyncSnapshot snapshot) => 110 | _handleSnapshot(model, _, snapshot)); 111 | } 112 | return _loginLoader; 113 | }); 114 | } 115 | 116 | Widget _buttons(AppInfo appInfo, AuthService authService, 117 | {String errorMessage}) { 118 | var passwordProvider = _getPasswordProvider(authService); 119 | var googleProvider = _getGoogleProvider(authService); 120 | 121 | List widgets = new List(); 122 | if (passwordProvider != null) { 123 | widgets.add(Padding( 124 | padding: EdgeInsets.symmetric(vertical: 8.0), 125 | child: email.SignInButton())); 126 | } 127 | if (googleProvider != null) { 128 | widgets.add( 129 | Padding( 130 | padding: EdgeInsets.symmetric(vertical: 8.0), 131 | child: google.SignInButton(action: (_) async { 132 | await performAction((BuildContext context) async { 133 | try { 134 | await googleProvider.signIn({}, termsAccepted: false); 135 | } on UserAcceptanceRequiredException catch (error) { 136 | bool accepted = await _handleAcceptanceRequired(); 137 | 138 | if (accepted) 139 | await googleProvider.signIn(error.data, termsAccepted: true); 140 | } 141 | }); 142 | }), 143 | ), 144 | ); 145 | } 146 | 147 | widgets.add(Padding(padding: EdgeInsets.only(top: 16.0))); 148 | if (passwordProvider != null) { 149 | widgets.add(Padding( 150 | padding: EdgeInsets.symmetric(vertical: 8.0), 151 | child: email.SignUpButton())); 152 | } 153 | 154 | return Column(children: widgets); 155 | } 156 | 157 | Future _handleAcceptanceRequired() async { 158 | var accepted = await openDialog( 159 | context: context, 160 | builder: (_) => TermsAcceptModal(), 161 | ); 162 | return accepted; 163 | } 164 | 165 | Widget _withProgress(Widget child) { 166 | return super.showProgress ? _progressIndicator() : child; 167 | } 168 | 169 | Widget _buildMobileView( 170 | AppInfo appInfo, Widget loader, Color splashForegroundColor) { 171 | return Center( 172 | child: Column( 173 | mainAxisAlignment: MainAxisAlignment.center, 174 | children: [ 175 | ScreenLogo(imagePath: appInfo.appIconPath), 176 | Text(appInfo.appName, 177 | style: TextStyle( 178 | color: splashForegroundColor, 179 | fontSize: 18.0, 180 | fontWeight: FontWeight.bold)), 181 | Column(children: [ 182 | Padding( 183 | padding: EdgeInsets.all(16.0), 184 | child: _withProgress(loader), 185 | ) 186 | ]) 187 | ], 188 | )); 189 | } 190 | 191 | Widget _buildTabletView(AppInfo appInfo, Widget loader, 192 | Color splashBackgroundColor, Color splashForegroundColor) { 193 | return Row( 194 | children: [ 195 | Expanded( 196 | flex: 1, 197 | child: Container( 198 | color: splashBackgroundColor, 199 | child: Column( 200 | mainAxisAlignment: MainAxisAlignment.center, 201 | children: [ 202 | ScreenLogo(imagePath: appInfo.appIconPath), 203 | Text(appInfo.appName, 204 | style: TextStyle( 205 | color: splashForegroundColor, 206 | fontSize: 18.0, 207 | fontWeight: FontWeight.bold)), 208 | ], 209 | ))), 210 | Expanded( 211 | flex: 1, 212 | child: SingleChildScrollView( 213 | child: Padding( 214 | padding: EdgeInsets.symmetric(horizontal: 36.0), 215 | child: Column( 216 | children: [ 217 | Padding( 218 | padding: EdgeInsets.all(16.0), 219 | child: _withProgress(loader), 220 | ) 221 | ], 222 | ), 223 | ), 224 | ), 225 | ), 226 | ], 227 | ); 228 | } 229 | 230 | @override 231 | Widget build(BuildContext context) { 232 | return ScopedModelDescendant( 233 | builder: (_, child, model) { 234 | //var theme = Theme.of(context); 235 | Color bgColor = Colors.white; 236 | Color fgColor = Colors.black87; 237 | 238 | return TabletAwareScaffold( 239 | mobileView: (_) => 240 | _buildMobileView(model.appInfo, _loader, fgColor), 241 | tabletView: (_) => 242 | _buildTabletView(model.appInfo, _loader, bgColor, fgColor), 243 | backgroundColor: bgColor); 244 | }, 245 | ); 246 | } 247 | } 248 | -------------------------------------------------------------------------------- /lib/core/validators/display_name_validator.dart: -------------------------------------------------------------------------------- 1 | String validate(String name) { 2 | if (name != null && name.length > 30) { 3 | return 'Name must no more than 30 characters'; 4 | } 5 | return null; 6 | } 7 | -------------------------------------------------------------------------------- /lib/core/validators/email_validator.dart: -------------------------------------------------------------------------------- 1 | String validate(String email) { 2 | if (email != null && email.length > 100) { 3 | return 'Email cannot be more than 100 characters'; 4 | } 5 | 6 | if (email == null || !email.contains("@")) { 7 | return "Not a valid email."; 8 | } 9 | 10 | return null; 11 | } 12 | -------------------------------------------------------------------------------- /lib/core/validators/password_validator.dart: -------------------------------------------------------------------------------- 1 | String validate(String password) { 2 | if (password != null && password.length > 50) { 3 | return 'Password cannot be more than 50 characters'; 4 | } 5 | 6 | if (password == null || password.length < 6) { 7 | return 'Password has to be more than 5 characters'; 8 | } 9 | return null; 10 | } 11 | -------------------------------------------------------------------------------- /lib/core/validators/validate_if.dart: -------------------------------------------------------------------------------- 1 | typedef String StringValidatorCallback(String value); 2 | 3 | String validateIfNotEmpty( 4 | bool force, String value, StringValidatorCallback validator) { 5 | if (force || (value != null && value.length > 0)) { 6 | return validator(value); 7 | } 8 | return null; 9 | } 10 | -------------------------------------------------------------------------------- /lib/core/validators/validator.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/foundation.dart'; 2 | 3 | class Validator { 4 | final List> validations = new List>(); 5 | 6 | String validate() { 7 | var errors = new List(); 8 | 9 | for (var validator in validations) { 10 | var error = validator(); 11 | if (error != null) errors.add(error); 12 | } 13 | 14 | if (errors.length > 1) { 15 | return "Please fix multiple validation errors"; 16 | } else { 17 | return errors.join("\n"); 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /lib/core/validators/words_match_validator.dart: -------------------------------------------------------------------------------- 1 | String validate(String value1, String value2, {String customOnError}) { 2 | if (value1 != value2) { 3 | return customOnError ?? 'Values do not match'; 4 | } 5 | 6 | return null; 7 | } 8 | -------------------------------------------------------------------------------- /lib/core/widgets/about_dialog.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | 3 | import 'package:flutter/material.dart' hide showLicensePage, LicensePage; 4 | import 'package:flutter/widgets.dart'; 5 | import 'package:flutter_platform_widgets/flutter_platform_widgets.dart'; 6 | import 'package:meta/meta.dart'; 7 | 8 | import '../pages/license_page.dart'; 9 | 10 | /// NOTE: this is taken from flutter/lib/src/material/about.dart 11 | /// So it can support both Cupertino and Material design 12 | 13 | /// An about box. This is a dialog box with the application's icon, name, 14 | /// version number, and copyright, plus a button to show licenses for software 15 | /// used by the application. 16 | /// 17 | /// To show an [AboutDialog], use [showAboutDialog]. 18 | /// 19 | /// If the application has a [Drawer], the [AboutListTile] widget can make the 20 | /// process of showing an about dialog simpler. 21 | /// 22 | /// The [AboutDialog] shown by [showAboutDialog] includes a button that calls 23 | /// [showLicensePage]. 24 | /// 25 | /// The licenses shown on the [LicensePage] are those returned by the 26 | /// [LicenseRegistry] API, which can be used to add more licenses to the list. 27 | class AboutDialog extends StatelessWidget { 28 | /// Creates an about box. 29 | /// 30 | /// The arguments are all optional. The application name, if omitted, will be 31 | /// derived from the nearest [Title] widget. The version, icon, and legalese 32 | /// values default to the empty string. 33 | const AboutDialog({ 34 | Key key, 35 | this.applicationName, 36 | this.applicationVersion, 37 | this.applicationIcon, 38 | this.applicationLegalese, 39 | this.children, 40 | }) : super(key: key); 41 | 42 | /// The name of the application. 43 | /// 44 | /// Defaults to the value of [Title.title], if a [Title] widget can be found. 45 | /// Otherwise, defaults to [Platform.resolvedExecutable]. 46 | final String applicationName; 47 | 48 | /// The version of this build of the application. 49 | /// 50 | /// This string is shown under the application name. 51 | /// 52 | /// Defaults to the empty string. 53 | final String applicationVersion; 54 | 55 | /// The icon to show next to the application name. 56 | /// 57 | /// By default no icon is shown. 58 | /// 59 | /// Typically this will be an [ImageIcon] widget. It should honor the 60 | /// [IconTheme]'s [IconThemeData.size]. 61 | final Widget applicationIcon; 62 | 63 | /// A string to show in small print. 64 | /// 65 | /// Typically this is a copyright notice. 66 | /// 67 | /// Defaults to the empty string. 68 | final String applicationLegalese; 69 | 70 | /// Widgets to add to the dialog box after the name, version, and legalese. 71 | /// 72 | /// This could include a link to a Web site, some descriptive text, credits, 73 | /// or other information to show in the about box. 74 | /// 75 | /// Defaults to nothing. 76 | final List children; 77 | 78 | @override 79 | Widget build(BuildContext context) { 80 | final String name = applicationName ?? _defaultApplicationName(context); 81 | final String version = 82 | applicationVersion ?? _defaultApplicationVersion(context); 83 | final Widget icon = applicationIcon ?? _defaultApplicationIcon(context); 84 | List body = []; 85 | if (icon != null) 86 | body.add( 87 | new IconTheme(data: const IconThemeData(size: 48.0), child: icon)); 88 | body.add(new Expanded( 89 | child: new Padding( 90 | padding: const EdgeInsets.symmetric(horizontal: 24.0), 91 | child: new ListBody(children: [ 92 | new Text(name, style: Theme.of(context).textTheme.headline), 93 | new Text(version, style: Theme.of(context).textTheme.body1), 94 | new Container(height: 18.0), 95 | new Text(applicationLegalese ?? '', 96 | style: Theme.of(context).textTheme.caption) 97 | ])))); 98 | body = [ 99 | new Row(crossAxisAlignment: CrossAxisAlignment.start, children: body), 100 | ]; 101 | if (children != null) body.addAll(children); 102 | return new PlatformAlertDialog( 103 | content: new SingleChildScrollView( 104 | child: new ListBody(children: body), 105 | ), 106 | actions: [ 107 | new PlatformDialogAction( 108 | child: new PlatformText('View Licenses'), 109 | onPressed: () { 110 | showLicensePage( 111 | context: context, 112 | applicationName: applicationName, 113 | applicationVersion: applicationVersion, 114 | applicationIcon: applicationIcon, 115 | applicationLegalese: applicationLegalese); 116 | }), 117 | new PlatformDialogAction( 118 | child: new PlatformText('Close'), 119 | onPressed: () { 120 | Navigator.pop(context); 121 | }), 122 | ]); 123 | } 124 | } 125 | 126 | String _defaultApplicationName(BuildContext context) { 127 | final Title ancestorTitle = context.ancestorWidgetOfExactType(Title); 128 | return ancestorTitle?.title ?? 129 | Platform.resolvedExecutable.split(Platform.pathSeparator).last; 130 | } 131 | 132 | String _defaultApplicationVersion(BuildContext context) { 133 | // TODO(ianh): Get this from the embedder somehow. 134 | return ''; 135 | } 136 | 137 | Widget _defaultApplicationIcon(BuildContext context) { 138 | // TODO(ianh): Get this from the embedder somehow. 139 | return null; 140 | } 141 | 142 | void showLicensePage( 143 | {@required BuildContext context, 144 | String applicationName, 145 | String applicationVersion, 146 | Widget applicationIcon, 147 | String applicationLegalese}) { 148 | Navigator.pop(context); 149 | Navigator.push( 150 | context, 151 | new MaterialPageRoute( 152 | builder: (BuildContext context) => new LicensePage( 153 | applicationName: applicationName, 154 | applicationVersion: applicationVersion, 155 | applicationLegalese: applicationLegalese))); 156 | } 157 | -------------------------------------------------------------------------------- /lib/core/widgets/email_image_circle_avatar.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | import 'package:flutter/material.dart'; 4 | import 'package:flutter_auth_base/flutter_auth_base.dart'; 5 | import 'package:meta/meta.dart'; 6 | 7 | class EmailImageCircleAvatar extends StatefulWidget { 8 | EmailImageCircleAvatar( 9 | {Key key, 10 | @required this.defaultImage, 11 | @required this.imageProvider, 12 | this.backgroundColor = Colors.transparent, 13 | this.email, 14 | this.checkIfImageExists = false, 15 | this.radius = 48.0, 16 | this.imageSize = 200}) 17 | : super(key: key); 18 | 19 | final AssetImage defaultImage; 20 | final PhotoUrlProvider imageProvider; 21 | final int imageSize; 22 | final double radius; 23 | final String email; 24 | final Color backgroundColor; 25 | final bool checkIfImageExists; 26 | 27 | @override 28 | createState() => new EmailImageCircleAvatarState(); 29 | } 30 | 31 | class EmailImageCircleAvatarState extends State { 32 | @override 33 | void initState() { 34 | super.initState(); 35 | 36 | if (_builder == null) { 37 | _builder = FutureBuilder( 38 | future: performUpdate(_email ?? widget.email), 39 | builder: (BuildContext context, AsyncSnapshot snapshot) { 40 | return _buildImage(); 41 | }); 42 | } 43 | } 44 | 45 | FutureBuilder _builder; 46 | String _gravatarImageUrl; 47 | String _email; 48 | 49 | Future performUpdate(String email) async { 50 | var url = await widget.imageProvider?.emailToPhotoUrl(email, 51 | size: widget.imageSize, checkIfImageExists: widget.checkIfImageExists); 52 | 53 | if (url != null && url.isValid) { 54 | setState(() { 55 | _email = email; 56 | _gravatarImageUrl = url.url; 57 | }); 58 | } else { 59 | setState(() { 60 | _email = email; 61 | _gravatarImageUrl = null; 62 | }); 63 | } 64 | return "done"; 65 | } 66 | 67 | NetworkImage _image; 68 | NetworkImage networkImage() { 69 | if (_image == null) { 70 | _image = new NetworkImage(_gravatarImageUrl); 71 | } else { 72 | if (_image.url != _gravatarImageUrl) { 73 | _image = new NetworkImage(_gravatarImageUrl); 74 | } 75 | } 76 | 77 | return _image; 78 | } 79 | 80 | Widget _buildImage() { 81 | return CircleAvatar( 82 | backgroundColor: widget.backgroundColor, 83 | backgroundImage: 84 | _gravatarImageUrl == null ? widget.defaultImage : networkImage(), 85 | radius: widget.radius, 86 | ); 87 | } 88 | 89 | @override 90 | Widget build(BuildContext context) { 91 | if (_gravatarImageUrl != null) { 92 | return _buildImage(); 93 | } else { 94 | return _builder; 95 | } 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /lib/core/widgets/form_progress_actionable_state.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | import 'package:flutter/material.dart'; 4 | 5 | import '../dialogs/show_error_dialog.dart'; 6 | import '../common/future_action_callback.dart'; 7 | 8 | import 'progress_actionable_state.dart'; 9 | 10 | abstract class FormProgressActionableState 11 | extends ProgressActionableState { 12 | final formKey = new GlobalKey(); 13 | 14 | Future validateAndSubmit( 15 | FutureActionCallback action) async { 16 | if (!showProgress) { 17 | final form = formKey.currentState; 18 | 19 | if (form?.validate() ?? true) { 20 | form?.save(); 21 | 22 | await super.performAction(action); 23 | } else { 24 | await showErrorDialog(context, 'Please correct any errors first.'); 25 | } 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /lib/core/widgets/header_button.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/widgets.dart'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:flutter_platform_widgets/flutter_platform_widgets.dart'; 4 | import 'package:meta/meta.dart'; 5 | 6 | class HeaderButton extends StatelessWidget { 7 | HeaderButton({@required this.text, this.onPressed}); 8 | 9 | final String text; 10 | final VoidCallback onPressed; 11 | 12 | @override 13 | Widget build(BuildContext context) { 14 | var theme = Theme.of(context); 15 | return PlatformWidget( 16 | android: (_) => FlatButton( 17 | onPressed: onPressed, 18 | child: Text( 19 | text, 20 | style: TextStyle( 21 | color: Theme.of(context).primaryTextTheme.title.color), 22 | ), 23 | ), 24 | ios: (_) => PlatformButton( 25 | onPressed: onPressed, 26 | child: Text( 27 | text, 28 | style: Theme 29 | .of(context) 30 | .textTheme 31 | .body1 32 | .copyWith(color: theme.primaryColor), 33 | ), 34 | ), 35 | ); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /lib/core/widgets/modalAppBar.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/widgets.dart'; 2 | import 'package:flutter_platform_widgets/flutter_platform_widgets.dart'; 3 | 4 | import 'package:flutter/material.dart' show Icons; 5 | import 'package:meta/meta.dart'; 6 | 7 | class ModalAppBar extends PlatformAppBar { 8 | ModalAppBar({ 9 | Key key, 10 | @required Widget title, 11 | @required VoidCallback closeAction, 12 | bool hideAccept: false, 13 | String acceptText: 'Save', 14 | VoidCallback acceptAction, 15 | Color backgroundColor, 16 | PlatformBuilder android, 17 | PlatformBuilder ios, 18 | }) : super( 19 | key: key, 20 | title: title, 21 | backgroundColor: backgroundColor, 22 | leading: _closeButton(closeAction), 23 | trailingActions: 24 | hideAccept ? [] : [_acceptButton(acceptText, acceptAction)], 25 | android: android, 26 | ios: ios); 27 | 28 | static Widget _acceptButton(String acceptText, VoidCallback acceptAction) { 29 | return PlatformWidget( 30 | android: (_) => PlatformIconButton( 31 | androidIcon: Icon(Icons.done), 32 | onPressed: acceptAction, 33 | ), 34 | ios: (_) => PlatformButton( 35 | child: Text( 36 | acceptText, 37 | ), 38 | onPressed: acceptAction, 39 | ), 40 | ); 41 | } 42 | 43 | static Widget _closeButton(VoidCallback closeAction) { 44 | return PlatformWidget( 45 | android: (_) => PlatformIconButton( 46 | androidIcon: Icon(Icons.close), 47 | onPressed: closeAction, 48 | ), 49 | ios: (_) => PlatformButton( 50 | child: Text( 51 | 'Cancel', 52 | ), 53 | onPressed: closeAction, 54 | ), 55 | ); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /lib/core/widgets/progress_actionable_state.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | import 'package:flutter/material.dart'; 4 | import 'package:flutter/services.dart'; 5 | 6 | import 'progressable_state.dart'; 7 | import '../common/future_action_callback.dart'; 8 | import '../common/actionable.dart'; 9 | import '../common/app_exception.dart'; 10 | import '../dialogs/show_error_dialog.dart'; 11 | 12 | abstract class ProgressActionableState 13 | extends ProgressableState implements Actionable { 14 | Future performAction(FutureActionCallback action) async { 15 | setProgress(true); 16 | try { 17 | FocusScope.of(context).requestFocus(FocusNode()); 18 | await action(context); 19 | } on AppException catch (error) { 20 | setProgress(false); 21 | 22 | await showErrorDialog(context, error.message ?? 'Unknown error occured'); 23 | } on PlatformException catch (error) { 24 | setProgress(false); 25 | 26 | await showErrorDialog(context, error.details ?? 'Unknown error occured'); 27 | } catch (error) { 28 | setProgress(false); 29 | 30 | await showErrorDialog(context, 'Unknown error occured'); 31 | } finally { 32 | await new Future.delayed(const Duration(milliseconds: 100), () { 33 | setProgress(false); 34 | }); 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /lib/core/widgets/progressable_state.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | abstract class ProgressableState extends State { 4 | bool showProgress = false; 5 | void setProgress(bool value) { 6 | if (mounted) { 7 | setState(() { 8 | showProgress = value; 9 | }); 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /lib/core/widgets/screen_aware_padding.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/foundation.dart'; 2 | import 'package:flutter/material.dart'; 3 | 4 | class ScreenAwarePadding extends StatelessWidget { 5 | ScreenAwarePadding({Key key, @required this.child}) : super(key: key); 6 | 7 | final Widget child; 8 | 9 | @override 10 | Widget build(BuildContext context) { 11 | var size = MediaQuery.of(context).size; 12 | 13 | var left = 0.0; 14 | var bottom = 0.0; 15 | var right = 0.0; 16 | var top = 0.0; 17 | 18 | if (size.height > 600 && size.height <= 750) { 19 | top = 16.0; 20 | bottom = 16.0; 21 | } 22 | if (size.height > 750 && size.height <= 900) { 23 | top = 32.0; 24 | bottom = 64.0; 25 | } 26 | if (size.height > 900 && size.height <= 1200) { 27 | top = 64.0; 28 | bottom = 128.0; 29 | } 30 | if (size.height > 1200) { 31 | top = 256.0; 32 | bottom = 256.0; 33 | } 34 | 35 | if (size.width > 350 && size.width <= 400) { 36 | left = 16.0; 37 | right = 16.0; 38 | } 39 | if (size.width > 400 && size.width <= 600) { 40 | left = 32.0; 41 | right = 32.0; 42 | } 43 | if (size.width > 600 && size.width <= 800) { 44 | left = 64.0; 45 | right = 64.0; 46 | } 47 | if (size.width > 800 && size.width <= 1000) { 48 | left = 128.0; 49 | right = 128.0; 50 | } 51 | if (size.width > 1000 && size.width <= 1200) { 52 | left = 256.0; 53 | right = 256.0; 54 | } 55 | if (size.width > 1200) { 56 | left = 384.0; 57 | right = 384.0; 58 | } 59 | 60 | var pad = 61 | EdgeInsets.only(left: left, bottom: bottom, right: right, top: top); 62 | 63 | return Padding(padding: pad, child: child); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /lib/core/widgets/screen_logo.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:meta/meta.dart'; 3 | 4 | class ScreenLogo extends StatelessWidget { 5 | ScreenLogo({Key key, @required this.imagePath}) : super(key: key); 6 | 7 | final String imagePath; 8 | 9 | @override 10 | Widget build(BuildContext context) { 11 | var icon = AssetImage(imagePath); 12 | 13 | var size = MediaQuery.of(context).size; 14 | 15 | return Image( 16 | image: icon, 17 | height: size.width > 700.0 && size.height > 500.0 ? 128.0 : 96.0, 18 | width: size.width > 700.0 && size.height > 500.0 ? 128.0 : 96.0, 19 | ); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /lib/core/widgets/tablet_aware_layout_builder.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:meta/meta.dart'; 3 | 4 | class TabletAwareLayoutBuilder extends StatelessWidget { 5 | TabletAwareLayoutBuilder( 6 | {Key key, @required this.mobileView, @required this.tabletView}) 7 | : assert(mobileView != null), 8 | assert(tabletView != null), 9 | super(key: key); 10 | 11 | final WidgetBuilder mobileView; 12 | final WidgetBuilder tabletView; 13 | 14 | final double tabletThreshold = 660.0; 15 | 16 | @override 17 | Widget build(BuildContext context) { 18 | return LayoutBuilder(builder: (context, constraints) { 19 | final bool useMobileLayout = constraints.maxWidth < tabletThreshold; 20 | 21 | if (useMobileLayout) { 22 | return mobileView(context); 23 | } else { 24 | return tabletView(context); 25 | } 26 | }); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /lib/core/widgets/tablet_aware_scaffold.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:meta/meta.dart'; 3 | 4 | import 'tablet_aware_layout_builder.dart'; 5 | 6 | class TabletAwareScaffold extends StatelessWidget { 7 | TabletAwareScaffold( 8 | {Key key, 9 | @required this.mobileView, 10 | @required this.tabletView, 11 | this.backgroundColor = Colors.white}) 12 | : assert(mobileView != null), 13 | assert(tabletView != null), 14 | super(key: key); 15 | 16 | final WidgetBuilder mobileView; 17 | final WidgetBuilder tabletView; 18 | final Color backgroundColor; 19 | 20 | @override 21 | Widget build(BuildContext context) { 22 | return Scaffold( 23 | backgroundColor: backgroundColor, 24 | body: TabletAwareLayoutBuilder( 25 | mobileView: mobileView, tabletView: tabletView)); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /lib/core/widgets/throttled_text_editing_controller.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/widgets.dart'; 2 | import 'package:meta/meta.dart'; 3 | import '../common/throttle.dart'; 4 | 5 | //typedef void StringCallback(String value); 6 | 7 | class ThrottledTextEditingController extends TextEditingController { 8 | ThrottledTextEditingController( 9 | {@required ValueCallback onUpdate, 10 | int throttleDurationMilliseconds = 1000}) { 11 | _textThrottler = new Throttler( 12 | delay: new Duration(milliseconds: throttleDurationMilliseconds), 13 | callback: onUpdate, 14 | argCallback: () => this.text); 15 | 16 | super.addListener(_textThrottler.throttle); 17 | } 18 | 19 | Throttler _textThrottler; 20 | } 21 | -------------------------------------------------------------------------------- /lib/main.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:scoped_model/scoped_model.dart'; 3 | import 'core/app_model.dart'; 4 | import 'core/app_info.dart'; 5 | import 'core/pages/splash_page.dart'; 6 | import 'theme.dart'; 7 | import 'routes.dart' as routing; 8 | import 'mock_auth_service.dart' as auth; 9 | 10 | final GlobalKey _navKey = new GlobalKey(); 11 | 12 | void main() { 13 | //TODO fill these out for your app 14 | var appInfo = new AppInfo( 15 | appName: 'Flutter Auth Starter', 16 | appVersion: "0.0.1", 17 | appIconPath: "assets/icons/appIcon.jpg", 18 | avatarDefaultAppIconPath: "assets/icons/profileIcon.png", 19 | applicationLegalese: '', 20 | privacyPolicyUrl: "http://yourPrivacyPolicyUrl", 21 | termsOfServiceUrl: "http://yourTermsOfServiceUrl"); 22 | 23 | var authService = auth.createMockedAuthService(); 24 | 25 | var app = ScopedModel( 26 | model: AppModel(appInfo: appInfo, authService: authService), 27 | child: MaterialApp( 28 | title: appInfo.appName, 29 | navigatorKey: _navKey, 30 | debugShowCheckedModeBanner: false, 31 | theme: theme(), 32 | home: Splash(), 33 | routes: routing.buildRoutes(authService), 34 | onGenerateRoute: routing.buildGenerator())); 35 | 36 | //This is so that we can route to the splash screen when the user state changes and is signed out 37 | //If the user has changed and is signed in route to the home page 38 | authService.authUserChanged.addListener(() { 39 | app.model.refreshAuthUser().then((model) { 40 | if (model.hasChanged) { 41 | if (model.isValidUser) { 42 | _navKey.currentState.pushNamedAndRemoveUntil('/home', (_) => false); 43 | } else { 44 | _navKey.currentState.pushNamedAndRemoveUntil('/', (_) => false); 45 | } 46 | } 47 | }); 48 | }); 49 | 50 | runApp(app); 51 | } 52 | -------------------------------------------------------------------------------- /lib/mock_auth_service.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_auth_base/flutter_auth_base.dart'; 2 | import 'core/auth/mock/mock_service.dart'; 3 | 4 | import 'core/imageProviders/combined_image_provider.dart'; 5 | import 'core/imageProviders/gravatar_provider.dart'; 6 | import 'core/imageProviders/user_photo_url_provider.dart'; 7 | 8 | AuthService createMockedAuthService() { 9 | print(">>>>> AUTHENTICATION in MOCKED MODE <<<<<"); 10 | var authService = new MockService(); 11 | authService.preAuthPhotoProvider = new GravatarProvider(); 12 | authService.postAuthPhotoProvider = new CombinedPhotoProvider() 13 | ..add(new AuthUserImageProvider(service: authService)) 14 | ..add(new GravatarProvider(missingImageType: ImageType.MysteryMan)); 15 | 16 | return authService; 17 | } 18 | -------------------------------------------------------------------------------- /lib/routes.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/widgets.dart'; 2 | import 'package:flutter_auth_base/flutter_auth_base.dart'; 3 | 4 | import 'src/pages/home_page.dart'; 5 | 6 | //TODO add more routes specific to the application 7 | Map buildRoutes(AuthService authService) { 8 | var routes = new Map(); 9 | 10 | routes['/home'] = (BuildContext context) => new HomePage(); 11 | 12 | return routes; 13 | } 14 | 15 | //As an alternative to the routes, you can return a generator 16 | RouteFactory buildGenerator() { 17 | return null; 18 | } 19 | -------------------------------------------------------------------------------- /lib/src/pages/home_page.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:scoped_model/scoped_model.dart'; 3 | import 'package:flutter_platform_widgets/flutter_platform_widgets.dart'; 4 | import 'package:flutter/cupertino.dart' show CupertinoIcons; 5 | 6 | import '../../core/app_model.dart'; 7 | import '../../core/pages/drawer_page.dart'; 8 | import '../../core/pages/profile_page.dart'; 9 | 10 | class HomePage extends StatefulWidget { 11 | @override 12 | State createState() => HomePageState(); 13 | } 14 | 15 | class HomePageState extends State { 16 | Widget _buildBody(AppModel model) { 17 | return Padding( 18 | padding: const EdgeInsets.all(8.0), 19 | child: new Center( 20 | child: Column( 21 | children: [ 22 | Text('Home page'), 23 | Padding( 24 | padding: const EdgeInsets.all(8.0), 25 | child: PlatformButton( 26 | child: Text('Sign Out'), 27 | onPressed: () async => await model.authService.signOut(), 28 | ), 29 | ), 30 | Padding( 31 | padding: const EdgeInsets.all(8.0), 32 | child: PlatformButton( 33 | child: Text('Switch Platform'), 34 | onPressed: () { 35 | setState(() { 36 | if (isCupertino) { 37 | changeToMaterialPlatform(); 38 | } else if (isMaterial) { 39 | changeToCupertinoPlatform(); 40 | } 41 | }); 42 | }, 43 | ), 44 | ), 45 | ], 46 | ), 47 | ), 48 | ); 49 | } 50 | 51 | @override 52 | Widget build(BuildContext context) { 53 | return ScopedModelDescendant( 54 | rebuildOnChange: false, 55 | builder: (_, child, model) { 56 | return PlatformScaffold( 57 | android: (_) => MaterialScaffoldData( 58 | drawer: new Drawer( 59 | child: new DrawerPage(), 60 | ), 61 | ), 62 | appBar: PlatformAppBar( 63 | title: Text( 64 | 'Flutter Auth Starter', 65 | ), 66 | ios: (_) => CupertinoNavigationBarData( 67 | leading: PlatformIconButton( 68 | iosIcon: Icon(CupertinoIcons.info), 69 | onPressed: () => Navigator.push( 70 | context, 71 | MaterialPageRoute( 72 | builder: (_) => Material( 73 | child: ProfilePage(), 74 | ), 75 | ), 76 | ), 77 | ), 78 | ), 79 | ), 80 | body: Material( 81 | color: isMaterial ? null : Theme.of(context).cardColor, 82 | child: _buildBody(model), 83 | ), 84 | ); 85 | }, 86 | ); 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /lib/theme.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | //TODO set you custom theme here 4 | ThemeData theme() { 5 | return ThemeData( 6 | primaryColor: Colors.blue, 7 | primaryColorDark: Colors.blue[900], 8 | primaryColorLight: Colors.blue[400], 9 | accentColor: Colors.lightBlueAccent); 10 | } 11 | -------------------------------------------------------------------------------- /pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: flutter_auth_starter 2 | description: A new Flutter project. 3 | 4 | environment: 5 | sdk: ">=1.23.0 <3.0.0" 6 | 7 | dependencies: 8 | flutter: 9 | sdk: flutter 10 | 11 | # The following adds the Cupertino Icons font to your application. 12 | # Use with the CupertinoIcons class for iOS style icons. 13 | cupertino_icons: ^0.1.2 14 | flutter_auth_base: ^0.3.1 15 | flutter_platform_widgets: "^0.3.1" 16 | scoped_model: "^0.3.0" 17 | async_loader: "^0.1.2" 18 | font_awesome_flutter: 8.0.1 19 | url_launcher: "^3.0.3" 20 | 21 | dev_dependencies: 22 | flutter_test: 23 | sdk: flutter 24 | 25 | 26 | # For information on the generic Dart part of this file, see the 27 | # following page: https://www.dartlang.org/tools/pub/pubspec 28 | 29 | # The following section is specific to Flutter. 30 | flutter: 31 | 32 | # The following line ensures that the Material Icons font is 33 | # included with your application, so that you can use the icons in 34 | # the material Icons class. 35 | uses-material-design: true 36 | 37 | assets: 38 | - assets/icons/appIcon.jpg 39 | - assets/icons/transparent.png 40 | - assets/icons/profileIcon.png 41 | 42 | # An image asset can refer to one or more resolution-specific "variants", see 43 | # https://flutter.io/assets-and-images/#resolution-aware. 44 | 45 | # For details regarding adding assets from package dependencies, see 46 | # https://flutter.io/assets-and-images/#from-packages 47 | 48 | # To add custom fonts to your application, add a fonts section here, 49 | # in this "flutter" section. Each entry in this list should have a 50 | # "family" key with the font family name, and a "fonts" key with a 51 | # list giving the asset and other descriptors for the font. For 52 | # example: 53 | # fonts: 54 | # - family: Schyler 55 | # fonts: 56 | # - asset: fonts/Schyler-Regular.ttf 57 | # - asset: fonts/Schyler-Italic.ttf 58 | # style: italic 59 | # - family: Trajan Pro 60 | # fonts: 61 | # - asset: fonts/TrajanPro.ttf 62 | # - asset: fonts/TrajanPro_Bold.ttf 63 | # weight: 700 64 | # 65 | # For details regarding fonts from package dependencies, 66 | # see https://flutter.io/custom-fonts/#from-packages 67 | --------------------------------------------------------------------------------