diff --git a/lib/constants.dart b/lib/constants.dart index 9fc1cb64f..678737493 100644 --- a/lib/constants.dart +++ b/lib/constants.dart @@ -3,7 +3,7 @@ import 'package:flutter/material.dart'; // This version must be updated in tandem with the pubspec version. const String kAppVersion = '0.1.47'; const String kSiteUrl = 'https://invoiceninja.com'; -const String kAppUrl = 'https://app.invoiceninja.com'; +const String kAppUrl = 'https://staging.invoiceninja.com'; const String kPrivacyPolicyURL = 'https://www.invoiceninja.com/privacy-policy'; const String kTermsOfServiceURL = 'https://www.invoiceninja.com/terms'; diff --git a/lib/redux/auth/auth_actions.dart b/lib/redux/auth/auth_actions.dart index e84d64c69..3022858f3 100644 --- a/lib/redux/auth/auth_actions.dart +++ b/lib/redux/auth/auth_actions.dart @@ -77,13 +77,18 @@ class UserLogout implements PersistData, PersistUI {} class UserSignUpRequest implements StartLoading { UserSignUpRequest({ + this.completer, + this.firstName, + this.lastName, this.email, this.password, this.platform, }); final Completer completer; + final String firstName; + final String lastName; final String email; final String password; final String platform; diff --git a/lib/ui/auth/login_view.dart b/lib/ui/auth/login_view.dart index 14743d70e..056a91f9f 100644 --- a/lib/ui/auth/login_view.dart +++ b/lib/ui/auth/login_view.dart @@ -3,6 +3,7 @@ import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:invoiceninja_flutter/constants.dart'; import 'package:invoiceninja_flutter/redux/ui/ui_state.dart'; import 'package:invoiceninja_flutter/ui/app/buttons/elevated_button.dart'; +import 'package:invoiceninja_flutter/ui/app/forms/decorated_form_field.dart'; import 'package:invoiceninja_flutter/ui/app/link_text.dart'; import 'package:invoiceninja_flutter/ui/app/progress_button.dart'; import 'package:invoiceninja_flutter/ui/auth/login_vm.dart'; @@ -27,6 +28,8 @@ class LoginView extends StatefulWidget { class _LoginState extends State { static final GlobalKey _formKey = GlobalKey(); + final _firstNameController = TextEditingController(); + final _lastNameController = TextEditingController(); final _emailController = TextEditingController(); final _passwordController = TextEditingController(); final _urlController = TextEditingController(); @@ -60,6 +63,8 @@ class _LoginState extends State { @override void dispose() { + _firstNameController.dispose(); + _lastNameController.dispose(); _emailController.dispose(); _passwordController.dispose(); _urlController.dispose(); @@ -68,7 +73,7 @@ class _LoginState extends State { super.dispose(); } - void _submitLoginForm() { + void _submitSignUpForm() { final bool isValid = _formKey.currentState.validate(); final localization = AppLocalization.of(context); @@ -103,13 +108,34 @@ class _LoginState extends State { return; } - widget.viewModel.onLoginPressed(context, - email: _emailController.text, - password: _passwordController.text, - url: _isSelfHosted ? _urlController.text : '', - secret: _isSelfHosted ? _secretController.text : '', - oneTimePassword: _oneTimePasswordController.text, - createAccount: _createAccount); + widget.viewModel.onSignUpPressed( + context, + email: _emailController.text, + password: _passwordController.text, + firstName: _firstNameController.text, + lastName: _lastNameController.text, + ); + } + + void _submitLoginForm() { + final bool isValid = _formKey.currentState.validate(); + + setState(() { + _autoValidate = !isValid; + }); + + if (!isValid) { + return; + } + + widget.viewModel.onLoginPressed( + context, + email: _emailController.text, + password: _passwordController.text, + url: _isSelfHosted ? _urlController.text : '', + secret: _isSelfHosted ? _secretController.text : '', + oneTimePassword: _oneTimePasswordController.text, + ); } @override @@ -161,134 +187,136 @@ class _LoginState extends State { child: FormCard( isResponsive: calculateLayout(context) != AppLayout.mobile, children: [ - isOneTimePassword - ? TextFormField( - controller: _oneTimePasswordController, - key: ValueKey(localization.oneTimePassword), + if (isOneTimePassword) + TextFormField( + controller: _oneTimePasswordController, + key: ValueKey(localization.oneTimePassword), + autocorrect: false, + decoration: InputDecoration( + labelText: localization.oneTimePassword), + ) + else + Column( + children: [ + if (_createAccount) + DecoratedFormField( + label: localization.firstName, + controller: _firstNameController, + ), + if (_createAccount) + DecoratedFormField( + label: localization.lastName, + controller: _lastNameController, + ), + TextFormField( + controller: _emailController, + key: ValueKey(localization.email), autocorrect: false, - decoration: InputDecoration( - labelText: localization.oneTimePassword), - ) - : Column( - children: [ - TextFormField( - controller: _emailController, - key: ValueKey(localization.email), - autocorrect: false, - textInputAction: TextInputAction.next, - decoration: InputDecoration( - labelText: localization.email), - keyboardType: TextInputType.emailAddress, - autovalidate: _autoValidate, - validator: (val) => - val.isEmpty || val.trim().isEmpty - ? localization.pleaseEnterYourEmail - : null, - onFieldSubmitted: (String value) => - FocusScope.of(context) - .requestFocus(_focusNode1), - ), - TextFormField( - controller: _passwordController, - key: ValueKey(localization.password), - autocorrect: false, - autovalidate: _autoValidate, - decoration: InputDecoration( - labelText: localization.password), - validator: (val) => - val.isEmpty || val.trim().isEmpty - ? localization.pleaseEnterYourPassword - : null, - obscureText: true, - focusNode: _focusNode1, - onFieldSubmitted: (value) => - _createAccount ? null : _submitLoginForm(), - ), - if (_isSelfHosted) - TextFormField( - controller: _urlController, - key: ValueKey(localization.url), - autocorrect: false, - autovalidate: _autoValidate, - decoration: InputDecoration( - labelText: localization.url), - validator: (val) => - val.isEmpty || val.trim().isEmpty - ? localization.pleaseEnterYourUrl - : null, - keyboardType: TextInputType.url, - ), - if (_isSelfHosted) - TextFormField( - controller: _secretController, - key: ValueKey(localization.secret), - autocorrect: false, - decoration: InputDecoration( - labelText: localization.secret), - obscureText: true, - ), - if (_createAccount) - Padding( - padding: EdgeInsets.only(top: 22), - child: Column( - children: [ - CheckboxListTile( - onChanged: (value) => - setState(() => _termsChecked = value), - controlAffinity: - ListTileControlAffinity.leading, - activeColor: - Theme.of(context).accentColor, - value: _termsChecked, - title: RichText( - text: TextSpan( - children: [ - TextSpan( - style: aboutTextStyle, - text: localization.iAgreeToThe + - ' ', - ), - LinkTextSpan( - style: linkStyle, - url: kTermsOfServiceURL, - text: localization - .termsOfServiceLink, - ), - ], - ), - ), - ), - CheckboxListTile( - onChanged: (value) => setState( - () => _privacyChecked = value), - controlAffinity: - ListTileControlAffinity.leading, - activeColor: - Theme.of(context).accentColor, - value: _privacyChecked, - title: RichText( - text: TextSpan( - children: [ - TextSpan( - style: aboutTextStyle, - text: localization.iAgreeToThe + - ' ', - ), - LinkTextSpan( - style: linkStyle, - url: kTermsOfServiceURL, - text: localization - .privacyPolicyLink, - ), - ], - ), - ), - ), - ], - ), - ), - ], + textInputAction: TextInputAction.next, + decoration: + InputDecoration(labelText: localization.email), + keyboardType: TextInputType.emailAddress, + autovalidate: _autoValidate, + validator: (val) => val.isEmpty || val.trim().isEmpty + ? localization.pleaseEnterYourEmail + : null, + onFieldSubmitted: (String value) => + FocusScope.of(context).requestFocus(_focusNode1), ), + TextFormField( + controller: _passwordController, + key: ValueKey(localization.password), + autocorrect: false, + autovalidate: _autoValidate, + decoration: + InputDecoration(labelText: localization.password), + validator: (val) => val.isEmpty || val.trim().isEmpty + ? localization.pleaseEnterYourPassword + : null, + obscureText: true, + focusNode: _focusNode1, + onFieldSubmitted: (value) => + _createAccount ? null : _submitLoginForm(), + ), + if (_isSelfHosted) + TextFormField( + controller: _urlController, + key: ValueKey(localization.url), + autocorrect: false, + autovalidate: _autoValidate, + decoration: + InputDecoration(labelText: localization.url), + validator: (val) => + val.isEmpty || val.trim().isEmpty + ? localization.pleaseEnterYourUrl + : null, + keyboardType: TextInputType.url, + ), + if (_isSelfHosted) + TextFormField( + controller: _secretController, + key: ValueKey(localization.secret), + autocorrect: false, + decoration: + InputDecoration(labelText: localization.secret), + obscureText: true, + ), + if (_createAccount) + Padding( + padding: EdgeInsets.only(top: 22), + child: Column( + children: [ + CheckboxListTile( + onChanged: (value) => + setState(() => _termsChecked = value), + controlAffinity: + ListTileControlAffinity.leading, + activeColor: Theme.of(context).accentColor, + value: _termsChecked, + title: RichText( + text: TextSpan( + children: [ + TextSpan( + style: aboutTextStyle, + text: localization.iAgreeToThe + ' ', + ), + LinkTextSpan( + style: linkStyle, + url: kTermsOfServiceURL, + text: localization.termsOfServiceLink, + ), + ], + ), + ), + ), + CheckboxListTile( + onChanged: (value) => + setState(() => _privacyChecked = value), + controlAffinity: + ListTileControlAffinity.leading, + activeColor: Theme.of(context).accentColor, + value: _privacyChecked, + title: RichText( + text: TextSpan( + children: [ + TextSpan( + style: aboutTextStyle, + text: localization.iAgreeToThe + ' ', + ), + LinkTextSpan( + style: linkStyle, + url: kTermsOfServiceURL, + text: localization.privacyPolicyLink, + ), + ], + ), + ), + ), + ], + ), + ), + ], + ), if (viewModel.authState.error != null && !error.contains(OTP_ERROR)) Container( @@ -309,7 +337,7 @@ class _LoginState extends State { ? ProgressButton( isLoading: viewModel.isLoading, label: localization.signUp.toUpperCase(), - onPressed: () => _submitLoginForm(), + onPressed: () => _submitSignUpForm(), ) : Row( children: [ diff --git a/lib/ui/auth/login_vm.dart b/lib/ui/auth/login_vm.dart index d6e948485..a34153f50 100644 --- a/lib/ui/auth/login_vm.dart +++ b/lib/ui/auth/login_vm.dart @@ -39,6 +39,7 @@ class LoginVM { @required this.isLoading, @required this.authState, @required this.onLoginPressed, + @required this.onSignUpPressed, @required this.onCancel2FAPressed, @required this.onGoogleLoginPressed, }); @@ -47,13 +48,22 @@ class LoginVM { AuthState authState; final Function() onCancel2FAPressed; - final Function(BuildContext, - {@required String email, - @required String password, - @required String url, - @required String secret, - @required String oneTimePassword, - @required bool createAccount}) onLoginPressed; + final Function( + BuildContext, { + @required String email, + @required String password, + @required String url, + @required String secret, + @required String oneTimePassword, + }) onLoginPressed; + + final Function( + BuildContext, { + @required String firstName, + @required String lastName, + @required String email, + @required String password, + }) onSignUpPressed; final Function(BuildContext, String, String) onGoogleLoginPressed; @@ -103,13 +113,36 @@ class LoginVM { print(error); } }, - onLoginPressed: (BuildContext context, - {@required String email, - @required String password, - @required String url, - @required String secret, - @required String oneTimePassword, - @required bool createAccount}) async { + onSignUpPressed: ( + BuildContext context, { + @required String firstName, + @required String lastName, + @required String email, + @required String password, + }) async { + if (store.state.isLoading) { + return; + } + + final Completer completer = Completer(); + store.dispatch(UserSignUpRequest( + completer: completer, + firstName: firstName.trim(), + lastName: lastName.trim(), + email: email.trim(), + password: password.trim(), + platform: getPlatform(context), + )); + completer.future.then((_) => _handleLogin(context)); + }, + onLoginPressed: ( + BuildContext context, { + @required String email, + @required String password, + @required String url, + @required String secret, + @required String oneTimePassword, + }) async { if (store.state.isLoading) { return; } @@ -119,24 +152,15 @@ class LoginVM { } final Completer completer = Completer(); - if (createAccount) { - store.dispatch(UserSignUpRequest( - completer: completer, - email: email.trim(), - password: password.trim(), - platform: getPlatform(context), - )); - } else { - store.dispatch(UserLoginRequest( - completer: completer, - email: email.trim(), - password: password.trim(), - url: url.trim(), - secret: secret.trim(), - platform: getPlatform(context), - oneTimePassword: oneTimePassword.trim(), - )); - } + store.dispatch(UserLoginRequest( + completer: completer, + email: email.trim(), + password: password.trim(), + url: url.trim(), + secret: secret.trim(), + platform: getPlatform(context), + oneTimePassword: oneTimePassword.trim(), + )); completer.future.then((_) => _handleLogin(context)); }); }