diff --git a/android/app/google-services.json b/android/app/google-services.json index 11d7a3f..ebd5fe0 100644 --- a/android/app/google-services.json +++ b/android/app/google-services.json @@ -22,6 +22,14 @@ "certificate_hash": "4cfcaae693f041e62c51224b17ff5920ca45db7c" } }, + { + "client_id": "123595545975-qlq2eju9tpm89igcgj13f96dne92dd12.apps.googleusercontent.com", + "client_type": 1, + "android_info": { + "package_name": "com.mszalek.weight_tracker", + "certificate_hash": "52f32b889e107f33b006c376421992370f90510c" + } + }, { "client_id": "123595545975-jvcpng9krrieeokh79673e448cekctnj.apps.googleusercontent.com", "client_type": 3 diff --git a/assets/google.png b/assets/google.png new file mode 100644 index 0000000..8a4c8a8 Binary files /dev/null and b/assets/google.png differ diff --git a/assets/user.png b/assets/user.png new file mode 100644 index 0000000..6927f5a Binary files /dev/null and b/assets/user.png differ diff --git a/lib/logic/actions.dart b/lib/logic/actions.dart index 25ca588..ded6c85 100644 --- a/lib/logic/actions.dart +++ b/lib/logic/actions.dart @@ -4,18 +4,20 @@ import 'package:weight_tracker/model/weight_entry.dart'; class UserLoadedAction { final FirebaseUser firebaseUser; + final List cachedEntries; - UserLoadedAction(this.firebaseUser); + UserLoadedAction(this.firebaseUser, {this.cachedEntries = const []}); } class AddDatabaseReferenceAction { final DatabaseReference databaseReference; + final List cachedEntries; - AddDatabaseReferenceAction(this.databaseReference); + AddDatabaseReferenceAction(this.databaseReference, + {this.cachedEntries = const []}); } -class GetSavedWeightNote { -} +class GetSavedWeightNote {} class AddWeightFromNotes { final double weight; @@ -23,8 +25,7 @@ class AddWeightFromNotes { AddWeightFromNotes(this.weight); } -class ConsumeWeightFromNotes { -} +class ConsumeWeightFromNotes {} class AddEntryAction { final WeightEntry weightEntry; @@ -88,8 +89,7 @@ class UpdateActiveWeightEntry { UpdateActiveWeightEntry(this.weightEntry); } -class OpenAddEntryDialog { -} +class OpenAddEntryDialog {} class OpenEditEntryDialog { final WeightEntry weightEntry; @@ -101,4 +101,14 @@ class ChangeProgressChartStartDate { final DateTime dateTime; ChangeProgressChartStartDate(this.dateTime); -} \ No newline at end of file +} + +class LoginWithGoogle { + final List cachedEntries; + + LoginWithGoogle({this.cachedEntries = const []}); +} + +class LogoutAction { + LogoutAction(); +} diff --git a/lib/logic/middleware.dart b/lib/logic/middleware.dart index 6aa7b96..4f2b9f6 100644 --- a/lib/logic/middleware.dart +++ b/lib/logic/middleware.dart @@ -3,6 +3,7 @@ import 'dart:async'; import 'package:firebase_auth/firebase_auth.dart'; import 'package:firebase_database/firebase_database.dart'; import 'package:flutter/services.dart'; +import 'package:google_sign_in/google_sign_in.dart'; import 'package:redux/redux.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:weight_tracker/logic/actions.dart'; @@ -10,6 +11,8 @@ import 'package:weight_tracker/logic/constants.dart'; import 'package:weight_tracker/logic/redux_state.dart'; import 'package:weight_tracker/model/weight_entry.dart'; +final GoogleSignIn _googleSignIn = new GoogleSignIn(); + middleware(Store store, action, NextDispatcher next) { print(action.runtimeType); if (action is InitAction) { @@ -28,19 +31,69 @@ middleware(Store store, action, NextDispatcher next) { _handleGetSavedWeightNote(store); } else if (action is AddWeightFromNotes) { _handleAddWeightFromNotes(store, action); + } else if (action is LoginWithGoogle) { + _handleLoginWithGoogle(store, action); + } else if (action is LogoutAction) { + _handleLogoutAction(store, action); } next(action); if (action is UserLoadedAction) { - _handleUserLoadedAction(store); + _handleUserLoadedAction(store, action); } else if (action is AddDatabaseReferenceAction) { - _handleAddedDatabaseReference(store); + _handleAddedDatabaseReference(store, action); } } +_handleLogoutAction(Store store, LogoutAction action) { + _googleSignIn.signOut(); + FirebaseAuth.instance.signOut().then((_) => FirebaseAuth.instance + .signInAnonymously() + .then((user) => store.dispatch(UserLoadedAction(user)))); +} + +_handleLoginWithGoogle(Store store, LoginWithGoogle action) async { + GoogleSignInAccount googleUser = await _getGoogleUser(); + GoogleSignInAuthentication credentials = await googleUser.authentication; + + bool hasLinkingFailed = false; + try { + await FirebaseAuth.instance.linkWithGoogleCredential( + idToken: credentials.idToken, + accessToken: credentials.accessToken, + ); + } catch (e) { + await FirebaseAuth.instance.signInWithGoogle( + idToken: credentials.idToken, + accessToken: credentials.accessToken, + ); + hasLinkingFailed = true; + } + await FirebaseAuth.instance.updateProfile(new UserUpdateInfo() + ..photoUrl = googleUser.photoUrl + ..displayName = googleUser.displayName); + FirebaseUser newUser = await FirebaseAuth.instance.currentUser(); + + store.dispatch(new UserLoadedAction( + newUser, + cachedEntries: hasLinkingFailed ? action.cachedEntries : [], + )); +} + +Future _getGoogleUser() async { + GoogleSignInAccount googleUser = _googleSignIn.currentUser; + if (googleUser == null) { + googleUser = await _googleSignIn.signInSilently(); + } + if (googleUser == null) { + googleUser = await _googleSignIn.signIn(); + } + return googleUser; +} + _handleAddWeightFromNotes(Store store, AddWeightFromNotes action) { if (store.state.firebaseState?.mainReference != null) { WeightEntry weightEntry = - new WeightEntry(new DateTime.now(), action.weight, null); + new WeightEntry(new DateTime.now(), action.weight, null); store.dispatch(new AddEntryAction(weightEntry)); action = new AddWeightFromNotes(null); } @@ -68,7 +121,14 @@ Future _getSavedWeightNote() async { return null; } -_handleAddedDatabaseReference(Store store) { +_handleAddedDatabaseReference( + Store store, AddDatabaseReferenceAction action) { + //maybe add cached entries + if (action.cachedEntries?.isNotEmpty ?? false) { + action.cachedEntries + .forEach((entry) => store.dispatch(AddEntryAction(entry))); + } + //maybe add height from notes double weight = store.state.weightFromNotes; if (weight != null) { if (store.state.unit == 'lbs') { @@ -76,24 +136,27 @@ _handleAddedDatabaseReference(Store store) { } if (weight >= MIN_KG_VALUE && weight <= MAX_KG_VALUE) { WeightEntry weightEntry = - new WeightEntry(new DateTime.now(), weight, null); + new WeightEntry(new DateTime.now(), weight, null); store.dispatch(new AddEntryAction(weightEntry)); store.dispatch(new ConsumeWeightFromNotes()); } } } -_handleUserLoadedAction(Store store) { - store.dispatch(new AddDatabaseReferenceAction(FirebaseDatabase.instance - .reference() - .child(store.state.firebaseState.firebaseUser.uid) - .child("entries") - ..onChildAdded - .listen((event) => store.dispatch(new OnAddedAction(event))) - ..onChildChanged - .listen((event) => store.dispatch(new OnChangedAction(event))) - ..onChildRemoved - .listen((event) => store.dispatch(new OnRemovedAction(event))))); +_handleUserLoadedAction(Store store, UserLoadedAction action) { + store.dispatch(new AddDatabaseReferenceAction( + FirebaseDatabase.instance + .reference() + .child(store.state.firebaseState.firebaseUser.uid) + .child("entries") + ..onChildAdded + .listen((event) => store.dispatch(new OnAddedAction(event))) + ..onChildChanged + .listen((event) => store.dispatch(new OnChangedAction(event))) + ..onChildRemoved + .listen((event) => store.dispatch(new OnRemovedAction(event))), + cachedEntries: action.cachedEntries, + )); } _handleSetUnitAction(SetUnitAction action, Store store) { diff --git a/lib/logic/reducer.dart b/lib/logic/reducer.dart index 611b125..b4791b6 100644 --- a/lib/logic/reducer.dart +++ b/lib/logic/reducer.dart @@ -8,10 +8,11 @@ ReduxState reduce(ReduxState state, action) { String unit = _reduceUnit(state, action); RemovedEntryState removedEntryState = _reduceRemovedEntryState(state, action); WeightEntryDialogReduxState weightEntryDialogState = - _reduceWeightEntryDialogState(state, action); + _reduceWeightEntryDialogState(state, action); FirebaseState firebaseState = _reduceFirebaseState(state, action); MainPageReduxState mainPageState = _reduceMainPageState(state, action); - DateTime progressChartStartDate = _reduceProgressChartStartDate(state, action); + DateTime progressChartStartDate = + _reduceProgressChartStartDate(state, action); double weightFromNotes = _reduceWeightFromNotes(state, action); return new ReduxState( @@ -78,8 +79,8 @@ RemovedEntryState _reduceRemovedEntryState(ReduxState reduxState, action) { return newState; } -WeightEntryDialogReduxState _reduceWeightEntryDialogState(ReduxState reduxState, - action) { +WeightEntryDialogReduxState _reduceWeightEntryDialogState( + ReduxState reduxState, action) { WeightEntryDialogReduxState newState = reduxState.weightEntryDialogState; if (action is UpdateActiveWeightEntry) { newState = newState.copyWith( @@ -107,7 +108,7 @@ List _reduceEntries(ReduxState state, action) { } else if (action is OnChangedAction) { WeightEntry newValue = new WeightEntry.fromSnapshot(action.event.snapshot); WeightEntry oldValue = - entries.singleWhere((entry) => entry.key == newValue.key); + entries.singleWhere((entry) => entry.key == newValue.key); entries ..[entries.indexOf(oldValue)] = newValue ..sort((we1, we2) => we2.dateTime.compareTo(we1.dateTime)); @@ -117,6 +118,8 @@ List _reduceEntries(ReduxState state, action) { entries ..remove(removedEntry) ..sort((we1, we2) => we2.dateTime.compareTo(we1.dateTime)); + } else if (action is UserLoadedAction) { + entries = []; } return entries; } diff --git a/lib/screens/main_page.dart b/lib/screens/main_page.dart index 91e9871..b19a3e0 100644 --- a/lib/screens/main_page.dart +++ b/lib/screens/main_page.dart @@ -4,7 +4,6 @@ import 'package:flutter_redux/flutter_redux.dart'; import 'package:weight_tracker/logic/actions.dart'; import 'package:weight_tracker/logic/redux_state.dart'; import 'package:weight_tracker/screens/history_page.dart'; -import 'package:weight_tracker/screens/profile_screen.dart'; import 'package:weight_tracker/screens/settings_screen.dart'; import 'package:weight_tracker/screens/statistics_page.dart'; import 'package:weight_tracker/screens/weight_entry_dialog.dart'; @@ -136,32 +135,11 @@ class MainPageState extends State } List _buildMenuActions(BuildContext context) { - List actions = [ - new IconButton( + return [ + IconButton( icon: new Icon(Icons.settings), onPressed: () => _openSettingsPage(context)), ]; - bool showProfile = false; - if (showProfile) { - actions.add(new PopupMenuButton( - onSelected: (val) { - if (val == "Profile") { - Navigator.of(context).push(new MaterialPageRoute( - builder: (context) => new ProfileScreen(), - )); - } - }, - itemBuilder: (context) { - return [ - new PopupMenuItem( - value: "Profile", - child: new Text("Profile"), - ), - ]; - }, - )); - } - return actions; } _scrollToTop() { diff --git a/lib/screens/profile_screen.dart b/lib/screens/profile_screen.dart deleted file mode 100644 index 8824504..0000000 --- a/lib/screens/profile_screen.dart +++ /dev/null @@ -1,53 +0,0 @@ -import 'package:firebase_auth/firebase_auth.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_redux/flutter_redux.dart'; -import 'package:weight_tracker/logic/redux_state.dart'; - -class ProfileScreen extends StatelessWidget { - @override - Widget build(BuildContext context) { - return new StoreConnector( - converter: (store) { - return new _ViewModel( - user: store.state.firebaseState.firebaseUser, - ); - }, - builder: (BuildContext context, _ViewModel vm) { - return new Scaffold( - appBar: new AppBar( - title: new Text("Profile"), - ), - body: new SingleChildScrollView( - child: new Center( - child: new Column( - children: [ - _getUserIcon(vm), - ], - ), - ), - ), - ); - }, - ); - } - - Widget _getUserIcon(_ViewModel vm) { - if (vm.user.isAnonymous) { - return new CircleAvatar( - backgroundImage: new AssetImage("assets/user_icon.png"), - radius: 36.0, - ); - } else { - return new CircleAvatar( - backgroundImage: new NetworkImage(vm.user.photoUrl), - radius: 36.0, - ); - } - } -} - -class _ViewModel { - final FirebaseUser user; - - _ViewModel({@required this.user}); -} diff --git a/lib/screens/profile_view.dart b/lib/screens/profile_view.dart new file mode 100644 index 0000000..ec8df06 --- /dev/null +++ b/lib/screens/profile_view.dart @@ -0,0 +1,149 @@ +import 'package:firebase_auth/firebase_auth.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_redux/flutter_redux.dart'; +import 'package:weight_tracker/logic/actions.dart'; +import 'package:weight_tracker/logic/redux_state.dart'; + +class ProfileView extends StatelessWidget { + @override + Widget build(BuildContext context) { + return new StoreConnector( + converter: (store) { + return new _ViewModel( + user: store.state.firebaseState.firebaseUser, + login: () => store + .dispatch(LoginWithGoogle(cachedEntries: store.state.entries)), + logout: () => store.dispatch(LogoutAction()), + ); + }, + builder: (BuildContext context, _ViewModel vm) { + return vm.user.isAnonymous + ? _anonymousView(context, vm) + : _loggedInView(context, vm); + }, + ); + } + + Widget _loggedInView(BuildContext context, _ViewModel vm) { + return Column( + children: [ + _drawAvatar(NetworkImage(vm.user.photoUrl)), + _drawLabel(context, vm.user.displayName), + Text(vm.user.email), + Padding( + padding: const EdgeInsets.symmetric(vertical: 16.0), + child: Container( + width: 120.0, + child: RaisedButton( + color: Colors.green, + child: Text( + "Logout", + style: TextStyle(color: Colors.white), + ), + onPressed: vm.logout, + ), + ), + ) + ], + ); + } + + Widget _anonymousView(BuildContext context, _ViewModel vm) { + return Column( + children: [ + _drawAvatar(AssetImage('assets/user.png')), + _drawLabel(context, 'Anonymous user'), + Text( + 'To synchronize your data across all devices link your data with a Google account.', + textAlign: TextAlign.center, + ), + Padding( + padding: const EdgeInsets.symmetric(vertical: 16.0), + child: OAuthLoginButton( + onPressed: vm.login, + text: 'Continue with Google', + assetName: 'assets/google.png', + backgroundColor: Colors.white, + ), + ), + ], + ); + } + + Padding _drawLabel(BuildContext context, String label) { + return Padding( + padding: const EdgeInsets.all(16.0), + child: Text( + label, + style: Theme.of(context).textTheme.display1, + ), + ); + } + + Padding _drawAvatar(ImageProvider imageProvider) { + return Padding( + padding: const EdgeInsets.only(top: 16.0), + child: CircleAvatar( + backgroundImage: imageProvider, + backgroundColor: Colors.white10, + radius: 48.0, + ), + ); + } +} + +class _ViewModel { + final FirebaseUser user; + final Function() login; + final Function() logout; + + _ViewModel({ + @required this.user, + @required this.login, + @required this.logout, + }); +} + +class OAuthLoginButton extends StatelessWidget { + final Function() onPressed; + final String text; + final String assetName; + final Color backgroundColor; + + OAuthLoginButton( + {@required this.onPressed, + @required this.text, + @required this.assetName, + @required this.backgroundColor}); + + @override + Widget build(BuildContext context) { + return new Container( + width: 240.0, + child: new RaisedButton( + color: backgroundColor, + onPressed: onPressed, + padding: new EdgeInsets.only(right: 8.0), + child: new Row( + children: [ + Padding( + padding: const EdgeInsets.all(8.0), + child: new Image.asset( + assetName, + height: 30.0, + ), + ), + new Expanded( + child: new Padding( + padding: const EdgeInsets.only(left: 8.0), + child: new Text( + text, + style: Theme.of(context).textTheme.button, + ), + )), + ], + ), + ), + ); + } +} diff --git a/lib/screens/settings_screen.dart b/lib/screens/settings_screen.dart index 8cb03d5..6a83b6b 100644 --- a/lib/screens/settings_screen.dart +++ b/lib/screens/settings_screen.dart @@ -3,6 +3,7 @@ import 'package:flutter_redux/flutter_redux.dart'; import 'package:meta/meta.dart'; import 'package:weight_tracker/logic/actions.dart'; import 'package:weight_tracker/logic/redux_state.dart'; +import 'package:weight_tracker/screens/profile_view.dart'; @immutable class SettingsPageViewModel { @@ -26,30 +27,39 @@ class SettingsPage extends StatelessWidget { appBar: new AppBar( title: new Text("Settings"), ), - body: new Padding( - padding: new EdgeInsets.all(16.0), - child: new Row( - children: [ - new Expanded( - child: new Text( - "Unit", - style: Theme.of(context).textTheme.headline, - )), - new DropdownButton( - key: const Key('UnitDropdown'), - value: viewModel.unit, - items: ["kg", "lbs"].map((String value) { - return new DropdownMenuItem( - value: value, - child: new Text(value), - ); - }).toList(), - onChanged: (newUnit) => viewModel.onUnitChanged(newUnit), - ), - ], - ), + body: Column( + children: [ + new Padding( + padding: new EdgeInsets.all(16.0), + child: _unitView(context, viewModel), + ), + ProfileView(), + ], ), ); }); } + + Row _unitView(BuildContext context, SettingsPageViewModel viewModel) { + return new Row( + children: [ + new Expanded( + child: new Text( + "Unit", + style: Theme.of(context).textTheme.headline, + )), + new DropdownButton( + key: const Key('UnitDropdown'), + value: viewModel.unit, + items: ["kg", "lbs"].map((String value) { + return new DropdownMenuItem( + value: value, + child: new Text(value), + ); + }).toList(), + onChanged: (newUnit) => viewModel.onUnitChanged(newUnit), + ), + ], + ); + } } diff --git a/pubspec.yaml b/pubspec.yaml index 1888a1d..446042d 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -14,6 +14,7 @@ dependencies: intl: "^0.15.7" shared_preferences: "^0.4.2" mockito: "^3.0.0" + google_sign_in: "^3.2.2" dev_dependencies: flutter_test: @@ -24,4 +25,6 @@ flutter: uses-material-design: true assets: - - assets/scale-bathroom.png + - assets/scale-bathroom.png + - assets/user.png + - assets/google.png