LCOV - code coverage report
Current view: top level - lib/pages/integration/widgets - fints_dialogs.dart Coverage Total Hit
Test: lcov.info Lines: 53.7 % 123 66
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 'dart:async';
       5              : 
       6              : import 'package:app_finance/_classes/herald/app_locale.dart';
       7              : import 'package:app_finance/_configs/theme_helper.dart';
       8              : import 'package:app_finance/_ext/build_context_ext.dart';
       9              : import 'package:fints/fints.dart';
      10              : import 'package:material_ui/material_ui.dart';
      11              : import 'package:flutter/services.dart';
      12              : 
      13              : class FinTsDialogs {
      14              :   final BuildContext Function() context;
      15              : 
      16            1 :   const FinTsDialogs(this.context);
      17              : 
      18            1 :   BuildContext? get _context {
      19            2 :     final ctx = context();
      20            1 :     return ctx.mounted ? ctx : null;
      21              :   }
      22              : 
      23            1 :   Future<String?> askPin() async {
      24            1 :     final ctx = _context;
      25              :     if (ctx == null) {
      26              :       return null;
      27              :     }
      28            1 :     final pin = await showDialog<String>(
      29              :       context: ctx,
      30              :       barrierDismissible: false,
      31            1 :       builder: (_) => const FinTsPinDialog(),
      32              :     );
      33            1 :     return pin == null || pin.isEmpty ? null : pin;
      34              :   }
      35              : 
      36            0 :   Future<bool> confirm(String text) async {
      37            0 :     final ctx = _context;
      38              :     if (ctx == null) {
      39              :       return false;
      40              :     }
      41            0 :     final result = await showDialog<bool>(
      42              :       context: ctx,
      43            0 :       builder: (context) => AlertDialog(
      44            0 :         title: Text(AppLocale.labels.confirmHeader),
      45            0 :         content: Text(text),
      46            0 :         actions: [
      47            0 :           TextButton(onPressed: () => Navigator.pop(context, false), child: Text(AppLocale.labels.closeTooltip)),
      48            0 :           FilledButton(onPressed: () => Navigator.pop(context, true), child: Text(AppLocale.labels.ok)),
      49              :         ],
      50              :       ),
      51              :     );
      52              :     return result ?? false;
      53              :   }
      54              : 
      55            0 :   Future<TanMethod?> selectTanMethod(List<TanMethod> methods) => _select<TanMethod>(
      56            0 :         AppLocale.labels.finTsTanMethod,
      57              :         methods,
      58            0 :         (method) => method.name,
      59              :       );
      60              : 
      61            0 :   Future<TanMedium?> selectTanMedium(List<TanMedium> media) => _select<TanMedium>(
      62            0 :         AppLocale.labels.finTsTanMedium,
      63              :         media,
      64            0 :         (medium) => medium.name ?? medium.mobileNumberMasked ?? medium.cardNumber ?? medium.mediumClass,
      65              :       );
      66              : 
      67            0 :   Future<T?> _select<T>(String title, List<T> options, String Function(T) label) async {
      68            0 :     final ctx = _context;
      69              :     if (ctx == null) {
      70              :       return null;
      71              :     }
      72            0 :     return showDialog<T>(
      73              :       context: ctx,
      74              :       barrierDismissible: false,
      75            0 :       builder: (context) => SimpleDialog(
      76            0 :         title: Text(title),
      77            0 :         children: [
      78            0 :           for (final option in options)
      79            0 :             SimpleDialogOption(
      80            0 :               onPressed: () => Navigator.pop(context, option),
      81            0 :               child: Text(label(option)),
      82              :             ),
      83              :         ],
      84              :       ),
      85              :     );
      86              :   }
      87              : 
      88            1 :   Future<TanAnswer> askTan(TanChallenge challenge) async {
      89            1 :     final ctx = _context;
      90              :     if (ctx == null) {
      91              :       return TanAnswer.cancelled;
      92              :     }
      93            1 :     final answer = await showDialog<TanAnswer>(
      94              :       context: ctx,
      95              :       barrierDismissible: false,
      96            2 :       builder: (_) => FinTsTanDialog(challenge: challenge),
      97              :     );
      98              :     return answer ?? TanAnswer.cancelled;
      99              :   }
     100              : }
     101              : 
     102              : class FinTsPinDialog extends StatefulWidget {
     103            2 :   const FinTsPinDialog({super.key});
     104              : 
     105            1 :   @override
     106            1 :   FinTsPinDialogState createState() => FinTsPinDialogState();
     107              : }
     108              : 
     109              : class FinTsPinDialogState extends State<FinTsPinDialog> {
     110              :   final controller = TextEditingController();
     111              : 
     112            1 :   @override
     113              :   void dispose() {
     114            2 :     controller.dispose();
     115            1 :     super.dispose();
     116              :   }
     117              : 
     118            1 :   @override
     119              :   Widget build(BuildContext context) {
     120            1 :     return AlertDialog(
     121            3 :       title: Text(AppLocale.labels.finTsPin),
     122            1 :       content: TextField(
     123              :         key: const ValueKey('fints_pin'),
     124            1 :         controller: controller,
     125              :         obscureText: true,
     126              :         autofocus: true,
     127              :         enableSuggestions: false,
     128              :         autocorrect: false,
     129            3 :         decoration: InputDecoration(helperText: AppLocale.labels.finTsPinHint, helperMaxLines: 2),
     130            0 :         onSubmitted: (value) => Navigator.pop(context, value),
     131              :       ),
     132            1 :       actions: [
     133            4 :         TextButton(onPressed: () => Navigator.pop(context), child: Text(AppLocale.labels.closeTooltip)),
     134            1 :         FilledButton(
     135            4 :           onPressed: () => Navigator.pop(context, controller.text),
     136            3 :           child: Text(AppLocale.labels.integrationConnect),
     137              :         ),
     138              :       ],
     139              :     );
     140              :   }
     141              : }
     142              : 
     143              : class FinTsTanDialog extends StatefulWidget {
     144              :   final TanChallenge challenge;
     145              : 
     146            1 :   const FinTsTanDialog({super.key, required this.challenge});
     147              : 
     148            1 :   @override
     149            1 :   FinTsTanDialogState createState() => FinTsTanDialogState();
     150              : }
     151              : 
     152              : class FinTsTanDialogState extends State<FinTsTanDialog> {
     153              :   final controller = TextEditingController();
     154              : 
     155            1 :   @override
     156              :   void initState() {
     157            1 :     super.initState();
     158            3 :     widget.challenge.resolved?.then((_) {
     159            0 :       if (mounted) {
     160            0 :         Navigator.pop(context, TanAnswer.approved);
     161              :       }
     162              :     });
     163              :   }
     164              : 
     165            1 :   @override
     166              :   void dispose() {
     167            2 :     controller.dispose();
     168            1 :     super.dispose();
     169              :   }
     170              : 
     171            1 :   @override
     172              :   Widget build(BuildContext context) {
     173            2 :     final challenge = widget.challenge;
     174            1 :     final method = challenge.method;
     175            1 :     final image = challenge.image;
     176            1 :     final flicker = challenge.flickerCode;
     177            1 :     return AlertDialog(
     178            3 :       title: Text(AppLocale.labels.finTsTanHeadline),
     179            1 :       content: SingleChildScrollView(
     180            1 :         child: Column(
     181              :           mainAxisSize: MainAxisSize.min,
     182              :           crossAxisAlignment: CrossAxisAlignment.start,
     183            1 :           children: [
     184            4 :             Text(method.name, style: context.textTheme.titleSmall),
     185              :             ThemeHelper.hIndent,
     186            5 :             if (challenge.plainMessage?.isNotEmpty == true) SelectableText(challenge.plainMessage!),
     187            0 :             if (image != null) ...[
     188              :               ThemeHelper.hIndent2x,
     189            0 :               Center(child: Image.memory(image.bytes, width: 240, fit: BoxFit.contain)),
     190              :             ],
     191            0 :             if (flicker != null) ...[
     192              :               ThemeHelper.hIndent2x,
     193            0 :               Center(child: FinTsFlickerCode(frames: flicker.frames())),
     194              :             ],
     195            1 :             ThemeHelper.hIndent2x,
     196            1 :             if (challenge.isDecoupled)
     197            0 :               const Center(child: CircularProgressIndicator())
     198              :             else
     199            1 :               TextField(
     200              :                 key: const ValueKey('fints_tan'),
     201            1 :                 controller: controller,
     202              :                 autofocus: true,
     203              :                 enableSuggestions: false,
     204              :                 autocorrect: false,
     205            1 :                 maxLength: method.maxTanLength,
     206            1 :                 keyboardType: method.numericTan ? TextInputType.number : TextInputType.text,
     207            3 :                 inputFormatters: method.numericTan ? [FilteringTextInputFormatter.digitsOnly] : null,
     208            3 :                 decoration: InputDecoration(labelText: AppLocale.labels.finTsTan),
     209            0 :                 onSubmitted: (value) => Navigator.pop(context, TanAnswer.tan(value)),
     210              :               ),
     211              :           ],
     212              :         ),
     213              :       ),
     214            1 :       actions: [
     215            1 :         TextButton(
     216            0 :           onPressed: () => Navigator.pop(context, TanAnswer.cancelled),
     217            3 :           child: Text(AppLocale.labels.closeTooltip),
     218              :         ),
     219            1 :         if (!challenge.isDecoupled)
     220            1 :           FilledButton(
     221            5 :             onPressed: () => Navigator.pop(context, TanAnswer.tan(controller.text)),
     222            3 :             child: Text(AppLocale.labels.ok),
     223              :           )
     224            0 :         else if (challenge.manualConfirmation)
     225            0 :           FilledButton(
     226            0 :             onPressed: () => Navigator.pop(context, TanAnswer.approved),
     227            0 :             child: Text(AppLocale.labels.finTsApproved),
     228              :           ),
     229              :       ],
     230              :     );
     231              :   }
     232              : }
     233              : 
     234              : class FinTsFlickerCode extends StatefulWidget {
     235              :   final List<List<bool>> frames;
     236              :   final Duration interval;
     237              : 
     238            0 :   const FinTsFlickerCode({
     239              :     super.key,
     240              :     required this.frames,
     241              :     this.interval = const Duration(milliseconds: 50),
     242              :   });
     243              : 
     244            0 :   @override
     245            0 :   FinTsFlickerCodeState createState() => FinTsFlickerCodeState();
     246              : }
     247              : 
     248              : class FinTsFlickerCodeState extends State<FinTsFlickerCode> {
     249              :   late final Timer _timer;
     250              :   int _index = 0;
     251              : 
     252            0 :   @override
     253              :   void initState() {
     254            0 :     super.initState();
     255            0 :     _timer = Timer.periodic(widget.interval, (_) {
     256            0 :       if (widget.frames.isNotEmpty) {
     257            0 :         setState(() => _index = (_index + 1) % widget.frames.length);
     258              :       }
     259              :     });
     260              :   }
     261              : 
     262            0 :   @override
     263              :   void dispose() {
     264            0 :     _timer.cancel();
     265            0 :     super.dispose();
     266              :   }
     267              : 
     268            0 :   @override
     269              :   Widget build(BuildContext context) {
     270            0 :     final frame = widget.frames.isEmpty ? List.filled(5, false) : widget.frames[_index];
     271            0 :     return Container(
     272              :       color: Colors.black,
     273              :       padding: const EdgeInsets.all(8),
     274            0 :       child: Row(
     275              :         mainAxisSize: MainAxisSize.min,
     276            0 :         children: [
     277            0 :           for (final bit in frame)
     278            0 :             Container(
     279              :               width: 40,
     280              :               height: 120,
     281              :               margin: const EdgeInsets.symmetric(horizontal: 4),
     282              :               color: bit ? Colors.white : Colors.black,
     283              :             ),
     284              :         ],
     285              :       ),
     286              :     );
     287              :   }
     288              : }
        

Generated by: LCOV version 2.0-1