TF-3487 Create retriable failure state and toast

This commit is contained in:
DatDang
2025-02-24 10:37:07 +07:00
committed by Dat H. Pham
parent e595aa02a0
commit 91dddff18d
6 changed files with 245 additions and 2 deletions
+5 -2
View File
@@ -1,3 +1,5 @@
import 'package:core/presentation/state/success.dart';
import 'package:dartz/dartz.dart';
import 'package:equatable/equatable.dart';
abstract class Failure with EquatableMixin {
@@ -8,9 +10,10 @@ abstract class Failure with EquatableMixin {
abstract class FeatureFailure extends Failure {
final dynamic exception;
final Stream<Either<Failure, Success>>? onRetry;
FeatureFailure({this.exception});
FeatureFailure({this.exception, this.onRetry});
@override
List<Object?> get props => [exception];
List<Object?> get props => [exception, onRetry];
}
+118
View File
@@ -184,4 +184,122 @@ class AppToast {
: (duration ?? const Duration(seconds: 3)),
);
}
void showToastMessageWithMultipleActions(
BuildContext context,
String message,
{
List<({
String? actionName,
Function? onActionClick,
Widget? actionIcon
})> actions = const [],
Widget? leadingIcon,
String? leadingSVGIcon,
Color? leadingSVGIconColor,
double? maxWidth,
bool infinityToast = false,
Color? backgroundColor,
Color? textColor,
Color? textActionColor,
TextStyle? textStyle,
EdgeInsets? padding,
TextAlign? textAlign,
Duration? duration,
}
) {
final responsiveUtils = Get.find<ResponsiveUtils>();
List<Widget> trailingWidgets = [];
for (var action in actions) {
if (action.actionName == null) continue;
if (action.actionIcon == null) {
trailingWidgets.add(PointerInterceptor(
child: TextButton(
onPressed: () {
ToastView.dismiss();
action.onActionClick?.call();
},
child: Text(
action.actionName!,
style: TextStyle(
fontSize: 15,
fontWeight: FontWeight.normal,
color: textActionColor ?? Colors.white
),
),
),
));
} else {
trailingWidgets.add(PointerInterceptor(
child: Material(
color: Colors.transparent,
child: InkWell(
onTap: () {
ToastView.dismiss();
action.onActionClick?.call();
},
customBorder: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(5),
),
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 6, horizontal: 8),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
action.actionIcon!,
Text(
action.actionName!,
style: TextStyle(
fontSize: 15,
fontWeight: FontWeight.normal,
color: textActionColor ?? Colors.white
),
)
],
),
),
),
),
));
}
}
Widget? leadingWidget;
if (leadingIcon != null) {
leadingWidget = PointerInterceptor(child: leadingIcon);
} else {
if (leadingSVGIcon != null) {
leadingWidget = PointerInterceptor(
child: SvgPicture.asset(
leadingSVGIcon,
width: 24,
height: 24,
fit: BoxFit.fill,
colorFilter: leadingSVGIconColor?.asFilter(),
)
);
}
}
TMailToast.showToast(
message,
context,
maxWidth: maxWidth ?? responsiveUtils.getMaxWidthToast(context),
toastPosition: ToastPosition.BOTTOM,
textStyle: textStyle ?? TextStyle(
fontSize: 15,
fontWeight: FontWeight.normal,
color: textColor ?? AppColor.primaryColor
),
backgroundColor: backgroundColor ?? Colors.white,
trailing: Row(children: trailingWidgets),
leading: leadingWidget,
padding: padding,
textAlign: textAlign,
toastDuration: infinityToast
? null
: (duration ?? const Duration(seconds: 3)),
);
}
}
@@ -0,0 +1,76 @@
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:get/get.dart';
import 'package:mockito/mockito.dart';
import 'package:core/presentation/utils/app_toast.dart';
import 'package:core/presentation/utils/responsive_utils.dart';
class MockResponsiveUtils extends Mock implements ResponsiveUtils {
@override
double getMaxWidthToast(BuildContext context) => 300;
}
void main() {
group('AppToast - showToastMessageWithMultipleActions', () {
late AppToast appToast;
late MockResponsiveUtils mockResponsiveUtils;
setUp(() {
appToast = AppToast();
mockResponsiveUtils = MockResponsiveUtils();
Get.put<ResponsiveUtils>(mockResponsiveUtils);
});
tearDown(() {
Get.reset();
});
testWidgets(
'should show action button '
'when actionName is provided and trigger callback when tapped',
(WidgetTester tester) async {
await tester.runAsync(() async {
var callbackTriggered = false;
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: Builder(
builder: (context) {
return TextButton(
onPressed: () {
appToast.showToastMessageWithMultipleActions(
context,
'Test message',
actions: [
(
actionName: 'Retry',
onActionClick: () {
callbackTriggered = true;
},
actionIcon: null,
),
],
);
},
child: const Text('Show toast'),
);
},
),
),
),
);
await tester.tap(find.text('Show toast'));
await tester.pump();
expect(find.text('Retry'), findsOneWidget);
await tester.tap(find.text('Retry'));
await tester.pump();
expect(callbackTriggered, true);
});
});
});
}
+33
View File
@@ -617,4 +617,37 @@ abstract class BaseController extends GetxController
final minInputLength = session.getMinInputLengthAutocomplete(accountId);
return minInputLength?.value.toInt() ?? AppConfig.defaultMinInputLengthAutocomplete;
}
void showRetryToast(FeatureFailure failure) {
if (currentOverlayContext == null || currentContext == null) return;
final exception = failure.exception;
final errorMessage = exception is MethodLevelErrors && exception.message != null
? AppLocalizations.of(currentContext!).unexpectedError('${exception.message!}')
: AppLocalizations.of(currentContext!).unknownError;
appToast.showToastMessageWithMultipleActions(
currentOverlayContext!,
errorMessage,
actions: [
if (failure.onRetry != null)
(
actionName: AppLocalizations.of(currentContext!).retry,
onActionClick: () => consumeState(failure.onRetry!),
actionIcon: SvgPicture.asset(imagePaths.icUndo),
),
(
actionName: AppLocalizations.of(currentContext!).close,
onActionClick: () => ToastView.dismiss(),
actionIcon: SvgPicture.asset(
imagePaths.icClose,
colorFilter: Colors.white.asFilter(),
),
)
],
backgroundColor: AppColor.toastErrorBackgroundColor,
textColor: Colors.white,
infinityToast: true,
);
}
}
+6
View File
@@ -4393,5 +4393,11 @@
"type": "text",
"placeholders_order": [],
"placeholders": {}
},
"retry": "Retry",
"@retry": {
"type": "text",
"placeholders_order": [],
"placeholders": {}
}
}
@@ -4618,4 +4618,11 @@ class AppLocalizations {
name: 'viewEntireMessage',
);
}
String get retry {
return Intl.message(
'Retry',
name: 'retry',
);
}
}