diff --git a/packages/timer_button/example/lib/app.dart b/packages/timer_button/example/lib/app.dart new file mode 100644 index 0000000..4fbf358 --- /dev/null +++ b/packages/timer_button/example/lib/app.dart @@ -0,0 +1,19 @@ +import 'package:flutter/material.dart'; + +import 'home_page.dart'; +import 'theme.dart'; + +class TimerButtonApp extends StatelessWidget { + const TimerButtonApp({super.key}); + + @override + Widget build(BuildContext context) { + return MaterialApp( + title: 'Timer Button Demo', + theme: AppTheme.light, + darkTheme: AppTheme.dark, + home: const HomePage(), + debugShowCheckedModeBanner: false, + ); + } +} diff --git a/packages/timer_button/example/lib/home_page.dart b/packages/timer_button/example/lib/home_page.dart new file mode 100644 index 0000000..a1c5e5a --- /dev/null +++ b/packages/timer_button/example/lib/home_page.dart @@ -0,0 +1,70 @@ +import 'dart:developer'; + +import 'package:flutter/material.dart'; +import 'package:timer_button/timer_button.dart'; + +class HomePage extends StatelessWidget { + const HomePage({super.key}); + + @override + Widget build(BuildContext context) { + final colorScheme = Theme.of(context).colorScheme; + + return Scaffold( + appBar: AppBar( + title: const Text('Timer Button Demo'), + centerTitle: true, + ), + body: Center( + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + TimerButton( + label: 'Try Again', + timeOutInSeconds: 5, + onPressed: () { + log('Time for some action!'); + }, + ), + TimerButton( + label: 'Outlined: Try Again', + timeOutInSeconds: 5, + onPressed: () {}, + buttonType: ButtonType.outlinedButton, + disabledColor: colorScheme.error, + color: colorScheme.primary, + activeTextStyle: TextStyle(color: colorScheme.primary), + disabledTextStyle: TextStyle(color: colorScheme.error), + ), + TimerButton( + label: 'Text: Try Again', + timeOutInSeconds: 5, + onPressed: () { + log('Time for some action!'); + }, + timeUpFlag: true, + buttonType: ButtonType.textButton, + disabledColor: colorScheme.errorContainer, + color: colorScheme.primaryContainer, + ), + TimerButton.builder( + builder: (context, timeLeft) { + return Text( + 'Custom: $timeLeft', + style: TextStyle(color: colorScheme.tertiary), + ); + }, + onPressed: () { + log('Time for some action!'); + }, + timeOutInSeconds: 5, + ), + ], + ), + ), + ), + ); + } +} diff --git a/packages/timer_button/example/lib/main.dart b/packages/timer_button/example/lib/main.dart index 995d152..bb6d913 100644 --- a/packages/timer_button/example/lib/main.dart +++ b/packages/timer_button/example/lib/main.dart @@ -1,94 +1,5 @@ -import 'dart:developer'; - import 'package:flutter/material.dart'; -import 'package:timer_button/timer_button.dart'; - -void main() => runApp(const MyApp()); - -class MyApp extends StatelessWidget { - const MyApp({super.key}); - - @override - Widget build(BuildContext context) { - return MaterialApp( - title: 'Timer Button Demo', - theme: ThemeData( - primarySwatch: Colors.blue, - ), - home: const MyHomePage(), - debugShowCheckedModeBanner: false, - ); - } -} - -class MyHomePage extends StatefulWidget { - const MyHomePage({super.key}); - @override - MyHomePageState createState() { - return MyHomePageState(); - } -} +import 'app.dart'; -class MyHomePageState extends State with TickerProviderStateMixin { - @override - Widget build(BuildContext context) { - return Scaffold( - appBar: AppBar( - title: const Text('Timer Button Demo'), - ), - body: Material( - child: Center( - child: Padding( - padding: const EdgeInsets.all(8.0), - child: Column( - mainAxisAlignment: MainAxisAlignment.spaceEvenly, - children: [ - TimerButton( - label: "Try Again", - timeOutInSeconds: 5, - onPressed: () { - log("Time for some action!"); - }, - ), - TimerButton( - label: "Outlined: Try Again", - timeOutInSeconds: 5, - onPressed: () {}, - buttonType: ButtonType.outlinedButton, - disabledColor: Colors.deepOrange, - color: Colors.green, - activeTextStyle: const TextStyle(color: Colors.yellow), - disabledTextStyle: const TextStyle(color: Colors.pink), - ), - TimerButton( - label: "Text: Try Again", - timeOutInSeconds: -5, - onPressed: () { - log("Time for some action!"); - }, - timeUpFlag: true, - buttonType: ButtonType.textButton, - disabledColor: Colors.deepOrange, - color: Colors.green, - ), - TimerButton.builder( - builder: (context, timeLeft) { - return Text( - "Custom: $timeLeft", - style: const TextStyle(color: Colors.red), - ); - }, - onPressed: () { - log("Time for some action!"); - }, - timeOutInSeconds: 5, - ), - ], - ), - ), - ), - ), - ); - } -} +void main() => runApp(const TimerButtonApp()); diff --git a/packages/timer_button/example/lib/theme.dart b/packages/timer_button/example/lib/theme.dart new file mode 100644 index 0000000..85371e1 --- /dev/null +++ b/packages/timer_button/example/lib/theme.dart @@ -0,0 +1,15 @@ +import 'package:flutter/material.dart'; + +class AppTheme { + static ThemeData get light => ThemeData( + useMaterial3: true, + colorSchemeSeed: Colors.indigo, + brightness: Brightness.light, + ); + + static ThemeData get dark => ThemeData( + useMaterial3: true, + colorSchemeSeed: Colors.indigo, + brightness: Brightness.dark, + ); +} diff --git a/packages/timer_button/lib/timer_button.dart b/packages/timer_button/lib/timer_button.dart index 770ba8e..163fdc0 100644 --- a/packages/timer_button/lib/timer_button.dart +++ b/packages/timer_button/lib/timer_button.dart @@ -115,12 +115,15 @@ class _TimerButtonState extends State with SafeStateMixin { int _timeCounter = 0; Timer? _timer; + int get _safeTimeout => + widget.timeOutInSeconds < 0 ? 0 : widget.timeOutInSeconds; + String get _timerText => '$_timeCounter${widget.secPostFix}'; @override void initState() { super.initState(); - _timeCounter = widget.timeOutInSeconds; + _timeCounter = _safeTimeout; _timeUpFlag = widget.timeUpFlag; WidgetsBinding.instance.addPostFrameCallback((_) { if (_timeCounter <= 0) { @@ -139,13 +142,14 @@ class _TimerButtonState extends State with SafeStateMixin { } void _updateTime() { - if (_timeUpFlag) { + if (_timeUpFlag || _timeCounter <= 0) { return; } - _timer = Timer(const Duration(seconds: aSec), () async { + _timer = Timer(const Duration(seconds: aSec), () { if (!mounted) return; _timeCounter--; if (_timeCounter <= 0) { + _timeCounter = 0; _timeUpFlag = true; } safeSetState(); @@ -157,10 +161,10 @@ class _TimerButtonState extends State with SafeStateMixin { void _onPressed() { widget.onPressed(); - // reset the timer when the button is pressed if (widget.resetTimerOnPressed) { + _timer?.cancel(); _timeUpFlag = false; - _timeCounter = widget.timeOutInSeconds; + _timeCounter = _safeTimeout; safeSetState(); _updateTime(); } diff --git a/packages/timer_button/test/timer_button_test.dart b/packages/timer_button/test/timer_button_test.dart index 77655b6..0b63f8b 100644 --- a/packages/timer_button/test/timer_button_test.dart +++ b/packages/timer_button/test/timer_button_test.dart @@ -562,6 +562,69 @@ void main() { expect(button.onPressed, isNotNull); expect(find.text('Negative'), findsOneWidget); }); + + testWidgets( + 'should never display negative counter values with negative timeOutInSeconds', + (tester) async { + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: TimerButton( + label: 'NegInput', + onPressed: () {}, + timeOutInSeconds: -5, + ), + ), + ), + ); + + // Counter should be clamped to 0, not -5 + expect(find.text('NegInput | 0s'), findsOneWidget); + + // After frame callback, button should be enabled immediately + await tester.pump(); + final button = + tester.widget(find.byType(ElevatedButton)); + expect(button.onPressed, isNotNull); + expect(find.text('NegInput'), findsOneWidget); + }); + + testWidgets( + 'should clamp counter to 0 and not show negative values during countdown', + (tester) async { + int lastSeenSeconds = 999; + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: TimerButton.builder( + builder: (context, seconds) { + lastSeenSeconds = seconds; + return Text('Time: $seconds'); + }, + onPressed: () {}, + timeOutInSeconds: 2, + ), + ), + ), + ); + + expect(lastSeenSeconds, 2); + + // Tick past the timeout + await tester.pump(const Duration(seconds: 1)); + await tester.pump(); + expect(lastSeenSeconds, 1); + + await tester.pump(const Duration(seconds: 1)); + await tester.pump(); + expect(lastSeenSeconds, 0); + + // Pump additional seconds — counter should stay at 0 + await tester.pump(const Duration(seconds: 1)); + await tester.pump(); + expect(lastSeenSeconds, 0); + expect(lastSeenSeconds >= 0, isTrue); + }); }); });