add.dart 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335
  1. import '../state/app_state.dart';
  2. import '../ui/adaptive.dart'
  3. show AdaptiveDialogAction, AppScaffold, isPlatformAndroid;
  4. import 'package:pb_authenticator_totp/totp_algorithm.dart';
  5. import 'package:pb_authenticator_totp/totp.dart' show Base32, TotpItem;
  6. import 'package:flutter/material.dart';
  7. import 'package:flutter/cupertino.dart';
  8. import '../l10n/app_localizations.dart';
  9. import 'package:provider/provider.dart';
  10. /// Page for adding accounts.
  11. class AddPage extends StatefulWidget {
  12. const AddPage({super.key});
  13. @override
  14. State<AddPage> createState() => _AddPageState();
  15. }
  16. class _AddPageState extends State<AddPage> {
  17. // Uniquely identify Form widget for validation
  18. final GlobalKey<FormState> _formKey = GlobalKey<FormState>();
  19. // Text field controllers
  20. final _issuerController = TextEditingController();
  21. final _accountNameController = TextEditingController();
  22. final _secretController = TextEditingController();
  23. // Value of dropdown fields
  24. int _digits = 6;
  25. int _period = 30;
  26. @override
  27. void initState() {
  28. super.initState();
  29. // Add listener to secret controller
  30. _secretController.addListener(_enableSecretAutoValidation);
  31. }
  32. // Enable validation when text is changed (but don't validate initially)
  33. bool _secretAutoValidation = false;
  34. void _enableSecretAutoValidation() {
  35. if (_secretController.text.isEmpty) return;
  36. _secretController.removeListener(_enableSecretAutoValidation);
  37. setState(() {
  38. _secretAutoValidation = true;
  39. });
  40. }
  41. @override
  42. dispose() {
  43. _issuerController.dispose();
  44. _accountNameController.dispose();
  45. _secretController.dispose();
  46. super.dispose();
  47. }
  48. // Adds item or display errors
  49. void addItem(TotpItem item) {
  50. try {
  51. Provider.of<AppState>(context, listen: false).addItem(item).then((_) {
  52. Navigator.pop(context);
  53. });
  54. } catch (e) {
  55. // TODO: Fix exception handling
  56. var errMessage = e.toString();
  57. if (e.toString().contains('DUPLICATE_ACCOUNT')) {
  58. errMessage = AppLocalizations.of(context)!.errDuplicateAccount;
  59. } else if (e.toString().contains('ID_COLLISION')) {
  60. errMessage = AppLocalizations.of(context)!.errIdCollision;
  61. }
  62. showAdaptiveDialog(
  63. context: context,
  64. builder: (BuildContext context) {
  65. return AlertDialog.adaptive(
  66. title: Text(AppLocalizations.of(context)!.error),
  67. content: Text(errMessage),
  68. actions: [
  69. AdaptiveDialogAction(
  70. onPressed: Navigator.of(context).pop,
  71. child: Text(AppLocalizations.of(context)!.ok))
  72. ]);
  73. });
  74. }
  75. }
  76. /// Whether the secret field is valid
  77. bool secretFieldValid() {
  78. return _secretController.text.isNotEmpty &&
  79. Base32.isBase32(_secretController.text);
  80. }
  81. // Handle add action
  82. void handleAdd() {
  83. // Validate
  84. if (!_formKey.currentState!.validate()) {
  85. return;
  86. }
  87. // No iOS validator, so may need to recheck and fail
  88. if (!secretFieldValid()) {
  89. showCupertinoDialog(
  90. context: context,
  91. builder: (BuildContext context) {
  92. return CupertinoAlertDialog(
  93. title: Text(AppLocalizations.of(context)!.error),
  94. content: Text(AppLocalizations.of(context)!.secretInvalidMessage),
  95. actions: [
  96. CupertinoDialogAction(
  97. onPressed: Navigator.of(context).pop,
  98. child: Text(AppLocalizations.of(context)!.ok))
  99. ],
  100. );
  101. },
  102. );
  103. return;
  104. }
  105. // Initialise and add TOTP item
  106. var item = TotpItem(
  107. _secretController.text,
  108. _digits,
  109. _period,
  110. OtpHashAlgorithm.sha1,
  111. _issuerController.text,
  112. _accountNameController.text);
  113. addItem(item);
  114. }
  115. // Separates fields
  116. // Adapted from flutter examples
  117. static const BorderSide _greyBorder = BorderSide(
  118. color: CupertinoColors.lightBackgroundGray,
  119. style: BorderStyle.solid,
  120. width: 0.0,
  121. );
  122. static const _boxDecoration = BoxDecoration(
  123. border: Border(
  124. bottom: _greyBorder,
  125. top: _greyBorder,
  126. left: BorderSide.none,
  127. right: BorderSide.none));
  128. @override
  129. Widget build(BuildContext context) {
  130. return AppScaffold(
  131. title: Text(AppLocalizations.of(context)!.addTitle),
  132. cupertinoNavigationBar: CupertinoNavigationBar(
  133. middle: Text(AppLocalizations.of(context)!.addTitle),
  134. trailing: CupertinoButton(
  135. padding: const EdgeInsets.all(0),
  136. onPressed: handleAdd,
  137. child: Text(AppLocalizations.of(context)!.add))),
  138. body: Form(
  139. key: _formKey,
  140. child: Padding(
  141. padding: const EdgeInsets.symmetric(vertical: 5, horizontal: 15),
  142. child: ListView(
  143. children: isPlatformAndroid()
  144. ? <Widget>[
  145. // Key
  146. TextFormField(
  147. controller: _secretController,
  148. textCapitalization: TextCapitalization.characters,
  149. autocorrect: false,
  150. autovalidateMode: _secretAutoValidation
  151. ? AutovalidateMode.onUserInteraction
  152. : AutovalidateMode.disabled,
  153. validator: (value) => secretFieldValid()
  154. ? null
  155. : AppLocalizations.of(context)!.secretInvalidMessage,
  156. decoration: InputDecoration(
  157. labelText: AppLocalizations.of(context)!.secret),
  158. ),
  159. // Provider
  160. TextFormField(
  161. controller: _issuerController,
  162. textCapitalization: TextCapitalization.words,
  163. decoration: InputDecoration(
  164. labelText: AppLocalizations.of(context)!.issuer,
  165. hintText: 'Example Service'),
  166. ),
  167. // Account name
  168. TextFormField(
  169. controller: _accountNameController,
  170. keyboardType: TextInputType.emailAddress,
  171. decoration: InputDecoration(
  172. labelText: AppLocalizations.of(context)!.accountName,
  173. hintText: 'user@example.com'),
  174. ),
  175. // # of digits
  176. InputDecorator(
  177. decoration: InputDecoration(
  178. contentPadding: const EdgeInsets.only(top: 15),
  179. labelText: AppLocalizations.of(context)!.digits,
  180. border: InputBorder.none,
  181. labelStyle: const TextStyle(fontSize: 20),
  182. ),
  183. child: DropdownButton(
  184. value: _digits,
  185. onChanged: (value) {
  186. setState(() {
  187. _digits = value;
  188. });
  189. },
  190. isExpanded: true,
  191. items: const <DropdownMenuItem>[
  192. DropdownMenuItem(value: 6, child: Text("6")),
  193. DropdownMenuItem(value: 8, child: Text("8")),
  194. ],
  195. ),
  196. ),
  197. // Time step
  198. InputDecorator(
  199. decoration: InputDecoration(
  200. contentPadding: const EdgeInsets.only(top: 10),
  201. labelText: AppLocalizations.of(context)!.period,
  202. border: InputBorder.none,
  203. labelStyle: const TextStyle(fontSize: 20)),
  204. child: DropdownButton(
  205. value: _period,
  206. onChanged: (value) {
  207. setState(() {
  208. _period = value;
  209. });
  210. },
  211. isExpanded: true,
  212. items: const <DropdownMenuItem>[
  213. DropdownMenuItem(value: 30, child: Text("30")),
  214. DropdownMenuItem(value: 60, child: Text("60")),
  215. ],
  216. ),
  217. ),
  218. // Add button
  219. Padding(
  220. padding: const EdgeInsets.symmetric(vertical: 5),
  221. child: ElevatedButton(
  222. onPressed: handleAdd,
  223. child: Text(AppLocalizations.of(context)!.add),
  224. ),
  225. )
  226. ]
  227. : <Widget>[
  228. // Key
  229. CupertinoTextField(
  230. controller: _secretController,
  231. textCapitalization: TextCapitalization.characters,
  232. autocorrect: false,
  233. decoration: _boxDecoration,
  234. padding: const EdgeInsets.symmetric(vertical: 15),
  235. prefix: Text(AppLocalizations.of(context)!.secret),
  236. textAlign: TextAlign.right),
  237. // Provider
  238. CupertinoTextField(
  239. controller: _issuerController,
  240. textCapitalization: TextCapitalization.words,
  241. decoration: _boxDecoration,
  242. padding: const EdgeInsets.symmetric(vertical: 15),
  243. prefix: Text(AppLocalizations.of(context)!.issuer),
  244. textAlign: TextAlign.right),
  245. // Account name
  246. CupertinoTextField(
  247. controller: _accountNameController,
  248. keyboardType: TextInputType.emailAddress,
  249. decoration: _boxDecoration,
  250. padding: const EdgeInsets.symmetric(vertical: 15),
  251. prefix: Text(AppLocalizations.of(context)!.accountName),
  252. textAlign: TextAlign.right),
  253. // # of digits
  254. Container(
  255. padding: const EdgeInsets.symmetric(vertical: 15),
  256. decoration: _boxDecoration,
  257. child: ListBody(
  258. children: [
  259. Padding(
  260. padding: const EdgeInsets.only(bottom: 8),
  261. child:
  262. Text(AppLocalizations.of(context)!.digits)),
  263. CupertinoSegmentedControl(
  264. groupValue: _digits.toString(),
  265. children: const {'6': Text('6'), '8': Text('8')},
  266. onValueChanged: (v) {
  267. setState(() {
  268. _digits = int.parse(v);
  269. });
  270. },
  271. )
  272. ],
  273. ),
  274. ),
  275. // Time step
  276. Container(
  277. padding: const EdgeInsets.symmetric(vertical: 15),
  278. decoration: _boxDecoration,
  279. child: ListBody(
  280. children: [
  281. Padding(
  282. padding: const EdgeInsets.only(bottom: 8),
  283. child:
  284. Text(AppLocalizations.of(context)!.period)),
  285. CupertinoSegmentedControl(
  286. groupValue: _period.toString(),
  287. children: const {
  288. '30': Text('30'),
  289. '60': Text('60')
  290. },
  291. onValueChanged: (v) {
  292. setState(() {
  293. _period = int.parse(v);
  294. });
  295. },
  296. )
  297. ],
  298. ),
  299. ),
  300. // Add button
  301. Container()
  302. ],
  303. ),
  304. ),
  305. ),
  306. );
  307. }
  308. }