import 'dart:async'; import 'package:flutter/material.dart'; import 'package:logging/logging.dart'; import 'package:pshared/models/auth/pending_login.dart'; import 'package:pshared/provider/account.dart'; import 'package:pshared/service/verification.dart'; import 'package:pweb/models/state/flow_status.dart'; class TwoFactorProvider extends ChangeNotifier { static final _logger = Logger('provider.two_factor'); late AccountProvider _accountProvider; TwoFactorProvider(); FlowStatus _status = FlowStatus.idle; String? _errorMessage; String? _currentPendingToken; Timer? _cooldownTimer; int _cooldownRemainingSeconds = 0; DateTime? _cooldownUntil; FlowStatus get status => _status; bool get isSubmitting => _status == FlowStatus.submitting; bool get isResending => _status == FlowStatus.resending; bool get hasError => _status == FlowStatus.error; bool get verificationSuccess => _status == FlowStatus.success; String? get errorMessage => _errorMessage; int get cooldownRemainingSeconds => _cooldownRemainingSeconds; bool get isCooldownActive => _cooldownRemainingSeconds > 0; PendingLogin? get pendingLogin => _accountProvider.pendingLogin; void update(AccountProvider accountProvider) { _accountProvider = accountProvider; final pending = accountProvider.pendingLogin; final token = pending?.pendingToken.token; if (token != _currentPendingToken || accountProvider.account == null) { _resetState(); _currentPendingToken = token; } _syncCooldown(pending); } Future submitCode(String code) async { _errorMessage = null; _setStatus(FlowStatus.submitting); try { final pending = _accountProvider.pendingLogin; if (pending == null) { throw Exception('No pending login available'); } final account = await VerificationService.confirmLoginCode( pending: pending, code: code, ); _accountProvider.completePendingLogin(account); _currentPendingToken = null; _setStatus(FlowStatus.success); } catch (e) { _errorMessage = e.toString(); _logger.warning('Failed to verify code: ${e.toString()}', e); _setStatus(FlowStatus.error); } } Future resendCode() async { final pending = _accountProvider.pendingLogin; if (pending == null) { _logger.warning('No pending login to resend code for'); return; } if (isResending || isCooldownActive) return; _errorMessage = null; _setStatus(FlowStatus.resending); try { final confirmation = await VerificationService.resendLoginCode(pending); _accountProvider.updatePendingLogin(confirmation); _startCooldown(confirmation.cooldownSeconds); _setStatus(FlowStatus.idle); } catch (e) { _logger.warning('Failed to resend login code', e); _errorMessage = e.toString(); _setStatus(FlowStatus.error); } } void reset() { _resetState(); _currentPendingToken = null; } void _resetState() { _status = FlowStatus.idle; _errorMessage = null; _stopCooldown(); notifyListeners(); } void _syncCooldown(PendingLogin? pending) { if (pending == null) { _stopCooldown(notify: _cooldownRemainingSeconds != 0); return; } final until = pending.cooldownUntil; if (until == null) { _stopCooldown(notify: _cooldownRemainingSeconds != 0); return; } if (!_isCooldownActive(until) && _cooldownRemainingSeconds != 0) { _stopCooldown(notify: true); return; } if (_cooldownUntil == null || _cooldownUntil != until) { _startCooldownUntil(until); } } void _startCooldown(int seconds) { final until = DateTime.now().add(Duration(seconds: seconds)); _startCooldownUntil(until); } void _startCooldownUntil(DateTime until) { _cooldownTimer?.cancel(); _cooldownUntil = until; _cooldownRemainingSeconds = _cooldownRemaining(); if (_cooldownRemainingSeconds <= 0) { _cooldownTimer = null; _cooldownUntil = null; notifyListeners(); return; } _cooldownTimer = Timer.periodic(const Duration(seconds: 1), (timer) { final remaining = _cooldownRemaining(); if (remaining <= 0) { _stopCooldown(notify: true); return; } if (remaining != _cooldownRemainingSeconds) { _cooldownRemainingSeconds = remaining; notifyListeners(); } }); notifyListeners(); } bool _isCooldownActive(DateTime until) => until.isAfter(DateTime.now()); int _cooldownRemaining() { final until = _cooldownUntil; if (until == null) return 0; final remaining = until.difference(DateTime.now()).inSeconds; return remaining < 0 ? 0 : remaining; } void _stopCooldown({bool notify = false}) { _cooldownTimer?.cancel(); _cooldownTimer = null; final hadCooldown = _cooldownRemainingSeconds != 0; _cooldownRemainingSeconds = 0; _cooldownUntil = null; if (notify && hadCooldown) { notifyListeners(); } } void _setStatus(FlowStatus status) { _status = status; notifyListeners(); } @override void dispose() { _stopCooldown(); super.dispose(); } }