| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335 |
- 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<AddPage> createState() => _AddPageState();
- }
- class _AddPageState extends State<AddPage> {
- // Uniquely identify Form widget for validation
- final GlobalKey<FormState> _formKey = GlobalKey<FormState>();
- // 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<AppState>(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()
- ? <Widget>[
- // 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>[
- 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>[
- 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),
- ),
- )
- ]
- : <Widget>[
- // 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()
- ],
- ),
- ),
- ),
- );
- }
- }
|