diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index b8a15b3..8b75340 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -2,9 +2,9 @@ name: test on: push: - branches: [ main, dev ] + branches: [ master, staging, dev ] pull_request: - branches: [ main, dev ] + branches: [ master, staging, dev ] jobs: build: diff --git a/CHANGELOG.md b/CHANGELOG.md index 69b1e7a..2695459 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,15 @@ 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. +- Exposed avatar click property. +- Implemented specific error code mapping. +- Enhanced style and theme customizations. + ## 1.0.0 This is the first public release of the package. @@ -9,3 +18,6 @@ This is the first public release of the package. - Flutter UI kit for displaying and managing in-app notifications. + + + diff --git a/README.md b/README.md index 2d4fbf1..7eda372 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 @@ -21,17 +24,16 @@ 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(), + userToken: 'YOUR_USER_TOKEN', + recipientId: 'YOUR_RECIPIENT_ID', + child: MyApp(), ), ); } ``` ### 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,168 +41,151 @@ 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 | 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 + 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 + Here are the custom style options for the notification icon: ```dart -customStyles: SirenStyleProps( - iconStyle: IconStyle(size: 35), - badgeStyle: BadgeStyle( - fontSize: 10, - size: 18, - inset: 1, - top: 2, - right: 0, - )) + customStyles: CustomStyles( + notificationIconStyle: NotificationIconStyle(size: 20), + badgeStyle: BadgeStyle(fontSize: 9, size: 5), + ) ``` ### 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) () { - // Handle error - }, -); + SirenInbox( + headerParams: HeaderParams(showBackButton: true), + cardParams: CardParams(hideAvatar: false), + onError: (error) { + // Handle Error + }, + ) ``` + #### 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 | 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 -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), - 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), - ), + 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 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), + timerIconStyle: TimerIconStyle(size: 30), + deleteIconStyle: DeleteIconStyle(size: 30), + clearAllIconStyle: ClearAllIconStyle(size: 30), +), ``` ## 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 | - -## 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 | +| 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 + Here's a basic example to help you get started ```dart @@ -222,8 +207,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( @@ -264,4 +249,4 @@ class _MyHomePageState extends State { ); } } -``` \ No newline at end of file +``` 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/.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/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/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 db66449..b68ab16 100644 --- a/example/pubspec.lock +++ b/example/pubspec.lock @@ -45,18 +45,18 @@ packages: 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: 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: @@ -248,5 +248,5 @@ packages: source: hosted version: "13.0.0" sdks: - dart: ">=3.2.3 <4.0.0" - flutter: ">=2.0.1" + dart: ">=3.2.0-0 <4.0.0" + flutter: ">=3.0.0" diff --git a/example/pubspec.yaml b/example/pubspec.yaml index d226fa1..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: '>=3.2.3 <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 @@ -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..cc6d356 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'; @@ -18,23 +19,21 @@ 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; - final apiError = ApiErrorDetails() - ..errorType = ErrorTypes.NOTIFICATION_DELETE_FAILED; + var apiError = Errors.deleteFailedError; if (SirenDataProvider.instance.tokenVerificationStatus != Status.SUCCESS) { - apiError.errorType = ErrorTypes.AUTHENTICATION_FAILED; + apiError = SirenDataProvider.instance.getVerificationErrorType(); result ..isLoading = false ..isError = true ..data = null - ..rawResponse = Generics.rawResponseError + ..rawResponse = Errors.rawResponseError ..error = apiError; return result; } @@ -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 @@ -61,7 +56,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 b08cf7a..74b1527 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'; @@ -11,15 +12,12 @@ 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 convertJsonToNotificationList( List dataList, ) { - return dataList.map((json) { + return dataList.map((dynamic json) { if (json is Map) { - return NotificationDataType.fromJson(json); + return NotificationType.fromJson(json); } throw const FormatException('Invalid JSON format'); }).toList(); @@ -32,9 +30,10 @@ class FetchAllNotifications { String? start, String? end, }) async { + final _apiPath = + '${Generics.V2}${Generics.BASE_URL}${SirenDataProvider.instance.recipientId}/notifications'; final result = ApiResponse()..isLoading = true; - final apiError = ApiErrorDetails() - ..errorType = ErrorTypes.NOTIFICATION_FETCH_FAILED; + var apiError = Errors.notificationFetchFailedError; // Manually construct query parameters final queryParams = { @@ -54,12 +53,12 @@ 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 ..data = null - ..rawResponse = Generics.rawResponseError + ..rawResponse = Errors.rawResponseError ..error = apiError; return result; } @@ -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 @@ -89,7 +85,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 48b137c..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,16 +20,15 @@ class FetchUnViewedNotificationsCount { Future fetchUnViewedNotificationsCount() async { final result = ApiResponse()..isLoading = true; - final apiError = ApiErrorDetails() - ..errorType = ErrorTypes.FETCH_COUNT_FAILED; + var apiError = Errors.fetchUnViewedCountFailedError; if (SirenDataProvider.instance.tokenVerificationStatus != Status.SUCCESS) { - apiError.errorType = ErrorTypes.AUTHENTICATION_FAILED; + apiError = SirenDataProvider.instance.getVerificationErrorType(); result ..isLoading = false ..isError = true ..data = null - ..rawResponse = Generics.rawResponseError + ..rawResponse = Errors.rawResponseError ..error = apiError; return result; } @@ -43,9 +43,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 @@ -59,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 8b8096e..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,20 +19,19 @@ class MarkAllNotificationsAsViewed { }) async { final api = ApiClient(apiProvider()); final result = ApiResponse()..isLoading; - final apiError = ApiErrorDetails() - ..errorType = ErrorTypes.UPDATE_VIEWED_FAILED; + var apiError = Errors.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 ..data = null - ..rawResponse = Generics.rawResponseError + ..rawResponse = Errors.rawResponseError ..error = apiError; return result; } @@ -42,9 +42,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 @@ -57,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 b7f4135..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'; @@ -15,21 +16,25 @@ 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 = Errors.markAsReadFailedError; + + if (operation == BulkUpdateType.MARK_AS_DELETED.name) { + apiError = Errors.deleteAllFailedError; + } if (SirenDataProvider.instance.tokenVerificationStatus != Status.SUCCESS) { - apiError.errorType = ErrorTypes.AUTHENTICATION_FAILED; + apiError = SirenDataProvider.instance.getVerificationErrorType(); result ..isLoading = false ..isError = true ..data = null - ..rawResponse = Generics.rawResponseError + ..rawResponse = Errors.rawResponseError ..error = apiError; return result; } @@ -39,10 +44,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 @@ -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 659f153..5c65dd7 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'; @@ -10,23 +11,20 @@ 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; - final apiError = ApiErrorDetails() - ..errorType = ErrorTypes.NOTIFICATION_READ_FAILED; - + var apiError = Errors.markAsReadFailedError; + final _apiPath = + '${Generics.V2}${Generics.BASE_URL}${SirenDataProvider.instance.recipientId}/notifications'; if (SirenDataProvider.instance.tokenVerificationStatus != Status.SUCCESS) { - apiError.errorType = ErrorTypes.AUTHENTICATION_FAILED; + apiError = SirenDataProvider.instance.getVerificationErrorType(); result ..isLoading = false ..isError = true ..data = null - ..rawResponse = Generics.rawResponseError + ..rawResponse = Errors.rawResponseError ..error = apiError; return result; } @@ -39,9 +37,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 @@ -54,7 +49,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 2cfef3c..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,8 +25,19 @@ class VerifyToken { Future verifyToken() async { final result = ApiResponse()..isLoading = true; - final apiError = ApiErrorDetails() - ..errorType = ErrorTypes.AUTHENTICATION_FAILED; + var apiError = Errors.authenticationFailed; + + if (SirenDataProvider.instance.userToken.isEmpty || + SirenDataProvider.instance.recipientId.isEmpty) { + apiError = Errors.invalidCredentialsError; + result + ..isLoading = false + ..isError = true + ..data = null + ..rawResponse = Errors.rawResponseError + ..error = apiError; + return result; + } final apiResponse = await api.get( path: @@ -34,9 +46,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 @@ -50,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 bf69202..25a7282 100644 --- a/lib/src/constants/generics.dart +++ b/lib/src/constants/generics.dart @@ -1,5 +1,3 @@ -import 'package:sirenapp_flutter_inbox/sirenapp_flutter_inbox.dart'; - class Generics { Generics._(); @@ -8,24 +6,17 @@ 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'; - - 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 const rawResponseError = - '{"data": null,"error": "AUTHENTICATION FAILED","errors":null,"meta":null}'; } enum Status { PENDING, SUCCESS, FAILED, + IN_PROGRESS, + INVALID_CREDENTIALS, } enum BulkUpdateType { @@ -34,22 +25,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 38dcf0f..f334f53 100644 --- a/lib/src/constants/strings.dart +++ b/lib/src/constants/strings.dart @@ -4,6 +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..d85167d 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. @@ -32,6 +33,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 +47,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,52 +70,69 @@ class SirenDataProvider { /// Verifies the user token. Future _verifyToken() async { + _tokenVerificationStatus = Status.IN_PROGRESS; _tokenVerificationResponse = await VerifyToken.instance.verifyToken(); 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) { - _tokenVerificationStatus = Status.FAILED; _retryCount++; + if (SirenDataProvider.instance.userToken.isEmpty || + SirenDataProvider.instance.recipientId.isEmpty) { + _retryCount = Generics.MAX_RETRIES; + _tokenVerificationStatus = Status.INVALID_CREDENTIALS; + triggerEvent(UpdateEvents.SHOW_ERROR); + return; + } Future.delayed( const Duration(seconds: Generics.DATA_FETCH_INTERVAL), - _verifyToken, + () { + if (_tokenVerificationStatus != Status.SUCCESS) { + _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; + triggerEvent(UpdateEvents.SHOW_ERROR); } } } + void triggerEvent(UpdateEvents event) { + SirenDataProvider.instance.inboxController.sink.add( + StreamResponse( + _tokenVerificationResponse, + event, + '', + ), + ); + SirenDataProvider.instance.iconController.sink.add( + StreamResponse( + _tokenVerificationResponse, + event, + '', + ), + ); + } + + /// Return error code based on the token verification status value + SirenErrorType getVerificationErrorType() { + if (_tokenVerificationStatus == Status.PENDING) { + return Errors.outsideSirenContextError; + } else if (_tokenVerificationStatus == Status.IN_PROGRESS) { + return Errors.authenticationPending; + } else if (_tokenVerificationStatus == Status.FAILED) { + return Errors.unauthorizedOperationError; + } else if (_tokenVerificationStatus == Status.INVALID_CREDENTIALS) { + return Errors.invalidCredentialsError; + } + return Errors.authenticationFailed; + } + /// Disposes the icon controller. void iconDispose() { _iconController.close(); 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/models/api_response.dart b/lib/src/models/api_response.dart index f2ec793..9000bb8 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; @@ -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,37 +86,33 @@ 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. -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..9ee9bd7 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), @@ -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 010c0f5..a19f99a 100644 --- a/lib/src/models/ui_models.dart +++ b/lib/src/models/ui_models.dart @@ -1,24 +1,45 @@ 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.showMedia, + this.disableAutoMarkAsRead, + this.deleteIcon, + this.hideDelete, + this.onAvatarClick, + this.hideMediaThumbnail, + this.onMediaThumbnailClick, }); /// 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? deleteIcon; + + /// 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(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. -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; @@ -29,11 +50,8 @@ 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 => 18; + static double get defaultSize => 20; /// Default top position for the badge count. static double get defaultTop => 0; @@ -50,7 +68,6 @@ class BadgeStyle { /// Constructs a [BadgeStyle] with optional parameters. const BadgeStyle({ this.fontSize, - this.inset, this.size, this.top, this.right, @@ -59,9 +76,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; @@ -73,58 +87,69 @@ 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.timerIconStyle, + this.deleteIconStyle, + this.clearAllIconStyle, }); - /// The decoration for the outer container of the card in Siren inbox. - final BoxDecoration? container; + /// The decoration for the Siren inbox list. + final ContainerStyle? container; - /// The decoration for the content container of the card in Siren inbox. - final BoxDecoration? contentContainer; + // The styles for inbox list item + final CardStyle? cardStyle; - /// The text style for the sub-header text in Siren inbox. - final TextStyle? subHeaderText; + /// The style for default app bar + final InboxHeaderStyle? appBarStyle; - /// The decoration for the avatar container of the card in Siren inbox. - final BoxDecoration? cardAvatarContainer; + /// The style for the notification icon. + final NotificationIconStyle? notificationIconStyle; - /// The decoration for the content container of the card in Siren inbox. - final BoxDecoration? cardContentContainer; + /// The style for the notification icon badge. + final BadgeStyle? badgeStyle; - /// The text style for the card title in Siren inbox. - final TextStyle? cardTitle; + /// Style of delete icon in inbox list card + final TimerIconStyle? timerIconStyle; - /// The text style for the card description in Siren inbox. - final TextStyle? cardDescription; + /// Style of delete icon in inbox list card + final DeleteIconStyle? deleteIconStyle; - /// The decoration for the footer row of the card in Siren inbox. - final BoxDecoration? cardFooterRow; + /// Style of clear all icon in inbox default header + final ClearAllIconStyle? clearAllIconStyle; +} - /// The text style for the date text in Siren inbox. - final TextStyle? dateStyle; +class TimerIconStyle { + TimerIconStyle({ + this.size, + }); - /// The style for the notification icon. - final IconStyle? iconStyle; + /// Size of timer icon in inbox list card + final double? size; +} - /// The style for the notification icon badge. - final BadgeStyle? badgeStyle; +class DeleteIconStyle { + DeleteIconStyle({ + this.size, + }); - /// Text style for the header provided by the sdk. - final TextStyle? defaultHeaderTextStyle; + /// Size of delete icon in inbox list card + final double? size; +} + +class ClearAllIconStyle { + ClearAllIconStyle({ + this.size, + }); + + /// Size of clear all icon in inbox default header + final double? size; } /// Custom theme colors to configure the appearance of UI elements. @@ -132,7 +157,7 @@ class CustomThemeColors { /// Constructs a [CustomThemeColors] with optional parameters. CustomThemeColors({ this.backgroundColor, - this.highlightedCardBorderColor, + this.primary, this.highlightedCardColor, this.borderColor, this.deleteIcon, @@ -140,17 +165,18 @@ class CustomThemeColors { this.textColor, this.dateColor, this.timerIcon, - this.badgeBackgroundColor, - this.badgeColor, - this.iconColor, - this.inboxTitleColor, + this.notificationIconColor, + this.loaderColor, + 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; @@ -173,29 +199,169 @@ 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? loaderColor; - /// The color for notification icon. - final Color? iconColor; + /// The colors for inbox list card + final CardColors? cardColors; - /// The color for window title in Siren inbox. - final Color? inboxTitleColor; + /// The colors for inbox header + final InboxHeaderColors? inboxHeaderColors; + + /// The colors for inbox list card + final BadgeColors? badgeColors; } -/// Custom Properties for notification card -class CardParams { - CardParams({ - this.hideAvatar, - this.deleteWidget, +/// Custom theme colors to configure the appearance inbox list item. +class CardColors { + CardColors({ + this.borderColor, + this.background, + this.titleColor, + this.subtitleColor, + this.descriptionColor, }); - /// The Flag to hide or show avatar - final bool? hideAvatar; + /// The border color inbox of list item + final Color? borderColor; - /// Custom widget that can be used instead of default delete in the card (x) - final Widget? deleteWidget; + /// 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.backgroundColor, + this.color, + }); + + /// The icon badge background color + final Color? backgroundColor; + + /// The text color of icon badge + final Color? color; +} + +/// Properties for configuring the appearance of the notification window app bar. +class HeaderParams { + HeaderParams({ + this.title, + this.hideHeader, + this.showBackButton, + this.backButton, + this.hideClearAll, + this.customHeader, + this.onBackPress, + }); + + /// Title of the inbox page or window. + final String? title; + + /// 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; +} + +/// 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/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/theme/app_colors.dart b/lib/src/theme/app_colors.dart new file mode 100644 index 0000000..78730ac --- /dev/null +++ b/lib/src/theme/app_colors.dart @@ -0,0 +1,88 @@ +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.emptyWidgetNotificationIconColor, + 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 emptyScreenDescription; + Color emptyScreenTitle; + Color emptyWidgetBackground; + Color emptyWidgetBorderColor; + Color emptyWidgetIconColor; + Color emptyWidgetNotificationColor; + Color emptyWidgetNotificationIconColor; + 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 04a9e7f..40a6e86 100644 --- a/lib/src/theme/app_theme.dart +++ b/lib/src/theme/app_theme.dart @@ -1,91 +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( - background: AppColors.emptyWidgetBgLightTheme, - inversePrimary: AppColors.grey500, - onBackground: Colors.black, - onPrimary: AppColors.black100, - onSecondary: AppColors.avatarPlaceholderBgLight, - onTertiary: Colors.white, - 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, - surfaceVariant: AppColors.avatarIconLight, - tertiary: AppColors.grey700, - tertiaryContainer: AppColors.red, - ), - ); - - static ThemeData darkTheme = ThemeData.dark().copyWith( - colorScheme: ThemeData.dark().colorScheme.copyWith( - background: AppColors.emptyWidgetBgDarkTheme, - inversePrimary: AppColors.grey400, - onBackground: AppColors.grey50, - onPrimary: Colors.white, - onSecondary: AppColors.avatarPlaceholderBgDark, - onTertiary: Colors.white, - 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, - surfaceVariant: 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.iconColor ?? baseTheme.colorScheme.onPrimary, - onTertiary: customColors.badgeColor ?? 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, - 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, - ), - ); +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..8ac18fa --- /dev/null +++ b/lib/src/theme/dark_colors.dart @@ -0,0 +1,44 @@ +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, + emptyWidgetNotificationIconColor: 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: Colors.white, + 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..9c9e622 --- /dev/null +++ b/lib/src/theme/light_colors.dart @@ -0,0 +1,44 @@ +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, + emptyWidgetNotificationIconColor: 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.black100, + primary: SirenAppColors.primary200, + scaffoldBackgroundColor: Colors.white, + skeletonLoaderColor: SirenAppColors.avatarPlaceholderBgLight, + textColor: SirenAppColors.grey700, + timerIcon: SirenAppColors.grey500, +); 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 new file mode 100644 index 0000000..c8709eb --- /dev/null +++ b/lib/src/widgets/app_bar.dart @@ -0,0 +1,139 @@ +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.isNonEmptyNotifications, + this.onClearAllPressed, + this.headerParams, + this.styles, + this.colors, + this.isDarkMode, + super.key, + }); + final VoidCallback? onClearAllPressed; + final bool isNonEmptyNotifications; + final HeaderParams? headerParams; + final CustomStyles? styles; + final CustomThemeColors? colors; + final bool? isDarkMode; + + @override + Size get preferredSize { + return headerParams?.hideHeader ?? false + ? Size.zero + : const Size.fromHeight(kToolbarHeight); + } + + @override + Widget build(BuildContext context) { + if (headerParams?.hideHeader ?? false) { + return const SizedBox.shrink(); + } + final defaultColors = SirenAppTheme.colors(isDarkMode: isDarkMode ?? false); + return Container( + decoration: BoxDecoration( + color: colors?.inboxHeaderColors?.background ?? + defaultColors.backgroundColor, + border: Border( + bottom: BorderSide( + width: styles?.appBarStyle?.borderWidth ?? 1, + color: colors?.inboxHeaderColors?.borderColor ?? + defaultColors.appBarBorderColor, + ), + ), + ), + height: preferredSize.height, + child: Padding( + padding: const EdgeInsets.only(right: 16, left: 20), + child: headerParams?.customHeader ?? + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Row( + children: [ + if (headerParams?.showBackButton ?? false) + Semantics( + label: 'siren-header-back', + hint: 'Tap to view navigate back', + child: GestureDetector( + key: const Key('siren-header-back'), + onTap: headerParams?.onBackPress, + child: headerParams?.backButton ?? + Icon( + Icons.arrow_back_ios, + color: colors?.inboxHeaderColors?.titleColor ?? + defaultColors.appBarBackIcon, + size: 20, + ), + ), + ), + Padding( + padding: + styles?.appBarStyle?.titlePadding ?? EdgeInsets.zero, + child: Text( + headerParams?.title ?? Strings.notifications, + style: styles?.appBarStyle?.headerTextStyle?.copyWith( + color: colors?.inboxHeaderColors?.titleColor ?? + colors?.textColor ?? + defaultColors.appBarTextColor, + ) ?? + TextStyle( + fontSize: 18, + fontWeight: FontWeight.w600, + color: colors?.inboxHeaderColors?.titleColor ?? + colors?.textColor ?? + defaultColors.appBarTextColor, + ), + ), + ), + ], + ), + if (!(headerParams?.hideClearAll ?? false)) + Semantics( + label: 'siren-header-clear-all', + hint: 'Tap to clear all notifications', + child: GestureDetector( + key: const Key('siren-header-clear-all'), + 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?.clearAllIconStyle?.size ?? 24, + color: colors?.clearAllIcon ?? + defaultColors.appBarActionText, + ), + ), + Text( + Strings.clear_all, + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.w500, + color: colors?.inboxHeaderColors + ?.headerActionColor ?? + defaultColors.appBarActionText, + ), + ), + ], + ), + ), + ), + ), + ], + ), + ), + ); + } +} diff --git a/lib/src/widgets/card.dart b/lib/src/widgets/card.dart index 0306b48..7f3f5e0 100644 --- a/lib/src/widgets/card.dart +++ b/lib/src/widgets/card.dart @@ -1,38 +1,46 @@ 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'; +import 'package:sirenapp_flutter_inbox/src/widgets/media_error_widget.dart'; class CardWidget extends StatefulWidget { /// Widget for displaying a notification card. const CardWidget({ required this.onTap, required this.notification, - required this.cardProps, + required this.cardParams, required this.styles, required this.onDelete, + this.colors, + this.isDarkMode, super.key, - this.deleteWidget, }); /// Callback function invoked when the card is tapped. 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 SirenStyleProps? styles; + final CustomStyles? styles; /// Callback function invoked when the card is deleted. final void Function(String) onDelete; - /// Widget to be displayed for deletion, if provided. - final Widget? deleteWidget; + /// 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(); @@ -46,191 +54,321 @@ class _CardWidgetState extends State { @override Widget build(BuildContext context) { - final currentTheme = Theme.of(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: () { widget.onTap(widget.notification); }, child: Container( - 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), - ], - ), - ), + 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), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (!(widget.cardParams.hideAvatar ?? false)) + _buildDefaultAvatarContainer(widget.colors, defaultColors), + Expanded( + child: Padding( + padding: const EdgeInsets.only(left: 6), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _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, + widget.styles?.timerIconStyle?.size ?? 14, ), - ), - GestureDetector( - onTap: () => widget.onDelete(widget.notification.id), - child: widget.deleteWidget ?? - _buildDefaultDeleteButton(currentTheme), - ), - ], + ], + ), ), ), - ), + ], ), ), ); } - BorderSide _getDefaultBorderDecoration(ThemeData theme) { + BorderSide _getDefaultBorderDecoration( + CustomThemeColors? colors, + AppColors defaultColors, + ) { return BorderSide( - color: 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 - ? theme.colorScheme.primary - : theme.colorScheme.secondary, + ? Colors.transparent + : 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 - ? null - : 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 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( + key: Key('siren-notification-avatar-${widget.notification.id}'), + onTap: () { + widget.cardParams.onAvatarClick?.call(widget.notification); + }, + child: Padding( + padding: const EdgeInsets.only( + right: 6, + left: 6, + ), + child: CircleAvatar( + radius: widget.styles?.cardStyle?.avatarSize ?? 21, + backgroundImage: avatarUrl != null && + avatarUrl.isNotEmpty && + avatarUrl != Strings.string_null + ? NetworkImage(avatarUrl) + : null, + backgroundColor: defaultColors.avatarBackground, + child: avatarUrl == null || + avatarUrl.isEmpty || + avatarUrl == Strings.string_null + ? Icon( + Icons.landscape_rounded, + color: defaultColors.avatarIconColor, + ) + : 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, + Widget _buildHeaderText(CustomThemeColors? colors, AppColors defaultColors) { + 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?.copyWith( + color: colors?.cardColors?.titleColor ?? + colors?.textColor ?? + defaultColors.textColor, + ) ?? + TextStyle( + fontSize: 14, + fontWeight: FontWeight.w600, + color: colors?.cardColors?.titleColor ?? + colors?.textColor ?? + defaultColors.textColor, + ), ), + ), + 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.cardParams.deleteIcon ?? + _buildDefaultDeleteButton( + colors, + defaultColors, + widget.styles?.deleteIconStyle?.size ?? 18, + ), + ), + ], + ], ); } - Widget _buildSubHeaderText(ThemeData theme) { + Widget _buildSubHeaderText( + CustomThemeColors? colors, + AppColors defaultColors, + ) { 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?.copyWith( + color: colors?.cardColors?.subtitleColor ?? + colors?.textColor ?? + defaultColors.textColor, + ) ?? TextStyle( fontSize: 14, fontWeight: FontWeight.w500, - color: 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?.cardDescription ?? + style: widget.styles?.cardStyle?.cardDescription?.copyWith( + color: colors?.cardColors?.descriptionColor ?? + colors?.textColor ?? + defaultColors.textColor, + ) ?? TextStyle( fontSize: 14, fontWeight: FontWeight.w400, - color: theme.colorScheme.tertiary, + color: colors?.cardColors?.descriptionColor ?? + colors?.textColor ?? + defaultColors.textColor, ), maxLines: 2, overflow: TextOverflow.ellipsis, ); } - Widget _buildFooterRow(ThemeData theme) { + 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, + margin: const EdgeInsets.only(right: 10), + 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, + double size, + ) { return Padding( - padding: const EdgeInsets.symmetric( - vertical: 10, + padding: const EdgeInsets.only( + top: 10, ), child: Container( - decoration: widget.styles?.cardFooterRow, - child: _buildTimestampText(theme), + child: _buildTimestampText(colors, defaultColors, size), ), ); } - Widget _buildTimestampText(ThemeData theme) { + 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, - size: 14, + color: colors?.timerIcon ?? defaultColors.timerIcon, + size: size, ), ), Text( generateElapsedTimeText( DateTime.parse(widget.notification.createdAt), ), - style: widget.styles?.dateStyle ?? + style: widget.styles?.cardStyle?.dateStyle?.copyWith( + color: colors?.dateColor ?? + colors?.textColor ?? + defaultColors.dateColor, + ) ?? TextStyle( fontSize: 12, fontWeight: FontWeight.w400, - color: theme.colorScheme.inversePrimary, + color: colors?.dateColor ?? + colors?.textColor ?? + defaultColors.dateColor, ), ), ], ); } - Widget _buildDefaultDeleteButton(ThemeData theme) { + Widget _buildDefaultDeleteButton( + CustomThemeColors? colors, + AppColors defaultColors, + double size, + ) { return Icon( Icons.close, - color: theme.colorScheme.outlineVariant, - size: 18, + color: colors?.deleteIcon ?? defaultColors.deleteIcon, + size: size, ); } } diff --git a/lib/src/widgets/empty_widget.dart b/lib/src/widgets/empty_widget.dart index b4a0f24..4c3d836 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,24 +63,24 @@ Widget _buildCircle(ThemeData theme) { height: 160, decoration: BoxDecoration( shape: BoxShape.circle, - color: theme.colorScheme.background, + color: colors.emptyWidgetBackground, ), ), Icon( Icons.notifications, size: 84, - color: theme.colorScheme.shadow, + color: colors.emptyWidgetIconColor, ), Positioned( right: 50, top: 55, child: Container( - padding: const EdgeInsets.all(4), + padding: const EdgeInsets.all(8), decoration: BoxDecoration( - color: theme.colorScheme.surface, + color: colors.emptyWidgetNotificationIconColor, shape: BoxShape.circle, border: Border.all( - color: theme.colorScheme.background, + color: colors.emptyWidgetBorderColor, width: 3, ), ), diff --git a/lib/src/widgets/error_widget.dart b/lib/src/widgets/error_widget.dart index 1d2d587..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.background, + color: colors.errorWidgetIconContainer, ), child: Icon( Icons.warning_rounded, size: 84, - color: theme.colorScheme.surfaceTint, + color: colors.errorWidgetIconColor, ), ); } diff --git a/lib/src/widgets/icon_badge.dart b/lib/src/widgets/icon_badge.dart new file mode 100644 index 0000000..e5a3db6 --- /dev/null +++ b/lib/src/widgets/icon_badge.dart @@ -0,0 +1,52 @@ +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, + 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) { + 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: const EdgeInsets.all( + 1, + ), + decoration: BoxDecoration( + shape: BoxShape.circle, + color: badgeBackgroundColor, + ), + child: Align( + child: Text( + notificationsCount > 99 + ? '99+' + : notificationsCount.toString(), + style: TextStyle( + color: color, + fontSize: badgeStyle?.fontSize ?? + DefaultIconStyle.defaultFontSize, + ), + ), + ), + ), + ); + } +} diff --git a/lib/src/widgets/inbox_body.dart b/lib/src/widgets/inbox_body.dart new file mode 100644 index 0000000..b5d10f1 --- /dev/null +++ b/lib/src/widgets/inbox_body.dart @@ -0,0 +1,125 @@ +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'; +import 'package:sirenapp_flutter_inbox/src/widgets/notification_list_view.dart'; + +class InboxBody extends StatelessWidget { + const InboxBody({ + required this.isLoading, + required this.loadingNextPage, + required this.isError, + required this.notifications, + required this.deleteNotification, + required this.markAsRead, + required this.customCard, + required this.onCardClick, + required this.deletingNotificationId, + required this.disableAutoMarkAsRead, + required this.onRefresh, + required this.endReached, + required this.onEndReached, + required this.scrollController, + this.customErrorWidget, + this.customLoader, + this.customStyles, + this.cardParams, + this.listEmptyWidget, + this.colors, + this.isDarkMode, + super.key, + }); + final CustomThemeColors? colors; + 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(NotificationType)? customCard; + final void Function(NotificationType)? onCardClick; + final String? deletingNotificationId; + final bool disableAutoMarkAsRead; + final Future Function() onRefresh; + final Widget? customErrorWidget; + final Widget? customLoader; + final bool endReached; + final CustomStyles? customStyles; + final CardParams? cardParams; + 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: colors?.loaderColor ?? defaultColors.loadingIndicator, + backgroundColor: defaultColors.loadingIndicatorBackground, + onRefresh: onRefresh, + child: ListView( + physics: const AlwaysScrollableScrollPhysics(), + children: [ + 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 ?? + DefaultErrorWidget( + isDarkMode: isDarkMode, + ), + ), + ), + ), + ], + ), + ); + } else if (isLoading && !loadingNextPage) { + 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 ?? EmptyWidget(isDarkMode: isDarkMode), + ); + } else { + return Container( + decoration: customStyles?.container?.decoration, + padding: customStyles?.container?.padding, + child: NotificationListView( + isDarkMode: isDarkMode, + cardParams: cardParams, + colors: colors, + customCard: customCard, + customStyles: customStyles, + deletingNotificationId: deletingNotificationId, + endReached: endReached, + isLoading: isLoading, + loadingNextPage: loadingNextPage, + markAsRead: markAsRead, + notifications: notifications, + onCardClick: onCardClick, + 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 3f552a2..c31091b 100644 --- a/lib/src/widgets/loader_widget.dart +++ b/lib/src/widgets/loader_widget.dart @@ -1,10 +1,59 @@ 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, + 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: 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, + ), + ), + ); + }, + ); + } +} class CardLoaderWidget extends StatefulWidget { const CardLoaderWidget({ + required this.hideAvatar, + this.isDarkMode, super.key, }); + final bool hideAvatar; + final bool? isDarkMode; + @override CardLoaderWidgetState createState() => CardLoaderWidgetState(); } @@ -30,39 +79,96 @@ 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), ), - padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 24), + padding: const EdgeInsets.symmetric(vertical: 10, horizontal: 10), child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ - _buildAnimatedCircleAvatar(theme: currentTheme), + if (!widget.hideAvatar) + Padding( + padding: const EdgeInsets.only(right: 6, left: 6), + child: _buildAnimatedWidget( + builder: (context, child) => Container( + width: 42, + height: 42, + decoration: BoxDecoration( + shape: BoxShape.circle, + color: defaultColors.skeletonLoaderColor + .withOpacity(0.5 + 0.5 * _controller.value), + ), + ), + ), + ), Expanded( child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 24), + padding: const EdgeInsets.only( + right: 12, + ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - _buildAnimatedContainer(height: 18, theme: currentTheme), - const SizedBox(height: 8), - _buildAnimatedContainer(height: 18, theme: currentTheme), - const SizedBox(height: 8), - _buildAnimatedContainer(height: 18, theme: currentTheme), - const SizedBox(height: 8), + _buildAnimatedWidget( + builder: (context, child) => Container( + height: 18, + decoration: BoxDecoration( + color: defaultColors.skeletonLoaderColor + .withOpacity(0.5 + 0.5 * _controller.value), + borderRadius: BorderRadius.circular(4), + ), + ), + ), + const SizedBox(height: 12), + _buildAnimatedWidget( + builder: (context, child) => Container( + height: 18, + decoration: BoxDecoration( + color: defaultColors.skeletonLoaderColor + .withOpacity(0.5 + 0.5 * _controller.value), + borderRadius: BorderRadius.circular(4), + ), + ), + ), + const SizedBox(height: 12), + _buildAnimatedWidget( + builder: (context, child) => Container( + height: 18, + decoration: BoxDecoration( + color: defaultColors.skeletonLoaderColor + .withOpacity(0.5 + 0.5 * _controller.value), + borderRadius: BorderRadius.circular(4), + ), + ), + ), + const SizedBox(height: 12), Row( children: [ - _buildAnimatedCircleAvatar( - theme: currentTheme, - radius: 5, + _buildAnimatedWidget( + builder: (context, child) => Container( + width: 10, + height: 10, + decoration: BoxDecoration( + shape: BoxShape.circle, + color: defaultColors.skeletonLoaderColor + .withOpacity(0.5 + 0.5 * _controller.value), + ), + ), ), - const SizedBox(width: 8), + const SizedBox(width: 12), Expanded( - child: _buildAnimatedContainer( - theme: currentTheme, - height: 12, + child: _buildAnimatedWidget( + builder: (context, child) => Container( + height: 12, + decoration: BoxDecoration( + color: defaultColors.skeletonLoaderColor + .withOpacity(0.5 + 0.5 * _controller.value), + borderRadius: BorderRadius.circular(4), + ), + ), ), ), ], @@ -71,67 +177,31 @@ class CardLoaderWidgetState extends State ), ), ), - _buildAnimatedDeleteIcon(theme: currentTheme), + _buildAnimatedWidget( + builder: (context, child) => Container( + width: 16, + height: 16, + decoration: BoxDecoration( + color: defaultColors.skeletonLoaderColor + .withOpacity(0.5 + 0.5 * _controller.value), + borderRadius: BorderRadius.circular(4), + ), + ), + ), ], ), ); } - Widget _buildAnimatedCircleAvatar({ - required ThemeData theme, - double radius = 21, + Widget _buildAnimatedWidget({ + required Widget Function(BuildContext, Widget?) builder, }) { - 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({ - required ThemeData theme, - required double height, - }) { - 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), - ), - ); - }, + return Padding( + padding: const EdgeInsets.only(left: 6), + child: AnimatedBuilder( + animation: _controller, + builder: builder, + ), ); } } 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/lib/src/widgets/notification_list_view.dart b/lib/src/widgets/notification_list_view.dart index 6a05034..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 { @@ -11,36 +12,36 @@ 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.customCard, + this.onCardClick, this.deletingNotificationId, - this.disableAutoMarkAsRead, - this.totalElements, + this.cardParams, + this.loadingIndicator, + this.isDarkMode, + this.colors, super.key, }); - final List notifications; + final List notifications; final bool isLoading; final bool endReached; final bool loadingNextPage; final Future Function() onRefresh; final VoidCallback onEndReached; - final SirenStyleProps? customStyles; - final bool? hideAvatar; - final Widget? deleteWidget; + final CustomStyles? customStyles; 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 bool? disableAutoMarkAsRead; - final int? totalElements; + final CardParams? cardParams; + final Color? loadingIndicator; + final bool? isDarkMode; + final CustomThemeColors? colors; @override State createState() => _NotificationListViewState(); @@ -55,7 +56,7 @@ class _NotificationListViewState extends State { super.initState(); } - void _afterLayout(_) { + void _afterLayout(dynamic _) { _getPositions(); } @@ -73,58 +74,65 @@ class _NotificationListViewState extends State { @override Widget build(BuildContext context) { + final defaultColors = + SirenAppTheme.colors(isDarkMode: widget.isDarkMode ?? false); return RefreshIndicator( - color: Theme.of(context).colorScheme.secondary, - backgroundColor: Theme.of(context).colorScheme.primary, + color: widget.colors?.loaderColor ?? defaultColors.loadingIndicator, + backgroundColor: defaultColors.loadingIndicatorBackground, 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.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, - ), - styles: widget.customStyles, - deleteWidget: widget.deleteWidget, - 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.customCard?.call(currentNotification) ?? + CardWidget( + onTap: (NotificationType notification) { + if (!(widget.cardParams?.disableAutoMarkAsRead ?? + false)) { + widget.markAsRead(currentNotification.id); + } + widget.onCardClick?.call(currentNotification); + }, + notification: currentNotification, + cardParams: widget.cardParams ?? const CardParams(), + styles: widget.customStyles, + onDelete: widget.onDelete, + isDarkMode: widget.isDarkMode, + colors: widget.colors, + ); + 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: widget.loadingIndicator ?? + defaultColors.loaderColor, + ), ), - ), - ) - : 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 25054da..ffd5712 100644 --- a/lib/src/widgets/siren_inbox.dart +++ b/lib/src/widgets/siren_inbox.dart @@ -10,72 +10,41 @@ 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/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.customCard, this.customLoader, this.customErrorWidget, - this.customHeader, - this.cardProps, - this.onNotificationCardClick, + this.cardParams, + this.headerParams, + this.onCardClick, this.onError, - this.handleBackNavigation, this.theme, this.customStyles, }); - /// Custom styles for the card of each notification. - final SirenStyleProps? customStyles; + /// Flag for enabling dark mode. + final bool? darkMode; - /// Flag to hide the header. - final bool? hideHeader; + /// Notifications to be fetched in each request + final int? itemsPerFetch; /// 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; - - /// Callback function when a notification card is clicked. - final void Function(NotificationDataType)? onNotificationCardClick; - - /// 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; - - /// Custom theme colors for the inbox, this focuses on the idea of colorSchemes in flutter theme. - final CustomThemeColors? theme; + final Widget Function(NotificationType)? customCard; /// Custom loader widget. final Widget? customLoader; @@ -83,43 +52,48 @@ class SirenInbox extends StatefulWidget { /// Custom error widget. final Widget? customErrorWidget; - /// Custom header or appBar widget. - final Widget? customHeader; + ///Custom props for Card properties + final CardParams? cardParams; - /// Callback function for handling back navigation. - final void Function()? handleBackNavigation; + /// Custom props for header properties + final HeaderParams? headerParams; - /// Notifications to be fetched in each request - final int? itemsPerFetch; + /// Callback function when a notification card is clicked. + final void Function(NotificationType)? onCardClick; + + /// Callback function for handling errors. + final void Function(SirenErrorType)? onError; + + /// Custom theme colors for the inbox. + final CustomThemeColors? theme; - ///Custom params for Card properties - final CardParams? cardProps; + /// Custom styles for the card of each notification. + final CustomStyles? customStyles; @override State createState() => _SirenInboxState(); } class _SirenInboxState extends State { - late ScrollController _scrollController; bool isLoading = true; - bool endReached = false; + bool isEndReached = false; bool isError = false; bool loadingNextPage = false; int currentPage = 0; - late int totalElements; String? deletingNotificationId; int pageSize = 20; - List notifications = []; + List notifications = []; late final DeleteNotificationById _deleteNotificationById; late final ReadNotificationById _readNotificationById; late Timer? _periodicUpdateRef; late StreamSubscription _subscription; + late ScrollController _scrollController; @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 +109,6 @@ class _SirenInboxState extends State { _scrollController.dispose(); _periodicUpdateRef?.cancel(); _subscription.cancel(); - SirenDataProvider.instance.inboxDispose(); super.dispose(); } @@ -143,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(Errors.outsideSirenContextError); if (mounted) { setState(() { isError = true; @@ -186,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()); } }, ); @@ -197,8 +172,7 @@ class _SirenInboxState extends State { setState(() { isLoading = true; notifications = []; - endReached = false; - totalElements = 0; + isEndReached = false; currentPage = 0; }); } @@ -208,11 +182,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) { @@ -238,7 +209,6 @@ class _SirenInboxState extends State { setState(() { notifications .removeWhere((notification) => notification.id == notificationId); - totalElements = totalElements - 1; }); } } @@ -247,7 +217,6 @@ class _SirenInboxState extends State { if (mounted) { setState(() { notifications = []; - totalElements = 0; }); } } @@ -261,7 +230,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), @@ -276,10 +245,12 @@ 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, + fetchedNotifications.data as Iterable, ); if (mounted) { setState( @@ -298,12 +269,10 @@ class _SirenInboxState extends State { ); } } - totalElements = - totalElements + (fetchedNotifications.meta?.totalElements ?? 0); newNotifications = []; } } else if (fetchedNotifications.isError) { - widget.onError?.call(fetchedNotifications.error ?? ApiErrorDetails()); + widget.onError?.call(fetchedNotifications.error ?? SirenErrorType()); } }, ); @@ -318,7 +287,7 @@ class _SirenInboxState extends State { if (notificationsMarkedAsViewed.isError) { widget.onError?.call( - notificationsMarkedAsViewed.error ?? ApiErrorDetails(), + notificationsMarkedAsViewed.error ?? SirenErrorType(), ); } } @@ -339,11 +308,10 @@ class _SirenInboxState extends State { unawaited(markAllNotificationsAsViewed()); setState(() { notifications.addAll( - fetchedNotifications.data as Iterable, + fetchedNotifications.data as Iterable, ); isLoading = false; isError = false; - totalElements = fetchedNotifications.meta?.totalElements ?? 0; }); fetchNewNotifications(); } else if (fetchedNotifications.isError) { @@ -352,7 +320,7 @@ class _SirenInboxState extends State { isError = fetchedNotifications.isError; }); } - widget.onError?.call(fetchedNotifications.error ?? ApiErrorDetails()); + widget.onError?.call(fetchedNotifications.error ?? SirenErrorType()); } } @@ -374,11 +342,19 @@ 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( + StreamResponse( + deleteAllResponse, + UpdateEvents.DELETE_ALL, + '', + ), + ); _deleteAllNotifications(); } else if (deleteAllResponse.isError) { - widget.onError?.call(deleteAllResponse.error ?? ApiErrorDetails()); + widget.onError?.call(deleteAllResponse.error ?? SirenErrorType()); } } @@ -394,28 +370,31 @@ 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, + UpdateEvents.DELETE_BY_ID, + id, + ), + ); if (mounted) { 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) { - widget.onError?.call(deletionStatus.error ?? ApiErrorDetails()); + widget.onError?.call(deletionStatus.error ?? SirenErrorType()); } } void onEndReached() { - if (!isLoading && - !loadingNextPage && - totalElements > notifications.length) { + if (!isLoading && !loadingNextPage && !isEndReached) { if (mounted) { setState(() { loadingNextPage = true; @@ -431,14 +410,16 @@ class _SirenInboxState extends State { size: pageSize, ); if (fetchedNotifications.isSuccess) { + final count = + (fetchedNotifications.data as Iterable).length; if (mounted) { setState(() { notifications.addAll( - fetchedNotifications.data as Iterable, + fetchedNotifications.data as Iterable, ); isLoading = false; loadingNextPage = false; - endReached = totalElements == notifications.length; + isEndReached = count < pageSize; }); } } else if (fetchedNotifications.isError) { @@ -447,7 +428,7 @@ class _SirenInboxState extends State { loadingNextPage = false; }); } - widget.onError?.call(fetchedNotifications.error ?? ApiErrorDetails()); + widget.onError?.call(fetchedNotifications.error ?? SirenErrorType()); } }); } @@ -457,193 +438,58 @@ 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()); + widget.onError?.call(readStatus.error ?? SirenErrorType()); } } @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: - 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), - ); - }, + 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, ), - ); - } - - 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, - ), - 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, - ), - ), - ], - ), - ), - ], - ), - ), - ], - ), - ); - } - - Widget _buildBody(ThemeData theme) { - if (notifications.isEmpty && isLoading) { - return LoaderWidget( + body: InboxBody( + cardParams: widget.cardParams, + colors: widget.theme, + customCard: widget.customCard, + customErrorWidget: widget.customErrorWidget, customLoader: widget.customLoader, - ); - } else if (notifications.isEmpty && !isLoading) { - return widget.listEmptyWidget ?? const EmptyWidget(); - } else { - return NotificationListView( - notifications: notifications, + customStyles: widget.customStyles, + deleteNotification: deleteNotification, + deletingNotificationId: deletingNotificationId, + disableAutoMarkAsRead: + widget.cardParams?.disableAutoMarkAsRead ?? false, + endReached: isEndReached, + isDarkMode: widget.darkMode, + isError: isError, isLoading: isLoading, - endReached: endReached, - onRefresh: onRefresh, - onEndReached: onEndReached, + listEmptyWidget: widget.listEmptyWidget, 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(), - ); - }, - ); + 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 5f4df8c..84f221e 100644 --- a/lib/src/widgets/siren_inbox_icon.dart +++ b/lib/src/widgets/siren_inbox_icon.dart @@ -6,7 +6,9 @@ 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'; /// Widget representing the inbox icon. class SirenInboxIcon extends StatefulWidget { @@ -33,10 +35,10 @@ 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; + final void Function(SirenErrorType)? onError; /// Callback function when the inbox icon is tapped. final VoidCallback? onTap; @@ -73,7 +75,6 @@ class _SirenInboxIconState extends State { super.dispose(); _periodicUpdateRef.cancel(); _subscription.cancel(); - SirenDataProvider.instance.iconDispose(); } void _subscribeToStream() { @@ -100,7 +101,7 @@ class _SirenInboxIconState extends State { } } else if (streamResponse.response?.isError ?? false) { widget.onError - ?.call(streamResponse.response?.error ?? ApiErrorDetails()); + ?.call(streamResponse.response?.error ?? SirenErrorType()); } }, ); @@ -159,90 +160,65 @@ 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(Errors.outsideSirenContextError); } } @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?.iconStyle?.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: [ - SizedBox( - width: size, - height: size, - child: widget.notificationIcon ?? - Icon( - Icons.notifications_none_outlined, - size: size, - color: currentTheme.colorScheme.onPrimary, - ), - ), - if (_notificationsCount > 0 && !(widget.hideBadge ?? false)) - _getBadge(context), - ], + 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, + ), ), ), - ); - }, - ), - ); - } - - 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, + 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 24b5e4e..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 @@ -11,21 +11,23 @@ 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: sdk: flutter - dio: '>=5.2.0 <=5.4.1' + dio: '>=5.2.0 <=5.4.3' 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.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/card_test.dart b/test/card_test.dart deleted file mode 100644 index 2a26cbb..0000000 --- a/test/card_test.dart +++ /dev/null @@ -1,71 +0,0 @@ -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/widgets/card.dart'; - -class MockNetworkImage extends Mock implements NetworkImage {} - -void main() { - testWidgets('CardWidget renders correctly', (WidgetTester tester) async { - // Create a mock notification data - // ignore: unused_local_variable - final notification = NotificationDataType( - id: '123', - 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: '456', - isRead: false, - cardColor: Colors.blue, // Mock card color - ); - - // 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), - ), - ), - ); - - // 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); - }); -} diff --git a/test/constants/generics_test.dart b/test/constants/generics_test.dart index 0187853..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,11 +11,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(Errors.defaultError.code, ErrorCodes.API_ERROR.name); + expect(Errors.defaultError.type, 'ERROR'); expect( - Generics.defaultError.message, - 'Oops something went wrong, if issue persist please contact Siren Team', + Errors.defaultError.message, + 'Something went wrong', ); }); }); @@ -24,6 +25,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 +34,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/data/siren_data_provider_test.dart b/test/data/siren_data_provider_test.dart new file mode 100644 index 0000000..2b2fe6d --- /dev/null +++ b/test/data/siren_data_provider_test.dart @@ -0,0 +1,63 @@ +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', () { + test('UpdateParams updates user token and recipient ID', () async { + sirenDataProvider.updateParams( + userToken: 'token', + recipientId: 'recipientId', + ); + + expect(sirenDataProvider.userToken, 'token'); + expect(sirenDataProvider.recipientId, 'recipientId'); + }); + + test('IconDispose closes icon controller', () { + sirenDataProvider.iconDispose(); + + expect(sirenDataProvider.iconController.isClosed, false); + }); + + test('InboxDispose closes inbox controller', () { + sirenDataProvider.inboxDispose(); + + 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/data/siren_data_provider_test.mocks.dart b/test/data/siren_data_provider_test.mocks.dart new file mode 100644 index 0000000..54dc9b7 --- /dev/null +++ b/test/data/siren_data_provider_test.mocks.dart @@ -0,0 +1,101 @@ +// 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. + +// 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: 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/delete_notification_by_id_test.dart b/test/delete_notification_by_id_test.dart index 309d9b7..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'; @@ -17,8 +18,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 +25,7 @@ class MockApiClient extends ApiClient { }, statusCode: 200, ); - return result; // Default to success for now + return result; } } @@ -52,8 +51,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', @@ -62,7 +60,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 @@ -77,7 +75,7 @@ class DeleteNotificationById { ..isSuccess = false ..isError = true ..rawResponse = apiResponse - ..error = Generics.defaultError; + ..error = Errors.defaultError; } 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 new file mode 100644 index 0000000..a1a3314 --- /dev/null +++ b/test/models/api_response_test.dart @@ -0,0 +1,66 @@ +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', + }, + }; + final response = ApiResponse.fromJson(json); + + expect(response.data, 'testData'); + expect(response.error?.type, '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', + }; + 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'); + }); + }); + + group('SirenErrorType', () { + test('fromJson() should parse JSON correctly', () { + final json = { + 'errorCode': '123', + 'message': 'Error message', + }; + final errorDetails = SirenErrorType.fromJson(json); + + 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 new file mode 100644 index 0000000..8fcf33a --- /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('NotificationType', () { + 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 = NotificationType.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 = NotificationType( + 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 57% rename from test/ui_models_test.dart rename to test/models/ui_models_test.dart index 66c7390..a441546 100644 --- a/test/ui_models_test.dart +++ b/test/models/ui_models_test.dart @@ -14,99 +14,102 @@ 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', () { + group('CardParams', () { test('constructor should initialize properties with provided values', () { - // Arrange & Act - const cardProps = CardProps(hideAvatar: true, showMedia: false); + const cardParams = CardParams( + hideAvatar: true, + ); - // Assert - expect(cardProps.hideAvatar, true); - expect(cardProps.showMedia, false); + expect(cardParams.hideAvatar, true); }); }); group('IconStyle', () { test('constructor should initialize size property with provided value', () { - // Arrange & Act - const iconStyle = IconStyle(size: 24); + const iconStyle = NotificationIconStyle(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 defaultSize = DefaultIconStyle.defaultSize; + final defaultTop = DefaultIconStyle.defaultTop; + final defaultRight = DefaultIconStyle.defaultRight; final iconSize = DefaultIconStyle.iconSize; - // Assert + expect(defaultFontSize, 10); + expect(defaultSize, 20); + expect(defaultTop, 0); + expect(defaultRight, 2); expect(iconSize, 35); }); }); 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); }); }); - group('SirenStyleProps', () { + group('CustomStyles', () { 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), + final sirenStyleProps = CustomStyles( + cardStyle: CardStyle( + cardContainer: ContainerStyle( + decoration: const BoxDecoration(color: Colors.blue), + ), + ), + notificationIconStyle: const NotificationIconStyle(size: 24), + badgeStyle: const BadgeStyle(fontSize: 16), ); - // Assert - 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); }); }); group('CustomThemeColors', () { test('constructor should initialize properties with provided values', () { - // Arrange & Act final customThemeColors = CustomThemeColors( backgroundColor: Colors.white, - highlightedCardBorderColor: Colors.grey, - badgeColor: Colors.red, + primary: Colors.grey, ); - // Assert expect(customThemeColors.backgroundColor, Colors.white); - expect(customThemeColors.highlightedCardBorderColor, Colors.grey); - expect(customThemeColors.badgeColor, Colors.red); + expect(customThemeColors.primary, Colors.grey); }); }); + + test('Card Params', () { + const hideAvatar = true; + const Widget deleteWidget = Icon(Icons.delete); + + const cardParams = CardParams( + hideAvatar: hideAvatar, + deleteIcon: deleteWidget, + ); + + expect(cardParams.hideAvatar, hideAvatar); + expect(cardParams.deleteIcon, deleteWidget); + }); } diff --git a/test/unviewed_notification_count_model_test.dart b/test/models/unviewed_notification_count_model_test.dart similarity index 86% rename from test/unviewed_notification_count_model_test.dart rename to test/models/unviewed_notification_count_model_test.dart index e801d73..213d21e 100644 --- a/test/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 new file mode 100644 index 0000000..8a3545c --- /dev/null +++ b/test/services/api_client_test.dart @@ -0,0 +1,272 @@ +// ignore_for_file: avoid_redundant_argument_values + +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( + data: null, + statusCode: 500, + requestOptions: RequestOptions(), + ); + final result = apiClient.isServerError(response); + expect( + result, + true, + ); + }); + + test('GET request', () async { + final responseData = {'key': 'value'}; + const responseStatusCode = 200; + + when(mockSirenDataProvider.apiDomain).thenReturn('http://example.com'); + + 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(), + ), + ); + + final response = await apiClient.get(path: '/test'); + + verify( + mockDio.get( + '/test', + ), + ).called(1); + + expect(response.data, responseData); + expect(response.statusCode, responseStatusCode); + }); + + test('POST request', () async { + final responseData = {'key': 'value'}; + const responseStatusCode = 201; + + when(mockSirenDataProvider.apiDomain).thenReturn('http://example.com'); + + 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(), + ), + ); + + final response = + await apiClient.post(path: '/test', data: {'key': 'value'}); + + verify( + mockDio.post( + '/test', + data: {'key': 'value'}, + ), + ).called(1); + + expect(response.data, responseData); + expect(response.statusCode, responseStatusCode); + }); + + test('PATCH request', () async { + final responseData = {'key': 'value'}; + const responseStatusCode = 200; + + when(mockSirenDataProvider.apiDomain).thenReturn('http://example.com'); + + 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(), + ), + ); + + final response = + await apiClient.patch(path: '/test', data: {'key': 'value'}); + + verify( + mockDio.patch( + '/test', + data: {'key': 'value'}, + ), + ).called(1); + + expect(response.data, responseData); + expect(response.statusCode, responseStatusCode); + }); + + test('DELETE request', () async { + final responseData = {'key': 'value'}; + const responseStatusCode = 200; + + when(mockSirenDataProvider.apiDomain).thenReturn('http://example.com'); + + 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); + }); + + test('Handles DioException on GET request', () async { + when(mockDio.get(any, queryParameters: anyNamed('queryParameters'))) + .thenThrow( + DioException( + requestOptions: RequestOptions(), + response: Response( + data: 'Error message', + statusCode: 404, + requestOptions: RequestOptions(), + ), + ), + ); + + final result = await apiClient.get(path: '/example'); + + expect(result.data, 'Error message'); + expect( + result.statusCode, + 404, + ); + }); + + test('Handles DioException on POST request', () async { + when(mockDio.post(any, queryParameters: anyNamed('queryParameters'))) + .thenThrow( + DioException( + requestOptions: RequestOptions(), + response: Response( + data: 'Error message', + statusCode: 404, + requestOptions: RequestOptions(), + ), + ), + ); + + final result = await apiClient.post(path: '/example'); + + expect(result.data, 'Error message'); + expect( + result.statusCode, + 404, + ); + }); + + test('Handles DioException on Patch request', () async { + when(mockDio.patch(any, queryParameters: anyNamed('queryParameters'))) + .thenThrow( + DioException( + requestOptions: RequestOptions(), + response: Response( + data: 'Error message', + statusCode: 404, + requestOptions: RequestOptions(), + ), + ), + ); + + final result = await apiClient.patch(path: '/example'); + + expect(result.data, 'Error message'); + expect( + result.statusCode, + 404, + ); + }); + + test('Handles DioException on delete request', () async { + when(mockDio.delete(any, queryParameters: anyNamed('queryParameters'))) + .thenThrow( + DioException( + requestOptions: RequestOptions(), + response: Response( + data: 'Error message', + statusCode: 404, + requestOptions: RequestOptions(), + ), + ), + ); + + final result = await apiClient.delete(path: '/example'); + + expect(result.data, 'Error message'); + expect( + result.statusCode, + 404, + ); + }); + }); +} diff --git a/test/services/api_client_test.mocks.dart b/test/services/api_client_test.mocks.dart new file mode 100644 index 0000000..888d18f --- /dev/null +++ b/test/services/api_client_test.mocks.dart @@ -0,0 +1,1208 @@ +// 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. + +// 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 _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: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 _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: 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, + ); +} + +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 _i9.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, + _i10.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, + _i10.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, + _i10.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, + _i10.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, + _i10.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, + _i10.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, + _i10.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, + _i10.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, + _i10.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, + _i10.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, + _i10.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, + _i10.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, + _i10.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, + _i10.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, + _i10.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, + _i10.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 _i11.SirenDataProvider { + @override + String get userToken => (super.noSuchMethod( + Invocation.getter(#userToken), + returnValue: '', + returnValueForMissingStub: '', + ) 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: '', + returnValueForMissingStub: '', + ) 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: '', + returnValueForMissingStub: '', + ) as String); + @override + set apiDomain(String? _apiDomain) => super.noSuchMethod( + Invocation.setter( + #apiDomain, + _apiDomain, + ), + returnValueForMissingStub: null, + ); + @override + _i7.StreamController<_i8.StreamResponse> get inboxController => + (super.noSuchMethod( + Invocation.getter(#inboxController), + returnValue: _FakeStreamController_5<_i8.StreamResponse>( + this, + Invocation.getter(#inboxController), + ), + returnValueForMissingStub: _FakeStreamController_5<_i8.StreamResponse>( + this, + Invocation.getter(#inboxController), + ), + ) as _i7.StreamController<_i8.StreamResponse>); + @override + _i7.StreamController<_i8.StreamResponse> get iconController => + (super.noSuchMethod( + Invocation.getter(#iconController), + returnValue: _FakeStreamController_5<_i8.StreamResponse>( + this, + Invocation.getter(#iconController), + ), + returnValueForMissingStub: _FakeStreamController_5<_i8.StreamResponse>( + this, + Invocation.getter(#iconController), + ), + ) as _i7.StreamController<_i8.StreamResponse>); + @override + _i12.Status get tokenVerificationStatus => (super.noSuchMethod( + Invocation.getter(#tokenVerificationStatus), + 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( + #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 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( + #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..1849066 --- /dev/null +++ b/test/services/network_service_test.dart @@ -0,0 +1,35 @@ +// 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'; + +@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', () { + final networkServiceInstance1 = NetworkService.instance; + final networkServiceInstance2 = NetworkService.instance; + + expect(networkServiceInstance1, equals(networkServiceInstance2)); + }); + + test('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..9c0af93 --- /dev/null +++ b/test/services/network_service_test.mocks.dart @@ -0,0 +1,263 @@ +// 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. + +// 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: 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..d2ef7e2 100644 --- a/test/utils/common_utils_test.dart +++ b/test/utils/common_utils_test.dart @@ -1,12 +1,13 @@ 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 {} + 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)), @@ -31,6 +32,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)), @@ -45,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)), @@ -58,77 +64,16 @@ 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)); }); }); - // 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(); - - // // Assert that an empty string is returned - // expect(apiDomain, ''); - // }); - // }); + group('loadEnv', () { + test('API_DOMAIN', () async { + TestWidgetsFlutterBinding.ensureInitialized(); + final envVariables = await loadEnv(); + expect(envVariables.keys.first, 'API_DOMAIN'); + await getApiDomain(); + }); + }); } - -// 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..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; @@ -40,51 +41,35 @@ 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, '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, + ); - // 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 new file mode 100644 index 0000000..5e2af73 --- /dev/null +++ b/test/widgets/app_bar_test.dart @@ -0,0 +1,135 @@ +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( + headerParams: HeaderParams( + title: title, + showBackButton: false, + ), + isNonEmptyNotifications: 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( + headerParams: HeaderParams( + title: 'Title', + showBackButton: true, + ), + isNonEmptyNotifications: 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( + headerParams: HeaderParams( + title: 'Title', + showBackButton: false, + hideClearAll: true, + ), + isNonEmptyNotifications: 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( + headerParams: HeaderParams( + title: 'Title', + showBackButton: false, + ), + isNonEmptyNotifications: 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( + headerParams: HeaderParams( + title: 'Title', + showBackButton: true, + onBackPress: () { + backButtonPressed = true; + }, + ), + isNonEmptyNotifications: 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 { + var clearAllPressed = false; + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + appBar: SirenAppBar( + headerParams: HeaderParams( + title: 'Title', + showBackButton: false, + ), + isNonEmptyNotifications: 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 new file mode 100644 index 0000000..b5c4ee1 --- /dev/null +++ b/test/widgets/card_test.dart @@ -0,0 +1,105 @@ +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 { + // ignore: unused_local_variable + final func = MockFunction().call; + final notification = NotificationType( + id: '123', + 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', + thumbnailUrl: 'https://picsum.photos/200/300', + avatar: AvatarData( + altText: 'Test alt text', + url: 'https://picsum.photos/200/300', + ), + additionalData: 'Test Additional Data', + ), + requestId: '456', + isRead: false, + cardColor: Colors.blue, + ); + + var deletePressed = false; + var thumbnailPressed = false; + await mockNetworkImagesFor(() async { + await tester.pumpWidget( + MaterialApp( + home: CardWidget( + onTap: (NotificationType notification) {}, + onDelete: (id) { + deletePressed = true; + }, + notification: notification, + cardParams: CardParams( + hideAvatar: false, + hideDelete: false, + hideMediaThumbnail: false, + onMediaThumbnailClick: (NotificationType notification) { + thumbnailPressed = true; + }, + onAvatarClick: (notification) { + func(); + }, + ), + styles: null, // Mock styles + colors: CustomThemeColors( + cardColors: CardColors( + borderColor: Colors.red, + background: Colors.blue, + titleColor: Colors.yellow, + subtitleColor: Colors.brown, + descriptionColor: Colors.orange, + ), + ), + ), + ), + ); + }); + expect(find.text('Test Header'), findsOneWidget); + expect(find.text('Test SubHeader'), findsOneWidget); + expect(find.text('Test Body'), 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)); + + 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; + 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/empty_widget_test.dart b/test/widgets/empty_widget_test.dart similarity index 84% rename from test/empty_widget_test.dart rename to test/widgets/empty_widget_test.dart index 4344eba..db239f7 100644 --- a/test/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/error_widget_test.dart b/test/widgets/error_widget_test.dart similarity index 83% rename from test/error_widget_test.dart rename to test/widgets/error_widget_test.dart index 1d9f719..9e30eaf 100644 --- a/test/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 new file mode 100644 index 0000000..fa3d867 --- /dev/null +++ b/test/widgets/icon_badge_test.dart @@ -0,0 +1,80 @@ +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, + color: Colors.red, + badgeBackgroundColor: Colors.black, + ), + ], + ), + ), + ), + ); + await tester.pumpAndSettle(const Duration(seconds: 1)); + + expect(find.byType(Positioned), findsOneWidget); + + 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, + color: Colors.red, + badgeBackgroundColor: Colors.black, + ), + ], + ), + ), + ), + ); + await tester.pumpAndSettle(const Duration(seconds: 1)); + + 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, + color: Colors.red, + badgeBackgroundColor: Colors.black, + ), + ], + ), + ), + ), + ); + await tester.pumpAndSettle(const Duration(seconds: 1)); + + expect(find.text('99+'), findsOneWidget); + }); + }); +} diff --git a/test/widgets/inbox_body_test.dart b/test/widgets/inbox_body_test.dart new file mode 100644 index 0000000..c2a008e --- /dev/null +++ b/test/widgets/inbox_body_test.dart @@ -0,0 +1,110 @@ +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( + isLoading: true, + loadingNextPage: false, + isError: false, + notifications: const [], + deleteNotification: (id) async {}, + markAsRead: (id) {}, + customCard: null, + onCardClick: null, + deletingNotificationId: null, + disableAutoMarkAsRead: false, + 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( + isLoading: false, + loadingNextPage: false, + isError: true, + notifications: const [], + deleteNotification: (id) async {}, + markAsRead: (id) {}, + customCard: null, + onCardClick: null, + deletingNotificationId: null, + disableAutoMarkAsRead: false, + onRefresh: () async {}, + endReached: false, + onEndReached: () {}, + scrollController: ScrollController(), + ), + ), + ); + + expect(find.byType(DefaultErrorWidget), findsOneWidget); + }); + + testWidgets('InboxBody displays notifications', (WidgetTester tester) async { + final notifications = [ + NotificationType( + 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( + isLoading: false, + loadingNextPage: false, + isError: false, + notifications: notifications, + deleteNotification: (id) async {}, + markAsRead: (id) {}, + customCard: null, + onCardClick: null, + deletingNotificationId: null, + disableAutoMarkAsRead: false, + onRefresh: () async {}, + endReached: false, + onEndReached: () {}, + scrollController: ScrollController(), + ), + ), + ); + + expect(find.byType(NotificationListView), findsOneWidget); + }); + }); +} diff --git a/test/loader_widget_test.dart b/test/widgets/loader_widget_test.dart similarity index 81% rename from test/loader_widget_test.dart rename to test/widgets/loader_widget_test.dart index 42cd2c8..29bd44b 100644 --- a/test/loader_widget_test.dart +++ b/test/widgets/loader_widget_test.dart @@ -8,14 +8,14 @@ void main() { (WidgetTester tester) async { await tester.pumpWidget( const MaterialApp( - home: CardLoaderWidget(), + home: CardLoaderWidget( + hideAvatar: false, + ), ), ); - // 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( @@ -28,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( @@ -39,7 +37,6 @@ void main() { ), ); - // Verify that only one Padding containing a Row is found expect(paddingWithRowFinder, findsOneWidget); }); }); 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); + }); +} diff --git a/test/widgets/notification_list_view_test.dart b/test/widgets/notification_list_view_test.dart new file mode 100644 index 0000000..c23d396 --- /dev/null +++ b/test/widgets/notification_list_view_test.dart @@ -0,0 +1,106 @@ +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 { + void call(); +} + +void main() { + final notificationsList = [ + NotificationType( + 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, + scrollController: ScrollController(), + onDelete: (id) async {}, + markAsRead: (id) {}, + onCardClick: (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, + scrollController: ScrollController(), + customCard: (n) { + return Text(n.message.subHeader.toString()); + }, + onDelete: (id) async {}, + markAsRead: (id) {}, + onCardClick: (n) { + func(); + }, + ), + ), + ), + ); + await tester.pumpAndSettle(const Duration(seconds: 1)); + expect(find.text('Test SubHeader'), findsOneWidget); + }); + }); +} diff --git a/test/nullabale_text_test.dart b/test/widgets/nullabale_text_test.dart similarity index 80% rename from test/nullabale_text_test.dart rename to test/widgets/nullabale_text_test.dart index bc140a5..3cabb34 100644 --- a/test/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 new file mode 100644 index 0000000..7cb9d48 --- /dev/null +++ b/test/widgets/siren_inbox_icon_test.dart @@ -0,0 +1,213 @@ +// 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_colors.dart'; + +import 'siren_inbox_test.mocks.dart'; + +class MockFunction extends Mock { + void call(); +} + +@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 = AppColors.darkColorTheme().primary; + + expect(primaryColor, const Color(0xfffa9874)); + }); + + testWidgets('Widget disposes controllers on dispose', + (WidgetTester tester) async { + await tester.pumpWidget( + const MaterialApp( + home: Scaffold( + body: SirenInboxIcon(), + ), + ), + ); + + await tester.pumpWidget(Container()); + + 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(notificationIconColor: 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..507e251 --- /dev/null +++ b/test/widgets/siren_inbox_icon_test.mocks.dart @@ -0,0 +1,250 @@ +// 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. + +// ignore_for_file: no_leading_underscores_for_library_prefixes +import 'dart:async' as _i2; + +import 'package:mockito/mockito.dart' as _i1; +import 'package:sirenapp_flutter_inbox/sirenapp_flutter_inbox.dart' as _i3; +import 'package:sirenapp_flutter_inbox/src/api/fetch_unviewed_notification_count.dart' + 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 _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: 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 _FakeSirenErrorType_1 extends _i1.SmartFake + implements _i3.SirenErrorType { + _FakeSirenErrorType_1( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} + +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( + 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: '', + ) 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: '', + ) 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: '', + ) as String); + @override + set apiDomain(String? _apiDomain) => super.noSuchMethod( + Invocation.setter( + #apiDomain, + _apiDomain, + ), + returnValueForMissingStub: null, + ); + @override + _i2.StreamController<_i3.StreamResponse> get inboxController => + (super.noSuchMethod( + Invocation.getter(#inboxController), + returnValue: _FakeStreamController_0<_i3.StreamResponse>( + this, + Invocation.getter(#inboxController), + ), + ) as _i2.StreamController<_i3.StreamResponse>); + @override + _i2.StreamController<_i3.StreamResponse> get iconController => + (super.noSuchMethod( + Invocation.getter(#iconController), + returnValue: _FakeStreamController_0<_i3.StreamResponse>( + this, + Invocation.getter(#iconController), + ), + ) as _i2.StreamController<_i3.StreamResponse>); + @override + _i6.Status get tokenVerificationStatus => (super.noSuchMethod( + Invocation.getter(#tokenVerificationStatus), + 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( + #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 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( + #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 _i7.FetchUnViewedNotificationsCount { + MockFetchUnViewedNotificationsCount() { + _i1.throwOnMissingStub(this); + } + + @override + _i4.ApiClient get api => (super.noSuchMethod( + Invocation.getter(#api), + returnValue: _FakeApiClient_2( + this, + Invocation.getter(#api), + ), + ) as _i4.ApiClient); + @override + set api(_i4.ApiClient? _api) => super.noSuchMethod( + Invocation.setter( + #api, + _api, + ), + returnValueForMissingStub: null, + ); + @override + _i2.Future<_i3.ApiResponse> fetchUnViewedNotificationsCount() => + (super.noSuchMethod( + Invocation.method( + #fetchUnViewedNotificationsCount, + [], + ), + returnValue: _i2.Future<_i3.ApiResponse>.value(_FakeApiResponse_3( + this, + Invocation.method( + #fetchUnViewedNotificationsCount, + [], + ), + )), + ) as _i2.Future<_i3.ApiResponse>); +} diff --git a/test/widgets/siren_inbox_test.dart b/test/widgets/siren_inbox_test.dart new file mode 100644 index 0000000..555fadc --- /dev/null +++ b/test/widgets/siren_inbox_test.dart @@ -0,0 +1,140 @@ +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 { + void call(); +} + +@GenerateNiceMocks([ + MockSpec(), + MockSpec(), +]) +void main() { + group('SirenInbox Widget Test', () { + late StreamController iconController; + late StreamController inboxController; + late MockSirenDataProvider mockSirenDataProvider; + + setUp(() { + iconController = StreamController.broadcast(); + inboxController = StreamController.broadcast(); + mockSirenDataProvider = MockSirenDataProvider(); + when(mockSirenDataProvider.tokenVerificationStatus) + .thenReturn(Status.SUCCESS); + }); + + tearDown(() { + iconController.close(); + inboxController.close(); + }); + + testWidgets('Test Title', (WidgetTester tester) async { + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: SirenInbox( + headerParams: HeaderParams( + title: 'Notifications Header', + ), + darkMode: true, + ), + ), + ), + ); + + await tester.pump(); + + expect(find.text('Notifications Header'), findsOneWidget); + }); + + testWidgets('Back navigation', (WidgetTester tester) async { + var backButtonPressed = false; + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: SirenInbox( + headerParams: HeaderParams( + showBackButton: true, + onBackPress: () { + backButtonPressed = true; + }, + ), + ), + ), + ), + ); + + await tester.pump(); + await tester.tap(find.byIcon(Icons.arrow_back_ios)); + expect(backButtonPressed, true); + }); + + testWidgets('Test theme', (WidgetTester tester) async { + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: SirenInbox( + theme: CustomThemeColors(backgroundColor: Colors.amber), + ), + ), + ), + ); + + 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 { + 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..1e3ca38 --- /dev/null +++ b/test/widgets/siren_inbox_test.mocks.dart @@ -0,0 +1,274 @@ +// 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. + +// ignore_for_file: no_leading_underscores_for_library_prefixes +import 'dart:async' as _i2; + +import 'package:mockito/mockito.dart' as _i1; +import 'package:sirenapp_flutter_inbox/sirenapp_flutter_inbox.dart' as _i3; +import 'package:sirenapp_flutter_inbox/src/api/fetch_unviewed_notification_count.dart' + 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 _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: 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 _FakeSirenErrorType_1 extends _i1.SmartFake + implements _i3.SirenErrorType { + _FakeSirenErrorType_1( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} + +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( + 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: '', + returnValueForMissingStub: '', + ) 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: '', + returnValueForMissingStub: '', + ) 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: '', + returnValueForMissingStub: '', + ) as String); + @override + set apiDomain(String? _apiDomain) => super.noSuchMethod( + Invocation.setter( + #apiDomain, + _apiDomain, + ), + returnValueForMissingStub: null, + ); + @override + _i2.StreamController<_i3.StreamResponse> get inboxController => + (super.noSuchMethod( + Invocation.getter(#inboxController), + returnValue: _FakeStreamController_0<_i3.StreamResponse>( + this, + Invocation.getter(#inboxController), + ), + returnValueForMissingStub: _FakeStreamController_0<_i3.StreamResponse>( + this, + Invocation.getter(#inboxController), + ), + ) as _i2.StreamController<_i3.StreamResponse>); + @override + _i2.StreamController<_i3.StreamResponse> get iconController => + (super.noSuchMethod( + Invocation.getter(#iconController), + returnValue: _FakeStreamController_0<_i3.StreamResponse>( + this, + Invocation.getter(#iconController), + ), + returnValueForMissingStub: _FakeStreamController_0<_i3.StreamResponse>( + this, + Invocation.getter(#iconController), + ), + ) as _i2.StreamController<_i3.StreamResponse>); + @override + _i6.Status get tokenVerificationStatus => (super.noSuchMethod( + Invocation.getter(#tokenVerificationStatus), + 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( + #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 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( + #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 _i7.FetchUnViewedNotificationsCount { + @override + _i4.ApiClient get api => (super.noSuchMethod( + Invocation.getter(#api), + returnValue: _FakeApiClient_2( + this, + Invocation.getter(#api), + ), + returnValueForMissingStub: _FakeApiClient_2( + this, + Invocation.getter(#api), + ), + ) as _i4.ApiClient); + @override + set api(_i4.ApiClient? _api) => super.noSuchMethod( + Invocation.setter( + #api, + _api, + ), + returnValueForMissingStub: null, + ); + @override + _i2.Future<_i3.ApiResponse> fetchUnViewedNotificationsCount() => + (super.noSuchMethod( + Invocation.method( + #fetchUnViewedNotificationsCount, + [], + ), + returnValue: _i2.Future<_i3.ApiResponse>.value(_FakeApiResponse_3( + this, + Invocation.method( + #fetchUnViewedNotificationsCount, + [], + ), + )), + returnValueForMissingStub: + _i2.Future<_i3.ApiResponse>.value(_FakeApiResponse_3( + this, + Invocation.method( + #fetchUnViewedNotificationsCount, + [], + ), + )), + ) as _i2.Future<_i3.ApiResponse>); +}