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';
|
import 'package:equatable/equatable.dart';
|
||||||
|
|
||||||
abstract class Failure with EquatableMixin {
|
abstract class Failure with EquatableMixin {
|
||||||
@@ -8,9 +10,10 @@ abstract class Failure with EquatableMixin {
|
|||||||
|
|
||||||
abstract class FeatureFailure extends Failure {
|
abstract class FeatureFailure extends Failure {
|
||||||
final dynamic exception;
|
final dynamic exception;
|
||||||
|
final Stream<Either<Failure, Success>>? onRetry;
|
||||||
|
|
||||||
FeatureFailure({this.exception});
|
FeatureFailure({this.exception, this.onRetry});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
List<Object?> get props => [exception];
|
List<Object?> get props => [exception, onRetry];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -184,4 +184,122 @@ class AppToast {
|
|||||||
: (duration ?? const Duration(seconds: 3)),
|
: (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);
|
final minInputLength = session.getMinInputLengthAutocomplete(accountId);
|
||||||
return minInputLength?.value.toInt() ?? AppConfig.defaultMinInputLengthAutocomplete;
|
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",
|
"type": "text",
|
||||||
"placeholders_order": [],
|
"placeholders_order": [],
|
||||||
"placeholders": {}
|
"placeholders": {}
|
||||||
|
},
|
||||||
|
"retry": "Retry",
|
||||||
|
"@retry": {
|
||||||
|
"type": "text",
|
||||||
|
"placeholders_order": [],
|
||||||
|
"placeholders": {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -4618,4 +4618,11 @@ class AppLocalizations {
|
|||||||
name: 'viewEntireMessage',
|
name: 'viewEntireMessage',
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
String get retry {
|
||||||
|
return Intl.message(
|
||||||
|
'Retry',
|
||||||
|
name: 'retry',
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user