import '../state/app_state.dart'; import '../ui/adaptive.dart' show AdaptiveDialogAction, AppScaffold, isPlatformAndroid; import 'package:pb_authenticator_totp/totp_algorithm.dart'; import 'package:pb_authenticator_totp/totp.dart' show Base32, TotpItem; import 'package:flutter/material.dart'; import 'package:flutter/cupertino.dart'; import '../l10n/app_localizations.dart'; import 'package:provider/provider.dart'; /// Page for adding accounts. class AddPage extends StatefulWidget { const AddPage({super.key}); @override State createState() => _AddPageState(); } class _AddPageState extends State { // Uniquely identify Form widget for validation final GlobalKey _formKey = GlobalKey(); // Text field controllers final _issuerController = TextEditingController(); final _accountNameController = TextEditingController(); final _secretController = TextEditingController(); // Value of dropdown fields int _digits = 6; int _period = 30; @override void initState() { super.initState(); // Add listener to secret controller _secretController.addListener(_enableSecretAutoValidation); } // Enable validation when text is changed (but don't validate initially) bool _secretAutoValidation = false; void _enableSecretAutoValidation() { if (_secretController.text.isEmpty) return; _secretController.removeListener(_enableSecretAutoValidation); setState(() { _secretAutoValidation = true; }); } @override dispose() { _issuerController.dispose(); _accountNameController.dispose(); _secretController.dispose(); super.dispose(); } // Adds item or display errors void addItem(TotpItem item) { try { Provider.of(context, listen: false).addItem(item).then((_) { Navigator.pop(context); }); } catch (e) { // TODO: Fix exception handling var errMessage = e.toString(); if (e.toString().contains('DUPLICATE_ACCOUNT')) { errMessage = AppLocalizations.of(context)!.errDuplicateAccount; } else if (e.toString().contains('ID_COLLISION')) { errMessage = AppLocalizations.of(context)!.errIdCollision; } showAdaptiveDialog( context: context, builder: (BuildContext context) { return AlertDialog.adaptive( title: Text(AppLocalizations.of(context)!.error), content: Text(errMessage), actions: [ AdaptiveDialogAction( onPressed: Navigator.of(context).pop, child: Text(AppLocalizations.of(context)!.ok)) ]); }); } } /// Whether the secret field is valid bool secretFieldValid() { return _secretController.text.isNotEmpty && Base32.isBase32(_secretController.text); } // Handle add action void handleAdd() { // Validate if (!_formKey.currentState!.validate()) { return; } // No iOS validator, so may need to recheck and fail if (!secretFieldValid()) { showCupertinoDialog( context: context, builder: (BuildContext context) { return CupertinoAlertDialog( title: Text(AppLocalizations.of(context)!.error), content: Text(AppLocalizations.of(context)!.secretInvalidMessage), actions: [ CupertinoDialogAction( onPressed: Navigator.of(context).pop, child: Text(AppLocalizations.of(context)!.ok)) ], ); }, ); return; } // Initialise and add TOTP item var item = TotpItem( _secretController.text, _digits, _period, OtpHashAlgorithm.sha1, _issuerController.text, _accountNameController.text); addItem(item); } // Separates fields // Adapted from flutter examples static const BorderSide _greyBorder = BorderSide( color: CupertinoColors.lightBackgroundGray, style: BorderStyle.solid, width: 0.0, ); static const _boxDecoration = BoxDecoration( border: Border( bottom: _greyBorder, top: _greyBorder, left: BorderSide.none, right: BorderSide.none)); @override Widget build(BuildContext context) { return AppScaffold( title: Text(AppLocalizations.of(context)!.addTitle), cupertinoNavigationBar: CupertinoNavigationBar( middle: Text(AppLocalizations.of(context)!.addTitle), trailing: CupertinoButton( padding: const EdgeInsets.all(0), onPressed: handleAdd, child: Text(AppLocalizations.of(context)!.add))), body: Form( key: _formKey, child: Padding( padding: const EdgeInsets.symmetric(vertical: 5, horizontal: 15), child: ListView( children: isPlatformAndroid() ? [ // Key TextFormField( controller: _secretController, textCapitalization: TextCapitalization.characters, autocorrect: false, autovalidateMode: _secretAutoValidation ? AutovalidateMode.onUserInteraction : AutovalidateMode.disabled, validator: (value) => secretFieldValid() ? null : AppLocalizations.of(context)!.secretInvalidMessage, decoration: InputDecoration( labelText: AppLocalizations.of(context)!.secret), ), // Provider TextFormField( controller: _issuerController, textCapitalization: TextCapitalization.words, decoration: InputDecoration( labelText: AppLocalizations.of(context)!.issuer, hintText: 'Example Service'), ), // Account name TextFormField( controller: _accountNameController, keyboardType: TextInputType.emailAddress, decoration: InputDecoration( labelText: AppLocalizations.of(context)!.accountName, hintText: 'user@example.com'), ), // # of digits InputDecorator( decoration: InputDecoration( contentPadding: const EdgeInsets.only(top: 15), labelText: AppLocalizations.of(context)!.digits, border: InputBorder.none, labelStyle: const TextStyle(fontSize: 20), ), child: DropdownButton( value: _digits, onChanged: (value) { setState(() { _digits = value; }); }, isExpanded: true, items: const [ DropdownMenuItem(value: 6, child: Text("6")), DropdownMenuItem(value: 8, child: Text("8")), ], ), ), // Time step InputDecorator( decoration: InputDecoration( contentPadding: const EdgeInsets.only(top: 10), labelText: AppLocalizations.of(context)!.period, border: InputBorder.none, labelStyle: const TextStyle(fontSize: 20)), child: DropdownButton( value: _period, onChanged: (value) { setState(() { _period = value; }); }, isExpanded: true, items: const [ DropdownMenuItem(value: 30, child: Text("30")), DropdownMenuItem(value: 60, child: Text("60")), ], ), ), // Add button Padding( padding: const EdgeInsets.symmetric(vertical: 5), child: ElevatedButton( onPressed: handleAdd, child: Text(AppLocalizations.of(context)!.add), ), ) ] : [ // Key CupertinoTextField( controller: _secretController, textCapitalization: TextCapitalization.characters, autocorrect: false, decoration: _boxDecoration, padding: const EdgeInsets.symmetric(vertical: 15), prefix: Text(AppLocalizations.of(context)!.secret), textAlign: TextAlign.right), // Provider CupertinoTextField( controller: _issuerController, textCapitalization: TextCapitalization.words, decoration: _boxDecoration, padding: const EdgeInsets.symmetric(vertical: 15), prefix: Text(AppLocalizations.of(context)!.issuer), textAlign: TextAlign.right), // Account name CupertinoTextField( controller: _accountNameController, keyboardType: TextInputType.emailAddress, decoration: _boxDecoration, padding: const EdgeInsets.symmetric(vertical: 15), prefix: Text(AppLocalizations.of(context)!.accountName), textAlign: TextAlign.right), // # of digits Container( padding: const EdgeInsets.symmetric(vertical: 15), decoration: _boxDecoration, child: ListBody( children: [ Padding( padding: const EdgeInsets.only(bottom: 8), child: Text(AppLocalizations.of(context)!.digits)), CupertinoSegmentedControl( groupValue: _digits.toString(), children: const {'6': Text('6'), '8': Text('8')}, onValueChanged: (v) { setState(() { _digits = int.parse(v); }); }, ) ], ), ), // Time step Container( padding: const EdgeInsets.symmetric(vertical: 15), decoration: _boxDecoration, child: ListBody( children: [ Padding( padding: const EdgeInsets.only(bottom: 8), child: Text(AppLocalizations.of(context)!.period)), CupertinoSegmentedControl( groupValue: _period.toString(), children: const { '30': Text('30'), '60': Text('60') }, onValueChanged: (v) { setState(() { _period = int.parse(v); }); }, ) ], ), ), // Add button Container() ], ), ), ), ); } }