import 'package:flutter/material.dart'; import 'package:invoiceninja_flutter/ui/app/buttons/elevated_button.dart'; import 'package:invoiceninja_flutter/ui/app/dialogs/error_dialog.dart'; import 'package:invoiceninja_flutter/ui/app/progress_button.dart'; import 'package:invoiceninja_flutter/ui/auth/login_vm.dart'; import 'package:invoiceninja_flutter/utils/formatting.dart'; import 'package:invoiceninja_flutter/utils/localization.dart'; import 'package:invoiceninja_flutter/ui/app/form_card.dart'; import 'package:invoiceninja_flutter/utils/keys.dart'; class LoginView extends StatefulWidget { final LoginVM viewModel; const LoginView({ Key key, @required this.viewModel, }) : super(key: key); @override _LoginState createState() => _LoginState(); } class _LoginState extends State { static final GlobalKey _formKey = GlobalKey(); final _emailController = TextEditingController(); final _passwordController = TextEditingController(); final _urlController = TextEditingController(); final _secretController = TextEditingController(); final _oneTimePasswordController = TextEditingController(); static const String OTP_ERROR = 'OTP_REQUIRED'; static final ValueKey _emailKey = Key(LoginKeys.emailKeyString); static final ValueKey _passwordKey = Key(LoginKeys.passwordKeyString); static final ValueKey _urlKey = Key(LoginKeys.urlKeyString); static final ValueKey _secretKey = Key(LoginKeys.secretKeyString); static final ValueKey _oneTimePasswordKey = Key(LoginKeys.oneTimePasswordKeyString); final FocusNode _focusNode1 = new FocusNode(); bool _isSelfHosted = false; bool _autoValidate = false; @override void didChangeDependencies() { final state = widget.viewModel.authState; _emailController.text = state.email; _passwordController.text = state.password; _urlController.text = formatApiUrlReadable(state.url); _secretController.text = state.secret; super.didChangeDependencies(); } @override void dispose() { _emailController.dispose(); _passwordController.dispose(); _urlController.dispose(); _secretController.dispose(); super.dispose(); } void _submitForm() { final bool isValid = _formKey.currentState.validate(); setState(() { _autoValidate = !isValid; }); if (!isValid) { return; } widget.viewModel.onLoginPressed(context, email: _emailController.text, password: _passwordController.text, url: _urlController.text, secret: _secretController.text, oneTimePassword: _oneTimePasswordController.text); } @override Widget build(BuildContext context) { final localization = AppLocalization.of(context); final viewModel = widget.viewModel; final error = viewModel.authState.error; final isOneTimePassword = error == OTP_ERROR || _oneTimePasswordController.text.isNotEmpty; if (!viewModel.authState.isInitialized) { return Container(); } return Stack( children: [ SizedBox( height: 250.0, child: ClipPath( clipper: ArcClipper(), child: Container( decoration: BoxDecoration( gradient: LinearGradient( colors: [ //Colors.grey.shade800, //Colors.black87, Theme.of(context).buttonColor, Theme.of(context).buttonColor.withOpacity(.7), ], )), ), ), ), ListView( shrinkWrap: true, children: [ Padding( padding: EdgeInsets.only(top: 20.0, bottom: 20.0), child: Image.asset('assets/images/logo.png', width: 100.0, height: 100.0), ), Form( key: _formKey, child: FormCard( children: [ isOneTimePassword ? TextFormField( controller: _oneTimePasswordController, key: _oneTimePasswordKey, autocorrect: false, decoration: InputDecoration( labelText: localization.oneTimePassword), ) : Column( children: [ TextFormField( controller: _emailController, key: _emailKey, 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: _passwordKey, 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) => _submitForm(), ), _isSelfHosted ? TextFormField( controller: _urlController, key: _urlKey, autocorrect: false, autovalidate: _autoValidate, decoration: InputDecoration( labelText: localization.url), validator: (val) => val.isEmpty || val.trim().isEmpty ? localization.pleaseEnterYourUrl : null, keyboardType: TextInputType.url, ) : Container(), _isSelfHosted ? TextFormField( controller: _secretController, key: _secretKey, autocorrect: false, decoration: InputDecoration( labelText: localization.secret), obscureText: true, ) : Container(), ], ), viewModel.authState.error == null || error == OTP_ERROR ? Container() : Container( padding: EdgeInsets.only(top: 26.0), child: Center( child: Text( viewModel.authState.error, style: TextStyle( color: Colors.red, fontWeight: FontWeight.bold, ), ), ), ), SizedBox(height: 24.0), ProgressButton( padding: EdgeInsets.only(top: 4.0, bottom: 8.0), isLoading: viewModel.isLoading, label: localization.login.toUpperCase(), onPressed: () => _submitForm(), ), Row( mainAxisAlignment: MainAxisAlignment.spaceAround, children: [ _isSelfHosted ? FlatButton( onPressed: () => setState(() => _isSelfHosted = false), child: Text(localization.hostedLogin)) : FlatButton( onPressed: () => setState(() => _isSelfHosted = true), child: Text(localization.selfhostLogin)), FlatButton( onPressed: () => setState(() => _isSelfHosted = true), child: Text(localization.googleLogin)), ], ), isOneTimePassword && !viewModel.isLoading ? ElevatedButton( label: localization.cancel.toUpperCase(), color: Colors.grey, onPressed: () { setState(() { _oneTimePasswordController.text = ''; }); viewModel.onCancel2FAPressed(); }, ) : Container(), /* Padding( padding: const EdgeInsets.symmetric(horizontal: 16.0), child: ElevatedButton( label: 'Google ${localization.login}'.toUpperCase(), onPressed: () => viewModel.onGoogleLoginPressed( context, _urlController.text, _secretController.text), ), ), */ ], ), ), ], ), ], ); } } // https://github.com/iampawan/Flutter-UI-Kit class ArcClipper extends CustomClipper { @override Path getClip(Size size) { final path = Path(); path.lineTo(0.0, size.height - 30); final firstControlPoint = Offset(size.width / 4, size.height); final firstPoint = Offset(size.width / 2, size.height); path.quadraticBezierTo(firstControlPoint.dx, firstControlPoint.dy, firstPoint.dx, firstPoint.dy); final secondControlPoint = Offset(size.width - (size.width / 4), size.height); final secondPoint = Offset(size.width, size.height - 30); path.quadraticBezierTo(secondControlPoint.dx, secondControlPoint.dy, secondPoint.dx, secondPoint.dy); path.lineTo(size.width, 0.0); path.close(); return path; } @override bool shouldReclip(CustomClipper oldClipper) => false; }