├── .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 | 
4 |
5 | Splash Screen
6 |
7 | 
8 |
9 | Sign In - ios
10 |
11 | 
12 |
13 | Sign In - android
14 |
15 | 
16 |
17 | Sign up - android
18 |
19 | 
20 |
21 | Reset password - ios
22 |
23 | 
24 |
25 | Reset password - android
26 |
27 | 
28 |
29 | Reset password sent - android
30 |
31 | 
32 |
33 | Terms Acceptance - ios
34 |
35 | 
36 |
37 | Profile account - ios
38 |
39 | 
40 |
41 | Profile info - ios
42 |
43 | 
44 |
45 | About - ios
46 |
47 | 
48 |
49 | Link / Connect accounts - ios
50 |
51 | 
52 |
53 | Link email - ios
54 |
55 | 
56 |
57 | Drawer info - android
58 |
59 | 
60 |
61 | Drawer account - android
62 |
63 | 
64 |
65 | Change display name - android
66 |
67 | 
68 |
69 | Change email - android
70 |
71 | 
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 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
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 |
--------------------------------------------------------------------------------