diff --git a/lib/data/file_storage.dart b/lib/data/file_storage.dart index 0f05a15fe..89d0f8395 100644 --- a/lib/data/file_storage.dart +++ b/lib/data/file_storage.dart @@ -16,7 +16,7 @@ class FileStorage { ); /// LoadProducts - Future> loadProducts() async { + Future> loadData() async { final file = await _getLocalFile(); final contents = await file.readAsString(); @@ -29,7 +29,7 @@ class FileStorage { */ } - Future saveProducts(List products) async { + Future saveData(List products) async { final file = await _getLocalFile(); /* diff --git a/lib/data/models/entities.dart b/lib/data/models/entities.dart index a46ad078f..8d72f2da5 100644 --- a/lib/data/models/entities.dart +++ b/lib/data/models/entities.dart @@ -3,18 +3,18 @@ import 'package:json_annotation/json_annotation.dart'; part 'entities.g.dart'; @JsonSerializable() -class ProductResponse extends Object with _$ProductResponseSerializerMixin { +class BaseResponse extends Object with _$BaseResponseSerializerMixin { //final String message; @JsonKey(name: "data") - final List products; + final List data; - ProductResponse( + BaseResponse( //this.message, - this.products, + this.data, ); - factory ProductResponse.fromJson(Map json) => _$ProductResponseFromJson(json); + factory BaseResponse.fromJson(Map json) => _$BaseResponseFromJson(json); } @JsonSerializable() diff --git a/lib/data/models/entities.g.dart b/lib/data/models/entities.g.dart index cc5ea0039..dcbf30093 100644 --- a/lib/data/models/entities.g.dart +++ b/lib/data/models/entities.g.dart @@ -6,16 +6,12 @@ part of 'entities.dart'; // Generator: JsonSerializableGenerator // ************************************************************************** -ProductResponse _$ProductResponseFromJson(Map json) => - new ProductResponse((json['data'] as List) - ?.map((e) => e == null - ? null - : new ProductEntity.fromJson(e as Map)) - ?.toList()); +BaseResponse _$BaseResponseFromJson(Map json) => + new BaseResponse(json['data'] as List); -abstract class _$ProductResponseSerializerMixin { - List get products; - Map toJson() => {'data': products}; +abstract class _$BaseResponseSerializerMixin { + List get data; + Map toJson() => {'data': data}; } ProductEntity _$ProductEntityFromJson(Map json) => diff --git a/lib/data/models/models.dart b/lib/data/models/models.dart index fcd076fe4..cb08dd3d5 100644 --- a/lib/data/models/models.dart +++ b/lib/data/models/models.dart @@ -2,7 +2,7 @@ export 'package:invoiceninja/data/models/entities.dart'; class User { final String token; - final String id; + final int id; User(this.token, this.id); diff --git a/lib/data/repositories/product_repository.dart b/lib/data/repositories/product_repository.dart index 02b6d6f9f..c235ffa0b 100644 --- a/lib/data/repositories/product_repository.dart +++ b/lib/data/repositories/product_repository.dart @@ -2,6 +2,7 @@ import 'dart:async'; import 'dart:core'; import 'package:meta/meta.dart'; +import 'package:invoiceninja/redux/auth/auth_state.dart'; import 'package:invoiceninja/data/models/entities.dart'; import 'package:invoiceninja/data/repositories/repositories.dart'; import 'package:invoiceninja/data/file_storage.dart'; @@ -9,7 +10,7 @@ import 'package:invoiceninja/data/web_client.dart'; /// A class that glues together our local file storage and web client. It has a /// clear responsibility: Load Products and Persist products. -class ProductsRepositoryFlutter implements ProductsRepository { +class ProductsRepositoryFlutter implements BaseRepository { final FileStorage fileStorage; final WebClient webClient; @@ -21,29 +22,35 @@ class ProductsRepositoryFlutter implements ProductsRepository { /// Loads products first from File storage. If they don't exist or encounter an /// error, it attempts to load the Products from a Web Client. @override - Future> loadProducts() async { - + Future> loadData(AuthState auth) async { print('ProductRepo: loadProducts...'); + final products = await webClient.fetchData( + auth.url + '/products', auth.token); + + //fileStorage.saveProducts(products); + + return products.map((product) => ProductEntity.fromJson(product)).toList(); + + /* try { - return await fileStorage.loadProducts(); - } catch (e) { - final products = await webClient.fetchProducts(); + return await fileStorage.loadData(); + } catch (exception) { + final products = await webClient.fetchData( + auth.url + '/products', auth.token); - print('ProductRepo: result'); - print(products); - - fileStorage.saveProducts(products); + //fileStorage.saveProducts(products); return products; } + */ } // Persists products to local disk and the web @override - Future saveProducts(List products) { + Future saveData(List products) { return Future.wait([ - fileStorage.saveProducts(products), + fileStorage.saveData(products), webClient.postProducts(products), ]); } diff --git a/lib/data/repositories/repositories.dart b/lib/data/repositories/repositories.dart index ebdfb9061..406b7e16a 100644 --- a/lib/data/repositories/repositories.dart +++ b/lib/data/repositories/repositories.dart @@ -1,5 +1,6 @@ import 'dart:async'; import 'dart:core'; +import 'package:invoiceninja/redux/auth/auth_state.dart'; import 'package:invoiceninja/data/models/entities.dart'; @@ -11,11 +12,11 @@ import 'package:invoiceninja/data/models/entities.dart'; /// The domain layer should depend on this abstract class, and each app can /// inject the correct implementation depending on the environment, such as /// web or Flutter. -abstract class ProductsRepository { +abstract class BaseRepository { /// Loads products first from File storage. If they don't exist or encounter an /// error, it attempts to load the Todos from a Web Client. - Future> loadProducts(); + Future> loadData(AuthState auth); // Persists products to local disk and the web - Future saveProducts(List products); + Future saveData(List data); } diff --git a/lib/data/web_client.dart b/lib/data/web_client.dart index 415dc3508..07e85433b 100644 --- a/lib/data/web_client.dart +++ b/lib/data/web_client.dart @@ -13,20 +13,16 @@ class WebClient { const WebClient(); /// Mock that "fetches" some Products from a "web service" after a short delay - Future> fetchProducts() async { - - print('Web Client: fetchProducts...'); - - var url = ""; + Future> fetchData(String url, String token) async { final response = await http.Client().get( url, - headers: {'X-Ninja-Token': ""}, + headers: {'X-Ninja-Token': token}, ); - return ProductResponse + return BaseResponse .fromJson(json.decode(response.body)) - .products + .data .toList(); } diff --git a/lib/main.dart b/lib/main.dart index 91dc3ebd1..d113aa280 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -10,6 +10,7 @@ import 'package:invoiceninja/routes.dart'; import 'package:invoiceninja/redux/product/product_actions.dart'; import 'package:invoiceninja/redux/product/product_middleware.dart'; import 'package:invoiceninja/redux/app/app_reducer.dart'; +import 'package:redux_logging/redux_logging.dart'; void main() { @@ -25,7 +26,11 @@ class InvoiceNinjaApp extends StatelessWidget { final store = Store( appReducer, initialState: AppState.loading(), - middleware: createStoreProductsMiddleware(), + middleware: [] + ..addAll(createStoreProductsMiddleware()) + ..addAll([ + LoggingMiddleware.printer(), + ]) ); @override @@ -40,7 +45,6 @@ class InvoiceNinjaApp extends StatelessWidget { theme: new ThemeData.dark(), title: 'Invoice Ninja', routes: { - /* NinjaRoutes.login: (context) { return StoreBuilder( builder: (context, store) { @@ -48,7 +52,6 @@ class InvoiceNinjaApp extends StatelessWidget { }, ); }, - */ NinjaRoutes.dashboard: (context) { return StoreBuilder( builder: (context, store) { diff --git a/lib/redux/app/app_reducer.dart b/lib/redux/app/app_reducer.dart index cefde7f94..e9825b956 100644 --- a/lib/redux/app/app_reducer.dart +++ b/lib/redux/app/app_reducer.dart @@ -18,7 +18,7 @@ AppState appReducer(AppState state, action) { return AppState( isLoading: loadingReducer(state.isLoading, action), - auth: authReducer(state.auth, action), products: productsReducer(state.products, action), + auth: authReducer(state.auth, action), ); } diff --git a/lib/redux/app/app_state.dart b/lib/redux/app/app_state.dart index ec58e9f40..de2490417 100644 --- a/lib/redux/app/app_state.dart +++ b/lib/redux/app/app_state.dart @@ -10,8 +10,9 @@ class AppState { AppState( {this.isLoading = false, - AuthState auth, - this.products = const []}); + this.products = const [], + AuthState auth}): + auth = auth ?? new AuthState(); factory AppState.loading() => AppState(isLoading: true); @@ -55,6 +56,6 @@ class AppState { @override String toString() { - return 'AppState{isLoading: $isLoading}'; + return 'AppState{isLoading: $isLoading, url: ${auth.url}, token ${auth.token}}'; } } \ No newline at end of file diff --git a/lib/redux/auth/auth_actions.dart b/lib/redux/auth/auth_actions.dart index 5f8c54e7b..270e9657c 100644 --- a/lib/redux/auth/auth_actions.dart +++ b/lib/redux/auth/auth_actions.dart @@ -3,7 +3,16 @@ import 'package:redux/redux.dart'; import 'package:invoiceninja/redux/app/app_state.dart'; import 'package:invoiceninja/data/models/models.dart'; -class UserLoginRequest {} +class UserLoginRequest { + final String email; + final String password; + final String url; + final String secret; + final String token; + + //UserLoginRequest(this.email, this.password, this.url, this.secret); + UserLoginRequest(this.url, this.token); +} class UserLoginSuccess { final User user; @@ -19,21 +28,21 @@ class UserLoginFailure { class UserLogout {} -final Function login = (BuildContext context, String username, String password) { +/* +final Function login = (BuildContext context, String url, String token) { return (Store store) { - store.dispatch(new UserLoginRequest()); - if (username == 'asd' && password == 'asd') { - store.dispatch(new UserLoginSuccess(new User('placeholder_token', 'placeholder_id'))); - Navigator.of(context).pushNamedAndRemoveUntil('/main', (_) => false); - } else { - store.dispatch(new UserLoginFailure('Username or password were incorrect.')); - } + store.dispatch(new UserLoginRequest(url, token)); + store.dispatch(new UserLoginSuccess(new User(token, 1))); + Navigator.of(context).pushNamed('/dashboard'); }; }; +*/ +/* final Function logout = (BuildContext context) { return (Store store) { store.dispatch(new UserLogout()); Navigator.of(context).pushNamedAndRemoveUntil('/login', (_) => false); }; -}; \ No newline at end of file +}; +*/ \ No newline at end of file diff --git a/lib/redux/auth/auth_reducer.dart b/lib/redux/auth/auth_reducer.dart index b8c32ef19..eaa57cdbc 100644 --- a/lib/redux/auth/auth_reducer.dart +++ b/lib/redux/auth/auth_reducer.dart @@ -4,21 +4,24 @@ import 'package:invoiceninja/redux/auth/auth_actions.dart'; import 'package:invoiceninja/redux/auth/auth_state.dart'; Reducer authReducer = combineReducers([ - new TypedReducer(userLoginRequestReducer), - new TypedReducer(userLoginSuccessReducer), - new TypedReducer(userLoginFailureReducer), - new TypedReducer(userLogoutReducer), + TypedReducer(userLoginRequestReducer), + TypedReducer(userLoginSuccessReducer), + TypedReducer(userLoginFailureReducer), + TypedReducer(userLogoutReducer), ]); AuthState userLoginRequestReducer(AuthState auth, UserLoginRequest action) { - return new AuthState().copyWith( + return AuthState().copyWith( + url: action.url, + token: action.token, + secret: action.secret, isAuthenticated: false, isAuthenticating: true, ); } AuthState userLoginSuccessReducer(AuthState auth, UserLoginSuccess action) { - return new AuthState().copyWith( + return AuthState().copyWith( isAuthenticated: true, isAuthenticating: false, user: action.user @@ -26,7 +29,7 @@ AuthState userLoginSuccessReducer(AuthState auth, UserLoginSuccess action) { } AuthState userLoginFailureReducer(AuthState auth, UserLoginFailure action) { - return new AuthState().copyWith( + return AuthState().copyWith( isAuthenticated: false, isAuthenticating: false, error: action.error @@ -34,5 +37,5 @@ AuthState userLoginFailureReducer(AuthState auth, UserLoginFailure action) { } AuthState userLogoutReducer(AuthState auth, UserLogout action) { - return new AuthState(); + return AuthState(); } \ No newline at end of file diff --git a/lib/redux/auth/auth_state.dart b/lib/redux/auth/auth_state.dart index 611d1ad98..e04fe9f72 100644 --- a/lib/redux/auth/auth_state.dart +++ b/lib/redux/auth/auth_state.dart @@ -6,6 +6,9 @@ import 'package:invoiceninja/data/models/models.dart'; class AuthState { // properties + final String url; + final String secret; + final String token; final bool isAuthenticated; final bool isAuthenticating; final User user; @@ -13,6 +16,9 @@ class AuthState { // constructor with default AuthState({ + this.url, + this.secret, + this.token, this.isAuthenticated = false, this.isAuthenticating = false, this.user, @@ -21,12 +27,18 @@ class AuthState { // allows to modify AuthState parameters while cloning previous ones AuthState copyWith({ + String url, + String secret, + String token, bool isAuthenticated, bool isAuthenticating, String error, User user }) { return new AuthState( + url: url ?? this.url, + token: token ?? this.token, + secret: secret ?? this.secret, isAuthenticated: isAuthenticated ?? this.isAuthenticated, isAuthenticating: isAuthenticating ?? this.isAuthenticating, error: error ?? this.error, @@ -35,6 +47,9 @@ class AuthState { } factory AuthState.fromJSON(Map json) => new AuthState( + url: json['url'], + token: json['token'], + secret: json['secret'], isAuthenticated: json['isAuthenticated'], isAuthenticating: json['isAuthenticating'], error: json['error'], @@ -42,6 +57,9 @@ class AuthState { ); Map toJSON() => { + 'url': this.url, + 'token': this.token, + 'secret': this.secret, 'isAuthenticated': this.isAuthenticated, 'isAuthenticating': this.isAuthenticating, 'user': this.user == null ? null : this.user.toJSON(), @@ -51,6 +69,7 @@ class AuthState { @override String toString() { return '''{ + url: $url, isAuthenticated: $isAuthenticated, isAuthenticating: $isAuthenticating, user: $user, diff --git a/lib/redux/product/product_actions.dart b/lib/redux/product/product_actions.dart index 26420ca10..cda08a8e5 100644 --- a/lib/redux/product/product_actions.dart +++ b/lib/redux/product/product_actions.dart @@ -2,7 +2,16 @@ import 'package:invoiceninja/data/models/models.dart'; class LoadProductsAction {} -class ProductsNotLoadedAction {} +class ProductsNotLoadedAction { + final dynamic error; + + ProductsNotLoadedAction(this.error); + + @override + String toString() { + return 'ProductsNotLoadedAction{products: $error}'; + } +} class ProductsLoadedAction { final List products; diff --git a/lib/redux/product/product_middleware.dart b/lib/redux/product/product_middleware.dart index b1d918f86..74d6cc6a7 100644 --- a/lib/redux/product/product_middleware.dart +++ b/lib/redux/product/product_middleware.dart @@ -6,9 +6,10 @@ import 'package:invoiceninja/data/repositories/repositories.dart'; import 'package:invoiceninja/data/repositories/product_repository.dart'; import 'package:invoiceninja/data/file_storage.dart'; import 'package:invoiceninja/redux/product/product_selectors.dart'; +import 'package:invoiceninja/data/models/entities.dart'; List> createStoreProductsMiddleware([ - ProductsRepository repository = const ProductsRepositoryFlutter( + BaseRepository repository = const ProductsRepositoryFlutter( fileStorage: const FileStorage( '__invoiceninja__', getApplicationDocumentsDirectory, @@ -24,7 +25,7 @@ List> createStoreProductsMiddleware([ ]; } -Middleware _createSaveProducts(ProductsRepository repository) { +Middleware _createSaveProducts(BaseRepository repository) { return (Store store, action, NextDispatcher next) { next(action); @@ -36,14 +37,12 @@ Middleware _createSaveProducts(ProductsRepository repository) { }; } -Middleware _createLoadProducts(ProductsRepository repository) { +Middleware _createLoadProducts(BaseRepository repository) { return (Store store, action, NextDispatcher next) { print('Product Middleware: createLoadProducts...'); - //repository.loadProducts(); - - repository.loadProducts().then( + repository.loadData(store.state.auth).then( (products) { store.dispatch( ProductsLoadedAction( @@ -52,7 +51,7 @@ Middleware _createLoadProducts(ProductsRepository repository) { ), ); }, - ).catchError((_) => store.dispatch(ProductsNotLoadedAction())); + ).catchError((error) => store.dispatch(ProductsNotLoadedAction(error))); next(action); }; diff --git a/lib/routes.dart b/lib/routes.dart index 4e54be2a8..463676c27 100644 --- a/lib/routes.dart +++ b/lib/routes.dart @@ -1,6 +1,6 @@ class NinjaRoutes { - //static final login = "/"; - static final dashboard = "/"; + static final login = "/"; + static final dashboard = "/dashboard"; static final clientList = "/clientList"; static final productList = "/productList"; } \ No newline at end of file diff --git a/lib/ui/auth/login_screen.dart b/lib/ui/auth/login_screen.dart index 9f67996e4..ca23104cd 100644 --- a/lib/ui/auth/login_screen.dart +++ b/lib/ui/auth/login_screen.dart @@ -3,6 +3,7 @@ import 'package:redux/redux.dart'; import 'package:flutter_redux/flutter_redux.dart'; import 'package:invoiceninja/redux/app/app_state.dart'; import 'package:invoiceninja/redux/auth/auth_actions.dart'; +import 'package:invoiceninja/data/models/models.dart'; class LoginScreen extends StatefulWidget { @override @@ -14,6 +15,9 @@ class _LoginScreenState extends State { String _username; String _password; + String _url; + String _token; + String _secret; void _submit() { final form = _formKey.currentState; @@ -27,8 +31,11 @@ class _LoginScreenState extends State { Widget build(BuildContext context) { return StoreConnector( converter: (Store store) { - return (BuildContext context, String username, String password) => - store.dispatch(login(context, username, password)); + return (BuildContext context, String url, String token) { + store.dispatch(UserLoginRequest(url, token)); + //store.dispatch(UserLoginSuccess(User(token, 1))); + Navigator.of(context).pushNamed('/dashboard'); + }; }, builder: (BuildContext context, loginAction) { return Scaffold( body: Form( @@ -37,26 +44,41 @@ class _LoginScreenState extends State { shrinkWrap: true, padding: EdgeInsets.only(left: 24.0, right: 24.0, top: 40.0), children: [ + TextFormField( + decoration: InputDecoration(labelText: 'URL'), + validator: (val) => + val.isEmpty ? 'Please enter your URL.' : null, + onSaved: (val) => _url = val, + ), + TextFormField( + decoration: InputDecoration(labelText: 'Token'), + validator: (val) => + val.isEmpty ? 'Please enter your token.' : null, + onSaved: (val) => _token = val, + ), + Padding( + padding: EdgeInsets.only(top: 40.0, bottom: 20.0), + child: Text( + 'Note: this will be changed to email/password in the future'), + ), + /* TextFormField( decoration: InputDecoration(labelText: 'Email'), keyboardType: TextInputType.emailAddress, //contentPadding: EdgeInsets.fromLTRB(20.0, 10.0, 20.0, 10.0), //autofocus: false, - /* validator: (val) => val.isEmpty ? 'Please enter your email.' : null, - */ onSaved: (val) => _username = val, ), TextFormField( decoration: InputDecoration(labelText: 'Password'), - /* validator: (val) => val.isEmpty ? 'Please enter your password.' : null, - */ onSaved: (val) => _password = val, obscureText: true, ), + */ Padding( padding: EdgeInsets.only(top: 20.0), child: Material( @@ -68,7 +90,7 @@ class _LoginScreenState extends State { height: 42.0, onPressed: () { _submit(); - loginAction(context, _username, _password); + loginAction(context, _url, _token); //Navigator.of(context).pushNamed(HomeScreen.tag); }, color: Colors.lightBlueAccent, diff --git a/lib/ui/product/product_list.dart b/lib/ui/product/product_list.dart index a8f29cd16..617f7530c 100644 --- a/lib/ui/product/product_list.dart +++ b/lib/ui/product/product_list.dart @@ -27,8 +27,6 @@ class ProductList extends StatelessWidget { } ListView _buildListView() { - print('_buildListView'); - print(products); return ListView.builder( key: NinjaKeys.productList, itemCount: products.length, diff --git a/pubspec.lock b/pubspec.lock index f35b61bf7..7d6851092 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -347,6 +347,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "3.0.0" + redux_logging: + dependency: "direct main" + description: + name: redux_logging + url: "https://pub.dartlang.org" + source: hosted + version: "0.3.0" shelf: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index d5ed021d3..413f27f9f 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -9,7 +9,7 @@ dependencies: cupertino_icons: ^0.1.0 json_annotation: ^0.2.3 path_provider: "^0.4.0" - + redux_logging: "^0.3.0" dev_dependencies: flutter_test: