0

I'm almost finished with my 2FA app. It generates TOTP codes for a given secret key, digits, interval and algorithm. However, I have a hard time with syncing multiple CircularProgressIndicators to repeat at the same time.

Have a look at my video. It shows two progress indicators, however, when they start over, they're not synced and there is almost 1 second difference. Any ideas how to fix it? https://streamable.com/p7uw8d

I'm using the BLoC pattern to generate TOTP codes.

Code:

custom_progress_indicator.dart

import 'package:flutter/material.dart';

class CustomProgressIndicator extends StatefulWidget {
  final int duration;
  final VoidCallback onComplete;
  final double initialDuration;

  CustomProgressIndicator({
    @required this.duration,
    @required this.onComplete,
    @required this.initialDuration,
  });

  @override
  _CustomProgressIndicatorState createState() =>
      _CustomProgressIndicatorState();
}

class _CustomProgressIndicatorState extends State<CustomProgressIndicator>
    with TickerProviderStateMixin {
  AnimationController _controller;
  Animation<double> _animation;

  @override
  void initState() {
    _controller = AnimationController(
      duration: Duration(seconds: widget.duration),
      lowerBound: 0.0,
      upperBound: 1.0,
      vsync: this,
      animationBehavior: AnimationBehavior.preserve,
    );
    _controller.forward(from: widget.initialDuration);
    _animation = Tween(begin: 0.0, end: 1.0).animate(_controller)
      ..addStatusListener((AnimationStatus status) {
        if (status == AnimationStatus.completed) {
          widget.onComplete();

          _controller.repeat();
        }
      });
    super.initState();
  }

  @override
  Widget build(BuildContext context) {
    return AnimatedBuilder(
      animation: _animation,
      builder: (BuildContext context, Widget child) {
        return CircularProgressIndicator(
          value: _animation.value,
        );
      },
    );
  }
}

otp_list_tile.dart

import 'package:duckie/screens/widgets/custom_progress_indicator.dart';
import 'package:flutter/material.dart';

class OtpListTile extends StatelessWidget {
  final int duration;
  final VoidCallback onComplete;
  final double initialDuration;
  final String accountName;
  final String otp;

  OtpListTile({
    @required this.duration,
    @required this.onComplete,
    @required this.initialDuration,
    @required this.accountName,
    @required this.otp,
  });

  @override
  Widget build(BuildContext context) {
    return ListTile(
      leading: CustomProgressIndicator(
        duration: duration,
        onComplete: onComplete,
        initialDuration: initialDuration,
      ),
      title: Text(accountName),
      subtitle: Text(otp),
    );
  }
}

home_screen.dart

import 'dart:io';

import 'package:duckie/blocs/manual_input/manual_input_bloc.dart';
import 'package:duckie/blocs/qr_code_scanner/qr_code_scanner_bloc.dart';
import 'package:duckie/blocs/totp_generator/totp_generator_bloc.dart';
import 'package:duckie/screens/widgets/custom_alert_dialog.dart';
import 'package:duckie/screens/widgets/otp_list_tile.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:duckie/shared/text_styles.dart';
import 'package:flutter_barcode_scanner/flutter_barcode_scanner.dart';
import 'package:flutter_bloc/flutter_bloc.dart';

class HomeScreen extends StatefulWidget {
  @override
  _HomeScreenState createState() => _HomeScreenState();
}

class _HomeScreenState extends State<HomeScreen> {
  List _totpItems;
  TotpGeneratorBloc _totpGeneratorBloc;

  @override
  void initState() {
    BlocProvider.of<TotpGeneratorBloc>(context)..add(GetTotpItemsEvent());
    super.initState();
  }

  @override
  void dispose() {
    _totpGeneratorBloc.close();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(
          'title',
          style: TextStyles.appBarText,
        ).tr(),
        centerTitle: false,
        elevation: 0.0,
        actions: [
          IconButton(
            icon: Icon(Icons.add),
            onPressed: () {
              Platform.isAndroid
                  ? showAndroidModalBottomSheet(context)
                  : showIosActionSheet(context);
            },
          ),
        ],
      ),
      body: MultiBlocListener(
        listeners: [
          BlocListener<QrCodeScannerBloc, QrCodeScannerState>(
            listener: (context, state) {
              if (state is QrCodeScannerError) {
                Platform.isAndroid
                    ? CustomAlertDialog.showAndroidAlertDialog(
                        context,
                        state.alertDialogErrorTitle,
                        state.alertDialogErrorContent)
                    : CustomAlertDialog.showIosAlertDialog(
                        context,
                        state.alertDialogErrorTitle,
                        state.alertDialogErrorContent);
              }
              if (state is QrCodeScannerFinal) {
                BlocProvider.of<TotpGeneratorBloc>(context)
                    .add(GetTotpItemsEvent());
              }
            },
          ),
          BlocListener<ManualInputBloc, ManualInputState>(
            listener: (context, state) {
              if (state is ManualInputFinal) {
                BlocProvider.of<TotpGeneratorBloc>(context)
                    .add(GetTotpItemsEvent());
              }
            },
          ),
        ],
        child: BlocBuilder<TotpGeneratorBloc, TotpGeneratorState>(
          builder: (context, state) {
            if (state is TotpGeneratorFinal) {
              _totpItems = state.totpItems;

              return ListView.builder(
                itemCount: _totpItems.length,
                itemBuilder: (context, index) {
                  return OtpListTile(
                    duration: _totpItems[index].duration,
                    initialDuration: _totpItems[index].initialDuration,
                    onComplete: () {
                      BlocProvider.of<TotpGeneratorBloc>(context)
                          .add(GetTotpItemsEvent());
                    },
                    accountName: _totpItems[index].accountName,
                    otp: _totpItems[index].otp,
                  );
                },
              );
            }
            return Container();
          },
        ),
      ),
    );
  }
}

void showAndroidModalBottomSheet(BuildContext context) {
  showModalBottomSheet(
    context: context,
    builder: (BuildContext context) {
      return ListView(
        shrinkWrap: true,
        children: [
          ListTile(
            onTap: () async {
              final qrCodeScannerBloc =
                  BlocProvider.of<QrCodeScannerBloc>(context);

              Navigator.of(context).pop();

              final String qrCodeResponse =
                  await FlutterBarcodeScanner.scanBarcode(
                      '#FF6666', 'cancel'.tr(), false, ScanMode.QR);

              qrCodeScannerBloc.add(GetQrCodeResponseEvent(qrCodeResponse));
            },
            leading: Icon(Icons.qr_code),
            title: Text('scan-qr-code').tr(),
          ),
          ListTile(
            onTap: () {
              Navigator.of(context).pop();
              Navigator.of(context).pushNamed('/manual-input');
            },
            leading: Icon(Icons.keyboard),
            title: Text('manual-input').tr(),
          ),
          ListTile(
            onTap: () {
              Navigator.of(context).pop();
            },
            leading: Icon(Icons.cancel),
            title: Text('cancel').tr(),
          )
        ],
      );
    },
  );
}

void showIosActionSheet(BuildContext context) {
  showCupertinoModalPopup(
    context: context,
    builder: (BuildContext context) {
      return CupertinoActionSheet(
        title: Text('action-sheet-title').tr(),
        message: Text('action-sheet-message').tr(),
        actions: [
          CupertinoActionSheetAction(
            onPressed: () async {
              final qrCodeScannerBloc =
                  BlocProvider.of<QrCodeScannerBloc>(context);

              Navigator.pop(context);

              final String qrCodeResponse =
                  await FlutterBarcodeScanner.scanBarcode(
                      '#FF6666', 'cancel'.tr(), false, ScanMode.QR);

              qrCodeScannerBloc.add(GetQrCodeResponseEvent(qrCodeResponse));
            },
            child: Text('scan-qr-code').tr(),
          ),
          CupertinoActionSheetAction(
            onPressed: () {
              Navigator.of(context).pop();
              Navigator.of(context).pushNamed('/manual-input');
            },
            child: Text('manual-input').tr(),
          ),
          CupertinoActionSheetAction(
            isDestructiveAction: true,
            onPressed: () {
              Navigator.of(context).pop();
            },
            child: Text('cancel').tr(),
          )
        ],
      );
    },
  );
}

totp_generator_bloc.dart

import 'dart:async';

import 'package:bloc/bloc.dart';
import 'package:dart_otp/dart_otp.dart';
import 'package:duckie/models/code/code_model.dart';
import 'package:hive/hive.dart';
import 'package:meta/meta.dart';

part 'totp_generator_event.dart';
part 'totp_generator_state.dart';

class TotpGeneratorBloc extends Bloc<TotpGeneratorEvent, TotpGeneratorState> {
  TotpGeneratorBloc() : super(TotpGeneratorInitial());

  @override
  Stream<TotpGeneratorState> mapEventToState(
    TotpGeneratorEvent event,
  ) async* {
    if (event is GetTotpItemsEvent) {
      Box box = Hive.box('totpmodel');
      List totpItems = [];

      for (var totpItem in box.values) {
        final String secret = totpItem.secret;
        final int digits = int.parse(totpItem.digits);
        final int interval = int.parse(totpItem.interval);
        final String algorithmString = totpItem.algorithm;
        final String accountName = totpItem.accountName;
        final int secondsSinceEpoch = totpItem.secondsSinceEpoch;
        final double initialDuration =
            (secondsSinceEpoch % interval) / interval;

        OTPAlgorithm algorithm = OTPAlgorithm.SHA1;

        switch (algorithmString) {
          case 'sha1':
            algorithm = OTPAlgorithm.SHA1;
            break;
          case 'sha256':
            algorithm = OTPAlgorithm.SHA256;
            break;
          case 'sha384':
            algorithm = OTPAlgorithm.SHA384;
            break;
          case 'sha512':
            algorithm = OTPAlgorithm.SHA512;
            break;
        }

        final TOTP totp = TOTP(
          secret: secret,
          digits: digits,
          interval: interval,
          algorithm: algorithm,
        );

        try {
          final String otp = totp.now();

          totpItems.add(CodeModel(otp, interval, initialDuration, accountName));

          yield TotpGeneratorFinal(totpItems);
        } catch (error) {
          yield TotpGeneratorError('totp-fail-title', 'totp-fail-content');
        }
      }
    }
  }
}

szakes1
  • 794
  • 3
  • 16
  • 35

1 Answers1

0

you can use value parameter or the widget with any state management or you can use any listener on DateTime.now() and get the valued based on fixed formula

Ahmed Masoud
  • 309
  • 3
  • 8