Working on login

This commit is contained in:
unknown 2018-05-21 11:14:36 -07:00
parent 9487c89307
commit b444ce2e2a
20 changed files with 155 additions and 85 deletions

View File

@ -16,7 +16,7 @@ class FileStorage {
);
/// LoadProducts
Future<List<ProductEntity>> loadProducts() async {
Future<List<dynamic>> loadData() async {
final file = await _getLocalFile();
final contents = await file.readAsString();
@ -29,7 +29,7 @@ class FileStorage {
*/
}
Future<File> saveProducts(List<ProductEntity> products) async {
Future<File> saveData(List<dynamic> products) async {
final file = await _getLocalFile();
/*

View File

@ -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<ProductEntity> products;
final List<dynamic> data;
ProductResponse(
BaseResponse(
//this.message,
this.products,
this.data,
);
factory ProductResponse.fromJson(Map<String, dynamic> json) => _$ProductResponseFromJson(json);
factory BaseResponse.fromJson(Map<String, dynamic> json) => _$BaseResponseFromJson(json);
}
@JsonSerializable()

View File

@ -6,16 +6,12 @@ part of 'entities.dart';
// Generator: JsonSerializableGenerator
// **************************************************************************
ProductResponse _$ProductResponseFromJson(Map<String, dynamic> json) =>
new ProductResponse((json['data'] as List)
?.map((e) => e == null
? null
: new ProductEntity.fromJson(e as Map<String, dynamic>))
?.toList());
BaseResponse _$BaseResponseFromJson(Map<String, dynamic> json) =>
new BaseResponse(json['data'] as List);
abstract class _$ProductResponseSerializerMixin {
List<ProductEntity> get products;
Map<String, dynamic> toJson() => <String, dynamic>{'data': products};
abstract class _$BaseResponseSerializerMixin {
List<dynamic> get data;
Map<String, dynamic> toJson() => <String, dynamic>{'data': data};
}
ProductEntity _$ProductEntityFromJson(Map<String, dynamic> json) =>

View File

@ -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);

View File

@ -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<List<ProductEntity>> loadProducts() async {
Future<List<dynamic>> 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<ProductEntity> products) {
Future saveData(List<dynamic> products) {
return Future.wait<dynamic>([
fileStorage.saveProducts(products),
fileStorage.saveData(products),
webClient.postProducts(products),
]);
}

View File

@ -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<List<ProductEntity>> loadProducts();
Future<List<dynamic>> loadData(AuthState auth);
// Persists products to local disk and the web
Future saveProducts(List<ProductEntity> products);
Future saveData(List<dynamic> data);
}

View File

@ -13,20 +13,16 @@ class WebClient {
const WebClient();
/// Mock that "fetches" some Products from a "web service" after a short delay
Future<List<ProductEntity>> fetchProducts() async {
print('Web Client: fetchProducts...');
var url = "";
Future<List<dynamic>> 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();
}

View File

@ -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<AppState>(
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<AppState>(
builder: (context, store) {
@ -48,7 +52,6 @@ class InvoiceNinjaApp extends StatelessWidget {
},
);
},
*/
NinjaRoutes.dashboard: (context) {
return StoreBuilder<AppState>(
builder: (context, store) {

View File

@ -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),
);
}

View File

@ -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}}';
}
}

View File

@ -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<AppState> 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<AppState> store) {
store.dispatch(new UserLogout());
Navigator.of(context).pushNamedAndRemoveUntil('/login', (_) => false);
};
};
};
*/

View File

@ -4,21 +4,24 @@ import 'package:invoiceninja/redux/auth/auth_actions.dart';
import 'package:invoiceninja/redux/auth/auth_state.dart';
Reducer<AuthState> authReducer = combineReducers([
new TypedReducer<AuthState, UserLoginRequest>(userLoginRequestReducer),
new TypedReducer<AuthState, UserLoginSuccess>(userLoginSuccessReducer),
new TypedReducer<AuthState, UserLoginFailure>(userLoginFailureReducer),
new TypedReducer<AuthState, UserLogout>(userLogoutReducer),
TypedReducer<AuthState, UserLoginRequest>(userLoginRequestReducer),
TypedReducer<AuthState, UserLoginSuccess>(userLoginSuccessReducer),
TypedReducer<AuthState, UserLoginFailure>(userLoginFailureReducer),
TypedReducer<AuthState, UserLogout>(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();
}

View File

@ -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<String, dynamic> 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<String, dynamic> toJSON() => <String, dynamic>{
'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,

View File

@ -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<ProductEntity> products;

View File

@ -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<Middleware<AppState>> createStoreProductsMiddleware([
ProductsRepository repository = const ProductsRepositoryFlutter(
BaseRepository repository = const ProductsRepositoryFlutter(
fileStorage: const FileStorage(
'__invoiceninja__',
getApplicationDocumentsDirectory,
@ -24,7 +25,7 @@ List<Middleware<AppState>> createStoreProductsMiddleware([
];
}
Middleware<AppState> _createSaveProducts(ProductsRepository repository) {
Middleware<AppState> _createSaveProducts(BaseRepository repository) {
return (Store<AppState> store, action, NextDispatcher next) {
next(action);
@ -36,14 +37,12 @@ Middleware<AppState> _createSaveProducts(ProductsRepository repository) {
};
}
Middleware<AppState> _createLoadProducts(ProductsRepository repository) {
Middleware<AppState> _createLoadProducts(BaseRepository repository) {
return (Store<AppState> 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<AppState> _createLoadProducts(ProductsRepository repository) {
),
);
},
).catchError((_) => store.dispatch(ProductsNotLoadedAction()));
).catchError((error) => store.dispatch(ProductsNotLoadedAction(error)));
next(action);
};

View File

@ -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";
}

View File

@ -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<LoginScreen> {
String _username;
String _password;
String _url;
String _token;
String _secret;
void _submit() {
final form = _formKey.currentState;
@ -27,8 +31,11 @@ class _LoginScreenState extends State<LoginScreen> {
Widget build(BuildContext context) {
return StoreConnector<AppState, dynamic>(
converter: (Store<AppState> 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<LoginScreen> {
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<LoginScreen> {
height: 42.0,
onPressed: () {
_submit();
loginAction(context, _username, _password);
loginAction(context, _url, _token);
//Navigator.of(context).pushNamed(HomeScreen.tag);
},
color: Colors.lightBlueAccent,

View File

@ -27,8 +27,6 @@ class ProductList extends StatelessWidget {
}
ListView _buildListView() {
print('_buildListView');
print(products);
return ListView.builder(
key: NinjaKeys.productList,
itemCount: products.length,

View File

@ -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:

View File

@ -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: