implemented correct colors and proper animations in mpin screen.

This commit is contained in:
asif
2025-09-03 23:45:52 +05:30
parent 57439f2acb
commit 3b9b1e507c

View File

@@ -1,12 +1,14 @@
import '../../../l10n/app_localizations.dart';
import 'dart:math';
import 'dart:developer';
// 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 }
@@ -26,22 +28,73 @@ class MPinScreen extends StatefulWidget {
State<MPinScreen> createState() => _MPinScreenState();
}
class _MPinScreenState extends State<MPinScreen> {
class _MPinScreenState extends State<MPinScreen> with TickerProviderStateMixin {
List<String> mPin = [];
String? errorText;
// Animation controllers
late final AnimationController _bounceController;
late final Animation<double> _bounceAnimation;
late final AnimationController _shakeController;
late final AnimationController _waveController;
late final Animation<double> _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<double>(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<double>(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<void> _tryBiometricBeforePin() async {
final storage = getIt<SecureStorage>();
final enabled = await storage.read('biometric_enabled');
log('biometric_enabled: $enabled');
// log('biometric_enabled: $enabled');
if (enabled != null && enabled) {
final auth = LocalAuthentication();
if (await auth.canCheckBiometrics) {
@@ -62,11 +115,13 @@ class _MPinScreenState extends State<MPinScreen> {
}
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();
}
@@ -74,6 +129,7 @@ class _MPinScreenState extends State<MPinScreen> {
}
void deleteDigit() {
if (_shakeController.isAnimating || _waveController.isAnimating) return;
if (mPin.isNotEmpty) {
setState(() {
mPin.removeLast();
@@ -83,19 +139,36 @@ class _MPinScreenState extends State<MPinScreen> {
}
Future<void> _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');
// log('storedPin: $storedPin');
if (storedPin == int.tryParse(pin)) {
widget.onCompleted?.call(pin);
} else {
// 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;
@@ -126,28 +199,81 @@ class _MPinScreenState extends State<MPinScreen> {
}
} else {
setState(() {
_isError = true;
errorText = AppLocalizations.of(context).pinsDoNotMatch;
});
await _shakeController.forward(from: 0);
setState(() {
mPin.clear();
_isError = false;
});
}
break;
}
}
Widget buildMPinDots() {
return Row(
mainAxisAlignment: MainAxisAlignment.center,
children: List.generate(4, (index) {
return Container(
margin: const EdgeInsets.all(8),
width: 15,
height: 15,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: index < mPin.length ? Colors.black : Colors.grey[400],
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,
),
),
);
}),
),
);
}),
},
);
}
@@ -165,9 +291,10 @@ class _MPinScreenState extends State<MPinScreen> {
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: row.map((key) {
return Padding(
padding: const EdgeInsets.all(8.0),
padding: const EdgeInsets.all(1.0),
child: GestureDetector(
onTap: () {
HapticFeedback.lightImpact();
if (key == '<') {
deleteDigit();
} else if (key == 'Enter') {
@@ -185,11 +312,10 @@ class _MPinScreenState extends State<MPinScreen> {
}
},
child: Container(
width: 70,
height: 70,
width: 80,
height: 80,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: Colors.grey[200],
),
alignment: Alignment.center,
child: key == 'Enter'
@@ -197,10 +323,10 @@ class _MPinScreenState extends State<MPinScreen> {
: Text(
key == '<' ? '' : key,
style: TextStyle(
fontSize: 20,
fontSize: 30,
color: key == 'Enter'
? Theme.of(context).primaryColor
: Colors.black,
: Theme.of(context).colorScheme.onSurface,
),
),
),
@@ -229,7 +355,7 @@ class _MPinScreenState extends State<MPinScreen> {
body: SafeArea(
child: Column(
children: [
const Spacer(),
const SizedBox(height: 50),
// Logo
Image.asset('assets/images/logo.png', height: 100),
const SizedBox(height: 20),
@@ -237,19 +363,18 @@ class _MPinScreenState extends State<MPinScreen> {
getTitle(),
style: const TextStyle(fontSize: 20, fontWeight: FontWeight.w500),
),
const SizedBox(height: 20),
buildMPinDots(),
const Spacer(),
buildMPinDots(context),
if (errorText != null)
Padding(
padding: const EdgeInsets.only(top: 8.0),
child: Text(
errorText!,
style: const TextStyle(color: Colors.red),
style: TextStyle(color: Theme.of(context).colorScheme.error),
),
),
const Spacer(),
buildNumberPad(),
const Spacer(),
],
),
),