import 'dart:async'; import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:flutter_animate/flutter_animate.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_form_builder/flutter_form_builder.dart'; import 'package:paperless_api/paperless_api.dart'; import 'package:paperless_mobile/constants.dart'; import 'package:paperless_mobile/core/exception/server_message_exception.dart'; import 'package:paperless_mobile/core/model/info_message_exception.dart'; import 'package:paperless_mobile/core/service/connectivity_status_service.dart'; import 'package:paperless_mobile/core/extensions/flutter_extensions.dart'; import 'package:paperless_mobile/features/login/model/client_certificate.dart'; import 'package:paperless_mobile/features/login/model/client_certificate_form_model.dart'; import 'package:paperless_mobile/features/login/model/login_form_credentials.dart'; import 'package:paperless_mobile/features/login/model/reachability_status.dart'; import 'package:paperless_mobile/features/login/view/widgets/form_fields/client_certificate_form_field.dart'; import 'package:paperless_mobile/features/login/view/widgets/form_fields/server_address_form_field.dart'; import 'package:paperless_mobile/features/login/view/widgets/form_fields/user_credentials_form_field.dart'; import 'package:paperless_mobile/generated/assets.gen.dart'; import 'package:paperless_mobile/generated/l10n/app_localizations.dart'; import 'package:paperless_mobile/helpers/message_helpers.dart'; import 'package:paperless_mobile/routing/routes/app_logs_route.dart'; class AddAccountPage extends StatefulWidget { final FutureOr Function( BuildContext context, String username, String password, String serverUrl, ClientCertificate? clientCertificate, ) onSubmit; final String? initialServerUrl; final String? initialUsername; final String? initialPassword; final ClientCertificate? initialClientCertificate; final String submitText; final String titleText; final bool showLocalAccounts; final Widget? bottomLeftButton; const AddAccountPage({ Key? key, required this.onSubmit, required this.submitText, required this.titleText, this.showLocalAccounts = false, this.initialServerUrl, this.initialUsername, this.initialPassword, this.initialClientCertificate, this.bottomLeftButton, }) : super(key: key); @override State createState() => _AddAccountPageState(); } class _AddAccountPageState extends State { final _formKey = GlobalKey(); bool _isCheckingConnection = false; ReachabilityStatus _reachabilityStatus = ReachabilityStatus.unknown; bool _isFormSubmitted = false; final _pageController = PageController(); @override Widget build(BuildContext context) { return Scaffold( resizeToAvoidBottomInset: false, appBar: AppBar( title: Text(widget.titleText), ), body: FormBuilder( key: _formKey, child: AutofillGroup( child: Column( crossAxisAlignment: CrossAxisAlignment.center, children: [ Assets.logos.paperlessLogoGreenPng.image( width: 150, height: 150, ), Text( 'Paperless Mobile', style: Theme.of(context).textTheme.displaySmall, ).padded(), SizedBox(height: 24), Expanded( child: PageView( physics: NeverScrollableScrollPhysics(), controller: _pageController, allowImplicitScrolling: false, children: [ Column( mainAxisAlignment: MainAxisAlignment.start, children: [ ServerAddressFormField( onChanged: (value) { setState(() { _reachabilityStatus = ReachabilityStatus.unknown; }); }, ).paddedSymmetrically( horizontal: 12, vertical: 12, ), ClientCertificateFormField( initialBytes: widget.initialClientCertificate?.bytes, initialPassphrase: widget.initialClientCertificate?.passphrase, ).padded(), Row( mainAxisAlignment: MainAxisAlignment.end, children: [ //TODO: Move additional headers and client cert to separate page // IconButton.filledTonal( // onPressed: () { // Navigator.of(context).push( // MaterialPageRoute(builder: (context) { // return LoginSettingsPage(); // }), // ); // }, // icon: Icon(Icons.settings), // ), SizedBox(width: 8), FilledButton.icon( onPressed: () async { final status = await _updateReachability(); if (status == ReachabilityStatus.reachable) { Future.delayed(1.seconds, () { _pageController.nextPage( duration: Duration(milliseconds: 300), curve: Curves.easeInOut, ); }); } }, icon: _isCheckingConnection ? SizedBox( width: 20, height: 20, child: CircularProgressIndicator( strokeWidth: 2, color: Theme.of(context) .colorScheme .onSecondary, ), ) : _reachabilityStatus == ReachabilityStatus.reachable ? Icon(Icons.done) : Icon(Icons.arrow_forward), label: Text(S.of(context)!.continueLabel), ), ], ).paddedSymmetrically( horizontal: 16, vertical: 8, ), _buildStatusIndicator().padded(), ], ), Column( children: [ UserCredentialsFormField( formKey: _formKey, initialUsername: widget.initialUsername, initialPassword: widget.initialPassword, ), Row( mainAxisAlignment: MainAxisAlignment.end, children: [ TextButton.icon( onPressed: () { _pageController.previousPage( duration: Duration(milliseconds: 300), curve: Curves.easeInOut, ); }, icon: Icon(Icons.arrow_back), label: Text(S.of(context)!.edit), ), FilledButton( onPressed: () { _onSubmit(); }, child: Text(S.of(context)!.signIn), ), ], ).padded(), Text( S.of(context)!.loginRequiredPermissionsHint, style: Theme.of(context).textTheme.bodySmall?.apply( color: Theme.of(context) .colorScheme .onBackground .withOpacity(0.6), ), ).padded(16), ], ), ], ), ), Text.rich( TextSpan( style: Theme.of(context).textTheme.labelLarge, children: [ TextSpan(text: S.of(context)!.version(packageInfo.version)), WidgetSpan(child: SizedBox(width: 24)), TextSpan( style: TextStyle( color: Theme.of(context).colorScheme.primary), text: S.of(context)!.appLogs(''), recognizer: TapGestureRecognizer() ..onTap = () { AppLogsRoute().push(context); }, ), ], ), ).padded(), ], ), ), ), ); return Scaffold( appBar: AppBar( title: Text(widget.titleText), ), bottomNavigationBar: BottomAppBar( child: Row( mainAxisAlignment: widget.bottomLeftButton != null ? MainAxisAlignment.spaceBetween : MainAxisAlignment.end, children: [ if (widget.bottomLeftButton != null) widget.bottomLeftButton!, FilledButton( child: Text(S.of(context)!.loginPageSignInTitle), onPressed: _reachabilityStatus == ReachabilityStatus.reachable && !_isFormSubmitted ? _onSubmit : null, ), ], ), ), resizeToAvoidBottomInset: true, body: AutofillGroup( child: FormBuilder( key: _formKey, child: ListView( children: [ ServerAddressFormField( initialValue: widget.initialServerUrl, onChanged: (address) { _updateReachability(address); }, ).padded(), ClientCertificateFormField( initialBytes: widget.initialClientCertificate?.bytes, initialPassphrase: widget.initialClientCertificate?.passphrase, onChanged: (_) => _updateReachability(), ).padded(), _buildStatusIndicator(), if (_reachabilityStatus == ReachabilityStatus.reachable) ...[ UserCredentialsFormField( formKey: _formKey, initialUsername: widget.initialUsername, initialPassword: widget.initialPassword, onFieldsSubmitted: _onSubmit, ), Text( S.of(context)!.loginRequiredPermissionsHint, style: Theme.of(context).textTheme.bodySmall?.apply( color: Theme.of(context) .colorScheme .onBackground .withOpacity(0.6), ), ).padded(16), ], ], ), ), ), ); } Future _updateReachability([String? address]) async { setState(() { _isCheckingConnection = true; }); final certForm = _formKey.currentState?.getRawValue( ClientCertificateFormField.fkClientCertificate, ); final status = await context .read() .isPaperlessServerReachable( address ?? _formKey.currentState! .getRawValue(ServerAddressFormField.fkServerAddress), certForm != null ? ClientCertificate( bytes: certForm.bytes, passphrase: certForm.passphrase, ) : null, ); setState(() { _isCheckingConnection = false; _reachabilityStatus = status; }); return status; } Widget _buildStatusIndicator() { Widget _buildIconText( IconData icon, String text, [ Color? color, ]) { return ListTile( title: Text( text, style: Theme.of(context).textTheme.bodySmall?.copyWith(color: color), ), leading: Icon( icon, color: color, ), ); } Color errorColor = Theme.of(context).colorScheme.error; switch (_reachabilityStatus) { case ReachabilityStatus.notReachable: return _buildIconText( Icons.close, S.of(context)!.couldNotEstablishConnectionToTheServer, errorColor, ); case ReachabilityStatus.unknownHost: return _buildIconText( Icons.close, S.of(context)!.hostCouldNotBeResolved, errorColor, ); case ReachabilityStatus.missingClientCertificate: return _buildIconText( Icons.close, S.of(context)!.loginPageReachabilityMissingClientCertificateText, errorColor, ); case ReachabilityStatus.invalidClientCertificateConfiguration: return _buildIconText( Icons.close, S.of(context)!.incorrectOrMissingCertificatePassphrase, errorColor, ); case ReachabilityStatus.connectionTimeout: return _buildIconText( Icons.close, S.of(context)!.connectionTimedOut, errorColor, ); default: return const ListTile(); } } Future _onSubmit() async { FocusScope.of(context).unfocus(); setState(() { _isFormSubmitted = true; }); if (_formKey.currentState?.saveAndValidate() ?? false) { final form = _formKey.currentState!.value; ClientCertificate? clientCert; final clientCertFormModel = form[ClientCertificateFormField.fkClientCertificate] as ClientCertificateFormModel?; if (clientCertFormModel != null) { clientCert = ClientCertificate( bytes: clientCertFormModel.bytes, passphrase: clientCertFormModel.passphrase, ); } final credentials = form[UserCredentialsFormField.fkCredentials] as LoginFormCredentials; try { await widget.onSubmit( context, credentials.username!, credentials.password!, form[ServerAddressFormField.fkServerAddress], clientCert, ); } on PaperlessApiException catch (error) { showErrorMessage(context, error); } on ServerMessageException catch (error) { showLocalizedError(context, error.message); } on InfoMessageException catch (error) { showInfoMessage(context, error); } catch (error) { showGenericError(context, error); } finally { setState(() { _isFormSubmitted = false; }); } } } }