diff --git a/lib/data/repository/rest/user_rest.dart b/lib/data/repository/rest/user_rest.dart index 1d9e86b..23afe0d 100644 --- a/lib/data/repository/rest/user_rest.dart +++ b/lib/data/repository/rest/user_rest.dart @@ -4,34 +4,42 @@ import 'package:hackatrix/data/repository/user_repository.dart'; import 'package:hackatrix/data/repository/util/api.dart'; import 'package:hackatrix/data/repository/util/network_util.dart'; import 'package:hackatrix/domain/model/user.dart'; +import 'package:hackatrix/presentation/profile/login/bloc/authenticate/login_bloc.dart'; class UserRest implements UserRepository { - NetworkUtil _netUtil = new NetworkUtil(); @override - Future authenticate(String username, String password){ - return _netUtil.post(API_USER_AUTHENTICATE, body: {"username": username, "password": password}).then((dynamic response) { + Future authenticate(LoginCredentials credentials) { + return _netUtil.post(API_USER_AUTHENTICATE, body: { + "username": credentials.username, + "password": credentials.password + }).then((dynamic response) { String token = response['token']; - if (token != null && token.isNotEmpty){ + if (token != null && token.isNotEmpty) { return token; } - throw new Exception("Email/Contraseña incorrectos"); + if (response["non_field_errors"] != null) { + throw ("Unable to log in with provided credentials"); + } + + throw ("Email/Contraseña incorrectos"); }); } @override Future profile(String token) { - return _netUtil.get(API_USER_PROFILE, headers: {"Authorization" : "Token $token"}).then((dynamic response) { + return _netUtil.get(API_USER_PROFILE, + headers: {"Authorization": "Token $token"}).then((dynamic response) { Map data = response as Map; return User.fromJson(data['user']); }); } @override - Future logout() { - return _netUtil.post(API_USER_LOGOUT).then((dynamic response) { - return true; - }); - } -} \ No newline at end of file + Future logout() { + return _netUtil.post(API_USER_LOGOUT).then((dynamic response) { + return true; + }); + } +} diff --git a/lib/data/repository/user_repository.dart b/lib/data/repository/user_repository.dart index c7ff77a..c6a10c9 100644 --- a/lib/data/repository/user_repository.dart +++ b/lib/data/repository/user_repository.dart @@ -1,9 +1,10 @@ import 'dart:async'; import 'package:hackatrix/domain/model/user.dart'; +import 'package:hackatrix/presentation/profile/login/bloc/authenticate/login_bloc.dart'; abstract class UserRepository { - Future authenticate(String username, String password); + Future authenticate(LoginCredentials credentials); Future profile(String token); Future logout(); } \ No newline at end of file diff --git a/lib/main.dart b/lib/main.dart index 880f0fd..3ee02b1 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,17 +1,19 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; -import 'package:hackatrix/presentation/home/home_page.dart'; +import 'package:hackatrix/data/repository/rest/event_rest.dart'; +import 'package:hackatrix/presentation/home/bloc/home_bloc.dart'; +import 'package:hackatrix/presentation/home/bloc/home_page_2.dart'; +import 'package:hackatrix/presentation/home/bloc/home_provider.dart'; import 'package:hackatrix/presentation/util/theme.dart' as theme; void main() => runApp(new HackatrixApp()); class HackatrixApp extends StatelessWidget { - - @override Widget build(BuildContext context) { config(); - return new MaterialApp( - home: new HomePage(), + return MaterialApp( + home: + HomeProvider(homeBloc: HomeBloc(EventRest()), child: HomePage2(true)), theme: theme.CompanyThemeData, ); } diff --git a/lib/presentation/home/bloc/home_bloc.dart b/lib/presentation/home/bloc/home_bloc.dart new file mode 100644 index 0000000..5c098e9 --- /dev/null +++ b/lib/presentation/home/bloc/home_bloc.dart @@ -0,0 +1,24 @@ +import 'dart:async'; +import 'package:hackatrix/data/repository/event_repository.dart'; +import 'package:hackatrix/domain/model/event.dart'; +import 'package:rxdart/rxdart.dart'; + +class HomeBloc { + final EventRepository repository; + + Stream> _results = Stream.empty(); + ReplaySubject _query = ReplaySubject(); + + // Getters + Stream> get results => _results; + + Sink get query => _query; + + HomeBloc(this.repository) { + _results = _query.asyncMap(repository.getEventList).asBroadcastStream(); + } + + void dispose() { + _query.close(); + } +} diff --git a/lib/presentation/home/bloc/home_page_2.dart b/lib/presentation/home/bloc/home_page_2.dart new file mode 100644 index 0000000..95f2e27 --- /dev/null +++ b/lib/presentation/home/bloc/home_page_2.dart @@ -0,0 +1,95 @@ +import 'dart:async'; + +import 'package:flutter/material.dart'; +import 'package:flutter_staggered_grid_view/flutter_staggered_grid_view.dart'; +import 'package:hackatrix/domain/model/event.dart'; +import 'package:hackatrix/presentation/event_detail/event_detail_page.dart'; +import 'package:hackatrix/presentation/home/bloc/home_bloc.dart'; +import 'package:hackatrix/presentation/home/home_item.dart'; +import 'package:hackatrix/presentation/home/bloc/home_provider.dart'; +import 'package:hackatrix/presentation/menu/menu_page.dart'; +import 'package:hackatrix/presentation/util/constants.dart'; +import 'package:hackatrix/presentation/util/custom_widgets/my_scaffold.dart'; + +class HomePage2 extends StatelessWidget { + //Completer _completer; + + final GlobalKey _refreshIndicatorKey = + new GlobalKey(); + final GlobalKey _scaffoldKey = new GlobalKey(); + Completer _completer; + + bool isFirstLaunched; + + // Events + void _onTapEvent(Event event, BuildContext context) { + print("event tapped: ${event.title}"); + Navigator.push( + context, + new MaterialPageRoute( + builder: (BuildContext context) => new EventDetailPage(event), + ), + ); + } + + HomePage2(this.isFirstLaunched); + + @override + Widget build(BuildContext context) { + final HomeBloc homeBloc = HomeProvider.of(context); + + if (isFirstLaunched) { + homeBloc.query.add(1); + isFirstLaunched = false; + } + + return MyScaffold( + key: _scaffoldKey, + title: APP_NAME, + drawer: Drawer( + child: MenuSidePage(_scaffoldKey), + ), + body: buildStaggeredGridView(homeBloc), + ); + } + + Widget buildStaggeredGridView(HomeBloc homeBloc) { + return StreamBuilder( + stream: homeBloc.results, + builder: (context, snapshot) { + if (!snapshot.hasData) { + return Center( + child: CircularProgressIndicator(), + ); + } else { + return RefreshIndicator( + key: _refreshIndicatorKey, + child: StaggeredGridView.countBuilder( + crossAxisCount: 2, + padding: EdgeInsets.all(4.0), + itemCount: snapshot.data == null ? 0 : snapshot.data.length, + staggeredTileBuilder: (int index) => + new StaggeredTile.count(index == 0 ? 2 : 1, 1.2), + mainAxisSpacing: 5.0, + crossAxisSpacing: 5.0, + itemBuilder: (BuildContext context, int index) => new HomeItem( + snapshot.data[index], context, + callBack: _onTapEvent)), + onRefresh: () async { + _completer = new Completer(); + + homeBloc.query.add(1); + + homeBloc.results.listen((List list) { + if (_completer != null && !_completer.isCompleted) + _completer.complete(); + }); + + return _completer.future; + }, + ); + } + }, + ); + } +} diff --git a/lib/presentation/home/bloc/home_provider.dart b/lib/presentation/home/bloc/home_provider.dart new file mode 100644 index 0000000..aa139d9 --- /dev/null +++ b/lib/presentation/home/bloc/home_provider.dart @@ -0,0 +1,22 @@ +import 'package:flutter/widgets.dart'; +import 'package:hackatrix/data/repository/rest/event_rest.dart'; +import 'package:hackatrix/presentation/home/bloc/home_bloc.dart'; + +// InheritedWidget Allows to pass its sates to his child widgets +class HomeProvider extends InheritedWidget { + final HomeBloc homeBloc; + + @override + bool updateShouldNotify(InheritedWidget oldWidget) { + return true; + } + + // To access the block from anywhere in the application + static HomeBloc of(BuildContext context) => + (context.inheritFromWidgetOfExactType(HomeProvider) as HomeProvider) + .homeBloc; + + HomeProvider({Key key, HomeBloc homeBloc, Widget child}) + : this.homeBloc = homeBloc ?? HomeBloc(EventRest()), + super(child: child, key: key); +} diff --git a/lib/presentation/home/home_item.dart b/lib/presentation/home/home_item.dart index 87f668b..b82c8a5 100644 --- a/lib/presentation/home/home_item.dart +++ b/lib/presentation/home/home_item.dart @@ -2,11 +2,13 @@ import 'package:flutter/material.dart'; import 'package:cached_network_image/cached_network_image.dart'; import 'package:hackatrix/domain/model/event.dart'; -typedef void OnTapItemCallBack(Event item); +typedef void OnTapItemCallBack(Event item, BuildContext context); class HomeItem extends StatelessWidget { - HomeItem(this._event, {this.callBack}); + HomeItem(this._event, this.context,{this.callBack}); + final Event _event; + final BuildContext context; final OnTapItemCallBack callBack; @override @@ -21,16 +23,18 @@ class HomeItem extends StatelessWidget { ); return new Container( - key: new Key("${_event.id}"), - child: new Stack(children: [ + key: new Key("${_event.id}"), + child: new Stack( + children: [ new GridTile( child: imageHero, footer: new GridTileBar( - title: new Text(_event.title, overflow: TextOverflow.clip, style: new TextStyle( - color: Colors.white, - fontSize: 14.0, - fontWeight: FontWeight.w400 - )), + title: new Text(_event.title, + overflow: TextOverflow.clip, + style: new TextStyle( + color: Colors.white, + fontSize: 14.0, + fontWeight: FontWeight.w400)), backgroundColor: Colors.black38, ), ), @@ -38,8 +42,10 @@ class HomeItem extends StatelessWidget { child: new Material( color: Colors.transparent, child: new InkWell( - onTap: () => callBack(_event), + onTap: () => callBack(_event,context), ))), - ])); + ], + ), + ); } } diff --git a/lib/presentation/home/home_page.dart b/lib/presentation/home/home_page.dart index 8dd25e0..78d5e91 100644 --- a/lib/presentation/home/home_page.dart +++ b/lib/presentation/home/home_page.dart @@ -21,9 +21,9 @@ class _HomePageState extends State implements HomeView { List _elements = List(); Completer _completer; final GlobalKey _refreshIndicatorKey = - new GlobalKey(); + new GlobalKey(); final GlobalKey _scaffoldKey = - new GlobalKey(); + new GlobalKey(); _HomePageState() { _presenter = new HomePresenter(this, new EventRest()); @@ -53,11 +53,11 @@ class _HomePageState extends State implements HomeView { }); } - void _onTapEvent(Event event){ + void _onTapEvent(Event event, BuildContext context) { print("event tapped: ${event.title}"); Navigator.push(context, new MaterialPageRoute( - builder: (BuildContext context) => new EventDetailPage(event), - ),); + builder: (BuildContext context) => new EventDetailPage(event), + ),); } @override @@ -76,20 +76,20 @@ class _HomePageState extends State implements HomeView { return new RefreshIndicator( key: _refreshIndicatorKey, child: StaggeredGridView.countBuilder( - crossAxisCount: 2, - padding: EdgeInsets.all(4.0), - itemCount: _elements.length, - staggeredTileBuilder: (int index) => - new StaggeredTile.count(index == 0 ? 2 : 1, 1.2), - mainAxisSpacing: 5.0, - crossAxisSpacing: 5.0, - itemBuilder: (BuildContext context, int index) => - new HomeItem(_elements[index], callBack: _onTapEvent) + crossAxisCount: 2, + padding: EdgeInsets.all(4.0), + itemCount: _elements.length, + staggeredTileBuilder: (int index) => + new StaggeredTile.count(index == 0 ? 2 : 1, 1.2), + mainAxisSpacing: 5.0, + crossAxisSpacing: 5.0, + itemBuilder: (BuildContext context, int index) => + new HomeItem(_elements[index], context, callBack: _onTapEvent) ), onRefresh: _refreshList, ); } - + } diff --git a/lib/presentation/menu/menu_page.dart b/lib/presentation/menu/menu_page.dart index 70f6b8c..21f11f5 100644 --- a/lib/presentation/menu/menu_page.dart +++ b/lib/presentation/menu/menu_page.dart @@ -1,12 +1,17 @@ import 'package:flutter/material.dart'; +import 'package:hackatrix/data/repository/rest/user_rest.dart'; import 'package:hackatrix/domain/model/user.dart'; import 'package:hackatrix/presentation/admin/event_list_page.dart'; +import 'package:hackatrix/presentation/profile/login/bloc/authenticate/login_bloc.dart'; +import 'package:hackatrix/presentation/profile/login/bloc/authenticate/login_provider.dart'; +import 'package:hackatrix/presentation/profile/login/bloc/login_page_2.dart'; import 'package:hackatrix/presentation/profile/login/login_page.dart'; import 'package:hackatrix/presentation/profile/my_profile/profile_page.dart'; import 'package:hackatrix/presentation/util/preferences/preference_manager.dart'; class MenuSidePage extends StatefulWidget { final GlobalKey _scaffoldKey; + MenuSidePage(this._scaffoldKey); @override @@ -22,7 +27,8 @@ class _MenuSidePageState extends State { Navigator.push( context, new MaterialPageRoute( - builder: (BuildContext context) => new LoginPage(_onUserLogged), + builder: (BuildContext context) => LoginProvider( + loginBloc: LoginBloC(UserRest()), child: LoginPage2(_onUserLogged)), ), ); } @@ -32,7 +38,8 @@ class _MenuSidePageState extends State { Navigator.push( context, new MaterialPageRoute( - builder: (BuildContext context) => new ProfilePage(_user, _onUserLogOut), + builder: (BuildContext context) => + new ProfilePage(_user, _onUserLogOut), ), ); } @@ -91,15 +98,20 @@ class _MenuSidePageState extends State { subItems.add(createHeaderWithPadding(new Text("Perfil"))); subItems.add(new FlatButton.icon( icon: new Icon(Icons.person), - label: new Text("Perfil", style: TextStyle(color: Colors.blueGrey),), + label: new Text( + "Perfil", + style: TextStyle(color: Colors.blueGrey), + ), onPressed: () => _onTapProfile(), )); - subItems.add(createHeaderWithPadding(new Text("Admin"))); subItems.add(new FlatButton.icon( icon: new Icon(Icons.settings), - label: new Text("Admin", style: TextStyle(color: Colors.blueGrey),), + label: new Text( + "Admin", + style: TextStyle(color: Colors.blueGrey), + ), onPressed: () => _onTapAdmin(), )); } else { @@ -156,7 +168,10 @@ class _MenuSidePageState extends State { } } - Padding createHeaderWithPadding(Widget child){ - return new Padding(child: child, padding: EdgeInsets.symmetric(vertical: 6.0),); + Padding createHeaderWithPadding(Widget child) { + return new Padding( + child: child, + padding: EdgeInsets.symmetric(vertical: 6.0), + ); } } diff --git a/lib/presentation/profile/login/bloc/authenticate/login_bloc.dart b/lib/presentation/profile/login/bloc/authenticate/login_bloc.dart new file mode 100644 index 0000000..504d34f --- /dev/null +++ b/lib/presentation/profile/login/bloc/authenticate/login_bloc.dart @@ -0,0 +1,89 @@ +import 'dart:async'; + +import 'package:flutter/material.dart'; +import 'package:hackatrix/data/repository/user_repository.dart'; +import 'package:hackatrix/presentation/profile/forgot_password/forgot_password_page.dart'; +import 'package:hackatrix/presentation/util/preferences/preference_manager.dart'; + +import 'package:rxdart/rxdart.dart'; + +class LoginBloC { + final UserRepository repository; + + final PreferenceManager _preferences = new PreferenceManager(); + + PublishSubject _output = PublishSubject(); + + ReplaySubject _input = ReplaySubject(); + + // Getters + Stream get output => _output; + + Sink get input => _input; + + LoginBloC(this.repository) { + _input.listen((credentials) { + _output.add(LoginOutput(LoginOutCase.isLoading, true)); + + repository.authenticate(credentials).then((token) { + saveToken(token).then((isSaved) { + if (isSaved) { + _output.add(LoginOutput(LoginOutCase.token, token)); + } else { + _output.add( + LoginOutput(LoginOutCase.errorMessage, "Error saving Token!")); + } + }); + }, onError: (error) { + _output.add(LoginOutput(LoginOutCase.errorMessage, error)); + }); + }); + } + + void dispose() { + _input.close(); + _output.close(); + } + + Future saveToken(String token) async { + return _preferences.saveToken(token); + } + + String validateFieldForInput(LoginInput input, String value) { + switch (input) { + case LoginInput.username: + return !value.contains('@') ? 'No es un email válido.' : null; + case LoginInput.password: + return value.isEmpty ? 'Contraseña no válida.' : null; + default: + return null; + } + } + + void forgotPassword(BuildContext context) { + Navigator.push( + context, + new MaterialPageRoute( + builder: (BuildContext context) => ForgotPasswordPage(), + ), + ); + } +} + +class LoginCredentials { + final String username; + final String password; + + LoginCredentials(this.username, this.password); +} + +class LoginOutput { + final LoginOutCase loginOutCase; + dynamic info; + + LoginOutput(this.loginOutCase, this.info); +} + +enum LoginInput { username, password } + +enum LoginOutCase { isLoading, token, errorMessage } diff --git a/lib/presentation/profile/login/bloc/authenticate/login_provider.dart b/lib/presentation/profile/login/bloc/authenticate/login_provider.dart new file mode 100644 index 0000000..72b11e0 --- /dev/null +++ b/lib/presentation/profile/login/bloc/authenticate/login_provider.dart @@ -0,0 +1,18 @@ +import 'package:flutter/widgets.dart'; +import 'package:hackatrix/data/repository/rest/user_rest.dart'; +import 'package:hackatrix/presentation/profile/login/bloc/authenticate/login_bloc.dart'; + +class LoginProvider extends InheritedWidget { + final LoginBloC loginBloc; + + @override + bool updateShouldNotify(InheritedWidget oldWidget) => true; + + static LoginBloC of(BuildContext context) => + (context.inheritFromWidgetOfExactType(LoginProvider) as LoginProvider) + .loginBloc; + + LoginProvider({Key key, LoginBloC loginBloc, Widget child}) + : this.loginBloc = loginBloc ?? LoginBloC(UserRest()), + super(child: child, key: key); +} diff --git a/lib/presentation/profile/login/bloc/login_page_2.dart b/lib/presentation/profile/login/bloc/login_page_2.dart new file mode 100644 index 0000000..52727c0 --- /dev/null +++ b/lib/presentation/profile/login/bloc/login_page_2.dart @@ -0,0 +1,190 @@ +import 'dart:async'; + +import 'package:flutter/material.dart'; +import 'package:hackatrix/presentation/profile/login/bloc/authenticate/login_bloc.dart'; +import 'package:hackatrix/presentation/profile/login/bloc/authenticate/login_provider.dart'; +import 'package:hackatrix/presentation/util/custom_widgets/my_primary_button.dart'; +import 'package:hackatrix/presentation/util/custom_widgets/my_scaffold.dart'; +import 'package:hackatrix/presentation/util/custom_widgets/my_secondary_button.dart'; +import 'package:hackatrix/presentation/util/custom_widgets/password_field.dart'; +import 'package:progress_hud/progress_hud.dart'; + +class LoginPage2 extends StatefulWidget { + final VoidCallback onLogin; + + LoginPage2(this.onLogin); + + @override + State createState() => _LoginPage2State(); +} + +class _LoginPage2State extends State { + // keys + final GlobalKey _formKey = new GlobalKey(); + final GlobalKey _scaffoldKey = new GlobalKey(); + + String _email; + String _password; + + @override + Widget build(BuildContext context) { + return MyScaffold( + key: _scaffoldKey, + title: "Iniciar Sesión", + body: getBodyView(), + ); + } + + Widget getBodyView() { + final LoginBloC bloc = LoginProvider.of(context); + + return Stack( + children: [ + new Container( + padding: new EdgeInsets.symmetric(horizontal: 20.0), + child: new Form( + key: _formKey, + autovalidate: true, + child: new ListView( + children: [ + _getVerticalPadding(), + new Image.asset( + "images/ic_launcher.png", + height: 100.0, + fit: BoxFit.contain, + ), + _getVerticalPadding(), + new TextFormField( + autocorrect: false, + autovalidate: false, + keyboardType: TextInputType.emailAddress, + maxLines: 1, + decoration: new InputDecoration( + border: const OutlineInputBorder(), + labelText: "Email", + ), + validator: (val) => + bloc.validateFieldForInput(LoginInput.username, val), + onSaved: (val) => _email = val, + ), + _getVerticalPadding(), + new PasswordField( + labelText: 'Contraseña', + inputBorder: const OutlineInputBorder(), + validator: (val) => + bloc.validateFieldForInput(LoginInput.password, val), + onSaved: (val) => _password = val, + ), + _getVerticalPadding(), + new RawMaterialButton( + child: new Align( + alignment: Alignment.topRight, + child: Text( + "Olvidé mi contraseña", + textAlign: TextAlign.end, + ), + ), + textStyle: new TextStyle( + color: Colors.orange, + ), + onPressed: () { + bloc.forgotPassword(context); + }, + ), + _getVerticalPadding(), + new MyPrimaryButton( + onPressed: _submit, + text: 'Login', + ), + _getVerticalPadding(), + new MySecondaryButton( + onPressed: _submit, + text: 'Crear Cuenta', + ), + ], + ), + ), + ), + StreamBuilder( + stream: bloc.output, + builder: (context, snapshot) { + if (snapshot.hasData && + snapshot.connectionState == ConnectionState.active) { + //print("Stream event"); + switch (snapshot.data.loginOutCase) { + case LoginOutCase.isLoading: + { + return snapshot.data.info + ? _getProgressHUD(true) + : _getEmptyContainer(); + } + case LoginOutCase.errorMessage: + { + Future + .delayed(Duration(milliseconds: 400)) + .whenComplete(() { + _onError(snapshot.data.info); + }); + return _getEmptyContainer(); + } + case LoginOutCase.token: + { + Future + .delayed(Duration(milliseconds: 400)) + .whenComplete(() { + _onLoginDone(); + }); + return _getEmptyContainer(); + } + } + } else { + return _getEmptyContainer(); + } + }, + ), + ], + ); + } + + void _submit() { + final LoginBloC bloc = LoginProvider.of(context); + final form = _formKey.currentState; + if (form.validate()) { + form.save(); + bloc.input.add(LoginCredentials(_email, _password)); + //dismiss the keyboard + FocusScope.of(context).requestFocus(new FocusNode()); + } + } + + _onLoginDone() { + widget.onLogin(); + Navigator.of(context).pop(); + } + + _onError(String message) { + final snackBar = SnackBar(content: new Text(message)); + _scaffoldKey.currentState.showSnackBar(snackBar); + } + + Padding _getVerticalPadding() { + return Padding( + padding: EdgeInsets.symmetric(vertical: 10.0), + ); + } + + Widget _getEmptyContainer() { + return Container( + height: 0.0, + width: 0.0, + color: Colors.transparent, + ); + } + + ProgressHUD _getProgressHUD(bool loading) { + return ProgressHUD( + backgroundColor: Colors.black12, + loading: loading, + ); + } +} diff --git a/lib/presentation/profile/login/login_presenter.dart b/lib/presentation/profile/login/login_presenter.dart index ef48277..79ac148 100644 --- a/lib/presentation/profile/login/login_presenter.dart +++ b/lib/presentation/profile/login/login_presenter.dart @@ -15,13 +15,13 @@ class LoginPresenter { LoginPresenter(this._view, this._repository); void actionAuthenticate(String email, String password) { - _repository + /*_repository .authenticate(email, password) .then((token) => _getUserProfile(token)) .catchError((onError) { print(onError.toString()); _view.onError(onError.toString()); - }); + });*/ } _getUserProfile(String token) {