TF-3487 Create retriable failure state and toast
This commit is contained in:
@@ -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];
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user