LCOV - code coverage report
Current view: top level - lib/pages/security - security_page.dart Coverage Total Hit
Test: lcov.info Lines: 1.0 % 103 1
Test Date: 2026-11-14 15:04:37 Functions: - 0 0

            Line data    Source code
       1              : // Copyright 2026 The terCAD team. All rights reserved.
       2              : // Use of this source code is governed by a CC BY-NC-ND 4.0 license that can be found in the LICENSE file.
       3              : 
       4              : import 'package:app_finance/_classes/controller/encryption_handler.dart';
       5              : import 'package:app_finance/_classes/controller/focus_controller.dart';
       6              : import 'package:app_finance/_classes/herald/app_design.dart';
       7              : import 'package:app_finance/_classes/herald/app_locale.dart';
       8              : import 'package:app_finance/_classes/storage/app_preferences.dart';
       9              : import 'package:app_finance/_configs/custom_color_scheme.dart';
      10              : import 'package:app_finance/_configs/theme_helper.dart';
      11              : import 'package:app_finance/_ext/build_context_ext.dart';
      12              : import 'package:app_finance/design/button/full_sized_button_widget.dart';
      13              : import 'package:app_finance/design/form/checkbox_input.dart';
      14              : import 'package:app_finance/design/generic/notification_bar.dart';
      15              : import 'package:app_finance/design/wrapper/input_wrapper.dart';
      16              : import 'package:app_finance/design/wrapper/single_scroll_wrapper.dart';
      17              : import 'package:app_finance/pages/_interfaces/abstract_page_state.dart';
      18              : import 'package:flutter/foundation.dart';
      19              : import 'package:material_ui/material_ui.dart';
      20              : import 'package:flutter/services.dart';
      21              : import 'package:local_auth/local_auth.dart';
      22              : import 'package:simple_totp_auth/simple_totp_auth.dart';
      23              : 
      24              : class SecurityPage extends StatefulWidget {
      25            2 :   const SecurityPage({super.key});
      26              : 
      27            0 :   @override
      28            0 :   SecurityPageState createState() => SecurityPageState();
      29              : }
      30              : 
      31              : class SecurityPageState extends AbstractPageState<SecurityPage> {
      32              :   late FocusController focus;
      33              :   late TextEditingController password;
      34              :   late TextEditingController passwordRepeat;
      35              :   late TextEditingController otpCode;
      36              :   bool isBio = AppPreferences.get(AppPreferences.prefIsBio) == AppPreferences.isActive;
      37              :   bool canAuthenticateBio = false;
      38              : 
      39            0 :   @override
      40              :   void initState() {
      41            0 :     focus = FocusController();
      42            0 :     password = TextEditingController();
      43            0 :     passwordRepeat = TextEditingController();
      44            0 :     otpCode = TextEditingController();
      45            0 :     super.initState();
      46              :   }
      47              : 
      48            0 :   @override
      49              :   void dispose() {
      50            0 :     focus.dispose();
      51            0 :     password.dispose();
      52            0 :     passwordRepeat.dispose();
      53            0 :     otpCode.dispose();
      54            0 :     super.dispose();
      55              :   }
      56              : 
      57            0 :   bool get isOtpActive => AppPreferences.get(AppPreferences.prefIsOTP) == AppPreferences.isActive;
      58              : 
      59            0 :   Future<void> checkBioAuthentication() async {
      60            0 :     final bio = LocalAuthentication();
      61              :     bool isAvailable = false;
      62              :     try {
      63            0 :       final canCheckBiometrics = await bio.canCheckBiometrics;
      64            0 :       final isDeviceSupported = await bio.isDeviceSupported();
      65              :       isAvailable = canCheckBiometrics && isDeviceSupported;
      66            0 :     } on Exception catch (_) {
      67              :       isAvailable = false;
      68              :     }
      69            0 :     setState(() => canAuthenticateBio = isAvailable);
      70              :   }
      71              : 
      72            0 :   @override
      73              :   String getButtonName() {
      74            0 :     return isOtpActive ? AppLocale.labels.deactivateAction : AppLocale.labels.saveSettingsTooltip;
      75              :   }
      76              : 
      77            0 :   @override
      78            0 :   Widget buildButton(BuildContext context, BoxConstraints constraints) => FullSizedButtonWidget(
      79              :         constraints: constraints,
      80            0 :         controller: focus,
      81            0 :         onPressed: () => isOtpActive ? triggerDeactivateButton() : triggerActionButton(),
      82            0 :         title: getButtonName(),
      83            0 :         icon: isOtpActive ? Icons.do_disturb_alt : Icons.save,
      84              :       );
      85              : 
      86            0 :   void triggerActionButton() {
      87            0 :     if (password.text.isEmpty) {
      88            0 :       NotificationBar.showSnackBar(context, AppLocale.labels.securePasswordEmpty, true);
      89              :       return;
      90              :     }
      91            0 :     if (password.text != passwordRepeat.text) {
      92            0 :       NotificationBar.showSnackBar(context, AppLocale.labels.securePasswordNotMatch, true);
      93              :       return;
      94              :     }
      95              : 
      96            0 :     final secret = AppPreferences.get(AppPreferences.prefPrivacyKey) ?? '';
      97            0 :     final totp = TOTP(secret: secret);
      98            0 :     final code = totp.now();
      99            0 :     if (otpCode.text.isNotEmpty && otpCode.text != code) {
     100            0 :       NotificationBar.showSnackBar(context, AppLocale.labels.secureOtpCodeInvalid, true);
     101              :       return;
     102              :     }
     103              : 
     104            0 :     AppPreferences.set(AppPreferences.prefIsOTP, AppPreferences.isActive);
     105            0 :     AppPreferences.set(AppPreferences.prefIsBio, isBio ? AppPreferences.isActive : AppPreferences.isInactive);
     106            0 :     AppPreferences.set(AppPreferences.prefRecoveryKey, EncryptionHandler.getHashString(password.text));
     107            0 :     setState(() {
     108            0 :       password.clear();
     109            0 :       passwordRepeat.clear();
     110            0 :       otpCode.clear();
     111              :     });
     112            0 :     NotificationBar.showSnackBar(context, AppLocale.labels.settingsSaved);
     113              :   }
     114              : 
     115            0 :   void triggerDeactivateButton() {
     116            0 :     if (password.text.isEmpty) {
     117            0 :       NotificationBar.showSnackBar(context, AppLocale.labels.securePasswordEmpty, true);
     118              :       return;
     119              :     }
     120            0 :     final key = AppPreferences.get(AppPreferences.prefRecoveryKey) ?? '';
     121            0 :     if (EncryptionHandler.getHashString(password.text) != key) {
     122            0 :       NotificationBar.showSnackBar(context, AppLocale.labels.securePasswordNotMatch, true);
     123              :       return;
     124              :     }
     125            0 :     AppPreferences.set(AppPreferences.prefIsOTP, AppPreferences.isInactive);
     126            0 :     AppPreferences.set(AppPreferences.prefIsBio, AppPreferences.isInactive);
     127            0 :     setState(() {
     128            0 :       isBio = false;
     129            0 :       password.clear();
     130              :     });
     131            0 :     NotificationBar.showSnackBar(context, AppLocale.labels.secureOtpDeactivated);
     132              :   }
     133              : 
     134            0 :   @override
     135            0 :   String getTitle() => AppLocale.labels.securityHeadline;
     136              : 
     137            0 :   @override
     138              :   Widget buildContent(BuildContext context, BoxConstraints constraints) {
     139            0 :     WidgetsBinding.instance.addPostFrameCallback((_) => checkBioAuthentication());
     140            0 :     final indent = ThemeHelper.getIndent();
     141            0 :     final textTheme = context.textTheme;
     142            0 :     final secret = AppPreferences.get(AppPreferences.prefPrivacyKey) ?? '';
     143            0 :     final totp = TOTP(secret: secret);
     144            0 :     final totpUrl = totp.generateOTPAuthURI(issuer: 'Fingrom', account: 'support@tercad.pl');
     145            0 :     return SingleScrollWrapper(
     146            0 :       controller: focus,
     147            0 :       child: Padding(
     148            0 :         padding: EdgeInsets.all(indent),
     149            0 :         child: Column(
     150            0 :           crossAxisAlignment: AppDesign.getAlignment(),
     151            0 :           children: [
     152            0 :             InputWrapper.text(
     153            0 :               title: AppLocale.labels.securePassword,
     154            0 :               controller: password,
     155              :               isRequired: true,
     156              :               inputType: TextInputType.visiblePassword,
     157              :             ),
     158            0 :             if (!isOtpActive) ...[
     159            0 :               InputWrapper.text(
     160            0 :                 title: AppLocale.labels.securePasswordRepeat,
     161            0 :                 controller: passwordRepeat,
     162              :                 isRequired: true,
     163              :                 inputType: TextInputType.visiblePassword,
     164              :               ),
     165              :               if (!kIsWeb)
     166            0 :                 CheckboxInput(
     167            0 :                   label: AppLocale.labels.secureBioPromptTitle,
     168            0 :                   value: isBio,
     169            0 :                   onChanged: (value) => setState(() => isBio = value),
     170              :                   constraints: constraints,
     171            0 :                   enabled: canAuthenticateBio,
     172              :                 ),
     173            0 :               TOTPQrWidget(
     174              :                 secret: secret,
     175              :                 issuer: 'Fingrom',
     176              :                 accountName: 'support@tercad.pl',
     177              :                 width: 200,
     178              :                 height: 200,
     179              :                 color: Colors.white,
     180              :               ),
     181            0 :               Container(
     182            0 :                 padding: EdgeInsets.all(indent),
     183            0 :                 color: context.colorScheme.fieldBackground,
     184            0 :                 child: SelectableText(
     185              :                   totpUrl,
     186            0 :                   style: textTheme.bodyLarge,
     187            0 :                   onTap: () {
     188            0 :                     Clipboard.setData(ClipboardData(text: totpUrl));
     189            0 :                     ScaffoldMessenger.of(context).showSnackBar(
     190            0 :                       SnackBar(content: Text(AppLocale.labels.copiedToClipboard)),
     191              :                     );
     192              :                   },
     193              :                 ),
     194              :               ),
     195            0 :               ThemeHelper.hIndent2x,
     196            0 :               InputWrapper.text(
     197            0 :                 title: AppLocale.labels.secureOtpCode,
     198            0 :                 controller: otpCode,
     199              :               ),
     200            0 :               ThemeHelper.hIndent2x,
     201              :             ],
     202              :           ],
     203              :         ),
     204              :       ),
     205              :     );
     206              :   }
     207              : }
        

Generated by: LCOV version 2.0-1