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/theme_helper.dart';
10 : import 'package:app_finance/design/button/full_sized_button_widget.dart';
11 : import 'package:app_finance/design/generic/notification_bar.dart';
12 : import 'package:app_finance/design/wrapper/input_wrapper.dart';
13 : import 'package:app_finance/design/wrapper/single_scroll_wrapper.dart';
14 : import 'package:app_finance/pages/_interfaces/abstract_page_state.dart';
15 : import 'package:material_ui/material_ui.dart';
16 : import 'package:local_auth/local_auth.dart';
17 :
18 : class SecurityBioTab extends StatefulWidget {
19 : final VoidCallback onSuccess;
20 :
21 0 : const SecurityBioTab({super.key, required this.onSuccess});
22 :
23 0 : @override
24 0 : SecurityBioTabState createState() => SecurityBioTabState();
25 : }
26 :
27 : class SecurityBioTabState extends AbstractPageState<SecurityBioTab> {
28 : late FocusController focus;
29 : late TextEditingController password;
30 : final bio = LocalAuthentication();
31 : bool _isAuthenticating = false;
32 :
33 0 : @override
34 : void initState() {
35 0 : focus = FocusController();
36 0 : password = TextEditingController();
37 0 : super.initState();
38 0 : WidgetsBinding.instance.addPostFrameCallback((_) {
39 0 : if (mounted) {
40 0 : checkBio(context);
41 : }
42 : });
43 : }
44 :
45 0 : @override
46 : void dispose() {
47 0 : focus.dispose();
48 0 : password.dispose();
49 0 : super.dispose();
50 : }
51 :
52 0 : @override
53 : Drawer? buildDrawer() => null;
54 :
55 0 : @override
56 0 : List<Widget> getBarActions(NavigatorState nav) => [];
57 :
58 0 : @override
59 : Widget? getBarLeading(NavigatorState nav) => null;
60 :
61 0 : @override
62 0 : String getTitle() => AppLocale.labels.securityHeadline;
63 :
64 0 : @override
65 0 : String getButtonName() => AppLocale.labels.btnConfirm;
66 :
67 0 : @override
68 0 : Widget buildButton(BuildContext context, BoxConstraints constraints) => FullSizedButtonWidget(
69 : constraints: constraints,
70 0 : controller: focus,
71 0 : onPressed: () => triggerActionButton(),
72 0 : title: getButtonName(),
73 : icon: Icons.password,
74 : );
75 :
76 0 : void triggerActionButton() {
77 0 : if (password.text.isEmpty) {
78 0 : NotificationBar.showSnackBar(context, AppLocale.labels.securePasswordEmpty, true);
79 : return;
80 : }
81 0 : final key = AppPreferences.get(AppPreferences.prefRecoveryKey) ?? '';
82 0 : if (EncryptionHandler.getHashString(password.text) != key) {
83 0 : NotificationBar.showSnackBar(context, AppLocale.labels.securePasswordNotMatch, true);
84 : return;
85 : }
86 0 : widget.onSuccess();
87 : }
88 :
89 0 : Future<void> checkBio(BuildContext context) async {
90 : bool canAuthenticate = false;
91 : try {
92 0 : final canCheckBiometrics = await bio.canCheckBiometrics;
93 0 : final isDeviceSupported = await bio.isDeviceSupported();
94 : canAuthenticate = canCheckBiometrics && isDeviceSupported;
95 0 : } on Exception catch (_) {
96 : canAuthenticate = false;
97 : }
98 0 : if (canAuthenticate && !_isAuthenticating) {
99 0 : _isAuthenticating = true;
100 0 : bio
101 0 : .authenticate(
102 0 : localizedReason: AppLocale.labels.secureBioPromptReason,
103 : options: const AuthenticationOptions(
104 : biometricOnly: true,
105 : stickyAuth: true,
106 : ),
107 : )
108 0 : .then((authenticated) {
109 : if (authenticated) {
110 0 : widget.onSuccess();
111 : }
112 0 : }).catchError((error) {
113 0 : if (context.mounted) {
114 0 : NotificationBar.showSnackBar(context, AppLocale.labels.secureBioNotMatch, true);
115 : }
116 0 : }).whenComplete(() => _isAuthenticating = false);
117 : }
118 : }
119 :
120 0 : @override
121 : Widget buildContent(BuildContext context, BoxConstraints constraints) {
122 0 : final indent = ThemeHelper.getIndent();
123 0 : return SingleScrollWrapper(
124 0 : controller: focus,
125 0 : child: Padding(
126 0 : padding: EdgeInsets.all(indent),
127 0 : child: Column(
128 0 : crossAxisAlignment: AppDesign.getAlignment(),
129 0 : children: [
130 0 : InputWrapper.text(
131 0 : title: AppLocale.labels.securePassword,
132 0 : controller: password,
133 : ),
134 : ],
135 : ),
136 : ),
137 : );
138 : }
139 : }
|