Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 5 additions & 2 deletions core/lib/presentation/state/failure.dart
Original file line number Diff line number Diff line change
@@ -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 {
Expand All @@ -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 changes: 118 additions & 0 deletions core/lib/presentation/utils/app_toast.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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)),
);
}
}
76 changes: 76 additions & 0 deletions core/test/presentation/utils/app_toast_test.dart
Original file line number Diff line number Diff line change
@@ -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 changes: 33 additions & 0 deletions lib/features/base/base_controller.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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,
);
}
}
3 changes: 2 additions & 1 deletion lib/features/email/domain/state/get_email_content_state.dart
Original file line number Diff line number Diff line change
Expand Up @@ -51,5 +51,6 @@ class GetEmailContentFromCacheSuccess extends UIState {

class GetEmailContentFailure extends FeatureFailure {

GetEmailContentFailure(dynamic exception) : super(exception: exception);
GetEmailContentFailure(dynamic exception, {super.onRetry})
: super(exception: exception);
}
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,17 @@ class GetEmailContentInteractor {
}
} catch (e) {
log('GetEmailContentInteractor::execute(): exception = $e');
yield Left<Failure, Success>(GetEmailContentFailure(e));
yield Left<Failure, Success>(GetEmailContentFailure(
e,
onRetry: execute(
session,
accountId,
emailId,
baseDownloadUrl,
transformConfiguration,
additionalProperties: additionalProperties
),
));
}
}

Expand Down Expand Up @@ -97,7 +107,17 @@ class GetEmailContentInteractor {
}
} catch (e) {
logError('GetEmailContentInteractor::_getContentEmailFromServer():EXCEPTION: $e');
yield Left<Failure, Success>(GetEmailContentFailure(e));
yield Left<Failure, Success>(GetEmailContentFailure(
e,
onRetry: execute(
session,
accountId,
emailId,
baseDownloadUrl,
transformConfiguration,
additionalProperties: additionalProperties
),
));
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ import 'package:jmap_dart_client/jmap/mail/email/keyword_identifier.dart';
import 'package:jmap_dart_client/jmap/mdn/disposition.dart';
import 'package:jmap_dart_client/jmap/mdn/mdn.dart';
import 'package:model/email/eml_attachment.dart';
import 'package:model/error_type_handler/unknown_uri_exception.dart';
import 'package:model/model.dart';
import 'package:permission_handler/permission_handler.dart';
import 'package:pointer_interceptor/pointer_interceptor.dart';
Expand Down Expand Up @@ -332,7 +333,7 @@ class SingleEmailController extends BaseController with AppLoaderMixin {
} else if (failure is ParseCalendarEventFailure) {
_handleParseCalendarEventFailure(failure);
} else if (failure is GetEmailContentFailure) {
emailLoadedViewState.value = Left<Failure, Success>(failure);
_handleGetEmailContentFailure(failure);
} else if (failure is PrintEmailFailure) {
_showMessageWhenEmailPrintingFailed(failure);
} else if (failure is CalendarEventReplyFailure) {
Expand All @@ -348,6 +349,11 @@ class SingleEmailController extends BaseController with AppLoaderMixin {
}
}

void _handleGetEmailContentFailure(GetEmailContentFailure failure) {
emailLoadedViewState.value = Left<Failure, Success>(failure);
showRetryToast(failure);
}

void _registerObxStreamListener() {
ever(mailboxDashBoardController.accountId, (accountId) {
if (accountId is AccountId) {
Expand Down Expand Up @@ -609,7 +615,20 @@ class SingleEmailController extends BaseController with AppLoaderMixin {
));
} catch (e) {
logError('SingleEmailController::_getEmailContentAction(): $e');
consumeState(Stream.value(Left(GetEmailContentFailure(e))));
consumeState(Stream.value(Left(GetEmailContentFailure(
e,
onRetry: e is UnknownUriException
? null
: _getEmailContentInteractor.execute(
session!,
accountId!,
emailId,
session!.getDownloadUrl(jmapUrl: dynamicUrlInterceptors.jmapUrl),
PlatformInfo.isWeb
? TransformConfiguration.forPreviewEmailOnWeb()
: TransformConfiguration.forPreviewEmail(),
),
))));
}
}
}
Expand Down
17 changes: 6 additions & 11 deletions lib/features/mailbox/data/repository/mailbox_repository_impl.dart
Original file line number Diff line number Diff line change
Expand Up @@ -87,17 +87,12 @@ class MailboxRepositoryImpl extends MailboxRepository {
Session session,
{Properties? properties}
) async {
try {
final jmapMailboxResponse = await mapDataSource[DataSourceType.network]!.getAllMailbox(
session,
accountId,
properties: properties);
log('MailboxRepositoryImpl::_getAllMailboxFromJMAP: MAILBOX_NETWORK = ${jmapMailboxResponse.mailboxes.length} | STATE_NETWORK = ${jmapMailboxResponse.state}');
return jmapMailboxResponse;
} catch (e) {
logError('MailboxRepositoryImpl::_getAllMailboxFromJMAP: Exception: $e');
return null;
}
final jmapMailboxResponse = await mapDataSource[DataSourceType.network]!.getAllMailbox(
session,
accountId,
properties: properties);
log('MailboxRepositoryImpl::_getAllMailboxFromJMAP: MAILBOX_NETWORK = ${jmapMailboxResponse.mailboxes.length} | STATE_NETWORK = ${jmapMailboxResponse.state}');
return jmapMailboxResponse;
}

Future<void> _syncNewInCache(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,5 +19,5 @@ class GetAllMailboxSuccess extends UIState {

class GetAllMailboxFailure extends FeatureFailure {

GetAllMailboxFailure(dynamic exception) : super(exception: exception);
GetAllMailboxFailure(dynamic exception, {super.onRetry}) : super(exception: exception);
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,10 @@ class GetAllMailboxInteractor {
properties: properties)
.map(_toGetMailboxState);
} catch (e) {
yield Left<Failure, Success>(GetAllMailboxFailure(e));
yield Left<Failure, Success>(GetAllMailboxFailure(
e,
onRetry: execute(session, accountId, properties: properties),
));
}
}

Expand Down
Loading
Loading