From 6c866f3c22c815cb085b9c00f62875ae6480f8a8 Mon Sep 17 00:00:00 2001 From: Anitta Babu <99161914+anitta-keyvalue@users.noreply.github.com> Date: Fri, 5 Apr 2024 10:23:08 +0530 Subject: [PATCH 01/17] refactor: Add separate widget for inbox icon badge --- example/.gitignore | 2 + lib/src/models/ui_models.dart | 2 +- lib/src/widgets/icon_badge.dart | 49 + lib/src/widgets/siren_inbox_icon.dart | 38 +- pubspec.yaml | 4 +- test/data/siren_data_provider_test.dart | 45 + test/data/siren_data_provider_test.mocks.dart | 106 ++ test/models/api_response_test.dart | 71 + test/models/notification_model_test.dart | 102 ++ test/{ => models}/ui_models_test.dart | 26 + ...nviewed_notification_count_model_test.dart | 0 test/services/api_client_test.dart | 199 +++ test/services/api_client_test.mocks.dart | 1219 +++++++++++++++++ test/services/network_service_test.dart | 39 + test/services/network_service_test.mocks.dart | 269 ++++ test/siren_inbox_icon_test.dart | 118 -- test/utils/common_utils_test.dart | 81 +- test/{ => widgets}/card_test.dart | 0 test/{ => widgets}/empty_widget_test.dart | 0 test/{ => widgets}/error_widget_test.dart | 0 test/widgets/icon_badge_test.dart | 78 ++ test/{ => widgets}/loader_widget_test.dart | 0 test/widgets/notification_list_view_test.dart | 111 ++ test/{ => widgets}/nullabale_text_test.dart | 0 test/widgets/siren_inbox_icon_test.dart | 216 +++ test/widgets/siren_inbox_icon_test.mocks.dart | 238 ++++ test/widgets/siren_inbox_test.dart | 207 +++ test/widgets/siren_inbox_test.mocks.dart | 263 ++++ 28 files changed, 3268 insertions(+), 215 deletions(-) create mode 100644 lib/src/widgets/icon_badge.dart create mode 100644 test/data/siren_data_provider_test.dart create mode 100644 test/data/siren_data_provider_test.mocks.dart create mode 100644 test/models/api_response_test.dart create mode 100644 test/models/notification_model_test.dart rename test/{ => models}/ui_models_test.dart (80%) rename test/{ => models}/unviewed_notification_count_model_test.dart (100%) create mode 100644 test/services/api_client_test.dart create mode 100644 test/services/api_client_test.mocks.dart create mode 100644 test/services/network_service_test.dart create mode 100644 test/services/network_service_test.mocks.dart delete mode 100644 test/siren_inbox_icon_test.dart rename test/{ => widgets}/card_test.dart (100%) rename test/{ => widgets}/empty_widget_test.dart (100%) rename test/{ => widgets}/error_widget_test.dart (100%) create mode 100644 test/widgets/icon_badge_test.dart rename test/{ => widgets}/loader_widget_test.dart (100%) create mode 100644 test/widgets/notification_list_view_test.dart rename test/{ => widgets}/nullabale_text_test.dart (100%) create mode 100644 test/widgets/siren_inbox_icon_test.dart create mode 100644 test/widgets/siren_inbox_icon_test.mocks.dart create mode 100644 test/widgets/siren_inbox_test.dart create mode 100644 test/widgets/siren_inbox_test.mocks.dart diff --git a/example/.gitignore b/example/.gitignore index 29a3a50..7bfa1cd 100644 --- a/example/.gitignore +++ b/example/.gitignore @@ -41,3 +41,5 @@ app.*.map.json /android/app/debug /android/app/profile /android/app/release + +pubspec.lock diff --git a/lib/src/models/ui_models.dart b/lib/src/models/ui_models.dart index 010c0f5..2b8d3c4 100644 --- a/lib/src/models/ui_models.dart +++ b/lib/src/models/ui_models.dart @@ -33,7 +33,7 @@ class DefaultIconStyle { static double get defaultInset => 1; /// Default size for the badge count. - static double get defaultSize => 18; + static double get defaultSize => 20; /// Default top position for the badge count. static double get defaultTop => 0; diff --git a/lib/src/widgets/icon_badge.dart b/lib/src/widgets/icon_badge.dart new file mode 100644 index 0000000..057fceb --- /dev/null +++ b/lib/src/widgets/icon_badge.dart @@ -0,0 +1,49 @@ +import 'package:flutter/material.dart'; +import 'package:sirenapp_flutter_inbox/sirenapp_flutter_inbox.dart'; + +class IconBadge extends StatelessWidget { + const IconBadge({ + required this.badgeStyle, + required this.notificationsCount, + required this.hideBadge, + super.key, + }); + + final BadgeStyle? badgeStyle; + final int notificationsCount; + final bool hideBadge; + + @override + Widget build(BuildContext context) { + final currentTheme = Theme.of(context); + return hideBadge + ? const SizedBox() + : Positioned( + right: badgeStyle?.right ?? DefaultIconStyle.defaultRight, + top: badgeStyle?.top ?? DefaultIconStyle.defaultTop, + child: Container( + width: badgeStyle?.size ?? DefaultIconStyle.defaultSize, + height: badgeStyle?.size ?? DefaultIconStyle.defaultSize, + padding: EdgeInsets.all( + badgeStyle?.inset ?? DefaultIconStyle.defaultInset, + ), + decoration: BoxDecoration( + shape: BoxShape.circle, + color: currentTheme.colorScheme.tertiaryContainer, + ), + child: Align( + child: Text( + notificationsCount > 99 + ? '99+' + : notificationsCount.toString(), + style: TextStyle( + color: currentTheme.colorScheme.onTertiary, + fontSize: badgeStyle?.fontSize ?? + DefaultIconStyle.defaultFontSize, + ), + ), + ), + ), + ); + } +} diff --git a/lib/src/widgets/siren_inbox_icon.dart b/lib/src/widgets/siren_inbox_icon.dart index 5f4df8c..1e64f3c 100644 --- a/lib/src/widgets/siren_inbox_icon.dart +++ b/lib/src/widgets/siren_inbox_icon.dart @@ -7,6 +7,7 @@ import 'package:sirenapp_flutter_inbox/src/api/fetch_unviewed_notification_count import 'package:sirenapp_flutter_inbox/src/constants/generics.dart'; import 'package:sirenapp_flutter_inbox/src/data/siren_data_provider.dart'; import 'package:sirenapp_flutter_inbox/src/theme/app_theme.dart'; +import 'package:sirenapp_flutter_inbox/src/widgets/icon_badge.dart'; /// Widget representing the inbox icon. class SirenInboxIcon extends StatefulWidget { @@ -208,8 +209,12 @@ class _SirenInboxIconState extends State { color: currentTheme.colorScheme.onPrimary, ), ), - if (_notificationsCount > 0 && !(widget.hideBadge ?? false)) - _getBadge(context), + IconBadge( + hideBadge: + _notificationsCount == 0 || (widget.hideBadge ?? false), + badgeStyle: widget.customStyles?.badgeStyle, + notificationsCount: _notificationsCount, + ), ], ), ), @@ -218,33 +223,4 @@ class _SirenInboxIconState extends State { ), ); } - - Widget _getBadge(BuildContext context) { - final badgeStyle = widget.customStyles?.badgeStyle; - final currentTheme = Theme.of(context); - return Positioned( - right: badgeStyle?.right ?? DefaultIconStyle.defaultRight, - top: badgeStyle?.top ?? DefaultIconStyle.defaultTop, - child: Container( - width: badgeStyle?.size ?? DefaultIconStyle.defaultSize, - height: badgeStyle?.size ?? DefaultIconStyle.defaultSize, - padding: - EdgeInsets.all(badgeStyle?.inset ?? DefaultIconStyle.defaultInset), - decoration: BoxDecoration( - shape: BoxShape.circle, - color: currentTheme.colorScheme.tertiaryContainer, - ), - child: Align( - child: Text( - _notificationsCount > 99 ? '99+' : _notificationsCount.toString(), - style: TextStyle( - color: currentTheme.colorScheme.onTertiary, - fontSize: - badgeStyle?.fontSize ?? DefaultIconStyle.defaultFontSize, - ), - ), - ), - ), - ); - } } diff --git a/pubspec.yaml b/pubspec.yaml index 24b5e4e..7357cfd 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -17,7 +17,7 @@ environment: dependencies: flutter: sdk: flutter - dio: '>=5.2.0 <=5.4.1' + dio: '>=5.2.0 <=5.4.2' dev_dependencies: flutter_test: @@ -26,6 +26,8 @@ dev_dependencies: very_good_analysis: ^5.1.0 husky: ^0.1.7 mockito: ^5.1.0 + network_image_mock: ^2.0.1 + build_runner: ^2.1.11 # For information on the generic Dart part of this file, see the # following page: https://dart.dev/tools/pub/pubspec diff --git a/test/data/siren_data_provider_test.dart b/test/data/siren_data_provider_test.dart new file mode 100644 index 0000000..1110e3e --- /dev/null +++ b/test/data/siren_data_provider_test.dart @@ -0,0 +1,45 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:mockito/annotations.dart'; +import 'package:sirenapp_flutter_inbox/src/api/verify_token.dart'; +import 'package:sirenapp_flutter_inbox/src/data/siren_data_provider.dart'; + +@GenerateNiceMocks([ + MockSpec(), +]) +void main() { + late SirenDataProvider sirenDataProvider; + + setUp(() { + sirenDataProvider = SirenDataProvider.instance..initialize(); + }); + + group('SirenDataProvider', () { + test('UpdateParams updates user token and recipient ID', () async { + // Perform updateParams + sirenDataProvider.updateParams( + userToken: 'token', + recipientId: 'recipientId', + ); + + // Verify that user token and recipient ID are updated correctly + expect(sirenDataProvider.userToken, 'token'); + expect(sirenDataProvider.recipientId, 'recipientId'); + }); + + test('IconDispose closes icon controller', () { + // Perform icon disposal + sirenDataProvider.iconDispose(); + + // Verify that icon controller is closed + expect(sirenDataProvider.iconController.isClosed, false); + }); + + test('InboxDispose closes inbox controller', () { + // Perform inbox disposal + sirenDataProvider.inboxDispose(); + + // Verify that inbox controller is closed + expect(sirenDataProvider.inboxController.isClosed, false); + }); + }); +} diff --git a/test/data/siren_data_provider_test.mocks.dart b/test/data/siren_data_provider_test.mocks.dart new file mode 100644 index 0000000..f4a1d01 --- /dev/null +++ b/test/data/siren_data_provider_test.mocks.dart @@ -0,0 +1,106 @@ +// Mocks generated by Mockito 5.4.4 from annotations +// in sirenapp_flutter_inbox/test/data/siren_data_provider_test.dart. +// Do not manually edit this file. + +// ignore_for_file: no_leading_underscores_for_library_prefixes +import 'dart:async' as _i6; + +import 'package:mockito/mockito.dart' as _i1; +import 'package:sirenapp_flutter_inbox/src/api/verify_token.dart' as _i4; +import 'package:sirenapp_flutter_inbox/src/constants/generics.dart' as _i5; +import 'package:sirenapp_flutter_inbox/src/models/api_response.dart' as _i3; +import 'package:sirenapp_flutter_inbox/src/services/api_client.dart' as _i2; + +// ignore_for_file: type=lint +// ignore_for_file: avoid_redundant_argument_values +// ignore_for_file: avoid_setters_without_getters +// ignore_for_file: comment_references +// ignore_for_file: deprecated_member_use +// ignore_for_file: deprecated_member_use_from_same_package +// ignore_for_file: implementation_imports +// ignore_for_file: invalid_use_of_visible_for_testing_member +// ignore_for_file: prefer_const_constructors +// ignore_for_file: unnecessary_parenthesis +// ignore_for_file: camel_case_types +// ignore_for_file: subtype_of_sealed_class + +class _FakeApiClient_0 extends _i1.SmartFake implements _i2.ApiClient { + _FakeApiClient_0( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} + +class _FakeApiResponse_1 extends _i1.SmartFake implements _i3.ApiResponse { + _FakeApiResponse_1( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} + +/// A class which mocks [VerifyToken]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockVerifyToken extends _i1.Mock implements _i4.VerifyToken { + @override + _i2.ApiClient get api => (super.noSuchMethod( + Invocation.getter(#api), + returnValue: _FakeApiClient_0( + this, + Invocation.getter(#api), + ), + returnValueForMissingStub: _FakeApiClient_0( + this, + Invocation.getter(#api), + ), + ) as _i2.ApiClient); + + @override + set api(_i2.ApiClient? _api) => super.noSuchMethod( + Invocation.setter( + #api, + _api, + ), + returnValueForMissingStub: null, + ); + + @override + _i5.Status convertJsonToVerificationStatus(dynamic response) => + (super.noSuchMethod( + Invocation.method( + #convertJsonToVerificationStatus, + [response], + ), + returnValue: _i5.Status.PENDING, + returnValueForMissingStub: _i5.Status.PENDING, + ) as _i5.Status); + + @override + _i6.Future<_i3.ApiResponse> verifyToken() => (super.noSuchMethod( + Invocation.method( + #verifyToken, + [], + ), + returnValue: _i6.Future<_i3.ApiResponse>.value(_FakeApiResponse_1( + this, + Invocation.method( + #verifyToken, + [], + ), + )), + returnValueForMissingStub: + _i6.Future<_i3.ApiResponse>.value(_FakeApiResponse_1( + this, + Invocation.method( + #verifyToken, + [], + ), + )), + ) as _i6.Future<_i3.ApiResponse>); +} diff --git a/test/models/api_response_test.dart b/test/models/api_response_test.dart new file mode 100644 index 0000000..5831d22 --- /dev/null +++ b/test/models/api_response_test.dart @@ -0,0 +1,71 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:sirenapp_flutter_inbox/src/models/api_response.dart'; + +void main() { + group('ApiResponse', () { + test('fromJson() should parse JSON correctly', () { + final json = { + 'data': 'testData', + 'error': {'errorCode': '123', 'message': 'Error message'}, + 'meta': { + 'last': 'last', + 'totalPages': '5', + 'pageSize': '10', + 'currentPage': '1', + 'first': 'first', + 'totalElements': '50', + }, + }; + final response = ApiResponse.fromJson(json); + + expect(response.data, 'testData'); + expect(response.error?.errorCode, '123'); + expect(response.meta?.last, 'last'); + expect(response.meta?.totalPages, 5); + }); + + test('Initial values are set correctly', () { + final response = ApiResponse(); + + expect(response.isLoading, true); + expect(response.isSuccess, false); + expect(response.isError, false); + }); + }); + + group('MetaResponse', () { + test('fromJson() should parse JSON correctly', () { + final json = { + 'last': 'last', + 'totalPages': '5', + 'pageSize': '10', + 'currentPage': '1', + 'first': 'first', + 'totalElements': '50', + }; + final meta = MetaResponse.fromJson(json); + + expect(meta.last, 'last'); + expect(meta.totalPages, 5); + expect(meta.pageSize, 10); + expect(meta.currentPage, 1); + expect(meta.first, 'first'); + expect(meta.totalElements, 50); + }); + }); + + group('ApiErrorDetails', () { + test('fromJson() should parse JSON correctly', () { + final json = { + 'errorCode': '123', + 'message': 'Error message', + }; + final errorDetails = ApiErrorDetails.fromJson(json); + + expect(errorDetails.errorCode, '123'); + expect(errorDetails.message, 'Error message'); + }); + }); + + // Similar tests can be written for DioResponse and StreamResponse classes +} diff --git a/test/models/notification_model_test.dart b/test/models/notification_model_test.dart new file mode 100644 index 0000000..67a7839 --- /dev/null +++ b/test/models/notification_model_test.dart @@ -0,0 +1,102 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:sirenapp_flutter_inbox/src/models/notification_model.dart'; + +void main() { + group('NotificationDataType', () { + test('fromJson() should parse JSON correctly', () { + final json = { + 'id': 'notificationId', + 'createdAt': '2022-01-01T00:00:00Z', + 'message': { + 'channel': 'channel', + 'header': 'header', + 'subHeader': 'subHeader', + 'body': 'body', + 'actionUrl': 'actionUrl', + 'avatar': {'imageUrl': 'avatarUrl', 'altText': 'altText'}, + 'additionalData': 'additionalData', + }, + 'requestId': 'requestId', + 'isRead': true, + 'cardColor': Colors.blue, + }; + final notification = NotificationDataType.fromJson(json); + + expect(notification.id, 'notificationId'); + expect(notification.createdAt, '2022-01-01T00:00:00Z'); + expect(notification.requestId, 'requestId'); + expect(notification.isRead, true); + expect(notification.cardColor, Colors.blue); + expect(notification.message.channel, 'channel'); + expect(notification.message.header, 'header'); + expect(notification.message.subHeader, 'subHeader'); + expect(notification.message.body, 'body'); + expect(notification.message.actionUrl, 'actionUrl'); + expect(notification.message.avatar?.url, 'avatarUrl'); + expect(notification.message.avatar?.altText, 'altText'); + expect(notification.message.additionalData, 'additionalData'); + }); + + test('markAsRead() should mark the notification as read', () { + final notification = NotificationDataType( + id: 'notificationId', + createdAt: '2022-01-01T00:00:00Z', + message: MessageData( + channel: 'channel', + header: 'header', + subHeader: 'subHeader', + body: 'body', + actionUrl: 'actionUrl', + avatar: AvatarData(url: 'avatarUrl', altText: 'altText'), + additionalData: 'additionalData', + ), + requestId: 'requestId', + isRead: false, + cardColor: Colors.blue, + ); + + expect(notification.isRead, false); + expect(notification.cardColor, Colors.blue); + + notification.markAsRead(); + + expect(notification.isRead, true); + expect(notification.cardColor, Colors.transparent); + }); + }); + + group('MessageData', () { + test('fromJson() should parse JSON correctly', () { + final json = { + 'channel': 'channel', + 'header': 'header', + 'subHeader': 'subHeader', + 'body': 'body', + 'actionUrl': 'actionUrl', + 'avatar': {'imageUrl': 'avatarUrl', 'altText': 'altText'}, + 'additionalData': 'additionalData', + }; + final message = MessageData.fromJson(json); + + expect(message.channel, 'channel'); + expect(message.header, 'header'); + expect(message.subHeader, 'subHeader'); + expect(message.body, 'body'); + expect(message.actionUrl, 'actionUrl'); + expect(message.avatar?.url, 'avatarUrl'); + expect(message.avatar?.altText, 'altText'); + expect(message.additionalData, 'additionalData'); + }); + }); + + group('AvatarData', () { + test('fromJson() should parse JSON correctly', () { + final json = {'imageUrl': 'avatarUrl', 'altText': 'altText'}; + final avatar = AvatarData.fromJson(json); + + expect(avatar.url, 'avatarUrl'); + expect(avatar.altText, 'altText'); + }); + }); +} diff --git a/test/ui_models_test.dart b/test/models/ui_models_test.dart similarity index 80% rename from test/ui_models_test.dart rename to test/models/ui_models_test.dart index 66c7390..ccaf27d 100644 --- a/test/ui_models_test.dart +++ b/test/models/ui_models_test.dart @@ -60,9 +60,19 @@ void main() { group('DefaultIconStyle', () { test('iconSize should return default size for the notification icon', () { // Arrange & Act + final defaultFontSize = DefaultIconStyle.defaultFontSize; + final defaultInset = DefaultIconStyle.defaultInset; + final defaultSize = DefaultIconStyle.defaultSize; + final defaultTop = DefaultIconStyle.defaultTop; + final defaultRight = DefaultIconStyle.defaultRight; final iconSize = DefaultIconStyle.iconSize; // Assert + expect(defaultFontSize, 10); + expect(defaultInset, 1); + expect(defaultSize, 20); + expect(defaultTop, 0); + expect(defaultRight, 2); expect(iconSize, 35); }); }); @@ -109,4 +119,20 @@ void main() { expect(customThemeColors.badgeColor, Colors.red); }); }); + + test('Card Params', () { + // Arrange + const hideAvatar = true; + const Widget deleteWidget = Icon(Icons.delete); + + // Act + final cardParams = CardParams( + hideAvatar: hideAvatar, + deleteWidget: deleteWidget, + ); + + // Assert + expect(cardParams.hideAvatar, hideAvatar); + expect(cardParams.deleteWidget, deleteWidget); + }); } diff --git a/test/unviewed_notification_count_model_test.dart b/test/models/unviewed_notification_count_model_test.dart similarity index 100% rename from test/unviewed_notification_count_model_test.dart rename to test/models/unviewed_notification_count_model_test.dart diff --git a/test/services/api_client_test.dart b/test/services/api_client_test.dart new file mode 100644 index 0000000..f89bc69 --- /dev/null +++ b/test/services/api_client_test.dart @@ -0,0 +1,199 @@ +import 'package:dio/dio.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mockito/annotations.dart'; +import 'package:mockito/mockito.dart'; +import 'package:sirenapp_flutter_inbox/src/data/siren_data_provider.dart'; +import 'package:sirenapp_flutter_inbox/src/services/api_client.dart'; + +import 'api_client_test.mocks.dart'; + +@GenerateNiceMocks([ + MockSpec(), + MockSpec(), +]) +void main() { + group('ApiClient', () { + late ApiClient apiClient; + late MockDio mockDio; + late MockSirenDataProvider mockSirenDataProvider; + + setUp(() { + mockDio = MockDio(); + mockSirenDataProvider = MockSirenDataProvider(); + apiClient = ApiClient(mockDio); + }); + + test('Test server error ', () { + final apiClient = ApiClient(Dio()); + final response = Response( + statusCode: 500, + requestOptions: RequestOptions(), + ); + final result = apiClient.isServerError(response); + expect( + result, + true, + ); // Expect true because status code is in server error range + }); + + test('GET request', () async { + // Mock response data + final responseData = {'key': 'value'}; + const responseStatusCode = 200; + + // Set up mock SirenDataProvider response + when(mockSirenDataProvider.apiDomain).thenReturn('http://example.com'); + + // Set up mock Dio response for GET request + when( + mockDio.get( + any, + queryParameters: anyNamed('queryParameters'), + options: anyNamed('options'), + cancelToken: anyNamed('cancelToken'), + onReceiveProgress: anyNamed('onReceiveProgress'), + ), + ).thenAnswer( + (_) async => Response( + data: responseData, + statusCode: responseStatusCode, + requestOptions: RequestOptions(), + ), + ); + + // Perform GET request + final response = await apiClient.get(path: '/test'); + + verify( + mockDio.get( + '/test', + ), + ).called(1); + + // Verify ApiResponse matches expected result + expect(response.data, responseData); + expect(response.statusCode, responseStatusCode); + }); + + test('POST request', () async { + // Mock response data + final responseData = {'key': 'value'}; + const responseStatusCode = 201; + + // Set up mock SirenDataProvider response + when(mockSirenDataProvider.apiDomain).thenReturn('http://example.com'); + + // Set up mock Dio response for POST request + when( + mockDio.post( + any, + data: anyNamed('data'), + queryParameters: anyNamed('queryParameters'), + options: anyNamed('options'), + cancelToken: anyNamed('cancelToken'), + onSendProgress: anyNamed('onSendProgress'), + onReceiveProgress: anyNamed('onReceiveProgress'), + ), + ).thenAnswer( + (_) async => Response( + data: responseData, + statusCode: responseStatusCode, + requestOptions: RequestOptions(), + ), + ); + + // Perform POST request + final response = + await apiClient.post(path: '/test', data: {'key': 'value'}); + + verify( + mockDio.post( + '/test', + data: {'key': 'value'}, + ), + ).called(1); + + // Verify ApiResponse matches expected result + expect(response.data, responseData); + expect(response.statusCode, responseStatusCode); + }); + + test('PATCH request', () async { + // Mock response data + final responseData = {'key': 'value'}; + const responseStatusCode = 200; + + // Set up mock SirenDataProvider response + when(mockSirenDataProvider.apiDomain).thenReturn('http://example.com'); + + // Set up mock Dio response for PATCH request + when( + mockDio.patch( + any, + data: anyNamed('data'), + queryParameters: anyNamed('queryParameters'), + options: anyNamed('options'), + cancelToken: anyNamed('cancelToken'), + onSendProgress: anyNamed('onSendProgress'), + onReceiveProgress: anyNamed('onReceiveProgress'), + ), + ).thenAnswer( + (_) async => Response( + data: responseData, + statusCode: responseStatusCode, + requestOptions: RequestOptions(), + ), + ); + + // Perform PATCH request + final response = + await apiClient.patch(path: '/test', data: {'key': 'value'}); + + verify( + mockDio.patch( + '/test', + data: {'key': 'value'}, + ), + ).called(1); + + // Verify ApiResponse matches expected result + expect(response.data, responseData); + expect(response.statusCode, responseStatusCode); + }); + + test('DELETE request', () async { + // Mock response data + final responseData = {'key': 'value'}; + const responseStatusCode = 200; + + // Set up mock SirenDataProvider response + when(mockSirenDataProvider.apiDomain).thenReturn('http://example.com'); + + // Set up mock Dio response for DELETE request + when( + mockDio.delete( + any, + queryParameters: anyNamed('queryParameters'), + options: anyNamed('options'), + cancelToken: anyNamed('cancelToken'), + ), + ).thenAnswer( + (_) async => Response( + data: responseData, + statusCode: responseStatusCode, + requestOptions: RequestOptions(), + ), + ); + + final response = await apiClient.delete(path: '/test'); + + verify( + mockDio.delete( + '/test', + ), + ).called(1); + expect(response.data, responseData); + expect(response.statusCode, responseStatusCode); + }); + }); +} diff --git a/test/services/api_client_test.mocks.dart b/test/services/api_client_test.mocks.dart new file mode 100644 index 0000000..d32fc16 --- /dev/null +++ b/test/services/api_client_test.mocks.dart @@ -0,0 +1,1219 @@ +// Mocks generated by Mockito 5.4.4 from annotations +// in sirenapp_flutter_inbox/test/services/api_client_test.dart. +// Do not manually edit this file. + +// ignore_for_file: no_leading_underscores_for_library_prefixes +import 'dart:async' as _i7; + +import 'package:dio/src/adapter.dart' as _i3; +import 'package:dio/src/cancel_token.dart' as _i9; +import 'package:dio/src/dio.dart' as _i8; +import 'package:dio/src/dio_mixin.dart' as _i5; +import 'package:dio/src/options.dart' as _i2; +import 'package:dio/src/response.dart' as _i6; +import 'package:dio/src/transformer.dart' as _i4; +import 'package:mockito/mockito.dart' as _i1; +import 'package:mockito/src/dummies.dart' as _i11; +import 'package:sirenapp_flutter_inbox/sirenapp_flutter_inbox.dart' as _i12; +import 'package:sirenapp_flutter_inbox/src/constants/generics.dart' as _i13; +import 'package:sirenapp_flutter_inbox/src/data/siren_data_provider.dart' + as _i10; + +// ignore_for_file: type=lint +// ignore_for_file: avoid_redundant_argument_values +// ignore_for_file: avoid_setters_without_getters +// ignore_for_file: comment_references +// ignore_for_file: deprecated_member_use +// ignore_for_file: deprecated_member_use_from_same_package +// ignore_for_file: implementation_imports +// ignore_for_file: invalid_use_of_visible_for_testing_member +// ignore_for_file: prefer_const_constructors +// ignore_for_file: unnecessary_parenthesis +// ignore_for_file: camel_case_types +// ignore_for_file: subtype_of_sealed_class + +class _FakeBaseOptions_0 extends _i1.SmartFake implements _i2.BaseOptions { + _FakeBaseOptions_0( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} + +class _FakeHttpClientAdapter_1 extends _i1.SmartFake + implements _i3.HttpClientAdapter { + _FakeHttpClientAdapter_1( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} + +class _FakeTransformer_2 extends _i1.SmartFake implements _i4.Transformer { + _FakeTransformer_2( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} + +class _FakeInterceptors_3 extends _i1.SmartFake implements _i5.Interceptors { + _FakeInterceptors_3( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} + +class _FakeResponse_4 extends _i1.SmartFake implements _i6.Response { + _FakeResponse_4( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} + +class _FakeStreamController_5 extends _i1.SmartFake + implements _i7.StreamController { + _FakeStreamController_5( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} + +/// A class which mocks [Dio]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockDio extends _i1.Mock implements _i8.Dio { + @override + _i2.BaseOptions get options => (super.noSuchMethod( + Invocation.getter(#options), + returnValue: _FakeBaseOptions_0( + this, + Invocation.getter(#options), + ), + returnValueForMissingStub: _FakeBaseOptions_0( + this, + Invocation.getter(#options), + ), + ) as _i2.BaseOptions); + + @override + set options(_i2.BaseOptions? _options) => super.noSuchMethod( + Invocation.setter( + #options, + _options, + ), + returnValueForMissingStub: null, + ); + + @override + _i3.HttpClientAdapter get httpClientAdapter => (super.noSuchMethod( + Invocation.getter(#httpClientAdapter), + returnValue: _FakeHttpClientAdapter_1( + this, + Invocation.getter(#httpClientAdapter), + ), + returnValueForMissingStub: _FakeHttpClientAdapter_1( + this, + Invocation.getter(#httpClientAdapter), + ), + ) as _i3.HttpClientAdapter); + + @override + set httpClientAdapter(_i3.HttpClientAdapter? _httpClientAdapter) => + super.noSuchMethod( + Invocation.setter( + #httpClientAdapter, + _httpClientAdapter, + ), + returnValueForMissingStub: null, + ); + + @override + _i4.Transformer get transformer => (super.noSuchMethod( + Invocation.getter(#transformer), + returnValue: _FakeTransformer_2( + this, + Invocation.getter(#transformer), + ), + returnValueForMissingStub: _FakeTransformer_2( + this, + Invocation.getter(#transformer), + ), + ) as _i4.Transformer); + + @override + set transformer(_i4.Transformer? _transformer) => super.noSuchMethod( + Invocation.setter( + #transformer, + _transformer, + ), + returnValueForMissingStub: null, + ); + + @override + _i5.Interceptors get interceptors => (super.noSuchMethod( + Invocation.getter(#interceptors), + returnValue: _FakeInterceptors_3( + this, + Invocation.getter(#interceptors), + ), + returnValueForMissingStub: _FakeInterceptors_3( + this, + Invocation.getter(#interceptors), + ), + ) as _i5.Interceptors); + + @override + void close({bool? force = false}) => super.noSuchMethod( + Invocation.method( + #close, + [], + {#force: force}, + ), + returnValueForMissingStub: null, + ); + + @override + _i7.Future<_i6.Response> head( + String? path, { + Object? data, + Map? queryParameters, + _i2.Options? options, + _i9.CancelToken? cancelToken, + }) => + (super.noSuchMethod( + Invocation.method( + #head, + [path], + { + #data: data, + #queryParameters: queryParameters, + #options: options, + #cancelToken: cancelToken, + }, + ), + returnValue: _i7.Future<_i6.Response>.value(_FakeResponse_4( + this, + Invocation.method( + #head, + [path], + { + #data: data, + #queryParameters: queryParameters, + #options: options, + #cancelToken: cancelToken, + }, + ), + )), + returnValueForMissingStub: + _i7.Future<_i6.Response>.value(_FakeResponse_4( + this, + Invocation.method( + #head, + [path], + { + #data: data, + #queryParameters: queryParameters, + #options: options, + #cancelToken: cancelToken, + }, + ), + )), + ) as _i7.Future<_i6.Response>); + + @override + _i7.Future<_i6.Response> headUri( + Uri? uri, { + Object? data, + _i2.Options? options, + _i9.CancelToken? cancelToken, + }) => + (super.noSuchMethod( + Invocation.method( + #headUri, + [uri], + { + #data: data, + #options: options, + #cancelToken: cancelToken, + }, + ), + returnValue: _i7.Future<_i6.Response>.value(_FakeResponse_4( + this, + Invocation.method( + #headUri, + [uri], + { + #data: data, + #options: options, + #cancelToken: cancelToken, + }, + ), + )), + returnValueForMissingStub: + _i7.Future<_i6.Response>.value(_FakeResponse_4( + this, + Invocation.method( + #headUri, + [uri], + { + #data: data, + #options: options, + #cancelToken: cancelToken, + }, + ), + )), + ) as _i7.Future<_i6.Response>); + + @override + _i7.Future<_i6.Response> get( + String? path, { + Object? data, + Map? queryParameters, + _i2.Options? options, + _i9.CancelToken? cancelToken, + _i2.ProgressCallback? onReceiveProgress, + }) => + (super.noSuchMethod( + Invocation.method( + #get, + [path], + { + #data: data, + #queryParameters: queryParameters, + #options: options, + #cancelToken: cancelToken, + #onReceiveProgress: onReceiveProgress, + }, + ), + returnValue: _i7.Future<_i6.Response>.value(_FakeResponse_4( + this, + Invocation.method( + #get, + [path], + { + #data: data, + #queryParameters: queryParameters, + #options: options, + #cancelToken: cancelToken, + #onReceiveProgress: onReceiveProgress, + }, + ), + )), + returnValueForMissingStub: + _i7.Future<_i6.Response>.value(_FakeResponse_4( + this, + Invocation.method( + #get, + [path], + { + #data: data, + #queryParameters: queryParameters, + #options: options, + #cancelToken: cancelToken, + #onReceiveProgress: onReceiveProgress, + }, + ), + )), + ) as _i7.Future<_i6.Response>); + + @override + _i7.Future<_i6.Response> getUri( + Uri? uri, { + Object? data, + _i2.Options? options, + _i9.CancelToken? cancelToken, + _i2.ProgressCallback? onReceiveProgress, + }) => + (super.noSuchMethod( + Invocation.method( + #getUri, + [uri], + { + #data: data, + #options: options, + #cancelToken: cancelToken, + #onReceiveProgress: onReceiveProgress, + }, + ), + returnValue: _i7.Future<_i6.Response>.value(_FakeResponse_4( + this, + Invocation.method( + #getUri, + [uri], + { + #data: data, + #options: options, + #cancelToken: cancelToken, + #onReceiveProgress: onReceiveProgress, + }, + ), + )), + returnValueForMissingStub: + _i7.Future<_i6.Response>.value(_FakeResponse_4( + this, + Invocation.method( + #getUri, + [uri], + { + #data: data, + #options: options, + #cancelToken: cancelToken, + #onReceiveProgress: onReceiveProgress, + }, + ), + )), + ) as _i7.Future<_i6.Response>); + + @override + _i7.Future<_i6.Response> post( + String? path, { + Object? data, + Map? queryParameters, + _i2.Options? options, + _i9.CancelToken? cancelToken, + _i2.ProgressCallback? onSendProgress, + _i2.ProgressCallback? onReceiveProgress, + }) => + (super.noSuchMethod( + Invocation.method( + #post, + [path], + { + #data: data, + #queryParameters: queryParameters, + #options: options, + #cancelToken: cancelToken, + #onSendProgress: onSendProgress, + #onReceiveProgress: onReceiveProgress, + }, + ), + returnValue: _i7.Future<_i6.Response>.value(_FakeResponse_4( + this, + Invocation.method( + #post, + [path], + { + #data: data, + #queryParameters: queryParameters, + #options: options, + #cancelToken: cancelToken, + #onSendProgress: onSendProgress, + #onReceiveProgress: onReceiveProgress, + }, + ), + )), + returnValueForMissingStub: + _i7.Future<_i6.Response>.value(_FakeResponse_4( + this, + Invocation.method( + #post, + [path], + { + #data: data, + #queryParameters: queryParameters, + #options: options, + #cancelToken: cancelToken, + #onSendProgress: onSendProgress, + #onReceiveProgress: onReceiveProgress, + }, + ), + )), + ) as _i7.Future<_i6.Response>); + + @override + _i7.Future<_i6.Response> postUri( + Uri? uri, { + Object? data, + _i2.Options? options, + _i9.CancelToken? cancelToken, + _i2.ProgressCallback? onSendProgress, + _i2.ProgressCallback? onReceiveProgress, + }) => + (super.noSuchMethod( + Invocation.method( + #postUri, + [uri], + { + #data: data, + #options: options, + #cancelToken: cancelToken, + #onSendProgress: onSendProgress, + #onReceiveProgress: onReceiveProgress, + }, + ), + returnValue: _i7.Future<_i6.Response>.value(_FakeResponse_4( + this, + Invocation.method( + #postUri, + [uri], + { + #data: data, + #options: options, + #cancelToken: cancelToken, + #onSendProgress: onSendProgress, + #onReceiveProgress: onReceiveProgress, + }, + ), + )), + returnValueForMissingStub: + _i7.Future<_i6.Response>.value(_FakeResponse_4( + this, + Invocation.method( + #postUri, + [uri], + { + #data: data, + #options: options, + #cancelToken: cancelToken, + #onSendProgress: onSendProgress, + #onReceiveProgress: onReceiveProgress, + }, + ), + )), + ) as _i7.Future<_i6.Response>); + + @override + _i7.Future<_i6.Response> put( + String? path, { + Object? data, + Map? queryParameters, + _i2.Options? options, + _i9.CancelToken? cancelToken, + _i2.ProgressCallback? onSendProgress, + _i2.ProgressCallback? onReceiveProgress, + }) => + (super.noSuchMethod( + Invocation.method( + #put, + [path], + { + #data: data, + #queryParameters: queryParameters, + #options: options, + #cancelToken: cancelToken, + #onSendProgress: onSendProgress, + #onReceiveProgress: onReceiveProgress, + }, + ), + returnValue: _i7.Future<_i6.Response>.value(_FakeResponse_4( + this, + Invocation.method( + #put, + [path], + { + #data: data, + #queryParameters: queryParameters, + #options: options, + #cancelToken: cancelToken, + #onSendProgress: onSendProgress, + #onReceiveProgress: onReceiveProgress, + }, + ), + )), + returnValueForMissingStub: + _i7.Future<_i6.Response>.value(_FakeResponse_4( + this, + Invocation.method( + #put, + [path], + { + #data: data, + #queryParameters: queryParameters, + #options: options, + #cancelToken: cancelToken, + #onSendProgress: onSendProgress, + #onReceiveProgress: onReceiveProgress, + }, + ), + )), + ) as _i7.Future<_i6.Response>); + + @override + _i7.Future<_i6.Response> putUri( + Uri? uri, { + Object? data, + _i2.Options? options, + _i9.CancelToken? cancelToken, + _i2.ProgressCallback? onSendProgress, + _i2.ProgressCallback? onReceiveProgress, + }) => + (super.noSuchMethod( + Invocation.method( + #putUri, + [uri], + { + #data: data, + #options: options, + #cancelToken: cancelToken, + #onSendProgress: onSendProgress, + #onReceiveProgress: onReceiveProgress, + }, + ), + returnValue: _i7.Future<_i6.Response>.value(_FakeResponse_4( + this, + Invocation.method( + #putUri, + [uri], + { + #data: data, + #options: options, + #cancelToken: cancelToken, + #onSendProgress: onSendProgress, + #onReceiveProgress: onReceiveProgress, + }, + ), + )), + returnValueForMissingStub: + _i7.Future<_i6.Response>.value(_FakeResponse_4( + this, + Invocation.method( + #putUri, + [uri], + { + #data: data, + #options: options, + #cancelToken: cancelToken, + #onSendProgress: onSendProgress, + #onReceiveProgress: onReceiveProgress, + }, + ), + )), + ) as _i7.Future<_i6.Response>); + + @override + _i7.Future<_i6.Response> patch( + String? path, { + Object? data, + Map? queryParameters, + _i2.Options? options, + _i9.CancelToken? cancelToken, + _i2.ProgressCallback? onSendProgress, + _i2.ProgressCallback? onReceiveProgress, + }) => + (super.noSuchMethod( + Invocation.method( + #patch, + [path], + { + #data: data, + #queryParameters: queryParameters, + #options: options, + #cancelToken: cancelToken, + #onSendProgress: onSendProgress, + #onReceiveProgress: onReceiveProgress, + }, + ), + returnValue: _i7.Future<_i6.Response>.value(_FakeResponse_4( + this, + Invocation.method( + #patch, + [path], + { + #data: data, + #queryParameters: queryParameters, + #options: options, + #cancelToken: cancelToken, + #onSendProgress: onSendProgress, + #onReceiveProgress: onReceiveProgress, + }, + ), + )), + returnValueForMissingStub: + _i7.Future<_i6.Response>.value(_FakeResponse_4( + this, + Invocation.method( + #patch, + [path], + { + #data: data, + #queryParameters: queryParameters, + #options: options, + #cancelToken: cancelToken, + #onSendProgress: onSendProgress, + #onReceiveProgress: onReceiveProgress, + }, + ), + )), + ) as _i7.Future<_i6.Response>); + + @override + _i7.Future<_i6.Response> patchUri( + Uri? uri, { + Object? data, + _i2.Options? options, + _i9.CancelToken? cancelToken, + _i2.ProgressCallback? onSendProgress, + _i2.ProgressCallback? onReceiveProgress, + }) => + (super.noSuchMethod( + Invocation.method( + #patchUri, + [uri], + { + #data: data, + #options: options, + #cancelToken: cancelToken, + #onSendProgress: onSendProgress, + #onReceiveProgress: onReceiveProgress, + }, + ), + returnValue: _i7.Future<_i6.Response>.value(_FakeResponse_4( + this, + Invocation.method( + #patchUri, + [uri], + { + #data: data, + #options: options, + #cancelToken: cancelToken, + #onSendProgress: onSendProgress, + #onReceiveProgress: onReceiveProgress, + }, + ), + )), + returnValueForMissingStub: + _i7.Future<_i6.Response>.value(_FakeResponse_4( + this, + Invocation.method( + #patchUri, + [uri], + { + #data: data, + #options: options, + #cancelToken: cancelToken, + #onSendProgress: onSendProgress, + #onReceiveProgress: onReceiveProgress, + }, + ), + )), + ) as _i7.Future<_i6.Response>); + + @override + _i7.Future<_i6.Response> delete( + String? path, { + Object? data, + Map? queryParameters, + _i2.Options? options, + _i9.CancelToken? cancelToken, + }) => + (super.noSuchMethod( + Invocation.method( + #delete, + [path], + { + #data: data, + #queryParameters: queryParameters, + #options: options, + #cancelToken: cancelToken, + }, + ), + returnValue: _i7.Future<_i6.Response>.value(_FakeResponse_4( + this, + Invocation.method( + #delete, + [path], + { + #data: data, + #queryParameters: queryParameters, + #options: options, + #cancelToken: cancelToken, + }, + ), + )), + returnValueForMissingStub: + _i7.Future<_i6.Response>.value(_FakeResponse_4( + this, + Invocation.method( + #delete, + [path], + { + #data: data, + #queryParameters: queryParameters, + #options: options, + #cancelToken: cancelToken, + }, + ), + )), + ) as _i7.Future<_i6.Response>); + + @override + _i7.Future<_i6.Response> deleteUri( + Uri? uri, { + Object? data, + _i2.Options? options, + _i9.CancelToken? cancelToken, + }) => + (super.noSuchMethod( + Invocation.method( + #deleteUri, + [uri], + { + #data: data, + #options: options, + #cancelToken: cancelToken, + }, + ), + returnValue: _i7.Future<_i6.Response>.value(_FakeResponse_4( + this, + Invocation.method( + #deleteUri, + [uri], + { + #data: data, + #options: options, + #cancelToken: cancelToken, + }, + ), + )), + returnValueForMissingStub: + _i7.Future<_i6.Response>.value(_FakeResponse_4( + this, + Invocation.method( + #deleteUri, + [uri], + { + #data: data, + #options: options, + #cancelToken: cancelToken, + }, + ), + )), + ) as _i7.Future<_i6.Response>); + + @override + _i7.Future<_i6.Response> download( + String? urlPath, + dynamic savePath, { + _i2.ProgressCallback? onReceiveProgress, + Map? queryParameters, + _i9.CancelToken? cancelToken, + bool? deleteOnError = true, + String? lengthHeader = r'content-length', + Object? data, + _i2.Options? options, + }) => + (super.noSuchMethod( + Invocation.method( + #download, + [ + urlPath, + savePath, + ], + { + #onReceiveProgress: onReceiveProgress, + #queryParameters: queryParameters, + #cancelToken: cancelToken, + #deleteOnError: deleteOnError, + #lengthHeader: lengthHeader, + #data: data, + #options: options, + }, + ), + returnValue: + _i7.Future<_i6.Response>.value(_FakeResponse_4( + this, + Invocation.method( + #download, + [ + urlPath, + savePath, + ], + { + #onReceiveProgress: onReceiveProgress, + #queryParameters: queryParameters, + #cancelToken: cancelToken, + #deleteOnError: deleteOnError, + #lengthHeader: lengthHeader, + #data: data, + #options: options, + }, + ), + )), + returnValueForMissingStub: + _i7.Future<_i6.Response>.value(_FakeResponse_4( + this, + Invocation.method( + #download, + [ + urlPath, + savePath, + ], + { + #onReceiveProgress: onReceiveProgress, + #queryParameters: queryParameters, + #cancelToken: cancelToken, + #deleteOnError: deleteOnError, + #lengthHeader: lengthHeader, + #data: data, + #options: options, + }, + ), + )), + ) as _i7.Future<_i6.Response>); + + @override + _i7.Future<_i6.Response> downloadUri( + Uri? uri, + dynamic savePath, { + _i2.ProgressCallback? onReceiveProgress, + _i9.CancelToken? cancelToken, + bool? deleteOnError = true, + String? lengthHeader = r'content-length', + Object? data, + _i2.Options? options, + }) => + (super.noSuchMethod( + Invocation.method( + #downloadUri, + [ + uri, + savePath, + ], + { + #onReceiveProgress: onReceiveProgress, + #cancelToken: cancelToken, + #deleteOnError: deleteOnError, + #lengthHeader: lengthHeader, + #data: data, + #options: options, + }, + ), + returnValue: + _i7.Future<_i6.Response>.value(_FakeResponse_4( + this, + Invocation.method( + #downloadUri, + [ + uri, + savePath, + ], + { + #onReceiveProgress: onReceiveProgress, + #cancelToken: cancelToken, + #deleteOnError: deleteOnError, + #lengthHeader: lengthHeader, + #data: data, + #options: options, + }, + ), + )), + returnValueForMissingStub: + _i7.Future<_i6.Response>.value(_FakeResponse_4( + this, + Invocation.method( + #downloadUri, + [ + uri, + savePath, + ], + { + #onReceiveProgress: onReceiveProgress, + #cancelToken: cancelToken, + #deleteOnError: deleteOnError, + #lengthHeader: lengthHeader, + #data: data, + #options: options, + }, + ), + )), + ) as _i7.Future<_i6.Response>); + + @override + _i7.Future<_i6.Response> request( + String? url, { + Object? data, + Map? queryParameters, + _i9.CancelToken? cancelToken, + _i2.Options? options, + _i2.ProgressCallback? onSendProgress, + _i2.ProgressCallback? onReceiveProgress, + }) => + (super.noSuchMethod( + Invocation.method( + #request, + [url], + { + #data: data, + #queryParameters: queryParameters, + #cancelToken: cancelToken, + #options: options, + #onSendProgress: onSendProgress, + #onReceiveProgress: onReceiveProgress, + }, + ), + returnValue: _i7.Future<_i6.Response>.value(_FakeResponse_4( + this, + Invocation.method( + #request, + [url], + { + #data: data, + #queryParameters: queryParameters, + #cancelToken: cancelToken, + #options: options, + #onSendProgress: onSendProgress, + #onReceiveProgress: onReceiveProgress, + }, + ), + )), + returnValueForMissingStub: + _i7.Future<_i6.Response>.value(_FakeResponse_4( + this, + Invocation.method( + #request, + [url], + { + #data: data, + #queryParameters: queryParameters, + #cancelToken: cancelToken, + #options: options, + #onSendProgress: onSendProgress, + #onReceiveProgress: onReceiveProgress, + }, + ), + )), + ) as _i7.Future<_i6.Response>); + + @override + _i7.Future<_i6.Response> requestUri( + Uri? uri, { + Object? data, + _i9.CancelToken? cancelToken, + _i2.Options? options, + _i2.ProgressCallback? onSendProgress, + _i2.ProgressCallback? onReceiveProgress, + }) => + (super.noSuchMethod( + Invocation.method( + #requestUri, + [uri], + { + #data: data, + #cancelToken: cancelToken, + #options: options, + #onSendProgress: onSendProgress, + #onReceiveProgress: onReceiveProgress, + }, + ), + returnValue: _i7.Future<_i6.Response>.value(_FakeResponse_4( + this, + Invocation.method( + #requestUri, + [uri], + { + #data: data, + #cancelToken: cancelToken, + #options: options, + #onSendProgress: onSendProgress, + #onReceiveProgress: onReceiveProgress, + }, + ), + )), + returnValueForMissingStub: + _i7.Future<_i6.Response>.value(_FakeResponse_4( + this, + Invocation.method( + #requestUri, + [uri], + { + #data: data, + #cancelToken: cancelToken, + #options: options, + #onSendProgress: onSendProgress, + #onReceiveProgress: onReceiveProgress, + }, + ), + )), + ) as _i7.Future<_i6.Response>); + + @override + _i7.Future<_i6.Response> fetch(_i2.RequestOptions? requestOptions) => + (super.noSuchMethod( + Invocation.method( + #fetch, + [requestOptions], + ), + returnValue: _i7.Future<_i6.Response>.value(_FakeResponse_4( + this, + Invocation.method( + #fetch, + [requestOptions], + ), + )), + returnValueForMissingStub: + _i7.Future<_i6.Response>.value(_FakeResponse_4( + this, + Invocation.method( + #fetch, + [requestOptions], + ), + )), + ) as _i7.Future<_i6.Response>); +} + +/// A class which mocks [SirenDataProvider]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockSirenDataProvider extends _i1.Mock implements _i10.SirenDataProvider { + @override + String get userToken => (super.noSuchMethod( + Invocation.getter(#userToken), + returnValue: _i11.dummyValue( + this, + Invocation.getter(#userToken), + ), + returnValueForMissingStub: _i11.dummyValue( + this, + Invocation.getter(#userToken), + ), + ) as String); + + @override + set userToken(String? _userToken) => super.noSuchMethod( + Invocation.setter( + #userToken, + _userToken, + ), + returnValueForMissingStub: null, + ); + + @override + String get recipientId => (super.noSuchMethod( + Invocation.getter(#recipientId), + returnValue: _i11.dummyValue( + this, + Invocation.getter(#recipientId), + ), + returnValueForMissingStub: _i11.dummyValue( + this, + Invocation.getter(#recipientId), + ), + ) as String); + + @override + set recipientId(String? _recipientId) => super.noSuchMethod( + Invocation.setter( + #recipientId, + _recipientId, + ), + returnValueForMissingStub: null, + ); + + @override + String get apiDomain => (super.noSuchMethod( + Invocation.getter(#apiDomain), + returnValue: _i11.dummyValue( + this, + Invocation.getter(#apiDomain), + ), + returnValueForMissingStub: _i11.dummyValue( + this, + Invocation.getter(#apiDomain), + ), + ) as String); + + @override + set apiDomain(String? _apiDomain) => super.noSuchMethod( + Invocation.setter( + #apiDomain, + _apiDomain, + ), + returnValueForMissingStub: null, + ); + + @override + _i7.StreamController<_i12.StreamResponse> get inboxController => + (super.noSuchMethod( + Invocation.getter(#inboxController), + returnValue: _FakeStreamController_5<_i12.StreamResponse>( + this, + Invocation.getter(#inboxController), + ), + returnValueForMissingStub: _FakeStreamController_5<_i12.StreamResponse>( + this, + Invocation.getter(#inboxController), + ), + ) as _i7.StreamController<_i12.StreamResponse>); + + @override + _i7.StreamController<_i12.StreamResponse> get iconController => + (super.noSuchMethod( + Invocation.getter(#iconController), + returnValue: _FakeStreamController_5<_i12.StreamResponse>( + this, + Invocation.getter(#iconController), + ), + returnValueForMissingStub: _FakeStreamController_5<_i12.StreamResponse>( + this, + Invocation.getter(#iconController), + ), + ) as _i7.StreamController<_i12.StreamResponse>); + + @override + _i13.Status get tokenVerificationStatus => (super.noSuchMethod( + Invocation.getter(#tokenVerificationStatus), + returnValue: _i13.Status.PENDING, + returnValueForMissingStub: _i13.Status.PENDING, + ) as _i13.Status); + + @override + _i7.Future initialize() => (super.noSuchMethod( + Invocation.method( + #initialize, + [], + ), + returnValue: _i7.Future.value(), + returnValueForMissingStub: _i7.Future.value(), + ) as _i7.Future); + + @override + void updateParams({ + required String? userToken, + required String? recipientId, + }) => + super.noSuchMethod( + Invocation.method( + #updateParams, + [], + { + #userToken: userToken, + #recipientId: recipientId, + }, + ), + returnValueForMissingStub: null, + ); + + @override + void iconDispose() => super.noSuchMethod( + Invocation.method( + #iconDispose, + [], + ), + returnValueForMissingStub: null, + ); + + @override + void inboxDispose() => super.noSuchMethod( + Invocation.method( + #inboxDispose, + [], + ), + returnValueForMissingStub: null, + ); +} diff --git a/test/services/network_service_test.dart b/test/services/network_service_test.dart new file mode 100644 index 0000000..0498c0c --- /dev/null +++ b/test/services/network_service_test.dart @@ -0,0 +1,39 @@ +// ignore_for_file: cascade_invocations + +import 'package:flutter_test/flutter_test.dart'; +import 'package:mockito/annotations.dart'; +import 'package:sirenapp_flutter_inbox/src/services/api_client.dart'; +import 'package:sirenapp_flutter_inbox/src/services/network_service.dart'; + +import 'network_service_test.mocks.dart'; + +// class MockApiClient extends Mock implements ApiClient {} + +@GenerateNiceMocks([ + MockSpec(), +]) +void main() { + late NetworkService networkService; + late MockApiClient mockApiClient; + + setUp(() { + mockApiClient = MockApiClient(); + networkService = NetworkService.instance; + networkService.api = mockApiClient; + }); + + group('NetworkService', () { + test('NetworkService instance is a singleton', () { + // Ensure that the instance is singleton + final networkServiceInstance1 = NetworkService.instance; + final networkServiceInstance2 = NetworkService.instance; + + expect(networkServiceInstance1, equals(networkServiceInstance2)); + }); + + test('ApiClient is correctly injected into NetworkService', () { + // Verify that the ApiClient is correctly injected into NetworkService + expect(networkService.api, equals(mockApiClient)); + }); + }); +} diff --git a/test/services/network_service_test.mocks.dart b/test/services/network_service_test.mocks.dart new file mode 100644 index 0000000..ee1e372 --- /dev/null +++ b/test/services/network_service_test.mocks.dart @@ -0,0 +1,269 @@ +// Mocks generated by Mockito 5.4.4 from annotations +// in sirenapp_flutter_inbox/test/services/network_service_test.dart. +// Do not manually edit this file. + +// ignore_for_file: no_leading_underscores_for_library_prefixes +import 'dart:async' as _i5; + +import 'package:dio/dio.dart' as _i4; +import 'package:mockito/mockito.dart' as _i1; +import 'package:sirenapp_flutter_inbox/src/models/api_response.dart' as _i2; +import 'package:sirenapp_flutter_inbox/src/services/api_client.dart' as _i3; + +// ignore_for_file: type=lint +// ignore_for_file: avoid_redundant_argument_values +// ignore_for_file: avoid_setters_without_getters +// ignore_for_file: comment_references +// ignore_for_file: deprecated_member_use +// ignore_for_file: deprecated_member_use_from_same_package +// ignore_for_file: implementation_imports +// ignore_for_file: invalid_use_of_visible_for_testing_member +// ignore_for_file: prefer_const_constructors +// ignore_for_file: unnecessary_parenthesis +// ignore_for_file: camel_case_types +// ignore_for_file: subtype_of_sealed_class + +class _FakeDioResponse_0 extends _i1.SmartFake implements _i2.DioResponse { + _FakeDioResponse_0( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} + +/// A class which mocks [ApiClient]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockApiClient extends _i1.Mock implements _i3.ApiClient { + @override + bool isServerError(_i4.Response? response) => (super.noSuchMethod( + Invocation.method( + #isServerError, + [response], + ), + returnValue: false, + returnValueForMissingStub: false, + ) as bool); + + @override + _i5.Future<_i2.DioResponse> get({ + String? path, + Map? queryParameters, + _i4.Options? options, + _i4.CancelToken? cancelToken, + _i4.ProgressCallback? onReceiveProgress, + }) => + (super.noSuchMethod( + Invocation.method( + #get, + [], + { + #path: path, + #queryParameters: queryParameters, + #options: options, + #cancelToken: cancelToken, + #onReceiveProgress: onReceiveProgress, + }, + ), + returnValue: _i5.Future<_i2.DioResponse>.value(_FakeDioResponse_0( + this, + Invocation.method( + #get, + [], + { + #path: path, + #queryParameters: queryParameters, + #options: options, + #cancelToken: cancelToken, + #onReceiveProgress: onReceiveProgress, + }, + ), + )), + returnValueForMissingStub: + _i5.Future<_i2.DioResponse>.value(_FakeDioResponse_0( + this, + Invocation.method( + #get, + [], + { + #path: path, + #queryParameters: queryParameters, + #options: options, + #cancelToken: cancelToken, + #onReceiveProgress: onReceiveProgress, + }, + ), + )), + ) as _i5.Future<_i2.DioResponse>); + + @override + _i5.Future<_i2.DioResponse> post({ + String? path, + dynamic data, + Map? queryParameters, + _i4.Options? options, + _i4.CancelToken? cancelToken, + _i4.ProgressCallback? onSendProgress, + _i4.ProgressCallback? onReceiveProgress, + }) => + (super.noSuchMethod( + Invocation.method( + #post, + [], + { + #path: path, + #data: data, + #queryParameters: queryParameters, + #options: options, + #cancelToken: cancelToken, + #onSendProgress: onSendProgress, + #onReceiveProgress: onReceiveProgress, + }, + ), + returnValue: _i5.Future<_i2.DioResponse>.value(_FakeDioResponse_0( + this, + Invocation.method( + #post, + [], + { + #path: path, + #data: data, + #queryParameters: queryParameters, + #options: options, + #cancelToken: cancelToken, + #onSendProgress: onSendProgress, + #onReceiveProgress: onReceiveProgress, + }, + ), + )), + returnValueForMissingStub: + _i5.Future<_i2.DioResponse>.value(_FakeDioResponse_0( + this, + Invocation.method( + #post, + [], + { + #path: path, + #data: data, + #queryParameters: queryParameters, + #options: options, + #cancelToken: cancelToken, + #onSendProgress: onSendProgress, + #onReceiveProgress: onReceiveProgress, + }, + ), + )), + ) as _i5.Future<_i2.DioResponse>); + + @override + _i5.Future<_i2.DioResponse> patch({ + String? path, + dynamic data, + Map? queryParameters, + _i4.Options? options, + _i4.CancelToken? cancelToken, + _i4.ProgressCallback? onSendProgress, + _i4.ProgressCallback? onReceiveProgress, + }) => + (super.noSuchMethod( + Invocation.method( + #patch, + [], + { + #path: path, + #data: data, + #queryParameters: queryParameters, + #options: options, + #cancelToken: cancelToken, + #onSendProgress: onSendProgress, + #onReceiveProgress: onReceiveProgress, + }, + ), + returnValue: _i5.Future<_i2.DioResponse>.value(_FakeDioResponse_0( + this, + Invocation.method( + #patch, + [], + { + #path: path, + #data: data, + #queryParameters: queryParameters, + #options: options, + #cancelToken: cancelToken, + #onSendProgress: onSendProgress, + #onReceiveProgress: onReceiveProgress, + }, + ), + )), + returnValueForMissingStub: + _i5.Future<_i2.DioResponse>.value(_FakeDioResponse_0( + this, + Invocation.method( + #patch, + [], + { + #path: path, + #data: data, + #queryParameters: queryParameters, + #options: options, + #cancelToken: cancelToken, + #onSendProgress: onSendProgress, + #onReceiveProgress: onReceiveProgress, + }, + ), + )), + ) as _i5.Future<_i2.DioResponse>); + + @override + _i5.Future<_i2.DioResponse> delete({ + String? path, + Map? queryParameters, + _i4.Options? options, + _i4.CancelToken? cancelToken, + _i4.ProgressCallback? onReceiveProgress, + }) => + (super.noSuchMethod( + Invocation.method( + #delete, + [], + { + #path: path, + #queryParameters: queryParameters, + #options: options, + #cancelToken: cancelToken, + #onReceiveProgress: onReceiveProgress, + }, + ), + returnValue: _i5.Future<_i2.DioResponse>.value(_FakeDioResponse_0( + this, + Invocation.method( + #delete, + [], + { + #path: path, + #queryParameters: queryParameters, + #options: options, + #cancelToken: cancelToken, + #onReceiveProgress: onReceiveProgress, + }, + ), + )), + returnValueForMissingStub: + _i5.Future<_i2.DioResponse>.value(_FakeDioResponse_0( + this, + Invocation.method( + #delete, + [], + { + #path: path, + #queryParameters: queryParameters, + #options: options, + #cancelToken: cancelToken, + #onReceiveProgress: onReceiveProgress, + }, + ), + )), + ) as _i5.Future<_i2.DioResponse>); +} diff --git a/test/siren_inbox_icon_test.dart b/test/siren_inbox_icon_test.dart deleted file mode 100644 index 9f9849f..0000000 --- a/test/siren_inbox_icon_test.dart +++ /dev/null @@ -1,118 +0,0 @@ -import 'dart:async'; - -import 'package:flutter/material.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:mockito/mockito.dart'; -import 'package:sirenapp_flutter_inbox/sirenapp_flutter_inbox.dart'; -import 'package:sirenapp_flutter_inbox/src/api/fetch_unviewed_notification_count.dart'; -import 'package:sirenapp_flutter_inbox/src/data/siren_data_provider.dart'; -import 'package:sirenapp_flutter_inbox/src/theme/app_theme.dart'; - -// Create a mock class for SirenDataProvider -class MockSirenDataProvider extends Mock implements SirenDataProvider {} - -class MockFetchUnViewedNotificationsCount extends Mock - implements FetchUnViewedNotificationsCount {} - -class MockApiResponse extends Mock implements ApiResponse {} - -void main() { - group('SirenInboxIcon', () { - late StreamController iconController; - late StreamController inboxController; - - setUp(() { - iconController = StreamController.broadcast(); - inboxController = StreamController.broadcast(); - }); - - tearDown(() { - iconController.close(); - inboxController.close(); - }); - - testWidgets('Widget initialization', (WidgetTester tester) async { - await tester.pumpWidget( - const MaterialApp( - home: Scaffold( - body: SirenInboxIcon(), - ), - ), - ); - - expect(find.byType(SirenInboxIcon), findsOneWidget); - }); - - testWidgets('Disabled widget', (WidgetTester tester) async { - await tester.pumpWidget( - const MaterialApp( - home: Scaffold( - body: Center( - child: SirenInboxIcon( - disabled: true, - ), - ), - ), - ), - ); - - final ignorePointerFinder = find.byWidgetPredicate( - (widget) => - widget is IgnorePointer && - widget.ignoring && - widget.child is GestureDetector, - ); - - expect(ignorePointerFinder, findsOneWidget); - }); - - testWidgets('Widget with custom notification icon', - (WidgetTester tester) async { - await tester.pumpWidget( - const MaterialApp( - home: Scaffold( - body: SirenInboxIcon( - notificationIcon: Icon(Icons.mail), - ), - ), - ), - ); - - expect(find.byIcon(Icons.mail), findsOneWidget); - }); - - testWidgets('Widget updates in dark mode', (WidgetTester tester) async { - await tester.pumpWidget( - const MaterialApp( - home: Scaffold( - body: SirenInboxIcon( - darkMode: true, - ), - ), - ), - ); - - final primaryColor = AppTheme.darkTheme.colorScheme.primary; - - // Ensure dark theme is applied - expect(primaryColor, const Color(0xff232326)); - }); - - testWidgets('Widget disposes controllers on dispose', - (WidgetTester tester) async { - await tester.pumpWidget( - const MaterialApp( - home: Scaffold( - body: SirenInboxIcon(), - ), - ), - ); - - await tester.pumpWidget(Container()); // Dispose the widget - - // Verify controllers are closed - expect(iconController.hasListener, false); - expect(inboxController.hasListener, false); - }); - }); -} diff --git a/test/utils/common_utils_test.dart b/test/utils/common_utils_test.dart index d260f74..a5b8247 100644 --- a/test/utils/common_utils_test.dart +++ b/test/utils/common_utils_test.dart @@ -3,6 +3,8 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:mockito/mockito.dart'; // Import mockito import 'package:sirenapp_flutter_inbox/src/utils/common_utils.dart'; +class MockRootBundle extends Mock implements AssetBundle {} + void main() { group('generateElapsedTimeText', () { test('should return correct elapsed time text', () { @@ -31,6 +33,12 @@ void main() { ), '2 days ago', ); + expect( + generateElapsedTimeText( + DateTime.now().subtract(const Duration(days: 35)), + ), + '1 month ago', + ); expect( generateElapsedTimeText( DateTime.now().subtract(const Duration(days: 370)), @@ -63,71 +71,16 @@ void main() { }); }); - // group('loadEnv', () { - // Inside the test group for loadEnv - // test('should load environment variables from .env file', () async { - // // Mock the rootBundle.loadString method - // const envContents = 'API_DOMAIN=test.com'; - // const expectedEnvVariables = { - // 'API_DOMAIN': 'test.com', - // }; - // final mockBundle = MockAssetBundle(); - - // // Use when to define behavior for the method call - // // when(mockBundle.loadString(Generics.ENV_PATH)) - // // .thenAnswer((_) => Future.value(envContents)); - - // // Load environment variables - // final envVariables = await loadEnv(); - // print('envVariables $envVariables'); - // print('expectedEnvVariables $expectedEnvVariables'); - - // // Assert that the loaded environment variables match the expected ones - // expect(envVariables, expectedEnvVariables); - // }); - - // test('should return empty map if failed to load .env file', () async { - // // Mock the rootBundle.loadString method to throw an error - // final mockBundle = MockAssetBundle(); - // when(mockBundle.loadString(Generics.ENV_PATH)) - // .thenThrow(Exception('Failed to load')); - - // // Load environment variables - // final envVariables = await loadEnv(); - - // // Assert that an empty map is returned - // expect(envVariables, {}); - // }); - // }); - - // group('getApiDomain', () { - // test('should return API domain from environment variables', () async { - // // Mock the loadEnv function to return environment variables - // const expectedApiDomain = 'test.com'; - // final mockEnv = {'API_DOMAIN': expectedApiDomain}; - // when(loadEnv()).thenAnswer((_) => Future.value(mockEnv)); - // // when(loadEnv()).thenAnswer((_) async => mockEnv); - - // // Get API domain - // final apiDomain = await getApiDomain(); - - // // Assert that the returned API domain matches the expected one - // expect(apiDomain, expectedApiDomain); - // }); - - // test( - // 'should return empty string if API domain is not found in environment variables', - // () async { - // // Mock the loadEnv function to return empty environment variables - // // when(loadEnv()).thenAnswer((_) => Future.value({})); - - // // Get API domain - // final apiDomain = await getApiDomain(); + group('loadEnv', () { + test('API_DOMAIN', () async { + TestWidgetsFlutterBinding.ensureInitialized(); + final envVariables = await loadEnv(); + expect(envVariables.keys.first, 'API_DOMAIN'); + await getApiDomain(); + }); - // // Assert that an empty string is returned - // expect(apiDomain, ''); - // }); - // }); + // Write other tests for loadEnv function... + }); } // Mock class for AssetBundle diff --git a/test/card_test.dart b/test/widgets/card_test.dart similarity index 100% rename from test/card_test.dart rename to test/widgets/card_test.dart diff --git a/test/empty_widget_test.dart b/test/widgets/empty_widget_test.dart similarity index 100% rename from test/empty_widget_test.dart rename to test/widgets/empty_widget_test.dart diff --git a/test/error_widget_test.dart b/test/widgets/error_widget_test.dart similarity index 100% rename from test/error_widget_test.dart rename to test/widgets/error_widget_test.dart diff --git a/test/widgets/icon_badge_test.dart b/test/widgets/icon_badge_test.dart new file mode 100644 index 0000000..758140b --- /dev/null +++ b/test/widgets/icon_badge_test.dart @@ -0,0 +1,78 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:sirenapp_flutter_inbox/sirenapp_flutter_inbox.dart'; +import 'package:sirenapp_flutter_inbox/src/widgets/icon_badge.dart'; + +void main() { + group('IconBadge Widget Test', () { + testWidgets('Testing with valid parameters', (WidgetTester tester) async { + await tester.pumpWidget( + const MaterialApp( + home: Scaffold( + body: Stack( + children: [ + IconBadge( + badgeStyle: BadgeStyle(), + notificationsCount: 5, + hideBadge: false, + ), + ], + ), + ), + ), + ); + await tester.pumpAndSettle(const Duration(seconds: 1)); + + // Check if IconBadge is rendered + expect(find.byType(Positioned), findsOneWidget); + + // Check if Text widget is rendered with correct text + expect(find.text('5'), findsOneWidget); + }); + + testWidgets('Testing with hideBadge true', (WidgetTester tester) async { + await tester.pumpWidget( + const MaterialApp( + home: Scaffold( + body: Stack( + children: [ + IconBadge( + badgeStyle: BadgeStyle(), + notificationsCount: 5, + hideBadge: true, + ), + ], + ), + ), + ), + ); + await tester.pumpAndSettle(const Duration(seconds: 1)); + + // Check if IconBadge is not rendered + expect(find.byType(Positioned), findsNothing); + }); + + testWidgets('Testing with notificationsCount > 99', + (WidgetTester tester) async { + await tester.pumpWidget( + const MaterialApp( + home: Scaffold( + body: Stack( + children: [ + IconBadge( + badgeStyle: BadgeStyle(), + notificationsCount: 100, + hideBadge: false, + ), + ], + ), + ), + ), + ); + await tester.pumpAndSettle(const Duration(seconds: 1)); + + // Check if Text widget is rendered with '99+' + expect(find.text('99+'), findsOneWidget); + }); + }); +} diff --git a/test/loader_widget_test.dart b/test/widgets/loader_widget_test.dart similarity index 100% rename from test/loader_widget_test.dart rename to test/widgets/loader_widget_test.dart diff --git a/test/widgets/notification_list_view_test.dart b/test/widgets/notification_list_view_test.dart new file mode 100644 index 0000000..43319a3 --- /dev/null +++ b/test/widgets/notification_list_view_test.dart @@ -0,0 +1,111 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mockito/mockito.dart'; +import 'package:network_image_mock/network_image_mock.dart'; +import 'package:sirenapp_flutter_inbox/sirenapp_flutter_inbox.dart'; +import 'package:sirenapp_flutter_inbox/src/widgets/notification_list_view.dart'; + +class MockFunction extends Mock { + // Define the mock function signature + void call(); // You can define parameters and return types as needed +} + +void main() { + final notificationsList = [ + NotificationDataType( + id: '1', + createdAt: '2024-03-15T04:07:14.577928Z', + message: MessageData( + header: 'Test Header', + subHeader: 'Test SubHeader', + body: 'Test Body', + channel: 'Test Channel', + actionUrl: 'Test Action Url', + avatar: AvatarData( + altText: 'Test alt text', + url: 'https://picsum.photos/200/300', + ), + additionalData: 'Test Additional Data', + ), + requestId: 'request-id', + isRead: false, + cardColor: Colors.black, + ), + ]; + testWidgets('NotificationListView renders correctly', + (WidgetTester tester) async { + const isLoading = false; + const endReached = false; + const loadingNextPage = false; + + await mockNetworkImagesFor(() async { + final func = MockFunction().call; + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: NotificationListView( + notifications: notificationsList, + isLoading: isLoading, + endReached: endReached, + loadingNextPage: loadingNextPage, + onRefresh: () async {}, + onEndReached: () {}, + customStyles: null, + hideAvatar: false, + deleteWidget: null, + scrollController: ScrollController(), + onDelete: (id) async {}, + markAsRead: (id) {}, + onNotificationCardClick: (n) { + func(); + }, + ), + ), + ), + ); + await tester.tap(find.byType(GestureDetector).first); + await tester.pumpAndSettle(const Duration(seconds: 1)); + verify(func()).called(1); + expect(find.byElementType(CircularProgressIndicator), findsNothing); + }); + }); + + testWidgets('NotificationListView with custom notification card', + (WidgetTester tester) async { + const isLoading = false; + const endReached = false; + const loadingNextPage = false; + + await mockNetworkImagesFor(() async { + final func = MockFunction().call; + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: NotificationListView( + notifications: notificationsList, + isLoading: isLoading, + endReached: endReached, + loadingNextPage: loadingNextPage, + onRefresh: () async {}, + onEndReached: () {}, + customStyles: null, + hideAvatar: false, + deleteWidget: null, + scrollController: ScrollController(), + customNotificationCard: (n) { + return Text(n.message.subHeader.toString()); + }, + onDelete: (id) async {}, + markAsRead: (id) {}, + onNotificationCardClick: (n) { + func(); + }, + ), + ), + ), + ); + await tester.pumpAndSettle(const Duration(seconds: 1)); + expect(find.text('Test SubHeader'), findsOne); + }); + }); +} diff --git a/test/nullabale_text_test.dart b/test/widgets/nullabale_text_test.dart similarity index 100% rename from test/nullabale_text_test.dart rename to test/widgets/nullabale_text_test.dart diff --git a/test/widgets/siren_inbox_icon_test.dart b/test/widgets/siren_inbox_icon_test.dart new file mode 100644 index 0000000..6a4231a --- /dev/null +++ b/test/widgets/siren_inbox_icon_test.dart @@ -0,0 +1,216 @@ +// ignore_for_file: cascade_invocations + +import 'dart:async'; + +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mockito/annotations.dart'; +import 'package:mockito/mockito.dart'; +import 'package:sirenapp_flutter_inbox/sirenapp_flutter_inbox.dart'; +import 'package:sirenapp_flutter_inbox/src/api/fetch_unviewed_notification_count.dart'; +import 'package:sirenapp_flutter_inbox/src/constants/generics.dart'; +import 'package:sirenapp_flutter_inbox/src/data/siren_data_provider.dart'; +import 'package:sirenapp_flutter_inbox/src/theme/app_theme.dart'; + +import 'siren_inbox_test.mocks.dart'; + +class MockFunction extends Mock { + // Define the mock function signature + void call(); // You can define parameters and return types as needed +} + +@GenerateMocks([SirenDataProvider, FetchUnViewedNotificationsCount]) +void main() { + group('SirenInboxIcon', () { + late StreamController iconController; + late StreamController inboxController; + + setUp(() { + iconController = StreamController.broadcast(); + inboxController = StreamController.broadcast(); + }); + + tearDown(() { + iconController.close(); + inboxController.close(); + }); + + testWidgets('Widget initialization', (WidgetTester tester) async { + await tester.pumpWidget( + const MaterialApp( + home: Scaffold( + body: SirenInboxIcon(), + ), + ), + ); + final mockSirenDataProvider = MockSirenDataProvider(); + final mockFetchUnViewedNotificationsCount = + MockFetchUnViewedNotificationsCount(); + final result = ApiResponse()..isLoading = true; + result.data = 5; + result.isSuccess = true; + + when(mockSirenDataProvider.tokenVerificationStatus) + .thenReturn(Status.SUCCESS); + when( + mockFetchUnViewedNotificationsCount.fetchUnViewedNotificationsCount(), + ).thenAnswer((_) async => result); + + expect(find.byType(SirenInboxIcon), findsOneWidget); + }); + + testWidgets('Disabled widget', (WidgetTester tester) async { + await tester.pumpWidget( + const MaterialApp( + home: Scaffold( + body: Center( + child: SirenInboxIcon( + disabled: true, + ), + ), + ), + ), + ); + + final ignorePointerFinder = find.byWidgetPredicate( + (widget) => + widget is IgnorePointer && + widget.ignoring && + widget.child is GestureDetector, + ); + + expect(ignorePointerFinder, findsOneWidget); + }); + + testWidgets('Widget with custom notification icon', + (WidgetTester tester) async { + await tester.pumpWidget( + const MaterialApp( + home: Scaffold( + body: SirenInboxIcon( + notificationIcon: Icon(Icons.mail), + ), + ), + ), + ); + + expect(find.byIcon(Icons.mail), findsOneWidget); + }); + + testWidgets('Widget updates in dark mode', (WidgetTester tester) async { + await tester.pumpWidget( + const MaterialApp( + home: Scaffold( + body: SirenInboxIcon( + darkMode: true, + ), + ), + ), + ); + + final primaryColor = AppTheme.darkTheme.colorScheme.primary; + + // Ensure dark theme is applied + expect(primaryColor, const Color(0xff232326)); + }); + + testWidgets('Widget disposes controllers on dispose', + (WidgetTester tester) async { + await tester.pumpWidget( + const MaterialApp( + home: Scaffold( + body: SirenInboxIcon(), + ), + ), + ); + + await tester.pumpWidget(Container()); // Dispose the widget + + // Verify controllers are closed + expect(iconController.hasListener, false); + expect(inboxController.hasListener, false); + }); + + testWidgets('Widget with no badge', (WidgetTester tester) async { + const widget = MaterialApp( + home: Scaffold( + body: SirenInboxIcon( + hideBadge: true, + ), + ), + ); + await tester.pumpWidget(widget); + + await tester.pumpWidget(Container()); + await tester.pumpAndSettle(); + expect(find.byType(Positioned), findsNothing); + }); + testWidgets('Widget test on Tap', (WidgetTester tester) async { + final func = MockFunction().call; + final widget = MaterialApp( + home: Scaffold( + body: SirenInboxIcon( + hideBadge: true, + onTap: func, + ), + ), + ); + await tester.pumpWidget(widget); + await tester.tap(find.byType(GestureDetector)); + await tester.pumpAndSettle(const Duration(seconds: 1)); + verify(func()).called(1); + }); + + testWidgets('Stream', (WidgetTester tester) async { + final result = ApiResponse()..isSuccess = true; + const widget = MaterialApp( + home: Scaffold( + body: SirenInboxIcon(), + ), + ); + await tester.pumpWidget(widget); + SirenDataProvider.instance.iconController.sink + .add(StreamResponse(null, UpdateEvents.PARAMS_CHANGED, '')); + SirenDataProvider.instance.iconController.sink + .add(StreamResponse(result, UpdateEvents.VIEW_ALL, '')); + SirenDataProvider.instance.iconController.sink + .add(StreamResponse(result, UpdateEvents.TOKEN_VERIFIED, '')); + }); + + testWidgets('Stream error', (WidgetTester tester) async { + final result = ApiResponse()..isError = true; + final errorFunc = MockFunction().call; + final widget = MaterialApp( + home: Scaffold( + body: SirenInboxIcon( + onError: (e) { + errorFunc(); + }, + ), + ), + ); + await tester.pumpWidget(widget); + + SirenDataProvider.instance.iconController.sink + .add(StreamResponse(result, UpdateEvents.VIEW_ALL, '')); + }); + + testWidgets('Theme', (WidgetTester tester) async { + final widget = MaterialApp( + home: Scaffold( + body: SirenInboxIcon( + darkMode: true, + theme: CustomThemeColors(iconColor: Colors.amber), + ), + ), + ); + await tester.pumpWidget(widget); + + final iconFinder = find.byWidgetPredicate( + (widget) => widget is Icon && widget.color == Colors.amber, + ); + + expect(iconFinder, findsOneWidget); + }); + }); +} diff --git a/test/widgets/siren_inbox_icon_test.mocks.dart b/test/widgets/siren_inbox_icon_test.mocks.dart new file mode 100644 index 0000000..f874f60 --- /dev/null +++ b/test/widgets/siren_inbox_icon_test.mocks.dart @@ -0,0 +1,238 @@ +// Mocks generated by Mockito 5.4.4 from annotations +// in sirenapp_flutter_inbox/test/widgets/siren_inbox_icon_test.dart. +// Do not manually edit this file. + +// ignore_for_file: no_leading_underscores_for_library_prefixes +import 'dart:async' as _i2; + +import 'package:mockito/mockito.dart' as _i1; +import 'package:mockito/src/dummies.dart' as _i6; +import 'package:sirenapp_flutter_inbox/sirenapp_flutter_inbox.dart' as _i4; +import 'package:sirenapp_flutter_inbox/src/api/fetch_unviewed_notification_count.dart' + as _i8; +import 'package:sirenapp_flutter_inbox/src/constants/generics.dart' as _i7; +import 'package:sirenapp_flutter_inbox/src/data/siren_data_provider.dart' + as _i5; +import 'package:sirenapp_flutter_inbox/src/services/api_client.dart' as _i3; + +// ignore_for_file: type=lint +// ignore_for_file: avoid_redundant_argument_values +// ignore_for_file: avoid_setters_without_getters +// ignore_for_file: comment_references +// ignore_for_file: deprecated_member_use +// ignore_for_file: deprecated_member_use_from_same_package +// ignore_for_file: implementation_imports +// ignore_for_file: invalid_use_of_visible_for_testing_member +// ignore_for_file: prefer_const_constructors +// ignore_for_file: unnecessary_parenthesis +// ignore_for_file: camel_case_types +// ignore_for_file: subtype_of_sealed_class + +class _FakeStreamController_0 extends _i1.SmartFake + implements _i2.StreamController { + _FakeStreamController_0( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} + +class _FakeApiClient_1 extends _i1.SmartFake implements _i3.ApiClient { + _FakeApiClient_1( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} + +class _FakeApiResponse_2 extends _i1.SmartFake implements _i4.ApiResponse { + _FakeApiResponse_2( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} + +/// A class which mocks [SirenDataProvider]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockSirenDataProvider extends _i1.Mock implements _i5.SirenDataProvider { + MockSirenDataProvider() { + _i1.throwOnMissingStub(this); + } + + @override + String get userToken => (super.noSuchMethod( + Invocation.getter(#userToken), + returnValue: _i6.dummyValue( + this, + Invocation.getter(#userToken), + ), + ) as String); + + @override + set userToken(String? _userToken) => super.noSuchMethod( + Invocation.setter( + #userToken, + _userToken, + ), + returnValueForMissingStub: null, + ); + + @override + String get recipientId => (super.noSuchMethod( + Invocation.getter(#recipientId), + returnValue: _i6.dummyValue( + this, + Invocation.getter(#recipientId), + ), + ) as String); + + @override + set recipientId(String? _recipientId) => super.noSuchMethod( + Invocation.setter( + #recipientId, + _recipientId, + ), + returnValueForMissingStub: null, + ); + + @override + String get apiDomain => (super.noSuchMethod( + Invocation.getter(#apiDomain), + returnValue: _i6.dummyValue( + this, + Invocation.getter(#apiDomain), + ), + ) as String); + + @override + set apiDomain(String? _apiDomain) => super.noSuchMethod( + Invocation.setter( + #apiDomain, + _apiDomain, + ), + returnValueForMissingStub: null, + ); + + @override + _i2.StreamController<_i4.StreamResponse> get inboxController => + (super.noSuchMethod( + Invocation.getter(#inboxController), + returnValue: _FakeStreamController_0<_i4.StreamResponse>( + this, + Invocation.getter(#inboxController), + ), + ) as _i2.StreamController<_i4.StreamResponse>); + + @override + _i2.StreamController<_i4.StreamResponse> get iconController => + (super.noSuchMethod( + Invocation.getter(#iconController), + returnValue: _FakeStreamController_0<_i4.StreamResponse>( + this, + Invocation.getter(#iconController), + ), + ) as _i2.StreamController<_i4.StreamResponse>); + + @override + _i7.Status get tokenVerificationStatus => (super.noSuchMethod( + Invocation.getter(#tokenVerificationStatus), + returnValue: _i7.Status.PENDING, + ) as _i7.Status); + + @override + _i2.Future initialize() => (super.noSuchMethod( + Invocation.method( + #initialize, + [], + ), + returnValue: _i2.Future.value(), + returnValueForMissingStub: _i2.Future.value(), + ) as _i2.Future); + + @override + void updateParams({ + required String? userToken, + required String? recipientId, + }) => + super.noSuchMethod( + Invocation.method( + #updateParams, + [], + { + #userToken: userToken, + #recipientId: recipientId, + }, + ), + returnValueForMissingStub: null, + ); + + @override + void iconDispose() => super.noSuchMethod( + Invocation.method( + #iconDispose, + [], + ), + returnValueForMissingStub: null, + ); + + @override + void inboxDispose() => super.noSuchMethod( + Invocation.method( + #inboxDispose, + [], + ), + returnValueForMissingStub: null, + ); +} + +/// A class which mocks [FetchUnViewedNotificationsCount]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockFetchUnViewedNotificationsCount extends _i1.Mock + implements _i8.FetchUnViewedNotificationsCount { + MockFetchUnViewedNotificationsCount() { + _i1.throwOnMissingStub(this); + } + + @override + _i3.ApiClient get api => (super.noSuchMethod( + Invocation.getter(#api), + returnValue: _FakeApiClient_1( + this, + Invocation.getter(#api), + ), + ) as _i3.ApiClient); + + @override + set api(_i3.ApiClient? _api) => super.noSuchMethod( + Invocation.setter( + #api, + _api, + ), + returnValueForMissingStub: null, + ); + + @override + _i2.Future<_i4.ApiResponse> fetchUnViewedNotificationsCount() => + (super.noSuchMethod( + Invocation.method( + #fetchUnViewedNotificationsCount, + [], + ), + returnValue: _i2.Future<_i4.ApiResponse>.value(_FakeApiResponse_2( + this, + Invocation.method( + #fetchUnViewedNotificationsCount, + [], + ), + )), + ) as _i2.Future<_i4.ApiResponse>); +} diff --git a/test/widgets/siren_inbox_test.dart b/test/widgets/siren_inbox_test.dart new file mode 100644 index 0000000..1ece45f --- /dev/null +++ b/test/widgets/siren_inbox_test.dart @@ -0,0 +1,207 @@ +// ignore_for_file: unused_local_variable + +import 'dart:async'; + +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mockito/annotations.dart'; +import 'package:mockito/mockito.dart'; +import 'package:sirenapp_flutter_inbox/sirenapp_flutter_inbox.dart'; +import 'package:sirenapp_flutter_inbox/src/api/fetch_unviewed_notification_count.dart'; +import 'package:sirenapp_flutter_inbox/src/constants/generics.dart'; +import 'package:sirenapp_flutter_inbox/src/data/siren_data_provider.dart'; + +import 'siren_inbox_test.mocks.dart'; + +class MockFunction extends Mock { + // Define the mock function signature + void call(); // You can define parameters and return types as needed +} + +@GenerateNiceMocks([ + MockSpec(), + MockSpec(), +]) +void main() { + group('SirenInbox Widget Test', () { + late StreamController iconController; + late StreamController inboxController; + late MockSirenDataProvider mockSirenDataProvider; + final notification = [ + NotificationDataType( + id: '1', + createdAt: '2024-03-15T04:07:14.577928Z', + message: MessageData( + header: 'Test Header', + subHeader: 'Test SubHeader', + body: 'Test Body', + channel: 'Test Channel', + actionUrl: 'Test Action Url', + avatar: AvatarData( + altText: 'Test alt text', + url: 'https://picsum.photos/200/300', + ), + additionalData: 'Test Additional Data', + ), + requestId: 'request-id', + isRead: false, + cardColor: Colors.black, + ), + ]; + + setUp(() { + iconController = StreamController.broadcast(); + inboxController = StreamController.broadcast(); + mockSirenDataProvider = MockSirenDataProvider(); + when(mockSirenDataProvider.tokenVerificationStatus) + .thenReturn(Status.SUCCESS); + }); + + tearDown(() { + iconController.close(); + inboxController.close(); + }); + testWidgets('Initial loading state', (WidgetTester tester) async { + // Mock SirenDataProvider + + await tester.pumpWidget( + const MaterialApp( + home: Scaffold( + body: SirenInbox(), + ), + ), + ); + + // Loading state widget should be displayed + expect(find.byType(LoaderWidget), findsOneWidget); + }); + + // testWidgets('Error state', (WidgetTester tester) async { + // // Mock SirenDataProvider + + // await tester.pumpWidget( + // const MaterialApp( + // home: Scaffold( + // body: SirenInbox(), + // ), + // ), + // ); + + // // Error state widget should be displayed + // expect(find.byType(DefaultErrorWidget), findsOneWidget); + // }); + + testWidgets('Test Title', (WidgetTester tester) async { + // Mock SirenDataProvider + + await tester.pumpWidget( + const MaterialApp( + home: Scaffold( + body: SirenInbox( + title: 'Notifications Header', + ), + ), + ), + ); + + // Loading state widget should be displayed + expect(find.byType(LoaderWidget), findsOneWidget); + + // Simulate a successful fetch + await tester.pump(); + + // Verify that notification list is displayed + expect(find.text('Notifications Header'), findsOneWidget); + }); + + testWidgets('Custom Header', (WidgetTester tester) async { + // Mock SirenDataProvider + + await tester.pumpWidget( + const MaterialApp( + home: Scaffold( + body: SirenInbox( + customHeader: Text('Custom Header'), + ), + ), + ), + ); + + // Loading state widget should be displayed + expect(find.byType(LoaderWidget), findsOneWidget); + + // Simulate a successful fetch + await tester.pump(); + + // Verify that notification list is displayed + expect(find.text('Custom Header'), findsOneWidget); + }); + + testWidgets('Widget Handle back navigation', (WidgetTester tester) async { + final func = MockFunction().call; + final widget = MaterialApp( + home: Scaffold( + body: SirenInbox( + showDefaultBackButton: true, + handleBackNavigation: func, + ), + ), + ); + await tester.pumpWidget(widget); + await tester.tap(find.byType(GestureDetector)); + await tester.pumpAndSettle(const Duration(seconds: 2)); + verify(func()).called(1); + }); + + testWidgets('Show default back button', (WidgetTester tester) async { + const widget = MaterialApp( + home: Scaffold( + body: SirenInbox( + showDefaultBackButton: true, + // defaultBackButton: Icon(Icons.back_hand), + ), + ), + ); + await tester.pumpWidget(widget); + expect(find.byIcon(Icons.arrow_back_ios), findsOneWidget); + }); + + testWidgets('Stream', (WidgetTester tester) async { + final result = ApiResponse()..isSuccess = true; + const widget = MaterialApp( + home: Scaffold( + body: SirenInbox(), + ), + ); + await tester.pumpWidget(widget); + SirenDataProvider.instance.inboxController.sink + .add(StreamResponse(null, UpdateEvents.PARAMS_CHANGED, '')); + SirenDataProvider.instance.inboxController.sink + .add(StreamResponse(null, UpdateEvents.SHOW_ERROR, '')); + SirenDataProvider.instance.inboxController.sink + .add(StreamResponse(result, UpdateEvents.DELETE_ALL, '')); + SirenDataProvider.instance.inboxController.sink + .add(StreamResponse(result, UpdateEvents.TOKEN_VERIFIED, '')); + SirenDataProvider.instance.inboxController.sink + .add(StreamResponse(result, UpdateEvents.READ_ALL, '')); + }); + + testWidgets('Stream error', (WidgetTester tester) async { + final result = ApiResponse()..isError = true; + final errorFunc = MockFunction().call; + final widget = MaterialApp( + home: Scaffold( + body: SirenInbox( + onError: (e) { + errorFunc(); + }, + ), + ), + ); + await tester.pumpWidget(widget); + + SirenDataProvider.instance.inboxController.sink + .add(StreamResponse(result, UpdateEvents.READ_ALL, '')); + }); + }); +} diff --git a/test/widgets/siren_inbox_test.mocks.dart b/test/widgets/siren_inbox_test.mocks.dart new file mode 100644 index 0000000..0843556 --- /dev/null +++ b/test/widgets/siren_inbox_test.mocks.dart @@ -0,0 +1,263 @@ +// Mocks generated by Mockito 5.4.4 from annotations +// in sirenapp_flutter_inbox/test/widgets/siren_inbox_test.dart. +// Do not manually edit this file. + +// ignore_for_file: no_leading_underscores_for_library_prefixes +import 'dart:async' as _i2; + +import 'package:mockito/mockito.dart' as _i1; +import 'package:mockito/src/dummies.dart' as _i6; +import 'package:sirenapp_flutter_inbox/sirenapp_flutter_inbox.dart' as _i4; +import 'package:sirenapp_flutter_inbox/src/api/fetch_unviewed_notification_count.dart' + as _i8; +import 'package:sirenapp_flutter_inbox/src/constants/generics.dart' as _i7; +import 'package:sirenapp_flutter_inbox/src/data/siren_data_provider.dart' + as _i5; +import 'package:sirenapp_flutter_inbox/src/services/api_client.dart' as _i3; + +// ignore_for_file: type=lint +// ignore_for_file: avoid_redundant_argument_values +// ignore_for_file: avoid_setters_without_getters +// ignore_for_file: comment_references +// ignore_for_file: deprecated_member_use +// ignore_for_file: deprecated_member_use_from_same_package +// ignore_for_file: implementation_imports +// ignore_for_file: invalid_use_of_visible_for_testing_member +// ignore_for_file: prefer_const_constructors +// ignore_for_file: unnecessary_parenthesis +// ignore_for_file: camel_case_types +// ignore_for_file: subtype_of_sealed_class + +class _FakeStreamController_0 extends _i1.SmartFake + implements _i2.StreamController { + _FakeStreamController_0( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} + +class _FakeApiClient_1 extends _i1.SmartFake implements _i3.ApiClient { + _FakeApiClient_1( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} + +class _FakeApiResponse_2 extends _i1.SmartFake implements _i4.ApiResponse { + _FakeApiResponse_2( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} + +/// A class which mocks [SirenDataProvider]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockSirenDataProvider extends _i1.Mock implements _i5.SirenDataProvider { + @override + String get userToken => (super.noSuchMethod( + Invocation.getter(#userToken), + returnValue: _i6.dummyValue( + this, + Invocation.getter(#userToken), + ), + returnValueForMissingStub: _i6.dummyValue( + this, + Invocation.getter(#userToken), + ), + ) as String); + + @override + set userToken(String? _userToken) => super.noSuchMethod( + Invocation.setter( + #userToken, + _userToken, + ), + returnValueForMissingStub: null, + ); + + @override + String get recipientId => (super.noSuchMethod( + Invocation.getter(#recipientId), + returnValue: _i6.dummyValue( + this, + Invocation.getter(#recipientId), + ), + returnValueForMissingStub: _i6.dummyValue( + this, + Invocation.getter(#recipientId), + ), + ) as String); + + @override + set recipientId(String? _recipientId) => super.noSuchMethod( + Invocation.setter( + #recipientId, + _recipientId, + ), + returnValueForMissingStub: null, + ); + + @override + String get apiDomain => (super.noSuchMethod( + Invocation.getter(#apiDomain), + returnValue: _i6.dummyValue( + this, + Invocation.getter(#apiDomain), + ), + returnValueForMissingStub: _i6.dummyValue( + this, + Invocation.getter(#apiDomain), + ), + ) as String); + + @override + set apiDomain(String? _apiDomain) => super.noSuchMethod( + Invocation.setter( + #apiDomain, + _apiDomain, + ), + returnValueForMissingStub: null, + ); + + @override + _i2.StreamController<_i4.StreamResponse> get inboxController => + (super.noSuchMethod( + Invocation.getter(#inboxController), + returnValue: _FakeStreamController_0<_i4.StreamResponse>( + this, + Invocation.getter(#inboxController), + ), + returnValueForMissingStub: _FakeStreamController_0<_i4.StreamResponse>( + this, + Invocation.getter(#inboxController), + ), + ) as _i2.StreamController<_i4.StreamResponse>); + + @override + _i2.StreamController<_i4.StreamResponse> get iconController => + (super.noSuchMethod( + Invocation.getter(#iconController), + returnValue: _FakeStreamController_0<_i4.StreamResponse>( + this, + Invocation.getter(#iconController), + ), + returnValueForMissingStub: _FakeStreamController_0<_i4.StreamResponse>( + this, + Invocation.getter(#iconController), + ), + ) as _i2.StreamController<_i4.StreamResponse>); + + @override + _i7.Status get tokenVerificationStatus => (super.noSuchMethod( + Invocation.getter(#tokenVerificationStatus), + returnValue: _i7.Status.PENDING, + returnValueForMissingStub: _i7.Status.PENDING, + ) as _i7.Status); + + @override + _i2.Future initialize() => (super.noSuchMethod( + Invocation.method( + #initialize, + [], + ), + returnValue: _i2.Future.value(), + returnValueForMissingStub: _i2.Future.value(), + ) as _i2.Future); + + @override + void updateParams({ + required String? userToken, + required String? recipientId, + }) => + super.noSuchMethod( + Invocation.method( + #updateParams, + [], + { + #userToken: userToken, + #recipientId: recipientId, + }, + ), + returnValueForMissingStub: null, + ); + + @override + void iconDispose() => super.noSuchMethod( + Invocation.method( + #iconDispose, + [], + ), + returnValueForMissingStub: null, + ); + + @override + void inboxDispose() => super.noSuchMethod( + Invocation.method( + #inboxDispose, + [], + ), + returnValueForMissingStub: null, + ); +} + +/// A class which mocks [FetchUnViewedNotificationsCount]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockFetchUnViewedNotificationsCount extends _i1.Mock + implements _i8.FetchUnViewedNotificationsCount { + @override + _i3.ApiClient get api => (super.noSuchMethod( + Invocation.getter(#api), + returnValue: _FakeApiClient_1( + this, + Invocation.getter(#api), + ), + returnValueForMissingStub: _FakeApiClient_1( + this, + Invocation.getter(#api), + ), + ) as _i3.ApiClient); + + @override + set api(_i3.ApiClient? _api) => super.noSuchMethod( + Invocation.setter( + #api, + _api, + ), + returnValueForMissingStub: null, + ); + + @override + _i2.Future<_i4.ApiResponse> fetchUnViewedNotificationsCount() => + (super.noSuchMethod( + Invocation.method( + #fetchUnViewedNotificationsCount, + [], + ), + returnValue: _i2.Future<_i4.ApiResponse>.value(_FakeApiResponse_2( + this, + Invocation.method( + #fetchUnViewedNotificationsCount, + [], + ), + )), + returnValueForMissingStub: + _i2.Future<_i4.ApiResponse>.value(_FakeApiResponse_2( + this, + Invocation.method( + #fetchUnViewedNotificationsCount, + [], + ), + )), + ) as _i2.Future<_i4.ApiResponse>); +} From 3f9ac8793f8818d706edb69f5281ebdbd5e75a3a Mon Sep 17 00:00:00 2001 From: Ameera Sherin <89637967+Ameera-Sherin@users.noreply.github.com> Date: Tue, 9 Apr 2024 15:29:09 +0530 Subject: [PATCH 02/17] feat: Add min bound for page size, group header props, tests --- .github/workflows/test.yml | 4 +- README.md | 106 +++---- lib/src/constants/strings.dart | 1 + lib/src/models/ui_models.dart | 55 +++- lib/src/widgets/app_bar.dart | 104 +++++++ lib/src/widgets/card.dart | 98 ++++--- lib/src/widgets/error_widget.dart | 2 +- lib/src/widgets/inbox_body.dart | 102 +++++++ lib/src/widgets/loader_widget.dart | 176 ++++++++---- lib/src/widgets/notification_list_view.dart | 16 +- lib/src/widgets/siren_inbox.dart | 267 ++++-------------- lib/src/widgets/siren_inbox_icon.dart | 1 - test/data/siren_data_provider_test.dart | 23 ++ test/models/api_response_test.dart | 2 +- test/models/ui_models_test.dart | 2 +- test/services/api_client_test.dart | 96 +++++++ test/services/network_service_test.dart | 2 - test/widgets/app_bar_test.dart | 141 +++++++++ test/widgets/card_test.dart | 69 ++--- test/widgets/inbox_body_test.dart | 116 ++++++++ test/widgets/loader_widget_test.dart | 4 +- test/widgets/notification_list_view_test.dart | 4 - test/widgets/siren_inbox_test.dart | 111 +++----- 23 files changed, 995 insertions(+), 507 deletions(-) create mode 100644 lib/src/widgets/app_bar.dart create mode 100644 lib/src/widgets/inbox_body.dart create mode 100644 test/widgets/app_bar_test.dart create mode 100644 test/widgets/inbox_body_test.dart diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index b8a15b3..1bc53e7 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -2,9 +2,9 @@ name: test on: push: - branches: [ main, dev ] + branches: [ main, staging, dev ] pull_request: - branches: [ main, dev ] + branches: [ main, staging, dev ] jobs: build: diff --git a/README.md b/README.md index 2d4fbf1..6deb830 100644 --- a/README.md +++ b/README.md @@ -5,6 +5,7 @@ The `sirenapp_flutter_inbox` is a comprehensive and customizable Flutter UI kit for displaying and managing notifications. This documentation provides comprehensive information on how to install, configure, and use the sdk effectively. ## 1. Installation + To install the `sirenapp_flutter_inbox` package, 1. Open your `pubspec.yaml` file. @@ -12,7 +13,9 @@ To install the `sirenapp_flutter_inbox` package, 3. Run `flutter pub get` in your terminal to install the package. ## 2. Configuration + ### 2.1 Initialization + Initialize the sdk with user token and recipient id. Wrap the provider around your App's root. ```dart @@ -32,6 +35,7 @@ void main() { ``` ### 2.2 Configure notification icon + Once the provider is configured, next step is to configure the notification icon This widget consists of a notification icon along with a badge to display the number of unviewed notifications. @@ -39,21 +43,24 @@ This widget consists of a notification icon along with a badge to display the nu ```dart SirenInboxIcon() ``` + #### Arguments for notification icon + Below are optional arguments available for the icon widget: -Arguments | Description | Type | Default value | ---- | --- | --- | --- | -darkMode | Toggle to enable dark mode when custom theme is not passed | boolean | false | -disabled | Toggle to disable click on icon | boolean | false | -hideBadge | Toggle to hide unviewed count badge| boolean | false | -notificationIcon | Option to use custom notification icon | Widget | null | -onError | Callback for handling errors | Function(ApiErrorDetails) | null | -onTap | Custom click handler for notification icon | VoidCallback | null | -theme | Theme properties for custom color theme | CustomThemeColors | null | -customStyles | Style properties for custom styling | SirenStyleProps | null | +| Arguments | Description | Type | Default value | +| ---------------- | ---------------------------------------------------------- | ------------------------- | ------------- | +| darkMode | Toggle to enable dark mode when custom theme is not passed | boolean | false | +| disabled | Toggle to disable click on icon | boolean | false | +| hideBadge | Toggle to hide unviewed count badge | boolean | false | +| notificationIcon | Option to use custom notification icon | Widget | null | +| onError | Callback for handling errors | Function(ApiErrorDetails) | null | +| onTap | Custom click handler for notification icon | VoidCallback | null | +| theme | Theme properties for custom color theme | CustomThemeColors | null | +| customStyles | Style properties for custom styling | SirenStyleProps | null | #### Theme customization + Here are the available theme options: ```dart @@ -62,7 +69,9 @@ theme: CustomThemeColors( iconColor: Colors.white, badgeColor: Colors.white) ``` + #### Style customization + Here are the custom style options for the notification icon: ```dart @@ -78,12 +87,12 @@ customStyles: SirenStyleProps( ``` ### 2.3. Configure notification inbox + Inbox is a paginated list view for displaying notifications. ```dart SirenInbox( theme: customTheme, - title: 'Notifications', hideHeader: false, darkMode: true, onError: (error) () { @@ -91,29 +100,25 @@ SirenInbox( }, ); ``` + #### Arguments for the notification inbox + Given below are the arguments of Siren Inbox Widget. -Arguments | Description | Type | Default value | ---- | --- | --- | --- | -hideHeader | Toggle to hide the header section| boolean | false | -hideClearAll | Toggle to hide clear all button| boolean | false | -showDefaultBackButton | Toggle to display back button in default Inbox app bar | boolean | false | -darkMode | Toggle to enable dark mode when custom theme is not passed | boolean | false | -itemsPerFetch | Number of notifications fetch per api request (have a max cap of 50) | int | 20 | -title | Title of the Inbox app bar | String | null | -defaultBackButton | Custom icon for back button | Icon | null | -listEmptyWidget | Custom widget for empty notification list | Widget | null | -customNotificationCard | Custom widget to display the notification cards | Widget | null | -customLoader | Custom widget to display the initial loading state | Widget | null | -customErrorWidget | Custom error widget| Widget | null | -customHeader | Custom header widget | Widget | null | -cardProps | Properties of notification card | CardParams | false | -onNotificationCardClick | Custom click handler for notification cards | Function(NotificationDataType) | null | -onError | Callback for handling errors | Function(ApiErrorDetails) | null | -handleBackNavigation | Function to handle the back button click | Function | null | -theme | Theme properties for custom color theme | CustomThemeColors | null | -customStyles | Style properties for custom styling | SirenStyleProps | null | +| Arguments | Description | Type | Default value | +| ----------------------- | -------------------------------------------------------------------- | ------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| darkMode | Toggle to enable dark mode when custom theme is not passed | boolean | false | +| itemsPerFetch | Number of notifications fetch per api request (have a max cap of 50) | int | 20 | +| listEmptyWidget | Custom widget for empty notification list | Widget | null | +| customNotificationCard | Custom widget to display the notification cards | Widget | null | +| customLoader | Custom widget to display the initial loading state | Widget | null | +| customErrorWidget | Custom error widget | Widget | null | +| cardProps | Properties of notification card | CardProps | CardProps(hideAvatar: false, disableAutoMarkAsRead: false, hideDelete: false, deleteWidget: Icon(Icons.close), onAvatarClick: Function(NotificationDataType)) | +| inboxHeaderProps | Properties of notification window header | InboxHeaderProps | InboxHeaderProps(hideHeader: false, hideClearAll: false,title: 'Notifications', customHeader: null showBackButton:false, backButton: null, onBackPress: ()=> null ) | +| onNotificationCardClick | Custom click handler for notification cards | Function(NotificationDataType) | null | +| onError | Callback for handling errors | Function(ApiErrorDetails) | null | +| theme | Theme properties for custom color theme | CustomThemeColors | null | +| customStyles | Style properties for custom styling | SirenStyleProps | null | #### Theme customization @@ -136,6 +141,7 @@ theme: CustomThemeColors( inboxTitleColor: const Color.fromRGBO(0, 0, 0, 1), ), ``` + #### Style options Here are some of the custom style options for the notification inbox: @@ -173,34 +179,36 @@ customStyles: SirenStyleProps( ## 3. Siren Class -The `Siren Class` class provides utility functions for modifying notifications. +The `Siren Class` provides utility functions for modifying notifications. ```dart Siren.markAsRead(id: 'notification-id'); ``` -Function | Arguments |Type | Description | ---- | --- | --- |----| -markAllNotificationsAsReadByDate | startDate| ISO date string | Sets the read status of notifications to true until the given date | -markAsRead | id| string | Set read status of a notification to true | -deleteNotification | id| string | Delete a notification by id | -deleteNotificationsByDate | startDate| ISO date string | Delete all notifications until given date | -markNotificationsAsViewed | startDate| ISO date string | Sets the viewed status of notifications to true until the given date | +| Function | Arguments | Type | Description | +| -------------------------------- | --------- | --------------- | -------------------------------------------------------------------- | +| markAllNotificationsAsReadByDate | startDate | ISO date string | Sets the read status of notifications to true until the given date | +| markAsRead | id | string | Set read status of a notification to true | +| deleteNotification | id | string | Delete a notification by id | +| deleteNotificationsByDate | startDate | ISO date string | Delete all notifications until given date | +| markNotificationsAsViewed | startDate | ISO date string | Sets the viewed status of notifications to true until the given date | ## 4. Error Codes + Given below are all possible error codes thrown by the package: -Error code | Description | ---- | --- | -GENERIC_API_ERROR | Occurrence of an unexpected api error | -AUTHENTICATION_FAILED | Verification of the given tokens has failed | -FETCH_COUNT_FAILED | An error occurred while fetching unviewed count | -NOTIFICATION_FETCH_FAILED | An error occurred while fetching notifications | -NOTIFICATION_READ_FAILED | An error occurred while marking notifications as read | -NOTIFICATION_DELETE_FAILED | An error occurred while deleting notifications | -UPDATE_VIEWED_FAILED | An error occurred while updating the viewed status of notifications | +| Error code | Description | +| -------------------------- | ------------------------------------------------------------------- | +| GENERIC_API_ERROR | Occurrence of an unexpected api error | +| AUTHENTICATION_FAILED | Verification of the given tokens has failed | +| FETCH_COUNT_FAILED | An error occurred while fetching unviewed count | +| NOTIFICATION_FETCH_FAILED | An error occurred while fetching notifications | +| NOTIFICATION_READ_FAILED | An error occurred while marking notifications as read | +| NOTIFICATION_DELETE_FAILED | An error occurred while deleting notifications | +| UPDATE_VIEWED_FAILED | An error occurred while updating the viewed status of notifications | ## Example + Here's a basic example to help you get started ```dart @@ -264,4 +272,4 @@ class _MyHomePageState extends State { ); } } -``` \ No newline at end of file +``` diff --git a/lib/src/constants/strings.dart b/lib/src/constants/strings.dart index 38dcf0f..9646fac 100644 --- a/lib/src/constants/strings.dart +++ b/lib/src/constants/strings.dart @@ -6,4 +6,5 @@ class Strings { static const error_title = 'Oops! Something went wrong.'; static const error_desc = 'Could not load the notifications. Please refresh the page.'; + static const clear_all = 'Clear All'; } diff --git a/lib/src/models/ui_models.dart b/lib/src/models/ui_models.dart index 2b8d3c4..deba2d2 100644 --- a/lib/src/models/ui_models.dart +++ b/lib/src/models/ui_models.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:sirenapp_flutter_inbox/src/models/notification_model.dart'; /// Properties for configuring the appearance of the notification card. class CardProps { @@ -6,6 +7,10 @@ class CardProps { const CardProps({ this.hideAvatar, this.showMedia, + this.disableAutoMarkAsRead, + this.deleteWidget, + this.hideDelete, + this.onAvatarClick, }); /// Determines whether to hide the avatar in the notification card in Siren inbox. @@ -13,6 +18,18 @@ class CardProps { /// Determines whether to show media content in the notification card in Siren inbox. final bool? showMedia; + + /// The flag to turn on and off the mark as read functionality + final bool? disableAutoMarkAsRead; + + /// Custom widget that can be used instead of default delete in the card (x) + final Widget? deleteWidget; + + /// Determines whether to hide the avatar in the notification card in Siren inbox. + final bool? hideDelete; + + /// Callback function when a notification card is clicked. + final void Function(NotificationDataType)? onAvatarClick; } /// Customizable style for the Siren notification icon. @@ -186,16 +203,36 @@ class CustomThemeColors { final Color? inboxTitleColor; } -/// Custom Properties for notification card -class CardParams { - CardParams({ - this.hideAvatar, - this.deleteWidget, +/// Properties for configuring the appearance of the notification window app bar. +class InboxHeaderProps { + InboxHeaderProps({ + this.title, + this.hideHeader, + this.showBackButton, + this.backButton, + this.hideClearAll, + this.customHeader, + this.onBackPress, }); - /// The Flag to hide or show avatar - final bool? hideAvatar; + /// Title of the inbox page or window. + final String? title; - /// Custom widget that can be used instead of default delete in the card (x) - final Widget? deleteWidget; + /// Flag to hide the header. + final bool? hideHeader; + + /// Flag to show the header back button provided by the sdk. + final bool? showBackButton; + + /// Default back button widget for the header provided by the sdk. + final Icon? backButton; + + /// Flag to hide the "Clear All" button. + final bool? hideClearAll; + + /// Custom header or appBar widget. + final Widget? customHeader; + + /// Callback function for handling back navigation. + final void Function()? onBackPress; } diff --git a/lib/src/widgets/app_bar.dart b/lib/src/widgets/app_bar.dart new file mode 100644 index 0000000..3c4b155 --- /dev/null +++ b/lib/src/widgets/app_bar.dart @@ -0,0 +1,104 @@ +import 'package:flutter/material.dart'; +import 'package:sirenapp_flutter_inbox/src/constants/strings.dart'; +import 'package:sirenapp_flutter_inbox/src/models/ui_models.dart'; + +class SirenAppBar extends StatelessWidget implements PreferredSizeWidget { + const SirenAppBar({ + required this.theme, + required this.showClearAllButton, + super.key, + this.onClearAllPressed, + this.inboxHeaderProps, + }); + final ThemeData theme; + final VoidCallback? onClearAllPressed; + final bool showClearAllButton; + final InboxHeaderProps? inboxHeaderProps; + + @override + Size get preferredSize { + return inboxHeaderProps?.hideHeader ?? false + ? Size.zero + : const Size.fromHeight(kToolbarHeight); + } + + @override + Widget build(BuildContext context) { + if (inboxHeaderProps?.hideHeader ?? false) { + return const SizedBox.shrink(); + } + + return Container( + decoration: BoxDecoration( + color: theme.colorScheme.primary, + border: Border( + bottom: BorderSide( + color: theme.colorScheme.surfaceTint, + ), + ), + ), + height: preferredSize.height, + child: inboxHeaderProps?.customHeader ?? + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Row( + children: [ + if (inboxHeaderProps?.showBackButton ?? false) + Padding( + padding: const EdgeInsets.only( + left: 24, + right: 16, + ), + child: IconButton( + onPressed: inboxHeaderProps?.onBackPress, + icon: inboxHeaderProps?.backButton ?? + const Icon(Icons.arrow_back_ios), + ), + ), + Padding( + padding: EdgeInsets.symmetric( + horizontal: + (inboxHeaderProps?.showBackButton ?? false) ? 2 : 24, + ), + child: Text( + inboxHeaderProps?.title ?? 'Notifications', + style: const TextStyle( + fontSize: 18, + fontWeight: FontWeight.w600, + ), + ), + ), + ], + ), + if (!(inboxHeaderProps?.hideClearAll ?? false) && + showClearAllButton) + Padding( + padding: const EdgeInsets.only(right: 24), + child: GestureDetector( + onTap: onClearAllPressed, + child: const Row( + children: [ + Padding( + padding: EdgeInsets.only(right: 4), + child: Icon( + Icons.clear_all, + size: 24, + ), + ), + Text( + Strings.clear_all, + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.w500, + ), + ), + ], + ), + ), + ), + ], + ), + ); + } +} diff --git a/lib/src/widgets/card.dart b/lib/src/widgets/card.dart index 0306b48..9f1bdb0 100644 --- a/lib/src/widgets/card.dart +++ b/lib/src/widgets/card.dart @@ -13,7 +13,6 @@ class CardWidget extends StatefulWidget { required this.styles, required this.onDelete, super.key, - this.deleteWidget, }); /// Callback function invoked when the card is tapped. @@ -31,9 +30,6 @@ class CardWidget extends StatefulWidget { /// Callback function invoked when the card is deleted. final void Function(String) onDelete; - /// Widget to be displayed for deletion, if provided. - final Widget? deleteWidget; - @override State createState() => _CardWidgetState(); } @@ -56,42 +52,40 @@ class _CardWidgetState extends State { decoration: widget.styles?.container ?? _getDefaultContainerDecoration(currentTheme), child: Padding( - padding: const EdgeInsets.symmetric(vertical: 12), - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 24), - child: Container( - decoration: widget.styles?.contentContainer, - child: Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - if (!(widget.cardProps.hideAvatar ?? false)) - _buildDefaultAvatarContainer(currentTheme), - Expanded( - child: Container( - decoration: widget.styles?.cardContentContainer, - child: Padding( - padding: const EdgeInsets.symmetric( - horizontal: 16, - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - _buildHeaderText(currentTheme), - _buildSubHeaderText(currentTheme), - _buildBodyText(currentTheme), - _buildFooterRow(currentTheme), - ], - ), + padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 24), + child: Container( + decoration: widget.styles?.contentContainer, + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (!(widget.cardProps.hideAvatar ?? false)) + _buildDefaultAvatarContainer(currentTheme), + Expanded( + child: Container( + decoration: widget.styles?.cardContentContainer, + child: Padding( + padding: const EdgeInsets.only( + right: 16, + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _buildHeaderText(currentTheme), + _buildSubHeaderText(currentTheme), + _buildBodyText(currentTheme), + _buildFooterRow(currentTheme), + ], ), ), ), + ), + if (!(widget.cardProps.hideDelete ?? false)) GestureDetector( onTap: () => widget.onDelete(widget.notification.id), - child: widget.deleteWidget ?? + child: widget.cardProps.deleteWidget ?? _buildDefaultDeleteButton(currentTheme), ), - ], - ), + ], ), ), ), @@ -127,20 +121,30 @@ class _CardWidgetState extends State { Widget _buildDefaultAvatarContainer(ThemeData theme) { final avatarUrl = widget.notification.message.avatar?.url; - return Container( - decoration: widget.styles?.cardAvatarContainer, - child: CircleAvatar( - radius: 21, - backgroundImage: avatarUrl != null && avatarUrl.isNotEmpty - ? NetworkImage(avatarUrl) - : null, - backgroundColor: theme.colorScheme.onSecondary, - child: avatarUrl == null || avatarUrl.isEmpty - ? Icon( - Icons.landscape_rounded, - color: theme.colorScheme.surfaceVariant, - ) - : null, + return GestureDetector( + onTap: () { + widget.cardProps.onAvatarClick?.call(widget.notification); + }, + child: Padding( + padding: const EdgeInsets.only( + right: 24, + ), + child: Container( + decoration: widget.styles?.cardAvatarContainer, + child: CircleAvatar( + radius: 21, + backgroundImage: avatarUrl != null && avatarUrl.isNotEmpty + ? NetworkImage(avatarUrl) + : null, + backgroundColor: theme.colorScheme.onSecondary, + child: avatarUrl == null || avatarUrl.isEmpty + ? Icon( + Icons.landscape_rounded, + color: theme.colorScheme.surfaceVariant, + ) + : null, + ), + ), ), ); } diff --git a/lib/src/widgets/error_widget.dart b/lib/src/widgets/error_widget.dart index 1d2d587..4099023 100644 --- a/lib/src/widgets/error_widget.dart +++ b/lib/src/widgets/error_widget.dart @@ -60,7 +60,7 @@ Widget _buildCircle(ThemeData theme) { child: Icon( Icons.warning_rounded, size: 84, - color: theme.colorScheme.surfaceTint, + color: theme.colorScheme.shadow, ), ); } diff --git a/lib/src/widgets/inbox_body.dart b/lib/src/widgets/inbox_body.dart new file mode 100644 index 0000000..8f798d7 --- /dev/null +++ b/lib/src/widgets/inbox_body.dart @@ -0,0 +1,102 @@ +import 'package:flutter/material.dart'; +import 'package:sirenapp_flutter_inbox/sirenapp_flutter_inbox.dart'; +import 'package:sirenapp_flutter_inbox/src/widgets/empty_widget.dart'; +import 'package:sirenapp_flutter_inbox/src/widgets/error_widget.dart'; +import 'package:sirenapp_flutter_inbox/src/widgets/loader_widget.dart'; +import 'package:sirenapp_flutter_inbox/src/widgets/notification_list_view.dart'; + +class InboxBody extends StatelessWidget { + const InboxBody({ + required this.currentTheme, + required this.isLoading, + required this.loadingNextPage, + required this.isError, + required this.notifications, + required this.deleteNotification, + required this.markAsRead, + required this.customNotificationCard, + required this.onNotificationCardClick, + required this.deletingNotificationId, + required this.disableAutoMarkAsRead, + required this.totalElements, + required this.onRefresh, + required this.endReached, + required this.onEndReached, + required this.scrollController, + this.customErrorWidget, + this.customLoader, + this.customStyles, + this.cardProps, + this.listEmptyWidget, + super.key, + }); + final ThemeData currentTheme; + final bool isLoading; + final bool loadingNextPage; + final bool isError; + final List notifications; + final Future Function(String) deleteNotification; + final void Function(String) markAsRead; + final Widget Function(NotificationDataType)? customNotificationCard; + final void Function(NotificationDataType)? onNotificationCardClick; + final String? deletingNotificationId; + final bool disableAutoMarkAsRead; + final int totalElements; + final Future Function() onRefresh; + final Widget? customErrorWidget; + final Widget? customLoader; + final bool endReached; + final SirenStyleProps? customStyles; + final CardProps? cardProps; + final VoidCallback onEndReached; + final ScrollController scrollController; + final Widget? listEmptyWidget; + + @override + Widget build(BuildContext context) { + if (isError) { + return RefreshIndicator( + color: currentTheme.colorScheme.secondary, + backgroundColor: currentTheme.colorScheme.primary, + onRefresh: onRefresh, + child: ListView( + physics: const AlwaysScrollableScrollPhysics(), + children: [ + SizedBox( + height: MediaQuery.of(context).size.height * 0.75, + width: MediaQuery.of(context).size.width, + child: Center( + child: customErrorWidget ?? const DefaultErrorWidget(), + ), + ), + ], + ), + ); + } else if (isLoading && !loadingNextPage) { + return LoaderWidget( + customLoader: customLoader, + hideAvatar: cardProps?.hideAvatar ?? false, + ); + } else if (notifications.isEmpty) { + return listEmptyWidget ?? const EmptyWidget(); + } else { + return NotificationListView( + notifications: notifications, + isLoading: isLoading, + endReached: endReached, + onRefresh: onRefresh, + onEndReached: onEndReached, + loadingNextPage: loadingNextPage, + customStyles: customStyles, + scrollController: scrollController, + onDelete: deleteNotification, + markAsRead: markAsRead, + customNotificationCard: customNotificationCard, + onNotificationCardClick: onNotificationCardClick, + deletingNotificationId: deletingNotificationId, + totalElements: totalElements, + cardProps: cardProps, + ); + } + } +} diff --git a/lib/src/widgets/loader_widget.dart b/lib/src/widgets/loader_widget.dart index 3f552a2..63230b1 100644 --- a/lib/src/widgets/loader_widget.dart +++ b/lib/src/widgets/loader_widget.dart @@ -1,10 +1,39 @@ import 'package:flutter/material.dart'; +import 'package:sirenapp_flutter_inbox/src/constants/generics.dart'; + +class LoaderWidget extends StatelessWidget { + const LoaderWidget({ + required this.hideAvatar, + super.key, + this.customLoader, + }); + + final Widget? customLoader; + final bool hideAvatar; + + @override + Widget build(BuildContext context) { + return customLoader ?? + ListView.builder( + itemCount: Generics.PAGE_SIZE, + itemBuilder: (context, index) { + return Padding( + padding: const EdgeInsets.symmetric(vertical: 8), + child: CardLoaderWidget(hideAvatar: hideAvatar), + ); + }, + ); + } +} class CardLoaderWidget extends StatefulWidget { const CardLoaderWidget({ + required this.hideAvatar, super.key, }); + final bool hideAvatar; + @override CardLoaderWidgetState createState() => CardLoaderWidgetState(); } @@ -39,30 +68,92 @@ class CardLoaderWidgetState extends State child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ - _buildAnimatedCircleAvatar(theme: currentTheme), + if (!widget.hideAvatar) + Padding( + padding: const EdgeInsets.only(right: 24), + child: _buildAnimatedWidget( + theme: currentTheme, + builder: (context, child) => Container( + width: 42, + height: 42, + decoration: BoxDecoration( + shape: BoxShape.circle, + color: currentTheme.colorScheme.onSecondary + .withOpacity(0.5 + 0.5 * _controller.value), + ), + ), + ), + ), Expanded( child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 24), + padding: const EdgeInsets.only( + right: 24, + ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - _buildAnimatedContainer(height: 18, theme: currentTheme), + _buildAnimatedWidget( + theme: currentTheme, + builder: (context, child) => Container( + height: 18, + decoration: BoxDecoration( + color: currentTheme.colorScheme.onSecondary + .withOpacity(0.5 + 0.5 * _controller.value), + borderRadius: BorderRadius.circular(4), + ), + ), + ), const SizedBox(height: 8), - _buildAnimatedContainer(height: 18, theme: currentTheme), + _buildAnimatedWidget( + theme: currentTheme, + builder: (context, child) => Container( + height: 18, + decoration: BoxDecoration( + color: currentTheme.colorScheme.onSecondary + .withOpacity(0.5 + 0.5 * _controller.value), + borderRadius: BorderRadius.circular(4), + ), + ), + ), const SizedBox(height: 8), - _buildAnimatedContainer(height: 18, theme: currentTheme), + _buildAnimatedWidget( + theme: currentTheme, + builder: (context, child) => Container( + height: 18, + decoration: BoxDecoration( + color: currentTheme.colorScheme.onSecondary + .withOpacity(0.5 + 0.5 * _controller.value), + borderRadius: BorderRadius.circular(4), + ), + ), + ), const SizedBox(height: 8), Row( children: [ - _buildAnimatedCircleAvatar( + _buildAnimatedWidget( theme: currentTheme, - radius: 5, + builder: (context, child) => Container( + width: 10, + height: 10, + decoration: BoxDecoration( + shape: BoxShape.circle, + color: currentTheme.colorScheme.onSecondary + .withOpacity(0.5 + 0.5 * _controller.value), + ), + ), ), const SizedBox(width: 8), Expanded( - child: _buildAnimatedContainer( + child: _buildAnimatedWidget( theme: currentTheme, - height: 12, + builder: (context, child) => Container( + height: 12, + decoration: BoxDecoration( + color: currentTheme.colorScheme.onSecondary + .withOpacity(0.5 + 0.5 * _controller.value), + borderRadius: BorderRadius.circular(4), + ), + ), ), ), ], @@ -71,67 +162,30 @@ class CardLoaderWidgetState extends State ), ), ), - _buildAnimatedDeleteIcon(theme: currentTheme), + _buildAnimatedWidget( + theme: currentTheme, + builder: (context, child) => Container( + width: 16, + height: 16, + decoration: BoxDecoration( + color: currentTheme.colorScheme.onSecondary + .withOpacity(0.5 + 0.5 * _controller.value), + borderRadius: BorderRadius.circular(4), + ), + ), + ), ], ), ); } - Widget _buildAnimatedCircleAvatar({ - required ThemeData theme, - double radius = 21, - }) { - return AnimatedBuilder( - animation: _controller, - builder: (context, child) { - return Container( - width: radius * 2, - height: radius * 2, - decoration: BoxDecoration( - shape: BoxShape.circle, - color: theme.colorScheme.onSecondary - .withOpacity(0.5 + 0.5 * _controller.value), - ), - ); - }, - ); - } - - Widget _buildAnimatedContainer({ + Widget _buildAnimatedWidget({ required ThemeData theme, - required double height, + required Widget Function(BuildContext, Widget?) builder, }) { return AnimatedBuilder( animation: _controller, - builder: (context, child) { - return Container( - height: height, - decoration: BoxDecoration( - color: theme.colorScheme.onSecondary - .withOpacity(0.5 + 0.5 * _controller.value), - borderRadius: BorderRadius.circular(4), - ), - ); - }, - ); - } - - Widget _buildAnimatedDeleteIcon({ - required ThemeData theme, - }) { - return AnimatedBuilder( - animation: _controller, - builder: (context, child) { - return Container( - width: 16, - height: 16, - decoration: BoxDecoration( - color: theme.colorScheme.onSecondary - .withOpacity(0.5 + 0.5 * _controller.value), - borderRadius: BorderRadius.circular(4), - ), - ); - }, + builder: builder, ); } } diff --git a/lib/src/widgets/notification_list_view.dart b/lib/src/widgets/notification_list_view.dart index 6a05034..6f864b5 100644 --- a/lib/src/widgets/notification_list_view.dart +++ b/lib/src/widgets/notification_list_view.dart @@ -11,16 +11,14 @@ class NotificationListView extends StatefulWidget { required this.onEndReached, required this.loadingNextPage, required this.customStyles, - required this.hideAvatar, - required this.deleteWidget, required this.scrollController, required this.onDelete, required this.markAsRead, this.customNotificationCard, this.onNotificationCardClick, this.deletingNotificationId, - this.disableAutoMarkAsRead, this.totalElements, + this.cardProps, super.key, }); @@ -31,16 +29,14 @@ class NotificationListView extends StatefulWidget { final Future Function() onRefresh; final VoidCallback onEndReached; final SirenStyleProps? customStyles; - final bool? hideAvatar; - final Widget? deleteWidget; final ScrollController scrollController; final Future Function(String) onDelete; final void Function(String) markAsRead; final Widget Function(NotificationDataType)? customNotificationCard; final void Function(NotificationDataType)? onNotificationCardClick; final String? deletingNotificationId; - final bool? disableAutoMarkAsRead; final int? totalElements; + final CardProps? cardProps; @override State createState() => _NotificationListViewState(); @@ -86,19 +82,15 @@ class _NotificationListViewState extends State { ?.call(widget.notifications[index]) ?? CardWidget( onTap: (notification) { - if (!(widget.disableAutoMarkAsRead ?? false)) { + if (!(widget.cardProps?.disableAutoMarkAsRead ?? false)) { widget.markAsRead(widget.notifications[index].id); } widget.onNotificationCardClick ?.call(widget.notifications[index]); }, notification: widget.notifications[index], - cardProps: CardProps( - hideAvatar: widget.hideAvatar, - showMedia: true, - ), + cardProps: widget.cardProps ?? const CardProps(), styles: widget.customStyles, - deleteWidget: widget.deleteWidget, onDelete: widget.onDelete, ); return AnimatedOpacity( diff --git a/lib/src/widgets/siren_inbox.dart b/lib/src/widgets/siren_inbox.dart index 25054da..959602b 100644 --- a/lib/src/widgets/siren_inbox.dart +++ b/lib/src/widgets/siren_inbox.dart @@ -12,53 +12,33 @@ import 'package:sirenapp_flutter_inbox/src/constants/generics.dart'; import 'package:sirenapp_flutter_inbox/src/data/siren_data_provider.dart'; import 'package:sirenapp_flutter_inbox/src/theme/app_theme.dart'; import 'package:sirenapp_flutter_inbox/src/utils/common_utils.dart'; -import 'package:sirenapp_flutter_inbox/src/widgets/empty_widget.dart'; -import 'package:sirenapp_flutter_inbox/src/widgets/error_widget.dart'; -import 'package:sirenapp_flutter_inbox/src/widgets/loader_widget.dart'; -import 'package:sirenapp_flutter_inbox/src/widgets/notification_list_view.dart'; +import 'package:sirenapp_flutter_inbox/src/widgets/app_bar.dart'; +import 'package:sirenapp_flutter_inbox/src/widgets/inbox_body.dart'; /// Widget for displaying an inbox of notifications. class SirenInbox extends StatefulWidget { const SirenInbox({ super.key, - this.hideHeader, - this.hideClearAll, - this.showDefaultBackButton, this.darkMode, this.itemsPerFetch, - this.title, - this.defaultBackButton, this.listEmptyWidget, this.customNotificationCard, this.customLoader, this.customErrorWidget, - this.customHeader, this.cardProps, this.onNotificationCardClick, this.onError, - this.handleBackNavigation, this.theme, this.customStyles, + this.inboxHeaderProps, }); /// Custom styles for the card of each notification. final SirenStyleProps? customStyles; - /// Flag to hide the header. - final bool? hideHeader; - /// Widget to display when the notification list is empty. final Widget? listEmptyWidget; - /// Title of the inbox page or window. - final String? title; - - /// Flag to show the header back button provided by the sdk. - final bool? showDefaultBackButton; - - /// Default back button widget for the header provided by the sdk. - final Icon? defaultBackButton; - /// Custom builder for notification cards. final Widget Function(NotificationDataType)? customNotificationCard; @@ -68,9 +48,6 @@ class SirenInbox extends StatefulWidget { /// Callback function for handling errors. final void Function(ApiErrorDetails)? onError; - /// Flag to hide the "Clear All" button. - final bool? hideClearAll; - /// Flag for enabling dark mode. final bool? darkMode; @@ -83,18 +60,14 @@ class SirenInbox extends StatefulWidget { /// Custom error widget. final Widget? customErrorWidget; - /// Custom header or appBar widget. - final Widget? customHeader; - - /// Callback function for handling back navigation. - final void Function()? handleBackNavigation; - /// Notifications to be fetched in each request final int? itemsPerFetch; - ///Custom params for Card properties - final CardParams? cardProps; + ///Custom props for Card properties + final CardProps? cardProps; + /// Custom props for header properties + final InboxHeaderProps? inboxHeaderProps; @override State createState() => _SirenInboxState(); } @@ -106,7 +79,7 @@ class _SirenInboxState extends State { bool isError = false; bool loadingNextPage = false; int currentPage = 0; - late int totalElements; + int totalElements = 0; String? deletingNotificationId; int pageSize = 20; @@ -119,7 +92,7 @@ class _SirenInboxState extends State { @override void initState() { super.initState(); - pageSize = min(widget.itemsPerFetch ?? Generics.PAGE_SIZE, 50); + pageSize = max(min(widget.itemsPerFetch ?? Generics.PAGE_SIZE, 50), 0); _periodicUpdateRef = Timer(const Duration(days: 1), () {}); _scrollController = ScrollController(); _scrollController.addListener(_scrollListener); @@ -135,7 +108,6 @@ class _SirenInboxState extends State { _scrollController.dispose(); _periodicUpdateRef?.cancel(); _subscription.cancel(); - SirenDataProvider.instance.inboxDispose(); super.dispose(); } @@ -208,11 +180,8 @@ class _SirenInboxState extends State { } } - PreferredSize _buildAppBar(ThemeData theme) { - return PreferredSize( - preferredSize: const Size.fromHeight(kToolbarHeight), - child: widget.customHeader ?? _buildCustomAppBar(theme, kToolbarHeight), - ); + bool shouldShowClearAllButton() { + return !isError && !isLoading && notifications.isNotEmpty; } void _markNotificationAsReadById(String? notificationId) { @@ -376,6 +345,13 @@ class _SirenInboxState extends State { data: data, ); if (deleteAllResponse.isSuccess) { + SirenDataProvider.instance.inboxController.sink.add( + StreamResponse( + deleteAllResponse, + UpdateEvents.DELETE_ALL, + '', + ), + ); _deleteAllNotifications(); } else if (deleteAllResponse.isError) { widget.onError?.call(deleteAllResponse.error ?? ApiErrorDetails()); @@ -395,7 +371,13 @@ class _SirenInboxState extends State { } await Future.delayed(const Duration(milliseconds: 500)); - + SirenDataProvider.instance.inboxController.sink.add( + StreamResponse( + deletionStatus, + UpdateEvents.DELETE_BY_ID, + id, + ), + ); if (mounted) { setState(() { deletingNotificationId = null; @@ -457,6 +439,13 @@ class _SirenInboxState extends State { final readStatus = await _readNotificationById.readNotificationById(notificationId: id); if (readStatus.isSuccess) { + SirenDataProvider.instance.inboxController.sink.add( + StreamResponse( + readStatus, + UpdateEvents.READ_BY_ID, + id, + ), + ); _markNotificationAsReadById(id); } else if (readStatus.isError) { widget.onError?.call(readStatus.error ?? ApiErrorDetails()); @@ -477,173 +466,41 @@ class _SirenInboxState extends State { child: Builder( builder: (context) { final currentTheme = Theme.of(context); - return Scaffold( backgroundColor: currentTheme.colorScheme.primary, - appBar: - widget.hideHeader ?? false ? null : _buildAppBar(currentTheme), - body: isError - ? RefreshIndicator( - color: currentTheme.colorScheme.secondary, - backgroundColor: currentTheme.colorScheme.primary, - onRefresh: onRefresh, - child: ListView( - physics: const AlwaysScrollableScrollPhysics(), - children: [ - SizedBox( - height: MediaQuery.of(context).size.height * 0.75, - width: MediaQuery.of(context).size.width, - child: Center( - child: widget.customErrorWidget ?? - const DefaultErrorWidget(), - ), - ), - ], - ), - ) - : (isLoading && !loadingNextPage) - ? LoaderWidget( - customLoader: widget.customLoader, - ) - : _buildBody(currentTheme), - ); - }, - ), - ); - } - - Widget _buildCustomAppBar(ThemeData theme, double appBarHeight) { - return Container( - decoration: BoxDecoration( - color: theme.colorScheme.primary, - border: Border( - bottom: BorderSide( - color: theme.colorScheme.surfaceTint, - ), - ), - ), - height: appBarHeight, - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Row( - children: [ - if (widget.showDefaultBackButton ?? false) - IconButton( - onPressed: () { - Navigator.of(context).pop(); - if (widget.handleBackNavigation != null) { - widget.handleBackNavigation?.call(); - } - }, - icon: widget.defaultBackButton ?? - const Icon(Icons.arrow_back_ios), - ), - Padding( - padding: EdgeInsets.symmetric( - horizontal: widget.showDefaultBackButton ?? false ? 2 : 24, - ), - child: Text( - widget.title ?? 'Notifications', - style: widget.customStyles?.defaultHeaderTextStyle ?? - TextStyle( - fontSize: 18, - fontWeight: FontWeight.w600, - color: theme.colorScheme.onBackground, - ), - ), - ), - ], - ), - Padding( - padding: const EdgeInsets.only( - right: 24, + appBar: SirenAppBar( + theme: currentTheme, + onClearAllPressed: onBulkDelete, + showClearAllButton: shouldShowClearAllButton(), + inboxHeaderProps: widget.inboxHeaderProps, ), - child: Row( - children: [ - if (!(widget.hideClearAll ?? false) && - (!isError && !isLoading && notifications.isNotEmpty)) - GestureDetector( - onTap: onBulkDelete, - child: Row( - children: [ - Padding( - padding: const EdgeInsets.only(right: 4), - child: Icon( - Icons.clear_all, - color: theme.colorScheme.outline, - size: 24, - ), - ), - Text( - 'Clear All', - style: TextStyle( - fontSize: 14, - fontWeight: FontWeight.w500, - color: theme.colorScheme.outline, - ), - ), - ], - ), - ), - ], + body: InboxBody( + currentTheme: currentTheme, + isLoading: isLoading, + loadingNextPage: loadingNextPage, + isError: isError, + notifications: notifications, + deleteNotification: deleteNotification, + markAsRead: _markNotificationAsRead, + customNotificationCard: widget.customNotificationCard, + onNotificationCardClick: widget.onNotificationCardClick, + deletingNotificationId: deletingNotificationId, + disableAutoMarkAsRead: + widget.cardProps?.disableAutoMarkAsRead ?? false, + totalElements: totalElements, + onRefresh: onRefresh, + customErrorWidget: widget.customErrorWidget, + customLoader: widget.customLoader, + endReached: endReached, + customStyles: widget.customStyles, + cardProps: widget.cardProps, + scrollController: _scrollController, + onEndReached: onEndReached, + listEmptyWidget: widget.listEmptyWidget, ), - ), - ], + ); + }, ), ); } - - Widget _buildBody(ThemeData theme) { - if (notifications.isEmpty && isLoading) { - return LoaderWidget( - customLoader: widget.customLoader, - ); - } else if (notifications.isEmpty && !isLoading) { - return widget.listEmptyWidget ?? const EmptyWidget(); - } else { - return NotificationListView( - notifications: notifications, - isLoading: isLoading, - endReached: endReached, - onRefresh: onRefresh, - onEndReached: onEndReached, - loadingNextPage: loadingNextPage, - customStyles: widget.customStyles, - deleteWidget: widget.cardProps?.deleteWidget, - hideAvatar: widget.cardProps?.hideAvatar, - scrollController: _scrollController, - onDelete: deleteNotification, - markAsRead: _markNotificationAsRead, - customNotificationCard: widget.customNotificationCard, - onNotificationCardClick: widget.onNotificationCardClick, - deletingNotificationId: deletingNotificationId, - disableAutoMarkAsRead: true, - totalElements: totalElements, - ); - } - } -} - -class LoaderWidget extends StatelessWidget { - const LoaderWidget({ - super.key, - this.customLoader, - }); - - final Widget? customLoader; - - @override - Widget build(BuildContext context) { - return customLoader ?? - ListView.builder( - itemCount: Generics.PAGE_SIZE, - itemBuilder: (context, index) { - return const Padding( - padding: EdgeInsets.symmetric(vertical: 8), - child: CardLoaderWidget(), - ); - }, - ); - } } diff --git a/lib/src/widgets/siren_inbox_icon.dart b/lib/src/widgets/siren_inbox_icon.dart index 1e64f3c..573a3e6 100644 --- a/lib/src/widgets/siren_inbox_icon.dart +++ b/lib/src/widgets/siren_inbox_icon.dart @@ -74,7 +74,6 @@ class _SirenInboxIconState extends State { super.dispose(); _periodicUpdateRef.cancel(); _subscription.cancel(); - SirenDataProvider.instance.iconDispose(); } void _subscribeToStream() { diff --git a/test/data/siren_data_provider_test.dart b/test/data/siren_data_provider_test.dart index 1110e3e..8034716 100644 --- a/test/data/siren_data_provider_test.dart +++ b/test/data/siren_data_provider_test.dart @@ -1,16 +1,23 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:mockito/annotations.dart'; +import 'package:mockito/mockito.dart'; +import 'package:sirenapp_flutter_inbox/sirenapp_flutter_inbox.dart'; import 'package:sirenapp_flutter_inbox/src/api/verify_token.dart'; +import 'package:sirenapp_flutter_inbox/src/constants/generics.dart'; import 'package:sirenapp_flutter_inbox/src/data/siren_data_provider.dart'; +import 'siren_data_provider_test.mocks.dart'; + @GenerateNiceMocks([ MockSpec(), ]) void main() { late SirenDataProvider sirenDataProvider; + late MockVerifyToken mockVerifyToken; setUp(() { sirenDataProvider = SirenDataProvider.instance..initialize(); + mockVerifyToken = MockVerifyToken(); }); group('SirenDataProvider', () { @@ -41,5 +48,21 @@ void main() { // Verify that inbox controller is closed expect(sirenDataProvider.inboxController.isClosed, false); }); + + test('Handles retry logic on token verification failure', () async { + final failedResponse = ApiResponse(data: false); + when(mockVerifyToken.verifyToken()) + .thenAnswer((_) async => failedResponse); + + await sirenDataProvider.initialize(); + + sirenDataProvider.updateParams(userToken: 'token', recipientId: '123'); + + await Future.delayed( + const Duration(seconds: Generics.DATA_FETCH_INTERVAL) * + (Generics.MAX_RETRIES + 1)); + + expect(sirenDataProvider.tokenVerificationStatus, Status.FAILED); + }); }); } diff --git a/test/models/api_response_test.dart b/test/models/api_response_test.dart index 5831d22..a00477a 100644 --- a/test/models/api_response_test.dart +++ b/test/models/api_response_test.dart @@ -14,7 +14,7 @@ void main() { 'currentPage': '1', 'first': 'first', 'totalElements': '50', - }, + } }; final response = ApiResponse.fromJson(json); diff --git a/test/models/ui_models_test.dart b/test/models/ui_models_test.dart index ccaf27d..bc0f04c 100644 --- a/test/models/ui_models_test.dart +++ b/test/models/ui_models_test.dart @@ -126,7 +126,7 @@ void main() { const Widget deleteWidget = Icon(Icons.delete); // Act - final cardParams = CardParams( + const cardParams = CardProps( hideAvatar: hideAvatar, deleteWidget: deleteWidget, ); diff --git a/test/services/api_client_test.dart b/test/services/api_client_test.dart index f89bc69..9c76402 100644 --- a/test/services/api_client_test.dart +++ b/test/services/api_client_test.dart @@ -195,5 +195,101 @@ void main() { expect(response.data, responseData); expect(response.statusCode, responseStatusCode); }); + + test('Handles DioException on GET request', () async { + // Simulate DioException when making a GET request + when(mockDio.get(any, queryParameters: anyNamed('queryParameters'))) + .thenThrow( + DioException( + requestOptions: RequestOptions(), + response: Response( + data: 'Error message', + statusCode: 404, + requestOptions: RequestOptions(), + ), + ), + ); + + // Perform the GET request using ApiClient + final result = await apiClient.get(path: '/example'); + + expect(result.data, 'Error message'); + expect( + result.statusCode, + 404, + ); + }); + + test('Handles DioException on POST request', () async { + // Simulate DioException when making a GET request + when(mockDio.post(any, queryParameters: anyNamed('queryParameters'))) + .thenThrow( + DioException( + requestOptions: RequestOptions(), + response: Response( + data: 'Error message', + statusCode: 404, + requestOptions: RequestOptions(), + ), + ), + ); + + // Perform the GET request using ApiClient + final result = await apiClient.post(path: '/example'); + + expect(result.data, 'Error message'); + expect( + result.statusCode, + 404, + ); + }); + + test('Handles DioException on Patch request', () async { + // Simulate DioException when making a GET request + when(mockDio.patch(any, queryParameters: anyNamed('queryParameters'))) + .thenThrow( + DioException( + requestOptions: RequestOptions(), + response: Response( + data: 'Error message', + statusCode: 404, + requestOptions: RequestOptions(), + ), + ), + ); + + // Perform the GET request using ApiClient + final result = await apiClient.patch(path: '/example'); + + expect(result.data, 'Error message'); + expect( + result.statusCode, + 404, + ); + }); + + test('Handles DioException on delete request', () async { + // Simulate DioException when making a GET request + when(mockDio.delete(any, queryParameters: anyNamed('queryParameters'))) + .thenThrow( + DioException( + requestOptions: RequestOptions(), + response: Response( + data: 'Error message', + statusCode: 404, + requestOptions: RequestOptions(), + ), + ), + ); + + // Perform the GET request using ApiClient + final result = await apiClient.delete(path: '/example'); + + expect(result.data, 'Error message'); + expect( + result.statusCode, + 404, + ); + }); }); } diff --git a/test/services/network_service_test.dart b/test/services/network_service_test.dart index 0498c0c..4937101 100644 --- a/test/services/network_service_test.dart +++ b/test/services/network_service_test.dart @@ -7,8 +7,6 @@ import 'package:sirenapp_flutter_inbox/src/services/network_service.dart'; import 'network_service_test.mocks.dart'; -// class MockApiClient extends Mock implements ApiClient {} - @GenerateNiceMocks([ MockSpec(), ]) diff --git a/test/widgets/app_bar_test.dart b/test/widgets/app_bar_test.dart new file mode 100644 index 0000000..38577ff --- /dev/null +++ b/test/widgets/app_bar_test.dart @@ -0,0 +1,141 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:sirenapp_flutter_inbox/sirenapp_flutter_inbox.dart'; +import 'package:sirenapp_flutter_inbox/src/widgets/app_bar.dart'; + +void main() { + testWidgets('SirenAppBar displays title', (WidgetTester tester) async { + const title = 'Notifications'; + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + appBar: SirenAppBar( + theme: ThemeData(), + inboxHeaderProps: InboxHeaderProps( + title: title, + showBackButton: false, + ), + showClearAllButton: false, + ), + ), + ), + ); + + expect(find.text(title), findsOneWidget); + }); + + testWidgets('SirenAppBar displays back button when showBackButton is true', + (WidgetTester tester) async { + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + appBar: SirenAppBar( + theme: ThemeData(), + inboxHeaderProps: InboxHeaderProps( + title: 'Title', + showBackButton: true, + ), + showClearAllButton: false, + ), + ), + ), + ); + + expect(find.byIcon(Icons.arrow_back_ios), findsOneWidget); + }); + + testWidgets( + 'SirenAppBar does not display clear all button when hideClearAll is true', + (WidgetTester tester) async { + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + appBar: SirenAppBar( + theme: ThemeData(), + inboxHeaderProps: InboxHeaderProps( + title: 'Title', + showBackButton: false, + hideClearAll: true, + ), + showClearAllButton: true, + ), + ), + ), + ); + + expect(find.text('Clear All'), findsNothing); + }); + + testWidgets( + 'SirenAppBar displays clear all button when showClearAllButton is true', + (WidgetTester tester) async { + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + appBar: SirenAppBar( + theme: ThemeData(), + inboxHeaderProps: InboxHeaderProps( + title: 'Title', + showBackButton: false, + ), + showClearAllButton: true, + ), + ), + ), + ); + + expect(find.text('Clear All'), findsOneWidget); + }); + + testWidgets( + 'SirenAppBar calls onBackButtonPressed when back button is pressed', + (WidgetTester tester) async { + var backButtonPressed = false; + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + appBar: SirenAppBar( + theme: ThemeData(), + inboxHeaderProps: InboxHeaderProps( + title: 'Title', + showBackButton: true, + onBackPress: () { + backButtonPressed = true; + }, + ), + showClearAllButton: false, + ), + ), + ), + ); + + await tester.tap(find.byIcon(Icons.arrow_back_ios)); + expect(backButtonPressed, true); + }); + + testWidgets( + 'SirenAppBar calls onClearAllPressed when clear all button is pressed', + (WidgetTester tester) async { + bool clearAllPressed = false; + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + appBar: SirenAppBar( + theme: ThemeData(), + inboxHeaderProps: InboxHeaderProps( + title: 'Title', + showBackButton: false, + ), + showClearAllButton: true, + onClearAllPressed: () { + clearAllPressed = true; + }, + ), + ), + ), + ); + + await tester.tap(find.text('Clear All')); + expect(clearAllPressed, true); + }); +} diff --git a/test/widgets/card_test.dart b/test/widgets/card_test.dart index 2a26cbb..fcf030e 100644 --- a/test/widgets/card_test.dart +++ b/test/widgets/card_test.dart @@ -1,15 +1,21 @@ import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:mockito/mockito.dart'; +import 'package:network_image_mock/network_image_mock.dart'; import 'package:sirenapp_flutter_inbox/sirenapp_flutter_inbox.dart'; import 'package:sirenapp_flutter_inbox/src/widgets/card.dart'; class MockNetworkImage extends Mock implements NetworkImage {} +class MockFunction extends Mock { + void call(); +} + void main() { testWidgets('CardWidget renders correctly', (WidgetTester tester) async { // Create a mock notification data // ignore: unused_local_variable + final func = MockFunction().call; final notification = NotificationDataType( id: '123', createdAt: '2024-03-15T04:07:14.577928Z', @@ -27,45 +33,40 @@ void main() { ), requestId: '456', isRead: false, - cardColor: Colors.blue, // Mock card color + cardColor: Colors.blue, ); - // Mock the NetworkImage provider - // final mockImageProvider = MockNetworkImage(); - // when(mockImageProvider.resolve(any, any)).thenAnswer( - // (_) => Future.value( - // ImageStreamCompleter( - // completer: Completer(), - // // Mock image stream completer - // ), - // ), - // ); - - // Build the CardWidget with the mock data - await tester.pumpWidget( - MaterialApp( - home: CardWidget( - onTap: (notification) {}, // Mock onTap function - onDelete: (id) {}, // Mock onDelete function - notification: notification, - cardProps: const CardProps(hideAvatar: true), - styles: null, // Mock styles - // Pass the mock image provider - // deleteWidget: Image(image: mockImageProvider), + var deletePressed = false; + await mockNetworkImagesFor(() async { + await tester.pumpWidget( + MaterialApp( + home: CardWidget( + onTap: (notification) {}, + onDelete: (id) { + deletePressed = true; + }, + notification: notification, + cardProps: CardProps( + hideAvatar: false, + hideDelete: false, + onAvatarClick: (notification) { + func(); + }, + ), + styles: null, // Mock styles + // deleteWidget: Image(image: mockImageProvider), + ), ), - ), - ); - - // Verify that the header text is rendered + ); + }); expect(find.text('Test Header'), findsOneWidget); - - // Verify that the sub-header text is rendered expect(find.text('Test SubHeader'), findsOneWidget); - - // Verify that the body text is rendered expect(find.text('Test Body'), findsOneWidget); - - // Verify that the delete button is rendered - // expect(find.byType(Image), findsOneWidget); + await tester.tap(find.byType(GestureDetector).at(1)); + await tester.pumpAndSettle(const Duration(seconds: 1)); + verify(func()).called(1); + await tester.tap(find.byType(GestureDetector).at(2)); + await tester.pumpAndSettle(const Duration(seconds: 1)); + expect(deletePressed, true); }); } diff --git a/test/widgets/inbox_body_test.dart b/test/widgets/inbox_body_test.dart new file mode 100644 index 0000000..83932a0 --- /dev/null +++ b/test/widgets/inbox_body_test.dart @@ -0,0 +1,116 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:network_image_mock/network_image_mock.dart'; +import 'package:sirenapp_flutter_inbox/sirenapp_flutter_inbox.dart'; +import 'package:sirenapp_flutter_inbox/src/widgets/error_widget.dart'; +import 'package:sirenapp_flutter_inbox/src/widgets/inbox_body.dart'; +import 'package:sirenapp_flutter_inbox/src/widgets/loader_widget.dart'; +import 'package:sirenapp_flutter_inbox/src/widgets/notification_list_view.dart'; + +void main() { + testWidgets('InboxBody displays loader when isLoading is true', + (WidgetTester tester) async { + await tester.pumpWidget( + MaterialApp( + home: InboxBody( + currentTheme: ThemeData(), + isLoading: true, + loadingNextPage: false, + isError: false, + notifications: [], + deleteNotification: (id) async {}, + markAsRead: (id) {}, + customNotificationCard: null, + onNotificationCardClick: null, + deletingNotificationId: null, + disableAutoMarkAsRead: false, + totalElements: 0, + onRefresh: () async {}, + endReached: false, + onEndReached: () {}, + scrollController: ScrollController(), + ), + ), + ); + + expect(find.byType(LoaderWidget), findsOneWidget); + }); + + testWidgets('InboxBody displays error widget when isError is true', + (WidgetTester tester) async { + await tester.pumpWidget( + MaterialApp( + home: InboxBody( + currentTheme: ThemeData(), + isLoading: false, + loadingNextPage: false, + isError: true, + notifications: [], + deleteNotification: (id) async {}, + markAsRead: (id) {}, + customNotificationCard: null, + onNotificationCardClick: null, + deletingNotificationId: null, + disableAutoMarkAsRead: false, + totalElements: 0, + onRefresh: () async {}, + endReached: false, + onEndReached: () {}, + scrollController: ScrollController(), + ), + ), + ); + + expect(find.byType(DefaultErrorWidget), findsOneWidget); + }); + + testWidgets('InboxBody displays notifications', (WidgetTester tester) async { + final notifications = [ + NotificationDataType( + id: '1', + createdAt: '2024-03-15T04:07:14.577928Z', + message: MessageData( + header: 'Test Header', + subHeader: 'Test SubHeader', + body: 'Test Body', + channel: 'Test Channel', + actionUrl: 'Test Action Url', + avatar: AvatarData( + altText: 'Test alt text', + url: 'https://picsum.photos/200/300', + ), + additionalData: 'Test Additional Data', + ), + requestId: 'request-id', + isRead: false, + cardColor: Colors.black, + ), + ]; + await mockNetworkImagesFor(() async { + await tester.pumpWidget( + MaterialApp( + home: InboxBody( + currentTheme: ThemeData(), + isLoading: false, + loadingNextPage: false, + isError: false, + notifications: notifications, + deleteNotification: (id) async {}, + markAsRead: (id) {}, + customNotificationCard: null, + onNotificationCardClick: null, + deletingNotificationId: null, + disableAutoMarkAsRead: false, + totalElements: 1, + onRefresh: () async {}, + endReached: false, + onEndReached: () {}, + scrollController: ScrollController(), + ), + ), + ); + + expect(find.byType(NotificationListView), findsOneWidget); + }); + }); +} diff --git a/test/widgets/loader_widget_test.dart b/test/widgets/loader_widget_test.dart index 42cd2c8..443b25a 100644 --- a/test/widgets/loader_widget_test.dart +++ b/test/widgets/loader_widget_test.dart @@ -8,7 +8,9 @@ void main() { (WidgetTester tester) async { await tester.pumpWidget( const MaterialApp( - home: CardLoaderWidget(), + home: CardLoaderWidget( + hideAvatar: false, + ), ), ); diff --git a/test/widgets/notification_list_view_test.dart b/test/widgets/notification_list_view_test.dart index 43319a3..69ad83e 100644 --- a/test/widgets/notification_list_view_test.dart +++ b/test/widgets/notification_list_view_test.dart @@ -51,8 +51,6 @@ void main() { onRefresh: () async {}, onEndReached: () {}, customStyles: null, - hideAvatar: false, - deleteWidget: null, scrollController: ScrollController(), onDelete: (id) async {}, markAsRead: (id) {}, @@ -89,8 +87,6 @@ void main() { onRefresh: () async {}, onEndReached: () {}, customStyles: null, - hideAvatar: false, - deleteWidget: null, scrollController: ScrollController(), customNotificationCard: (n) { return Text(n.message.subHeader.toString()); diff --git a/test/widgets/siren_inbox_test.dart b/test/widgets/siren_inbox_test.dart index 1ece45f..e1abba1 100644 --- a/test/widgets/siren_inbox_test.dart +++ b/test/widgets/siren_inbox_test.dart @@ -1,5 +1,3 @@ -// ignore_for_file: unused_local_variable - import 'dart:async'; import 'package:flutter/material.dart'; @@ -10,6 +8,8 @@ import 'package:sirenapp_flutter_inbox/sirenapp_flutter_inbox.dart'; import 'package:sirenapp_flutter_inbox/src/api/fetch_unviewed_notification_count.dart'; import 'package:sirenapp_flutter_inbox/src/constants/generics.dart'; import 'package:sirenapp_flutter_inbox/src/data/siren_data_provider.dart'; +import 'package:sirenapp_flutter_inbox/src/models/ui_models.dart'; +import 'package:sirenapp_flutter_inbox/src/widgets/loader_widget.dart'; import 'siren_inbox_test.mocks.dart'; @@ -27,27 +27,6 @@ void main() { late StreamController iconController; late StreamController inboxController; late MockSirenDataProvider mockSirenDataProvider; - final notification = [ - NotificationDataType( - id: '1', - createdAt: '2024-03-15T04:07:14.577928Z', - message: MessageData( - header: 'Test Header', - subHeader: 'Test SubHeader', - body: 'Test Body', - channel: 'Test Channel', - actionUrl: 'Test Action Url', - avatar: AvatarData( - altText: 'Test alt text', - url: 'https://picsum.photos/200/300', - ), - additionalData: 'Test Additional Data', - ), - requestId: 'request-id', - isRead: false, - cardColor: Colors.black, - ), - ]; setUp(() { iconController = StreamController.broadcast(); @@ -62,8 +41,6 @@ void main() { inboxController.close(); }); testWidgets('Initial loading state', (WidgetTester tester) async { - // Mock SirenDataProvider - await tester.pumpWidget( const MaterialApp( home: Scaffold( @@ -76,29 +53,17 @@ void main() { expect(find.byType(LoaderWidget), findsOneWidget); }); - // testWidgets('Error state', (WidgetTester tester) async { - // // Mock SirenDataProvider - - // await tester.pumpWidget( - // const MaterialApp( - // home: Scaffold( - // body: SirenInbox(), - // ), - // ), - // ); - - // // Error state widget should be displayed - // expect(find.byType(DefaultErrorWidget), findsOneWidget); - // }); - testWidgets('Test Title', (WidgetTester tester) async { // Mock SirenDataProvider await tester.pumpWidget( - const MaterialApp( + MaterialApp( home: Scaffold( body: SirenInbox( - title: 'Notifications Header', + inboxHeaderProps: InboxHeaderProps( + title: 'Notifications Header', + ), + darkMode: true, ), ), ), @@ -114,56 +79,48 @@ void main() { expect(find.text('Notifications Header'), findsOneWidget); }); - testWidgets('Custom Header', (WidgetTester tester) async { - // Mock SirenDataProvider - + testWidgets('Back navigation', (WidgetTester tester) async { + var backButtonPressed = false; await tester.pumpWidget( - const MaterialApp( + MaterialApp( home: Scaffold( body: SirenInbox( - customHeader: Text('Custom Header'), + inboxHeaderProps: InboxHeaderProps( + showBackButton: true, + onBackPress: () { + backButtonPressed = true; + }, + ), ), ), ), ); - // Loading state widget should be displayed - expect(find.byType(LoaderWidget), findsOneWidget); - - // Simulate a successful fetch await tester.pump(); - - // Verify that notification list is displayed - expect(find.text('Custom Header'), findsOneWidget); + await tester.tap(find.byIcon(Icons.arrow_back_ios)); + expect(backButtonPressed, true); }); - testWidgets('Widget Handle back navigation', (WidgetTester tester) async { - final func = MockFunction().call; - final widget = MaterialApp( - home: Scaffold( - body: SirenInbox( - showDefaultBackButton: true, - handleBackNavigation: func, - ), - ), - ); - await tester.pumpWidget(widget); - await tester.tap(find.byType(GestureDetector)); - await tester.pumpAndSettle(const Duration(seconds: 2)); - verify(func()).called(1); - }); + testWidgets('Test theme', (WidgetTester tester) async { + // Mock SirenDataProvider - testWidgets('Show default back button', (WidgetTester tester) async { - const widget = MaterialApp( - home: Scaffold( - body: SirenInbox( - showDefaultBackButton: true, - // defaultBackButton: Icon(Icons.back_hand), + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: SirenInbox( + theme: CustomThemeColors(backgroundColor: Colors.amber), + ), ), ), ); - await tester.pumpWidget(widget); - expect(find.byIcon(Icons.arrow_back_ios), findsOneWidget); + + final scaffoldFinder = find.byType(Scaffold).at(1); + + final scaffoldWidget = tester.widget(scaffoldFinder); + final scaffoldBackgroundColor = scaffoldWidget.backgroundColor; + expect(scaffoldBackgroundColor, equals(Colors.amber)); + + await tester.pump(); }); testWidgets('Stream', (WidgetTester tester) async { From 377b93cbc78e39093a2cddee63590f920aa5faf5 Mon Sep 17 00:00:00 2001 From: Anitta Babu <99161914+anitta-keyvalue@users.noreply.github.com> Date: Mon, 15 Apr 2024 14:49:43 +0530 Subject: [PATCH 03/17] refactor: Remove commented code, add unique key, semantics to the widgets --- README.md | 8 +- example/pubspec.lock | 59 ++++++++---- lib/src/constants/strings.dart | 1 + lib/src/models/ui_models.dart | 7 -- lib/src/services/api_provider.dart | 10 -- lib/src/widgets/app_bar.dart | 57 +++++++----- lib/src/widgets/card.dart | 9 +- lib/src/widgets/icon_badge.dart | 4 +- lib/src/widgets/inbox_body.dart | 22 +++-- lib/src/widgets/notification_list_view.dart | 92 ++++++++++--------- lib/src/widgets/siren_inbox.dart | 2 +- lib/src/widgets/siren_inbox_icon.dart | 23 +++-- test/data/siren_data_provider_test.dart | 11 +-- test/delete_notification_by_id_test.dart | 13 +-- test/models/api_response_test.dart | 4 +- test/models/ui_models_test.dart | 29 ------ ...nviewed_notification_count_model_test.dart | 12 --- test/services/api_client_test.dart | 28 +----- test/services/network_service_test.dart | 2 - test/utils/common_utils_test.dart | 10 +- test/utils/siren_test.dart | 19 ---- test/widgets/app_bar_test.dart | 2 +- test/widgets/card_test.dart | 2 - test/widgets/empty_widget_test.dart | 7 +- test/widgets/error_widget_test.dart | 6 -- test/widgets/icon_badge_test.dart | 4 - test/widgets/inbox_body_test.dart | 4 +- test/widgets/loader_widget_test.dart | 5 - test/widgets/notification_list_view_test.dart | 3 +- test/widgets/nullabale_text_test.dart | 10 -- test/widgets/siren_inbox_icon_test.dart | 7 +- test/widgets/siren_inbox_test.dart | 12 +-- 32 files changed, 183 insertions(+), 301 deletions(-) diff --git a/README.md b/README.md index 6deb830..214dd42 100644 --- a/README.md +++ b/README.md @@ -24,11 +24,9 @@ import 'package:sirenapp_flutter_inbox/sirenapp_flutter_inbox.dart'; void main() { runApp( SirenProvider( - config: SirenConfig( userToken: 'your_user_token', recipientId: 'your_recipient_id', - ), - child: MyApp(), + child: MyApp(), ), ); } @@ -80,7 +78,6 @@ customStyles: SirenStyleProps( badgeStyle: BadgeStyle( fontSize: 10, size: 18, - inset: 1, top: 2, right: 0, )) @@ -135,9 +132,6 @@ theme: CustomThemeColors( textColor: const Color.fromRGBO(0, 0, 0, 1), dateColor: const Color.fromRGBO(0, 0, 0, 1), timerIcon: const Color.fromRGBO(133, 146, 230, 1), - badgeBackgroundColor: const Color.fromRGBO(103, 58, 183, 1), - badgeColor: const Color.fromRGBO(103, 58, 183, 1), - iconColor: const Color.fromRGBO(0, 0, 0, 1), inboxTitleColor: const Color.fromRGBO(0, 0, 0, 1), ), ``` diff --git a/example/pubspec.lock b/example/pubspec.lock index db66449..2b9090b 100644 --- a/example/pubspec.lock +++ b/example/pubspec.lock @@ -41,6 +41,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.18.0" + crypto: + dependency: transitive + description: + name: crypto + sha256: ff625774173754681d66daaf4a448684fb04b78f902da9cb3d308c19cc5e8bab + url: "https://pub.dev" + source: hosted + version: "3.0.3" cupertino_icons: dependency: "direct main" description: @@ -91,30 +99,30 @@ packages: url: "https://pub.dev" source: hosted version: "4.0.2" - leak_tracker: + intl: dependency: transitive description: - name: leak_tracker - sha256: "78eb209deea09858f5269f5a5b02be4049535f568c07b275096836f01ea323fa" + name: intl + sha256: "3bc132a9dbce73a7e4a21a17d06e1878839ffbf975568bc875c60537824b0c4d" url: "https://pub.dev" source: hosted - version: "10.0.0" - leak_tracker_flutter_testing: + version: "0.18.1" + leak_tracker: dependency: transitive description: - name: leak_tracker_flutter_testing - sha256: b46c5e37c19120a8a01918cfaf293547f47269f7cb4b0058f21531c2465d6ef0 + name: leak_tracker + sha256: "7e108028e3d258667d079986da8c0bc32da4cb57431c2af03b1dc1038621a9dc" url: "https://pub.dev" source: hosted - version: "2.0.1" + version: "9.0.13" leak_tracker_testing: dependency: transitive description: name: leak_tracker_testing - sha256: a597f72a664dbd293f3bfc51f9ba69816f84dcd403cdac7066cb3f6003f3ab47 + sha256: b06739349ec2477e943055aea30172c5c7000225f79dad4702e2ec0eda79a6ff url: "https://pub.dev" source: hosted - version: "2.0.1" + version: "1.0.5" lints: dependency: transitive description: @@ -127,10 +135,10 @@ packages: dependency: transitive description: name: matcher - sha256: d2323aa2060500f906aa31a895b4030b6da3ebdcc5619d14ce1aada65cd161cb + sha256: "1803e76e6653768d64ed8ff2e1e67bea3ad4b923eb5c56a295c3e634bad5960e" url: "https://pub.dev" source: hosted - version: "0.12.16+1" + version: "0.12.16" material_color_utilities: dependency: transitive description: @@ -159,16 +167,17 @@ packages: dependency: transitive description: name: path - sha256: "087ce49c3f0dc39180befefc60fdb4acd8f8620e5682fe2476afd0b3688bb4af" + sha256: "8829d8a55c13fc0e37127c29fedf290c102f4e40ae94ada574091fe0ff96c917" url: "https://pub.dev" source: hosted - version: "1.9.0" + version: "1.8.3" sirenapp_flutter_inbox: dependency: "direct main" description: - path: ".." - relative: true - source: path + name: sirenapp_flutter_inbox + sha256: "3abd9c5d42acbf062a3eef8cfe5691600e10e33b32533d1a2a1c9ed0a1063106" + url: "https://pub.dev" + source: hosted version: "1.0.0" sky_engine: dependency: transitive @@ -247,6 +256,22 @@ packages: url: "https://pub.dev" source: hosted version: "13.0.0" + web: + dependency: transitive + description: + name: web + sha256: afe077240a270dcfd2aafe77602b4113645af95d0ad31128cc02bce5ac5d5152 + url: "https://pub.dev" + source: hosted + version: "0.3.0" + web_socket_channel: + dependency: transitive + description: + name: web_socket_channel + sha256: d88238e5eac9a42bb43ca4e721edba3c08c6354d4a53063afaa568516217621b + url: "https://pub.dev" + source: hosted + version: "2.4.0" sdks: dart: ">=3.2.3 <4.0.0" flutter: ">=2.0.1" diff --git a/lib/src/constants/strings.dart b/lib/src/constants/strings.dart index 9646fac..8f85ad2 100644 --- a/lib/src/constants/strings.dart +++ b/lib/src/constants/strings.dart @@ -7,4 +7,5 @@ class Strings { static const error_desc = 'Could not load the notifications. Please refresh the page.'; static const clear_all = 'Clear All'; + static const notifications = 'Notifications'; } diff --git a/lib/src/models/ui_models.dart b/lib/src/models/ui_models.dart index deba2d2..13d8901 100644 --- a/lib/src/models/ui_models.dart +++ b/lib/src/models/ui_models.dart @@ -46,9 +46,6 @@ class DefaultIconStyle { /// Default font size for the badge count. static double get defaultFontSize => 10; - /// Default inset for the badge count. - static double get defaultInset => 1; - /// Default size for the badge count. static double get defaultSize => 20; @@ -67,7 +64,6 @@ class BadgeStyle { /// Constructs a [BadgeStyle] with optional parameters. const BadgeStyle({ this.fontSize, - this.inset, this.size, this.top, this.right, @@ -76,9 +72,6 @@ class BadgeStyle { /// The font size of the notification icon badge. final double? fontSize; - /// The inset of the notification icon badge. - final double? inset; - /// The size of the notification icon badge. final double? size; diff --git a/lib/src/services/api_provider.dart b/lib/src/services/api_provider.dart index 281b0d4..167d773 100644 --- a/lib/src/services/api_provider.dart +++ b/lib/src/services/api_provider.dart @@ -1,13 +1,9 @@ -import 'dart:io'; import 'package:dio/dio.dart'; import 'package:sirenapp_flutter_inbox/src/data/siren_data_provider.dart'; /// Provides an instance of Dio with configured interceptors. Dio apiProvider() { final dio = Dio(); - // Configuring timeouts - // dio.options.connectTimeout = 10000; - // dio.options.receiveTimeout = 3000; // Adding interceptors dio.interceptors.add( @@ -27,18 +23,12 @@ Dio apiProvider() { */ onResponse: (Response response, ResponseInterceptorHandler handler) async { - if (response.data != '') { - // handle error - } return handler.next(response); }, /** * onError interceptor - called on error */ onError: (DioException dioError, ErrorInterceptorHandler handler) async { - if (dioError.error is SocketException) { - // HANDLE ERROR - } return handler.next(dioError); }, ), diff --git a/lib/src/widgets/app_bar.dart b/lib/src/widgets/app_bar.dart index 3c4b155..36933f5 100644 --- a/lib/src/widgets/app_bar.dart +++ b/lib/src/widgets/app_bar.dart @@ -27,7 +27,6 @@ class SirenAppBar extends StatelessWidget implements PreferredSizeWidget { if (inboxHeaderProps?.hideHeader ?? false) { return const SizedBox.shrink(); } - return Container( decoration: BoxDecoration( color: theme.colorScheme.primary, @@ -50,10 +49,15 @@ class SirenAppBar extends StatelessWidget implements PreferredSizeWidget { left: 24, right: 16, ), - child: IconButton( - onPressed: inboxHeaderProps?.onBackPress, - icon: inboxHeaderProps?.backButton ?? - const Icon(Icons.arrow_back_ios), + child: Semantics( + label: 'siren-header-back', + hint: 'Tap to view navigate back', + child: IconButton( + key: const Key('siren-header-back'), + onPressed: inboxHeaderProps?.onBackPress, + icon: inboxHeaderProps?.backButton ?? + const Icon(Icons.arrow_back_ios), + ), ), ), Padding( @@ -62,7 +66,7 @@ class SirenAppBar extends StatelessWidget implements PreferredSizeWidget { (inboxHeaderProps?.showBackButton ?? false) ? 2 : 24, ), child: Text( - inboxHeaderProps?.title ?? 'Notifications', + inboxHeaderProps?.title ?? Strings.notifications, style: const TextStyle( fontSize: 18, fontWeight: FontWeight.w600, @@ -75,25 +79,32 @@ class SirenAppBar extends StatelessWidget implements PreferredSizeWidget { showClearAllButton) Padding( padding: const EdgeInsets.only(right: 24), - child: GestureDetector( - onTap: onClearAllPressed, - child: const Row( - children: [ - Padding( - padding: EdgeInsets.only(right: 4), - child: Icon( - Icons.clear_all, - size: 24, + child: Semantics( + label: 'siren-header-clear-all', + hint: 'Tap to clear all notifications', + child: GestureDetector( + key: const Key('siren-header-clear-all'), + onTap: onClearAllPressed, + child: Row( + children: [ + Padding( + padding: const EdgeInsets.only(right: 4), + child: Icon( + Icons.clear_all, + size: 24, + color: theme.colorScheme.outline, + ), ), - ), - Text( - Strings.clear_all, - style: TextStyle( - fontSize: 14, - fontWeight: FontWeight.w500, + Text( + Strings.clear_all, + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.w500, + color: theme.colorScheme.outline, + ), ), - ), - ], + ], + ), ), ), ), diff --git a/lib/src/widgets/card.dart b/lib/src/widgets/card.dart index 9f1bdb0..5f4d718 100644 --- a/lib/src/widgets/card.dart +++ b/lib/src/widgets/card.dart @@ -45,6 +45,7 @@ class _CardWidgetState extends State { final currentTheme = Theme.of(context); return GestureDetector( + key: Key('siren-notification-card-${widget.notification.id}'), onTap: () { widget.onTap(widget.notification); }, @@ -81,6 +82,9 @@ class _CardWidgetState extends State { ), if (!(widget.cardProps.hideDelete ?? false)) GestureDetector( + key: Key( + 'siren-notification-delete-${widget.notification.id}', + ), onTap: () => widget.onDelete(widget.notification.id), child: widget.cardProps.deleteWidget ?? _buildDefaultDeleteButton(currentTheme), @@ -122,6 +126,7 @@ class _CardWidgetState extends State { Widget _buildDefaultAvatarContainer(ThemeData theme) { final avatarUrl = widget.notification.message.avatar?.url; return GestureDetector( + key: Key('siren-notification-avatar-${widget.notification.id}'), onTap: () { widget.cardProps.onAvatarClick?.call(widget.notification); }, @@ -194,8 +199,8 @@ class _CardWidgetState extends State { Widget _buildFooterRow(ThemeData theme) { return Padding( - padding: const EdgeInsets.symmetric( - vertical: 10, + padding: const EdgeInsets.only( + top: 10, ), child: Container( decoration: widget.styles?.cardFooterRow, diff --git a/lib/src/widgets/icon_badge.dart b/lib/src/widgets/icon_badge.dart index 057fceb..f52eaed 100644 --- a/lib/src/widgets/icon_badge.dart +++ b/lib/src/widgets/icon_badge.dart @@ -24,8 +24,8 @@ class IconBadge extends StatelessWidget { child: Container( width: badgeStyle?.size ?? DefaultIconStyle.defaultSize, height: badgeStyle?.size ?? DefaultIconStyle.defaultSize, - padding: EdgeInsets.all( - badgeStyle?.inset ?? DefaultIconStyle.defaultInset, + padding: const EdgeInsets.all( + 1, ), decoration: BoxDecoration( shape: BoxShape.circle, diff --git a/lib/src/widgets/inbox_body.dart b/lib/src/widgets/inbox_body.dart index 8f798d7..3c6a8db 100644 --- a/lib/src/widgets/inbox_body.dart +++ b/lib/src/widgets/inbox_body.dart @@ -62,11 +62,16 @@ class InboxBody extends StatelessWidget { child: ListView( physics: const AlwaysScrollableScrollPhysics(), children: [ - SizedBox( - height: MediaQuery.of(context).size.height * 0.75, - width: MediaQuery.of(context).size.width, - child: Center( - child: customErrorWidget ?? const DefaultErrorWidget(), + Semantics( + label: 'siren-error-state', + hint: 'Notification error state', + child: SizedBox( + key: const Key('siren-error-state'), + height: MediaQuery.of(context).size.height * 0.75, + width: MediaQuery.of(context).size.width, + child: Center( + child: customErrorWidget ?? const DefaultErrorWidget(), + ), ), ), ], @@ -78,7 +83,12 @@ class InboxBody extends StatelessWidget { hideAvatar: cardProps?.hideAvatar ?? false, ); } else if (notifications.isEmpty) { - return listEmptyWidget ?? const EmptyWidget(); + return Semantics( + label: 'siren-empty-state', + hint: 'Empty notification list', + key: const Key('siren-empty-state'), + child: listEmptyWidget ?? const EmptyWidget(), + ); } else { return NotificationListView( notifications: notifications, diff --git a/lib/src/widgets/notification_list_view.dart b/lib/src/widgets/notification_list_view.dart index 6f864b5..5ad1823 100644 --- a/lib/src/widgets/notification_list_view.dart +++ b/lib/src/widgets/notification_list_view.dart @@ -73,50 +73,56 @@ class _NotificationListViewState extends State { color: Theme.of(context).colorScheme.secondary, backgroundColor: Theme.of(context).colorScheme.primary, onRefresh: widget.onRefresh, - child: ListView.builder( - itemCount: widget.notifications.length + (widget.endReached ? 0 : 1), - itemBuilder: (context, index) { - if (index < widget.notifications.length) { - final isLastIndex = index == widget.notifications.length - 1; - final itemWidget = widget.customNotificationCard - ?.call(widget.notifications[index]) ?? - CardWidget( - onTap: (notification) { - if (!(widget.cardProps?.disableAutoMarkAsRead ?? false)) { - widget.markAsRead(widget.notifications[index].id); - } - widget.onNotificationCardClick - ?.call(widget.notifications[index]); - }, - notification: widget.notifications[index], - cardProps: widget.cardProps ?? const CardProps(), - styles: widget.customStyles, - onDelete: widget.onDelete, - ); - return AnimatedOpacity( - key: isLastIndex ? _listViewKey : null, - duration: const Duration(milliseconds: 500), - opacity: widget.deletingNotificationId == - widget.notifications[index].id - ? 0.0 - : 1.0, - child: itemWidget, - ); - } else { - return widget.loadingNextPage - ? Padding( - padding: const EdgeInsets.symmetric(vertical: 8), - child: Center( - child: CircularProgressIndicator( - color: Theme.of(context).colorScheme.secondary, + child: Semantics( + label: 'siren-notification-list', + hint: 'Swipe up or down to view notifications', + child: ListView.builder( + key: const Key('siren-notification-list'), + itemCount: widget.notifications.length + (widget.endReached ? 0 : 1), + itemBuilder: (context, index) { + if (index < widget.notifications.length) { + final isLastIndex = index == widget.notifications.length - 1; + final currentNotification = widget.notifications[index]; + final itemWidget = widget.customNotificationCard + ?.call(currentNotification) ?? + CardWidget( + onTap: (notification) { + if (!(widget.cardProps?.disableAutoMarkAsRead ?? false)) { + widget.markAsRead(currentNotification.id); + } + widget.onNotificationCardClick?.call(currentNotification); + }, + notification: currentNotification, + cardProps: widget.cardProps ?? const CardProps(), + styles: widget.customStyles, + onDelete: widget.onDelete, + ); + return AnimatedOpacity( + key: isLastIndex + ? _listViewKey + : ValueKey(currentNotification.id), + duration: const Duration(milliseconds: 500), + opacity: widget.deletingNotificationId == currentNotification.id + ? 0.0 + : 1.0, + child: itemWidget, + ); + } else { + return widget.loadingNextPage + ? Padding( + padding: const EdgeInsets.symmetric(vertical: 8), + child: Center( + child: CircularProgressIndicator( + color: Theme.of(context).colorScheme.secondary, + ), ), - ), - ) - : const SizedBox(); - } - }, - physics: const AlwaysScrollableScrollPhysics(), - controller: widget.scrollController, + ) + : const SizedBox(); + } + }, + physics: const AlwaysScrollableScrollPhysics(), + controller: widget.scrollController, + ), ), ); } diff --git a/lib/src/widgets/siren_inbox.dart b/lib/src/widgets/siren_inbox.dart index 959602b..97d185e 100644 --- a/lib/src/widgets/siren_inbox.dart +++ b/lib/src/widgets/siren_inbox.dart @@ -73,7 +73,6 @@ class SirenInbox extends StatefulWidget { } class _SirenInboxState extends State { - late ScrollController _scrollController; bool isLoading = true; bool endReached = false; bool isError = false; @@ -88,6 +87,7 @@ class _SirenInboxState extends State { late final ReadNotificationById _readNotificationById; late Timer? _periodicUpdateRef; late StreamSubscription _subscription; + late ScrollController _scrollController; @override void initState() { diff --git a/lib/src/widgets/siren_inbox_icon.dart b/lib/src/widgets/siren_inbox_icon.dart index 573a3e6..4d5812b 100644 --- a/lib/src/widgets/siren_inbox_icon.dart +++ b/lib/src/widgets/siren_inbox_icon.dart @@ -198,15 +198,20 @@ class _SirenInboxIconState extends State { }, child: Stack( children: [ - SizedBox( - width: size, - height: size, - child: widget.notificationIcon ?? - Icon( - Icons.notifications_none_outlined, - size: size, - color: currentTheme.colorScheme.onPrimary, - ), + Semantics( + label: 'siren-notification-icon', + hint: 'Tap to view notifications', + child: SizedBox( + key: const Key('siren-notification-icon'), + width: size, + height: size, + child: widget.notificationIcon ?? + Icon( + Icons.notifications_none_outlined, + size: size, + color: currentTheme.colorScheme.onPrimary, + ), + ), ), IconBadge( hideBadge: diff --git a/test/data/siren_data_provider_test.dart b/test/data/siren_data_provider_test.dart index 8034716..90d7fbc 100644 --- a/test/data/siren_data_provider_test.dart +++ b/test/data/siren_data_provider_test.dart @@ -22,30 +22,24 @@ void main() { group('SirenDataProvider', () { test('UpdateParams updates user token and recipient ID', () async { - // Perform updateParams sirenDataProvider.updateParams( userToken: 'token', recipientId: 'recipientId', ); - // Verify that user token and recipient ID are updated correctly expect(sirenDataProvider.userToken, 'token'); expect(sirenDataProvider.recipientId, 'recipientId'); }); test('IconDispose closes icon controller', () { - // Perform icon disposal sirenDataProvider.iconDispose(); - // Verify that icon controller is closed expect(sirenDataProvider.iconController.isClosed, false); }); test('InboxDispose closes inbox controller', () { - // Perform inbox disposal sirenDataProvider.inboxDispose(); - // Verify that inbox controller is closed expect(sirenDataProvider.inboxController.isClosed, false); }); @@ -59,8 +53,9 @@ void main() { sirenDataProvider.updateParams(userToken: 'token', recipientId: '123'); await Future.delayed( - const Duration(seconds: Generics.DATA_FETCH_INTERVAL) * - (Generics.MAX_RETRIES + 1)); + const Duration(seconds: Generics.DATA_FETCH_INTERVAL) * + (Generics.MAX_RETRIES + 1), + ); expect(sirenDataProvider.tokenVerificationStatus, Status.FAILED); }); diff --git a/test/delete_notification_by_id_test.dart b/test/delete_notification_by_id_test.dart index 309d9b7..8d31364 100644 --- a/test/delete_notification_by_id_test.dart +++ b/test/delete_notification_by_id_test.dart @@ -17,8 +17,6 @@ class MockApiClient extends ApiClient { CancelToken? cancelToken, ProgressCallback? onReceiveProgress, }) async { - // Simulate different API responses here - // (e.g., return ApiResponse with different status codes, data, and errors) final result = DioResponse( data: { 'data': {'status': 'SUCCESS'}, @@ -26,7 +24,7 @@ class MockApiClient extends ApiClient { }, statusCode: 200, ); - return result; // Default to success for now + return result; } } @@ -89,23 +87,16 @@ void main() { const notificationId = 'test-notification-id'; test('deleteNotificationById - success', () async { - // Arrange - // final fakeApi = MockApiClient(apiProvider()); final deleteNotification = DeleteNotificationById._internal(); - // Act final apiResponse = await deleteNotification.deleteNotificationById( notificationId: notificationId, ); - // Assert expect(apiResponse.isLoading, false); expect(apiResponse.isSuccess, true); expect(apiResponse.isError, false); - expect(apiResponse.data, Status.SUCCESS); // No data expected for success - // expect(apiResponse.error, ApiErrorDetails( )); + expect(apiResponse.data, Status.SUCCESS); }); - - // Add additional test cases for different API responses (error, network failure, etc.) }); } diff --git a/test/models/api_response_test.dart b/test/models/api_response_test.dart index a00477a..350f09e 100644 --- a/test/models/api_response_test.dart +++ b/test/models/api_response_test.dart @@ -14,7 +14,7 @@ void main() { 'currentPage': '1', 'first': 'first', 'totalElements': '50', - } + }, }; final response = ApiResponse.fromJson(json); @@ -66,6 +66,4 @@ void main() { expect(errorDetails.message, 'Error message'); }); }); - - // Similar tests can be written for DioResponse and StreamResponse classes } diff --git a/test/models/ui_models_test.dart b/test/models/ui_models_test.dart index bc0f04c..69adfaa 100644 --- a/test/models/ui_models_test.dart +++ b/test/models/ui_models_test.dart @@ -14,34 +14,20 @@ class MockSirenDataProvider extends Mock implements SirenDataProvider { } void main() { - // late MockSirenDataProvider mockSirenDataProvider; - // setUp(() { - // mockSirenDataProvider = MockSirenDataProvider(); - // }); group('SirenDataProvider', () { test('initialize should set apiDomain from environment', () async { - // Arrange final mockSirenDataProvider = MockSirenDataProvider(); const expectedApiDomain = 'https://example.com'; - // Stub the getApiDomain method to return a specific value - // when(mockSirenDataProvider.initialize()).thenAnswer((_) => Future.value()); - - // Act await mockSirenDataProvider.initialize(); - //verify(mockSirenDataProvider.initialize()).called(1); - - // Assert expect(mockSirenDataProvider.apiDomain, expectedApiDomain); }); }); group('CardProps', () { test('constructor should initialize properties with provided values', () { - // Arrange & Act const cardProps = CardProps(hideAvatar: true, showMedia: false); - // Assert expect(cardProps.hideAvatar, true); expect(cardProps.showMedia, false); }); @@ -49,27 +35,21 @@ void main() { group('IconStyle', () { test('constructor should initialize size property with provided value', () { - // Arrange & Act const iconStyle = IconStyle(size: 24); - // Assert expect(iconStyle.size, 24.0); }); }); group('DefaultIconStyle', () { test('iconSize should return default size for the notification icon', () { - // Arrange & Act final defaultFontSize = DefaultIconStyle.defaultFontSize; - final defaultInset = DefaultIconStyle.defaultInset; final defaultSize = DefaultIconStyle.defaultSize; final defaultTop = DefaultIconStyle.defaultTop; final defaultRight = DefaultIconStyle.defaultRight; final iconSize = DefaultIconStyle.iconSize; - // Assert expect(defaultFontSize, 10); - expect(defaultInset, 1); expect(defaultSize, 20); expect(defaultTop, 0); expect(defaultRight, 2); @@ -79,10 +59,8 @@ void main() { group('BadgeStyle', () { test('constructor should initialize properties with provided values', () { - // Arrange & Act const badgeStyle = BadgeStyle(fontSize: 16, size: 20); - // Assert expect(badgeStyle.fontSize, 16.0); expect(badgeStyle.size, 20.0); }); @@ -90,14 +68,12 @@ void main() { group('SirenStyleProps', () { test('constructor should initialize properties with provided values', () { - // Arrange & Act const sirenStyleProps = SirenStyleProps( container: BoxDecoration(color: Colors.blue), iconStyle: IconStyle(size: 24), badgeStyle: BadgeStyle(fontSize: 16), ); - // Assert expect(sirenStyleProps.container!.color, Colors.blue); expect(sirenStyleProps.iconStyle!.size, 24.0); expect(sirenStyleProps.badgeStyle!.fontSize, 16.0); @@ -106,14 +82,12 @@ void main() { group('CustomThemeColors', () { test('constructor should initialize properties with provided values', () { - // Arrange & Act final customThemeColors = CustomThemeColors( backgroundColor: Colors.white, highlightedCardBorderColor: Colors.grey, badgeColor: Colors.red, ); - // Assert expect(customThemeColors.backgroundColor, Colors.white); expect(customThemeColors.highlightedCardBorderColor, Colors.grey); expect(customThemeColors.badgeColor, Colors.red); @@ -121,17 +95,14 @@ void main() { }); test('Card Params', () { - // Arrange const hideAvatar = true; const Widget deleteWidget = Icon(Icons.delete); - // Act const cardParams = CardProps( hideAvatar: hideAvatar, deleteWidget: deleteWidget, ); - // Assert expect(cardParams.hideAvatar, hideAvatar); expect(cardParams.deleteWidget, deleteWidget); }); diff --git a/test/models/unviewed_notification_count_model_test.dart b/test/models/unviewed_notification_count_model_test.dart index e801d73..213d21e 100644 --- a/test/models/unviewed_notification_count_model_test.dart +++ b/test/models/unviewed_notification_count_model_test.dart @@ -4,45 +4,33 @@ import 'package:sirenapp_flutter_inbox/src/models/unviewed_notification_count_mo void main() { group('UnViewedNotificationsCountModel', () { test('Constructor should initialize totalUnViewed', () { - // Arrange final model = UnViewedNotificationsCountModel(totalUnViewed: 10); - // Assert expect(model.totalUnViewed, 10); }); test('fromJson should correctly parse JSON', () { - // Arrange final json = {'totalUnviewed': 5}; - // Act final model = UnViewedNotificationsCountModel.fromJson(json); - // Assert expect(model.totalUnViewed, 5); }); test('fromJson should default totalUnViewed to 0 if not present in JSON', () { - // Arrange final json = {}; - - // Act final model = UnViewedNotificationsCountModel.fromJson(json); - // Assert expect(model.totalUnViewed, 0); }); test('fromJson should default totalUnViewed to 0 if JSON value is null', () { - // Arrange final json = {'totalUnviewed': null}; - // Act final model = UnViewedNotificationsCountModel.fromJson(json); - // Assert expect(model.totalUnViewed, 0); }); }); diff --git a/test/services/api_client_test.dart b/test/services/api_client_test.dart index 9c76402..6bd53fa 100644 --- a/test/services/api_client_test.dart +++ b/test/services/api_client_test.dart @@ -33,18 +33,15 @@ void main() { expect( result, true, - ); // Expect true because status code is in server error range + ); }); test('GET request', () async { - // Mock response data final responseData = {'key': 'value'}; const responseStatusCode = 200; - // Set up mock SirenDataProvider response when(mockSirenDataProvider.apiDomain).thenReturn('http://example.com'); - // Set up mock Dio response for GET request when( mockDio.get( any, @@ -61,7 +58,6 @@ void main() { ), ); - // Perform GET request final response = await apiClient.get(path: '/test'); verify( @@ -70,20 +66,16 @@ void main() { ), ).called(1); - // Verify ApiResponse matches expected result expect(response.data, responseData); expect(response.statusCode, responseStatusCode); }); test('POST request', () async { - // Mock response data final responseData = {'key': 'value'}; const responseStatusCode = 201; - // Set up mock SirenDataProvider response when(mockSirenDataProvider.apiDomain).thenReturn('http://example.com'); - // Set up mock Dio response for POST request when( mockDio.post( any, @@ -102,7 +94,6 @@ void main() { ), ); - // Perform POST request final response = await apiClient.post(path: '/test', data: {'key': 'value'}); @@ -113,20 +104,16 @@ void main() { ), ).called(1); - // Verify ApiResponse matches expected result expect(response.data, responseData); expect(response.statusCode, responseStatusCode); }); test('PATCH request', () async { - // Mock response data final responseData = {'key': 'value'}; const responseStatusCode = 200; - // Set up mock SirenDataProvider response when(mockSirenDataProvider.apiDomain).thenReturn('http://example.com'); - // Set up mock Dio response for PATCH request when( mockDio.patch( any, @@ -145,7 +132,6 @@ void main() { ), ); - // Perform PATCH request final response = await apiClient.patch(path: '/test', data: {'key': 'value'}); @@ -156,20 +142,16 @@ void main() { ), ).called(1); - // Verify ApiResponse matches expected result expect(response.data, responseData); expect(response.statusCode, responseStatusCode); }); test('DELETE request', () async { - // Mock response data final responseData = {'key': 'value'}; const responseStatusCode = 200; - // Set up mock SirenDataProvider response when(mockSirenDataProvider.apiDomain).thenReturn('http://example.com'); - // Set up mock Dio response for DELETE request when( mockDio.delete( any, @@ -197,7 +179,6 @@ void main() { }); test('Handles DioException on GET request', () async { - // Simulate DioException when making a GET request when(mockDio.get(any, queryParameters: anyNamed('queryParameters'))) .thenThrow( DioException( @@ -210,7 +191,6 @@ void main() { ), ); - // Perform the GET request using ApiClient final result = await apiClient.get(path: '/example'); expect(result.data, 'Error message'); @@ -221,7 +201,6 @@ void main() { }); test('Handles DioException on POST request', () async { - // Simulate DioException when making a GET request when(mockDio.post(any, queryParameters: anyNamed('queryParameters'))) .thenThrow( DioException( @@ -234,7 +213,6 @@ void main() { ), ); - // Perform the GET request using ApiClient final result = await apiClient.post(path: '/example'); expect(result.data, 'Error message'); @@ -245,7 +223,6 @@ void main() { }); test('Handles DioException on Patch request', () async { - // Simulate DioException when making a GET request when(mockDio.patch(any, queryParameters: anyNamed('queryParameters'))) .thenThrow( DioException( @@ -258,7 +235,6 @@ void main() { ), ); - // Perform the GET request using ApiClient final result = await apiClient.patch(path: '/example'); expect(result.data, 'Error message'); @@ -269,7 +245,6 @@ void main() { }); test('Handles DioException on delete request', () async { - // Simulate DioException when making a GET request when(mockDio.delete(any, queryParameters: anyNamed('queryParameters'))) .thenThrow( DioException( @@ -282,7 +257,6 @@ void main() { ), ); - // Perform the GET request using ApiClient final result = await apiClient.delete(path: '/example'); expect(result.data, 'Error message'); diff --git a/test/services/network_service_test.dart b/test/services/network_service_test.dart index 4937101..1849066 100644 --- a/test/services/network_service_test.dart +++ b/test/services/network_service_test.dart @@ -22,7 +22,6 @@ void main() { group('NetworkService', () { test('NetworkService instance is a singleton', () { - // Ensure that the instance is singleton final networkServiceInstance1 = NetworkService.instance; final networkServiceInstance2 = NetworkService.instance; @@ -30,7 +29,6 @@ void main() { }); test('ApiClient is correctly injected into NetworkService', () { - // Verify that the ApiClient is correctly injected into NetworkService expect(networkService.api, equals(mockApiClient)); }); }); diff --git a/test/utils/common_utils_test.dart b/test/utils/common_utils_test.dart index a5b8247..d2ef7e2 100644 --- a/test/utils/common_utils_test.dart +++ b/test/utils/common_utils_test.dart @@ -1,6 +1,6 @@ import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; -import 'package:mockito/mockito.dart'; // Import mockito +import 'package:mockito/mockito.dart'; import 'package:sirenapp_flutter_inbox/src/utils/common_utils.dart'; class MockRootBundle extends Mock implements AssetBundle {} @@ -8,7 +8,6 @@ class MockRootBundle extends Mock implements AssetBundle {} void main() { group('generateElapsedTimeText', () { test('should return correct elapsed time text', () { - // Test cases for different time differences expect( generateElapsedTimeText( DateTime.now().subtract(const Duration(seconds: 10)), @@ -53,7 +52,6 @@ void main() { const dateString = '2022-01-01T00:00:00Z'; final isoString = modifyAndConvertToISOString(dateString); - // Assert the modified date is one millisecond after the original date expect( DateTime.parse(isoString), DateTime.parse(dateString).add(const Duration(milliseconds: 1)), @@ -66,7 +64,6 @@ void main() { const dateString = '2022-01-01T00:00:00Z'; final isoString = convertToISOString(dateString); - // Assert the converted date is equal to the original date expect(DateTime.parse(isoString), DateTime.parse(dateString)); }); }); @@ -78,10 +75,5 @@ void main() { expect(envVariables.keys.first, 'API_DOMAIN'); await getApiDomain(); }); - - // Write other tests for loadEnv function... }); } - -// Mock class for AssetBundle -class MockAssetBundle extends Mock implements AssetBundle {} diff --git a/test/utils/siren_test.dart b/test/utils/siren_test.dart index 60d964c..cfe95be 100644 --- a/test/utils/siren_test.dart +++ b/test/utils/siren_test.dart @@ -40,39 +40,23 @@ void main() { setUp(() { mockReadNotificationById = MockReadNotificationById(); mockMockNotificationsBulkUpdate = MockNotificationsBulkUpdate(); - // mockSirenDataProvider = MockSirenDataProvider(); }); test( 'markAsRead method should call ReadNotificationById and update inboxController', () async { - // Arrange const notificationId = 'notification_id'; final mockResponse = ApiResponse(data: 'SUCCESS'); final response = await mockReadNotificationById.readNotificationById( notificationId: notificationId, ); - // when(mockReadNotificationById.readNotificationById(notificationId: notificationId)) - // .thenAnswer((_) => Future.value(mockResponse)); - - // Act - // final response = await Siren.markAsRead(id: notificationId); - - // Assert expect(mockResponse.data, response.data); - - // verify(mockReadNotificationById.readNotificationById(notificationId: notificationId)).called(1); - - // verify(mockSirenDataProvider.inboxController.sink.add( - // StreamResponse(mockResponse, UpdateEvents.READ_BY_ID, notificationId), - // ),).called(1); }); test( 'mark notifications as read by a specific date and update inboxController', () async { - // Arrange const startDate = '2024-03-15T04:07:14.577928Z'; final mockData = { 'until': startDate, @@ -82,9 +66,6 @@ void main() { final response = await mockMockNotificationsBulkUpdate .notificationsBulkUpdate(data: mockData); - // Assert expect(mockResponse.data, response.data); - - //verify(mockMockNotificationsBulkUpdate.notificationsBulkUpdate(data: mockData)).called(1); }); } diff --git a/test/widgets/app_bar_test.dart b/test/widgets/app_bar_test.dart index 38577ff..033c753 100644 --- a/test/widgets/app_bar_test.dart +++ b/test/widgets/app_bar_test.dart @@ -116,7 +116,7 @@ void main() { testWidgets( 'SirenAppBar calls onClearAllPressed when clear all button is pressed', (WidgetTester tester) async { - bool clearAllPressed = false; + var clearAllPressed = false; await tester.pumpWidget( MaterialApp( home: Scaffold( diff --git a/test/widgets/card_test.dart b/test/widgets/card_test.dart index fcf030e..ccd8861 100644 --- a/test/widgets/card_test.dart +++ b/test/widgets/card_test.dart @@ -13,7 +13,6 @@ class MockFunction extends Mock { void main() { testWidgets('CardWidget renders correctly', (WidgetTester tester) async { - // Create a mock notification data // ignore: unused_local_variable final func = MockFunction().call; final notification = NotificationDataType( @@ -54,7 +53,6 @@ void main() { }, ), styles: null, // Mock styles - // deleteWidget: Image(image: mockImageProvider), ), ), ); diff --git a/test/widgets/empty_widget_test.dart b/test/widgets/empty_widget_test.dart index 4344eba..db239f7 100644 --- a/test/widgets/empty_widget_test.dart +++ b/test/widgets/empty_widget_test.dart @@ -17,14 +17,11 @@ void main() { ), ); - // Verify that EmptyWidget is rendered expect(find.byType(EmptyWidget), findsOneWidget); - // Verify the texts expect(find.text(Strings.empty_title), findsOneWidget); expect(find.text(Strings.empty_desc), findsOneWidget); - // Verify the circle widget expect(find.byType(Stack), findsOneWidget); expect(find.byType(Icon), findsOneWidget); expect(find.text('0'), findsOneWidget); @@ -37,9 +34,7 @@ void main() { builder: (context) { final theme = Theme.of(context); return MaterialApp( - theme: theme.copyWith( - // Define your tertiary and outline colors here if needed - ), + theme: theme.copyWith(), home: const EmptyWidget(), ); }, diff --git a/test/widgets/error_widget_test.dart b/test/widgets/error_widget_test.dart index 1d9f719..9e30eaf 100644 --- a/test/widgets/error_widget_test.dart +++ b/test/widgets/error_widget_test.dart @@ -13,14 +13,11 @@ void main() { ), ); - // Verify that CustomErrorWidget is rendered expect(find.byType(DefaultErrorWidget), findsOneWidget); - // Verify the texts expect(find.text(Strings.error_title), findsOneWidget); expect(find.text(Strings.error_desc), findsOneWidget); - // Verify the circle widget final circleFinder = find.byWidgetPredicate( (widget) => widget is Container && @@ -29,13 +26,10 @@ void main() { ); expect(circleFinder, findsOneWidget); final circleContainer = tester.widget(circleFinder); - // expect(circleContainer.decoration, isA()); - // expect(circleContainer.child, isA()); final iconWidget = circleContainer.child! as Icon; expect(iconWidget.icon, Icons.warning_rounded); expect(iconWidget.size, 84.0); - // Verify the text styles final titleText = find.text(Strings.error_title); final descText = find.text(Strings.error_desc); expect(titleText, findsOneWidget); diff --git a/test/widgets/icon_badge_test.dart b/test/widgets/icon_badge_test.dart index 758140b..d540577 100644 --- a/test/widgets/icon_badge_test.dart +++ b/test/widgets/icon_badge_test.dart @@ -23,10 +23,8 @@ void main() { ); await tester.pumpAndSettle(const Duration(seconds: 1)); - // Check if IconBadge is rendered expect(find.byType(Positioned), findsOneWidget); - // Check if Text widget is rendered with correct text expect(find.text('5'), findsOneWidget); }); @@ -48,7 +46,6 @@ void main() { ); await tester.pumpAndSettle(const Duration(seconds: 1)); - // Check if IconBadge is not rendered expect(find.byType(Positioned), findsNothing); }); @@ -71,7 +68,6 @@ void main() { ); await tester.pumpAndSettle(const Duration(seconds: 1)); - // Check if Text widget is rendered with '99+' expect(find.text('99+'), findsOneWidget); }); }); diff --git a/test/widgets/inbox_body_test.dart b/test/widgets/inbox_body_test.dart index 83932a0..64cc089 100644 --- a/test/widgets/inbox_body_test.dart +++ b/test/widgets/inbox_body_test.dart @@ -17,7 +17,7 @@ void main() { isLoading: true, loadingNextPage: false, isError: false, - notifications: [], + notifications: const [], deleteNotification: (id) async {}, markAsRead: (id) {}, customNotificationCard: null, @@ -45,7 +45,7 @@ void main() { isLoading: false, loadingNextPage: false, isError: true, - notifications: [], + notifications: const [], deleteNotification: (id) async {}, markAsRead: (id) {}, customNotificationCard: null, diff --git a/test/widgets/loader_widget_test.dart b/test/widgets/loader_widget_test.dart index 443b25a..29bd44b 100644 --- a/test/widgets/loader_widget_test.dart +++ b/test/widgets/loader_widget_test.dart @@ -14,10 +14,8 @@ void main() { ), ); - // Verify that CardLoaderWidget is rendered expect(find.byType(CardLoaderWidget), findsOneWidget); - // Find the circular Container final circularContainerFinder = find.descendant( of: find.byType(CardLoaderWidget), matching: find.byWidgetPredicate( @@ -30,10 +28,8 @@ void main() { ), ); - // Verify that only one circular Container is found expect(circularContainerFinder, findsOneWidget); - // Find the Padding containing the Row final paddingWithRowFinder = find.descendant( of: find.byType(CardLoaderWidget), matching: find.byWidgetPredicate( @@ -41,7 +37,6 @@ void main() { ), ); - // Verify that only one Padding containing a Row is found expect(paddingWithRowFinder, findsOneWidget); }); }); diff --git a/test/widgets/notification_list_view_test.dart b/test/widgets/notification_list_view_test.dart index 69ad83e..4c0d5b8 100644 --- a/test/widgets/notification_list_view_test.dart +++ b/test/widgets/notification_list_view_test.dart @@ -6,8 +6,7 @@ import 'package:sirenapp_flutter_inbox/sirenapp_flutter_inbox.dart'; import 'package:sirenapp_flutter_inbox/src/widgets/notification_list_view.dart'; class MockFunction extends Mock { - // Define the mock function signature - void call(); // You can define parameters and return types as needed + void call(); } void main() { diff --git a/test/widgets/nullabale_text_test.dart b/test/widgets/nullabale_text_test.dart index bc140a5..3cabb34 100644 --- a/test/widgets/nullabale_text_test.dart +++ b/test/widgets/nullabale_text_test.dart @@ -5,7 +5,6 @@ import 'package:sirenapp_flutter_inbox/src/widgets/common/nullable_text.dart'; void main() { testWidgets('NullableText displays text when not null or empty', (WidgetTester tester) async { - // Build the widget await tester.pumpWidget( const MaterialApp( home: NullableText( @@ -14,17 +13,13 @@ void main() { ), ), ); - - // Find the Text widget final textFinder = find.text('Hello'); - // Verify that the Text widget is present expect(textFinder, findsOneWidget); }); testWidgets('NullableText displays nothing when text is null', (WidgetTester tester) async { - // Build the widget await tester.pumpWidget( const MaterialApp( home: NullableText( @@ -33,16 +28,13 @@ void main() { ), ); - // Find the Text widget final textFinder = find.byType(Text); - // Verify that the Text widget is not present expect(textFinder, findsNothing); }); testWidgets('NullableText displays nothing when text is empty', (WidgetTester tester) async { - // Build the widget await tester.pumpWidget( const MaterialApp( home: NullableText( @@ -52,10 +44,8 @@ void main() { ), ); - // Find the Text widget final textFinder = find.byType(Text); - // Verify that the Text widget is not present expect(textFinder, findsNothing); }); } diff --git a/test/widgets/siren_inbox_icon_test.dart b/test/widgets/siren_inbox_icon_test.dart index 6a4231a..a8966f5 100644 --- a/test/widgets/siren_inbox_icon_test.dart +++ b/test/widgets/siren_inbox_icon_test.dart @@ -15,8 +15,7 @@ import 'package:sirenapp_flutter_inbox/src/theme/app_theme.dart'; import 'siren_inbox_test.mocks.dart'; class MockFunction extends Mock { - // Define the mock function signature - void call(); // You can define parameters and return types as needed + void call(); } @GenerateMocks([SirenDataProvider, FetchUnViewedNotificationsCount]) @@ -110,7 +109,6 @@ void main() { final primaryColor = AppTheme.darkTheme.colorScheme.primary; - // Ensure dark theme is applied expect(primaryColor, const Color(0xff232326)); }); @@ -124,9 +122,8 @@ void main() { ), ); - await tester.pumpWidget(Container()); // Dispose the widget + await tester.pumpWidget(Container()); - // Verify controllers are closed expect(iconController.hasListener, false); expect(inboxController.hasListener, false); }); diff --git a/test/widgets/siren_inbox_test.dart b/test/widgets/siren_inbox_test.dart index e1abba1..73ea6b1 100644 --- a/test/widgets/siren_inbox_test.dart +++ b/test/widgets/siren_inbox_test.dart @@ -8,14 +8,12 @@ import 'package:sirenapp_flutter_inbox/sirenapp_flutter_inbox.dart'; import 'package:sirenapp_flutter_inbox/src/api/fetch_unviewed_notification_count.dart'; import 'package:sirenapp_flutter_inbox/src/constants/generics.dart'; import 'package:sirenapp_flutter_inbox/src/data/siren_data_provider.dart'; -import 'package:sirenapp_flutter_inbox/src/models/ui_models.dart'; import 'package:sirenapp_flutter_inbox/src/widgets/loader_widget.dart'; import 'siren_inbox_test.mocks.dart'; class MockFunction extends Mock { - // Define the mock function signature - void call(); // You can define parameters and return types as needed + void call(); } @GenerateNiceMocks([ @@ -49,13 +47,10 @@ void main() { ), ); - // Loading state widget should be displayed expect(find.byType(LoaderWidget), findsOneWidget); }); testWidgets('Test Title', (WidgetTester tester) async { - // Mock SirenDataProvider - await tester.pumpWidget( MaterialApp( home: Scaffold( @@ -69,13 +64,10 @@ void main() { ), ); - // Loading state widget should be displayed expect(find.byType(LoaderWidget), findsOneWidget); - // Simulate a successful fetch await tester.pump(); - // Verify that notification list is displayed expect(find.text('Notifications Header'), findsOneWidget); }); @@ -102,8 +94,6 @@ void main() { }); testWidgets('Test theme', (WidgetTester tester) async { - // Mock SirenDataProvider - await tester.pumpWidget( MaterialApp( home: Scaffold( From afc3940f0e0bac62cc6a87e610f8d1a7b6de5ba3 Mon Sep 17 00:00:00 2001 From: Anitta Babu <99161914+anitta-keyvalue@users.noreply.github.com> Date: Thu, 18 Apr 2024 16:37:53 +0530 Subject: [PATCH 04/17] feat: Add additional theme and style property support --- README.md | 129 ++++++----- lib/src/models/ui_models.dart | 227 ++++++++++++++------ lib/src/theme/app_theme.dart | 61 ++++-- lib/src/widgets/app_bar.dart | 134 ++++++------ lib/src/widgets/card.dart | 167 +++++++------- lib/src/widgets/empty_widget.dart | 2 +- lib/src/widgets/icon_badge.dart | 6 +- lib/src/widgets/inbox_body.dart | 41 ++-- lib/src/widgets/loader_widget.dart | 23 +- lib/src/widgets/notification_list_view.dart | 9 +- lib/src/widgets/siren_inbox.dart | 40 ++-- lib/src/widgets/siren_inbox_icon.dart | 6 +- test/models/ui_models_test.dart | 38 ++-- test/widgets/app_bar_test.dart | 12 +- test/widgets/siren_inbox_icon_test.dart | 2 +- 15 files changed, 536 insertions(+), 361 deletions(-) diff --git a/README.md b/README.md index 214dd42..482397c 100644 --- a/README.md +++ b/README.md @@ -24,8 +24,8 @@ import 'package:sirenapp_flutter_inbox/sirenapp_flutter_inbox.dart'; void main() { runApp( SirenProvider( - userToken: 'your_user_token', - recipientId: 'your_recipient_id', + userToken: 'YOUR_USER_TOKEN', + recipientId: 'YOUR_RECIPIENT_ID', child: MyApp(), ), ); @@ -55,17 +55,18 @@ Below are optional arguments available for the icon widget: | onError | Callback for handling errors | Function(ApiErrorDetails) | null | | onTap | Custom click handler for notification icon | VoidCallback | null | | theme | Theme properties for custom color theme | CustomThemeColors | null | -| customStyles | Style properties for custom styling | SirenStyleProps | null | +| customStyles | Style properties for custom styling | CustomStyles | null | #### Theme customization Here are the available theme options: ```dart -theme: CustomThemeColors( - badgeBackgroundColor: Colors.deepPurpleAccent, - iconColor: Colors.white, - badgeColor: Colors.white) + theme: CustomThemeColors( + notificationIconColor: Colors.purple, + badgeColors: BadgeColors( + color: Colors.greenAccent, textColor: Colors.black), + ) ``` #### Style customization @@ -73,14 +74,10 @@ theme: CustomThemeColors( Here are the custom style options for the notification icon: ```dart -customStyles: SirenStyleProps( - iconStyle: IconStyle(size: 35), - badgeStyle: BadgeStyle( - fontSize: 10, - size: 18, - top: 2, - right: 0, - )) + customStyles: CustomStyles( + notificationIconStyle: NotificationIconStyle(size: 20), + badgeStyle: BadgeStyle(fontSize: 9, size: 5), + ) ``` ### 2.3. Configure notification inbox @@ -88,14 +85,13 @@ customStyles: SirenStyleProps( Inbox is a paginated list view for displaying notifications. ```dart -SirenInbox( - theme: customTheme, - hideHeader: false, - darkMode: true, - onError: (error) () { - // Handle error - }, -); + SirenInbox( + inboxHeaderProps: InboxHeaderProps(showBackButton: true), + cardProps: CardProps(hideAvatar: false), + onError: (error) { + // Handle Error + }, + ) ``` #### Arguments for the notification inbox @@ -110,30 +106,32 @@ Given below are the arguments of Siren Inbox Widget. | customNotificationCard | Custom widget to display the notification cards | Widget | null | | customLoader | Custom widget to display the initial loading state | Widget | null | | customErrorWidget | Custom error widget | Widget | null | -| cardProps | Properties of notification card | CardProps | CardProps(hideAvatar: false, disableAutoMarkAsRead: false, hideDelete: false, deleteWidget: Icon(Icons.close), onAvatarClick: Function(NotificationDataType)) | +| cardProps | Properties of notification card | CardProps | CardProps(hideAvatar: false, disableAutoMarkAsRead: false, hideDelete: false, deleteIcon: Icon(Icons.close), onAvatarClick: Function(NotificationDataType)) | | inboxHeaderProps | Properties of notification window header | InboxHeaderProps | InboxHeaderProps(hideHeader: false, hideClearAll: false,title: 'Notifications', customHeader: null showBackButton:false, backButton: null, onBackPress: ()=> null ) | | onNotificationCardClick | Custom click handler for notification cards | Function(NotificationDataType) | null | | onError | Callback for handling errors | Function(ApiErrorDetails) | null | | theme | Theme properties for custom color theme | CustomThemeColors | null | -| customStyles | Style properties for custom styling | SirenStyleProps | null | +| customStyles | Style properties for custom styling | CustomStyles | null | #### Theme customization -Here are the available theme options: +Here are some of the available theme options: ```dart theme: CustomThemeColors( - backgroundColor: const Color.fromRGBO(218, 223, 254, 1), - highlightedCardBorderColor: const Color.fromRGBO(103, 58, 183, 1), - highlightedCardColor: const Color.fromRGBO(171, 242, 251, 1), - borderColor: const Color.fromRGBO(133, 146, 230, 1), - deleteIcon: const Color.fromRGBO(103, 58, 183, 1), - clearAllIcon: const Color.fromRGBO(103, 58, 183, 1), - textColor: const Color.fromRGBO(0, 0, 0, 1), - dateColor: const Color.fromRGBO(0, 0, 0, 1), - timerIcon: const Color.fromRGBO(133, 146, 230, 1), - inboxTitleColor: const Color.fromRGBO(0, 0, 0, 1), - ), + primary: Colors.blue, + highlightedCardColor: Colors.blueAccent, + textColor: Colors.green, + cardColors: CardColors( + titleColor: Colors.grey, + subtitleColor: Colors.grey, + ), + inboxHeaderColors: InboxHeaderColors( + titleColor: Colors.redAccent, + headerActionColor: Colors.purpleAccent, + borderColor: Colors.cyanAccent + ), + ), ``` #### Style options @@ -141,34 +139,33 @@ theme: CustomThemeColors( Here are some of the custom style options for the notification inbox: ```dart -customStyles: SirenStyleProps( - cardAvatarContainer: BoxDecoration( - border: Border.all( - color: AppColors.primaryBlue, - width: 1, - ), - shape: BoxShape.circle, +customStyles: CustomStyles( + container: ContainerStyle( + padding: EdgeInsets.all(20), + decoration: BoxDecoration(color: Colors.yellow)), + cardStyle: CardStyle( + cardContainer: ContainerStyle( + padding: EdgeInsets.all(20), + decoration: BoxDecoration( + color: Colors.yellow, + border: Border.all(color: Colors.red))), + cardTitle: TextStyle(fontSize: 22, fontWeight: FontWeight.w800), + cardSubtitle: + TextStyle(fontSize: 20, fontWeight: FontWeight.w700), + cardDescription: + TextStyle(fontSize: 18, fontWeight: FontWeight.w600), + dateStyle: TextStyle(fontSize: 16, fontWeight: FontWeight.w500), + avatarSize: 30, ), - container: BoxDecoration( - border: Border.all( - color: Colors.black, - ), - ), - contentContainer: BoxDecoration( - border: Border.all( - color: Colors.red, - ), - ), - subHeaderText: TextStyle( - color: Colors.red, - ), - cardTitle: TextStyle( - color: Colors.black, - ), - dateStyle: TextStyle( - color: Colors.green, - ), -) + appBarStyle: InboxHeaderStyle( + headerTextStyle: + TextStyle(fontSize: 20, fontWeight: FontWeight.w900), + titlePadding: EdgeInsets.symmetric(horizontal: 30), + borderWidth: 5), + dateIconSize: 30, + deleteIconSize: 30, + clearAllIconSize: 40 +), ``` ## 3. Siren Class @@ -224,8 +221,8 @@ class _MyAppState extends State { @override Widget build(BuildContext context) { return SirenProvider( - userToken: 'your-token', - recipientId: 'your-recipient-id', + userToken: 'YOUR_USER_TOKEN', + recipientId: 'YOUR_RECIPIENT_ID', child: MaterialApp( title: 'Siren Flutter Inbox', theme: ThemeData( diff --git a/lib/src/models/ui_models.dart b/lib/src/models/ui_models.dart index 13d8901..85abd88 100644 --- a/lib/src/models/ui_models.dart +++ b/lib/src/models/ui_models.dart @@ -6,9 +6,8 @@ class CardProps { /// Constructs a [CardProps] with optional parameters. const CardProps({ this.hideAvatar, - this.showMedia, this.disableAutoMarkAsRead, - this.deleteWidget, + this.deleteIcon, this.hideDelete, this.onAvatarClick, }); @@ -16,14 +15,11 @@ class CardProps { /// Determines whether to hide the avatar in the notification card in Siren inbox. final bool? hideAvatar; - /// Determines whether to show media content in the notification card in Siren inbox. - final bool? showMedia; - /// The flag to turn on and off the mark as read functionality final bool? disableAutoMarkAsRead; /// Custom widget that can be used instead of default delete in the card (x) - final Widget? deleteWidget; + final Widget? deleteIcon; /// Determines whether to hide the avatar in the notification card in Siren inbox. final bool? hideDelete; @@ -33,9 +29,9 @@ class CardProps { } /// Customizable style for the Siren notification icon. -class IconStyle { - /// Constructs an [IconStyle] with optional parameters. - const IconStyle({this.size}); +class NotificationIconStyle { + /// Constructs an [NotificationIconStyle] with optional parameters. + const NotificationIconStyle({this.size}); /// Size of the notification icon. final double? size; @@ -83,58 +79,42 @@ class BadgeStyle { } /// Style properties for customizing the appearance of various UI elements in the Siren theme. -class SirenStyleProps { - /// Constructs a [SirenStyleProps] with optional parameters. - const SirenStyleProps({ +class CustomStyles { + /// Constructs a [CustomStyles] with optional parameters. + const CustomStyles({ this.container, - this.contentContainer, - this.subHeaderText, - this.cardAvatarContainer, - this.cardContentContainer, - this.cardTitle, - this.cardDescription, - this.cardFooterRow, - this.dateStyle, - this.iconStyle, + this.cardStyle, + this.appBarStyle, + this.notificationIconStyle, this.badgeStyle, - this.defaultHeaderTextStyle, + this.deleteIconSize, + this.dateIconSize, + this.clearAllIconSize, }); - /// The decoration for the outer container of the card in Siren inbox. - final BoxDecoration? container; - - /// The decoration for the content container of the card in Siren inbox. - final BoxDecoration? contentContainer; - - /// The text style for the sub-header text in Siren inbox. - final TextStyle? subHeaderText; - - /// The decoration for the avatar container of the card in Siren inbox. - final BoxDecoration? cardAvatarContainer; - - /// The decoration for the content container of the card in Siren inbox. - final BoxDecoration? cardContentContainer; + /// The decoration for the Siren inbox list. + final ContainerStyle? container; - /// The text style for the card title in Siren inbox. - final TextStyle? cardTitle; - - /// The text style for the card description in Siren inbox. - final TextStyle? cardDescription; + // The styles for inbox list item + final CardStyle? cardStyle; - /// The decoration for the footer row of the card in Siren inbox. - final BoxDecoration? cardFooterRow; - - /// The text style for the date text in Siren inbox. - final TextStyle? dateStyle; + /// The style for default app bar + final InboxHeaderStyle? appBarStyle; /// The style for the notification icon. - final IconStyle? iconStyle; + final NotificationIconStyle? notificationIconStyle; /// The style for the notification icon badge. final BadgeStyle? badgeStyle; - /// Text style for the header provided by the sdk. - final TextStyle? defaultHeaderTextStyle; + /// Size of delete icon in inbox list card + final double? deleteIconSize; + + /// Size of date icon in inbox list card + final double? dateIconSize; + + /// Size of clear all icon in inbox default header + final double? clearAllIconSize; } /// Custom theme colors to configure the appearance of UI elements. @@ -142,7 +122,7 @@ class CustomThemeColors { /// Constructs a [CustomThemeColors] with optional parameters. CustomThemeColors({ this.backgroundColor, - this.highlightedCardBorderColor, + this.primary, this.highlightedCardColor, this.borderColor, this.deleteIcon, @@ -150,17 +130,18 @@ class CustomThemeColors { this.textColor, this.dateColor, this.timerIcon, - this.badgeBackgroundColor, - this.badgeColor, - this.iconColor, - this.inboxTitleColor, + this.notificationIconColor, + this.refreshIndicatorColor, + this.inboxHeaderColors, + this.badgeColors, + this.cardColors, }); /// The background color for Siren inbox. final Color? backgroundColor; /// The color for the border of active cards in Siren inbox. - final Color? highlightedCardBorderColor; + final Color? primary; /// The color for active cards in Siren inbox. final Color? highlightedCardColor; @@ -183,17 +164,82 @@ class CustomThemeColors { /// The color of timer icon final Color? timerIcon; - /// The background color for notification icon badge. - final Color? badgeBackgroundColor; + /// The color for notification icon. + final Color? notificationIconColor; - /// The text color for notification icon badge. - final Color? badgeColor; + /// The color for refresh indicator in inbox list. + final Color? refreshIndicatorColor; - /// The color for notification icon. - final Color? iconColor; + /// The colors for inbox list card + final CardColors? cardColors; + + /// The colors for inbox header + final InboxHeaderColors? inboxHeaderColors; - /// The color for window title in Siren inbox. - final Color? inboxTitleColor; + /// The colors for inbox list card + final BadgeColors? badgeColors; +} + +/// Custom theme colors to configure the appearance inbox list item. +class CardColors { + CardColors({ + this.borderColor, + this.background, + this.titleColor, + this.subtitleColor, + this.descriptionColor, + }); + + /// The border color inbox of list item + final Color? borderColor; + + /// The default background color of inbox list item + final Color? background; + + /// The title color inbox of list item + final Color? titleColor; + + /// The sub title color of inbox list item + final Color? subtitleColor; + + /// The description text color of inbox list item + final Color? descriptionColor; +} + +/// Custom theme colors to configure the inbox header +class InboxHeaderColors { + InboxHeaderColors({ + this.background, + this.titleColor, + this.headerActionColor, + this.borderColor, + }); + + /// The background color of inbox header + final Color? background; + + /// The title color of inbox header + final Color? titleColor; + + /// The action texts color of inbox header + final Color? headerActionColor; + + /// The border color of inbox header + final Color? borderColor; +} + +/// Custom theme colors to configure icon badge +class BadgeColors { + BadgeColors({ + this.color, + this.textColor, + }); + + /// The icon badge color + final Color? color; + + /// The text color of icon badge + final Color? textColor; } /// Properties for configuring the appearance of the notification window app bar. @@ -229,3 +275,58 @@ class InboxHeaderProps { /// Callback function for handling back navigation. final void Function()? onBackPress; } + +/// Properties to configure the style of container +class ContainerStyle { + ContainerStyle({this.padding, this.decoration}); + + /// The padding values for all sides of a container + final EdgeInsetsGeometry? padding; + + /// The appearance of the container, including + /// properties like background color, border, border radius, etc. of a container + final BoxDecoration? decoration; +} + +/// Properties to configure the style of default inbox header +class InboxHeaderStyle { + InboxHeaderStyle({this.headerTextStyle, this.titlePadding, this.borderWidth}); + + /// Text style for the default header text + final TextStyle? headerTextStyle; + + /// Padding values for all sides for header text + final EdgeInsetsGeometry? titlePadding; + + /// Border bottom with of default header container + final double? borderWidth; +} + +class CardStyle { + CardStyle({ + this.cardContainer, + this.cardTitle, + this.cardSubtitle, + this.cardDescription, + this.dateStyle, + this.avatarSize, + }); + + /// The decoration for each card in Siren inbox. + final ContainerStyle? cardContainer; + + /// The text style for the card title in Siren inbox. + final TextStyle? cardTitle; + + /// The text style for the sub-header text in Siren inbox. + final TextStyle? cardSubtitle; + + /// The text style for the card description in Siren inbox. + final TextStyle? cardDescription; + + /// The text style for the date text in Siren inbox. + final TextStyle? dateStyle; + + /// The size of avatar image + final double? avatarSize; +} diff --git a/lib/src/theme/app_theme.dart b/lib/src/theme/app_theme.dart index 04a9e7f..d62644b 100644 --- a/lib/src/theme/app_theme.dart +++ b/lib/src/theme/app_theme.dart @@ -11,10 +11,10 @@ class AppTheme { colorScheme: ThemeData.light().colorScheme.copyWith( background: AppColors.emptyWidgetBgLightTheme, inversePrimary: AppColors.grey500, - onBackground: Colors.black, + onBackground: AppColors.grey300Complementary, onPrimary: AppColors.black100, onSecondary: AppColors.avatarPlaceholderBgLight, - onTertiary: Colors.white, + onTertiary: AppColors.primary200, outline: AppColors.grey500, outlineVariant: AppColors.grey400, primary: Colors.white, @@ -27,6 +27,7 @@ class AppTheme { surfaceVariant: AppColors.avatarIconLight, tertiary: AppColors.grey700, tertiaryContainer: AppColors.red, + onInverseSurface: Colors.white, ), ); @@ -37,7 +38,7 @@ class AppTheme { onBackground: AppColors.grey50, onPrimary: Colors.white, onSecondary: AppColors.avatarPlaceholderBgDark, - onTertiary: Colors.white, + onTertiary: AppColors.primary200Complementary, outline: AppColors.grey500Complementary, outlineVariant: AppColors.grey400Complementary, primary: AppColors.black100, @@ -50,6 +51,7 @@ class AppTheme { surfaceVariant: AppColors.avatarIconDark, tertiary: AppColors.grey700Complementary, tertiaryContainer: AppColors.red, + onInverseSurface: Colors.white, ), ); @@ -62,30 +64,65 @@ class AppTheme { baseTheme.colorScheme.copyWith( inversePrimary: customColors.dateColor ?? baseTheme.colorScheme.inversePrimary, - onPrimary: customColors.iconColor ?? baseTheme.colorScheme.onPrimary, - onTertiary: customColors.badgeColor ?? baseTheme.colorScheme.onTertiary, + onPrimary: customColors.notificationIconColor ?? + baseTheme.colorScheme.onPrimary, + onTertiary: customColors.refreshIndicatorColor ?? + baseTheme.colorScheme.onTertiary, outline: customColors.clearAllIcon ?? baseTheme.colorScheme.outline, outlineVariant: customColors.deleteIcon ?? baseTheme.colorScheme.outlineVariant, primary: customColors.backgroundColor ?? baseTheme.colorScheme.primary, - secondary: customColors.highlightedCardBorderColor ?? - baseTheme.colorScheme.secondary, + secondary: customColors.primary ?? baseTheme.colorScheme.secondary, secondaryContainer: customColors.highlightedCardColor ?? baseTheme.colorScheme.secondaryContainer, surfaceTint: customColors.borderColor ?? baseTheme.colorScheme.surfaceTint, tertiary: customColors.textColor ?? baseTheme.colorScheme.tertiary, - tertiaryContainer: customColors.badgeBackgroundColor ?? - baseTheme.colorScheme.tertiaryContainer, scrim: customColors.timerIcon ?? baseTheme.colorScheme.scrim, - onBackground: - customColors.inboxTitleColor ?? baseTheme.colorScheme.onBackground, background: baseTheme.colorScheme.background, onSecondary: baseTheme.colorScheme.onSecondary, shadow: baseTheme.colorScheme.shadow, surface: baseTheme.colorScheme.surface, surfaceVariant: baseTheme.colorScheme.surfaceVariant, + onInverseSurface: baseTheme.colorScheme.onInverseSurface, ), - ); + ) + .copyWith( + badgeTheme: baseTheme.badgeTheme.copyWith( + backgroundColor: customColors.badgeColors?.color ?? + baseTheme.badgeTheme.backgroundColor, + textColor: customColors.badgeColors?.textColor ?? + baseTheme.badgeTheme.textColor, + ), + ) + .copyWith( + appBarTheme: baseTheme.appBarTheme.copyWith( + backgroundColor: customColors.inboxHeaderColors?.background ?? + baseTheme.appBarTheme.backgroundColor, + foregroundColor: + customColors.inboxHeaderColors?.headerActionColor ?? + baseTheme.appBarTheme.foregroundColor, + shadowColor: customColors.inboxHeaderColors?.borderColor ?? + baseTheme.appBarTheme.shadowColor, + ), + ) + .copyWith( + cardTheme: baseTheme.cardTheme.copyWith( + color: customColors.cardColors?.background, // Card background + shadowColor: customColors.cardColors?.borderColor, // Card border + surfaceTintColor: + customColors.cardColors?.titleColor, // Card title color + ), + ) + .copyWith( + bannerTheme: baseTheme.bannerTheme.copyWith( + backgroundColor: + customColors.cardColors?.subtitleColor, // Card sub title color + surfaceTintColor: customColors + .cardColors?.descriptionColor, // Card description color + dividerColor: customColors + .inboxHeaderColors?.titleColor, // Header title color + ), + ); } } diff --git a/lib/src/widgets/app_bar.dart b/lib/src/widgets/app_bar.dart index 36933f5..2ae881d 100644 --- a/lib/src/widgets/app_bar.dart +++ b/lib/src/widgets/app_bar.dart @@ -5,15 +5,17 @@ import 'package:sirenapp_flutter_inbox/src/models/ui_models.dart'; class SirenAppBar extends StatelessWidget implements PreferredSizeWidget { const SirenAppBar({ required this.theme, - required this.showClearAllButton, + required this.isNonEmptyNotifications, super.key, this.onClearAllPressed, this.inboxHeaderProps, + this.styles, }); final ThemeData theme; final VoidCallback? onClearAllPressed; - final bool showClearAllButton; + final bool isNonEmptyNotifications; final InboxHeaderProps? inboxHeaderProps; + final CustomStyles? styles; @override Size get preferredSize { @@ -29,87 +31,97 @@ class SirenAppBar extends StatelessWidget implements PreferredSizeWidget { } return Container( decoration: BoxDecoration( - color: theme.colorScheme.primary, + color: theme.appBarTheme.backgroundColor ?? theme.colorScheme.primary, border: Border( bottom: BorderSide( - color: theme.colorScheme.surfaceTint, + width: styles?.appBarStyle?.borderWidth ?? 1, + color: + theme.appBarTheme.shadowColor ?? theme.colorScheme.surfaceTint, ), ), ), height: preferredSize.height, - child: inboxHeaderProps?.customHeader ?? - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Row( - children: [ - if (inboxHeaderProps?.showBackButton ?? false) - Padding( - padding: const EdgeInsets.only( - left: 24, - right: 16, - ), - child: Semantics( + child: Padding( + padding: const EdgeInsets.only(right: 16, left: 20), + child: inboxHeaderProps?.customHeader ?? + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Row( + children: [ + if (inboxHeaderProps?.showBackButton ?? false) + Semantics( label: 'siren-header-back', hint: 'Tap to view navigate back', - child: IconButton( + child: GestureDetector( key: const Key('siren-header-back'), - onPressed: inboxHeaderProps?.onBackPress, - icon: inboxHeaderProps?.backButton ?? - const Icon(Icons.arrow_back_ios), + onTap: inboxHeaderProps?.onBackPress, + child: inboxHeaderProps?.backButton ?? + Icon( + Icons.arrow_back_ios, + color: theme.bannerTheme.dividerColor ?? + theme.colorScheme.onBackground, + size: 20, + ), ), ), - ), - Padding( - padding: EdgeInsets.symmetric( - horizontal: - (inboxHeaderProps?.showBackButton ?? false) ? 2 : 24, - ), - child: Text( - inboxHeaderProps?.title ?? Strings.notifications, - style: const TextStyle( - fontSize: 18, - fontWeight: FontWeight.w600, + Padding( + padding: + styles?.appBarStyle?.titlePadding ?? EdgeInsets.zero, + child: Text( + inboxHeaderProps?.title ?? Strings.notifications, + style: styles?.appBarStyle?.headerTextStyle ?? + TextStyle( + fontSize: 18, + fontWeight: FontWeight.w600, + color: theme.bannerTheme.dividerColor ?? + theme.colorScheme.onBackground, + ), ), ), - ), - ], - ), - if (!(inboxHeaderProps?.hideClearAll ?? false) && - showClearAllButton) - Padding( - padding: const EdgeInsets.only(right: 24), - child: Semantics( + ], + ), + if (!(inboxHeaderProps?.hideClearAll ?? false)) + Semantics( label: 'siren-header-clear-all', hint: 'Tap to clear all notifications', child: GestureDetector( key: const Key('siren-header-clear-all'), - onTap: onClearAllPressed, - child: Row( - children: [ - Padding( - padding: const EdgeInsets.only(right: 4), - child: Icon( - Icons.clear_all, - size: 24, - color: theme.colorScheme.outline, + onTap: () { + if (isNonEmptyNotifications && + onClearAllPressed != null) { + onClearAllPressed!(); + } + }, + child: Opacity( + opacity: isNonEmptyNotifications ? 1 : 0.4, + child: Row( + children: [ + Padding( + padding: const EdgeInsets.only(right: 4), + child: Icon( + Icons.clear_all, + size: styles?.clearAllIconSize ?? 24, + color: theme.colorScheme.outline, + ), ), - ), - Text( - Strings.clear_all, - style: TextStyle( - fontSize: 14, - fontWeight: FontWeight.w500, - color: theme.colorScheme.outline, + Text( + Strings.clear_all, + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.w500, + color: theme.appBarTheme.foregroundColor ?? + theme.colorScheme.outline, + ), ), - ), - ], + ], + ), ), ), ), - ), - ], - ), + ], + ), + ), ); } } diff --git a/lib/src/widgets/card.dart b/lib/src/widgets/card.dart index 5f4d718..64c37d7 100644 --- a/lib/src/widgets/card.dart +++ b/lib/src/widgets/card.dart @@ -25,7 +25,7 @@ class CardWidget extends StatefulWidget { final CardProps cardProps; /// Styles to be applied to various elements of the card. - final SirenStyleProps? styles; + final CustomStyles? styles; /// Callback function invoked when the card is deleted. final void Function(String) onDelete; @@ -50,48 +50,33 @@ class _CardWidgetState extends State { widget.onTap(widget.notification); }, child: Container( - decoration: widget.styles?.container ?? + decoration: widget.styles?.cardStyle?.cardContainer?.decoration ?? _getDefaultContainerDecoration(currentTheme), - child: Padding( - padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 24), - child: Container( - decoration: widget.styles?.contentContainer, - child: Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - if (!(widget.cardProps.hideAvatar ?? false)) - _buildDefaultAvatarContainer(currentTheme), - Expanded( - child: Container( - decoration: widget.styles?.cardContentContainer, - child: Padding( - padding: const EdgeInsets.only( - right: 16, - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - _buildHeaderText(currentTheme), - _buildSubHeaderText(currentTheme), - _buildBodyText(currentTheme), - _buildFooterRow(currentTheme), - ], - ), + padding: widget.styles?.cardStyle?.cardContainer?.padding ?? + const EdgeInsets.symmetric(vertical: 12, horizontal: 12), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (!(widget.cardProps.hideAvatar ?? false)) + _buildDefaultAvatarContainer(currentTheme), + Expanded( + child: Padding( + padding: const EdgeInsets.only(left: 6), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _buildHeaderText(currentTheme), + _buildSubHeaderText(currentTheme), + _buildBodyText(currentTheme), + _buildFooterRow( + currentTheme, + widget.styles?.dateIconSize ?? 14, ), - ), + ], ), - if (!(widget.cardProps.hideDelete ?? false)) - GestureDetector( - key: Key( - 'siren-notification-delete-${widget.notification.id}', - ), - onTap: () => widget.onDelete(widget.notification.id), - child: widget.cardProps.deleteWidget ?? - _buildDefaultDeleteButton(currentTheme), - ), - ], + ), ), - ), + ], ), ), ); @@ -99,7 +84,7 @@ class _CardWidgetState extends State { BorderSide _getDefaultBorderDecoration(ThemeData theme) { return BorderSide( - color: theme.colorScheme.surfaceTint, + color: theme.cardTheme.shadowColor ?? theme.colorScheme.surfaceTint, width: 0.5, ); } @@ -109,7 +94,7 @@ class _CardWidgetState extends State { border: Border( left: BorderSide( color: widget.notification.isRead - ? theme.colorScheme.primary + ? Colors.transparent : theme.colorScheme.secondary, width: 4, ), @@ -118,7 +103,7 @@ class _CardWidgetState extends State { ), color: widget.notification.cardColor ?? (widget.notification.isRead - ? null + ? theme.cardTheme.color ?? Colors.transparent : theme.colorScheme.secondaryContainer), ); } @@ -132,52 +117,74 @@ class _CardWidgetState extends State { }, child: Padding( padding: const EdgeInsets.only( - right: 24, + right: 6, + left: 6, ), - child: Container( - decoration: widget.styles?.cardAvatarContainer, - child: CircleAvatar( - radius: 21, - backgroundImage: avatarUrl != null && avatarUrl.isNotEmpty - ? NetworkImage(avatarUrl) - : null, - backgroundColor: theme.colorScheme.onSecondary, - child: avatarUrl == null || avatarUrl.isEmpty - ? Icon( - Icons.landscape_rounded, - color: theme.colorScheme.surfaceVariant, - ) - : null, - ), + child: CircleAvatar( + radius: widget.styles?.cardStyle?.avatarSize ?? 21, + backgroundImage: avatarUrl != null && avatarUrl.isNotEmpty + ? NetworkImage(avatarUrl) + : null, + backgroundColor: theme.colorScheme.onSecondary, + child: avatarUrl == null || avatarUrl.isEmpty + ? Icon( + Icons.landscape_rounded, + color: theme.colorScheme.surfaceVariant, + ) + : null, ), ), ); } Widget _buildHeaderText(ThemeData theme) { - return Text( - widget.notification.message.header ?? '', - maxLines: 2, - overflow: TextOverflow.ellipsis, - style: widget.styles?.cardTitle ?? - TextStyle( - fontSize: 14, - fontWeight: FontWeight.w600, - color: theme.colorScheme.tertiary, + return Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + child: Text( + widget.notification.message.header ?? '', + maxLines: 2, + overflow: TextOverflow.ellipsis, + style: widget.styles?.cardStyle?.cardTitle ?? + TextStyle( + fontSize: 14, + fontWeight: FontWeight.w600, + color: theme.cardTheme.surfaceTintColor ?? + theme.colorScheme.tertiary, + ), + ), + ), + if (!(widget.cardProps.hideDelete ?? false)) ...[ + const SizedBox(width: 8), + GestureDetector( + key: Key( + 'siren-notification-delete-${widget.notification.id}', + ), + onTap: () => widget.onDelete(widget.notification.id), + child: widget.cardProps.deleteIcon ?? + _buildDefaultDeleteButton( + theme, + widget.styles?.deleteIconSize ?? 18, + ), ), + ], + ], ); } Widget _buildSubHeaderText(ThemeData theme) { return Padding( - padding: const EdgeInsets.symmetric(vertical: 8), + padding: const EdgeInsets.symmetric(vertical: 6), child: NullableText( text: widget.notification.message.subHeader, - style: widget.styles?.subHeaderText ?? + style: widget.styles?.cardStyle?.cardSubtitle ?? TextStyle( fontSize: 14, fontWeight: FontWeight.w500, - color: theme.colorScheme.tertiary, + color: theme.bannerTheme.backgroundColor ?? + theme.colorScheme.tertiary, ), ), ); @@ -186,30 +193,30 @@ class _CardWidgetState extends State { Widget _buildBodyText(ThemeData theme) { return Text( widget.notification.message.body ?? '', - style: widget.styles?.cardDescription ?? + style: widget.styles?.cardStyle?.cardDescription ?? TextStyle( fontSize: 14, fontWeight: FontWeight.w400, - color: theme.colorScheme.tertiary, + color: theme.bannerTheme.surfaceTintColor ?? + theme.colorScheme.tertiary, ), maxLines: 2, overflow: TextOverflow.ellipsis, ); } - Widget _buildFooterRow(ThemeData theme) { + Widget _buildFooterRow(ThemeData theme, double size) { return Padding( padding: const EdgeInsets.only( top: 10, ), child: Container( - decoration: widget.styles?.cardFooterRow, - child: _buildTimestampText(theme), + child: _buildTimestampText(theme, size), ), ); } - Widget _buildTimestampText(ThemeData theme) { + Widget _buildTimestampText(ThemeData theme, double size) { return Row( children: [ Padding( @@ -217,14 +224,14 @@ class _CardWidgetState extends State { child: Icon( Icons.access_time_sharp, color: theme.colorScheme.scrim, - size: 14, + size: size, ), ), Text( generateElapsedTimeText( DateTime.parse(widget.notification.createdAt), ), - style: widget.styles?.dateStyle ?? + style: widget.styles?.cardStyle?.dateStyle ?? TextStyle( fontSize: 12, fontWeight: FontWeight.w400, @@ -235,11 +242,11 @@ class _CardWidgetState extends State { ); } - Widget _buildDefaultDeleteButton(ThemeData theme) { + Widget _buildDefaultDeleteButton(ThemeData theme, double size) { return Icon( Icons.close, color: theme.colorScheme.outlineVariant, - size: 18, + size: size, ); } } diff --git a/lib/src/widgets/empty_widget.dart b/lib/src/widgets/empty_widget.dart index b4a0f24..af294b9 100644 --- a/lib/src/widgets/empty_widget.dart +++ b/lib/src/widgets/empty_widget.dart @@ -68,7 +68,7 @@ Widget _buildCircle(ThemeData theme) { right: 50, top: 55, child: Container( - padding: const EdgeInsets.all(4), + padding: const EdgeInsets.all(8), decoration: BoxDecoration( color: theme.colorScheme.surface, shape: BoxShape.circle, diff --git a/lib/src/widgets/icon_badge.dart b/lib/src/widgets/icon_badge.dart index f52eaed..17ee846 100644 --- a/lib/src/widgets/icon_badge.dart +++ b/lib/src/widgets/icon_badge.dart @@ -29,7 +29,8 @@ class IconBadge extends StatelessWidget { ), decoration: BoxDecoration( shape: BoxShape.circle, - color: currentTheme.colorScheme.tertiaryContainer, + color: currentTheme.badgeTheme.backgroundColor ?? + currentTheme.colorScheme.tertiaryContainer, ), child: Align( child: Text( @@ -37,7 +38,8 @@ class IconBadge extends StatelessWidget { ? '99+' : notificationsCount.toString(), style: TextStyle( - color: currentTheme.colorScheme.onTertiary, + color: currentTheme.badgeTheme.textColor ?? + currentTheme.colorScheme.onInverseSurface, fontSize: badgeStyle?.fontSize ?? DefaultIconStyle.defaultFontSize, ), diff --git a/lib/src/widgets/inbox_body.dart b/lib/src/widgets/inbox_body.dart index 3c6a8db..8200541 100644 --- a/lib/src/widgets/inbox_body.dart +++ b/lib/src/widgets/inbox_body.dart @@ -46,7 +46,7 @@ class InboxBody extends StatelessWidget { final Widget? customErrorWidget; final Widget? customLoader; final bool endReached; - final SirenStyleProps? customStyles; + final CustomStyles? customStyles; final CardProps? cardProps; final VoidCallback onEndReached; final ScrollController scrollController; @@ -56,7 +56,7 @@ class InboxBody extends StatelessWidget { Widget build(BuildContext context) { if (isError) { return RefreshIndicator( - color: currentTheme.colorScheme.secondary, + color: currentTheme.colorScheme.onTertiary, backgroundColor: currentTheme.colorScheme.primary, onRefresh: onRefresh, child: ListView( @@ -90,22 +90,27 @@ class InboxBody extends StatelessWidget { child: listEmptyWidget ?? const EmptyWidget(), ); } else { - return NotificationListView( - notifications: notifications, - isLoading: isLoading, - endReached: endReached, - onRefresh: onRefresh, - onEndReached: onEndReached, - loadingNextPage: loadingNextPage, - customStyles: customStyles, - scrollController: scrollController, - onDelete: deleteNotification, - markAsRead: markAsRead, - customNotificationCard: customNotificationCard, - onNotificationCardClick: onNotificationCardClick, - deletingNotificationId: deletingNotificationId, - totalElements: totalElements, - cardProps: cardProps, + return Container( + decoration: customStyles?.container?.decoration, + padding: customStyles?.container?.padding, + child: NotificationListView( + notifications: notifications, + isLoading: isLoading, + endReached: endReached, + onRefresh: onRefresh, + onEndReached: onEndReached, + loadingNextPage: loadingNextPage, + customStyles: customStyles, + scrollController: scrollController, + onDelete: deleteNotification, + markAsRead: markAsRead, + customNotificationCard: customNotificationCard, + onNotificationCardClick: onNotificationCardClick, + deletingNotificationId: deletingNotificationId, + totalElements: totalElements, + cardProps: cardProps, + loadingIndicator: currentTheme.colorScheme.onTertiary, + ), ); } } diff --git a/lib/src/widgets/loader_widget.dart b/lib/src/widgets/loader_widget.dart index 63230b1..96fda86 100644 --- a/lib/src/widgets/loader_widget.dart +++ b/lib/src/widgets/loader_widget.dart @@ -64,13 +64,13 @@ class CardLoaderWidgetState extends State decoration: BoxDecoration( borderRadius: BorderRadius.circular(8), ), - padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 24), + padding: const EdgeInsets.symmetric(vertical: 10, horizontal: 10), child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ if (!widget.hideAvatar) Padding( - padding: const EdgeInsets.only(right: 24), + padding: const EdgeInsets.only(right: 6, left: 6), child: _buildAnimatedWidget( theme: currentTheme, builder: (context, child) => Container( @@ -87,7 +87,7 @@ class CardLoaderWidgetState extends State Expanded( child: Padding( padding: const EdgeInsets.only( - right: 24, + right: 12, ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, @@ -103,7 +103,7 @@ class CardLoaderWidgetState extends State ), ), ), - const SizedBox(height: 8), + const SizedBox(height: 12), _buildAnimatedWidget( theme: currentTheme, builder: (context, child) => Container( @@ -115,7 +115,7 @@ class CardLoaderWidgetState extends State ), ), ), - const SizedBox(height: 8), + const SizedBox(height: 12), _buildAnimatedWidget( theme: currentTheme, builder: (context, child) => Container( @@ -127,7 +127,7 @@ class CardLoaderWidgetState extends State ), ), ), - const SizedBox(height: 8), + const SizedBox(height: 12), Row( children: [ _buildAnimatedWidget( @@ -142,7 +142,7 @@ class CardLoaderWidgetState extends State ), ), ), - const SizedBox(width: 8), + const SizedBox(width: 12), Expanded( child: _buildAnimatedWidget( theme: currentTheme, @@ -183,9 +183,12 @@ class CardLoaderWidgetState extends State required ThemeData theme, required Widget Function(BuildContext, Widget?) builder, }) { - return AnimatedBuilder( - animation: _controller, - builder: builder, + return Padding( + padding: const EdgeInsets.only(left: 6), + child: AnimatedBuilder( + animation: _controller, + builder: builder, + ), ); } } diff --git a/lib/src/widgets/notification_list_view.dart b/lib/src/widgets/notification_list_view.dart index 5ad1823..81c5216 100644 --- a/lib/src/widgets/notification_list_view.dart +++ b/lib/src/widgets/notification_list_view.dart @@ -19,6 +19,7 @@ class NotificationListView extends StatefulWidget { this.deletingNotificationId, this.totalElements, this.cardProps, + this.loadingIndicator, super.key, }); @@ -28,7 +29,7 @@ class NotificationListView extends StatefulWidget { final bool loadingNextPage; final Future Function() onRefresh; final VoidCallback onEndReached; - final SirenStyleProps? customStyles; + final CustomStyles? customStyles; final ScrollController scrollController; final Future Function(String) onDelete; final void Function(String) markAsRead; @@ -37,6 +38,7 @@ class NotificationListView extends StatefulWidget { final String? deletingNotificationId; final int? totalElements; final CardProps? cardProps; + final Color? loadingIndicator; @override State createState() => _NotificationListViewState(); @@ -70,7 +72,7 @@ class _NotificationListViewState extends State { @override Widget build(BuildContext context) { return RefreshIndicator( - color: Theme.of(context).colorScheme.secondary, + color: widget.loadingIndicator ?? Theme.of(context).colorScheme.secondary, backgroundColor: Theme.of(context).colorScheme.primary, onRefresh: widget.onRefresh, child: Semantics( @@ -113,7 +115,8 @@ class _NotificationListViewState extends State { padding: const EdgeInsets.symmetric(vertical: 8), child: Center( child: CircularProgressIndicator( - color: Theme.of(context).colorScheme.secondary, + color: widget.loadingIndicator ?? + Theme.of(context).colorScheme.secondary, ), ), ) diff --git a/lib/src/widgets/siren_inbox.dart b/lib/src/widgets/siren_inbox.dart index 97d185e..0e6217d 100644 --- a/lib/src/widgets/siren_inbox.dart +++ b/lib/src/widgets/siren_inbox.dart @@ -26,15 +26,18 @@ class SirenInbox extends StatefulWidget { this.customLoader, this.customErrorWidget, this.cardProps, + this.inboxHeaderProps, this.onNotificationCardClick, this.onError, this.theme, this.customStyles, - this.inboxHeaderProps, }); - /// Custom styles for the card of each notification. - final SirenStyleProps? customStyles; + /// Flag for enabling dark mode. + final bool? darkMode; + + /// Notifications to be fetched in each request + final int? itemsPerFetch; /// Widget to display when the notification list is empty. final Widget? listEmptyWidget; @@ -42,32 +45,30 @@ class SirenInbox extends StatefulWidget { /// Custom builder for notification cards. final Widget Function(NotificationDataType)? customNotificationCard; - /// Callback function when a notification card is clicked. - final void Function(NotificationDataType)? onNotificationCardClick; - - /// Callback function for handling errors. - final void Function(ApiErrorDetails)? onError; - - /// Flag for enabling dark mode. - final bool? darkMode; - - /// Custom theme colors for the inbox, this focuses on the idea of colorSchemes in flutter theme. - final CustomThemeColors? theme; - /// Custom loader widget. final Widget? customLoader; /// Custom error widget. final Widget? customErrorWidget; - /// Notifications to be fetched in each request - final int? itemsPerFetch; - ///Custom props for Card properties final CardProps? cardProps; /// Custom props for header properties final InboxHeaderProps? inboxHeaderProps; + + /// Callback function when a notification card is clicked. + final void Function(NotificationDataType)? onNotificationCardClick; + + /// Callback function for handling errors. + final void Function(ApiErrorDetails)? onError; + + /// Custom theme colors for the inbox, this focuses on the idea of colorSchemes in flutter theme. + final CustomThemeColors? theme; + + /// Custom styles for the card of each notification. + final CustomStyles? customStyles; + @override State createState() => _SirenInboxState(); } @@ -471,8 +472,9 @@ class _SirenInboxState extends State { appBar: SirenAppBar( theme: currentTheme, onClearAllPressed: onBulkDelete, - showClearAllButton: shouldShowClearAllButton(), + isNonEmptyNotifications: shouldShowClearAllButton(), inboxHeaderProps: widget.inboxHeaderProps, + styles: widget.customStyles, ), body: InboxBody( currentTheme: currentTheme, diff --git a/lib/src/widgets/siren_inbox_icon.dart b/lib/src/widgets/siren_inbox_icon.dart index 4d5812b..e66762d 100644 --- a/lib/src/widgets/siren_inbox_icon.dart +++ b/lib/src/widgets/siren_inbox_icon.dart @@ -34,7 +34,7 @@ class SirenInboxIcon extends StatefulWidget { final CustomThemeColors? theme; /// Custom styles for the inbox icon. - final SirenStyleProps? customStyles; + final CustomStyles? customStyles; /// Callback function to handle errors. final void Function(ApiErrorDetails)? onError; @@ -175,8 +175,8 @@ class _SirenInboxIconState extends State { : (widget.darkMode ? AppTheme.darkTheme : AppTheme.lightTheme), child: Builder( builder: (context) { - final size = - widget.customStyles?.iconStyle?.size ?? DefaultIconStyle.iconSize; + final size = widget.customStyles?.notificationIconStyle?.size ?? + DefaultIconStyle.iconSize; final currentTheme = Theme.of(context); return IgnorePointer( ignoring: widget.disabled, diff --git a/test/models/ui_models_test.dart b/test/models/ui_models_test.dart index 69adfaa..7c489c9 100644 --- a/test/models/ui_models_test.dart +++ b/test/models/ui_models_test.dart @@ -26,16 +26,17 @@ void main() { group('CardProps', () { test('constructor should initialize properties with provided values', () { - const cardProps = CardProps(hideAvatar: true, showMedia: false); + const cardProps = CardProps( + hideAvatar: true, + ); expect(cardProps.hideAvatar, true); - expect(cardProps.showMedia, false); }); }); group('IconStyle', () { test('constructor should initialize size property with provided value', () { - const iconStyle = IconStyle(size: 24); + const iconStyle = NotificationIconStyle(size: 24); expect(iconStyle.size, 24.0); }); @@ -66,16 +67,23 @@ void main() { }); }); - group('SirenStyleProps', () { + group('CustomStyles', () { test('constructor should initialize properties with provided values', () { - const sirenStyleProps = SirenStyleProps( - container: BoxDecoration(color: Colors.blue), - iconStyle: IconStyle(size: 24), - badgeStyle: BadgeStyle(fontSize: 16), + final sirenStyleProps = CustomStyles( + cardStyle: CardStyle( + cardContainer: ContainerStyle( + decoration: const BoxDecoration(color: Colors.blue), + ), + ), + notificationIconStyle: const NotificationIconStyle(size: 24), + badgeStyle: const BadgeStyle(fontSize: 16), ); - expect(sirenStyleProps.container!.color, Colors.blue); - expect(sirenStyleProps.iconStyle!.size, 24.0); + expect( + sirenStyleProps.cardStyle?.cardContainer!.decoration!.color, + Colors.blue, + ); + expect(sirenStyleProps.notificationIconStyle!.size, 24.0); expect(sirenStyleProps.badgeStyle!.fontSize, 16.0); }); }); @@ -84,13 +92,11 @@ void main() { test('constructor should initialize properties with provided values', () { final customThemeColors = CustomThemeColors( backgroundColor: Colors.white, - highlightedCardBorderColor: Colors.grey, - badgeColor: Colors.red, + primary: Colors.grey, ); expect(customThemeColors.backgroundColor, Colors.white); - expect(customThemeColors.highlightedCardBorderColor, Colors.grey); - expect(customThemeColors.badgeColor, Colors.red); + expect(customThemeColors.primary, Colors.grey); }); }); @@ -100,10 +106,10 @@ void main() { const cardParams = CardProps( hideAvatar: hideAvatar, - deleteWidget: deleteWidget, + deleteIcon: deleteWidget, ); expect(cardParams.hideAvatar, hideAvatar); - expect(cardParams.deleteWidget, deleteWidget); + expect(cardParams.deleteIcon, deleteWidget); }); } diff --git a/test/widgets/app_bar_test.dart b/test/widgets/app_bar_test.dart index 033c753..1648b98 100644 --- a/test/widgets/app_bar_test.dart +++ b/test/widgets/app_bar_test.dart @@ -15,7 +15,7 @@ void main() { title: title, showBackButton: false, ), - showClearAllButton: false, + isNonEmptyNotifications: false, ), ), ), @@ -35,7 +35,7 @@ void main() { title: 'Title', showBackButton: true, ), - showClearAllButton: false, + isNonEmptyNotifications: false, ), ), ), @@ -57,7 +57,7 @@ void main() { showBackButton: false, hideClearAll: true, ), - showClearAllButton: true, + isNonEmptyNotifications: true, ), ), ), @@ -78,7 +78,7 @@ void main() { title: 'Title', showBackButton: false, ), - showClearAllButton: true, + isNonEmptyNotifications: true, ), ), ), @@ -103,7 +103,7 @@ void main() { backButtonPressed = true; }, ), - showClearAllButton: false, + isNonEmptyNotifications: false, ), ), ), @@ -126,7 +126,7 @@ void main() { title: 'Title', showBackButton: false, ), - showClearAllButton: true, + isNonEmptyNotifications: true, onClearAllPressed: () { clearAllPressed = true; }, diff --git a/test/widgets/siren_inbox_icon_test.dart b/test/widgets/siren_inbox_icon_test.dart index a8966f5..9dfc5c2 100644 --- a/test/widgets/siren_inbox_icon_test.dart +++ b/test/widgets/siren_inbox_icon_test.dart @@ -197,7 +197,7 @@ void main() { home: Scaffold( body: SirenInboxIcon( darkMode: true, - theme: CustomThemeColors(iconColor: Colors.amber), + theme: CustomThemeColors(notificationIconColor: Colors.amber), ), ), ); From da3ac02315b47255f0fbd5714c1fcd4f601ba6a8 Mon Sep 17 00:00:00 2001 From: Anitta Babu <99161914+anitta-keyvalue@users.noreply.github.com> Date: Wed, 24 Apr 2024 11:11:34 +0530 Subject: [PATCH 05/17] feat: Update arguments, function names , types, add error codes --- README.md | 76 ++++++------- example/lib/main.dart | 98 +++++++++++++++- example/lib/siren_icon.dart | 75 ------------- example/lib/siren_window.dart | 21 ---- example/pubspec.lock | 83 +++++--------- example/pubspec.yaml | 5 +- lib/src/api/delete_notification_by_id.dart | 9 +- lib/src/api/fetch_all_notification.dart | 12 +- .../fetch_unviewed_notification_count.dart | 8 +- .../api/mark_all_notifications_as_viewed.dart | 8 +- lib/src/api/notifications_bulk_update.dart | 14 +-- lib/src/api/read_notification_by_id.dart | 8 +- lib/src/api/verify_token.dart | 18 ++- lib/src/constants/generics.dart | 105 +++++++++++++++--- lib/src/constants/strings.dart | 21 ++++ lib/src/data/siren_data_provider.dart | 60 +++++++--- lib/src/models/api_response.dart | 27 +++-- lib/src/models/notification_model.dart | 12 +- lib/src/models/ui_models.dart | 12 +- lib/src/theme/app_theme.dart | 21 ++-- lib/src/utils/siren.dart | 34 +++--- lib/src/widgets/app_bar.dart | 24 ++-- lib/src/widgets/card.dart | 25 +++-- lib/src/widgets/empty_widget.dart | 4 +- lib/src/widgets/error_widget.dart | 2 +- lib/src/widgets/inbox_body.dart | 22 ++-- lib/src/widgets/notification_list_view.dart | 24 ++-- lib/src/widgets/siren_inbox.dart | 59 +++++----- lib/src/widgets/siren_inbox_icon.dart | 8 +- pubspec.yaml | 2 +- test/constants/generics_test.dart | 41 ++++--- test/delete_notification_by_id_test.dart | 5 +- test/models/api_response_test.dart | 8 +- test/models/notification_model_test.dart | 6 +- test/models/ui_models_test.dart | 8 +- test/utils/siren_test.dart | 8 +- test/widgets/app_bar_test.dart | 12 +- test/widgets/card_test.dart | 4 +- test/widgets/inbox_body_test.dart | 16 +-- test/widgets/notification_list_view_test.dart | 10 +- test/widgets/siren_inbox_test.dart | 18 +-- 41 files changed, 558 insertions(+), 475 deletions(-) delete mode 100644 example/lib/siren_icon.dart delete mode 100644 example/lib/siren_window.dart diff --git a/README.md b/README.md index 482397c..1b062dc 100644 --- a/README.md +++ b/README.md @@ -46,16 +46,16 @@ SirenInboxIcon() Below are optional arguments available for the icon widget: -| Arguments | Description | Type | Default value | -| ---------------- | ---------------------------------------------------------- | ------------------------- | ------------- | -| darkMode | Toggle to enable dark mode when custom theme is not passed | boolean | false | -| disabled | Toggle to disable click on icon | boolean | false | -| hideBadge | Toggle to hide unviewed count badge | boolean | false | -| notificationIcon | Option to use custom notification icon | Widget | null | -| onError | Callback for handling errors | Function(ApiErrorDetails) | null | -| onTap | Custom click handler for notification icon | VoidCallback | null | -| theme | Theme properties for custom color theme | CustomThemeColors | null | -| customStyles | Style properties for custom styling | CustomStyles | null | +| Arguments | Description | Type | Default value | +| ---------------- | ---------------------------------------------------------- | ------------------------ | ------------- | +| darkMode | Toggle to enable dark mode when custom theme is not passed | bool | false | +| disabled | Toggle to disable click on icon | bool | false | +| hideBadge | Toggle to hide unviewed count badge | bool | false | +| notificationIcon | Option to use custom notification icon | Widget | null | +| onError | Callback for handling errors | Function(SirenErrorType) | null | +| onTap | Custom click handler for notification icon | VoidCallback | null | +| theme | Theme properties for custom color theme | CustomThemeColors | null | +| customStyles | Style properties for custom styling | CustomStyles | null | #### Theme customization @@ -86,8 +86,8 @@ Inbox is a paginated list view for displaying notifications. ```dart SirenInbox( - inboxHeaderProps: InboxHeaderProps(showBackButton: true), - cardProps: CardProps(hideAvatar: false), + headerParams: HeaderParams(showBackButton: true), + cardParams: CardParams(hideAvatar: false), onError: (error) { // Handle Error }, @@ -98,20 +98,20 @@ Inbox is a paginated list view for displaying notifications. Given below are the arguments of Siren Inbox Widget. -| Arguments | Description | Type | Default value | -| ----------------------- | -------------------------------------------------------------------- | ------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| darkMode | Toggle to enable dark mode when custom theme is not passed | boolean | false | -| itemsPerFetch | Number of notifications fetch per api request (have a max cap of 50) | int | 20 | -| listEmptyWidget | Custom widget for empty notification list | Widget | null | -| customNotificationCard | Custom widget to display the notification cards | Widget | null | -| customLoader | Custom widget to display the initial loading state | Widget | null | -| customErrorWidget | Custom error widget | Widget | null | -| cardProps | Properties of notification card | CardProps | CardProps(hideAvatar: false, disableAutoMarkAsRead: false, hideDelete: false, deleteIcon: Icon(Icons.close), onAvatarClick: Function(NotificationDataType)) | -| inboxHeaderProps | Properties of notification window header | InboxHeaderProps | InboxHeaderProps(hideHeader: false, hideClearAll: false,title: 'Notifications', customHeader: null showBackButton:false, backButton: null, onBackPress: ()=> null ) | -| onNotificationCardClick | Custom click handler for notification cards | Function(NotificationDataType) | null | -| onError | Callback for handling errors | Function(ApiErrorDetails) | null | -| theme | Theme properties for custom color theme | CustomThemeColors | null | -| customStyles | Style properties for custom styling | CustomStyles | null | +| Arguments | Description | Type | Default value | +| ----------------- | -------------------------------------------------------------------- | -------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| darkMode | Toggle to enable dark mode when custom theme is not passed | bool | false | +| itemsPerFetch | Number of notifications fetch per api request (have a max cap of 50) | int | 20 | +| listEmptyWidget | Custom widget for empty notification list | Widget | null | +| customCard | Custom widget to display the notification cards | Widget | null | +| customLoader | Custom widget to display the initial loading state | Widget | null | +| customErrorWidget | Custom error widget | Widget | null | +| cardParams | Properties of notification card | CardParams | CardParams(hideAvatar: false, disableAutoMarkAsRead: false, hideDelete: false, deleteIcon: Icon(Icons.close), onAvatarClick: Function(NotificationType)) | +| headerParams | Properties of notification window header | HeaderParams | HeaderParams(hideHeader: false, hideClearAll: false,title: 'Notifications', customHeader: null showBackButton:false, backButton: null, onBackPress: ()=> null ) | +| onCardClick | Custom click handler for notification cards | Function(NotificationType) | null | +| onError | Callback for handling errors | Function(SirenErrorType) | null | +| theme | Theme properties for custom color theme | CustomThemeColors | null | +| customStyles | Style properties for custom styling | CustomStyles | null | #### Theme customization @@ -178,25 +178,11 @@ Siren.markAsRead(id: 'notification-id'); | Function | Arguments | Type | Description | | -------------------------------- | --------- | --------------- | -------------------------------------------------------------------- | -| markAllNotificationsAsReadByDate | startDate | ISO date string | Sets the read status of notifications to true until the given date | -| markAsRead | id | string | Set read status of a notification to true | -| deleteNotification | id | string | Delete a notification by id | -| deleteNotificationsByDate | startDate | ISO date string | Delete all notifications until given date | -| markNotificationsAsViewed | startDate | ISO date string | Sets the viewed status of notifications to true until the given date | - -## 4. Error Codes - -Given below are all possible error codes thrown by the package: - -| Error code | Description | -| -------------------------- | ------------------------------------------------------------------- | -| GENERIC_API_ERROR | Occurrence of an unexpected api error | -| AUTHENTICATION_FAILED | Verification of the given tokens has failed | -| FETCH_COUNT_FAILED | An error occurred while fetching unviewed count | -| NOTIFICATION_FETCH_FAILED | An error occurred while fetching notifications | -| NOTIFICATION_READ_FAILED | An error occurred while marking notifications as read | -| NOTIFICATION_DELETE_FAILED | An error occurred while deleting notifications | -| UPDATE_VIEWED_FAILED | An error occurred while updating the viewed status of notifications | +| markAsReadByDate | startDate | ISO date string | Sets the read status of notifications to true until the given date | +| markAsReadById | id | string | Set read status of a notification to true | +| deleteById | id | string | Delete a notification by id | +| deleteByDate | startDate | ISO date string | Delete all notifications until given date | +| markAllAsViewed | startDate | ISO date string | Sets the viewed status of notifications to true until the given date | ## Example diff --git a/example/lib/main.dart b/example/lib/main.dart index 1df54e4..f5e1241 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -1,6 +1,4 @@ import 'package:sirenapp_flutter_inbox/sirenapp_flutter_inbox.dart'; -import './siren_icon.dart'; -import './siren_window.dart'; import 'package:flutter/material.dart'; void main() { @@ -50,13 +48,13 @@ class MyHomePageState extends State { : [ IconButton( onPressed: () { - Siren.deleteNotificationByDate( + Siren.deleteByDate( startDate: DateTime.now().toUtc().toIso8601String()); }, icon: const Icon(Icons.delete_forever)), IconButton( onPressed: () { - Siren.markNotificationsAsReadByDate( + Siren.markAsReadByDate( startDate: DateTime.now().toUtc().toIso8601String()); }, icon: const Icon(Icons.mark_email_read)), @@ -86,3 +84,95 @@ class MyHomePageState extends State { ); } } + +class SirenIconWidget extends StatefulWidget { + const SirenIconWidget({Key? key}) : super(key: key); + + @override + State createState() => _SirenIconWidgetState(); +} + +class _SirenIconWidgetState extends State { + Icon? notificationIcon; + bool? hideBadge; + + @override + Widget build(BuildContext context) { + return Scaffold( + body: Column( + children: [ + const Spacer(), + Center( + child: SirenInboxIcon( + notificationIcon: notificationIcon, + darkMode: true, + hideBadge: hideBadge, + onError: (error) { + // print('This is the inApp error message ${error.message}'); + }, + ), + ), + Text( + 'You are viewing ${notificationIcon == null ? 'default' : 'custom'} icon', + ), + Text( + 'You are ${hideBadge == false ? 'viewing' : 'not viewing'} notification count', + ), + const Spacer(), + Padding( + padding: const EdgeInsets.all(16), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + ElevatedButton( + onPressed: () { + setState(() { + notificationIcon = notificationIcon == null + ? const Icon( + Icons.notification_add, + color: Colors.black, + ) + : null; + }); + }, + child: Text( + notificationIcon == null ? 'Custom Icon' : 'Default Icon', + ), + ), + ElevatedButton( + onPressed: () { + setState(() { + hideBadge = hideBadge == false ? true : false; + }); + }, + child: Text( + hideBadge == true ? 'Show Count Badge' : 'Hide Count Badge', + ), + ), + ], + ), + ), + ], + ), + ); + } +} + +class SirenWindowWidget extends StatelessWidget { + const SirenWindowWidget({super.key}); + + @override + Widget build(BuildContext context) { + return Scaffold( + body: Stack( + children: [ + SirenInbox( + onError: (error) { + // print('This is the inApp error message ${error.message}'); + }, + ), + ], + ), + ); + } +} diff --git a/example/lib/siren_icon.dart b/example/lib/siren_icon.dart deleted file mode 100644 index 8e35553..0000000 --- a/example/lib/siren_icon.dart +++ /dev/null @@ -1,75 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:sirenapp_flutter_inbox/sirenapp_flutter_inbox.dart'; - -class SirenIconWidget extends StatefulWidget { - const SirenIconWidget({Key? key}) : super(key: key); - - @override - State createState() => _SirenIconWidgetState(); -} - -class _SirenIconWidgetState extends State { - Icon? notificationIcon; - bool? hideBadge; - - @override - Widget build(BuildContext context) { - return Scaffold( - body: Column( - children: [ - const Spacer(), - Center( - child: SirenInboxIcon( - notificationIcon: notificationIcon, - darkMode: true, - hideBadge: hideBadge, - onError: (error) { - // print('This is the inApp error message ${error.message}'); - }, - ), - ), - Text( - 'You are viewing ${notificationIcon == null ? 'default' : 'custom'} icon', - ), - Text( - 'You are ${hideBadge == false ? 'viewing' : 'not viewing'} notification count', - ), - const Spacer(), - Padding( - padding: const EdgeInsets.all(16), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - ElevatedButton( - onPressed: () { - setState(() { - notificationIcon = notificationIcon == null - ? const Icon( - Icons.notification_add, - color: Colors.black, - ) - : null; - }); - }, - child: Text( - notificationIcon == null ? 'Custom Icon' : 'Default Icon', - ), - ), - ElevatedButton( - onPressed: () { - setState(() { - hideBadge = hideBadge == false ? true : false; - }); - }, - child: Text( - hideBadge == true ? 'Show Count Badge' : 'Hide Count Badge', - ), - ), - ], - ), - ), - ], - ), - ); - } -} diff --git a/example/lib/siren_window.dart b/example/lib/siren_window.dart deleted file mode 100644 index 0b4c323..0000000 --- a/example/lib/siren_window.dart +++ /dev/null @@ -1,21 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:sirenapp_flutter_inbox/sirenapp_flutter_inbox.dart'; - -class SirenWindowWidget extends StatelessWidget { - const SirenWindowWidget({super.key}); - - @override - Widget build(BuildContext context) { - return Scaffold( - body: Stack( - children: [ - SirenInbox( - onError: (error) { - // print('This is the inApp error message ${error.message}'); - }, - ), - ], - ), - ); - } -} diff --git a/example/pubspec.lock b/example/pubspec.lock index 2b9090b..8aaa274 100644 --- a/example/pubspec.lock +++ b/example/pubspec.lock @@ -41,22 +41,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.18.0" - crypto: - dependency: transitive - description: - name: crypto - sha256: ff625774173754681d66daaf4a448684fb04b78f902da9cb3d308c19cc5e8bab - url: "https://pub.dev" - source: hosted - version: "3.0.3" cupertino_icons: dependency: "direct main" description: name: cupertino_icons - sha256: d57953e10f9f8327ce64a508a355f0b1ec902193f66288e8cb5070e7c47eeb2d + sha256: ba631d1c7f7bef6b729a622b7b752645a2d076dba9976925b8f25725a30e1ee6 url: "https://pub.dev" source: hosted - version: "1.0.6" + version: "1.0.8" dio: dependency: transitive description: @@ -99,30 +91,30 @@ packages: url: "https://pub.dev" source: hosted version: "4.0.2" - intl: + leak_tracker: dependency: transitive description: - name: intl - sha256: "3bc132a9dbce73a7e4a21a17d06e1878839ffbf975568bc875c60537824b0c4d" + name: leak_tracker + sha256: "3f87a60e8c63aecc975dda1ceedbc8f24de75f09e4856ea27daf8958f2f0ce05" url: "https://pub.dev" source: hosted - version: "0.18.1" - leak_tracker: + version: "10.0.5" + leak_tracker_flutter_testing: dependency: transitive description: - name: leak_tracker - sha256: "7e108028e3d258667d079986da8c0bc32da4cb57431c2af03b1dc1038621a9dc" + name: leak_tracker_flutter_testing + sha256: "932549fb305594d82d7183ecd9fa93463e9914e1b67cacc34bc40906594a1806" url: "https://pub.dev" source: hosted - version: "9.0.13" + version: "3.0.5" leak_tracker_testing: dependency: transitive description: name: leak_tracker_testing - sha256: b06739349ec2477e943055aea30172c5c7000225f79dad4702e2ec0eda79a6ff + sha256: "6ba465d5d76e67ddf503e1161d1f4a6bc42306f9d66ca1e8f079a47290fb06d3" url: "https://pub.dev" source: hosted - version: "1.0.5" + version: "3.0.1" lints: dependency: transitive description: @@ -135,26 +127,26 @@ packages: dependency: transitive description: name: matcher - sha256: "1803e76e6653768d64ed8ff2e1e67bea3ad4b923eb5c56a295c3e634bad5960e" + sha256: d2323aa2060500f906aa31a895b4030b6da3ebdcc5619d14ce1aada65cd161cb url: "https://pub.dev" source: hosted - version: "0.12.16" + version: "0.12.16+1" material_color_utilities: dependency: transitive description: name: material_color_utilities - sha256: "0e0a020085b65b6083975e499759762399b4475f766c21668c4ecca34ea74e5a" + sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec url: "https://pub.dev" source: hosted - version: "0.8.0" + version: "0.11.1" meta: dependency: transitive description: name: meta - sha256: d584fa6707a52763a52446f02cc621b077888fb63b93bbcb1143a7be5a0c0c04 + sha256: "25dfcaf170a0190f47ca6355bdd4552cb8924b430512ff0cafb8db9bd41fe33b" url: "https://pub.dev" source: hosted - version: "1.11.0" + version: "1.14.0" network_logger: dependency: "direct main" description: @@ -167,17 +159,16 @@ packages: dependency: transitive description: name: path - sha256: "8829d8a55c13fc0e37127c29fedf290c102f4e40ae94ada574091fe0ff96c917" + sha256: "087ce49c3f0dc39180befefc60fdb4acd8f8620e5682fe2476afd0b3688bb4af" url: "https://pub.dev" source: hosted - version: "1.8.3" + version: "1.9.0" sirenapp_flutter_inbox: dependency: "direct main" description: - name: sirenapp_flutter_inbox - sha256: "3abd9c5d42acbf062a3eef8cfe5691600e10e33b32533d1a2a1c9ed0a1063106" - url: "https://pub.dev" - source: hosted + path: ".." + relative: true + source: path version: "1.0.0" sky_engine: dependency: transitive @@ -228,10 +219,10 @@ packages: dependency: transitive description: name: test_api - sha256: "5c2f730018264d276c20e4f1503fd1308dfbbae39ec8ee63c5236311ac06954b" + sha256: "2419f20b0c8677b2d67c8ac4d1ac7372d862dc6c460cdbb052b40155408cd794" url: "https://pub.dev" source: hosted - version: "0.6.1" + version: "0.7.1" typed_data: dependency: transitive description: @@ -252,26 +243,10 @@ packages: dependency: transitive description: name: vm_service - sha256: b3d56ff4341b8f182b96aceb2fa20e3dcb336b9f867bc0eafc0de10f1048e957 - url: "https://pub.dev" - source: hosted - version: "13.0.0" - web: - dependency: transitive - description: - name: web - sha256: afe077240a270dcfd2aafe77602b4113645af95d0ad31128cc02bce5ac5d5152 - url: "https://pub.dev" - source: hosted - version: "0.3.0" - web_socket_channel: - dependency: transitive - description: - name: web_socket_channel - sha256: d88238e5eac9a42bb43ca4e721edba3c08c6354d4a53063afaa568516217621b + sha256: "3923c89304b715fb1eb6423f017651664a03bf5f4b29983627c4da791f74a4ec" url: "https://pub.dev" source: hosted - version: "2.4.0" + version: "14.2.1" sdks: - dart: ">=3.2.3 <4.0.0" - flutter: ">=2.0.1" + dart: ">=3.3.0 <4.0.0" + flutter: ">=3.18.0-18.0.pre.54" diff --git a/example/pubspec.yaml b/example/pubspec.yaml index d226fa1..7ec340b 100644 --- a/example/pubspec.yaml +++ b/example/pubspec.yaml @@ -19,7 +19,7 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev version: 1.0.0+1 environment: - sdk: '>=3.2.3 <4.0.0' + sdk: '>=2.18.0 <4.0.0' # Dependencies specify other packages that your package needs in order to work. # To automatically upgrade your package dependencies to the latest versions @@ -36,7 +36,8 @@ dependencies: # The following adds the Cupertino Icons font to your application. # Use with the CupertinoIcons class for iOS style icons. cupertino_icons: ^1.0.2 - sirenapp_flutter_inbox: ^1.0.0 + sirenapp_flutter_inbox: + path: ../ network_logger: ^1.0.4 diff --git a/lib/src/api/delete_notification_by_id.dart b/lib/src/api/delete_notification_by_id.dart index 1a2d5f5..2cc3a6b 100644 --- a/lib/src/api/delete_notification_by_id.dart +++ b/lib/src/api/delete_notification_by_id.dart @@ -25,11 +25,10 @@ class DeleteNotificationById { required String notificationId, }) async { final result = ApiResponse()..isLoading = true; - final apiError = ApiErrorDetails() - ..errorType = ErrorTypes.NOTIFICATION_DELETE_FAILED; + var apiError = Generics.deleteFailedError; if (SirenDataProvider.instance.tokenVerificationStatus != Status.SUCCESS) { - apiError.errorType = ErrorTypes.AUTHENTICATION_FAILED; + apiError = SirenDataProvider.instance.getVerificationErrorType(); result ..isLoading = false ..isError = true @@ -44,10 +43,6 @@ class DeleteNotificationById { ); if (apiResponse.statusCode != 0 && apiResponse.data != null) { final deletionStatus = convertJsonToDeletionStatus(apiResponse.data); - - apiError - ..errorCode = ApiResponse.fromJson(apiResponse.data).error?.errorCode - ..message = ApiResponse.fromJson(apiResponse.data).error?.message; result ..isLoading = false ..isSuccess = apiResponse.statusCode == 200 diff --git a/lib/src/api/fetch_all_notification.dart b/lib/src/api/fetch_all_notification.dart index b08cf7a..1a884f0 100644 --- a/lib/src/api/fetch_all_notification.dart +++ b/lib/src/api/fetch_all_notification.dart @@ -14,12 +14,12 @@ class FetchAllNotifications { static final String _apiPath = '${Generics.V2}${Generics.BASE_URL}${SirenDataProvider.instance.recipientId}/notifications'; - List convertJsonToNotificationList( + List convertJsonToNotificationList( List dataList, ) { return dataList.map((json) { if (json is Map) { - return NotificationDataType.fromJson(json); + return NotificationType.fromJson(json); } throw const FormatException('Invalid JSON format'); }).toList(); @@ -33,8 +33,7 @@ class FetchAllNotifications { String? end, }) async { final result = ApiResponse()..isLoading = true; - final apiError = ApiErrorDetails() - ..errorType = ErrorTypes.NOTIFICATION_FETCH_FAILED; + var apiError = Generics.notificationFetchFailedError; // Manually construct query parameters final queryParams = { @@ -54,7 +53,7 @@ class FetchAllNotifications { queryParams.entries.map((e) => '${e.key}=${e.value}').join('&'); if (SirenDataProvider.instance.tokenVerificationStatus != Status.SUCCESS) { - apiError.errorType = ErrorTypes.AUTHENTICATION_FAILED; + apiError = SirenDataProvider.instance.getVerificationErrorType(); result ..isLoading = false ..isError = true @@ -72,9 +71,6 @@ class FetchAllNotifications { final dataList = ApiResponse.fromJson(apiResponse.data).data as List?; final metaData = ApiResponse.fromJson(apiResponse.data).meta; - apiError - ..errorCode = ApiResponse.fromJson(apiResponse.data).error?.errorCode - ..message = ApiResponse.fromJson(apiResponse.data).error?.message; result ..isLoading = false ..isSuccess = apiResponse.statusCode == 200 diff --git a/lib/src/api/fetch_unviewed_notification_count.dart b/lib/src/api/fetch_unviewed_notification_count.dart index 48b137c..5faa1b7 100644 --- a/lib/src/api/fetch_unviewed_notification_count.dart +++ b/lib/src/api/fetch_unviewed_notification_count.dart @@ -19,11 +19,10 @@ class FetchUnViewedNotificationsCount { Future fetchUnViewedNotificationsCount() async { final result = ApiResponse()..isLoading = true; - final apiError = ApiErrorDetails() - ..errorType = ErrorTypes.FETCH_COUNT_FAILED; + var apiError = Generics.fetchUnViewedCountFailedError; if (SirenDataProvider.instance.tokenVerificationStatus != Status.SUCCESS) { - apiError.errorType = ErrorTypes.AUTHENTICATION_FAILED; + apiError = SirenDataProvider.instance.getVerificationErrorType(); result ..isLoading = false ..isError = true @@ -43,9 +42,6 @@ class FetchUnViewedNotificationsCount { ApiResponse.fromJson(apiResponse.data).data as Map?; final notificationCount = UnViewedNotificationsCountModel.fromJson(data ?? {}); - apiError - ..errorCode = ApiResponse.fromJson(apiResponse.data).error?.errorCode - ..message = ApiResponse.fromJson(apiResponse.data).error?.message; count = notificationCount.totalUnViewed; result ..isLoading = false diff --git a/lib/src/api/mark_all_notifications_as_viewed.dart b/lib/src/api/mark_all_notifications_as_viewed.dart index 8b8096e..e13c218 100644 --- a/lib/src/api/mark_all_notifications_as_viewed.dart +++ b/lib/src/api/mark_all_notifications_as_viewed.dart @@ -18,15 +18,14 @@ class MarkAllNotificationsAsViewed { }) async { final api = ApiClient(apiProvider()); final result = ApiResponse()..isLoading; - final apiError = ApiErrorDetails() - ..errorType = ErrorTypes.UPDATE_VIEWED_FAILED; + var apiError = Generics.markAllAsViewedError; final data = { 'lastOpenedAt': untilDate, }; if (SirenDataProvider.instance.tokenVerificationStatus != Status.SUCCESS) { - apiError.errorType = ErrorTypes.AUTHENTICATION_FAILED; + apiError = SirenDataProvider.instance.getVerificationErrorType(); result ..isLoading = false ..isError = true @@ -42,9 +41,6 @@ class MarkAllNotificationsAsViewed { data: data, ); if (apiResponse.statusCode != 0 && apiResponse.data != null) { - apiError - ..errorCode = ApiResponse.fromJson(apiResponse.data).error?.errorCode - ..message = ApiResponse.fromJson(apiResponse.data).error?.message; result ..isLoading = false ..isSuccess = apiResponse.statusCode == 200 diff --git a/lib/src/api/notifications_bulk_update.dart b/lib/src/api/notifications_bulk_update.dart index b7f4135..a9ac9a6 100644 --- a/lib/src/api/notifications_bulk_update.dart +++ b/lib/src/api/notifications_bulk_update.dart @@ -15,16 +15,20 @@ class NotificationsBulkUpdate { Future notificationsBulkUpdate({ required Map data, + required String operation, }) async { final api = ApiClient(apiProvider()); final apiPath = '${Generics.V2}${Generics.BASE_URL}${SirenDataProvider.instance.recipientId}/notifications/bulk-update'; final result = ApiResponse()..isLoading; - final apiError = ApiErrorDetails() - ..errorType = ErrorTypes.NOTIFICATION_DELETE_FAILED; + var apiError = Generics.markAsReadFailedError; + + if (operation == BulkUpdateType.MARK_AS_DELETED.name) { + apiError = Generics.deleteAllFailedError; + } if (SirenDataProvider.instance.tokenVerificationStatus != Status.SUCCESS) { - apiError.errorType = ErrorTypes.AUTHENTICATION_FAILED; + apiError = SirenDataProvider.instance.getVerificationErrorType(); result ..isLoading = false ..isError = true @@ -39,10 +43,6 @@ class NotificationsBulkUpdate { data: data, ); if (apiResponse.statusCode != 0 && apiResponse.data != null) { - apiError - ..errorCode = ApiResponse.fromJson(apiResponse.data).error?.errorCode - ..message = ApiResponse.fromJson(apiResponse.data).error?.message; - result ..isLoading = false ..isSuccess = apiResponse.statusCode == 200 diff --git a/lib/src/api/read_notification_by_id.dart b/lib/src/api/read_notification_by_id.dart index 659f153..37eedc6 100644 --- a/lib/src/api/read_notification_by_id.dart +++ b/lib/src/api/read_notification_by_id.dart @@ -17,11 +17,10 @@ class ReadNotificationById { required String notificationId, }) async { final result = ApiResponse()..isLoading = true; - final apiError = ApiErrorDetails() - ..errorType = ErrorTypes.NOTIFICATION_READ_FAILED; + var apiError = Generics.markAsReadFailedError; if (SirenDataProvider.instance.tokenVerificationStatus != Status.SUCCESS) { - apiError.errorType = ErrorTypes.AUTHENTICATION_FAILED; + apiError = SirenDataProvider.instance.getVerificationErrorType(); result ..isLoading = false ..isError = true @@ -39,9 +38,6 @@ class ReadNotificationById { }, ); if (apiResponse.statusCode != 0 && apiResponse.data != null) { - apiError - ..errorCode = ApiResponse.fromJson(apiResponse.data).error?.errorCode - ..message = ApiResponse.fromJson(apiResponse.data).error?.message; result ..isLoading = false ..isSuccess = apiResponse.statusCode == 200 diff --git a/lib/src/api/verify_token.dart b/lib/src/api/verify_token.dart index 2cfef3c..4941cc0 100644 --- a/lib/src/api/verify_token.dart +++ b/lib/src/api/verify_token.dart @@ -24,8 +24,19 @@ class VerifyToken { Future verifyToken() async { final result = ApiResponse()..isLoading = true; - final apiError = ApiErrorDetails() - ..errorType = ErrorTypes.AUTHENTICATION_FAILED; + var apiError = Generics.authenticationFailed; + + if (SirenDataProvider.instance.userToken.isEmpty || + SirenDataProvider.instance.recipientId.isEmpty) { + apiError = Generics.invalidCredentialsError; + result + ..isLoading = false + ..isError = true + ..data = null + ..rawResponse = Generics.rawResponseError + ..error = apiError; + return result; + } final apiResponse = await api.get( path: @@ -34,9 +45,6 @@ class VerifyToken { if (apiResponse.statusCode != 0 && apiResponse.data != null) { final verificationStatus = convertJsonToVerificationStatus(apiResponse.data); - apiError - ..errorCode = ApiResponse.fromJson(apiResponse.data).error?.errorCode - ..message = ApiResponse.fromJson(apiResponse.data).error?.message; result ..isLoading = false diff --git a/lib/src/constants/generics.dart b/lib/src/constants/generics.dart index bf69202..13035ad 100644 --- a/lib/src/constants/generics.dart +++ b/lib/src/constants/generics.dart @@ -1,4 +1,5 @@ import 'package:sirenapp_flutter_inbox/sirenapp_flutter_inbox.dart'; +import 'package:sirenapp_flutter_inbox/src/constants/strings.dart'; class Generics { Generics._(); @@ -11,11 +12,76 @@ class Generics { static const int MAX_RETRIES = 2; static const String ENV_PATH = 'packages/sirenapp_flutter_inbox/env'; - static final defaultError = ApiErrorDetails( - errorType: ErrorTypes.GENERIC_API_ERROR, - errorCode: 'INTERNAL SERVER ERROR', - message: - 'Oops something went wrong, if issue persist please contact Siren Team', + static final defaultError = SirenErrorType( + code: ErrorCodes.API_ERROR.name, + type: Strings.error_type_error, + message: Strings.something_went_wrong, + ); + + static final authenticationFailed = SirenErrorType( + code: ErrorCodes.AUTHENTICATION_FAILED.name, + type: Strings.error_type_error, + message: Strings.authenticationFailed, + ); + + static final fetchUnViewedCountFailedError = SirenErrorType( + code: ErrorCodes.UNVIEWED_COUNT_FETCH_FAILED.name, + type: Strings.error_type_error, + message: Strings.fetchUnViewedCountFailedError, + ); + + static final notificationFetchFailedError = SirenErrorType( + code: ErrorCodes.NOTIFICATION_FETCH_FAILED.name, + type: Strings.error_type_error, + message: Strings.notificationFetchFailedError, + ); + + static final markAsReadFailedError = SirenErrorType( + code: ErrorCodes.MARK_AS_READ_FAILED.name, + type: Strings.error_type_error, + message: Strings.markAsReadFailedError, + ); + + static final deleteFailedError = SirenErrorType( + code: ErrorCodes.DELETE_FAILED.name, + type: Strings.error_type_error, + message: Strings.deleteFailedError, + ); + + static final deleteAllFailedError = SirenErrorType( + code: ErrorCodes.BULK_DELETE_FAILED.name, + type: Strings.error_type_error, + message: Strings.deleteAllFailedError, + ); + + static final markAllAsViewedError = SirenErrorType( + code: ErrorCodes.MARK_ALL_AS_VIEWED_FAILED.name, + type: Strings.error_type_error, + message: Strings.markAllAsViewedError, + ); + + static final outsideSirenContextError = SirenErrorType( + code: ErrorCodes.OUTSIDE_SIREN_CONTEXT.name, + type: Strings.error_type_error, + message: Strings.outsideSirenContextError, + ); + + static final authenticationPending = SirenErrorType( + code: ErrorCodes.AUTHENTICATION_PENDING.name, + type: Strings.error_type_error, + message: Strings.authenticationPending, + ); + + static final unauthorizedOperationError = SirenErrorType( + code: ErrorCodes.UNAUTHORIZED_OPERATION.name, + type: Strings.error_type_error, + message: Strings.unauthorizedOperationError, + ); + + static final invalidCredentialsError = SirenErrorType( + code: ErrorCodes.INVALID_CREDENTIALS.name, + type: Strings.error_type_error, + message: Strings.invalidCredentialsError, ); static const rawResponseError = @@ -26,6 +92,8 @@ enum Status { PENDING, SUCCESS, FAILED, + IN_PROGRESS, + INVALID_CREDENTIALS, } enum BulkUpdateType { @@ -34,22 +102,29 @@ enum BulkUpdateType { } enum UpdateEvents { - READ_BY_ID, - READ_ALL, - DELETE_BY_ID, DELETE_ALL, - VIEW_ALL, + DELETE_BY_ID, PARAMS_CHANGED, - TOKEN_VERIFIED, + READ_ALL, + READ_BY_ID, SHOW_ERROR, + TOKEN_VERIFIED, + VIEW_ALL, } -enum ErrorTypes { - GENERIC_API_ERROR, +enum ErrorCodes { + API_ERROR, AUTHENTICATION_FAILED, - FETCH_COUNT_FAILED, + AUTHENTICATION_PENDING, + BULK_DELETE_FAILED, + DELETE_FAILED, + INVALID_CREDENTIALS, + MARK_ALL_AS_READ_FAILED, + MARK_ALL_AS_VIEWED_FAILED, + MARK_AS_READ_FAILED, NOTIFICATION_FETCH_FAILED, NOTIFICATION_READ_FAILED, - NOTIFICATION_DELETE_FAILED, - UPDATE_VIEWED_FAILED, + OUTSIDE_SIREN_CONTEXT, + UNAUTHORIZED_OPERATION, + UNVIEWED_COUNT_FETCH_FAILED, } diff --git a/lib/src/constants/strings.dart b/lib/src/constants/strings.dart index 8f85ad2..f334f53 100644 --- a/lib/src/constants/strings.dart +++ b/lib/src/constants/strings.dart @@ -4,8 +4,29 @@ class Strings { static const empty_title = 'No new notifications'; static const empty_desc = 'Check back later for updates and alerts.'; static const error_title = 'Oops! Something went wrong.'; + static const something_went_wrong = 'Something went wrong'; static const error_desc = 'Could not load the notifications. Please refresh the page.'; static const clear_all = 'Clear All'; static const notifications = 'Notifications'; + static const string_null = 'null'; + static const authentication_failed_message = + 'Failed to authenticate given credentials'; + static const error_type_error = 'ERROR'; + static const authenticationFailed = + 'Failed to authenticate given credentials'; + static const fetchUnViewedCountFailedError = + 'Failed to fetch unviewed notifications count'; + static const notificationFetchFailedError = 'Failed to fetch notifications'; + static const markAsReadFailedError = 'Failed to mark notification as read'; + static const deleteFailedError = 'Failed to delete notification'; + static const deleteAllFailedError = 'Bulk deletion of notifications failed'; + static const markAllAsViewedError = 'Failed to mark notifications as viewed'; + static const outsideSirenContextError = + 'Trying to invoke function outside the siren context'; + static const authenticationPending = 'Authentication in progress'; + static const unauthorizedOperationError = + 'This operation require valid credentials'; + static const invalidCredentialsError = + 'Invalid credentials found. Please check your token and recipient ID'; } diff --git a/lib/src/data/siren_data_provider.dart b/lib/src/data/siren_data_provider.dart index aa614df..4675769 100644 --- a/lib/src/data/siren_data_provider.dart +++ b/lib/src/data/siren_data_provider.dart @@ -32,6 +32,7 @@ class SirenDataProvider { int _retryCount = 0; Status _tokenVerificationStatus = Status.PENDING; ApiResponse _tokenVerificationResponse = ApiResponse()..isLoading; + bool _isProviderInitialized = false; late StreamController _inboxController; late StreamController _iconController; @@ -45,8 +46,12 @@ class SirenDataProvider { /// Getter for the token verification status. Status get tokenVerificationStatus => _tokenVerificationStatus; + /// Getter to check if provider initialized + bool get isProviderInitialized => _isProviderInitialized; + /// Initializes the Siren Data Provider. Future initialize() async { + _isProviderInitialized = true; apiDomain = await getApiDomain(); } @@ -64,6 +69,7 @@ class SirenDataProvider { /// Verifies the user token. Future _verifyToken() async { + _tokenVerificationStatus = Status.IN_PROGRESS; _tokenVerificationResponse = await VerifyToken.instance.verifyToken(); if (_tokenVerificationResponse.isSuccess) { _retryCount = 0; @@ -85,31 +91,55 @@ class SirenDataProvider { } else { if (_retryCount < Generics.MAX_RETRIES && _tokenVerificationStatus != Status.SUCCESS) { - _tokenVerificationStatus = Status.FAILED; _retryCount++; + if (SirenDataProvider.instance.userToken.isEmpty || + SirenDataProvider.instance.recipientId.isEmpty) { + _retryCount = Generics.MAX_RETRIES; + _tokenVerificationStatus = Status.INVALID_CREDENTIALS; + triggerError(); + return; + } Future.delayed( const Duration(seconds: Generics.DATA_FETCH_INTERVAL), _verifyToken, ); } else if (_retryCount >= Generics.MAX_RETRIES) { - SirenDataProvider.instance.inboxController.sink.add( - StreamResponse( - _tokenVerificationResponse, - UpdateEvents.SHOW_ERROR, - '', - ), - ); - SirenDataProvider.instance.iconController.sink.add( - StreamResponse( - _tokenVerificationResponse, - UpdateEvents.SHOW_ERROR, - '', - ), - ); + _tokenVerificationStatus = Status.FAILED; + triggerError(); } } } + void triggerError() { + SirenDataProvider.instance.inboxController.sink.add( + StreamResponse( + _tokenVerificationResponse, + UpdateEvents.SHOW_ERROR, + '', + ), + ); + SirenDataProvider.instance.iconController.sink.add( + StreamResponse( + _tokenVerificationResponse, + UpdateEvents.SHOW_ERROR, + '', + ), + ); + } + + SirenErrorType getVerificationErrorType() { + if (_tokenVerificationStatus == Status.PENDING) { + return Generics.outsideSirenContextError; + } else if (_tokenVerificationStatus == Status.IN_PROGRESS) { + return Generics.authenticationPending; + } else if (_tokenVerificationStatus == Status.FAILED) { + return Generics.unauthorizedOperationError; + } else if (_tokenVerificationStatus == Status.INVALID_CREDENTIALS) { + return Generics.invalidCredentialsError; + } + return Generics.authenticationFailed; + } + /// Disposes the icon controller. void iconDispose() { _iconController.close(); diff --git a/lib/src/models/api_response.dart b/lib/src/models/api_response.dart index f2ec793..839c830 100644 --- a/lib/src/models/api_response.dart +++ b/lib/src/models/api_response.dart @@ -14,7 +14,7 @@ class ApiResponse { return ApiResponse( data: json['data'], error: json['error'] != null - ? ApiErrorDetails.fromJson(json['error'] as Map?) + ? SirenErrorType.fromJson(json['error'] as Map?) : null, meta: json['meta'] != null ? MetaResponse.fromJson(json?['meta'] as Map?) @@ -29,7 +29,7 @@ class ApiResponse { late MetaResponse? meta; /// Details about any errors that occurred during the request. - late ApiErrorDetails? error; + late SirenErrorType? error; /// Indicates whether the response is still loading. bool isLoading = true; @@ -96,31 +96,30 @@ class MetaResponse { } /// Represents details of an API error. -class ApiErrorDetails { - /// Constructs an [ApiErrorDetails] instance. - ApiErrorDetails({ - this.errorCode, +class SirenErrorType { + /// Constructs an [SirenErrorType] instance. + SirenErrorType({ + this.type, this.message, - this.errorType, + this.code, }); - /// Factory method to create ApiErrorDetails from JSON. - factory ApiErrorDetails.fromJson(Map? json) { - return ApiErrorDetails( - errorCode: - json?['errorCode'] != null ? (json?['errorCode'] as String) : '', + /// Factory method to create SirenErrorType from JSON. + factory SirenErrorType.fromJson(Map? json) { + return SirenErrorType( + type: json?['errorCode'] != null ? (json?['errorCode'] as String) : '', message: json?['message'] != null ? (json?['message'] as String) : '', ); } /// The error code associated with the error. - String? errorCode; + String? type; /// The message describing the error. String? message; /// The type of error. - ErrorTypes? errorType; + String? code; } /// Represents a response from Dio HTTP client. diff --git a/lib/src/models/notification_model.dart b/lib/src/models/notification_model.dart index 2ab03b2..9a34121 100644 --- a/lib/src/models/notification_model.dart +++ b/lib/src/models/notification_model.dart @@ -1,9 +1,9 @@ import 'package:flutter/material.dart'; /// Class representing the data structure of a notification. -class NotificationDataType { - /// Constructs a [NotificationDataType] instance. - NotificationDataType({ +class NotificationType { + /// Constructs a [NotificationType] instance. + NotificationType({ required this.id, required this.createdAt, required this.message, @@ -12,9 +12,9 @@ class NotificationDataType { required this.cardColor, }); - /// Factory method to create NotificationDataType from JSON. - factory NotificationDataType.fromJson(Map? json) { - return NotificationDataType( + /// Factory method to create NotificationType from JSON. + factory NotificationType.fromJson(Map? json) { + return NotificationType( id: json?['id'] as String, createdAt: json?['createdAt'] as String, message: MessageData.fromJson(json?['message'] as Map), diff --git a/lib/src/models/ui_models.dart b/lib/src/models/ui_models.dart index 85abd88..0646e09 100644 --- a/lib/src/models/ui_models.dart +++ b/lib/src/models/ui_models.dart @@ -2,9 +2,9 @@ import 'package:flutter/material.dart'; import 'package:sirenapp_flutter_inbox/src/models/notification_model.dart'; /// Properties for configuring the appearance of the notification card. -class CardProps { - /// Constructs a [CardProps] with optional parameters. - const CardProps({ +class CardParams { + /// Constructs a [CardParams] with optional parameters. + const CardParams({ this.hideAvatar, this.disableAutoMarkAsRead, this.deleteIcon, @@ -25,7 +25,7 @@ class CardProps { final bool? hideDelete; /// Callback function when a notification card is clicked. - final void Function(NotificationDataType)? onAvatarClick; + final void Function(NotificationType)? onAvatarClick; } /// Customizable style for the Siren notification icon. @@ -243,8 +243,8 @@ class BadgeColors { } /// Properties for configuring the appearance of the notification window app bar. -class InboxHeaderProps { - InboxHeaderProps({ +class HeaderParams { + HeaderParams({ this.title, this.hideHeader, this.showBackButton, diff --git a/lib/src/theme/app_theme.dart b/lib/src/theme/app_theme.dart index d62644b..3d931ee 100644 --- a/lib/src/theme/app_theme.dart +++ b/lib/src/theme/app_theme.dart @@ -9,9 +9,10 @@ class AppTheme { static ThemeData lightTheme = ThemeData.light().copyWith( colorScheme: ThemeData.light().colorScheme.copyWith( - background: AppColors.emptyWidgetBgLightTheme, + primaryContainer: AppColors.emptyWidgetBgLightTheme, inversePrimary: AppColors.grey500, - onBackground: AppColors.grey300Complementary, + onPrimaryContainer: AppColors.grey300Complementary, + onInverseSurface: Colors.white, onPrimary: AppColors.black100, onSecondary: AppColors.avatarPlaceholderBgLight, onTertiary: AppColors.primary200, @@ -24,18 +25,18 @@ class AppTheme { shadow: AppColors.emptyWidgetBellLight, surface: AppColors.emptyWidgetBadgeLight, surfaceTint: AppColors.grey300, - surfaceVariant: AppColors.avatarIconLight, + onTertiaryContainer: AppColors.avatarIconLight, tertiary: AppColors.grey700, tertiaryContainer: AppColors.red, - onInverseSurface: Colors.white, ), ); static ThemeData darkTheme = ThemeData.dark().copyWith( colorScheme: ThemeData.dark().colorScheme.copyWith( - background: AppColors.emptyWidgetBgDarkTheme, + primaryContainer: AppColors.emptyWidgetBgDarkTheme, inversePrimary: AppColors.grey400, - onBackground: AppColors.grey50, + onPrimaryContainer: AppColors.grey50, + onInverseSurface: Colors.white, onPrimary: Colors.white, onSecondary: AppColors.avatarPlaceholderBgDark, onTertiary: AppColors.primary200Complementary, @@ -48,10 +49,9 @@ class AppTheme { shadow: AppColors.emptyWidgetBellDark, surface: AppColors.emptyWidgetBadgeDark, surfaceTint: AppColors.grey300Complementary, - surfaceVariant: AppColors.avatarIconDark, + onTertiaryContainer: AppColors.avatarIconDark, tertiary: AppColors.grey700Complementary, tertiaryContainer: AppColors.red, - onInverseSurface: Colors.white, ), ); @@ -79,12 +79,13 @@ class AppTheme { customColors.borderColor ?? baseTheme.colorScheme.surfaceTint, tertiary: customColors.textColor ?? baseTheme.colorScheme.tertiary, scrim: customColors.timerIcon ?? baseTheme.colorScheme.scrim, - background: baseTheme.colorScheme.background, + primaryContainer: baseTheme.colorScheme.primaryContainer, onSecondary: baseTheme.colorScheme.onSecondary, shadow: baseTheme.colorScheme.shadow, surface: baseTheme.colorScheme.surface, - surfaceVariant: baseTheme.colorScheme.surfaceVariant, + onTertiaryContainer: baseTheme.colorScheme.onTertiaryContainer, onInverseSurface: baseTheme.colorScheme.onInverseSurface, + onPrimaryContainer: baseTheme.colorScheme.onPrimaryContainer, ), ) .copyWith( diff --git a/lib/src/utils/siren.dart b/lib/src/utils/siren.dart index a79b74c..e7f2ca9 100644 --- a/lib/src/utils/siren.dart +++ b/lib/src/utils/siren.dart @@ -10,73 +10,79 @@ class Siren { /// Marks a notification as read by its ID. /// [id] is the notification id to be mark as read. /// Returns the response from the API call. - static Future markAsRead({ + static Future markAsReadById({ required String id, }) async { final response = await ReadNotificationById.instance .readNotificationById(notificationId: id); SirenDataProvider.instance.inboxController.sink .add(StreamResponse(response, UpdateEvents.READ_BY_ID, id)); - return response.rawResponse; + return response.isError ? response.error : response.rawResponse; } /// Marks notifications as read by date until a specific date. /// [startDate] is the date until a specific date in the format "yyyy-MM-dd'T'HH:mm:ss'Z'". /// Returns the response from the API call. - static Future markNotificationsAsReadByDate({ + static Future markAsReadByDate({ required String startDate, }) async { final data = { 'until': startDate, 'operation': BulkUpdateType.MARK_AS_READ.name, }; - final response = await NotificationsBulkUpdate.instance - .notificationsBulkUpdate(data: data); + final response = + await NotificationsBulkUpdate.instance.notificationsBulkUpdate( + data: data, + operation: BulkUpdateType.MARK_AS_READ.name, + ); SirenDataProvider.instance.inboxController.sink .add(StreamResponse(response, UpdateEvents.READ_ALL, '')); - return response.rawResponse; + return response.isError ? response.error : response.rawResponse; } /// Marks notifications as viewed until a specific date. /// [startDate] is the date until a specific date in the format "yyyy-MM-dd'T'HH:mm:ss'Z'". /// Returns the response from the API call. - static Future markNotificationsAsViewed({ + static Future markAllAsViewed({ required String startDate, }) async { final response = await MarkAllNotificationsAsViewed.instance .markAllNotificationsAsViewed(untilDate: startDate); SirenDataProvider.instance.inboxController.sink .add(StreamResponse(response, UpdateEvents.VIEW_ALL, '')); - return response.rawResponse; + return response.isError ? response.error : response.rawResponse; } /// Deletes a notification by its ID. /// [id] is the notification id to be deleted. /// Returns the response from the API call. - static Future deleteNotification({ + static Future deleteById({ required String id, }) async { final response = await DeleteNotificationById.instance .deleteNotificationById(notificationId: id); SirenDataProvider.instance.inboxController.sink .add(StreamResponse(response, UpdateEvents.DELETE_BY_ID, id)); - return response.rawResponse; + return response.isError ? response.error : response.rawResponse; } /// Deletes notifications by date until a specific date. /// [startDate] is the date until a specific date in the format "yyyy-MM-dd'T'HH:mm:ss'Z'". /// Returns the response from the API call. - static Future deleteNotificationByDate({ + static Future deleteByDate({ required String startDate, }) async { final data = { 'until': startDate, 'operation': BulkUpdateType.MARK_AS_DELETED.name, }; - final response = await NotificationsBulkUpdate.instance - .notificationsBulkUpdate(data: data); + final response = + await NotificationsBulkUpdate.instance.notificationsBulkUpdate( + data: data, + operation: BulkUpdateType.MARK_AS_DELETED.name, + ); SirenDataProvider.instance.inboxController.sink .add(StreamResponse(response, UpdateEvents.DELETE_ALL, '')); - return response.rawResponse; + return response.isError ? response.error : response.rawResponse; } } diff --git a/lib/src/widgets/app_bar.dart b/lib/src/widgets/app_bar.dart index 2ae881d..245833a 100644 --- a/lib/src/widgets/app_bar.dart +++ b/lib/src/widgets/app_bar.dart @@ -8,25 +8,25 @@ class SirenAppBar extends StatelessWidget implements PreferredSizeWidget { required this.isNonEmptyNotifications, super.key, this.onClearAllPressed, - this.inboxHeaderProps, + this.headerParams, this.styles, }); final ThemeData theme; final VoidCallback? onClearAllPressed; final bool isNonEmptyNotifications; - final InboxHeaderProps? inboxHeaderProps; + final HeaderParams? headerParams; final CustomStyles? styles; @override Size get preferredSize { - return inboxHeaderProps?.hideHeader ?? false + return headerParams?.hideHeader ?? false ? Size.zero : const Size.fromHeight(kToolbarHeight); } @override Widget build(BuildContext context) { - if (inboxHeaderProps?.hideHeader ?? false) { + if (headerParams?.hideHeader ?? false) { return const SizedBox.shrink(); } return Container( @@ -43,24 +43,24 @@ class SirenAppBar extends StatelessWidget implements PreferredSizeWidget { height: preferredSize.height, child: Padding( padding: const EdgeInsets.only(right: 16, left: 20), - child: inboxHeaderProps?.customHeader ?? + child: headerParams?.customHeader ?? Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Row( children: [ - if (inboxHeaderProps?.showBackButton ?? false) + if (headerParams?.showBackButton ?? false) Semantics( label: 'siren-header-back', hint: 'Tap to view navigate back', child: GestureDetector( key: const Key('siren-header-back'), - onTap: inboxHeaderProps?.onBackPress, - child: inboxHeaderProps?.backButton ?? + onTap: headerParams?.onBackPress, + child: headerParams?.backButton ?? Icon( Icons.arrow_back_ios, color: theme.bannerTheme.dividerColor ?? - theme.colorScheme.onBackground, + theme.colorScheme.onPrimaryContainer, size: 20, ), ), @@ -69,19 +69,19 @@ class SirenAppBar extends StatelessWidget implements PreferredSizeWidget { padding: styles?.appBarStyle?.titlePadding ?? EdgeInsets.zero, child: Text( - inboxHeaderProps?.title ?? Strings.notifications, + headerParams?.title ?? Strings.notifications, style: styles?.appBarStyle?.headerTextStyle ?? TextStyle( fontSize: 18, fontWeight: FontWeight.w600, color: theme.bannerTheme.dividerColor ?? - theme.colorScheme.onBackground, + theme.colorScheme.onPrimaryContainer, ), ), ), ], ), - if (!(inboxHeaderProps?.hideClearAll ?? false)) + if (!(headerParams?.hideClearAll ?? false)) Semantics( label: 'siren-header-clear-all', hint: 'Tap to clear all notifications', diff --git a/lib/src/widgets/card.dart b/lib/src/widgets/card.dart index 64c37d7..b49aee5 100644 --- a/lib/src/widgets/card.dart +++ b/lib/src/widgets/card.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:sirenapp_flutter_inbox/src/constants/strings.dart'; import 'package:sirenapp_flutter_inbox/src/models/notification_model.dart'; import 'package:sirenapp_flutter_inbox/src/models/ui_models.dart'; import 'package:sirenapp_flutter_inbox/src/utils/common_utils.dart'; @@ -9,7 +10,7 @@ class CardWidget extends StatefulWidget { const CardWidget({ required this.onTap, required this.notification, - required this.cardProps, + required this.cardParams, required this.styles, required this.onDelete, super.key, @@ -19,10 +20,10 @@ class CardWidget extends StatefulWidget { final Function onTap; /// Notification data to be displayed. - final NotificationDataType notification; + final NotificationType notification; /// Properties for customizing the card. - final CardProps cardProps; + final CardParams cardParams; /// Styles to be applied to various elements of the card. final CustomStyles? styles; @@ -57,7 +58,7 @@ class _CardWidgetState extends State { child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ - if (!(widget.cardProps.hideAvatar ?? false)) + if (!(widget.cardParams.hideAvatar ?? false)) _buildDefaultAvatarContainer(currentTheme), Expanded( child: Padding( @@ -113,7 +114,7 @@ class _CardWidgetState extends State { return GestureDetector( key: Key('siren-notification-avatar-${widget.notification.id}'), onTap: () { - widget.cardProps.onAvatarClick?.call(widget.notification); + widget.cardParams.onAvatarClick?.call(widget.notification); }, child: Padding( padding: const EdgeInsets.only( @@ -122,14 +123,18 @@ class _CardWidgetState extends State { ), child: CircleAvatar( radius: widget.styles?.cardStyle?.avatarSize ?? 21, - backgroundImage: avatarUrl != null && avatarUrl.isNotEmpty + backgroundImage: avatarUrl != null && + avatarUrl.isNotEmpty && + avatarUrl != Strings.string_null ? NetworkImage(avatarUrl) : null, backgroundColor: theme.colorScheme.onSecondary, - child: avatarUrl == null || avatarUrl.isEmpty + child: avatarUrl == null || + avatarUrl.isEmpty || + avatarUrl == Strings.string_null ? Icon( Icons.landscape_rounded, - color: theme.colorScheme.surfaceVariant, + color: theme.colorScheme.onTertiaryContainer, ) : null, ), @@ -156,14 +161,14 @@ class _CardWidgetState extends State { ), ), ), - if (!(widget.cardProps.hideDelete ?? false)) ...[ + if (!(widget.cardParams.hideDelete ?? false)) ...[ const SizedBox(width: 8), GestureDetector( key: Key( 'siren-notification-delete-${widget.notification.id}', ), onTap: () => widget.onDelete(widget.notification.id), - child: widget.cardProps.deleteIcon ?? + child: widget.cardParams.deleteIcon ?? _buildDefaultDeleteButton( theme, widget.styles?.deleteIconSize ?? 18, diff --git a/lib/src/widgets/empty_widget.dart b/lib/src/widgets/empty_widget.dart index af294b9..9f2bcd7 100644 --- a/lib/src/widgets/empty_widget.dart +++ b/lib/src/widgets/empty_widget.dart @@ -56,7 +56,7 @@ Widget _buildCircle(ThemeData theme) { height: 160, decoration: BoxDecoration( shape: BoxShape.circle, - color: theme.colorScheme.background, + color: theme.colorScheme.primaryContainer, ), ), Icon( @@ -73,7 +73,7 @@ Widget _buildCircle(ThemeData theme) { color: theme.colorScheme.surface, shape: BoxShape.circle, border: Border.all( - color: theme.colorScheme.background, + color: theme.colorScheme.primaryContainer, width: 3, ), ), diff --git a/lib/src/widgets/error_widget.dart b/lib/src/widgets/error_widget.dart index 4099023..30252db 100644 --- a/lib/src/widgets/error_widget.dart +++ b/lib/src/widgets/error_widget.dart @@ -55,7 +55,7 @@ Widget _buildCircle(ThemeData theme) { height: 160, decoration: BoxDecoration( shape: BoxShape.circle, - color: theme.colorScheme.background, + color: theme.colorScheme.primaryContainer, ), child: Icon( Icons.warning_rounded, diff --git a/lib/src/widgets/inbox_body.dart b/lib/src/widgets/inbox_body.dart index 8200541..fd41397 100644 --- a/lib/src/widgets/inbox_body.dart +++ b/lib/src/widgets/inbox_body.dart @@ -14,8 +14,8 @@ class InboxBody extends StatelessWidget { required this.notifications, required this.deleteNotification, required this.markAsRead, - required this.customNotificationCard, - required this.onNotificationCardClick, + required this.customCard, + required this.onCardClick, required this.deletingNotificationId, required this.disableAutoMarkAsRead, required this.totalElements, @@ -26,7 +26,7 @@ class InboxBody extends StatelessWidget { this.customErrorWidget, this.customLoader, this.customStyles, - this.cardProps, + this.cardParams, this.listEmptyWidget, super.key, }); @@ -34,11 +34,11 @@ class InboxBody extends StatelessWidget { final bool isLoading; final bool loadingNextPage; final bool isError; - final List notifications; + final List notifications; final Future Function(String) deleteNotification; final void Function(String) markAsRead; - final Widget Function(NotificationDataType)? customNotificationCard; - final void Function(NotificationDataType)? onNotificationCardClick; + final Widget Function(NotificationType)? customCard; + final void Function(NotificationType)? onCardClick; final String? deletingNotificationId; final bool disableAutoMarkAsRead; final int totalElements; @@ -47,7 +47,7 @@ class InboxBody extends StatelessWidget { final Widget? customLoader; final bool endReached; final CustomStyles? customStyles; - final CardProps? cardProps; + final CardParams? cardParams; final VoidCallback onEndReached; final ScrollController scrollController; final Widget? listEmptyWidget; @@ -80,7 +80,7 @@ class InboxBody extends StatelessWidget { } else if (isLoading && !loadingNextPage) { return LoaderWidget( customLoader: customLoader, - hideAvatar: cardProps?.hideAvatar ?? false, + hideAvatar: cardParams?.hideAvatar ?? false, ); } else if (notifications.isEmpty) { return Semantics( @@ -104,11 +104,11 @@ class InboxBody extends StatelessWidget { scrollController: scrollController, onDelete: deleteNotification, markAsRead: markAsRead, - customNotificationCard: customNotificationCard, - onNotificationCardClick: onNotificationCardClick, + customCard: customCard, + onCardClick: onCardClick, deletingNotificationId: deletingNotificationId, totalElements: totalElements, - cardProps: cardProps, + cardParams: cardParams, loadingIndicator: currentTheme.colorScheme.onTertiary, ), ); diff --git a/lib/src/widgets/notification_list_view.dart b/lib/src/widgets/notification_list_view.dart index 81c5216..9f073d0 100644 --- a/lib/src/widgets/notification_list_view.dart +++ b/lib/src/widgets/notification_list_view.dart @@ -14,16 +14,16 @@ class NotificationListView extends StatefulWidget { required this.scrollController, required this.onDelete, required this.markAsRead, - this.customNotificationCard, - this.onNotificationCardClick, + this.customCard, + this.onCardClick, this.deletingNotificationId, this.totalElements, - this.cardProps, + this.cardParams, this.loadingIndicator, super.key, }); - final List notifications; + final List notifications; final bool isLoading; final bool endReached; final bool loadingNextPage; @@ -33,11 +33,11 @@ class NotificationListView extends StatefulWidget { final ScrollController scrollController; final Future Function(String) onDelete; final void Function(String) markAsRead; - final Widget Function(NotificationDataType)? customNotificationCard; - final void Function(NotificationDataType)? onNotificationCardClick; + final Widget Function(NotificationType)? customCard; + final void Function(NotificationType)? onCardClick; final String? deletingNotificationId; final int? totalElements; - final CardProps? cardProps; + final CardParams? cardParams; final Color? loadingIndicator; @override @@ -85,17 +85,17 @@ class _NotificationListViewState extends State { if (index < widget.notifications.length) { final isLastIndex = index == widget.notifications.length - 1; final currentNotification = widget.notifications[index]; - final itemWidget = widget.customNotificationCard - ?.call(currentNotification) ?? + final itemWidget = widget.customCard?.call(currentNotification) ?? CardWidget( onTap: (notification) { - if (!(widget.cardProps?.disableAutoMarkAsRead ?? false)) { + if (!(widget.cardParams?.disableAutoMarkAsRead ?? + false)) { widget.markAsRead(currentNotification.id); } - widget.onNotificationCardClick?.call(currentNotification); + widget.onCardClick?.call(currentNotification); }, notification: currentNotification, - cardProps: widget.cardProps ?? const CardProps(), + cardParams: widget.cardParams ?? const CardParams(), styles: widget.customStyles, onDelete: widget.onDelete, ); diff --git a/lib/src/widgets/siren_inbox.dart b/lib/src/widgets/siren_inbox.dart index 0e6217d..8d2c594 100644 --- a/lib/src/widgets/siren_inbox.dart +++ b/lib/src/widgets/siren_inbox.dart @@ -22,12 +22,12 @@ class SirenInbox extends StatefulWidget { this.darkMode, this.itemsPerFetch, this.listEmptyWidget, - this.customNotificationCard, + this.customCard, this.customLoader, this.customErrorWidget, - this.cardProps, - this.inboxHeaderProps, - this.onNotificationCardClick, + this.cardParams, + this.headerParams, + this.onCardClick, this.onError, this.theme, this.customStyles, @@ -43,7 +43,7 @@ class SirenInbox extends StatefulWidget { final Widget? listEmptyWidget; /// Custom builder for notification cards. - final Widget Function(NotificationDataType)? customNotificationCard; + final Widget Function(NotificationType)? customCard; /// Custom loader widget. final Widget? customLoader; @@ -52,16 +52,16 @@ class SirenInbox extends StatefulWidget { final Widget? customErrorWidget; ///Custom props for Card properties - final CardProps? cardProps; + final CardParams? cardParams; /// Custom props for header properties - final InboxHeaderProps? inboxHeaderProps; + final HeaderParams? headerParams; /// Callback function when a notification card is clicked. - final void Function(NotificationDataType)? onNotificationCardClick; + final void Function(NotificationType)? onCardClick; /// Callback function for handling errors. - final void Function(ApiErrorDetails)? onError; + final void Function(SirenErrorType)? onError; /// Custom theme colors for the inbox, this focuses on the idea of colorSchemes in flutter theme. final CustomThemeColors? theme; @@ -83,7 +83,7 @@ class _SirenInboxState extends State { String? deletingNotificationId; int pageSize = 20; - List notifications = []; + List notifications = []; late final DeleteNotificationById _deleteNotificationById; late final ReadNotificationById _readNotificationById; late Timer? _periodicUpdateRef; @@ -116,7 +116,9 @@ class _SirenInboxState extends State { if (SirenDataProvider.instance.tokenVerificationStatus == Status.SUCCESS) { await initialFetchNotification(); } else if (SirenDataProvider.instance.tokenVerificationStatus == - Status.FAILED) { + Status.FAILED || + !SirenDataProvider.instance.isProviderInitialized) { + widget.onError?.call(Generics.outsideSirenContextError); if (mounted) { setState(() { isError = true; @@ -159,7 +161,7 @@ class _SirenInboxState extends State { } } else if (streamResponse.response?.isError ?? false) { widget.onError - ?.call(streamResponse.response?.error ?? ApiErrorDetails()); + ?.call(streamResponse.response?.error ?? SirenErrorType()); } }, ); @@ -231,7 +233,7 @@ class _SirenInboxState extends State { } void fetchNewNotifications() { - late var newNotifications = []; + late var newNotifications = []; _periodicUpdateRef?.cancel(); _periodicUpdateRef = Timer.periodic( const Duration(seconds: Generics.DATA_FETCH_INTERVAL), @@ -249,7 +251,7 @@ class _SirenInboxState extends State { if ((fetchedNotifications.meta?.totalElements ?? 0) > 0) { unawaited(markAllNotificationsAsViewed()); newNotifications.addAll( - fetchedNotifications.data as Iterable, + fetchedNotifications.data as Iterable, ); if (mounted) { setState( @@ -273,7 +275,7 @@ class _SirenInboxState extends State { newNotifications = []; } } else if (fetchedNotifications.isError) { - widget.onError?.call(fetchedNotifications.error ?? ApiErrorDetails()); + widget.onError?.call(fetchedNotifications.error ?? SirenErrorType()); } }, ); @@ -288,7 +290,7 @@ class _SirenInboxState extends State { if (notificationsMarkedAsViewed.isError) { widget.onError?.call( - notificationsMarkedAsViewed.error ?? ApiErrorDetails(), + notificationsMarkedAsViewed.error ?? SirenErrorType(), ); } } @@ -309,7 +311,7 @@ class _SirenInboxState extends State { unawaited(markAllNotificationsAsViewed()); setState(() { notifications.addAll( - fetchedNotifications.data as Iterable, + fetchedNotifications.data as Iterable, ); isLoading = false; isError = false; @@ -322,7 +324,7 @@ class _SirenInboxState extends State { isError = fetchedNotifications.isError; }); } - widget.onError?.call(fetchedNotifications.error ?? ApiErrorDetails()); + widget.onError?.call(fetchedNotifications.error ?? SirenErrorType()); } } @@ -344,6 +346,7 @@ class _SirenInboxState extends State { final deleteAllResponse = await NotificationsBulkUpdate.instance.notificationsBulkUpdate( data: data, + operation: BulkUpdateType.MARK_AS_DELETED.name, ); if (deleteAllResponse.isSuccess) { SirenDataProvider.instance.inboxController.sink.add( @@ -355,7 +358,7 @@ class _SirenInboxState extends State { ); _deleteAllNotifications(); } else if (deleteAllResponse.isError) { - widget.onError?.call(deleteAllResponse.error ?? ApiErrorDetails()); + widget.onError?.call(deleteAllResponse.error ?? SirenErrorType()); } } @@ -391,7 +394,7 @@ class _SirenInboxState extends State { onEndReached(); } } else if (deletionStatus.isError) { - widget.onError?.call(deletionStatus.error ?? ApiErrorDetails()); + widget.onError?.call(deletionStatus.error ?? SirenErrorType()); } } @@ -417,7 +420,7 @@ class _SirenInboxState extends State { if (mounted) { setState(() { notifications.addAll( - fetchedNotifications.data as Iterable, + fetchedNotifications.data as Iterable, ); isLoading = false; loadingNextPage = false; @@ -430,7 +433,7 @@ class _SirenInboxState extends State { loadingNextPage = false; }); } - widget.onError?.call(fetchedNotifications.error ?? ApiErrorDetails()); + widget.onError?.call(fetchedNotifications.error ?? SirenErrorType()); } }); } @@ -449,7 +452,7 @@ class _SirenInboxState extends State { ); _markNotificationAsReadById(id); } else if (readStatus.isError) { - widget.onError?.call(readStatus.error ?? ApiErrorDetails()); + widget.onError?.call(readStatus.error ?? SirenErrorType()); } } @@ -473,7 +476,7 @@ class _SirenInboxState extends State { theme: currentTheme, onClearAllPressed: onBulkDelete, isNonEmptyNotifications: shouldShowClearAllButton(), - inboxHeaderProps: widget.inboxHeaderProps, + headerParams: widget.headerParams, styles: widget.customStyles, ), body: InboxBody( @@ -484,18 +487,18 @@ class _SirenInboxState extends State { notifications: notifications, deleteNotification: deleteNotification, markAsRead: _markNotificationAsRead, - customNotificationCard: widget.customNotificationCard, - onNotificationCardClick: widget.onNotificationCardClick, + customCard: widget.customCard, + onCardClick: widget.onCardClick, deletingNotificationId: deletingNotificationId, disableAutoMarkAsRead: - widget.cardProps?.disableAutoMarkAsRead ?? false, + widget.cardParams?.disableAutoMarkAsRead ?? false, totalElements: totalElements, onRefresh: onRefresh, customErrorWidget: widget.customErrorWidget, customLoader: widget.customLoader, endReached: endReached, customStyles: widget.customStyles, - cardProps: widget.cardProps, + cardParams: widget.cardParams, scrollController: _scrollController, onEndReached: onEndReached, listEmptyWidget: widget.listEmptyWidget, diff --git a/lib/src/widgets/siren_inbox_icon.dart b/lib/src/widgets/siren_inbox_icon.dart index e66762d..a79bdd0 100644 --- a/lib/src/widgets/siren_inbox_icon.dart +++ b/lib/src/widgets/siren_inbox_icon.dart @@ -37,7 +37,7 @@ class SirenInboxIcon extends StatefulWidget { final CustomStyles? customStyles; /// Callback function to handle errors. - final void Function(ApiErrorDetails)? onError; + final void Function(SirenErrorType)? onError; /// Callback function when the inbox icon is tapped. final VoidCallback? onTap; @@ -100,7 +100,7 @@ class _SirenInboxIconState extends State { } } else if (streamResponse.response?.isError ?? false) { widget.onError - ?.call(streamResponse.response?.error ?? ApiErrorDetails()); + ?.call(streamResponse.response?.error ?? SirenErrorType()); } }, ); @@ -159,8 +159,10 @@ class _SirenInboxIconState extends State { ); } } else if (response.isError) { - widget.onError?.call(response.error ?? ApiErrorDetails()); + widget.onError?.call(response.error ?? SirenErrorType()); } + } else if (!SirenDataProvider.instance.isProviderInitialized) { + widget.onError?.call(Generics.outsideSirenContextError); } } diff --git a/pubspec.yaml b/pubspec.yaml index 7357cfd..9d08281 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -17,7 +17,7 @@ environment: dependencies: flutter: sdk: flutter - dio: '>=5.2.0 <=5.4.2' + dio: '>=5.2.0 <=5.4.3' dev_dependencies: flutter_test: diff --git a/test/constants/generics_test.dart b/test/constants/generics_test.dart index 0187853..de6aadd 100644 --- a/test/constants/generics_test.dart +++ b/test/constants/generics_test.dart @@ -10,11 +10,11 @@ void main() { expect(Generics.PAGE_SIZE, 20); expect(Generics.MAX_RETRIES, 2); expect(Generics.ENV_PATH, 'packages/sirenapp_flutter_inbox/env'); - expect(Generics.defaultError.errorType, ErrorTypes.GENERIC_API_ERROR); - expect(Generics.defaultError.errorCode, 'INTERNAL SERVER ERROR'); + expect(Generics.defaultError.code, ErrorCodes.API_ERROR.name); + expect(Generics.defaultError.type, 'ERROR'); expect( Generics.defaultError.message, - 'Oops something went wrong, if issue persist please contact Siren Team', + 'Something went wrong', ); }); }); @@ -24,6 +24,7 @@ void main() { expect(Status.PENDING.index, 0); expect(Status.SUCCESS.index, 1); expect(Status.FAILED.index, 2); + expect(Status.IN_PROGRESS.index, 3); }); test('BulkUpdateType enum values are correct', () { @@ -32,23 +33,31 @@ void main() { }); test('UpdateEvents enum values are correct', () { - expect(UpdateEvents.READ_BY_ID.index, 0); - expect(UpdateEvents.READ_ALL.index, 1); - expect(UpdateEvents.DELETE_BY_ID.index, 2); - expect(UpdateEvents.DELETE_ALL.index, 3); - expect(UpdateEvents.VIEW_ALL.index, 4); - expect(UpdateEvents.PARAMS_CHANGED.index, 5); + expect(UpdateEvents.DELETE_ALL.index, 0); + expect(UpdateEvents.DELETE_BY_ID.index, 1); + expect(UpdateEvents.PARAMS_CHANGED.index, 2); + expect(UpdateEvents.READ_ALL.index, 3); + expect(UpdateEvents.READ_BY_ID.index, 4); + expect(UpdateEvents.SHOW_ERROR.index, 5); expect(UpdateEvents.TOKEN_VERIFIED.index, 6); + expect(UpdateEvents.VIEW_ALL.index, 7); }); test('ErrorTypes enum values are correct', () { - expect(ErrorTypes.GENERIC_API_ERROR.index, 0); - expect(ErrorTypes.AUTHENTICATION_FAILED.index, 1); - expect(ErrorTypes.FETCH_COUNT_FAILED.index, 2); - expect(ErrorTypes.NOTIFICATION_FETCH_FAILED.index, 3); - expect(ErrorTypes.NOTIFICATION_READ_FAILED.index, 4); - expect(ErrorTypes.NOTIFICATION_DELETE_FAILED.index, 5); - expect(ErrorTypes.UPDATE_VIEWED_FAILED.index, 6); + expect(ErrorCodes.API_ERROR.index, 0); + expect(ErrorCodes.AUTHENTICATION_FAILED.index, 1); + expect(ErrorCodes.AUTHENTICATION_PENDING.index, 2); + expect(ErrorCodes.BULK_DELETE_FAILED.index, 3); + expect(ErrorCodes.DELETE_FAILED.index, 4); + expect(ErrorCodes.INVALID_CREDENTIALS.index, 5); + expect(ErrorCodes.MARK_ALL_AS_READ_FAILED.index, 6); + expect(ErrorCodes.MARK_ALL_AS_VIEWED_FAILED.index, 7); + expect(ErrorCodes.MARK_AS_READ_FAILED.index, 8); + expect(ErrorCodes.NOTIFICATION_FETCH_FAILED.index, 9); + expect(ErrorCodes.NOTIFICATION_READ_FAILED.index, 10); + expect(ErrorCodes.OUTSIDE_SIREN_CONTEXT.index, 11); + expect(ErrorCodes.UNAUTHORIZED_OPERATION.index, 12); + expect(ErrorCodes.UNVIEWED_COUNT_FETCH_FAILED.index, 13); }); }); } diff --git a/test/delete_notification_by_id_test.dart b/test/delete_notification_by_id_test.dart index 8d31364..84a728b 100644 --- a/test/delete_notification_by_id_test.dart +++ b/test/delete_notification_by_id_test.dart @@ -50,8 +50,7 @@ class DeleteNotificationById { required String notificationId, }) async { final result = ApiResponse()..isLoading = true; - final apiError = ApiErrorDetails() - ..errorType = ErrorTypes.NOTIFICATION_DELETE_FAILED; + final apiError = SirenErrorType()..code = ErrorCodes.DELETE_FAILED.name; final apiResponse = await api.delete( path: '$_apiPath/$notificationId', @@ -60,7 +59,7 @@ class DeleteNotificationById { final deletionStatus = convertJsonToDeletionStatus(apiResponse.data); apiError - ..errorCode = ApiResponse.fromJson(apiResponse.data).error?.errorCode + ..type = ApiResponse.fromJson(apiResponse.data).error?.type ..message = ApiResponse.fromJson(apiResponse.data).error?.message; result ..isLoading = false diff --git a/test/models/api_response_test.dart b/test/models/api_response_test.dart index 350f09e..5532d1b 100644 --- a/test/models/api_response_test.dart +++ b/test/models/api_response_test.dart @@ -19,7 +19,7 @@ void main() { final response = ApiResponse.fromJson(json); expect(response.data, 'testData'); - expect(response.error?.errorCode, '123'); + expect(response.error?.type, '123'); expect(response.meta?.last, 'last'); expect(response.meta?.totalPages, 5); }); @@ -54,15 +54,15 @@ void main() { }); }); - group('ApiErrorDetails', () { + group('SirenErrorType', () { test('fromJson() should parse JSON correctly', () { final json = { 'errorCode': '123', 'message': 'Error message', }; - final errorDetails = ApiErrorDetails.fromJson(json); + final errorDetails = SirenErrorType.fromJson(json); - expect(errorDetails.errorCode, '123'); + expect(errorDetails.type, '123'); expect(errorDetails.message, 'Error message'); }); }); diff --git a/test/models/notification_model_test.dart b/test/models/notification_model_test.dart index 67a7839..8fcf33a 100644 --- a/test/models/notification_model_test.dart +++ b/test/models/notification_model_test.dart @@ -3,7 +3,7 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:sirenapp_flutter_inbox/src/models/notification_model.dart'; void main() { - group('NotificationDataType', () { + group('NotificationType', () { test('fromJson() should parse JSON correctly', () { final json = { 'id': 'notificationId', @@ -21,7 +21,7 @@ void main() { 'isRead': true, 'cardColor': Colors.blue, }; - final notification = NotificationDataType.fromJson(json); + final notification = NotificationType.fromJson(json); expect(notification.id, 'notificationId'); expect(notification.createdAt, '2022-01-01T00:00:00Z'); @@ -39,7 +39,7 @@ void main() { }); test('markAsRead() should mark the notification as read', () { - final notification = NotificationDataType( + final notification = NotificationType( id: 'notificationId', createdAt: '2022-01-01T00:00:00Z', message: MessageData( diff --git a/test/models/ui_models_test.dart b/test/models/ui_models_test.dart index 7c489c9..a441546 100644 --- a/test/models/ui_models_test.dart +++ b/test/models/ui_models_test.dart @@ -24,13 +24,13 @@ void main() { }); }); - group('CardProps', () { + group('CardParams', () { test('constructor should initialize properties with provided values', () { - const cardProps = CardProps( + const cardParams = CardParams( hideAvatar: true, ); - expect(cardProps.hideAvatar, true); + expect(cardParams.hideAvatar, true); }); }); @@ -104,7 +104,7 @@ void main() { const hideAvatar = true; const Widget deleteWidget = Icon(Icons.delete); - const cardParams = CardProps( + const cardParams = CardParams( hideAvatar: hideAvatar, deleteIcon: deleteWidget, ); diff --git a/test/utils/siren_test.dart b/test/utils/siren_test.dart index cfe95be..204ef2f 100644 --- a/test/utils/siren_test.dart +++ b/test/utils/siren_test.dart @@ -24,6 +24,7 @@ class MockNotificationsBulkUpdate extends Mock @override Future notificationsBulkUpdate({ required Map data, + required String operation, }) { final result = ApiResponse()..data = 'SUCCESS'; result.error = null; @@ -63,8 +64,11 @@ void main() { 'operation': BulkUpdateType.MARK_AS_READ.name, }; final mockResponse = ApiResponse(data: 'SUCCESS'); - final response = await mockMockNotificationsBulkUpdate - .notificationsBulkUpdate(data: mockData); + final response = + await mockMockNotificationsBulkUpdate.notificationsBulkUpdate( + data: mockData, + operation: BulkUpdateType.MARK_AS_READ.name, + ); expect(mockResponse.data, response.data); }); diff --git a/test/widgets/app_bar_test.dart b/test/widgets/app_bar_test.dart index 1648b98..ee64083 100644 --- a/test/widgets/app_bar_test.dart +++ b/test/widgets/app_bar_test.dart @@ -11,7 +11,7 @@ void main() { home: Scaffold( appBar: SirenAppBar( theme: ThemeData(), - inboxHeaderProps: InboxHeaderProps( + headerParams: HeaderParams( title: title, showBackButton: false, ), @@ -31,7 +31,7 @@ void main() { home: Scaffold( appBar: SirenAppBar( theme: ThemeData(), - inboxHeaderProps: InboxHeaderProps( + headerParams: HeaderParams( title: 'Title', showBackButton: true, ), @@ -52,7 +52,7 @@ void main() { home: Scaffold( appBar: SirenAppBar( theme: ThemeData(), - inboxHeaderProps: InboxHeaderProps( + headerParams: HeaderParams( title: 'Title', showBackButton: false, hideClearAll: true, @@ -74,7 +74,7 @@ void main() { home: Scaffold( appBar: SirenAppBar( theme: ThemeData(), - inboxHeaderProps: InboxHeaderProps( + headerParams: HeaderParams( title: 'Title', showBackButton: false, ), @@ -96,7 +96,7 @@ void main() { home: Scaffold( appBar: SirenAppBar( theme: ThemeData(), - inboxHeaderProps: InboxHeaderProps( + headerParams: HeaderParams( title: 'Title', showBackButton: true, onBackPress: () { @@ -122,7 +122,7 @@ void main() { home: Scaffold( appBar: SirenAppBar( theme: ThemeData(), - inboxHeaderProps: InboxHeaderProps( + headerParams: HeaderParams( title: 'Title', showBackButton: false, ), diff --git a/test/widgets/card_test.dart b/test/widgets/card_test.dart index ccd8861..f35e25d 100644 --- a/test/widgets/card_test.dart +++ b/test/widgets/card_test.dart @@ -15,7 +15,7 @@ void main() { testWidgets('CardWidget renders correctly', (WidgetTester tester) async { // ignore: unused_local_variable final func = MockFunction().call; - final notification = NotificationDataType( + final notification = NotificationType( id: '123', createdAt: '2024-03-15T04:07:14.577928Z', message: MessageData( @@ -45,7 +45,7 @@ void main() { deletePressed = true; }, notification: notification, - cardProps: CardProps( + cardParams: CardParams( hideAvatar: false, hideDelete: false, onAvatarClick: (notification) { diff --git a/test/widgets/inbox_body_test.dart b/test/widgets/inbox_body_test.dart index 64cc089..ce18bbd 100644 --- a/test/widgets/inbox_body_test.dart +++ b/test/widgets/inbox_body_test.dart @@ -20,8 +20,8 @@ void main() { notifications: const [], deleteNotification: (id) async {}, markAsRead: (id) {}, - customNotificationCard: null, - onNotificationCardClick: null, + customCard: null, + onCardClick: null, deletingNotificationId: null, disableAutoMarkAsRead: false, totalElements: 0, @@ -48,8 +48,8 @@ void main() { notifications: const [], deleteNotification: (id) async {}, markAsRead: (id) {}, - customNotificationCard: null, - onNotificationCardClick: null, + customCard: null, + onCardClick: null, deletingNotificationId: null, disableAutoMarkAsRead: false, totalElements: 0, @@ -65,8 +65,8 @@ void main() { }); testWidgets('InboxBody displays notifications', (WidgetTester tester) async { - final notifications = [ - NotificationDataType( + final notifications = [ + NotificationType( id: '1', createdAt: '2024-03-15T04:07:14.577928Z', message: MessageData( @@ -97,8 +97,8 @@ void main() { notifications: notifications, deleteNotification: (id) async {}, markAsRead: (id) {}, - customNotificationCard: null, - onNotificationCardClick: null, + customCard: null, + onCardClick: null, deletingNotificationId: null, disableAutoMarkAsRead: false, totalElements: 1, diff --git a/test/widgets/notification_list_view_test.dart b/test/widgets/notification_list_view_test.dart index 4c0d5b8..55cc5be 100644 --- a/test/widgets/notification_list_view_test.dart +++ b/test/widgets/notification_list_view_test.dart @@ -10,8 +10,8 @@ class MockFunction extends Mock { } void main() { - final notificationsList = [ - NotificationDataType( + final notificationsList = [ + NotificationType( id: '1', createdAt: '2024-03-15T04:07:14.577928Z', message: MessageData( @@ -53,7 +53,7 @@ void main() { scrollController: ScrollController(), onDelete: (id) async {}, markAsRead: (id) {}, - onNotificationCardClick: (n) { + onCardClick: (n) { func(); }, ), @@ -87,12 +87,12 @@ void main() { onEndReached: () {}, customStyles: null, scrollController: ScrollController(), - customNotificationCard: (n) { + customCard: (n) { return Text(n.message.subHeader.toString()); }, onDelete: (id) async {}, markAsRead: (id) {}, - onNotificationCardClick: (n) { + onCardClick: (n) { func(); }, ), diff --git a/test/widgets/siren_inbox_test.dart b/test/widgets/siren_inbox_test.dart index 73ea6b1..555fadc 100644 --- a/test/widgets/siren_inbox_test.dart +++ b/test/widgets/siren_inbox_test.dart @@ -8,7 +8,6 @@ import 'package:sirenapp_flutter_inbox/sirenapp_flutter_inbox.dart'; import 'package:sirenapp_flutter_inbox/src/api/fetch_unviewed_notification_count.dart'; import 'package:sirenapp_flutter_inbox/src/constants/generics.dart'; import 'package:sirenapp_flutter_inbox/src/data/siren_data_provider.dart'; -import 'package:sirenapp_flutter_inbox/src/widgets/loader_widget.dart'; import 'siren_inbox_test.mocks.dart'; @@ -38,24 +37,13 @@ void main() { iconController.close(); inboxController.close(); }); - testWidgets('Initial loading state', (WidgetTester tester) async { - await tester.pumpWidget( - const MaterialApp( - home: Scaffold( - body: SirenInbox(), - ), - ), - ); - - expect(find.byType(LoaderWidget), findsOneWidget); - }); testWidgets('Test Title', (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( home: Scaffold( body: SirenInbox( - inboxHeaderProps: InboxHeaderProps( + headerParams: HeaderParams( title: 'Notifications Header', ), darkMode: true, @@ -64,8 +52,6 @@ void main() { ), ); - expect(find.byType(LoaderWidget), findsOneWidget); - await tester.pump(); expect(find.text('Notifications Header'), findsOneWidget); @@ -77,7 +63,7 @@ void main() { MaterialApp( home: Scaffold( body: SirenInbox( - inboxHeaderProps: InboxHeaderProps( + headerParams: HeaderParams( showBackButton: true, onBackPress: () { backButtonPressed = true; From 588f58890c3c7d9e05edc1b3c2c568af133b2304 Mon Sep 17 00:00:00 2001 From: Anitta Babu <99161914+anitta-keyvalue@users.noreply.github.com> Date: Mon, 29 Apr 2024 17:19:32 +0530 Subject: [PATCH 06/17] feat: Changes to support flutter 3.0.0 , remove totalElements, theming changes --- analysis_options.yaml | 4 + example/.packages | 36 ++++ example/pubspec.lock | 36 ++-- example/pubspec.yaml | 2 +- lib/src/api/fetch_all_notification.dart | 2 +- lib/src/constants/generics.dart | 1 + lib/src/models/api_response.dart | 7 - lib/src/models/ui_models.dart | 12 +- lib/src/theme/app_colors.dart | 86 ++++++++ lib/src/theme/app_theme.dart | 138 +------------ lib/src/theme/colors.dart | 8 +- lib/src/theme/dark_colors.dart | 43 ++++ lib/src/theme/light_colors.dart | 43 ++++ lib/src/widgets/app_bar.dart | 34 ++-- lib/src/widgets/card.dart | 115 +++++++---- lib/src/widgets/empty_widget.dart | 27 ++- lib/src/widgets/error_widget.dart | 19 +- lib/src/widgets/icon_badge.dart | 11 +- lib/src/widgets/inbox_body.dart | 48 +++-- lib/src/widgets/loader_widget.dart | 49 +++-- lib/src/widgets/notification_list_view.dart | 21 +- lib/src/widgets/siren_inbox.dart | 113 +++++------ lib/src/widgets/siren_inbox_icon.dart | 103 +++++----- pubspec.yaml | 10 +- test/data/siren_data_provider_test.dart | 2 +- test/data/siren_data_provider_test.mocks.dart | 7 +- test/models/api_response_test.dart | 3 - test/services/api_client_test.dart | 3 + test/services/api_client_test.mocks.dart | 189 +++++++++--------- test/services/network_service_test.mocks.dart | 8 +- test/widgets/app_bar_test.dart | 6 - test/widgets/card_test.dart | 27 ++- test/widgets/icon_badge_test.dart | 6 + test/widgets/inbox_body_test.dart | 6 - test/widgets/notification_list_view_test.dart | 2 +- test/widgets/siren_inbox_icon_test.dart | 6 +- test/widgets/siren_inbox_icon_test.mocks.dart | 122 ++++++----- test/widgets/siren_inbox_test.mocks.dart | 155 +++++++------- 38 files changed, 845 insertions(+), 665 deletions(-) create mode 100644 example/.packages create mode 100644 lib/src/theme/app_colors.dart create mode 100644 lib/src/theme/dark_colors.dart create mode 100644 lib/src/theme/light_colors.dart diff --git a/analysis_options.yaml b/analysis_options.yaml index a3c163b..9906411 100644 --- a/analysis_options.yaml +++ b/analysis_options.yaml @@ -16,6 +16,10 @@ analyzer: errors: inference_failure_on_instance_creation: false inference_failure_on_function_invocation: false + implicit_dynamic_list_literal: false + implicit_dynamic_map_literal: false + implicit_dynamic_method: false + implicit_dynamic_type: false linter: # The lint rules applied to this project can be customized in the diff --git a/example/.packages b/example/.packages new file mode 100644 index 0000000..7f153fc --- /dev/null +++ b/example/.packages @@ -0,0 +1,36 @@ +# This file is deprecated. Tools should instead consume +# `.dart_tool/package_config.json`. +# +# For more info see: https://dart.dev/go/dot-packages-deprecation +# +# Generated by pub on 2024-04-27 11:07:12.015241. +async:file:///Users/anitta/.pub-cache/hosted/pub.dartlang.org/async-2.9.0/lib/ +boolean_selector:file:///Users/anitta/.pub-cache/hosted/pub.dartlang.org/boolean_selector-2.1.0/lib/ +characters:file:///Users/anitta/.pub-cache/hosted/pub.dartlang.org/characters-1.2.0/lib/ +charcode:file:///Users/anitta/.pub-cache/hosted/pub.dartlang.org/charcode-1.3.1/lib/ +clock:file:///Users/anitta/.pub-cache/hosted/pub.dartlang.org/clock-1.1.0/lib/ +collection:file:///Users/anitta/.pub-cache/hosted/pub.dartlang.org/collection-1.16.0/lib/ +cupertino_icons:file:///Users/anitta/.pub-cache/hosted/pub.dartlang.org/cupertino_icons-1.0.5/lib/ +dio:file:///Users/anitta/.pub-cache/hosted/pub.dartlang.org/dio-5.4.1/lib/ +fake_async:file:///Users/anitta/.pub-cache/hosted/pub.dartlang.org/fake_async-1.3.0/lib/ +flutter:file:///Users/anitta/Desktop/Development/flutter/packages/flutter/lib/ +flutter_lints:file:///Users/anitta/.pub-cache/hosted/pub.dartlang.org/flutter_lints-2.0.1/lib/ +flutter_test:file:///Users/anitta/Desktop/Development/flutter/packages/flutter_test/lib/ +http_parser:file:///Users/anitta/.pub-cache/hosted/pub.dartlang.org/http_parser-4.0.2/lib/ +lints:file:///Users/anitta/.pub-cache/hosted/pub.dartlang.org/lints-2.0.1/lib/ +matcher:file:///Users/anitta/.pub-cache/hosted/pub.dartlang.org/matcher-0.12.11/lib/ +material_color_utilities:file:///Users/anitta/.pub-cache/hosted/pub.dartlang.org/material_color_utilities-0.1.4/lib/ +meta:file:///Users/anitta/.pub-cache/hosted/pub.dartlang.org/meta-1.7.0/lib/ +network_logger:file:///Users/anitta/.pub-cache/hosted/pub.dartlang.org/network_logger-1.0.4/lib/ +path:file:///Users/anitta/.pub-cache/hosted/pub.dartlang.org/path-1.8.1/lib/ +sirenapp_flutter_inbox:../lib/ +sky_engine:file:///Users/anitta/Desktop/Development/flutter/bin/cache/pkg/sky_engine/lib/ +source_span:file:///Users/anitta/.pub-cache/hosted/pub.dartlang.org/source_span-1.8.2/lib/ +stack_trace:file:///Users/anitta/.pub-cache/hosted/pub.dartlang.org/stack_trace-1.10.0/lib/ +stream_channel:file:///Users/anitta/.pub-cache/hosted/pub.dartlang.org/stream_channel-2.1.0/lib/ +string_scanner:file:///Users/anitta/.pub-cache/hosted/pub.dartlang.org/string_scanner-1.1.0/lib/ +term_glyph:file:///Users/anitta/.pub-cache/hosted/pub.dartlang.org/term_glyph-1.2.0/lib/ +test_api:file:///Users/anitta/.pub-cache/hosted/pub.dartlang.org/test_api-0.4.9/lib/ +typed_data:file:///Users/anitta/.pub-cache/hosted/pub.dartlang.org/typed_data-1.3.2/lib/ +vector_math:file:///Users/anitta/.pub-cache/hosted/pub.dartlang.org/vector_math-2.1.2/lib/ +example:lib/ diff --git a/example/pubspec.lock b/example/pubspec.lock index 8aaa274..b68ab16 100644 --- a/example/pubspec.lock +++ b/example/pubspec.lock @@ -53,10 +53,10 @@ packages: dependency: transitive description: name: dio - sha256: "49af28382aefc53562459104f64d16b9dfd1e8ef68c862d5af436cc8356ce5a8" + sha256: "639179e1cc0957779e10dd5b786ce180c477c4c0aca5aaba5d1700fa2e834801" url: "https://pub.dev" source: hosted - version: "5.4.1" + version: "5.4.3" fake_async: dependency: transitive description: @@ -95,26 +95,26 @@ packages: dependency: transitive description: name: leak_tracker - sha256: "3f87a60e8c63aecc975dda1ceedbc8f24de75f09e4856ea27daf8958f2f0ce05" + sha256: "78eb209deea09858f5269f5a5b02be4049535f568c07b275096836f01ea323fa" url: "https://pub.dev" source: hosted - version: "10.0.5" + version: "10.0.0" leak_tracker_flutter_testing: dependency: transitive description: name: leak_tracker_flutter_testing - sha256: "932549fb305594d82d7183ecd9fa93463e9914e1b67cacc34bc40906594a1806" + sha256: b46c5e37c19120a8a01918cfaf293547f47269f7cb4b0058f21531c2465d6ef0 url: "https://pub.dev" source: hosted - version: "3.0.5" + version: "2.0.1" leak_tracker_testing: dependency: transitive description: name: leak_tracker_testing - sha256: "6ba465d5d76e67ddf503e1161d1f4a6bc42306f9d66ca1e8f079a47290fb06d3" + sha256: a597f72a664dbd293f3bfc51f9ba69816f84dcd403cdac7066cb3f6003f3ab47 url: "https://pub.dev" source: hosted - version: "3.0.1" + version: "2.0.1" lints: dependency: transitive description: @@ -135,18 +135,18 @@ packages: dependency: transitive description: name: material_color_utilities - sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec + sha256: "0e0a020085b65b6083975e499759762399b4475f766c21668c4ecca34ea74e5a" url: "https://pub.dev" source: hosted - version: "0.11.1" + version: "0.8.0" meta: dependency: transitive description: name: meta - sha256: "25dfcaf170a0190f47ca6355bdd4552cb8924b430512ff0cafb8db9bd41fe33b" + sha256: d584fa6707a52763a52446f02cc621b077888fb63b93bbcb1143a7be5a0c0c04 url: "https://pub.dev" source: hosted - version: "1.14.0" + version: "1.11.0" network_logger: dependency: "direct main" description: @@ -219,10 +219,10 @@ packages: dependency: transitive description: name: test_api - sha256: "2419f20b0c8677b2d67c8ac4d1ac7372d862dc6c460cdbb052b40155408cd794" + sha256: "5c2f730018264d276c20e4f1503fd1308dfbbae39ec8ee63c5236311ac06954b" url: "https://pub.dev" source: hosted - version: "0.7.1" + version: "0.6.1" typed_data: dependency: transitive description: @@ -243,10 +243,10 @@ packages: dependency: transitive description: name: vm_service - sha256: "3923c89304b715fb1eb6423f017651664a03bf5f4b29983627c4da791f74a4ec" + sha256: b3d56ff4341b8f182b96aceb2fa20e3dcb336b9f867bc0eafc0de10f1048e957 url: "https://pub.dev" source: hosted - version: "14.2.1" + version: "13.0.0" sdks: - dart: ">=3.3.0 <4.0.0" - flutter: ">=3.18.0-18.0.pre.54" + dart: ">=3.2.0-0 <4.0.0" + flutter: ">=3.0.0" diff --git a/example/pubspec.yaml b/example/pubspec.yaml index 7ec340b..5c5038c 100644 --- a/example/pubspec.yaml +++ b/example/pubspec.yaml @@ -19,7 +19,7 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev version: 1.0.0+1 environment: - sdk: '>=2.18.0 <4.0.0' + sdk: '>=2.17.0 <4.0.0' # Dependencies specify other packages that your package needs in order to work. # To automatically upgrade your package dependencies to the latest versions diff --git a/lib/src/api/fetch_all_notification.dart b/lib/src/api/fetch_all_notification.dart index 1a884f0..a51698f 100644 --- a/lib/src/api/fetch_all_notification.dart +++ b/lib/src/api/fetch_all_notification.dart @@ -17,7 +17,7 @@ class FetchAllNotifications { List convertJsonToNotificationList( List dataList, ) { - return dataList.map((json) { + return dataList.map((dynamic json) { if (json is Map) { return NotificationType.fromJson(json); } diff --git a/lib/src/constants/generics.dart b/lib/src/constants/generics.dart index 13035ad..d089fb1 100644 --- a/lib/src/constants/generics.dart +++ b/lib/src/constants/generics.dart @@ -9,6 +9,7 @@ class Generics { static const int DATA_FETCH_INTERVAL = 5; static const int PAGE_SIZE = 20; + static const int AVERAGE_ITEMS_ON_SCREEN = 7; static const int MAX_RETRIES = 2; static const String ENV_PATH = 'packages/sirenapp_flutter_inbox/env'; diff --git a/lib/src/models/api_response.dart b/lib/src/models/api_response.dart index 839c830..9000bb8 100644 --- a/lib/src/models/api_response.dart +++ b/lib/src/models/api_response.dart @@ -53,7 +53,6 @@ class MetaResponse { required this.pageSize, required this.currentPage, required this.first, - required this.totalElements, }); /// Factory method to create MetaResponse from JSON. @@ -70,9 +69,6 @@ class MetaResponse { ? int.tryParse(json?['currentPage'] as String) : null, first: json?['first'] != null ? (json?['first'] as String) : null, - totalElements: json?['totalElements'] != null - ? int.tryParse(json?['totalElements'] as String) - : null, ); } @@ -90,9 +86,6 @@ class MetaResponse { /// The ID of the first element. final String? first; - - /// The total number of elements. - final int? totalElements; } /// Represents details of an API error. diff --git a/lib/src/models/ui_models.dart b/lib/src/models/ui_models.dart index 0646e09..2d7b192 100644 --- a/lib/src/models/ui_models.dart +++ b/lib/src/models/ui_models.dart @@ -131,7 +131,7 @@ class CustomThemeColors { this.dateColor, this.timerIcon, this.notificationIconColor, - this.refreshIndicatorColor, + this.loaderColor, this.inboxHeaderColors, this.badgeColors, this.cardColors, @@ -168,7 +168,7 @@ class CustomThemeColors { final Color? notificationIconColor; /// The color for refresh indicator in inbox list. - final Color? refreshIndicatorColor; + final Color? loaderColor; /// The colors for inbox list card final CardColors? cardColors; @@ -231,15 +231,15 @@ class InboxHeaderColors { /// Custom theme colors to configure icon badge class BadgeColors { BadgeColors({ + this.backgroundColor, this.color, - this.textColor, }); - /// The icon badge color - final Color? color; + /// The icon badge background color + final Color? backgroundColor; /// The text color of icon badge - final Color? textColor; + final Color? color; } /// Properties for configuring the appearance of the notification window app bar. diff --git a/lib/src/theme/app_colors.dart b/lib/src/theme/app_colors.dart new file mode 100644 index 0000000..5254423 --- /dev/null +++ b/lib/src/theme/app_colors.dart @@ -0,0 +1,86 @@ +import 'package:flutter/material.dart'; +import 'package:sirenapp_flutter_inbox/src/theme/dark_colors.dart'; +import 'package:sirenapp_flutter_inbox/src/theme/light_colors.dart'; + +class AppColors { + AppColors({ + required this.appBarActionText, + required this.appBarBackIcon, + required this.appBarBorderColor, + required this.appBarTextColor, + required this.avatarBackground, + required this.avatarIconColor, + required this.backgroundColor, + required this.badgeBackgroundColor, + required this.badgeTextColor, + required this.borderColor, + required this.borderDecorationColor, + required this.cardBackgroundUnread, + required this.cardBorderColor, + required this.cardBorderUnread, + required this.clearAllIcon, + required this.dateColor, + required this.deleteIcon, + required this.emptyScreenDescription, + required this.emptyScreenTitle, + required this.emptyWidgetBackground, + required this.emptyWidgetBorderColor, + required this.emptyWidgetIconColor, + required this.emptyWidgetNotificationColor, + required this.errorWidgetIconColor, + required this.errorWidgetIconContainer, + required this.errorWidgetText1, + required this.errorWidgetText2, + required this.highlightedCardColor, + required this.loaderColor, + required this.loadingIndicator, + required this.loadingIndicatorBackground, + required this.notificationIconColor, + required this.primary, + required this.scaffoldBackgroundColor, + required this.skeletonLoaderColor, + required this.textColor, + required this.timerIcon, + }); + factory AppColors.lightColorTheme() => lightColors; + + factory AppColors.darkColorTheme() => darkColors; + + Color appBarActionText; + Color appBarBackIcon; + Color appBarBorderColor; + Color appBarTextColor; + Color avatarBackground; + Color avatarIconColor; + Color backgroundColor; + Color badgeBackgroundColor; + Color badgeTextColor; + Color borderColor; + Color borderDecorationColor; + Color cardBackgroundUnread; + Color cardBorderColor; + Color cardBorderUnread; + Color clearAllIcon; + Color dateColor; + Color deleteIcon; + Color emptyScreenTitle; + Color emptyScreenDescription; + Color emptyWidgetBackground; + Color emptyWidgetBorderColor; + Color emptyWidgetIconColor; + Color emptyWidgetNotificationColor; + Color errorWidgetIconColor; + Color errorWidgetIconContainer; + Color errorWidgetText1; + Color errorWidgetText2; + Color highlightedCardColor; + Color loaderColor; + Color loadingIndicator; + Color loadingIndicatorBackground; + Color notificationIconColor; + Color primary; + Color scaffoldBackgroundColor; + Color skeletonLoaderColor; + Color textColor; + Color timerIcon; +} diff --git a/lib/src/theme/app_theme.dart b/lib/src/theme/app_theme.dart index 3d931ee..40a6e86 100644 --- a/lib/src/theme/app_theme.dart +++ b/lib/src/theme/app_theme.dart @@ -1,129 +1,13 @@ -import 'package:flutter/material.dart'; -import 'package:sirenapp_flutter_inbox/sirenapp_flutter_inbox.dart'; -import 'package:sirenapp_flutter_inbox/src/theme/colors.dart'; - -class AppTheme { - static ThemeData _createThemeData(ColorScheme colorScheme) { - return ThemeData.from(colorScheme: colorScheme); - } - - static ThemeData lightTheme = ThemeData.light().copyWith( - colorScheme: ThemeData.light().colorScheme.copyWith( - primaryContainer: AppColors.emptyWidgetBgLightTheme, - inversePrimary: AppColors.grey500, - onPrimaryContainer: AppColors.grey300Complementary, - onInverseSurface: Colors.white, - onPrimary: AppColors.black100, - onSecondary: AppColors.avatarPlaceholderBgLight, - onTertiary: AppColors.primary200, - outline: AppColors.grey500, - outlineVariant: AppColors.grey400, - primary: Colors.white, - scrim: AppColors.grey500, - secondary: AppColors.primary200, - secondaryContainer: AppColors.primary50, - shadow: AppColors.emptyWidgetBellLight, - surface: AppColors.emptyWidgetBadgeLight, - surfaceTint: AppColors.grey300, - onTertiaryContainer: AppColors.avatarIconLight, - tertiary: AppColors.grey700, - tertiaryContainer: AppColors.red, - ), - ); - - static ThemeData darkTheme = ThemeData.dark().copyWith( - colorScheme: ThemeData.dark().colorScheme.copyWith( - primaryContainer: AppColors.emptyWidgetBgDarkTheme, - inversePrimary: AppColors.grey400, - onPrimaryContainer: AppColors.grey50, - onInverseSurface: Colors.white, - onPrimary: Colors.white, - onSecondary: AppColors.avatarPlaceholderBgDark, - onTertiary: AppColors.primary200Complementary, - outline: AppColors.grey500Complementary, - outlineVariant: AppColors.grey400Complementary, - primary: AppColors.black100, - scrim: AppColors.grey400, - secondary: AppColors.primary200Complementary, - secondaryContainer: AppColors.primary50Complementary, - shadow: AppColors.emptyWidgetBellDark, - surface: AppColors.emptyWidgetBadgeDark, - surfaceTint: AppColors.grey300Complementary, - onTertiaryContainer: AppColors.avatarIconDark, - tertiary: AppColors.grey700Complementary, - tertiaryContainer: AppColors.red, - ), - ); - - static ThemeData customTheme( - CustomThemeColors customColors, { - bool isDarkMode = false, - }) { - final baseTheme = isDarkMode ? darkTheme : lightTheme; - return _createThemeData( - baseTheme.colorScheme.copyWith( - inversePrimary: - customColors.dateColor ?? baseTheme.colorScheme.inversePrimary, - onPrimary: customColors.notificationIconColor ?? - baseTheme.colorScheme.onPrimary, - onTertiary: customColors.refreshIndicatorColor ?? - baseTheme.colorScheme.onTertiary, - outline: customColors.clearAllIcon ?? baseTheme.colorScheme.outline, - outlineVariant: - customColors.deleteIcon ?? baseTheme.colorScheme.outlineVariant, - primary: customColors.backgroundColor ?? baseTheme.colorScheme.primary, - secondary: customColors.primary ?? baseTheme.colorScheme.secondary, - secondaryContainer: customColors.highlightedCardColor ?? - baseTheme.colorScheme.secondaryContainer, - surfaceTint: - customColors.borderColor ?? baseTheme.colorScheme.surfaceTint, - tertiary: customColors.textColor ?? baseTheme.colorScheme.tertiary, - scrim: customColors.timerIcon ?? baseTheme.colorScheme.scrim, - primaryContainer: baseTheme.colorScheme.primaryContainer, - onSecondary: baseTheme.colorScheme.onSecondary, - shadow: baseTheme.colorScheme.shadow, - surface: baseTheme.colorScheme.surface, - onTertiaryContainer: baseTheme.colorScheme.onTertiaryContainer, - onInverseSurface: baseTheme.colorScheme.onInverseSurface, - onPrimaryContainer: baseTheme.colorScheme.onPrimaryContainer, - ), - ) - .copyWith( - badgeTheme: baseTheme.badgeTheme.copyWith( - backgroundColor: customColors.badgeColors?.color ?? - baseTheme.badgeTheme.backgroundColor, - textColor: customColors.badgeColors?.textColor ?? - baseTheme.badgeTheme.textColor, - ), - ) - .copyWith( - appBarTheme: baseTheme.appBarTheme.copyWith( - backgroundColor: customColors.inboxHeaderColors?.background ?? - baseTheme.appBarTheme.backgroundColor, - foregroundColor: - customColors.inboxHeaderColors?.headerActionColor ?? - baseTheme.appBarTheme.foregroundColor, - shadowColor: customColors.inboxHeaderColors?.borderColor ?? - baseTheme.appBarTheme.shadowColor, - ), - ) - .copyWith( - cardTheme: baseTheme.cardTheme.copyWith( - color: customColors.cardColors?.background, // Card background - shadowColor: customColors.cardColors?.borderColor, // Card border - surfaceTintColor: - customColors.cardColors?.titleColor, // Card title color - ), - ) - .copyWith( - bannerTheme: baseTheme.bannerTheme.copyWith( - backgroundColor: - customColors.cardColors?.subtitleColor, // Card sub title color - surfaceTintColor: customColors - .cardColors?.descriptionColor, // Card description color - dividerColor: customColors - .inboxHeaderColors?.titleColor, // Header title color - ), - ); +import 'package:sirenapp_flutter_inbox/src/theme/app_colors.dart'; + +/// This class represents the theme of the app and handles theme-related functionality. +class SirenAppTheme { + /// Returns the AppColors object based on the current theme mode + static AppColors colors({required bool isDarkMode}) { + if (isDarkMode) { + return AppColors.darkColorTheme(); + } else { + return AppColors.lightColorTheme(); + } } } diff --git a/lib/src/theme/colors.dart b/lib/src/theme/colors.dart index d0bb3d6..39245a9 100644 --- a/lib/src/theme/colors.dart +++ b/lib/src/theme/colors.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; -class AppColors { - const AppColors._(); +class SirenAppColors { + const SirenAppColors._(); //light mode colors static const Color primary200 = Color(0xFFFA9874); static const Color primary400 = Color(0xFFF56630); @@ -26,8 +26,8 @@ class AppColors { static const Color emptyWidgetBgLightTheme = Color(0xFFF7F9FC); static const Color emptyWidgetBgDarkTheme = Color(0xFF38383D); static const Color emptyWidgetBellDark = Color(0xFF5E5E6A); - static const Color emptyWidgetBellLight = AppColors.grey300; - static const Color emptyWidgetBadgeLight = AppColors.grey400; + static const Color emptyWidgetBellLight = SirenAppColors.grey300; + static const Color emptyWidgetBadgeLight = SirenAppColors.grey400; static const Color emptyWidgetBadgeDark = Color(0xFF63636C); static const Color avatarPlaceholderBgLight = Color(0xFFF0F2F5); diff --git a/lib/src/theme/dark_colors.dart b/lib/src/theme/dark_colors.dart new file mode 100644 index 0000000..06a5fd2 --- /dev/null +++ b/lib/src/theme/dark_colors.dart @@ -0,0 +1,43 @@ +import 'package:flutter/material.dart'; +import 'package:sirenapp_flutter_inbox/src/theme/app_colors.dart'; +import 'package:sirenapp_flutter_inbox/src/theme/colors.dart'; + +final darkColors = AppColors( + appBarActionText: SirenAppColors.grey500Complementary, + appBarBackIcon: SirenAppColors.grey50, + appBarBorderColor: SirenAppColors.grey300Complementary, + appBarTextColor: SirenAppColors.grey50, + avatarBackground: SirenAppColors.avatarPlaceholderBgDark, + avatarIconColor: SirenAppColors.avatarIconDark, + backgroundColor: SirenAppColors.black100, + badgeBackgroundColor: Colors.red, + badgeTextColor: Colors.white, + borderColor: SirenAppColors.grey300Complementary, + borderDecorationColor: SirenAppColors.emptyWidgetBadgeDark, + cardBackgroundUnread: SirenAppColors.primary50Complementary, + cardBorderColor: SirenAppColors.grey300Complementary, + cardBorderUnread: SirenAppColors.primary200Complementary, + clearAllIcon: SirenAppColors.grey500Complementary, + dateColor: SirenAppColors.grey400, + deleteIcon: SirenAppColors.grey400Complementary, + emptyScreenDescription: SirenAppColors.grey500Complementary, + emptyScreenTitle: SirenAppColors.grey700Complementary, + emptyWidgetBackground: SirenAppColors.emptyWidgetBgDarkTheme, + emptyWidgetBorderColor: SirenAppColors.emptyWidgetBgDarkTheme, + emptyWidgetIconColor: SirenAppColors.emptyWidgetBadgeDark, + emptyWidgetNotificationColor: SirenAppColors.emptyWidgetBadgeDark, + errorWidgetIconColor: SirenAppColors.emptyWidgetBellDark, + errorWidgetIconContainer: SirenAppColors.emptyWidgetBgDarkTheme, + errorWidgetText1: SirenAppColors.grey700Complementary, + errorWidgetText2: SirenAppColors.grey500Complementary, + highlightedCardColor: SirenAppColors.primary50Complementary, + loaderColor: SirenAppColors.primary200Complementary, + loadingIndicator: SirenAppColors.primary200Complementary, + loadingIndicatorBackground: SirenAppColors.emptyWidgetBgDarkTheme, + notificationIconColor: SirenAppColors.emptyWidgetBadgeDark, + primary: SirenAppColors.primary200Complementary, + scaffoldBackgroundColor: SirenAppColors.black100, + skeletonLoaderColor: SirenAppColors.avatarPlaceholderBgDark, + textColor: SirenAppColors.grey700Complementary, + timerIcon: SirenAppColors.grey400, +); diff --git a/lib/src/theme/light_colors.dart b/lib/src/theme/light_colors.dart new file mode 100644 index 0000000..2c8ad28 --- /dev/null +++ b/lib/src/theme/light_colors.dart @@ -0,0 +1,43 @@ +import 'package:flutter/material.dart'; +import 'package:sirenapp_flutter_inbox/src/theme/app_colors.dart'; +import 'package:sirenapp_flutter_inbox/src/theme/colors.dart'; + +final lightColors = AppColors( + appBarActionText: SirenAppColors.grey500, + appBarBackIcon: SirenAppColors.grey300Complementary, + appBarBorderColor: SirenAppColors.grey300, + appBarTextColor: SirenAppColors.grey300Complementary, + avatarBackground: SirenAppColors.avatarPlaceholderBgLight, + avatarIconColor: SirenAppColors.avatarIconLight, + backgroundColor: Colors.white, + badgeBackgroundColor: Colors.red, + badgeTextColor: Colors.white, + borderColor: SirenAppColors.grey300, + borderDecorationColor: SirenAppColors.emptyWidgetBadgeLight, + cardBackgroundUnread: SirenAppColors.primary50, + cardBorderColor: SirenAppColors.grey300, + cardBorderUnread: SirenAppColors.primary200, + clearAllIcon: SirenAppColors.grey500, + dateColor: SirenAppColors.grey500, + deleteIcon: SirenAppColors.grey400, + emptyScreenDescription: SirenAppColors.grey500, + emptyScreenTitle: SirenAppColors.grey700, + emptyWidgetBackground: SirenAppColors.emptyWidgetBgLightTheme, + emptyWidgetBorderColor: SirenAppColors.emptyWidgetBgLightTheme, + emptyWidgetIconColor: SirenAppColors.emptyWidgetBellLight, + emptyWidgetNotificationColor: SirenAppColors.emptyWidgetBadgeLight, + errorWidgetIconColor: SirenAppColors.emptyWidgetBellLight, + errorWidgetIconContainer: SirenAppColors.emptyWidgetBgLightTheme, + errorWidgetText1: SirenAppColors.grey700, + errorWidgetText2: SirenAppColors.grey500, + highlightedCardColor: SirenAppColors.primary50, + loaderColor: SirenAppColors.primary200, + loadingIndicator: SirenAppColors.primary200, + loadingIndicatorBackground: SirenAppColors.emptyWidgetBgLightTheme, + notificationIconColor: SirenAppColors.emptyWidgetBadgeLight, + primary: SirenAppColors.primary200, + scaffoldBackgroundColor: Colors.white, + skeletonLoaderColor: SirenAppColors.avatarPlaceholderBgLight, + textColor: SirenAppColors.grey700, + timerIcon: SirenAppColors.grey500, +); diff --git a/lib/src/widgets/app_bar.dart b/lib/src/widgets/app_bar.dart index 245833a..9d1ed7e 100644 --- a/lib/src/widgets/app_bar.dart +++ b/lib/src/widgets/app_bar.dart @@ -1,21 +1,24 @@ import 'package:flutter/material.dart'; import 'package:sirenapp_flutter_inbox/src/constants/strings.dart'; import 'package:sirenapp_flutter_inbox/src/models/ui_models.dart'; +import 'package:sirenapp_flutter_inbox/src/theme/app_theme.dart'; class SirenAppBar extends StatelessWidget implements PreferredSizeWidget { const SirenAppBar({ - required this.theme, required this.isNonEmptyNotifications, - super.key, this.onClearAllPressed, this.headerParams, this.styles, + this.colors, + this.isDarkMode, + super.key, }); - final ThemeData theme; final VoidCallback? onClearAllPressed; final bool isNonEmptyNotifications; final HeaderParams? headerParams; final CustomStyles? styles; + final CustomThemeColors? colors; + final bool? isDarkMode; @override Size get preferredSize { @@ -29,14 +32,16 @@ class SirenAppBar extends StatelessWidget implements PreferredSizeWidget { if (headerParams?.hideHeader ?? false) { return const SizedBox.shrink(); } + final defaultColors = SirenAppTheme.colors(isDarkMode: isDarkMode ?? false); return Container( decoration: BoxDecoration( - color: theme.appBarTheme.backgroundColor ?? theme.colorScheme.primary, + color: colors?.inboxHeaderColors?.background ?? + defaultColors.backgroundColor, border: Border( bottom: BorderSide( width: styles?.appBarStyle?.borderWidth ?? 1, - color: - theme.appBarTheme.shadowColor ?? theme.colorScheme.surfaceTint, + color: colors?.inboxHeaderColors?.borderColor ?? + defaultColors.appBarBorderColor, ), ), ), @@ -59,8 +64,8 @@ class SirenAppBar extends StatelessWidget implements PreferredSizeWidget { child: headerParams?.backButton ?? Icon( Icons.arrow_back_ios, - color: theme.bannerTheme.dividerColor ?? - theme.colorScheme.onPrimaryContainer, + color: colors?.inboxHeaderColors?.titleColor ?? + defaultColors.appBarBackIcon, size: 20, ), ), @@ -74,8 +79,9 @@ class SirenAppBar extends StatelessWidget implements PreferredSizeWidget { TextStyle( fontSize: 18, fontWeight: FontWeight.w600, - color: theme.bannerTheme.dividerColor ?? - theme.colorScheme.onPrimaryContainer, + color: colors?.inboxHeaderColors?.titleColor ?? + colors?.textColor ?? + defaultColors.appBarTextColor, ), ), ), @@ -102,7 +108,8 @@ class SirenAppBar extends StatelessWidget implements PreferredSizeWidget { child: Icon( Icons.clear_all, size: styles?.clearAllIconSize ?? 24, - color: theme.colorScheme.outline, + color: colors?.clearAllIcon ?? + defaultColors.appBarActionText, ), ), Text( @@ -110,8 +117,9 @@ class SirenAppBar extends StatelessWidget implements PreferredSizeWidget { style: TextStyle( fontSize: 14, fontWeight: FontWeight.w500, - color: theme.appBarTheme.foregroundColor ?? - theme.colorScheme.outline, + color: colors?.inboxHeaderColors + ?.headerActionColor ?? + defaultColors.appBarActionText, ), ), ], diff --git a/lib/src/widgets/card.dart b/lib/src/widgets/card.dart index b49aee5..6b7305d 100644 --- a/lib/src/widgets/card.dart +++ b/lib/src/widgets/card.dart @@ -2,6 +2,8 @@ import 'package:flutter/material.dart'; import 'package:sirenapp_flutter_inbox/src/constants/strings.dart'; import 'package:sirenapp_flutter_inbox/src/models/notification_model.dart'; import 'package:sirenapp_flutter_inbox/src/models/ui_models.dart'; +import 'package:sirenapp_flutter_inbox/src/theme/app_colors.dart'; +import 'package:sirenapp_flutter_inbox/src/theme/app_theme.dart'; import 'package:sirenapp_flutter_inbox/src/utils/common_utils.dart'; import 'package:sirenapp_flutter_inbox/src/widgets/common/nullable_text.dart'; @@ -13,6 +15,8 @@ class CardWidget extends StatefulWidget { required this.cardParams, required this.styles, required this.onDelete, + this.colors, + this.isDarkMode, super.key, }); @@ -31,6 +35,12 @@ class CardWidget extends StatefulWidget { /// Callback function invoked when the card is deleted. final void Function(String) onDelete; + /// Colors to be applied to various elements of the card. + final CustomThemeColors? colors; + + /// Flag to check if dark mode colors are to be applied + final bool? isDarkMode; + @override State createState() => _CardWidgetState(); } @@ -43,7 +53,8 @@ class _CardWidgetState extends State { @override Widget build(BuildContext context) { - final currentTheme = Theme.of(context); + final defaultColors = + SirenAppTheme.colors(isDarkMode: widget.isDarkMode ?? false); return GestureDetector( key: Key('siren-notification-card-${widget.notification.id}'), @@ -52,25 +63,26 @@ class _CardWidgetState extends State { }, child: Container( decoration: widget.styles?.cardStyle?.cardContainer?.decoration ?? - _getDefaultContainerDecoration(currentTheme), + _getDefaultContainerDecoration(widget.colors, defaultColors), padding: widget.styles?.cardStyle?.cardContainer?.padding ?? const EdgeInsets.symmetric(vertical: 12, horizontal: 12), child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ if (!(widget.cardParams.hideAvatar ?? false)) - _buildDefaultAvatarContainer(currentTheme), + _buildDefaultAvatarContainer(widget.colors, defaultColors), Expanded( child: Padding( padding: const EdgeInsets.only(left: 6), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - _buildHeaderText(currentTheme), - _buildSubHeaderText(currentTheme), - _buildBodyText(currentTheme), + _buildHeaderText(widget.colors, defaultColors), + _buildSubHeaderText(widget.colors, defaultColors), + _buildBodyText(widget.colors, defaultColors), _buildFooterRow( - currentTheme, + widget.colors, + defaultColors, widget.styles?.dateIconSize ?? 14, ), ], @@ -83,33 +95,45 @@ class _CardWidgetState extends State { ); } - BorderSide _getDefaultBorderDecoration(ThemeData theme) { + BorderSide _getDefaultBorderDecoration( + CustomThemeColors? colors, + AppColors defaultColors, + ) { return BorderSide( - color: theme.cardTheme.shadowColor ?? theme.colorScheme.surfaceTint, + color: colors?.cardColors?.borderColor ?? + colors?.borderColor ?? + defaultColors.cardBorderColor, width: 0.5, ); } - BoxDecoration _getDefaultContainerDecoration(ThemeData theme) { + BoxDecoration _getDefaultContainerDecoration( + CustomThemeColors? colors, + AppColors defaultColors, + ) { return BoxDecoration( border: Border( left: BorderSide( color: widget.notification.isRead ? Colors.transparent - : theme.colorScheme.secondary, + : colors?.primary ?? defaultColors.cardBorderUnread, width: 4, ), - right: _getDefaultBorderDecoration(theme), - bottom: _getDefaultBorderDecoration(theme), + right: _getDefaultBorderDecoration(colors, defaultColors), + bottom: _getDefaultBorderDecoration(colors, defaultColors), ), color: widget.notification.cardColor ?? (widget.notification.isRead - ? theme.cardTheme.color ?? Colors.transparent - : theme.colorScheme.secondaryContainer), + ? colors?.cardColors?.background ?? Colors.transparent + : colors?.highlightedCardColor ?? + defaultColors.cardBackgroundUnread), ); } - Widget _buildDefaultAvatarContainer(ThemeData theme) { + Widget _buildDefaultAvatarContainer( + CustomThemeColors? colors, + AppColors defaultColors, + ) { final avatarUrl = widget.notification.message.avatar?.url; return GestureDetector( key: Key('siren-notification-avatar-${widget.notification.id}'), @@ -128,13 +152,13 @@ class _CardWidgetState extends State { avatarUrl != Strings.string_null ? NetworkImage(avatarUrl) : null, - backgroundColor: theme.colorScheme.onSecondary, + backgroundColor: defaultColors.avatarBackground, child: avatarUrl == null || avatarUrl.isEmpty || avatarUrl == Strings.string_null ? Icon( Icons.landscape_rounded, - color: theme.colorScheme.onTertiaryContainer, + color: defaultColors.avatarIconColor, ) : null, ), @@ -142,7 +166,7 @@ class _CardWidgetState extends State { ); } - Widget _buildHeaderText(ThemeData theme) { + Widget _buildHeaderText(CustomThemeColors? colors, AppColors defaultColors) { return Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, crossAxisAlignment: CrossAxisAlignment.start, @@ -156,8 +180,9 @@ class _CardWidgetState extends State { TextStyle( fontSize: 14, fontWeight: FontWeight.w600, - color: theme.cardTheme.surfaceTintColor ?? - theme.colorScheme.tertiary, + color: colors?.cardColors?.titleColor ?? + colors?.textColor ?? + defaultColors.textColor, ), ), ), @@ -170,7 +195,8 @@ class _CardWidgetState extends State { onTap: () => widget.onDelete(widget.notification.id), child: widget.cardParams.deleteIcon ?? _buildDefaultDeleteButton( - theme, + colors, + defaultColors, widget.styles?.deleteIconSize ?? 18, ), ), @@ -179,7 +205,10 @@ class _CardWidgetState extends State { ); } - Widget _buildSubHeaderText(ThemeData theme) { + Widget _buildSubHeaderText( + CustomThemeColors? colors, + AppColors defaultColors, + ) { return Padding( padding: const EdgeInsets.symmetric(vertical: 6), child: NullableText( @@ -188,47 +217,57 @@ class _CardWidgetState extends State { TextStyle( fontSize: 14, fontWeight: FontWeight.w500, - color: theme.bannerTheme.backgroundColor ?? - theme.colorScheme.tertiary, + color: colors?.cardColors?.subtitleColor ?? + colors?.textColor ?? + defaultColors.textColor, ), ), ); } - Widget _buildBodyText(ThemeData theme) { + Widget _buildBodyText(CustomThemeColors? colors, AppColors defaultColors) { return Text( widget.notification.message.body ?? '', style: widget.styles?.cardStyle?.cardDescription ?? TextStyle( fontSize: 14, fontWeight: FontWeight.w400, - color: theme.bannerTheme.surfaceTintColor ?? - theme.colorScheme.tertiary, + color: colors?.cardColors?.descriptionColor ?? + colors?.textColor ?? + defaultColors.textColor, ), maxLines: 2, overflow: TextOverflow.ellipsis, ); } - Widget _buildFooterRow(ThemeData theme, double size) { + Widget _buildFooterRow( + CustomThemeColors? colors, + AppColors defaultColors, + double size, + ) { return Padding( padding: const EdgeInsets.only( top: 10, ), child: Container( - child: _buildTimestampText(theme, size), + child: _buildTimestampText(colors, defaultColors, size), ), ); } - Widget _buildTimestampText(ThemeData theme, double size) { + Widget _buildTimestampText( + CustomThemeColors? colors, + AppColors defaultColors, + double size, + ) { return Row( children: [ Padding( padding: const EdgeInsets.only(right: 4), child: Icon( Icons.access_time_sharp, - color: theme.colorScheme.scrim, + color: colors?.timerIcon ?? defaultColors.timerIcon, size: size, ), ), @@ -240,17 +279,23 @@ class _CardWidgetState extends State { TextStyle( fontSize: 12, fontWeight: FontWeight.w400, - color: theme.colorScheme.inversePrimary, + color: colors?.dateColor ?? + colors?.textColor ?? + defaultColors.dateColor, ), ), ], ); } - Widget _buildDefaultDeleteButton(ThemeData theme, double size) { + Widget _buildDefaultDeleteButton( + CustomThemeColors? colors, + AppColors defaultColors, + double size, + ) { return Icon( Icons.close, - color: theme.colorScheme.outlineVariant, + color: colors?.deleteIcon ?? defaultColors.deleteIcon, size: size, ); } diff --git a/lib/src/widgets/empty_widget.dart b/lib/src/widgets/empty_widget.dart index 9f2bcd7..e321af8 100644 --- a/lib/src/widgets/empty_widget.dart +++ b/lib/src/widgets/empty_widget.dart @@ -1,12 +1,19 @@ import 'package:flutter/material.dart'; import 'package:sirenapp_flutter_inbox/src/constants/strings.dart'; +import 'package:sirenapp_flutter_inbox/src/theme/app_colors.dart'; +import 'package:sirenapp_flutter_inbox/src/theme/app_theme.dart'; class EmptyWidget extends StatelessWidget { - const EmptyWidget({super.key}); + const EmptyWidget({ + this.isDarkMode, + super.key, + }); + + final bool? isDarkMode; @override Widget build(BuildContext context) { - final currentTheme = Theme.of(context); + final colors = SirenAppTheme.colors(isDarkMode: isDarkMode ?? false); return Center( child: Padding( @@ -16,7 +23,7 @@ class EmptyWidget extends StatelessWidget { child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ - _buildCircle(currentTheme), + _buildCircle(colors), const SizedBox( height: 10, ), @@ -25,7 +32,7 @@ class EmptyWidget extends StatelessWidget { style: TextStyle( fontSize: 14, fontWeight: FontWeight.w600, - color: currentTheme.colorScheme.tertiary, + color: colors.emptyScreenTitle, ), ), const SizedBox( @@ -36,7 +43,7 @@ class EmptyWidget extends StatelessWidget { style: TextStyle( fontSize: 12, fontWeight: FontWeight.w400, - color: currentTheme.colorScheme.outline, + color: colors.emptyScreenDescription, ), textAlign: TextAlign.center, ), @@ -47,7 +54,7 @@ class EmptyWidget extends StatelessWidget { } } -Widget _buildCircle(ThemeData theme) { +Widget _buildCircle(AppColors colors) { return Stack( alignment: Alignment.center, children: [ @@ -56,13 +63,13 @@ Widget _buildCircle(ThemeData theme) { height: 160, decoration: BoxDecoration( shape: BoxShape.circle, - color: theme.colorScheme.primaryContainer, + color: colors.emptyWidgetBackground, ), ), Icon( Icons.notifications, size: 84, - color: theme.colorScheme.shadow, + color: colors.emptyWidgetIconColor, ), Positioned( right: 50, @@ -70,10 +77,10 @@ Widget _buildCircle(ThemeData theme) { child: Container( padding: const EdgeInsets.all(8), decoration: BoxDecoration( - color: theme.colorScheme.surface, + color: colors.notificationIconColor, shape: BoxShape.circle, border: Border.all( - color: theme.colorScheme.primaryContainer, + color: colors.emptyWidgetBorderColor, width: 3, ), ), diff --git a/lib/src/widgets/error_widget.dart b/lib/src/widgets/error_widget.dart index 30252db..7d01733 100644 --- a/lib/src/widgets/error_widget.dart +++ b/lib/src/widgets/error_widget.dart @@ -1,14 +1,19 @@ import 'package:flutter/material.dart'; import 'package:sirenapp_flutter_inbox/src/constants/strings.dart'; +import 'package:sirenapp_flutter_inbox/src/theme/app_colors.dart'; +import 'package:sirenapp_flutter_inbox/src/theme/app_theme.dart'; class DefaultErrorWidget extends StatelessWidget { const DefaultErrorWidget({ + this.isDarkMode, super.key, }); + final bool? isDarkMode; + @override Widget build(BuildContext context) { - final currentTheme = Theme.of(context); + final colors = SirenAppTheme.colors(isDarkMode: isDarkMode ?? false); return Center( child: Padding( @@ -18,7 +23,7 @@ class DefaultErrorWidget extends StatelessWidget { child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ - _buildCircle(currentTheme), + _buildCircle(colors), const SizedBox( height: 10, ), @@ -27,7 +32,7 @@ class DefaultErrorWidget extends StatelessWidget { style: TextStyle( fontSize: 14, fontWeight: FontWeight.w600, - color: currentTheme.colorScheme.tertiary, + color: colors.errorWidgetText1, ), ), const SizedBox( @@ -38,7 +43,7 @@ class DefaultErrorWidget extends StatelessWidget { style: TextStyle( fontSize: 12, fontWeight: FontWeight.w400, - color: currentTheme.colorScheme.outline, + color: colors.errorWidgetText2, ), textAlign: TextAlign.center, ), @@ -49,18 +54,18 @@ class DefaultErrorWidget extends StatelessWidget { } } -Widget _buildCircle(ThemeData theme) { +Widget _buildCircle(AppColors colors) { return Container( width: 160, height: 160, decoration: BoxDecoration( shape: BoxShape.circle, - color: theme.colorScheme.primaryContainer, + color: colors.errorWidgetIconContainer, ), child: Icon( Icons.warning_rounded, size: 84, - color: theme.colorScheme.shadow, + color: colors.errorWidgetIconColor, ), ); } diff --git a/lib/src/widgets/icon_badge.dart b/lib/src/widgets/icon_badge.dart index 17ee846..e5a3db6 100644 --- a/lib/src/widgets/icon_badge.dart +++ b/lib/src/widgets/icon_badge.dart @@ -6,16 +6,19 @@ class IconBadge extends StatelessWidget { required this.badgeStyle, required this.notificationsCount, required this.hideBadge, + required this.badgeBackgroundColor, + required this.color, super.key, }); final BadgeStyle? badgeStyle; final int notificationsCount; final bool hideBadge; + final Color badgeBackgroundColor; + final Color color; @override Widget build(BuildContext context) { - final currentTheme = Theme.of(context); return hideBadge ? const SizedBox() : Positioned( @@ -29,8 +32,7 @@ class IconBadge extends StatelessWidget { ), decoration: BoxDecoration( shape: BoxShape.circle, - color: currentTheme.badgeTheme.backgroundColor ?? - currentTheme.colorScheme.tertiaryContainer, + color: badgeBackgroundColor, ), child: Align( child: Text( @@ -38,8 +40,7 @@ class IconBadge extends StatelessWidget { ? '99+' : notificationsCount.toString(), style: TextStyle( - color: currentTheme.badgeTheme.textColor ?? - currentTheme.colorScheme.onInverseSurface, + color: color, fontSize: badgeStyle?.fontSize ?? DefaultIconStyle.defaultFontSize, ), diff --git a/lib/src/widgets/inbox_body.dart b/lib/src/widgets/inbox_body.dart index fd41397..b5d10f1 100644 --- a/lib/src/widgets/inbox_body.dart +++ b/lib/src/widgets/inbox_body.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:sirenapp_flutter_inbox/sirenapp_flutter_inbox.dart'; +import 'package:sirenapp_flutter_inbox/src/theme/app_theme.dart'; import 'package:sirenapp_flutter_inbox/src/widgets/empty_widget.dart'; import 'package:sirenapp_flutter_inbox/src/widgets/error_widget.dart'; import 'package:sirenapp_flutter_inbox/src/widgets/loader_widget.dart'; @@ -7,7 +8,6 @@ import 'package:sirenapp_flutter_inbox/src/widgets/notification_list_view.dart'; class InboxBody extends StatelessWidget { const InboxBody({ - required this.currentTheme, required this.isLoading, required this.loadingNextPage, required this.isError, @@ -18,7 +18,6 @@ class InboxBody extends StatelessWidget { required this.onCardClick, required this.deletingNotificationId, required this.disableAutoMarkAsRead, - required this.totalElements, required this.onRefresh, required this.endReached, required this.onEndReached, @@ -28,9 +27,11 @@ class InboxBody extends StatelessWidget { this.customStyles, this.cardParams, this.listEmptyWidget, + this.colors, + this.isDarkMode, super.key, }); - final ThemeData currentTheme; + final CustomThemeColors? colors; final bool isLoading; final bool loadingNextPage; final bool isError; @@ -41,7 +42,6 @@ class InboxBody extends StatelessWidget { final void Function(NotificationType)? onCardClick; final String? deletingNotificationId; final bool disableAutoMarkAsRead; - final int totalElements; final Future Function() onRefresh; final Widget? customErrorWidget; final Widget? customLoader; @@ -51,13 +51,15 @@ class InboxBody extends StatelessWidget { final VoidCallback onEndReached; final ScrollController scrollController; final Widget? listEmptyWidget; + final bool? isDarkMode; @override Widget build(BuildContext context) { + final defaultColors = SirenAppTheme.colors(isDarkMode: isDarkMode ?? false); if (isError) { return RefreshIndicator( - color: currentTheme.colorScheme.onTertiary, - backgroundColor: currentTheme.colorScheme.primary, + color: colors?.loaderColor ?? defaultColors.loadingIndicator, + backgroundColor: defaultColors.loadingIndicatorBackground, onRefresh: onRefresh, child: ListView( physics: const AlwaysScrollableScrollPhysics(), @@ -70,7 +72,10 @@ class InboxBody extends StatelessWidget { height: MediaQuery.of(context).size.height * 0.75, width: MediaQuery.of(context).size.width, child: Center( - child: customErrorWidget ?? const DefaultErrorWidget(), + child: customErrorWidget ?? + DefaultErrorWidget( + isDarkMode: isDarkMode, + ), ), ), ), @@ -81,35 +86,38 @@ class InboxBody extends StatelessWidget { return LoaderWidget( customLoader: customLoader, hideAvatar: cardParams?.hideAvatar ?? false, + isDarkMode: isDarkMode, ); } else if (notifications.isEmpty) { return Semantics( label: 'siren-empty-state', hint: 'Empty notification list', key: const Key('siren-empty-state'), - child: listEmptyWidget ?? const EmptyWidget(), + child: listEmptyWidget ?? EmptyWidget(isDarkMode: isDarkMode), ); } else { return Container( decoration: customStyles?.container?.decoration, padding: customStyles?.container?.padding, child: NotificationListView( - notifications: notifications, - isLoading: isLoading, + isDarkMode: isDarkMode, + cardParams: cardParams, + colors: colors, + customCard: customCard, + customStyles: customStyles, + deletingNotificationId: deletingNotificationId, endReached: endReached, - onRefresh: onRefresh, - onEndReached: onEndReached, + isLoading: isLoading, loadingNextPage: loadingNextPage, - customStyles: customStyles, - scrollController: scrollController, - onDelete: deleteNotification, markAsRead: markAsRead, - customCard: customCard, + notifications: notifications, onCardClick: onCardClick, - deletingNotificationId: deletingNotificationId, - totalElements: totalElements, - cardParams: cardParams, - loadingIndicator: currentTheme.colorScheme.onTertiary, + onDelete: deleteNotification, + onEndReached: onEndReached, + onRefresh: onRefresh, + scrollController: scrollController, + loadingIndicator: + colors?.loaderColor ?? defaultColors.loadingIndicator, ), ); } diff --git a/lib/src/widgets/loader_widget.dart b/lib/src/widgets/loader_widget.dart index 96fda86..c31091b 100644 --- a/lib/src/widgets/loader_widget.dart +++ b/lib/src/widgets/loader_widget.dart @@ -1,25 +1,43 @@ import 'package:flutter/material.dart'; import 'package:sirenapp_flutter_inbox/src/constants/generics.dart'; +import 'package:sirenapp_flutter_inbox/src/theme/app_theme.dart'; class LoaderWidget extends StatelessWidget { const LoaderWidget({ required this.hideAvatar, - super.key, this.customLoader, + this.isDarkMode, + super.key, }); final Widget? customLoader; final bool hideAvatar; + final bool? isDarkMode; @override Widget build(BuildContext context) { + final defaultColors = SirenAppTheme.colors(isDarkMode: isDarkMode ?? false); return customLoader ?? ListView.builder( itemCount: Generics.PAGE_SIZE, itemBuilder: (context, index) { return Padding( padding: const EdgeInsets.symmetric(vertical: 8), - child: CardLoaderWidget(hideAvatar: hideAvatar), + child: Container( + padding: const EdgeInsets.only(bottom: 5), + decoration: BoxDecoration( + border: Border( + bottom: BorderSide( + color: defaultColors.cardBorderColor, + width: 0.25, + ), + ), + ), + child: CardLoaderWidget( + hideAvatar: hideAvatar, + isDarkMode: isDarkMode, + ), + ), ); }, ); @@ -29,10 +47,12 @@ class LoaderWidget extends StatelessWidget { class CardLoaderWidget extends StatefulWidget { const CardLoaderWidget({ required this.hideAvatar, + this.isDarkMode, super.key, }); final bool hideAvatar; + final bool? isDarkMode; @override CardLoaderWidgetState createState() => CardLoaderWidgetState(); @@ -59,7 +79,8 @@ class CardLoaderWidgetState extends State @override Widget build(BuildContext context) { - final currentTheme = Theme.of(context); + final defaultColors = + SirenAppTheme.colors(isDarkMode: widget.isDarkMode ?? false); return Container( decoration: BoxDecoration( borderRadius: BorderRadius.circular(8), @@ -72,13 +93,12 @@ class CardLoaderWidgetState extends State Padding( padding: const EdgeInsets.only(right: 6, left: 6), child: _buildAnimatedWidget( - theme: currentTheme, builder: (context, child) => Container( width: 42, height: 42, decoration: BoxDecoration( shape: BoxShape.circle, - color: currentTheme.colorScheme.onSecondary + color: defaultColors.skeletonLoaderColor .withOpacity(0.5 + 0.5 * _controller.value), ), ), @@ -93,11 +113,10 @@ class CardLoaderWidgetState extends State crossAxisAlignment: CrossAxisAlignment.start, children: [ _buildAnimatedWidget( - theme: currentTheme, builder: (context, child) => Container( height: 18, decoration: BoxDecoration( - color: currentTheme.colorScheme.onSecondary + color: defaultColors.skeletonLoaderColor .withOpacity(0.5 + 0.5 * _controller.value), borderRadius: BorderRadius.circular(4), ), @@ -105,11 +124,10 @@ class CardLoaderWidgetState extends State ), const SizedBox(height: 12), _buildAnimatedWidget( - theme: currentTheme, builder: (context, child) => Container( height: 18, decoration: BoxDecoration( - color: currentTheme.colorScheme.onSecondary + color: defaultColors.skeletonLoaderColor .withOpacity(0.5 + 0.5 * _controller.value), borderRadius: BorderRadius.circular(4), ), @@ -117,11 +135,10 @@ class CardLoaderWidgetState extends State ), const SizedBox(height: 12), _buildAnimatedWidget( - theme: currentTheme, builder: (context, child) => Container( height: 18, decoration: BoxDecoration( - color: currentTheme.colorScheme.onSecondary + color: defaultColors.skeletonLoaderColor .withOpacity(0.5 + 0.5 * _controller.value), borderRadius: BorderRadius.circular(4), ), @@ -131,13 +148,12 @@ class CardLoaderWidgetState extends State Row( children: [ _buildAnimatedWidget( - theme: currentTheme, builder: (context, child) => Container( width: 10, height: 10, decoration: BoxDecoration( shape: BoxShape.circle, - color: currentTheme.colorScheme.onSecondary + color: defaultColors.skeletonLoaderColor .withOpacity(0.5 + 0.5 * _controller.value), ), ), @@ -145,11 +161,10 @@ class CardLoaderWidgetState extends State const SizedBox(width: 12), Expanded( child: _buildAnimatedWidget( - theme: currentTheme, builder: (context, child) => Container( height: 12, decoration: BoxDecoration( - color: currentTheme.colorScheme.onSecondary + color: defaultColors.skeletonLoaderColor .withOpacity(0.5 + 0.5 * _controller.value), borderRadius: BorderRadius.circular(4), ), @@ -163,12 +178,11 @@ class CardLoaderWidgetState extends State ), ), _buildAnimatedWidget( - theme: currentTheme, builder: (context, child) => Container( width: 16, height: 16, decoration: BoxDecoration( - color: currentTheme.colorScheme.onSecondary + color: defaultColors.skeletonLoaderColor .withOpacity(0.5 + 0.5 * _controller.value), borderRadius: BorderRadius.circular(4), ), @@ -180,7 +194,6 @@ class CardLoaderWidgetState extends State } Widget _buildAnimatedWidget({ - required ThemeData theme, required Widget Function(BuildContext, Widget?) builder, }) { return Padding( diff --git a/lib/src/widgets/notification_list_view.dart b/lib/src/widgets/notification_list_view.dart index 9f073d0..ba2f922 100644 --- a/lib/src/widgets/notification_list_view.dart +++ b/lib/src/widgets/notification_list_view.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:sirenapp_flutter_inbox/sirenapp_flutter_inbox.dart'; +import 'package:sirenapp_flutter_inbox/src/theme/app_theme.dart'; import 'package:sirenapp_flutter_inbox/src/widgets/card.dart'; class NotificationListView extends StatefulWidget { @@ -17,9 +18,10 @@ class NotificationListView extends StatefulWidget { this.customCard, this.onCardClick, this.deletingNotificationId, - this.totalElements, this.cardParams, this.loadingIndicator, + this.isDarkMode, + this.colors, super.key, }); @@ -36,9 +38,10 @@ class NotificationListView extends StatefulWidget { final Widget Function(NotificationType)? customCard; final void Function(NotificationType)? onCardClick; final String? deletingNotificationId; - final int? totalElements; final CardParams? cardParams; final Color? loadingIndicator; + final bool? isDarkMode; + final CustomThemeColors? colors; @override State createState() => _NotificationListViewState(); @@ -53,7 +56,7 @@ class _NotificationListViewState extends State { super.initState(); } - void _afterLayout(_) { + void _afterLayout(dynamic _) { _getPositions(); } @@ -71,9 +74,11 @@ class _NotificationListViewState extends State { @override Widget build(BuildContext context) { + final defaultColors = + SirenAppTheme.colors(isDarkMode: widget.isDarkMode ?? false); return RefreshIndicator( - color: widget.loadingIndicator ?? Theme.of(context).colorScheme.secondary, - backgroundColor: Theme.of(context).colorScheme.primary, + color: widget.colors?.loaderColor ?? defaultColors.loadingIndicator, + backgroundColor: defaultColors.loadingIndicatorBackground, onRefresh: widget.onRefresh, child: Semantics( label: 'siren-notification-list', @@ -87,7 +92,7 @@ class _NotificationListViewState extends State { final currentNotification = widget.notifications[index]; final itemWidget = widget.customCard?.call(currentNotification) ?? CardWidget( - onTap: (notification) { + onTap: (NotificationType notification) { if (!(widget.cardParams?.disableAutoMarkAsRead ?? false)) { widget.markAsRead(currentNotification.id); @@ -98,6 +103,8 @@ class _NotificationListViewState extends State { cardParams: widget.cardParams ?? const CardParams(), styles: widget.customStyles, onDelete: widget.onDelete, + isDarkMode: widget.isDarkMode, + colors: widget.colors, ); return AnimatedOpacity( key: isLastIndex @@ -116,7 +123,7 @@ class _NotificationListViewState extends State { child: Center( child: CircularProgressIndicator( color: widget.loadingIndicator ?? - Theme.of(context).colorScheme.secondary, + defaultColors.loaderColor, ), ), ) diff --git a/lib/src/widgets/siren_inbox.dart b/lib/src/widgets/siren_inbox.dart index 8d2c594..a4a9674 100644 --- a/lib/src/widgets/siren_inbox.dart +++ b/lib/src/widgets/siren_inbox.dart @@ -63,7 +63,7 @@ class SirenInbox extends StatefulWidget { /// Callback function for handling errors. final void Function(SirenErrorType)? onError; - /// Custom theme colors for the inbox, this focuses on the idea of colorSchemes in flutter theme. + /// Custom theme colors for the inbox. final CustomThemeColors? theme; /// Custom styles for the card of each notification. @@ -75,11 +75,10 @@ class SirenInbox extends StatefulWidget { class _SirenInboxState extends State { bool isLoading = true; - bool endReached = false; + bool isEndReached = false; bool isError = false; bool loadingNextPage = false; int currentPage = 0; - int totalElements = 0; String? deletingNotificationId; int pageSize = 20; @@ -172,8 +171,7 @@ class _SirenInboxState extends State { setState(() { isLoading = true; notifications = []; - endReached = false; - totalElements = 0; + isEndReached = false; currentPage = 0; }); } @@ -210,7 +208,6 @@ class _SirenInboxState extends State { setState(() { notifications .removeWhere((notification) => notification.id == notificationId); - totalElements = totalElements - 1; }); } } @@ -219,7 +216,6 @@ class _SirenInboxState extends State { if (mounted) { setState(() { notifications = []; - totalElements = 0; }); } } @@ -248,7 +244,9 @@ class _SirenInboxState extends State { : null, ); if (fetchedNotifications.isSuccess) { - if ((fetchedNotifications.meta?.totalElements ?? 0) > 0) { + final count = + (fetchedNotifications.data as Iterable).length; + if (count > 0) { unawaited(markAllNotificationsAsViewed()); newNotifications.addAll( fetchedNotifications.data as Iterable, @@ -270,8 +268,6 @@ class _SirenInboxState extends State { ); } } - totalElements = - totalElements + (fetchedNotifications.meta?.totalElements ?? 0); newNotifications = []; } } else if (fetchedNotifications.isError) { @@ -315,7 +311,6 @@ class _SirenInboxState extends State { ); isLoading = false; isError = false; - totalElements = fetchedNotifications.meta?.totalElements ?? 0; }); fetchNewNotifications(); } else if (fetchedNotifications.isError) { @@ -374,7 +369,7 @@ class _SirenInboxState extends State { }); } - await Future.delayed(const Duration(milliseconds: 500)); + await Future.delayed(const Duration(milliseconds: 500)); SirenDataProvider.instance.inboxController.sink.add( StreamResponse( deletionStatus, @@ -386,11 +381,10 @@ class _SirenInboxState extends State { setState(() { deletingNotificationId = null; _deleteById(id); - totalElements = totalElements - 1; }); } if (notifications.length < pageSize && - notifications.length < totalElements) { + notifications.length < Generics.AVERAGE_ITEMS_ON_SCREEN) { onEndReached(); } } else if (deletionStatus.isError) { @@ -399,9 +393,7 @@ class _SirenInboxState extends State { } void onEndReached() { - if (!isLoading && - !loadingNextPage && - totalElements > notifications.length) { + if (!isLoading && !loadingNextPage && !isEndReached) { if (mounted) { setState(() { loadingNextPage = true; @@ -417,6 +409,8 @@ class _SirenInboxState extends State { size: pageSize, ); if (fetchedNotifications.isSuccess) { + final count = + (fetchedNotifications.data as Iterable).length; if (mounted) { setState(() { notifications.addAll( @@ -424,7 +418,7 @@ class _SirenInboxState extends State { ); isLoading = false; loadingNextPage = false; - endReached = totalElements == notifications.length; + isEndReached = count < pageSize; }); } } else if (fetchedNotifications.isError) { @@ -458,53 +452,42 @@ class _SirenInboxState extends State { @override Widget build(BuildContext context) { - return Theme( - data: widget.theme != null - ? AppTheme.customTheme( - widget.theme!, - isDarkMode: widget.darkMode ?? false, - ) - : (widget.darkMode ?? false - ? AppTheme.darkTheme - : AppTheme.lightTheme), - child: Builder( - builder: (context) { - final currentTheme = Theme.of(context); - return Scaffold( - backgroundColor: currentTheme.colorScheme.primary, - appBar: SirenAppBar( - theme: currentTheme, - onClearAllPressed: onBulkDelete, - isNonEmptyNotifications: shouldShowClearAllButton(), - headerParams: widget.headerParams, - styles: widget.customStyles, - ), - body: InboxBody( - currentTheme: currentTheme, - isLoading: isLoading, - loadingNextPage: loadingNextPage, - isError: isError, - notifications: notifications, - deleteNotification: deleteNotification, - markAsRead: _markNotificationAsRead, - customCard: widget.customCard, - onCardClick: widget.onCardClick, - deletingNotificationId: deletingNotificationId, - disableAutoMarkAsRead: - widget.cardParams?.disableAutoMarkAsRead ?? false, - totalElements: totalElements, - onRefresh: onRefresh, - customErrorWidget: widget.customErrorWidget, - customLoader: widget.customLoader, - endReached: endReached, - customStyles: widget.customStyles, - cardParams: widget.cardParams, - scrollController: _scrollController, - onEndReached: onEndReached, - listEmptyWidget: widget.listEmptyWidget, - ), - ); - }, + final colors = SirenAppTheme.colors(isDarkMode: widget.darkMode ?? false); + + return Scaffold( + backgroundColor: + widget.theme?.backgroundColor ?? colors.scaffoldBackgroundColor, + appBar: SirenAppBar( + colors: widget.theme, + isDarkMode: widget.darkMode, + onClearAllPressed: onBulkDelete, + isNonEmptyNotifications: shouldShowClearAllButton(), + headerParams: widget.headerParams, + styles: widget.customStyles, + ), + body: InboxBody( + cardParams: widget.cardParams, + colors: widget.theme, + customCard: widget.customCard, + customErrorWidget: widget.customErrorWidget, + customLoader: widget.customLoader, + customStyles: widget.customStyles, + deleteNotification: deleteNotification, + deletingNotificationId: deletingNotificationId, + disableAutoMarkAsRead: + widget.cardParams?.disableAutoMarkAsRead ?? false, + endReached: isEndReached, + isDarkMode: widget.darkMode, + isError: isError, + isLoading: isLoading, + listEmptyWidget: widget.listEmptyWidget, + loadingNextPage: loadingNextPage, + markAsRead: _markNotificationAsRead, + notifications: notifications, + onCardClick: widget.onCardClick, + onEndReached: onEndReached, + onRefresh: onRefresh, + scrollController: _scrollController, ), ); } diff --git a/lib/src/widgets/siren_inbox_icon.dart b/lib/src/widgets/siren_inbox_icon.dart index a79bdd0..9be2a7c 100644 --- a/lib/src/widgets/siren_inbox_icon.dart +++ b/lib/src/widgets/siren_inbox_icon.dart @@ -168,64 +168,57 @@ class _SirenInboxIconState extends State { @override Widget build(BuildContext context) { - return Theme( - data: widget.theme != null - ? AppTheme.customTheme( - widget.theme!, - isDarkMode: widget.darkMode, - ) - : (widget.darkMode ? AppTheme.darkTheme : AppTheme.lightTheme), - child: Builder( - builder: (context) { - final size = widget.customStyles?.notificationIconStyle?.size ?? - DefaultIconStyle.iconSize; - final currentTheme = Theme.of(context); - return IgnorePointer( - ignoring: widget.disabled, - child: GestureDetector( - onTap: () { - if (!_processingGesture && mounted) { - setState(() { - _processingGesture = true; - }); - if (widget.onTap != null) { - widget.onTap?.call(); - } - } - Future.delayed(const Duration(milliseconds: 500), () { - setState(() { - _processingGesture = false; - }); - }); - }, - child: Stack( - children: [ - Semantics( - label: 'siren-notification-icon', - hint: 'Tap to view notifications', - child: SizedBox( - key: const Key('siren-notification-icon'), - width: size, - height: size, - child: widget.notificationIcon ?? - Icon( - Icons.notifications_none_outlined, - size: size, - color: currentTheme.colorScheme.onPrimary, - ), + final size = widget.customStyles?.notificationIconStyle?.size ?? + DefaultIconStyle.iconSize; + final colors = SirenAppTheme.colors(isDarkMode: widget.darkMode); + return IgnorePointer( + ignoring: widget.disabled, + child: GestureDetector( + onTap: () { + if (!_processingGesture && mounted) { + setState(() { + _processingGesture = true; + }); + if (widget.onTap != null) { + widget.onTap?.call(); + } + } + Future.delayed(const Duration(milliseconds: 500), () { + setState(() { + _processingGesture = false; + }); + }); + }, + child: Stack( + children: [ + Semantics( + label: 'siren-notification-icon', + hint: 'Tap to view notifications', + child: SizedBox( + key: const Key('siren-notification-icon'), + width: size, + height: size, + child: widget.notificationIcon ?? + Icon( + Icons.notifications_none_outlined, + size: size, + color: widget.theme?.notificationIconColor ?? + colors.notificationIconColor, ), - ), - IconBadge( - hideBadge: - _notificationsCount == 0 || (widget.hideBadge ?? false), - badgeStyle: widget.customStyles?.badgeStyle, - notificationsCount: _notificationsCount, - ), - ], ), ), - ); - }, + IconBadge( + hideBadge: + _notificationsCount == 0 || (widget.hideBadge ?? false), + badgeStyle: widget.customStyles?.badgeStyle, + notificationsCount: _notificationsCount, + badgeBackgroundColor: + widget.theme?.badgeColors?.backgroundColor ?? + colors.badgeBackgroundColor, + color: widget.theme?.badgeColors?.color ?? colors.badgeTextColor, + ), + ], + ), ), ); } diff --git a/pubspec.yaml b/pubspec.yaml index 9d08281..045c036 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -11,8 +11,8 @@ keywords: - notifications environment: - sdk: '>=2.18.0 <4.0.0' - flutter: ">=1.17.0" + sdk: '>=2.17.0 <4.0.0' + flutter: ">=3.0.0" dependencies: flutter: @@ -23,11 +23,11 @@ dev_dependencies: flutter_test: sdk: flutter flutter_lints: ^2.0.0 - very_good_analysis: ^5.1.0 + very_good_analysis: ^2.4.0 husky: ^0.1.7 - mockito: ^5.1.0 + mockito: ^5.0.0 network_image_mock: ^2.0.1 - build_runner: ^2.1.11 + build_runner: ^2.0.0 # For information on the generic Dart part of this file, see the # following page: https://dart.dev/tools/pub/pubspec diff --git a/test/data/siren_data_provider_test.dart b/test/data/siren_data_provider_test.dart index 90d7fbc..2b2fe6d 100644 --- a/test/data/siren_data_provider_test.dart +++ b/test/data/siren_data_provider_test.dart @@ -52,7 +52,7 @@ void main() { sirenDataProvider.updateParams(userToken: 'token', recipientId: '123'); - await Future.delayed( + await Future.delayed( const Duration(seconds: Generics.DATA_FETCH_INTERVAL) * (Generics.MAX_RETRIES + 1), ); diff --git a/test/data/siren_data_provider_test.mocks.dart b/test/data/siren_data_provider_test.mocks.dart index f4a1d01..54dc9b7 100644 --- a/test/data/siren_data_provider_test.mocks.dart +++ b/test/data/siren_data_provider_test.mocks.dart @@ -1,4 +1,4 @@ -// Mocks generated by Mockito 5.4.4 from annotations +// Mocks generated by Mockito 5.3.2 from annotations // in sirenapp_flutter_inbox/test/data/siren_data_provider_test.dart. // Do not manually edit this file. @@ -15,8 +15,6 @@ import 'package:sirenapp_flutter_inbox/src/services/api_client.dart' as _i2; // ignore_for_file: avoid_redundant_argument_values // ignore_for_file: avoid_setters_without_getters // ignore_for_file: comment_references -// ignore_for_file: deprecated_member_use -// ignore_for_file: deprecated_member_use_from_same_package // ignore_for_file: implementation_imports // ignore_for_file: invalid_use_of_visible_for_testing_member // ignore_for_file: prefer_const_constructors @@ -60,7 +58,6 @@ class MockVerifyToken extends _i1.Mock implements _i4.VerifyToken { Invocation.getter(#api), ), ) as _i2.ApiClient); - @override set api(_i2.ApiClient? _api) => super.noSuchMethod( Invocation.setter( @@ -69,7 +66,6 @@ class MockVerifyToken extends _i1.Mock implements _i4.VerifyToken { ), returnValueForMissingStub: null, ); - @override _i5.Status convertJsonToVerificationStatus(dynamic response) => (super.noSuchMethod( @@ -80,7 +76,6 @@ class MockVerifyToken extends _i1.Mock implements _i4.VerifyToken { returnValue: _i5.Status.PENDING, returnValueForMissingStub: _i5.Status.PENDING, ) as _i5.Status); - @override _i6.Future<_i3.ApiResponse> verifyToken() => (super.noSuchMethod( Invocation.method( diff --git a/test/models/api_response_test.dart b/test/models/api_response_test.dart index 5532d1b..a1a3314 100644 --- a/test/models/api_response_test.dart +++ b/test/models/api_response_test.dart @@ -13,7 +13,6 @@ void main() { 'pageSize': '10', 'currentPage': '1', 'first': 'first', - 'totalElements': '50', }, }; final response = ApiResponse.fromJson(json); @@ -41,7 +40,6 @@ void main() { 'pageSize': '10', 'currentPage': '1', 'first': 'first', - 'totalElements': '50', }; final meta = MetaResponse.fromJson(json); @@ -50,7 +48,6 @@ void main() { expect(meta.pageSize, 10); expect(meta.currentPage, 1); expect(meta.first, 'first'); - expect(meta.totalElements, 50); }); }); diff --git a/test/services/api_client_test.dart b/test/services/api_client_test.dart index 6bd53fa..8a3545c 100644 --- a/test/services/api_client_test.dart +++ b/test/services/api_client_test.dart @@ -1,3 +1,5 @@ +// ignore_for_file: avoid_redundant_argument_values + import 'package:dio/dio.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:mockito/annotations.dart'; @@ -26,6 +28,7 @@ void main() { test('Test server error ', () { final apiClient = ApiClient(Dio()); final response = Response( + data: null, statusCode: 500, requestOptions: RequestOptions(), ); diff --git a/test/services/api_client_test.mocks.dart b/test/services/api_client_test.mocks.dart index d32fc16..888d18f 100644 --- a/test/services/api_client_test.mocks.dart +++ b/test/services/api_client_test.mocks.dart @@ -1,4 +1,4 @@ -// Mocks generated by Mockito 5.4.4 from annotations +// Mocks generated by Mockito 5.3.2 from annotations // in sirenapp_flutter_inbox/test/services/api_client_test.dart. // Do not manually edit this file. @@ -6,25 +6,22 @@ import 'dart:async' as _i7; import 'package:dio/src/adapter.dart' as _i3; -import 'package:dio/src/cancel_token.dart' as _i9; -import 'package:dio/src/dio.dart' as _i8; +import 'package:dio/src/cancel_token.dart' as _i10; +import 'package:dio/src/dio.dart' as _i9; import 'package:dio/src/dio_mixin.dart' as _i5; import 'package:dio/src/options.dart' as _i2; import 'package:dio/src/response.dart' as _i6; import 'package:dio/src/transformer.dart' as _i4; import 'package:mockito/mockito.dart' as _i1; -import 'package:mockito/src/dummies.dart' as _i11; -import 'package:sirenapp_flutter_inbox/sirenapp_flutter_inbox.dart' as _i12; -import 'package:sirenapp_flutter_inbox/src/constants/generics.dart' as _i13; +import 'package:sirenapp_flutter_inbox/sirenapp_flutter_inbox.dart' as _i8; +import 'package:sirenapp_flutter_inbox/src/constants/generics.dart' as _i12; import 'package:sirenapp_flutter_inbox/src/data/siren_data_provider.dart' - as _i10; + as _i11; // ignore_for_file: type=lint // ignore_for_file: avoid_redundant_argument_values // ignore_for_file: avoid_setters_without_getters // ignore_for_file: comment_references -// ignore_for_file: deprecated_member_use -// ignore_for_file: deprecated_member_use_from_same_package // ignore_for_file: implementation_imports // ignore_for_file: invalid_use_of_visible_for_testing_member // ignore_for_file: prefer_const_constructors @@ -73,7 +70,7 @@ class _FakeInterceptors_3 extends _i1.SmartFake implements _i5.Interceptors { ); } -class _FakeResponse_4 extends _i1.SmartFake implements _i6.Response { +class _FakeResponse_4 extends _i1.SmartFake implements _i6.Response { _FakeResponse_4( Object parent, Invocation parentInvocation, @@ -94,10 +91,21 @@ class _FakeStreamController_5 extends _i1.SmartFake ); } +class _FakeSirenErrorType_6 extends _i1.SmartFake + implements _i8.SirenErrorType { + _FakeSirenErrorType_6( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} + /// A class which mocks [Dio]. /// /// See the documentation for Mockito's code generation for more information. -class MockDio extends _i1.Mock implements _i8.Dio { +class MockDio extends _i1.Mock implements _i9.Dio { @override _i2.BaseOptions get options => (super.noSuchMethod( Invocation.getter(#options), @@ -110,7 +118,6 @@ class MockDio extends _i1.Mock implements _i8.Dio { Invocation.getter(#options), ), ) as _i2.BaseOptions); - @override set options(_i2.BaseOptions? _options) => super.noSuchMethod( Invocation.setter( @@ -119,7 +126,6 @@ class MockDio extends _i1.Mock implements _i8.Dio { ), returnValueForMissingStub: null, ); - @override _i3.HttpClientAdapter get httpClientAdapter => (super.noSuchMethod( Invocation.getter(#httpClientAdapter), @@ -132,7 +138,6 @@ class MockDio extends _i1.Mock implements _i8.Dio { Invocation.getter(#httpClientAdapter), ), ) as _i3.HttpClientAdapter); - @override set httpClientAdapter(_i3.HttpClientAdapter? _httpClientAdapter) => super.noSuchMethod( @@ -142,7 +147,6 @@ class MockDio extends _i1.Mock implements _i8.Dio { ), returnValueForMissingStub: null, ); - @override _i4.Transformer get transformer => (super.noSuchMethod( Invocation.getter(#transformer), @@ -155,7 +159,6 @@ class MockDio extends _i1.Mock implements _i8.Dio { Invocation.getter(#transformer), ), ) as _i4.Transformer); - @override set transformer(_i4.Transformer? _transformer) => super.noSuchMethod( Invocation.setter( @@ -164,7 +167,6 @@ class MockDio extends _i1.Mock implements _i8.Dio { ), returnValueForMissingStub: null, ); - @override _i5.Interceptors get interceptors => (super.noSuchMethod( Invocation.getter(#interceptors), @@ -177,7 +179,6 @@ class MockDio extends _i1.Mock implements _i8.Dio { Invocation.getter(#interceptors), ), ) as _i5.Interceptors); - @override void close({bool? force = false}) => super.noSuchMethod( Invocation.method( @@ -187,14 +188,13 @@ class MockDio extends _i1.Mock implements _i8.Dio { ), returnValueForMissingStub: null, ); - @override _i7.Future<_i6.Response> head( String? path, { Object? data, Map? queryParameters, _i2.Options? options, - _i9.CancelToken? cancelToken, + _i10.CancelToken? cancelToken, }) => (super.noSuchMethod( Invocation.method( @@ -235,13 +235,12 @@ class MockDio extends _i1.Mock implements _i8.Dio { ), )), ) as _i7.Future<_i6.Response>); - @override _i7.Future<_i6.Response> headUri( Uri? uri, { Object? data, _i2.Options? options, - _i9.CancelToken? cancelToken, + _i10.CancelToken? cancelToken, }) => (super.noSuchMethod( Invocation.method( @@ -279,14 +278,13 @@ class MockDio extends _i1.Mock implements _i8.Dio { ), )), ) as _i7.Future<_i6.Response>); - @override _i7.Future<_i6.Response> get( String? path, { Object? data, Map? queryParameters, _i2.Options? options, - _i9.CancelToken? cancelToken, + _i10.CancelToken? cancelToken, _i2.ProgressCallback? onReceiveProgress, }) => (super.noSuchMethod( @@ -331,13 +329,12 @@ class MockDio extends _i1.Mock implements _i8.Dio { ), )), ) as _i7.Future<_i6.Response>); - @override _i7.Future<_i6.Response> getUri( Uri? uri, { Object? data, _i2.Options? options, - _i9.CancelToken? cancelToken, + _i10.CancelToken? cancelToken, _i2.ProgressCallback? onReceiveProgress, }) => (super.noSuchMethod( @@ -379,14 +376,13 @@ class MockDio extends _i1.Mock implements _i8.Dio { ), )), ) as _i7.Future<_i6.Response>); - @override _i7.Future<_i6.Response> post( String? path, { Object? data, Map? queryParameters, _i2.Options? options, - _i9.CancelToken? cancelToken, + _i10.CancelToken? cancelToken, _i2.ProgressCallback? onSendProgress, _i2.ProgressCallback? onReceiveProgress, }) => @@ -435,13 +431,12 @@ class MockDio extends _i1.Mock implements _i8.Dio { ), )), ) as _i7.Future<_i6.Response>); - @override _i7.Future<_i6.Response> postUri( Uri? uri, { Object? data, _i2.Options? options, - _i9.CancelToken? cancelToken, + _i10.CancelToken? cancelToken, _i2.ProgressCallback? onSendProgress, _i2.ProgressCallback? onReceiveProgress, }) => @@ -487,14 +482,13 @@ class MockDio extends _i1.Mock implements _i8.Dio { ), )), ) as _i7.Future<_i6.Response>); - @override _i7.Future<_i6.Response> put( String? path, { Object? data, Map? queryParameters, _i2.Options? options, - _i9.CancelToken? cancelToken, + _i10.CancelToken? cancelToken, _i2.ProgressCallback? onSendProgress, _i2.ProgressCallback? onReceiveProgress, }) => @@ -543,13 +537,12 @@ class MockDio extends _i1.Mock implements _i8.Dio { ), )), ) as _i7.Future<_i6.Response>); - @override _i7.Future<_i6.Response> putUri( Uri? uri, { Object? data, _i2.Options? options, - _i9.CancelToken? cancelToken, + _i10.CancelToken? cancelToken, _i2.ProgressCallback? onSendProgress, _i2.ProgressCallback? onReceiveProgress, }) => @@ -595,14 +588,13 @@ class MockDio extends _i1.Mock implements _i8.Dio { ), )), ) as _i7.Future<_i6.Response>); - @override _i7.Future<_i6.Response> patch( String? path, { Object? data, Map? queryParameters, _i2.Options? options, - _i9.CancelToken? cancelToken, + _i10.CancelToken? cancelToken, _i2.ProgressCallback? onSendProgress, _i2.ProgressCallback? onReceiveProgress, }) => @@ -651,13 +643,12 @@ class MockDio extends _i1.Mock implements _i8.Dio { ), )), ) as _i7.Future<_i6.Response>); - @override _i7.Future<_i6.Response> patchUri( Uri? uri, { Object? data, _i2.Options? options, - _i9.CancelToken? cancelToken, + _i10.CancelToken? cancelToken, _i2.ProgressCallback? onSendProgress, _i2.ProgressCallback? onReceiveProgress, }) => @@ -703,14 +694,13 @@ class MockDio extends _i1.Mock implements _i8.Dio { ), )), ) as _i7.Future<_i6.Response>); - @override _i7.Future<_i6.Response> delete( String? path, { Object? data, Map? queryParameters, _i2.Options? options, - _i9.CancelToken? cancelToken, + _i10.CancelToken? cancelToken, }) => (super.noSuchMethod( Invocation.method( @@ -751,13 +741,12 @@ class MockDio extends _i1.Mock implements _i8.Dio { ), )), ) as _i7.Future<_i6.Response>); - @override _i7.Future<_i6.Response> deleteUri( Uri? uri, { Object? data, _i2.Options? options, - _i9.CancelToken? cancelToken, + _i10.CancelToken? cancelToken, }) => (super.noSuchMethod( Invocation.method( @@ -795,14 +784,13 @@ class MockDio extends _i1.Mock implements _i8.Dio { ), )), ) as _i7.Future<_i6.Response>); - @override _i7.Future<_i6.Response> download( String? urlPath, dynamic savePath, { _i2.ProgressCallback? onReceiveProgress, Map? queryParameters, - _i9.CancelToken? cancelToken, + _i10.CancelToken? cancelToken, bool? deleteOnError = true, String? lengthHeader = r'content-length', Object? data, @@ -866,13 +854,12 @@ class MockDio extends _i1.Mock implements _i8.Dio { ), )), ) as _i7.Future<_i6.Response>); - @override _i7.Future<_i6.Response> downloadUri( Uri? uri, dynamic savePath, { _i2.ProgressCallback? onReceiveProgress, - _i9.CancelToken? cancelToken, + _i10.CancelToken? cancelToken, bool? deleteOnError = true, String? lengthHeader = r'content-length', Object? data, @@ -933,13 +920,12 @@ class MockDio extends _i1.Mock implements _i8.Dio { ), )), ) as _i7.Future<_i6.Response>); - @override _i7.Future<_i6.Response> request( String? url, { Object? data, Map? queryParameters, - _i9.CancelToken? cancelToken, + _i10.CancelToken? cancelToken, _i2.Options? options, _i2.ProgressCallback? onSendProgress, _i2.ProgressCallback? onReceiveProgress, @@ -989,12 +975,11 @@ class MockDio extends _i1.Mock implements _i8.Dio { ), )), ) as _i7.Future<_i6.Response>); - @override _i7.Future<_i6.Response> requestUri( Uri? uri, { Object? data, - _i9.CancelToken? cancelToken, + _i10.CancelToken? cancelToken, _i2.Options? options, _i2.ProgressCallback? onSendProgress, _i2.ProgressCallback? onReceiveProgress, @@ -1041,7 +1026,6 @@ class MockDio extends _i1.Mock implements _i8.Dio { ), )), ) as _i7.Future<_i6.Response>); - @override _i7.Future<_i6.Response> fetch(_i2.RequestOptions? requestOptions) => (super.noSuchMethod( @@ -1070,20 +1054,13 @@ class MockDio extends _i1.Mock implements _i8.Dio { /// A class which mocks [SirenDataProvider]. /// /// See the documentation for Mockito's code generation for more information. -class MockSirenDataProvider extends _i1.Mock implements _i10.SirenDataProvider { +class MockSirenDataProvider extends _i1.Mock implements _i11.SirenDataProvider { @override String get userToken => (super.noSuchMethod( Invocation.getter(#userToken), - returnValue: _i11.dummyValue( - this, - Invocation.getter(#userToken), - ), - returnValueForMissingStub: _i11.dummyValue( - this, - Invocation.getter(#userToken), - ), + returnValue: '', + returnValueForMissingStub: '', ) as String); - @override set userToken(String? _userToken) => super.noSuchMethod( Invocation.setter( @@ -1092,20 +1069,12 @@ class MockSirenDataProvider extends _i1.Mock implements _i10.SirenDataProvider { ), returnValueForMissingStub: null, ); - @override String get recipientId => (super.noSuchMethod( Invocation.getter(#recipientId), - returnValue: _i11.dummyValue( - this, - Invocation.getter(#recipientId), - ), - returnValueForMissingStub: _i11.dummyValue( - this, - Invocation.getter(#recipientId), - ), + returnValue: '', + returnValueForMissingStub: '', ) as String); - @override set recipientId(String? _recipientId) => super.noSuchMethod( Invocation.setter( @@ -1114,20 +1083,12 @@ class MockSirenDataProvider extends _i1.Mock implements _i10.SirenDataProvider { ), returnValueForMissingStub: null, ); - @override String get apiDomain => (super.noSuchMethod( Invocation.getter(#apiDomain), - returnValue: _i11.dummyValue( - this, - Invocation.getter(#apiDomain), - ), - returnValueForMissingStub: _i11.dummyValue( - this, - Invocation.getter(#apiDomain), - ), + returnValue: '', + returnValueForMissingStub: '', ) as String); - @override set apiDomain(String? _apiDomain) => super.noSuchMethod( Invocation.setter( @@ -1136,42 +1097,44 @@ class MockSirenDataProvider extends _i1.Mock implements _i10.SirenDataProvider { ), returnValueForMissingStub: null, ); - @override - _i7.StreamController<_i12.StreamResponse> get inboxController => + _i7.StreamController<_i8.StreamResponse> get inboxController => (super.noSuchMethod( Invocation.getter(#inboxController), - returnValue: _FakeStreamController_5<_i12.StreamResponse>( + returnValue: _FakeStreamController_5<_i8.StreamResponse>( this, Invocation.getter(#inboxController), ), - returnValueForMissingStub: _FakeStreamController_5<_i12.StreamResponse>( + returnValueForMissingStub: _FakeStreamController_5<_i8.StreamResponse>( this, Invocation.getter(#inboxController), ), - ) as _i7.StreamController<_i12.StreamResponse>); - + ) as _i7.StreamController<_i8.StreamResponse>); @override - _i7.StreamController<_i12.StreamResponse> get iconController => + _i7.StreamController<_i8.StreamResponse> get iconController => (super.noSuchMethod( Invocation.getter(#iconController), - returnValue: _FakeStreamController_5<_i12.StreamResponse>( + returnValue: _FakeStreamController_5<_i8.StreamResponse>( this, Invocation.getter(#iconController), ), - returnValueForMissingStub: _FakeStreamController_5<_i12.StreamResponse>( + returnValueForMissingStub: _FakeStreamController_5<_i8.StreamResponse>( this, Invocation.getter(#iconController), ), - ) as _i7.StreamController<_i12.StreamResponse>); - + ) as _i7.StreamController<_i8.StreamResponse>); @override - _i13.Status get tokenVerificationStatus => (super.noSuchMethod( + _i12.Status get tokenVerificationStatus => (super.noSuchMethod( Invocation.getter(#tokenVerificationStatus), - returnValue: _i13.Status.PENDING, - returnValueForMissingStub: _i13.Status.PENDING, - ) as _i13.Status); - + returnValue: _i12.Status.PENDING, + returnValueForMissingStub: _i12.Status.PENDING, + ) as _i12.Status); + @override + bool get isProviderInitialized => (super.noSuchMethod( + Invocation.getter(#isProviderInitialized), + returnValue: false, + returnValueForMissingStub: false, + ) as bool); @override _i7.Future initialize() => (super.noSuchMethod( Invocation.method( @@ -1181,7 +1144,6 @@ class MockSirenDataProvider extends _i1.Mock implements _i10.SirenDataProvider { returnValue: _i7.Future.value(), returnValueForMissingStub: _i7.Future.value(), ) as _i7.Future); - @override void updateParams({ required String? userToken, @@ -1198,7 +1160,35 @@ class MockSirenDataProvider extends _i1.Mock implements _i10.SirenDataProvider { ), returnValueForMissingStub: null, ); - + @override + void triggerError() => super.noSuchMethod( + Invocation.method( + #triggerError, + [], + ), + returnValueForMissingStub: null, + ); + @override + _i8.SirenErrorType getVerificationErrorType() => (super.noSuchMethod( + Invocation.method( + #getVerificationErrorType, + [], + ), + returnValue: _FakeSirenErrorType_6( + this, + Invocation.method( + #getVerificationErrorType, + [], + ), + ), + returnValueForMissingStub: _FakeSirenErrorType_6( + this, + Invocation.method( + #getVerificationErrorType, + [], + ), + ), + ) as _i8.SirenErrorType); @override void iconDispose() => super.noSuchMethod( Invocation.method( @@ -1207,7 +1197,6 @@ class MockSirenDataProvider extends _i1.Mock implements _i10.SirenDataProvider { ), returnValueForMissingStub: null, ); - @override void inboxDispose() => super.noSuchMethod( Invocation.method( diff --git a/test/services/network_service_test.mocks.dart b/test/services/network_service_test.mocks.dart index ee1e372..9c0af93 100644 --- a/test/services/network_service_test.mocks.dart +++ b/test/services/network_service_test.mocks.dart @@ -1,4 +1,4 @@ -// Mocks generated by Mockito 5.4.4 from annotations +// Mocks generated by Mockito 5.3.2 from annotations // in sirenapp_flutter_inbox/test/services/network_service_test.dart. // Do not manually edit this file. @@ -14,8 +14,6 @@ import 'package:sirenapp_flutter_inbox/src/services/api_client.dart' as _i3; // ignore_for_file: avoid_redundant_argument_values // ignore_for_file: avoid_setters_without_getters // ignore_for_file: comment_references -// ignore_for_file: deprecated_member_use -// ignore_for_file: deprecated_member_use_from_same_package // ignore_for_file: implementation_imports // ignore_for_file: invalid_use_of_visible_for_testing_member // ignore_for_file: prefer_const_constructors @@ -46,7 +44,6 @@ class MockApiClient extends _i1.Mock implements _i3.ApiClient { returnValue: false, returnValueForMissingStub: false, ) as bool); - @override _i5.Future<_i2.DioResponse> get({ String? path, @@ -97,7 +94,6 @@ class MockApiClient extends _i1.Mock implements _i3.ApiClient { ), )), ) as _i5.Future<_i2.DioResponse>); - @override _i5.Future<_i2.DioResponse> post({ String? path, @@ -156,7 +152,6 @@ class MockApiClient extends _i1.Mock implements _i3.ApiClient { ), )), ) as _i5.Future<_i2.DioResponse>); - @override _i5.Future<_i2.DioResponse> patch({ String? path, @@ -215,7 +210,6 @@ class MockApiClient extends _i1.Mock implements _i3.ApiClient { ), )), ) as _i5.Future<_i2.DioResponse>); - @override _i5.Future<_i2.DioResponse> delete({ String? path, diff --git a/test/widgets/app_bar_test.dart b/test/widgets/app_bar_test.dart index ee64083..5e2af73 100644 --- a/test/widgets/app_bar_test.dart +++ b/test/widgets/app_bar_test.dart @@ -10,7 +10,6 @@ void main() { MaterialApp( home: Scaffold( appBar: SirenAppBar( - theme: ThemeData(), headerParams: HeaderParams( title: title, showBackButton: false, @@ -30,7 +29,6 @@ void main() { MaterialApp( home: Scaffold( appBar: SirenAppBar( - theme: ThemeData(), headerParams: HeaderParams( title: 'Title', showBackButton: true, @@ -51,7 +49,6 @@ void main() { MaterialApp( home: Scaffold( appBar: SirenAppBar( - theme: ThemeData(), headerParams: HeaderParams( title: 'Title', showBackButton: false, @@ -73,7 +70,6 @@ void main() { MaterialApp( home: Scaffold( appBar: SirenAppBar( - theme: ThemeData(), headerParams: HeaderParams( title: 'Title', showBackButton: false, @@ -95,7 +91,6 @@ void main() { MaterialApp( home: Scaffold( appBar: SirenAppBar( - theme: ThemeData(), headerParams: HeaderParams( title: 'Title', showBackButton: true, @@ -121,7 +116,6 @@ void main() { MaterialApp( home: Scaffold( appBar: SirenAppBar( - theme: ThemeData(), headerParams: HeaderParams( title: 'Title', showBackButton: false, diff --git a/test/widgets/card_test.dart b/test/widgets/card_test.dart index f35e25d..9e27a8e 100644 --- a/test/widgets/card_test.dart +++ b/test/widgets/card_test.dart @@ -40,7 +40,7 @@ void main() { await tester.pumpWidget( MaterialApp( home: CardWidget( - onTap: (notification) {}, + onTap: (NotificationType notification) {}, onDelete: (id) { deletePressed = true; }, @@ -53,6 +53,15 @@ void main() { }, ), styles: null, // Mock styles + colors: CustomThemeColors( + cardColors: CardColors( + borderColor: Colors.red, + background: Colors.blue, + titleColor: Colors.yellow, + subtitleColor: Colors.brown, + descriptionColor: Colors.orange, + ), + ), ), ), ); @@ -65,6 +74,22 @@ void main() { verify(func()).called(1); await tester.tap(find.byType(GestureDetector).at(2)); await tester.pumpAndSettle(const Duration(seconds: 1)); + + final textFinder = find.byType(Text).at(0); + final textWidget = tester.widget(textFinder); + final textColor = textWidget.style?.color; + expect(textColor, equals(Colors.yellow)); + + final textFinder2 = find.byType(Text).at(1); + final textWidget2 = tester.widget(textFinder2); + final textColor2 = textWidget2.style?.color; + expect(textColor2, equals(Colors.brown)); + + final textFinder3 = find.byType(Text).at(2); + final textWidget3 = tester.widget(textFinder3); + final textColor3 = textWidget3.style?.color; + expect(textColor3, equals(Colors.orange)); + expect(deletePressed, true); }); } diff --git a/test/widgets/icon_badge_test.dart b/test/widgets/icon_badge_test.dart index d540577..fa3d867 100644 --- a/test/widgets/icon_badge_test.dart +++ b/test/widgets/icon_badge_test.dart @@ -15,6 +15,8 @@ void main() { badgeStyle: BadgeStyle(), notificationsCount: 5, hideBadge: false, + color: Colors.red, + badgeBackgroundColor: Colors.black, ), ], ), @@ -38,6 +40,8 @@ void main() { badgeStyle: BadgeStyle(), notificationsCount: 5, hideBadge: true, + color: Colors.red, + badgeBackgroundColor: Colors.black, ), ], ), @@ -60,6 +64,8 @@ void main() { badgeStyle: BadgeStyle(), notificationsCount: 100, hideBadge: false, + color: Colors.red, + badgeBackgroundColor: Colors.black, ), ], ), diff --git a/test/widgets/inbox_body_test.dart b/test/widgets/inbox_body_test.dart index ce18bbd..c2a008e 100644 --- a/test/widgets/inbox_body_test.dart +++ b/test/widgets/inbox_body_test.dart @@ -13,7 +13,6 @@ void main() { await tester.pumpWidget( MaterialApp( home: InboxBody( - currentTheme: ThemeData(), isLoading: true, loadingNextPage: false, isError: false, @@ -24,7 +23,6 @@ void main() { onCardClick: null, deletingNotificationId: null, disableAutoMarkAsRead: false, - totalElements: 0, onRefresh: () async {}, endReached: false, onEndReached: () {}, @@ -41,7 +39,6 @@ void main() { await tester.pumpWidget( MaterialApp( home: InboxBody( - currentTheme: ThemeData(), isLoading: false, loadingNextPage: false, isError: true, @@ -52,7 +49,6 @@ void main() { onCardClick: null, deletingNotificationId: null, disableAutoMarkAsRead: false, - totalElements: 0, onRefresh: () async {}, endReached: false, onEndReached: () {}, @@ -90,7 +86,6 @@ void main() { await tester.pumpWidget( MaterialApp( home: InboxBody( - currentTheme: ThemeData(), isLoading: false, loadingNextPage: false, isError: false, @@ -101,7 +96,6 @@ void main() { onCardClick: null, deletingNotificationId: null, disableAutoMarkAsRead: false, - totalElements: 1, onRefresh: () async {}, endReached: false, onEndReached: () {}, diff --git a/test/widgets/notification_list_view_test.dart b/test/widgets/notification_list_view_test.dart index 55cc5be..c23d396 100644 --- a/test/widgets/notification_list_view_test.dart +++ b/test/widgets/notification_list_view_test.dart @@ -100,7 +100,7 @@ void main() { ), ); await tester.pumpAndSettle(const Duration(seconds: 1)); - expect(find.text('Test SubHeader'), findsOne); + expect(find.text('Test SubHeader'), findsOneWidget); }); }); } diff --git a/test/widgets/siren_inbox_icon_test.dart b/test/widgets/siren_inbox_icon_test.dart index 9dfc5c2..7cb9d48 100644 --- a/test/widgets/siren_inbox_icon_test.dart +++ b/test/widgets/siren_inbox_icon_test.dart @@ -10,7 +10,7 @@ import 'package:sirenapp_flutter_inbox/sirenapp_flutter_inbox.dart'; import 'package:sirenapp_flutter_inbox/src/api/fetch_unviewed_notification_count.dart'; import 'package:sirenapp_flutter_inbox/src/constants/generics.dart'; import 'package:sirenapp_flutter_inbox/src/data/siren_data_provider.dart'; -import 'package:sirenapp_flutter_inbox/src/theme/app_theme.dart'; +import 'package:sirenapp_flutter_inbox/src/theme/app_colors.dart'; import 'siren_inbox_test.mocks.dart'; @@ -107,9 +107,9 @@ void main() { ), ); - final primaryColor = AppTheme.darkTheme.colorScheme.primary; + final primaryColor = AppColors.darkColorTheme().primary; - expect(primaryColor, const Color(0xff232326)); + expect(primaryColor, const Color(0xfffa9874)); }); testWidgets('Widget disposes controllers on dispose', diff --git a/test/widgets/siren_inbox_icon_test.mocks.dart b/test/widgets/siren_inbox_icon_test.mocks.dart index f874f60..507e251 100644 --- a/test/widgets/siren_inbox_icon_test.mocks.dart +++ b/test/widgets/siren_inbox_icon_test.mocks.dart @@ -1,4 +1,4 @@ -// Mocks generated by Mockito 5.4.4 from annotations +// Mocks generated by Mockito 5.3.2 from annotations // in sirenapp_flutter_inbox/test/widgets/siren_inbox_icon_test.dart. // Do not manually edit this file. @@ -6,21 +6,18 @@ import 'dart:async' as _i2; import 'package:mockito/mockito.dart' as _i1; -import 'package:mockito/src/dummies.dart' as _i6; -import 'package:sirenapp_flutter_inbox/sirenapp_flutter_inbox.dart' as _i4; +import 'package:sirenapp_flutter_inbox/sirenapp_flutter_inbox.dart' as _i3; import 'package:sirenapp_flutter_inbox/src/api/fetch_unviewed_notification_count.dart' - as _i8; -import 'package:sirenapp_flutter_inbox/src/constants/generics.dart' as _i7; + as _i7; +import 'package:sirenapp_flutter_inbox/src/constants/generics.dart' as _i6; import 'package:sirenapp_flutter_inbox/src/data/siren_data_provider.dart' as _i5; -import 'package:sirenapp_flutter_inbox/src/services/api_client.dart' as _i3; +import 'package:sirenapp_flutter_inbox/src/services/api_client.dart' as _i4; // ignore_for_file: type=lint // ignore_for_file: avoid_redundant_argument_values // ignore_for_file: avoid_setters_without_getters // ignore_for_file: comment_references -// ignore_for_file: deprecated_member_use -// ignore_for_file: deprecated_member_use_from_same_package // ignore_for_file: implementation_imports // ignore_for_file: invalid_use_of_visible_for_testing_member // ignore_for_file: prefer_const_constructors @@ -39,8 +36,9 @@ class _FakeStreamController_0 extends _i1.SmartFake ); } -class _FakeApiClient_1 extends _i1.SmartFake implements _i3.ApiClient { - _FakeApiClient_1( +class _FakeSirenErrorType_1 extends _i1.SmartFake + implements _i3.SirenErrorType { + _FakeSirenErrorType_1( Object parent, Invocation parentInvocation, ) : super( @@ -49,8 +47,18 @@ class _FakeApiClient_1 extends _i1.SmartFake implements _i3.ApiClient { ); } -class _FakeApiResponse_2 extends _i1.SmartFake implements _i4.ApiResponse { - _FakeApiResponse_2( +class _FakeApiClient_2 extends _i1.SmartFake implements _i4.ApiClient { + _FakeApiClient_2( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} + +class _FakeApiResponse_3 extends _i1.SmartFake implements _i3.ApiResponse { + _FakeApiResponse_3( Object parent, Invocation parentInvocation, ) : super( @@ -70,12 +78,8 @@ class MockSirenDataProvider extends _i1.Mock implements _i5.SirenDataProvider { @override String get userToken => (super.noSuchMethod( Invocation.getter(#userToken), - returnValue: _i6.dummyValue( - this, - Invocation.getter(#userToken), - ), + returnValue: '', ) as String); - @override set userToken(String? _userToken) => super.noSuchMethod( Invocation.setter( @@ -84,16 +88,11 @@ class MockSirenDataProvider extends _i1.Mock implements _i5.SirenDataProvider { ), returnValueForMissingStub: null, ); - @override String get recipientId => (super.noSuchMethod( Invocation.getter(#recipientId), - returnValue: _i6.dummyValue( - this, - Invocation.getter(#recipientId), - ), + returnValue: '', ) as String); - @override set recipientId(String? _recipientId) => super.noSuchMethod( Invocation.setter( @@ -102,16 +101,11 @@ class MockSirenDataProvider extends _i1.Mock implements _i5.SirenDataProvider { ), returnValueForMissingStub: null, ); - @override String get apiDomain => (super.noSuchMethod( Invocation.getter(#apiDomain), - returnValue: _i6.dummyValue( - this, - Invocation.getter(#apiDomain), - ), + returnValue: '', ) as String); - @override set apiDomain(String? _apiDomain) => super.noSuchMethod( Invocation.setter( @@ -120,33 +114,34 @@ class MockSirenDataProvider extends _i1.Mock implements _i5.SirenDataProvider { ), returnValueForMissingStub: null, ); - @override - _i2.StreamController<_i4.StreamResponse> get inboxController => + _i2.StreamController<_i3.StreamResponse> get inboxController => (super.noSuchMethod( Invocation.getter(#inboxController), - returnValue: _FakeStreamController_0<_i4.StreamResponse>( + returnValue: _FakeStreamController_0<_i3.StreamResponse>( this, Invocation.getter(#inboxController), ), - ) as _i2.StreamController<_i4.StreamResponse>); - + ) as _i2.StreamController<_i3.StreamResponse>); @override - _i2.StreamController<_i4.StreamResponse> get iconController => + _i2.StreamController<_i3.StreamResponse> get iconController => (super.noSuchMethod( Invocation.getter(#iconController), - returnValue: _FakeStreamController_0<_i4.StreamResponse>( + returnValue: _FakeStreamController_0<_i3.StreamResponse>( this, Invocation.getter(#iconController), ), - ) as _i2.StreamController<_i4.StreamResponse>); - + ) as _i2.StreamController<_i3.StreamResponse>); @override - _i7.Status get tokenVerificationStatus => (super.noSuchMethod( + _i6.Status get tokenVerificationStatus => (super.noSuchMethod( Invocation.getter(#tokenVerificationStatus), - returnValue: _i7.Status.PENDING, - ) as _i7.Status); - + returnValue: _i6.Status.PENDING, + ) as _i6.Status); + @override + bool get isProviderInitialized => (super.noSuchMethod( + Invocation.getter(#isProviderInitialized), + returnValue: false, + ) as bool); @override _i2.Future initialize() => (super.noSuchMethod( Invocation.method( @@ -156,7 +151,6 @@ class MockSirenDataProvider extends _i1.Mock implements _i5.SirenDataProvider { returnValue: _i2.Future.value(), returnValueForMissingStub: _i2.Future.value(), ) as _i2.Future); - @override void updateParams({ required String? userToken, @@ -173,7 +167,28 @@ class MockSirenDataProvider extends _i1.Mock implements _i5.SirenDataProvider { ), returnValueForMissingStub: null, ); - + @override + void triggerError() => super.noSuchMethod( + Invocation.method( + #triggerError, + [], + ), + returnValueForMissingStub: null, + ); + @override + _i3.SirenErrorType getVerificationErrorType() => (super.noSuchMethod( + Invocation.method( + #getVerificationErrorType, + [], + ), + returnValue: _FakeSirenErrorType_1( + this, + Invocation.method( + #getVerificationErrorType, + [], + ), + ), + ) as _i3.SirenErrorType); @override void iconDispose() => super.noSuchMethod( Invocation.method( @@ -182,7 +197,6 @@ class MockSirenDataProvider extends _i1.Mock implements _i5.SirenDataProvider { ), returnValueForMissingStub: null, ); - @override void inboxDispose() => super.noSuchMethod( Invocation.method( @@ -197,42 +211,40 @@ class MockSirenDataProvider extends _i1.Mock implements _i5.SirenDataProvider { /// /// See the documentation for Mockito's code generation for more information. class MockFetchUnViewedNotificationsCount extends _i1.Mock - implements _i8.FetchUnViewedNotificationsCount { + implements _i7.FetchUnViewedNotificationsCount { MockFetchUnViewedNotificationsCount() { _i1.throwOnMissingStub(this); } @override - _i3.ApiClient get api => (super.noSuchMethod( + _i4.ApiClient get api => (super.noSuchMethod( Invocation.getter(#api), - returnValue: _FakeApiClient_1( + returnValue: _FakeApiClient_2( this, Invocation.getter(#api), ), - ) as _i3.ApiClient); - + ) as _i4.ApiClient); @override - set api(_i3.ApiClient? _api) => super.noSuchMethod( + set api(_i4.ApiClient? _api) => super.noSuchMethod( Invocation.setter( #api, _api, ), returnValueForMissingStub: null, ); - @override - _i2.Future<_i4.ApiResponse> fetchUnViewedNotificationsCount() => + _i2.Future<_i3.ApiResponse> fetchUnViewedNotificationsCount() => (super.noSuchMethod( Invocation.method( #fetchUnViewedNotificationsCount, [], ), - returnValue: _i2.Future<_i4.ApiResponse>.value(_FakeApiResponse_2( + returnValue: _i2.Future<_i3.ApiResponse>.value(_FakeApiResponse_3( this, Invocation.method( #fetchUnViewedNotificationsCount, [], ), )), - ) as _i2.Future<_i4.ApiResponse>); + ) as _i2.Future<_i3.ApiResponse>); } diff --git a/test/widgets/siren_inbox_test.mocks.dart b/test/widgets/siren_inbox_test.mocks.dart index 0843556..1e3ca38 100644 --- a/test/widgets/siren_inbox_test.mocks.dart +++ b/test/widgets/siren_inbox_test.mocks.dart @@ -1,4 +1,4 @@ -// Mocks generated by Mockito 5.4.4 from annotations +// Mocks generated by Mockito 5.3.2 from annotations // in sirenapp_flutter_inbox/test/widgets/siren_inbox_test.dart. // Do not manually edit this file. @@ -6,21 +6,18 @@ import 'dart:async' as _i2; import 'package:mockito/mockito.dart' as _i1; -import 'package:mockito/src/dummies.dart' as _i6; -import 'package:sirenapp_flutter_inbox/sirenapp_flutter_inbox.dart' as _i4; +import 'package:sirenapp_flutter_inbox/sirenapp_flutter_inbox.dart' as _i3; import 'package:sirenapp_flutter_inbox/src/api/fetch_unviewed_notification_count.dart' - as _i8; -import 'package:sirenapp_flutter_inbox/src/constants/generics.dart' as _i7; + as _i7; +import 'package:sirenapp_flutter_inbox/src/constants/generics.dart' as _i6; import 'package:sirenapp_flutter_inbox/src/data/siren_data_provider.dart' as _i5; -import 'package:sirenapp_flutter_inbox/src/services/api_client.dart' as _i3; +import 'package:sirenapp_flutter_inbox/src/services/api_client.dart' as _i4; // ignore_for_file: type=lint // ignore_for_file: avoid_redundant_argument_values // ignore_for_file: avoid_setters_without_getters // ignore_for_file: comment_references -// ignore_for_file: deprecated_member_use -// ignore_for_file: deprecated_member_use_from_same_package // ignore_for_file: implementation_imports // ignore_for_file: invalid_use_of_visible_for_testing_member // ignore_for_file: prefer_const_constructors @@ -39,8 +36,9 @@ class _FakeStreamController_0 extends _i1.SmartFake ); } -class _FakeApiClient_1 extends _i1.SmartFake implements _i3.ApiClient { - _FakeApiClient_1( +class _FakeSirenErrorType_1 extends _i1.SmartFake + implements _i3.SirenErrorType { + _FakeSirenErrorType_1( Object parent, Invocation parentInvocation, ) : super( @@ -49,8 +47,18 @@ class _FakeApiClient_1 extends _i1.SmartFake implements _i3.ApiClient { ); } -class _FakeApiResponse_2 extends _i1.SmartFake implements _i4.ApiResponse { - _FakeApiResponse_2( +class _FakeApiClient_2 extends _i1.SmartFake implements _i4.ApiClient { + _FakeApiClient_2( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} + +class _FakeApiResponse_3 extends _i1.SmartFake implements _i3.ApiResponse { + _FakeApiResponse_3( Object parent, Invocation parentInvocation, ) : super( @@ -66,16 +74,9 @@ class MockSirenDataProvider extends _i1.Mock implements _i5.SirenDataProvider { @override String get userToken => (super.noSuchMethod( Invocation.getter(#userToken), - returnValue: _i6.dummyValue( - this, - Invocation.getter(#userToken), - ), - returnValueForMissingStub: _i6.dummyValue( - this, - Invocation.getter(#userToken), - ), + returnValue: '', + returnValueForMissingStub: '', ) as String); - @override set userToken(String? _userToken) => super.noSuchMethod( Invocation.setter( @@ -84,20 +85,12 @@ class MockSirenDataProvider extends _i1.Mock implements _i5.SirenDataProvider { ), returnValueForMissingStub: null, ); - @override String get recipientId => (super.noSuchMethod( Invocation.getter(#recipientId), - returnValue: _i6.dummyValue( - this, - Invocation.getter(#recipientId), - ), - returnValueForMissingStub: _i6.dummyValue( - this, - Invocation.getter(#recipientId), - ), + returnValue: '', + returnValueForMissingStub: '', ) as String); - @override set recipientId(String? _recipientId) => super.noSuchMethod( Invocation.setter( @@ -106,20 +99,12 @@ class MockSirenDataProvider extends _i1.Mock implements _i5.SirenDataProvider { ), returnValueForMissingStub: null, ); - @override String get apiDomain => (super.noSuchMethod( Invocation.getter(#apiDomain), - returnValue: _i6.dummyValue( - this, - Invocation.getter(#apiDomain), - ), - returnValueForMissingStub: _i6.dummyValue( - this, - Invocation.getter(#apiDomain), - ), + returnValue: '', + returnValueForMissingStub: '', ) as String); - @override set apiDomain(String? _apiDomain) => super.noSuchMethod( Invocation.setter( @@ -128,42 +113,44 @@ class MockSirenDataProvider extends _i1.Mock implements _i5.SirenDataProvider { ), returnValueForMissingStub: null, ); - @override - _i2.StreamController<_i4.StreamResponse> get inboxController => + _i2.StreamController<_i3.StreamResponse> get inboxController => (super.noSuchMethod( Invocation.getter(#inboxController), - returnValue: _FakeStreamController_0<_i4.StreamResponse>( + returnValue: _FakeStreamController_0<_i3.StreamResponse>( this, Invocation.getter(#inboxController), ), - returnValueForMissingStub: _FakeStreamController_0<_i4.StreamResponse>( + returnValueForMissingStub: _FakeStreamController_0<_i3.StreamResponse>( this, Invocation.getter(#inboxController), ), - ) as _i2.StreamController<_i4.StreamResponse>); - + ) as _i2.StreamController<_i3.StreamResponse>); @override - _i2.StreamController<_i4.StreamResponse> get iconController => + _i2.StreamController<_i3.StreamResponse> get iconController => (super.noSuchMethod( Invocation.getter(#iconController), - returnValue: _FakeStreamController_0<_i4.StreamResponse>( + returnValue: _FakeStreamController_0<_i3.StreamResponse>( this, Invocation.getter(#iconController), ), - returnValueForMissingStub: _FakeStreamController_0<_i4.StreamResponse>( + returnValueForMissingStub: _FakeStreamController_0<_i3.StreamResponse>( this, Invocation.getter(#iconController), ), - ) as _i2.StreamController<_i4.StreamResponse>); - + ) as _i2.StreamController<_i3.StreamResponse>); @override - _i7.Status get tokenVerificationStatus => (super.noSuchMethod( + _i6.Status get tokenVerificationStatus => (super.noSuchMethod( Invocation.getter(#tokenVerificationStatus), - returnValue: _i7.Status.PENDING, - returnValueForMissingStub: _i7.Status.PENDING, - ) as _i7.Status); - + returnValue: _i6.Status.PENDING, + returnValueForMissingStub: _i6.Status.PENDING, + ) as _i6.Status); + @override + bool get isProviderInitialized => (super.noSuchMethod( + Invocation.getter(#isProviderInitialized), + returnValue: false, + returnValueForMissingStub: false, + ) as bool); @override _i2.Future initialize() => (super.noSuchMethod( Invocation.method( @@ -173,7 +160,6 @@ class MockSirenDataProvider extends _i1.Mock implements _i5.SirenDataProvider { returnValue: _i2.Future.value(), returnValueForMissingStub: _i2.Future.value(), ) as _i2.Future); - @override void updateParams({ required String? userToken, @@ -190,7 +176,35 @@ class MockSirenDataProvider extends _i1.Mock implements _i5.SirenDataProvider { ), returnValueForMissingStub: null, ); - + @override + void triggerError() => super.noSuchMethod( + Invocation.method( + #triggerError, + [], + ), + returnValueForMissingStub: null, + ); + @override + _i3.SirenErrorType getVerificationErrorType() => (super.noSuchMethod( + Invocation.method( + #getVerificationErrorType, + [], + ), + returnValue: _FakeSirenErrorType_1( + this, + Invocation.method( + #getVerificationErrorType, + [], + ), + ), + returnValueForMissingStub: _FakeSirenErrorType_1( + this, + Invocation.method( + #getVerificationErrorType, + [], + ), + ), + ) as _i3.SirenErrorType); @override void iconDispose() => super.noSuchMethod( Invocation.method( @@ -199,7 +213,6 @@ class MockSirenDataProvider extends _i1.Mock implements _i5.SirenDataProvider { ), returnValueForMissingStub: null, ); - @override void inboxDispose() => super.noSuchMethod( Invocation.method( @@ -214,37 +227,35 @@ class MockSirenDataProvider extends _i1.Mock implements _i5.SirenDataProvider { /// /// See the documentation for Mockito's code generation for more information. class MockFetchUnViewedNotificationsCount extends _i1.Mock - implements _i8.FetchUnViewedNotificationsCount { + implements _i7.FetchUnViewedNotificationsCount { @override - _i3.ApiClient get api => (super.noSuchMethod( + _i4.ApiClient get api => (super.noSuchMethod( Invocation.getter(#api), - returnValue: _FakeApiClient_1( + returnValue: _FakeApiClient_2( this, Invocation.getter(#api), ), - returnValueForMissingStub: _FakeApiClient_1( + returnValueForMissingStub: _FakeApiClient_2( this, Invocation.getter(#api), ), - ) as _i3.ApiClient); - + ) as _i4.ApiClient); @override - set api(_i3.ApiClient? _api) => super.noSuchMethod( + set api(_i4.ApiClient? _api) => super.noSuchMethod( Invocation.setter( #api, _api, ), returnValueForMissingStub: null, ); - @override - _i2.Future<_i4.ApiResponse> fetchUnViewedNotificationsCount() => + _i2.Future<_i3.ApiResponse> fetchUnViewedNotificationsCount() => (super.noSuchMethod( Invocation.method( #fetchUnViewedNotificationsCount, [], ), - returnValue: _i2.Future<_i4.ApiResponse>.value(_FakeApiResponse_2( + returnValue: _i2.Future<_i3.ApiResponse>.value(_FakeApiResponse_3( this, Invocation.method( #fetchUnViewedNotificationsCount, @@ -252,12 +263,12 @@ class MockFetchUnViewedNotificationsCount extends _i1.Mock ), )), returnValueForMissingStub: - _i2.Future<_i4.ApiResponse>.value(_FakeApiResponse_2( + _i2.Future<_i3.ApiResponse>.value(_FakeApiResponse_3( this, Invocation.method( #fetchUnViewedNotificationsCount, [], ), )), - ) as _i2.Future<_i4.ApiResponse>); + ) as _i2.Future<_i3.ApiResponse>); } From 544ad3c394da16ced8e4e10c878fa4a2a33b2888 Mon Sep 17 00:00:00 2001 From: Anitta Babu <99161914+anitta-keyvalue@users.noreply.github.com> Date: Thu, 2 May 2024 10:35:26 +0530 Subject: [PATCH 07/17] refactor: Move errors from generics to errors file --- lib/src/api/delete_notification_by_id.dart | 7 +- lib/src/api/fetch_all_notification.dart | 7 +- .../fetch_unviewed_notification_count.dart | 7 +- .../api/mark_all_notifications_as_viewed.dart | 7 +- lib/src/api/notifications_bulk_update.dart | 9 ++- lib/src/api/read_notification_by_id.dart | 7 +- lib/src/api/verify_token.dart | 9 ++- lib/src/constants/generics.dart | 78 ------------------ lib/src/data/siren_data_provider.dart | 37 +++------ lib/src/errors/error_codes.dart | 12 --- lib/src/errors/errors.dart | 80 +++++++++++++++++++ lib/src/widgets/siren_inbox.dart | 3 +- lib/src/widgets/siren_inbox_icon.dart | 3 +- test/constants/generics_test.dart | 7 +- test/delete_notification_by_id_test.dart | 3 +- 15 files changed, 133 insertions(+), 143 deletions(-) delete mode 100644 lib/src/errors/error_codes.dart create mode 100644 lib/src/errors/errors.dart diff --git a/lib/src/api/delete_notification_by_id.dart b/lib/src/api/delete_notification_by_id.dart index 2cc3a6b..cb5eb58 100644 --- a/lib/src/api/delete_notification_by_id.dart +++ b/lib/src/api/delete_notification_by_id.dart @@ -1,5 +1,6 @@ import 'package:sirenapp_flutter_inbox/src/constants/generics.dart'; import 'package:sirenapp_flutter_inbox/src/data/siren_data_provider.dart'; +import 'package:sirenapp_flutter_inbox/src/errors/errors.dart'; import 'package:sirenapp_flutter_inbox/src/models/api_response.dart'; import 'package:sirenapp_flutter_inbox/src/services/api_client.dart'; import 'package:sirenapp_flutter_inbox/src/services/api_provider.dart'; @@ -25,7 +26,7 @@ class DeleteNotificationById { required String notificationId, }) async { final result = ApiResponse()..isLoading = true; - var apiError = Generics.deleteFailedError; + var apiError = Errors.deleteFailedError; if (SirenDataProvider.instance.tokenVerificationStatus != Status.SUCCESS) { apiError = SirenDataProvider.instance.getVerificationErrorType(); @@ -33,7 +34,7 @@ class DeleteNotificationById { ..isLoading = false ..isError = true ..data = null - ..rawResponse = Generics.rawResponseError + ..rawResponse = Errors.rawResponseError ..error = apiError; return result; } @@ -56,7 +57,7 @@ class DeleteNotificationById { ..isSuccess = false ..isError = true ..rawResponse = apiResponse - ..error = Generics.defaultError; + ..error = Errors.defaultError; } return result; diff --git a/lib/src/api/fetch_all_notification.dart b/lib/src/api/fetch_all_notification.dart index a51698f..aa44a69 100644 --- a/lib/src/api/fetch_all_notification.dart +++ b/lib/src/api/fetch_all_notification.dart @@ -1,5 +1,6 @@ import 'package:sirenapp_flutter_inbox/src/constants/generics.dart'; import 'package:sirenapp_flutter_inbox/src/data/siren_data_provider.dart'; +import 'package:sirenapp_flutter_inbox/src/errors/errors.dart'; import 'package:sirenapp_flutter_inbox/src/models/api_response.dart'; import 'package:sirenapp_flutter_inbox/src/models/notification_model.dart'; import 'package:sirenapp_flutter_inbox/src/services/api_client.dart'; @@ -33,7 +34,7 @@ class FetchAllNotifications { String? end, }) async { final result = ApiResponse()..isLoading = true; - var apiError = Generics.notificationFetchFailedError; + var apiError = Errors.notificationFetchFailedError; // Manually construct query parameters final queryParams = { @@ -58,7 +59,7 @@ class FetchAllNotifications { ..isLoading = false ..isError = true ..data = null - ..rawResponse = Generics.rawResponseError + ..rawResponse = Errors.rawResponseError ..error = apiError; return result; } @@ -85,7 +86,7 @@ class FetchAllNotifications { ..isSuccess = false ..isError = true ..rawResponse = apiResponse - ..error = Generics.defaultError; + ..error = Errors.defaultError; } return result; diff --git a/lib/src/api/fetch_unviewed_notification_count.dart b/lib/src/api/fetch_unviewed_notification_count.dart index 5faa1b7..2bb609f 100644 --- a/lib/src/api/fetch_unviewed_notification_count.dart +++ b/lib/src/api/fetch_unviewed_notification_count.dart @@ -2,6 +2,7 @@ import 'dart:async'; import 'package:sirenapp_flutter_inbox/src/constants/generics.dart'; import 'package:sirenapp_flutter_inbox/src/data/siren_data_provider.dart'; +import 'package:sirenapp_flutter_inbox/src/errors/errors.dart'; import 'package:sirenapp_flutter_inbox/src/models/api_response.dart'; import 'package:sirenapp_flutter_inbox/src/models/unviewed_notification_count_model.dart'; import 'package:sirenapp_flutter_inbox/src/services/api_client.dart'; @@ -19,7 +20,7 @@ class FetchUnViewedNotificationsCount { Future fetchUnViewedNotificationsCount() async { final result = ApiResponse()..isLoading = true; - var apiError = Generics.fetchUnViewedCountFailedError; + var apiError = Errors.fetchUnViewedCountFailedError; if (SirenDataProvider.instance.tokenVerificationStatus != Status.SUCCESS) { apiError = SirenDataProvider.instance.getVerificationErrorType(); @@ -27,7 +28,7 @@ class FetchUnViewedNotificationsCount { ..isLoading = false ..isError = true ..data = null - ..rawResponse = Generics.rawResponseError + ..rawResponse = Errors.rawResponseError ..error = apiError; return result; } @@ -55,7 +56,7 @@ class FetchUnViewedNotificationsCount { ..isLoading = false ..isSuccess = false ..isError = true - ..error = Generics.defaultError; + ..error = Errors.defaultError; } return result; } diff --git a/lib/src/api/mark_all_notifications_as_viewed.dart b/lib/src/api/mark_all_notifications_as_viewed.dart index e13c218..acff2c8 100644 --- a/lib/src/api/mark_all_notifications_as_viewed.dart +++ b/lib/src/api/mark_all_notifications_as_viewed.dart @@ -1,5 +1,6 @@ import 'package:sirenapp_flutter_inbox/src/constants/generics.dart'; import 'package:sirenapp_flutter_inbox/src/data/siren_data_provider.dart'; +import 'package:sirenapp_flutter_inbox/src/errors/errors.dart'; import 'package:sirenapp_flutter_inbox/src/models/api_response.dart'; import 'package:sirenapp_flutter_inbox/src/services/api_client.dart'; import 'package:sirenapp_flutter_inbox/src/services/api_provider.dart'; @@ -18,7 +19,7 @@ class MarkAllNotificationsAsViewed { }) async { final api = ApiClient(apiProvider()); final result = ApiResponse()..isLoading; - var apiError = Generics.markAllAsViewedError; + var apiError = Errors.markAllAsViewedError; final data = { 'lastOpenedAt': untilDate, @@ -30,7 +31,7 @@ class MarkAllNotificationsAsViewed { ..isLoading = false ..isError = true ..data = null - ..rawResponse = Generics.rawResponseError + ..rawResponse = Errors.rawResponseError ..error = apiError; return result; } @@ -53,7 +54,7 @@ class MarkAllNotificationsAsViewed { ..isSuccess = false ..isError = true ..rawResponse = apiResponse - ..error = Generics.defaultError; + ..error = Errors.defaultError; } return result; diff --git a/lib/src/api/notifications_bulk_update.dart b/lib/src/api/notifications_bulk_update.dart index a9ac9a6..3505d41 100644 --- a/lib/src/api/notifications_bulk_update.dart +++ b/lib/src/api/notifications_bulk_update.dart @@ -1,5 +1,6 @@ import 'package:sirenapp_flutter_inbox/src/constants/generics.dart'; import 'package:sirenapp_flutter_inbox/src/data/siren_data_provider.dart'; +import 'package:sirenapp_flutter_inbox/src/errors/errors.dart'; import 'package:sirenapp_flutter_inbox/src/models/api_response.dart'; import 'package:sirenapp_flutter_inbox/src/services/api_client.dart'; import 'package:sirenapp_flutter_inbox/src/services/api_provider.dart'; @@ -21,10 +22,10 @@ class NotificationsBulkUpdate { final apiPath = '${Generics.V2}${Generics.BASE_URL}${SirenDataProvider.instance.recipientId}/notifications/bulk-update'; final result = ApiResponse()..isLoading; - var apiError = Generics.markAsReadFailedError; + var apiError = Errors.markAsReadFailedError; if (operation == BulkUpdateType.MARK_AS_DELETED.name) { - apiError = Generics.deleteAllFailedError; + apiError = Errors.deleteAllFailedError; } if (SirenDataProvider.instance.tokenVerificationStatus != Status.SUCCESS) { @@ -33,7 +34,7 @@ class NotificationsBulkUpdate { ..isLoading = false ..isError = true ..data = null - ..rawResponse = Generics.rawResponseError + ..rawResponse = Errors.rawResponseError ..error = apiError; return result; } @@ -55,7 +56,7 @@ class NotificationsBulkUpdate { ..isSuccess = false ..isError = true ..rawResponse = apiResponse - ..error = Generics.defaultError; + ..error = Errors.defaultError; } return result; diff --git a/lib/src/api/read_notification_by_id.dart b/lib/src/api/read_notification_by_id.dart index 37eedc6..ad5762e 100644 --- a/lib/src/api/read_notification_by_id.dart +++ b/lib/src/api/read_notification_by_id.dart @@ -1,5 +1,6 @@ import 'package:sirenapp_flutter_inbox/src/constants/generics.dart'; import 'package:sirenapp_flutter_inbox/src/data/siren_data_provider.dart'; +import 'package:sirenapp_flutter_inbox/src/errors/errors.dart'; import 'package:sirenapp_flutter_inbox/src/models/api_response.dart'; import 'package:sirenapp_flutter_inbox/src/services/api_client.dart'; import 'package:sirenapp_flutter_inbox/src/services/api_provider.dart'; @@ -17,7 +18,7 @@ class ReadNotificationById { required String notificationId, }) async { final result = ApiResponse()..isLoading = true; - var apiError = Generics.markAsReadFailedError; + var apiError = Errors.markAsReadFailedError; if (SirenDataProvider.instance.tokenVerificationStatus != Status.SUCCESS) { apiError = SirenDataProvider.instance.getVerificationErrorType(); @@ -25,7 +26,7 @@ class ReadNotificationById { ..isLoading = false ..isError = true ..data = null - ..rawResponse = Generics.rawResponseError + ..rawResponse = Errors.rawResponseError ..error = apiError; return result; } @@ -50,7 +51,7 @@ class ReadNotificationById { ..isSuccess = false ..isError = true ..rawResponse = apiResponse - ..error = Generics.defaultError; + ..error = Errors.defaultError; } return result; } diff --git a/lib/src/api/verify_token.dart b/lib/src/api/verify_token.dart index 4941cc0..87f86a4 100644 --- a/lib/src/api/verify_token.dart +++ b/lib/src/api/verify_token.dart @@ -1,5 +1,6 @@ import 'package:sirenapp_flutter_inbox/src/constants/generics.dart'; import 'package:sirenapp_flutter_inbox/src/data/siren_data_provider.dart'; +import 'package:sirenapp_flutter_inbox/src/errors/errors.dart'; import 'package:sirenapp_flutter_inbox/src/models/api_response.dart'; import 'package:sirenapp_flutter_inbox/src/services/api_client.dart'; import 'package:sirenapp_flutter_inbox/src/services/api_provider.dart'; @@ -24,16 +25,16 @@ class VerifyToken { Future verifyToken() async { final result = ApiResponse()..isLoading = true; - var apiError = Generics.authenticationFailed; + var apiError = Errors.authenticationFailed; if (SirenDataProvider.instance.userToken.isEmpty || SirenDataProvider.instance.recipientId.isEmpty) { - apiError = Generics.invalidCredentialsError; + apiError = Errors.invalidCredentialsError; result ..isLoading = false ..isError = true ..data = null - ..rawResponse = Generics.rawResponseError + ..rawResponse = Errors.rawResponseError ..error = apiError; return result; } @@ -58,7 +59,7 @@ class VerifyToken { ..isSuccess = false ..isError = true ..data = Status.FAILED - ..error = Generics.defaultError; + ..error = Errors.defaultError; } return result; diff --git a/lib/src/constants/generics.dart b/lib/src/constants/generics.dart index d089fb1..25a7282 100644 --- a/lib/src/constants/generics.dart +++ b/lib/src/constants/generics.dart @@ -1,6 +1,3 @@ -import 'package:sirenapp_flutter_inbox/sirenapp_flutter_inbox.dart'; -import 'package:sirenapp_flutter_inbox/src/constants/strings.dart'; - class Generics { Generics._(); @@ -12,81 +9,6 @@ class Generics { static const int AVERAGE_ITEMS_ON_SCREEN = 7; static const int MAX_RETRIES = 2; static const String ENV_PATH = 'packages/sirenapp_flutter_inbox/env'; - - static final defaultError = SirenErrorType( - code: ErrorCodes.API_ERROR.name, - type: Strings.error_type_error, - message: Strings.something_went_wrong, - ); - - static final authenticationFailed = SirenErrorType( - code: ErrorCodes.AUTHENTICATION_FAILED.name, - type: Strings.error_type_error, - message: Strings.authenticationFailed, - ); - - static final fetchUnViewedCountFailedError = SirenErrorType( - code: ErrorCodes.UNVIEWED_COUNT_FETCH_FAILED.name, - type: Strings.error_type_error, - message: Strings.fetchUnViewedCountFailedError, - ); - - static final notificationFetchFailedError = SirenErrorType( - code: ErrorCodes.NOTIFICATION_FETCH_FAILED.name, - type: Strings.error_type_error, - message: Strings.notificationFetchFailedError, - ); - - static final markAsReadFailedError = SirenErrorType( - code: ErrorCodes.MARK_AS_READ_FAILED.name, - type: Strings.error_type_error, - message: Strings.markAsReadFailedError, - ); - - static final deleteFailedError = SirenErrorType( - code: ErrorCodes.DELETE_FAILED.name, - type: Strings.error_type_error, - message: Strings.deleteFailedError, - ); - - static final deleteAllFailedError = SirenErrorType( - code: ErrorCodes.BULK_DELETE_FAILED.name, - type: Strings.error_type_error, - message: Strings.deleteAllFailedError, - ); - - static final markAllAsViewedError = SirenErrorType( - code: ErrorCodes.MARK_ALL_AS_VIEWED_FAILED.name, - type: Strings.error_type_error, - message: Strings.markAllAsViewedError, - ); - - static final outsideSirenContextError = SirenErrorType( - code: ErrorCodes.OUTSIDE_SIREN_CONTEXT.name, - type: Strings.error_type_error, - message: Strings.outsideSirenContextError, - ); - - static final authenticationPending = SirenErrorType( - code: ErrorCodes.AUTHENTICATION_PENDING.name, - type: Strings.error_type_error, - message: Strings.authenticationPending, - ); - - static final unauthorizedOperationError = SirenErrorType( - code: ErrorCodes.UNAUTHORIZED_OPERATION.name, - type: Strings.error_type_error, - message: Strings.unauthorizedOperationError, - ); - - static final invalidCredentialsError = SirenErrorType( - code: ErrorCodes.INVALID_CREDENTIALS.name, - type: Strings.error_type_error, - message: Strings.invalidCredentialsError, - ); - - static const rawResponseError = - '{"data": null,"error": "AUTHENTICATION FAILED","errors":null,"meta":null}'; } enum Status { diff --git a/lib/src/data/siren_data_provider.dart b/lib/src/data/siren_data_provider.dart index 4675769..5efc791 100644 --- a/lib/src/data/siren_data_provider.dart +++ b/lib/src/data/siren_data_provider.dart @@ -3,6 +3,7 @@ import 'dart:async'; import 'package:sirenapp_flutter_inbox/sirenapp_flutter_inbox.dart'; import 'package:sirenapp_flutter_inbox/src/api/verify_token.dart'; import 'package:sirenapp_flutter_inbox/src/constants/generics.dart'; +import 'package:sirenapp_flutter_inbox/src/errors/errors.dart'; import 'package:sirenapp_flutter_inbox/src/utils/common_utils.dart'; /// Singleton class responsible for providing data to the Siren Inbox and Icon. @@ -74,20 +75,7 @@ class SirenDataProvider { if (_tokenVerificationResponse.isSuccess) { _retryCount = 0; _tokenVerificationStatus = _tokenVerificationResponse.data as Status; - SirenDataProvider.instance.iconController.sink.add( - StreamResponse( - _tokenVerificationResponse, - UpdateEvents.TOKEN_VERIFIED, - '', - ), - ); - SirenDataProvider.instance.inboxController.sink.add( - StreamResponse( - _tokenVerificationResponse, - UpdateEvents.TOKEN_VERIFIED, - '', - ), - ); + triggerEvent(UpdateEvents.TOKEN_VERIFIED); } else { if (_retryCount < Generics.MAX_RETRIES && _tokenVerificationStatus != Status.SUCCESS) { @@ -96,7 +84,7 @@ class SirenDataProvider { SirenDataProvider.instance.recipientId.isEmpty) { _retryCount = Generics.MAX_RETRIES; _tokenVerificationStatus = Status.INVALID_CREDENTIALS; - triggerError(); + triggerEvent(UpdateEvents.SHOW_ERROR); return; } Future.delayed( @@ -105,39 +93,40 @@ class SirenDataProvider { ); } else if (_retryCount >= Generics.MAX_RETRIES) { _tokenVerificationStatus = Status.FAILED; - triggerError(); + triggerEvent(UpdateEvents.SHOW_ERROR); } } } - void triggerError() { + void triggerEvent(UpdateEvents event) { SirenDataProvider.instance.inboxController.sink.add( StreamResponse( _tokenVerificationResponse, - UpdateEvents.SHOW_ERROR, + event, '', ), ); SirenDataProvider.instance.iconController.sink.add( StreamResponse( _tokenVerificationResponse, - UpdateEvents.SHOW_ERROR, + event, '', ), ); } + /// Return error code based on the token verification status value SirenErrorType getVerificationErrorType() { if (_tokenVerificationStatus == Status.PENDING) { - return Generics.outsideSirenContextError; + return Errors.outsideSirenContextError; } else if (_tokenVerificationStatus == Status.IN_PROGRESS) { - return Generics.authenticationPending; + return Errors.authenticationPending; } else if (_tokenVerificationStatus == Status.FAILED) { - return Generics.unauthorizedOperationError; + return Errors.unauthorizedOperationError; } else if (_tokenVerificationStatus == Status.INVALID_CREDENTIALS) { - return Generics.invalidCredentialsError; + return Errors.invalidCredentialsError; } - return Generics.authenticationFailed; + return Errors.authenticationFailed; } /// Disposes the icon controller. diff --git a/lib/src/errors/error_codes.dart b/lib/src/errors/error_codes.dart deleted file mode 100644 index a4765b7..0000000 --- a/lib/src/errors/error_codes.dart +++ /dev/null @@ -1,12 +0,0 @@ -// ignore_for_file: constant_identifier_names - -enum ErrorCode { - ERR_INTERNAL_SERVER, - ERR_UNAUTHORIZED, - ERR_DUPLICATE_GROUP_NAME, - ERR_NETWORK_ERROR; - - String toJson() => name; - - static ErrorCode fromJson(String json) => values.byName(json); -} diff --git a/lib/src/errors/errors.dart b/lib/src/errors/errors.dart new file mode 100644 index 0000000..a64b2fa --- /dev/null +++ b/lib/src/errors/errors.dart @@ -0,0 +1,80 @@ +import 'package:sirenapp_flutter_inbox/sirenapp_flutter_inbox.dart'; +import 'package:sirenapp_flutter_inbox/src/constants/generics.dart'; +import 'package:sirenapp_flutter_inbox/src/constants/strings.dart'; + +class Errors { + static final defaultError = SirenErrorType( + code: ErrorCodes.API_ERROR.name, + type: Strings.error_type_error, + message: Strings.something_went_wrong, + ); + + static final authenticationFailed = SirenErrorType( + code: ErrorCodes.AUTHENTICATION_FAILED.name, + type: Strings.error_type_error, + message: Strings.authenticationFailed, + ); + + static final fetchUnViewedCountFailedError = SirenErrorType( + code: ErrorCodes.UNVIEWED_COUNT_FETCH_FAILED.name, + type: Strings.error_type_error, + message: Strings.fetchUnViewedCountFailedError, + ); + + static final notificationFetchFailedError = SirenErrorType( + code: ErrorCodes.NOTIFICATION_FETCH_FAILED.name, + type: Strings.error_type_error, + message: Strings.notificationFetchFailedError, + ); + + static final markAsReadFailedError = SirenErrorType( + code: ErrorCodes.MARK_AS_READ_FAILED.name, + type: Strings.error_type_error, + message: Strings.markAsReadFailedError, + ); + + static final deleteFailedError = SirenErrorType( + code: ErrorCodes.DELETE_FAILED.name, + type: Strings.error_type_error, + message: Strings.deleteFailedError, + ); + + static final deleteAllFailedError = SirenErrorType( + code: ErrorCodes.BULK_DELETE_FAILED.name, + type: Strings.error_type_error, + message: Strings.deleteAllFailedError, + ); + + static final markAllAsViewedError = SirenErrorType( + code: ErrorCodes.MARK_ALL_AS_VIEWED_FAILED.name, + type: Strings.error_type_error, + message: Strings.markAllAsViewedError, + ); + + static final outsideSirenContextError = SirenErrorType( + code: ErrorCodes.OUTSIDE_SIREN_CONTEXT.name, + type: Strings.error_type_error, + message: Strings.outsideSirenContextError, + ); + + static final authenticationPending = SirenErrorType( + code: ErrorCodes.AUTHENTICATION_PENDING.name, + type: Strings.error_type_error, + message: Strings.authenticationPending, + ); + + static final unauthorizedOperationError = SirenErrorType( + code: ErrorCodes.UNAUTHORIZED_OPERATION.name, + type: Strings.error_type_error, + message: Strings.unauthorizedOperationError, + ); + + static final invalidCredentialsError = SirenErrorType( + code: ErrorCodes.INVALID_CREDENTIALS.name, + type: Strings.error_type_error, + message: Strings.invalidCredentialsError, + ); + + static const rawResponseError = + '{"data": null,"error": "AUTHENTICATION FAILED","errors":null,"meta":null}'; +} diff --git a/lib/src/widgets/siren_inbox.dart b/lib/src/widgets/siren_inbox.dart index a4a9674..ffd5712 100644 --- a/lib/src/widgets/siren_inbox.dart +++ b/lib/src/widgets/siren_inbox.dart @@ -10,6 +10,7 @@ import 'package:sirenapp_flutter_inbox/src/api/notifications_bulk_update.dart'; import 'package:sirenapp_flutter_inbox/src/api/read_notification_by_id.dart'; import 'package:sirenapp_flutter_inbox/src/constants/generics.dart'; import 'package:sirenapp_flutter_inbox/src/data/siren_data_provider.dart'; +import 'package:sirenapp_flutter_inbox/src/errors/errors.dart'; import 'package:sirenapp_flutter_inbox/src/theme/app_theme.dart'; import 'package:sirenapp_flutter_inbox/src/utils/common_utils.dart'; import 'package:sirenapp_flutter_inbox/src/widgets/app_bar.dart'; @@ -117,7 +118,7 @@ class _SirenInboxState extends State { } else if (SirenDataProvider.instance.tokenVerificationStatus == Status.FAILED || !SirenDataProvider.instance.isProviderInitialized) { - widget.onError?.call(Generics.outsideSirenContextError); + widget.onError?.call(Errors.outsideSirenContextError); if (mounted) { setState(() { isError = true; diff --git a/lib/src/widgets/siren_inbox_icon.dart b/lib/src/widgets/siren_inbox_icon.dart index 9be2a7c..84f221e 100644 --- a/lib/src/widgets/siren_inbox_icon.dart +++ b/lib/src/widgets/siren_inbox_icon.dart @@ -6,6 +6,7 @@ import 'package:sirenapp_flutter_inbox/sirenapp_flutter_inbox.dart'; import 'package:sirenapp_flutter_inbox/src/api/fetch_unviewed_notification_count.dart'; import 'package:sirenapp_flutter_inbox/src/constants/generics.dart'; import 'package:sirenapp_flutter_inbox/src/data/siren_data_provider.dart'; +import 'package:sirenapp_flutter_inbox/src/errors/errors.dart'; import 'package:sirenapp_flutter_inbox/src/theme/app_theme.dart'; import 'package:sirenapp_flutter_inbox/src/widgets/icon_badge.dart'; @@ -162,7 +163,7 @@ class _SirenInboxIconState extends State { widget.onError?.call(response.error ?? SirenErrorType()); } } else if (!SirenDataProvider.instance.isProviderInitialized) { - widget.onError?.call(Generics.outsideSirenContextError); + widget.onError?.call(Errors.outsideSirenContextError); } } diff --git a/test/constants/generics_test.dart b/test/constants/generics_test.dart index de6aadd..49956b1 100644 --- a/test/constants/generics_test.dart +++ b/test/constants/generics_test.dart @@ -1,5 +1,6 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:sirenapp_flutter_inbox/src/constants/generics.dart'; +import 'package:sirenapp_flutter_inbox/src/errors/errors.dart'; void main() { group('Generics', () { @@ -10,10 +11,10 @@ void main() { expect(Generics.PAGE_SIZE, 20); expect(Generics.MAX_RETRIES, 2); expect(Generics.ENV_PATH, 'packages/sirenapp_flutter_inbox/env'); - expect(Generics.defaultError.code, ErrorCodes.API_ERROR.name); - expect(Generics.defaultError.type, 'ERROR'); + expect(Errors.defaultError.code, ErrorCodes.API_ERROR.name); + expect(Errors.defaultError.type, 'ERROR'); expect( - Generics.defaultError.message, + Errors.defaultError.message, 'Something went wrong', ); }); diff --git a/test/delete_notification_by_id_test.dart b/test/delete_notification_by_id_test.dart index 84a728b..0daf9c4 100644 --- a/test/delete_notification_by_id_test.dart +++ b/test/delete_notification_by_id_test.dart @@ -2,6 +2,7 @@ import 'package:dio/dio.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:sirenapp_flutter_inbox/src/constants/generics.dart'; import 'package:sirenapp_flutter_inbox/src/data/siren_data_provider.dart'; +import 'package:sirenapp_flutter_inbox/src/errors/errors.dart'; import 'package:sirenapp_flutter_inbox/src/models/api_response.dart'; import 'package:sirenapp_flutter_inbox/src/services/api_client.dart'; import 'package:sirenapp_flutter_inbox/src/services/api_provider.dart'; @@ -74,7 +75,7 @@ class DeleteNotificationById { ..isSuccess = false ..isError = true ..rawResponse = apiResponse - ..error = Generics.defaultError; + ..error = Errors.defaultError; } return result; From c93e7124365cab7e3ca59bda219c813c9a8546d9 Mon Sep 17 00:00:00 2001 From: Anitta Babu <99161914+anitta-keyvalue@users.noreply.github.com> Date: Tue, 7 May 2024 18:31:54 +0530 Subject: [PATCH 08/17] feat: Add media support in list view item --- README.md | 42 ++++----- lib/src/api/delete_notification_by_id.dart | 5 +- lib/src/api/fetch_all_notification.dart | 5 +- lib/src/api/read_notification_by_id.dart | 6 +- lib/src/data/siren_data_provider.dart | 6 +- lib/src/models/notification_model.dart | 7 ++ lib/src/models/ui_models.dart | 8 ++ lib/src/widgets/card.dart | 51 +++++++++- lib/src/widgets/media_error_widget.dart | 103 +++++++++++++++++++++ test/widgets/card_test.dart | 10 ++ test/widgets/media_error_widget_test.dart | 25 +++++ 11 files changed, 235 insertions(+), 33 deletions(-) create mode 100644 lib/src/widgets/media_error_widget.dart create mode 100644 test/widgets/media_error_widget_test.dart diff --git a/README.md b/README.md index 1b062dc..48c1fae 100644 --- a/README.md +++ b/README.md @@ -98,20 +98,20 @@ Inbox is a paginated list view for displaying notifications. Given below are the arguments of Siren Inbox Widget. -| Arguments | Description | Type | Default value | -| ----------------- | -------------------------------------------------------------------- | -------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| darkMode | Toggle to enable dark mode when custom theme is not passed | bool | false | -| itemsPerFetch | Number of notifications fetch per api request (have a max cap of 50) | int | 20 | -| listEmptyWidget | Custom widget for empty notification list | Widget | null | -| customCard | Custom widget to display the notification cards | Widget | null | -| customLoader | Custom widget to display the initial loading state | Widget | null | -| customErrorWidget | Custom error widget | Widget | null | -| cardParams | Properties of notification card | CardParams | CardParams(hideAvatar: false, disableAutoMarkAsRead: false, hideDelete: false, deleteIcon: Icon(Icons.close), onAvatarClick: Function(NotificationType)) | -| headerParams | Properties of notification window header | HeaderParams | HeaderParams(hideHeader: false, hideClearAll: false,title: 'Notifications', customHeader: null showBackButton:false, backButton: null, onBackPress: ()=> null ) | -| onCardClick | Custom click handler for notification cards | Function(NotificationType) | null | -| onError | Callback for handling errors | Function(SirenErrorType) | null | -| theme | Theme properties for custom color theme | CustomThemeColors | null | -| customStyles | Style properties for custom styling | CustomStyles | null | +| Arguments | Description | Type | Default value | +| ----------------- | -------------------------------------------------------------------- | -------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| darkMode | Toggle to enable dark mode when custom theme is not passed | bool | false | +| itemsPerFetch | Number of notifications fetch per api request (have a max cap of 50) | int | 20 | +| listEmptyWidget | Custom widget for empty notification list | Widget | null | +| customCard | Custom widget to display the notification cards | Widget | null | +| customLoader | Custom widget to display the initial loading state | Widget | null | +| customErrorWidget | Custom error widget | Widget | null | +| cardParams | Properties of notification card | CardParams | CardParams(hideAvatar: false, disableAutoMarkAsRead: false, hideDelete: false, deleteIcon: Icon(Icons.close), onAvatarClick: Function(NotificationType), hideMediaThumbnail: false, onMediaThumbnailClick: Function(NotificationType)) | +| headerParams | Properties of notification window header | HeaderParams | HeaderParams(hideHeader: false, hideClearAll: false,title: 'Notifications', customHeader: null showBackButton:false, backButton: null, onBackPress: ()=> null ) | +| onCardClick | Custom click handler for notification cards | Function(NotificationType) | null | +| onError | Callback for handling errors | Function(SirenErrorType) | null | +| theme | Theme properties for custom color theme | CustomThemeColors | null | +| customStyles | Style properties for custom styling | CustomStyles | null | #### Theme customization @@ -176,13 +176,13 @@ The `Siren Class` provides utility functions for modifying notifications. Siren.markAsRead(id: 'notification-id'); ``` -| Function | Arguments | Type | Description | -| -------------------------------- | --------- | --------------- | -------------------------------------------------------------------- | -| markAsReadByDate | startDate | ISO date string | Sets the read status of notifications to true until the given date | -| markAsReadById | id | string | Set read status of a notification to true | -| deleteById | id | string | Delete a notification by id | -| deleteByDate | startDate | ISO date string | Delete all notifications until given date | -| markAllAsViewed | startDate | ISO date string | Sets the viewed status of notifications to true until the given date | +| Function | Arguments | Type | Description | +| ---------------- | --------- | --------------- | -------------------------------------------------------------------- | +| markAsReadByDate | startDate | ISO date string | Sets the read status of notifications to true until the given date | +| markAsReadById | id | string | Set read status of a notification to true | +| deleteById | id | string | Delete a notification by id | +| deleteByDate | startDate | ISO date string | Delete all notifications until given date | +| markAllAsViewed | startDate | ISO date string | Sets the viewed status of notifications to true until the given date | ## Example diff --git a/lib/src/api/delete_notification_by_id.dart b/lib/src/api/delete_notification_by_id.dart index cb5eb58..cc6d356 100644 --- a/lib/src/api/delete_notification_by_id.dart +++ b/lib/src/api/delete_notification_by_id.dart @@ -19,12 +19,11 @@ class DeleteNotificationById { final ApiClient api = ApiClient(apiProvider()); - static final String _apiPath = - '${Generics.V2}${Generics.BASE_URL}${SirenDataProvider.instance.recipientId}/notifications'; - Future deleteNotificationById({ required String notificationId, }) async { + final _apiPath = + '${Generics.V2}${Generics.BASE_URL}${SirenDataProvider.instance.recipientId}/notifications'; final result = ApiResponse()..isLoading = true; var apiError = Errors.deleteFailedError; diff --git a/lib/src/api/fetch_all_notification.dart b/lib/src/api/fetch_all_notification.dart index aa44a69..74b1527 100644 --- a/lib/src/api/fetch_all_notification.dart +++ b/lib/src/api/fetch_all_notification.dart @@ -12,9 +12,6 @@ class FetchAllNotifications { FetchAllNotifications._internal(); final ApiClient api = ApiClient(apiProvider()); - static final String _apiPath = - '${Generics.V2}${Generics.BASE_URL}${SirenDataProvider.instance.recipientId}/notifications'; - List convertJsonToNotificationList( List dataList, ) { @@ -33,6 +30,8 @@ class FetchAllNotifications { String? start, String? end, }) async { + final _apiPath = + '${Generics.V2}${Generics.BASE_URL}${SirenDataProvider.instance.recipientId}/notifications'; final result = ApiResponse()..isLoading = true; var apiError = Errors.notificationFetchFailedError; diff --git a/lib/src/api/read_notification_by_id.dart b/lib/src/api/read_notification_by_id.dart index ad5762e..5c65dd7 100644 --- a/lib/src/api/read_notification_by_id.dart +++ b/lib/src/api/read_notification_by_id.dart @@ -11,15 +11,13 @@ class ReadNotificationById { final ApiClient api = ApiClient(apiProvider()); - static final String _apiPath = - '${Generics.V2}${Generics.BASE_URL}${SirenDataProvider.instance.recipientId}/notifications'; - Future readNotificationById({ required String notificationId, }) async { final result = ApiResponse()..isLoading = true; var apiError = Errors.markAsReadFailedError; - + final _apiPath = + '${Generics.V2}${Generics.BASE_URL}${SirenDataProvider.instance.recipientId}/notifications'; if (SirenDataProvider.instance.tokenVerificationStatus != Status.SUCCESS) { apiError = SirenDataProvider.instance.getVerificationErrorType(); result diff --git a/lib/src/data/siren_data_provider.dart b/lib/src/data/siren_data_provider.dart index 5efc791..d85167d 100644 --- a/lib/src/data/siren_data_provider.dart +++ b/lib/src/data/siren_data_provider.dart @@ -89,7 +89,11 @@ class SirenDataProvider { } Future.delayed( const Duration(seconds: Generics.DATA_FETCH_INTERVAL), - _verifyToken, + () { + if (_tokenVerificationStatus != Status.SUCCESS) { + _verifyToken(); + } + }, ); } else if (_retryCount >= Generics.MAX_RETRIES) { _tokenVerificationStatus = Status.FAILED; diff --git a/lib/src/models/notification_model.dart b/lib/src/models/notification_model.dart index 9a34121..9ee9bd7 100644 --- a/lib/src/models/notification_model.dart +++ b/lib/src/models/notification_model.dart @@ -59,6 +59,7 @@ class MessageData { required this.actionUrl, required this.avatar, required this.additionalData, + this.thumbnailUrl, this.subHeader, }); @@ -73,6 +74,9 @@ class MessageData { avatar: json?['avatar'] != null ? AvatarData.fromJson(json?['avatar'] as Map) : null, + thumbnailUrl: json?['thumbnailUrl'] != null + ? (json?['thumbnailUrl'] as String?) + : '', additionalData: json?['additionalData'] as String?, ); } @@ -97,6 +101,9 @@ class MessageData { /// Additional data related to the message. final String? additionalData; + + /// The thumbnail URL associated with the message to display + final String? thumbnailUrl; } /// Class representing the data structure of an avatar. diff --git a/lib/src/models/ui_models.dart b/lib/src/models/ui_models.dart index 2d7b192..6b44ea6 100644 --- a/lib/src/models/ui_models.dart +++ b/lib/src/models/ui_models.dart @@ -10,6 +10,8 @@ class CardParams { this.deleteIcon, this.hideDelete, this.onAvatarClick, + this.hideMediaThumbnail, + this.onMediaThumbnailClick, }); /// Determines whether to hide the avatar in the notification card in Siren inbox. @@ -26,6 +28,12 @@ class CardParams { /// Callback function when a notification card is clicked. final void Function(NotificationType)? onAvatarClick; + + /// The flag to show media thumbnail + final bool? hideMediaThumbnail; + + /// Callback function when a thumbnail media is clicked. + final void Function(NotificationType)? onMediaThumbnailClick; } /// Customizable style for the Siren notification icon. diff --git a/lib/src/widgets/card.dart b/lib/src/widgets/card.dart index 6b7305d..5651a48 100644 --- a/lib/src/widgets/card.dart +++ b/lib/src/widgets/card.dart @@ -6,6 +6,7 @@ import 'package:sirenapp_flutter_inbox/src/theme/app_colors.dart'; import 'package:sirenapp_flutter_inbox/src/theme/app_theme.dart'; import 'package:sirenapp_flutter_inbox/src/utils/common_utils.dart'; import 'package:sirenapp_flutter_inbox/src/widgets/common/nullable_text.dart'; +import 'package:sirenapp_flutter_inbox/src/widgets/media_error_widget.dart'; class CardWidget extends StatefulWidget { /// Widget for displaying a notification card. @@ -55,7 +56,7 @@ class _CardWidgetState extends State { Widget build(BuildContext context) { final defaultColors = SirenAppTheme.colors(isDarkMode: widget.isDarkMode ?? false); - + final thumbnailUrl = widget.notification.message.thumbnailUrl ?? ''; return GestureDetector( key: Key('siren-notification-card-${widget.notification.id}'), onTap: () { @@ -80,6 +81,10 @@ class _CardWidgetState extends State { _buildHeaderText(widget.colors, defaultColors), _buildSubHeaderText(widget.colors, defaultColors), _buildBodyText(widget.colors, defaultColors), + if (thumbnailUrl.isNotEmpty && + thumbnailUrl != Strings.string_null && + !(widget.cardParams.hideMediaThumbnail ?? false)) + _buildMediaContent(defaultColors, thumbnailUrl), _buildFooterRow( widget.colors, defaultColors, @@ -241,6 +246,50 @@ class _CardWidgetState extends State { ); } + Widget _buildMediaContent(AppColors defaultColors, String url) { + return Column( + children: [ + const SizedBox( + height: 10, + ), + GestureDetector( + onTap: () { + if (widget.cardParams.onMediaThumbnailClick != null) { + widget.cardParams.onMediaThumbnailClick + ?.call(widget.notification); + } + }, + child: Container( + height: 140, + width: double.infinity, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(6), + color: defaultColors.avatarBackground, + ), + child: ClipRRect( + borderRadius: BorderRadius.circular(6), + child: Image.network( + url, + height: 140, + width: double.infinity, + fit: BoxFit.cover, + errorBuilder: ( + _, + Object exception, + StackTrace? stackTrace, + ) { + return MediaErrorWidget( + isDarkMode: widget.isDarkMode ?? false, + ); + }, + ), + ), + ), + ), + ], + ); + } + Widget _buildFooterRow( CustomThemeColors? colors, AppColors defaultColors, diff --git a/lib/src/widgets/media_error_widget.dart b/lib/src/widgets/media_error_widget.dart new file mode 100644 index 0000000..4696233 --- /dev/null +++ b/lib/src/widgets/media_error_widget.dart @@ -0,0 +1,103 @@ +// ignore_for_file: cascade_invocations + +import 'package:flutter/material.dart'; +import 'package:sirenapp_flutter_inbox/src/theme/app_theme.dart'; + +class MediaErrorWidget extends StatelessWidget { + const MediaErrorWidget({ + required this.isDarkMode, + super.key, + }); + + final bool isDarkMode; + @override + Widget build(BuildContext context) { + final defaultColors = SirenAppTheme.colors(isDarkMode: isDarkMode); + return Align( + child: Stack( + children: [ + Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular( + 6, + ), + border: Border.all( + color: defaultColors.avatarIconColor, + width: 3, + ), + ), + width: 30, + height: 27, + ), + Padding( + padding: const EdgeInsets.only(left: 2), + child: Icon( + Icons.landscape_sharp, + size: 27, + color: defaultColors.avatarIconColor, + ), + ), + CustomPaint( + size: const Size(30, 27), + painter: DiagonalPainter( + defaultColors.avatarBackground, + defaultColors.avatarIconColor, + ), + ), + ], + ), + ); + } +} + +class DiagonalPainter extends CustomPainter { + DiagonalPainter(this.lightLineColor, this.darkLineColor); + final Color lightLineColor; + final Color darkLineColor; + + @override + void paint( + Canvas canvas, + Size size, + ) { + const lineWidth = 3.0; + const gap = 4.0; + + final lightColorPaint = Paint() + ..color = lightLineColor + ..strokeWidth = lineWidth + ..style = PaintingStyle.stroke + ..strokeCap = StrokeCap.round; + + final darkColorPaint = Paint() + ..color = darkLineColor + ..strokeWidth = lineWidth + ..style = PaintingStyle.stroke + ..strokeCap = StrokeCap.round; + + final lightColorPath = Path(); + lightColorPath + ..moveTo( + size.width + 2, + size.height + 2, + ) + ..lineTo(-2, -2); + + final darkColorPath = Path(); + darkColorPath + ..moveTo( + size.width + gap, + size.height, + ) + ..lineTo(-2 + gap, -2); + + canvas + ..drawPath(lightColorPath, lightColorPaint) + ..drawPath(darkColorPath, darkColorPaint); + } + + @override + bool shouldRepaint(CustomPainter oldDelegate) { + return false; + } +} diff --git a/test/widgets/card_test.dart b/test/widgets/card_test.dart index 9e27a8e..b5c4ee1 100644 --- a/test/widgets/card_test.dart +++ b/test/widgets/card_test.dart @@ -24,6 +24,7 @@ void main() { body: 'Test Body', channel: 'Test Channel', actionUrl: 'Test Action Url', + thumbnailUrl: 'https://picsum.photos/200/300', avatar: AvatarData( altText: 'Test alt text', url: 'https://picsum.photos/200/300', @@ -36,6 +37,7 @@ void main() { ); var deletePressed = false; + var thumbnailPressed = false; await mockNetworkImagesFor(() async { await tester.pumpWidget( MaterialApp( @@ -48,6 +50,10 @@ void main() { cardParams: CardParams( hideAvatar: false, hideDelete: false, + hideMediaThumbnail: false, + onMediaThumbnailClick: (NotificationType notification) { + thumbnailPressed = true; + }, onAvatarClick: (notification) { func(); }, @@ -75,6 +81,10 @@ void main() { await tester.tap(find.byType(GestureDetector).at(2)); await tester.pumpAndSettle(const Duration(seconds: 1)); + await tester.tap(find.byType(GestureDetector).last); + await tester.pumpAndSettle(const Duration(seconds: 1)); + expect(thumbnailPressed, true); + final textFinder = find.byType(Text).at(0); final textWidget = tester.widget(textFinder); final textColor = textWidget.style?.color; diff --git a/test/widgets/media_error_widget_test.dart b/test/widgets/media_error_widget_test.dart new file mode 100644 index 0000000..f7b50ed --- /dev/null +++ b/test/widgets/media_error_widget_test.dart @@ -0,0 +1,25 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:sirenapp_flutter_inbox/src/widgets/media_error_widget.dart'; + +void main() { + testWidgets('MediaErrorWidget should render correctly', + (WidgetTester tester) async { + await tester.pumpWidget( + const MaterialApp( + home: Scaffold( + body: MediaErrorWidget( + isDarkMode: false, + ), + ), + ), + ); + + expect(find.byType(Align), findsOneWidget); + expect(find.byType(Stack), findsWidgets); + expect(find.byType(Container), findsOneWidget); + expect(find.byType(Padding), findsWidgets); + expect(find.byType(Icon), findsOneWidget); + expect(find.byType(CustomPaint), findsWidgets); + }); +} From fb6294ee8054daf7807b56f75fa6019b7c77e14d Mon Sep 17 00:00:00 2001 From: Anitta Babu <99161914+anitta-keyvalue@users.noreply.github.com> Date: Wed, 8 May 2024 20:24:01 +0530 Subject: [PATCH 09/17] feat: Custom styles for delete icon, timer icon and clear all icon --- README.md | 16 +++++++------- lib/src/models/ui_models.dart | 41 +++++++++++++++++++++++++++++------ lib/src/widgets/app_bar.dart | 2 +- lib/src/widgets/card.dart | 4 ++-- 4 files changed, 45 insertions(+), 18 deletions(-) diff --git a/README.md b/README.md index 48c1fae..7eda372 100644 --- a/README.md +++ b/README.md @@ -157,14 +157,14 @@ customStyles: CustomStyles( dateStyle: TextStyle(fontSize: 16, fontWeight: FontWeight.w500), avatarSize: 30, ), - appBarStyle: InboxHeaderStyle( - headerTextStyle: - TextStyle(fontSize: 20, fontWeight: FontWeight.w900), - titlePadding: EdgeInsets.symmetric(horizontal: 30), - borderWidth: 5), - dateIconSize: 30, - deleteIconSize: 30, - clearAllIconSize: 40 + appBarStyle: InboxHeaderStyle( + headerTextStyle: + TextStyle(fontSize: 20, fontWeight: FontWeight.w900), + titlePadding: EdgeInsets.symmetric(horizontal: 30), + borderWidth: 5), + timerIconStyle: TimerIconStyle(size: 30), + deleteIconStyle: DeleteIconStyle(size: 30), + clearAllIconStyle: ClearAllIconStyle(size: 30), ), ``` diff --git a/lib/src/models/ui_models.dart b/lib/src/models/ui_models.dart index 6b44ea6..a19f99a 100644 --- a/lib/src/models/ui_models.dart +++ b/lib/src/models/ui_models.dart @@ -95,9 +95,9 @@ class CustomStyles { this.appBarStyle, this.notificationIconStyle, this.badgeStyle, - this.deleteIconSize, - this.dateIconSize, - this.clearAllIconSize, + this.timerIconStyle, + this.deleteIconStyle, + this.clearAllIconStyle, }); /// The decoration for the Siren inbox list. @@ -115,14 +115,41 @@ class CustomStyles { /// The style for the notification icon badge. final BadgeStyle? badgeStyle; + /// Style of delete icon in inbox list card + final TimerIconStyle? timerIconStyle; + + /// Style of delete icon in inbox list card + final DeleteIconStyle? deleteIconStyle; + + /// Style of clear all icon in inbox default header + final ClearAllIconStyle? clearAllIconStyle; +} + +class TimerIconStyle { + TimerIconStyle({ + this.size, + }); + + /// Size of timer icon in inbox list card + final double? size; +} + +class DeleteIconStyle { + DeleteIconStyle({ + this.size, + }); + /// Size of delete icon in inbox list card - final double? deleteIconSize; + final double? size; +} - /// Size of date icon in inbox list card - final double? dateIconSize; +class ClearAllIconStyle { + ClearAllIconStyle({ + this.size, + }); /// Size of clear all icon in inbox default header - final double? clearAllIconSize; + final double? size; } /// Custom theme colors to configure the appearance of UI elements. diff --git a/lib/src/widgets/app_bar.dart b/lib/src/widgets/app_bar.dart index 9d1ed7e..272fa0b 100644 --- a/lib/src/widgets/app_bar.dart +++ b/lib/src/widgets/app_bar.dart @@ -107,7 +107,7 @@ class SirenAppBar extends StatelessWidget implements PreferredSizeWidget { padding: const EdgeInsets.only(right: 4), child: Icon( Icons.clear_all, - size: styles?.clearAllIconSize ?? 24, + size: styles?.clearAllIconStyle?.size ?? 24, color: colors?.clearAllIcon ?? defaultColors.appBarActionText, ), diff --git a/lib/src/widgets/card.dart b/lib/src/widgets/card.dart index 5651a48..e0c869b 100644 --- a/lib/src/widgets/card.dart +++ b/lib/src/widgets/card.dart @@ -88,7 +88,7 @@ class _CardWidgetState extends State { _buildFooterRow( widget.colors, defaultColors, - widget.styles?.dateIconSize ?? 14, + widget.styles?.timerIconStyle?.size ?? 14, ), ], ), @@ -202,7 +202,7 @@ class _CardWidgetState extends State { _buildDefaultDeleteButton( colors, defaultColors, - widget.styles?.deleteIconSize ?? 18, + widget.styles?.deleteIconStyle?.size ?? 18, ), ), ], From 212930192530f8ea7e07022143cd31929519504f Mon Sep 17 00:00:00 2001 From: Anitta Babu <99161914+anitta-keyvalue@users.noreply.github.com> Date: Tue, 14 May 2024 17:42:09 +0530 Subject: [PATCH 10/17] fix: Add margin right for thumbnail --- lib/src/widgets/card.dart | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/src/widgets/card.dart b/lib/src/widgets/card.dart index e0c869b..804fb15 100644 --- a/lib/src/widgets/card.dart +++ b/lib/src/widgets/card.dart @@ -261,6 +261,7 @@ class _CardWidgetState extends State { }, child: Container( height: 140, + margin: const EdgeInsets.only(right: 10), width: double.infinity, decoration: BoxDecoration( borderRadius: BorderRadius.circular(6), From 3cdad0a88e72f2c700614f2b5ecf1ed95fed92bd Mon Sep 17 00:00:00 2001 From: Anitta Babu <99161914+anitta-keyvalue@users.noreply.github.com> Date: Wed, 15 May 2024 11:10:25 +0530 Subject: [PATCH 11/17] fix: Fix for style and theme issues --- lib/src/theme/app_colors.dart | 4 +++- lib/src/theme/dark_colors.dart | 3 ++- lib/src/theme/light_colors.dart | 3 ++- lib/src/widgets/app_bar.dart | 6 +++++- lib/src/widgets/card.dart | 32 ++++++++++++++++++++++++++----- lib/src/widgets/empty_widget.dart | 2 +- 6 files changed, 40 insertions(+), 10 deletions(-) diff --git a/lib/src/theme/app_colors.dart b/lib/src/theme/app_colors.dart index 5254423..78730ac 100644 --- a/lib/src/theme/app_colors.dart +++ b/lib/src/theme/app_colors.dart @@ -27,6 +27,7 @@ class AppColors { required this.emptyWidgetBorderColor, required this.emptyWidgetIconColor, required this.emptyWidgetNotificationColor, + required this.emptyWidgetNotificationIconColor, required this.errorWidgetIconColor, required this.errorWidgetIconContainer, required this.errorWidgetText1, @@ -63,12 +64,13 @@ class AppColors { Color clearAllIcon; Color dateColor; Color deleteIcon; - Color emptyScreenTitle; Color emptyScreenDescription; + Color emptyScreenTitle; Color emptyWidgetBackground; Color emptyWidgetBorderColor; Color emptyWidgetIconColor; Color emptyWidgetNotificationColor; + Color emptyWidgetNotificationIconColor; Color errorWidgetIconColor; Color errorWidgetIconContainer; Color errorWidgetText1; diff --git a/lib/src/theme/dark_colors.dart b/lib/src/theme/dark_colors.dart index 06a5fd2..8ac18fa 100644 --- a/lib/src/theme/dark_colors.dart +++ b/lib/src/theme/dark_colors.dart @@ -26,6 +26,7 @@ final darkColors = AppColors( emptyWidgetBorderColor: SirenAppColors.emptyWidgetBgDarkTheme, emptyWidgetIconColor: SirenAppColors.emptyWidgetBadgeDark, emptyWidgetNotificationColor: SirenAppColors.emptyWidgetBadgeDark, + emptyWidgetNotificationIconColor: SirenAppColors.emptyWidgetBadgeDark, errorWidgetIconColor: SirenAppColors.emptyWidgetBellDark, errorWidgetIconContainer: SirenAppColors.emptyWidgetBgDarkTheme, errorWidgetText1: SirenAppColors.grey700Complementary, @@ -34,7 +35,7 @@ final darkColors = AppColors( loaderColor: SirenAppColors.primary200Complementary, loadingIndicator: SirenAppColors.primary200Complementary, loadingIndicatorBackground: SirenAppColors.emptyWidgetBgDarkTheme, - notificationIconColor: SirenAppColors.emptyWidgetBadgeDark, + notificationIconColor: Colors.white, primary: SirenAppColors.primary200Complementary, scaffoldBackgroundColor: SirenAppColors.black100, skeletonLoaderColor: SirenAppColors.avatarPlaceholderBgDark, diff --git a/lib/src/theme/light_colors.dart b/lib/src/theme/light_colors.dart index 2c8ad28..9c9e622 100644 --- a/lib/src/theme/light_colors.dart +++ b/lib/src/theme/light_colors.dart @@ -26,6 +26,7 @@ final lightColors = AppColors( emptyWidgetBorderColor: SirenAppColors.emptyWidgetBgLightTheme, emptyWidgetIconColor: SirenAppColors.emptyWidgetBellLight, emptyWidgetNotificationColor: SirenAppColors.emptyWidgetBadgeLight, + emptyWidgetNotificationIconColor: SirenAppColors.emptyWidgetBadgeLight, errorWidgetIconColor: SirenAppColors.emptyWidgetBellLight, errorWidgetIconContainer: SirenAppColors.emptyWidgetBgLightTheme, errorWidgetText1: SirenAppColors.grey700, @@ -34,7 +35,7 @@ final lightColors = AppColors( loaderColor: SirenAppColors.primary200, loadingIndicator: SirenAppColors.primary200, loadingIndicatorBackground: SirenAppColors.emptyWidgetBgLightTheme, - notificationIconColor: SirenAppColors.emptyWidgetBadgeLight, + notificationIconColor: SirenAppColors.black100, primary: SirenAppColors.primary200, scaffoldBackgroundColor: Colors.white, skeletonLoaderColor: SirenAppColors.avatarPlaceholderBgLight, diff --git a/lib/src/widgets/app_bar.dart b/lib/src/widgets/app_bar.dart index 272fa0b..c8709eb 100644 --- a/lib/src/widgets/app_bar.dart +++ b/lib/src/widgets/app_bar.dart @@ -75,7 +75,11 @@ class SirenAppBar extends StatelessWidget implements PreferredSizeWidget { styles?.appBarStyle?.titlePadding ?? EdgeInsets.zero, child: Text( headerParams?.title ?? Strings.notifications, - style: styles?.appBarStyle?.headerTextStyle ?? + style: styles?.appBarStyle?.headerTextStyle?.copyWith( + color: colors?.inboxHeaderColors?.titleColor ?? + colors?.textColor ?? + defaultColors.appBarTextColor, + ) ?? TextStyle( fontSize: 18, fontWeight: FontWeight.w600, diff --git a/lib/src/widgets/card.dart b/lib/src/widgets/card.dart index 804fb15..7f3f5e0 100644 --- a/lib/src/widgets/card.dart +++ b/lib/src/widgets/card.dart @@ -63,7 +63,13 @@ class _CardWidgetState extends State { widget.onTap(widget.notification); }, child: Container( - decoration: widget.styles?.cardStyle?.cardContainer?.decoration ?? + decoration: widget.styles?.cardStyle?.cardContainer?.decoration + ?.copyWith( + color: widget.notification.isRead + ? widget.colors?.cardColors?.background ?? Colors.transparent + : widget.colors?.highlightedCardColor ?? + defaultColors.cardBackgroundUnread, + ) ?? _getDefaultContainerDecoration(widget.colors, defaultColors), padding: widget.styles?.cardStyle?.cardContainer?.padding ?? const EdgeInsets.symmetric(vertical: 12, horizontal: 12), @@ -181,7 +187,11 @@ class _CardWidgetState extends State { widget.notification.message.header ?? '', maxLines: 2, overflow: TextOverflow.ellipsis, - style: widget.styles?.cardStyle?.cardTitle ?? + style: widget.styles?.cardStyle?.cardTitle?.copyWith( + color: colors?.cardColors?.titleColor ?? + colors?.textColor ?? + defaultColors.textColor, + ) ?? TextStyle( fontSize: 14, fontWeight: FontWeight.w600, @@ -218,7 +228,11 @@ class _CardWidgetState extends State { padding: const EdgeInsets.symmetric(vertical: 6), child: NullableText( text: widget.notification.message.subHeader, - style: widget.styles?.cardStyle?.cardSubtitle ?? + style: widget.styles?.cardStyle?.cardSubtitle?.copyWith( + color: colors?.cardColors?.subtitleColor ?? + colors?.textColor ?? + defaultColors.textColor, + ) ?? TextStyle( fontSize: 14, fontWeight: FontWeight.w500, @@ -233,7 +247,11 @@ class _CardWidgetState extends State { Widget _buildBodyText(CustomThemeColors? colors, AppColors defaultColors) { return Text( widget.notification.message.body ?? '', - style: widget.styles?.cardStyle?.cardDescription ?? + style: widget.styles?.cardStyle?.cardDescription?.copyWith( + color: colors?.cardColors?.descriptionColor ?? + colors?.textColor ?? + defaultColors.textColor, + ) ?? TextStyle( fontSize: 14, fontWeight: FontWeight.w400, @@ -325,7 +343,11 @@ class _CardWidgetState extends State { generateElapsedTimeText( DateTime.parse(widget.notification.createdAt), ), - style: widget.styles?.cardStyle?.dateStyle ?? + style: widget.styles?.cardStyle?.dateStyle?.copyWith( + color: colors?.dateColor ?? + colors?.textColor ?? + defaultColors.dateColor, + ) ?? TextStyle( fontSize: 12, fontWeight: FontWeight.w400, diff --git a/lib/src/widgets/empty_widget.dart b/lib/src/widgets/empty_widget.dart index e321af8..4c3d836 100644 --- a/lib/src/widgets/empty_widget.dart +++ b/lib/src/widgets/empty_widget.dart @@ -77,7 +77,7 @@ Widget _buildCircle(AppColors colors) { child: Container( padding: const EdgeInsets.all(8), decoration: BoxDecoration( - color: colors.notificationIconColor, + color: colors.emptyWidgetNotificationIconColor, shape: BoxShape.circle, border: Border.all( color: colors.emptyWidgetBorderColor, From 8a49b6a7e76b03f9714ee296bc0a51dc649c3bca Mon Sep 17 00:00:00 2001 From: Anitta Babu <99161914+anitta-keyvalue@users.noreply.github.com> Date: Wed, 15 May 2024 14:08:32 +0530 Subject: [PATCH 12/17] docs: Add changelog for v1.1.0 --- CHANGELOG.md | 11 +++++++++++ pubspec.yaml | 2 +- 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 69b1e7a..2dbd6ca 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,4 +8,15 @@ This is the first public release of the package. ### Added - Flutter UI kit for displaying and managing in-app notifications. +## 1.1.0 +This is an update to the package. + +### Added +- Added support for custom delete icon and a flag to toggle the visibility of the delete icon. +- Added functionality to display thumbnail URL previews for media content. +- Exposed avatar click property. +- Implemented specific error code mapping. +- Enhanced style and theme customizations. + + diff --git a/pubspec.yaml b/pubspec.yaml index 045c036..82252c8 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: sirenapp_flutter_inbox description: "Flutter SDK tailored for creating and managing in-app notification inboxes." -version: 1.0.0 +version: 1.1.0 homepage: 'https://github.com/KeyValueSoftwareSystems/siren-flutter-inbox#readme' repository: 'https://github.com/KeyValueSoftwareSystems/siren-flutter-inbox' license: MIT From 8dc018c9e7147185819b8547a464d0e62985d35c Mon Sep 17 00:00:00 2001 From: Anitta Babu <99161914+anitta-keyvalue@users.noreply.github.com> Date: Wed, 15 May 2024 14:11:47 +0530 Subject: [PATCH 13/17] Dev -> Staging Release v1.1.0 --- CHANGELOG.md | 11 +++++++++++ lib/src/theme/app_colors.dart | 4 +++- lib/src/theme/dark_colors.dart | 3 ++- lib/src/theme/light_colors.dart | 3 ++- lib/src/widgets/app_bar.dart | 6 +++++- lib/src/widgets/card.dart | 32 ++++++++++++++++++++++++++----- lib/src/widgets/empty_widget.dart | 2 +- pubspec.yaml | 2 +- 8 files changed, 52 insertions(+), 11 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 69b1e7a..2dbd6ca 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,4 +8,15 @@ This is the first public release of the package. ### Added - Flutter UI kit for displaying and managing in-app notifications. +## 1.1.0 +This is an update to the package. + +### Added +- Added support for custom delete icon and a flag to toggle the visibility of the delete icon. +- Added functionality to display thumbnail URL previews for media content. +- Exposed avatar click property. +- Implemented specific error code mapping. +- Enhanced style and theme customizations. + + diff --git a/lib/src/theme/app_colors.dart b/lib/src/theme/app_colors.dart index 5254423..78730ac 100644 --- a/lib/src/theme/app_colors.dart +++ b/lib/src/theme/app_colors.dart @@ -27,6 +27,7 @@ class AppColors { required this.emptyWidgetBorderColor, required this.emptyWidgetIconColor, required this.emptyWidgetNotificationColor, + required this.emptyWidgetNotificationIconColor, required this.errorWidgetIconColor, required this.errorWidgetIconContainer, required this.errorWidgetText1, @@ -63,12 +64,13 @@ class AppColors { Color clearAllIcon; Color dateColor; Color deleteIcon; - Color emptyScreenTitle; Color emptyScreenDescription; + Color emptyScreenTitle; Color emptyWidgetBackground; Color emptyWidgetBorderColor; Color emptyWidgetIconColor; Color emptyWidgetNotificationColor; + Color emptyWidgetNotificationIconColor; Color errorWidgetIconColor; Color errorWidgetIconContainer; Color errorWidgetText1; diff --git a/lib/src/theme/dark_colors.dart b/lib/src/theme/dark_colors.dart index 06a5fd2..8ac18fa 100644 --- a/lib/src/theme/dark_colors.dart +++ b/lib/src/theme/dark_colors.dart @@ -26,6 +26,7 @@ final darkColors = AppColors( emptyWidgetBorderColor: SirenAppColors.emptyWidgetBgDarkTheme, emptyWidgetIconColor: SirenAppColors.emptyWidgetBadgeDark, emptyWidgetNotificationColor: SirenAppColors.emptyWidgetBadgeDark, + emptyWidgetNotificationIconColor: SirenAppColors.emptyWidgetBadgeDark, errorWidgetIconColor: SirenAppColors.emptyWidgetBellDark, errorWidgetIconContainer: SirenAppColors.emptyWidgetBgDarkTheme, errorWidgetText1: SirenAppColors.grey700Complementary, @@ -34,7 +35,7 @@ final darkColors = AppColors( loaderColor: SirenAppColors.primary200Complementary, loadingIndicator: SirenAppColors.primary200Complementary, loadingIndicatorBackground: SirenAppColors.emptyWidgetBgDarkTheme, - notificationIconColor: SirenAppColors.emptyWidgetBadgeDark, + notificationIconColor: Colors.white, primary: SirenAppColors.primary200Complementary, scaffoldBackgroundColor: SirenAppColors.black100, skeletonLoaderColor: SirenAppColors.avatarPlaceholderBgDark, diff --git a/lib/src/theme/light_colors.dart b/lib/src/theme/light_colors.dart index 2c8ad28..9c9e622 100644 --- a/lib/src/theme/light_colors.dart +++ b/lib/src/theme/light_colors.dart @@ -26,6 +26,7 @@ final lightColors = AppColors( emptyWidgetBorderColor: SirenAppColors.emptyWidgetBgLightTheme, emptyWidgetIconColor: SirenAppColors.emptyWidgetBellLight, emptyWidgetNotificationColor: SirenAppColors.emptyWidgetBadgeLight, + emptyWidgetNotificationIconColor: SirenAppColors.emptyWidgetBadgeLight, errorWidgetIconColor: SirenAppColors.emptyWidgetBellLight, errorWidgetIconContainer: SirenAppColors.emptyWidgetBgLightTheme, errorWidgetText1: SirenAppColors.grey700, @@ -34,7 +35,7 @@ final lightColors = AppColors( loaderColor: SirenAppColors.primary200, loadingIndicator: SirenAppColors.primary200, loadingIndicatorBackground: SirenAppColors.emptyWidgetBgLightTheme, - notificationIconColor: SirenAppColors.emptyWidgetBadgeLight, + notificationIconColor: SirenAppColors.black100, primary: SirenAppColors.primary200, scaffoldBackgroundColor: Colors.white, skeletonLoaderColor: SirenAppColors.avatarPlaceholderBgLight, diff --git a/lib/src/widgets/app_bar.dart b/lib/src/widgets/app_bar.dart index 272fa0b..c8709eb 100644 --- a/lib/src/widgets/app_bar.dart +++ b/lib/src/widgets/app_bar.dart @@ -75,7 +75,11 @@ class SirenAppBar extends StatelessWidget implements PreferredSizeWidget { styles?.appBarStyle?.titlePadding ?? EdgeInsets.zero, child: Text( headerParams?.title ?? Strings.notifications, - style: styles?.appBarStyle?.headerTextStyle ?? + style: styles?.appBarStyle?.headerTextStyle?.copyWith( + color: colors?.inboxHeaderColors?.titleColor ?? + colors?.textColor ?? + defaultColors.appBarTextColor, + ) ?? TextStyle( fontSize: 18, fontWeight: FontWeight.w600, diff --git a/lib/src/widgets/card.dart b/lib/src/widgets/card.dart index 804fb15..7f3f5e0 100644 --- a/lib/src/widgets/card.dart +++ b/lib/src/widgets/card.dart @@ -63,7 +63,13 @@ class _CardWidgetState extends State { widget.onTap(widget.notification); }, child: Container( - decoration: widget.styles?.cardStyle?.cardContainer?.decoration ?? + decoration: widget.styles?.cardStyle?.cardContainer?.decoration + ?.copyWith( + color: widget.notification.isRead + ? widget.colors?.cardColors?.background ?? Colors.transparent + : widget.colors?.highlightedCardColor ?? + defaultColors.cardBackgroundUnread, + ) ?? _getDefaultContainerDecoration(widget.colors, defaultColors), padding: widget.styles?.cardStyle?.cardContainer?.padding ?? const EdgeInsets.symmetric(vertical: 12, horizontal: 12), @@ -181,7 +187,11 @@ class _CardWidgetState extends State { widget.notification.message.header ?? '', maxLines: 2, overflow: TextOverflow.ellipsis, - style: widget.styles?.cardStyle?.cardTitle ?? + style: widget.styles?.cardStyle?.cardTitle?.copyWith( + color: colors?.cardColors?.titleColor ?? + colors?.textColor ?? + defaultColors.textColor, + ) ?? TextStyle( fontSize: 14, fontWeight: FontWeight.w600, @@ -218,7 +228,11 @@ class _CardWidgetState extends State { padding: const EdgeInsets.symmetric(vertical: 6), child: NullableText( text: widget.notification.message.subHeader, - style: widget.styles?.cardStyle?.cardSubtitle ?? + style: widget.styles?.cardStyle?.cardSubtitle?.copyWith( + color: colors?.cardColors?.subtitleColor ?? + colors?.textColor ?? + defaultColors.textColor, + ) ?? TextStyle( fontSize: 14, fontWeight: FontWeight.w500, @@ -233,7 +247,11 @@ class _CardWidgetState extends State { Widget _buildBodyText(CustomThemeColors? colors, AppColors defaultColors) { return Text( widget.notification.message.body ?? '', - style: widget.styles?.cardStyle?.cardDescription ?? + style: widget.styles?.cardStyle?.cardDescription?.copyWith( + color: colors?.cardColors?.descriptionColor ?? + colors?.textColor ?? + defaultColors.textColor, + ) ?? TextStyle( fontSize: 14, fontWeight: FontWeight.w400, @@ -325,7 +343,11 @@ class _CardWidgetState extends State { generateElapsedTimeText( DateTime.parse(widget.notification.createdAt), ), - style: widget.styles?.cardStyle?.dateStyle ?? + style: widget.styles?.cardStyle?.dateStyle?.copyWith( + color: colors?.dateColor ?? + colors?.textColor ?? + defaultColors.dateColor, + ) ?? TextStyle( fontSize: 12, fontWeight: FontWeight.w400, diff --git a/lib/src/widgets/empty_widget.dart b/lib/src/widgets/empty_widget.dart index e321af8..4c3d836 100644 --- a/lib/src/widgets/empty_widget.dart +++ b/lib/src/widgets/empty_widget.dart @@ -77,7 +77,7 @@ Widget _buildCircle(AppColors colors) { child: Container( padding: const EdgeInsets.all(8), decoration: BoxDecoration( - color: colors.notificationIconColor, + color: colors.emptyWidgetNotificationIconColor, shape: BoxShape.circle, border: Border.all( color: colors.emptyWidgetBorderColor, diff --git a/pubspec.yaml b/pubspec.yaml index 045c036..82252c8 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: sirenapp_flutter_inbox description: "Flutter SDK tailored for creating and managing in-app notification inboxes." -version: 1.0.0 +version: 1.1.0 homepage: 'https://github.com/KeyValueSoftwareSystems/siren-flutter-inbox#readme' repository: 'https://github.com/KeyValueSoftwareSystems/siren-flutter-inbox' license: MIT From 1efa93d79c063daac3371a415cdeec81cd1d99a2 Mon Sep 17 00:00:00 2001 From: Anitta Babu <99161914+anitta-keyvalue@users.noreply.github.com> Date: Wed, 15 May 2024 14:25:37 +0530 Subject: [PATCH 14/17] format: Format changelog --- CHANGELOG.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2dbd6ca..ba12a49 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,12 +2,6 @@ All notable changes to this project will be documented in this file. -## 1.0.0 -This is the first public release of the package. - -### Added -- Flutter UI kit for displaying and managing in-app notifications. - ## 1.1.0 This is an update to the package. @@ -18,5 +12,11 @@ This is an update to the package. - Implemented specific error code mapping. - Enhanced style and theme customizations. +## 1.0.0 +This is the first public release of the package. + +### Added +- Flutter UI kit for displaying and managing in-app notifications. + From 2ba7b1624a9d045452e9b889b868968921e416c0 Mon Sep 17 00:00:00 2001 From: Anitta Babu <99161914+anitta-keyvalue@users.noreply.github.com> Date: Wed, 15 May 2024 14:49:12 +0530 Subject: [PATCH 15/17] format: Format changelog --- CHANGELOG.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ba12a49..a4bc96b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,8 +3,6 @@ All notable changes to this project will be documented in this file. ## 1.1.0 -This is an update to the package. - ### Added - Added support for custom delete icon and a flag to toggle the visibility of the delete icon. - Added functionality to display thumbnail URL previews for media content. From 14cbd5fa209364edad4b3e3c8b065148ed71c2f5 Mon Sep 17 00:00:00 2001 From: Anitta Babu <99161914+anitta-keyvalue@users.noreply.github.com> Date: Wed, 15 May 2024 15:51:33 +0530 Subject: [PATCH 16/17] fix: Update branch name --- .github/workflows/test.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 1bc53e7..8b75340 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -2,9 +2,9 @@ name: test on: push: - branches: [ main, staging, dev ] + branches: [ master, staging, dev ] pull_request: - branches: [ main, staging, dev ] + branches: [ master, staging, dev ] jobs: build: From c2fae6656e85072ae0a1db35e85911fb0c6457b1 Mon Sep 17 00:00:00 2001 From: Anitta Babu Date: Wed, 15 May 2024 16:02:42 +0530 Subject: [PATCH 17/17] docs: Update changelog --- CHANGELOG.md | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7f63d65..2695459 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ All notable changes to this project will be documented in this file. ## 1.1.0 + ### Added - Added support for custom delete icon and a flag to toggle the visibility of the delete icon. - Added functionality to display thumbnail URL previews for media content. @@ -16,15 +17,6 @@ This is the first public release of the package. ### Added - Flutter UI kit for displaying and managing in-app notifications. -## 1.1.0 -This is an update to the package. - -### Added -- Added support for custom delete icon and a flag to toggle the visibility of the delete icon. -- Added functionality to display thumbnail URL previews for media content. -- Exposed avatar click property. -- Implemented specific error code mapping. -- Enhanced style and theme customizations.