import '../../../l10n/app_localizations.dart'; import 'dart:math'; // import 'dart:developer'; import 'package:flutter/material.dart'; import 'package:kmobile/app.dart'; import 'package:kmobile/di/injection.dart'; import 'package:kmobile/security/secure_storage.dart'; import 'package:local_auth/local_auth.dart'; import 'package:flutter/services.dart'; enum MPinMode { enter, set, confirm } class MPinScreen extends StatefulWidget { final MPinMode mode; final String? initialPin; final void Function(String pin)? onCompleted; const MPinScreen({ super.key, required this.mode, this.initialPin, this.onCompleted, }); @override State createState() => _MPinScreenState(); } class _MPinScreenState extends State with TickerProviderStateMixin { List mPin = []; String? errorText; // Animation controllers late final AnimationController _bounceController; late final Animation _bounceAnimation; late final AnimationController _shakeController; late final AnimationController _waveController; late final Animation _waveAnimation; // State flags for animations bool _isError = false; bool _isSuccess = false; @override void initState() { super.initState(); // Bounce animation for single dot entry _bounceController = AnimationController( vsync: this, duration: const Duration(milliseconds: 150), ); _bounceAnimation = Tween(begin: 1.0, end: 1.2).animate( CurvedAnimation( parent: _bounceController, curve: Curves.elasticIn, reverseCurve: Curves.elasticOut, ), )..addStatusListener((status) { if (status == AnimationStatus.completed) { _bounceController.reverse(); } }); // Shake animation for error _shakeController = AnimationController( vsync: this, duration: const Duration(milliseconds: 400), ); // Wave animation for success _waveController = AnimationController( vsync: this, duration: const Duration(milliseconds: 800), ); _waveAnimation = Tween(begin: 0, end: 1).animate( CurvedAnimation(parent: _waveController, curve: Curves.easeInOut), ); if (widget.mode == MPinMode.enter) { _tryBiometricBeforePin(); } } @override void dispose() { _bounceController.dispose(); _shakeController.dispose(); _waveController.dispose(); super.dispose(); } Future _tryBiometricBeforePin() async { final storage = getIt(); final enabled = await storage.read('biometric_enabled'); // log('biometric_enabled: $enabled'); if (enabled != null && enabled) { final auth = LocalAuthentication(); if (await auth.canCheckBiometrics) { final didAuth = await auth.authenticate( // ignore: use_build_context_synchronously localizedReason: AppLocalizations.of(context).authenticateToAccess, options: const AuthenticationOptions(biometricOnly: true), ); if (didAuth && mounted) { // success → directly “complete” your flow widget.onCompleted?.call(''); // or navigate yourself: // Navigator.of(context).pushReplacement( // MaterialPageRoute(builder: (_) => const NavigationScaffold())); } } } } void addDigit(String digit) { if (_shakeController.isAnimating || _waveController.isAnimating) return; if (mPin.length < 4) { setState(() { mPin.add(digit); errorText = null; }); _bounceController.forward(from: 0); if (mPin.length == 4) { _handleComplete(); } } } void deleteDigit() { if (_shakeController.isAnimating || _waveController.isAnimating) return; if (mPin.isNotEmpty) { setState(() { mPin.removeLast(); errorText = null; }); } } Future _handleComplete() async { if (_shakeController.isAnimating || _waveController.isAnimating) return; final pin = mPin.join(); final storage = SecureStorage(); switch (widget.mode) { case MPinMode.enter: final storedPin = await storage.read('mpin'); // log('storedPin: $storedPin'); if (storedPin == int.tryParse(pin)) { // Correct PIN setState(() { _isSuccess = true; errorText = null; }); await Future.delayed(const Duration(milliseconds: 100)); _waveController.forward(from: 0).whenComplete(() { widget.onCompleted?.call(pin); }); } else { // Incorrect PIN setState(() { _isError = true; errorText = AppLocalizations.of(context).incorrectMPIN; }); await _shakeController.forward(from: 0); setState(() { mPin.clear(); _isError = false; // Keep error text until next digit is entered }); } break; case MPinMode.set: // propagate parent onCompleted into confirm step Navigator.push( context, MaterialPageRoute( builder: (_) => MPinScreen( mode: MPinMode.confirm, initialPin: pin, onCompleted: widget.onCompleted, // <-- use parent callback ), ), ); break; case MPinMode.confirm: if (widget.initialPin == pin) { // 1) persist the pin await storage.write('mpin', pin); // 3) now clear the entire navigation stack and go to your main scaffold if (mounted) { Navigator.of(context, rootNavigator: true).pushAndRemoveUntil( MaterialPageRoute(builder: (_) => const NavigationScaffold()), (route) => false, ); } } else { setState(() { _isError = true; errorText = AppLocalizations.of(context).pinsDoNotMatch; }); await _shakeController.forward(from: 0); setState(() { mPin.clear(); _isError = false; }); } break; } } Widget buildMPinDots(BuildContext context) { return AnimatedBuilder( animation: Listenable.merge( [_bounceController, _shakeController, _waveController]), builder: (context, child) { double shakeOffset = 0; if (_shakeController.isAnimating) { // 4 cycles of sine wave for shake shakeOffset = sin(_shakeController.value * 4 * pi) * 12; } return Transform.translate( offset: Offset(shakeOffset, 0), child: Row( mainAxisAlignment: MainAxisAlignment.center, children: List.generate(4, (index) { final isFilled = index < mPin.length; // Bounce animation for single dot final isAnimatingBounce = index == mPin.length - 1 && _bounceController.isAnimating; double bounceScale = isAnimatingBounce ? _bounceAnimation.value : 1.0; // Success wave animation double waveScale = 1.0; if (_isSuccess) { final waveDelay = index * 0.15; final waveValue = (_waveAnimation.value - waveDelay).clamp(0.0, 1.0); // Grow and shrink waveScale = 1.0 + sin(waveValue * pi) * 0.4; } // Determine dot color Color dotColor; if (_isError) { dotColor = Theme.of(context).colorScheme.error; } else if (_isSuccess) { dotColor = Colors.green.shade600; } else { dotColor = isFilled ? Theme.of(context).colorScheme.onSurfaceVariant : Theme.of(context).colorScheme.surfaceContainerHighest; } return Transform.scale( scale: bounceScale * waveScale, child: Container( margin: const EdgeInsets.all(8), width: 25, height: 25, decoration: BoxDecoration( shape: BoxShape.circle, color: dotColor, ), ), ); }), ), ); }, ); } Widget buildNumberPad() { List> keys = [ ['1', '2', '3'], ['4', '5', '6'], ['7', '8', '9'], ['Enter', '0', '<'], ]; return Column( children: keys.map((row) { return Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: row.map((key) { return Padding( padding: const EdgeInsets.all(1.0), child: GestureDetector( onTap: () { HapticFeedback.lightImpact(); if (key == '<') { deleteDigit(); } else if (key == 'Enter') { if (mPin.length == 4) { _handleComplete(); } else { setState(() { errorText = AppLocalizations.of( context, ).pleaseEnter4Digits; }); } } else if (key.isNotEmpty) { addDigit(key); } }, child: Container( width: 80, height: 80, decoration: const BoxDecoration( shape: BoxShape.circle, ), alignment: Alignment.center, child: key == 'Enter' ? const Icon(Icons.check) : Text( key == '<' ? '⌫' : key, style: TextStyle( fontSize: 30, color: key == 'Enter' ? Theme.of(context).primaryColor : Theme.of(context).colorScheme.onSurface, ), ), ), ), ); }).toList(), ); }).toList(), ); } String getTitle() { switch (widget.mode) { case MPinMode.enter: return AppLocalizations.of(context).enterMPIN; case MPinMode.set: return AppLocalizations.of(context).setMPIN; case MPinMode.confirm: return AppLocalizations.of(context).confirmMPIN; } } @override Widget build(BuildContext context) { return Scaffold( body: SafeArea( child: Column( children: [ const SizedBox(height: 50), // Logo Image.asset('assets/images/logo.png', height: 100), const SizedBox(height: 20), Text( getTitle(), style: const TextStyle(fontSize: 20, fontWeight: FontWeight.w500), ), const Spacer(), buildMPinDots(context), if (errorText != null) Padding( padding: const EdgeInsets.only(top: 8.0), child: Text( errorText!, style: TextStyle(color: Theme.of(context).colorScheme.error), ), ), const Spacer(), buildNumberPad(), ], ), ), ); } }