diff --git a/analysis_options.yaml b/analysis_options.yaml index 6f697717..7b5c1a64 100644 --- a/analysis_options.yaml +++ b/analysis_options.yaml @@ -11,6 +11,7 @@ analyzer: document_ignores: ignore one_member_abstracts: ignore cascade_invocations: ignore + cast_nullable_to_non_nullable: ignore exclude: - build/** linter: diff --git a/lib/src/config/app_dependencies.dart b/lib/src/config/app_dependencies.dart index de78c861..da51e5e8 100644 --- a/lib/src/config/app_dependencies.dart +++ b/lib/src/config/app_dependencies.dart @@ -65,7 +65,7 @@ class AppDependencies { late final DataRepository countryRepository; late final DataRepository languageRepository; late final DataRepository userRepository; - late final DataRepository userAppSettingsRepository; + late final DataRepository appSettingsRepository; late final DataRepository userContentPreferencesRepository; late final DataRepository @@ -190,12 +190,12 @@ class AppDependencies { toJson: (item) => item.toJson(), logger: Logger('DataMongodb'), ); - final userAppSettingsClient = DataMongodb( + final appSettingsClient = DataMongodb( connectionManager: _mongoDbConnectionManager, - modelName: 'user_app_settings', - fromJson: UserAppSettings.fromJson, + modelName: 'app_settings', + fromJson: AppSettings.fromJson, toJson: (item) => item.toJson(), - logger: Logger('DataMongodb'), + logger: Logger('DataMongodb'), ); final userContentPreferencesClient = DataMongodb( connectionManager: _mongoDbConnectionManager, @@ -306,8 +306,8 @@ class AppDependencies { countryRepository = DataRepository(dataClient: countryClient); languageRepository = DataRepository(dataClient: languageClient); userRepository = DataRepository(dataClient: userClient); - userAppSettingsRepository = DataRepository( - dataClient: userAppSettingsClient, + appSettingsRepository = DataRepository( + dataClient: appSettingsClient, ); userContentPreferencesRepository = DataRepository( dataClient: userContentPreferencesClient, @@ -359,7 +359,7 @@ class AppDependencies { verificationCodeStorageService: verificationCodeStorageService, permissionService: permissionService, emailRepository: emailRepository, - userAppSettingsRepository: userAppSettingsRepository, + appSettingsRepository: appSettingsRepository, userContentPreferencesRepository: userContentPreferencesRepository, log: Logger('AuthService'), ); diff --git a/lib/src/models/models.dart b/lib/src/models/models.dart new file mode 100644 index 00000000..97f2ceaa --- /dev/null +++ b/lib/src/models/models.dart @@ -0,0 +1,2 @@ +export 'push_notification/push_notification.dart'; +export 'request_id.dart'; diff --git a/lib/src/models/push_notification/firebase_request_body.dart b/lib/src/models/push_notification/firebase_request_body.dart new file mode 100644 index 00000000..90671411 --- /dev/null +++ b/lib/src/models/push_notification/firebase_request_body.dart @@ -0,0 +1,95 @@ +import 'package:core/core.dart'; +import 'package:equatable/equatable.dart'; +import 'package:json_annotation/json_annotation.dart'; + +part 'firebase_request_body.g.dart'; + +/// {@template firebase_request_body} +/// Represents the top-level structure for a Firebase Cloud Messaging +/// v1 API request. +/// {@endtemplate} +@JsonSerializable( + explicitToJson: true, + includeIfNull: true, + checked: true, + createFactory: false, +) +class FirebaseRequestBody extends Equatable { + /// {@macro firebase_request_body} + const FirebaseRequestBody({required this.message}); + + /// The message payload. + final FirebaseMessage message; + + /// Converts this [FirebaseRequestBody] instance to a JSON map. + Map toJson() => _$FirebaseRequestBodyToJson(this); + + @override + List get props => [message]; +} + +/// {@template firebase_message} +/// Represents the message object within a Firebase request. +/// {@endtemplate} +@JsonSerializable( + explicitToJson: true, + includeIfNull: true, + checked: true, + createFactory: false, +) +class FirebaseMessage extends Equatable { + /// {@macro firebase_message} + const FirebaseMessage({ + required this.token, + required this.notification, + required this.data, + }); + + /// The registration token of the device to send the message to. + final String token; + + /// The notification content. + final FirebaseNotification notification; + + /// The custom data payload. + final PushNotificationPayload data; + + /// Converts this [FirebaseMessage] instance to a JSON map. + Map toJson() => _$FirebaseMessageToJson(this); + + @override + List get props => [token, notification, data]; +} + +/// {@template firebase_notification} +/// Represents the notification content within a Firebase message. +/// {@endtemplate} +@JsonSerializable( + explicitToJson: true, + includeIfNull: true, + checked: true, + createFactory: false, +) +class FirebaseNotification extends Equatable { + /// {@macro firebase_notification} + const FirebaseNotification({ + required this.title, + this.body, + this.image, + }); + + /// The notification's title. + final String title; + + /// The notification's body text. + final String? body; + + /// The URL of an image to be displayed in the notification. + final String? image; + + /// Converts this [FirebaseNotification] instance to a JSON map. + Map toJson() => _$FirebaseNotificationToJson(this); + + @override + List get props => [title, body, image]; +} diff --git a/lib/src/models/push_notification/firebase_request_body.g.dart b/lib/src/models/push_notification/firebase_request_body.g.dart new file mode 100644 index 00000000..bba3ed3d --- /dev/null +++ b/lib/src/models/push_notification/firebase_request_body.g.dart @@ -0,0 +1,37 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'firebase_request_body.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +Map _$FirebaseRequestBodyToJson( + FirebaseRequestBody instance, +) => { + 'stringify': instance.stringify, + 'hashCode': instance.hashCode, + 'message': instance.message.toJson(), + 'props': instance.props, +}; + +Map _$FirebaseMessageToJson(FirebaseMessage instance) => + { + 'stringify': instance.stringify, + 'hashCode': instance.hashCode, + 'token': instance.token, + 'notification': instance.notification.toJson(), + 'data': instance.data.toJson(), + 'props': instance.props, + }; + +Map _$FirebaseNotificationToJson( + FirebaseNotification instance, +) => { + 'stringify': instance.stringify, + 'hashCode': instance.hashCode, + 'title': instance.title, + 'body': instance.body, + 'image': instance.image, + 'props': instance.props, +}; diff --git a/lib/src/models/push_notification/onesignal_request_body.dart b/lib/src/models/push_notification/onesignal_request_body.dart new file mode 100644 index 00000000..3ee22289 --- /dev/null +++ b/lib/src/models/push_notification/onesignal_request_body.dart @@ -0,0 +1,58 @@ +import 'package:core/core.dart'; +import 'package:equatable/equatable.dart'; +import 'package:json_annotation/json_annotation.dart'; + +part 'onesignal_request_body.g.dart'; + +/// {@template onesignal_request_body} +/// Represents the request body for the OneSignal /notifications endpoint. +/// {@endtemplate} +@JsonSerializable( + explicitToJson: true, + includeIfNull: false, // Do not include null fields in the JSON + checked: true, + fieldRename: FieldRename.snake, + createFactory: false, +) +class OneSignalRequestBody extends Equatable { + /// {@macro onesignal_request_body} + const OneSignalRequestBody({ + required this.appId, + required this.includePlayerIds, + required this.headings, + required this.data, + this.contents, + this.bigPicture, + }); + + /// The OneSignal App ID. + final String appId; + + /// A list of OneSignal Player IDs to send the notification to. + final List includePlayerIds; + + /// The notification's title. + final Map headings; + + /// The notification's content. + final Map? contents; + + /// The custom data payload + final PushNotificationPayload data; + + /// The URL of a large image to display in the notification. + final String? bigPicture; + + /// Converts this [OneSignalRequestBody] instance to a JSON map. + Map toJson() => _$OneSignalRequestBodyToJson(this); + + @override + List get props => [ + appId, + includePlayerIds, + headings, + contents, + data, + bigPicture, + ]; +} diff --git a/lib/src/models/push_notification/onesignal_request_body.g.dart b/lib/src/models/push_notification/onesignal_request_body.g.dart new file mode 100644 index 00000000..aa0b00a6 --- /dev/null +++ b/lib/src/models/push_notification/onesignal_request_body.g.dart @@ -0,0 +1,21 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'onesignal_request_body.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +Map _$OneSignalRequestBodyToJson( + OneSignalRequestBody instance, +) => { + 'stringify': ?instance.stringify, + 'hash_code': instance.hashCode, + 'app_id': instance.appId, + 'include_player_ids': instance.includePlayerIds, + 'headings': instance.headings, + 'contents': ?instance.contents, + 'data': instance.data.toJson(), + 'big_picture': ?instance.bigPicture, + 'props': instance.props, +}; diff --git a/lib/src/models/push_notification/push_notification.dart b/lib/src/models/push_notification/push_notification.dart new file mode 100644 index 00000000..0d93a137 --- /dev/null +++ b/lib/src/models/push_notification/push_notification.dart @@ -0,0 +1,2 @@ +export 'firebase_request_body.dart'; +export 'onesignal_request_body.dart'; diff --git a/lib/src/rbac/permissions.dart b/lib/src/rbac/permissions.dart index 307f68cf..58761c63 100644 --- a/lib/src/rbac/permissions.dart +++ b/lib/src/rbac/permissions.dart @@ -50,9 +50,8 @@ abstract class Permissions { static const String userUpdate = 'user.update'; // User App Settings Permissions (User-owned) - static const String userAppSettingsReadOwned = 'user_app_settings.read_owned'; - static const String userAppSettingsUpdateOwned = - 'user_app_settings.update_owned'; + static const String appSettingsReadOwned = 'app_settings.read_owned'; + static const String appSettingsUpdateOwned = 'app_settings.update_owned'; // User Content Preferences Permissions (User-owned) static const String userContentPreferencesReadOwned = diff --git a/lib/src/rbac/role_permissions.dart b/lib/src/rbac/role_permissions.dart index eb3d5818..96b77a75 100644 --- a/lib/src/rbac/role_permissions.dart +++ b/lib/src/rbac/role_permissions.dart @@ -9,8 +9,8 @@ final Set _appGuestUserPermissions = { Permissions.sourceRead, Permissions.countryRead, Permissions.languageRead, - Permissions.userAppSettingsReadOwned, - Permissions.userAppSettingsUpdateOwned, + Permissions.appSettingsReadOwned, + Permissions.appSettingsUpdateOwned, Permissions.userContentPreferencesReadOwned, Permissions.userContentPreferencesUpdateOwned, Permissions.remoteConfigRead, diff --git a/lib/src/registry/data_operation_registry.dart b/lib/src/registry/data_operation_registry.dart index 78047e73..b4fbc60f 100644 --- a/lib/src/registry/data_operation_registry.dart +++ b/lib/src/registry/data_operation_registry.dart @@ -113,8 +113,8 @@ class DataOperationRegistry { c.read>().read(id: id, userId: null), 'user': (c, id) => c.read>().read(id: id, userId: null), - 'user_app_settings': (c, id) => - c.read>().read(id: id, userId: null), + 'app_settings': (c, id) => + c.read>().read(id: id, userId: null), 'user_content_preferences': (c, id) => c .read>() .read(id: id, userId: null), @@ -389,9 +389,9 @@ class DataOperationRegistry { userId: uid, ); }, - 'user_app_settings': (c, id, item, uid) => c - .read>() - .update(id: id, item: item as UserAppSettings, userId: uid), + 'app_settings': (c, id, item, uid) => c + .read>() + .update(id: id, item: item as AppSettings, userId: uid), 'user_content_preferences': (context, id, item, uid) async { _log.info( 'Executing custom updater for user_content_preferences ID: $id.', @@ -454,8 +454,8 @@ class DataOperationRegistry { c.read>().delete(id: id, userId: uid), 'language': (c, id, uid) => c.read>().delete(id: id, userId: uid), - 'user_app_settings': (c, id, uid) => - c.read>().delete(id: id, userId: uid), + 'app_settings': (c, id, uid) => + c.read>().delete(id: id, userId: uid), 'user_content_preferences': (c, id, uid) => c .read>() .delete(id: id, userId: uid), diff --git a/lib/src/registry/model_registry.dart b/lib/src/registry/model_registry.dart index 58a258bb..bf07f048 100644 --- a/lib/src/registry/model_registry.dart +++ b/lib/src/registry/model_registry.dart @@ -305,37 +305,36 @@ final modelRegistry = >{ type: RequiredPermissionType.unsupported, ), ), - 'user_app_settings': ModelConfig( - fromJson: UserAppSettings.fromJson, + 'app_settings': ModelConfig( + fromJson: AppSettings.fromJson, getId: (s) => s.id, - getOwnerId: (dynamic item) => - (item as UserAppSettings).id as String?, // User ID is the owner ID + getOwnerId: (dynamic item) => (item as AppSettings).id, getCollectionPermission: const ModelActionPermission( type: RequiredPermissionType.unsupported, // Not accessible via collection requiresAuthentication: true, ), getItemPermission: const ModelActionPermission( type: RequiredPermissionType.specificPermission, - permission: Permissions.userAppSettingsReadOwned, + permission: Permissions.appSettingsReadOwned, requiresOwnershipCheck: true, requiresAuthentication: true, ), postPermission: const ModelActionPermission( type: RequiredPermissionType.unsupported, requiresAuthentication: true, - // Creation of UserAppSettings is handled by the authentication service + // Creation of AppSettings is handled by the authentication service // during user creation, not via a direct POST to /api/v1/data. ), putPermission: const ModelActionPermission( type: RequiredPermissionType.specificPermission, - permission: Permissions.userAppSettingsUpdateOwned, + permission: Permissions.appSettingsUpdateOwned, requiresOwnershipCheck: true, requiresAuthentication: true, ), deletePermission: const ModelActionPermission( type: RequiredPermissionType.unsupported, requiresAuthentication: true, - // Deletion of UserAppSettings is handled by the authentication service + // Deletion of AppSettings is handled by the authentication service // during account deletion, not via a direct DELETE to /api/v1/data. ), ), diff --git a/lib/src/services/auth_service.dart b/lib/src/services/auth_service.dart index 1b65484b..fe70e34e 100644 --- a/lib/src/services/auth_service.dart +++ b/lib/src/services/auth_service.dart @@ -26,7 +26,7 @@ class AuthService { required AuthTokenService authTokenService, required VerificationCodeStorageService verificationCodeStorageService, required EmailRepository emailRepository, - required DataRepository userAppSettingsRepository, + required DataRepository appSettingsRepository, required DataRepository userContentPreferencesRepository, required PermissionService permissionService, @@ -36,7 +36,7 @@ class AuthService { _verificationCodeStorageService = verificationCodeStorageService, _permissionService = permissionService, _emailRepository = emailRepository, - _userAppSettingsRepository = userAppSettingsRepository, + _appSettingsRepository = appSettingsRepository, _userContentPreferencesRepository = userContentPreferencesRepository, _log = log; @@ -44,7 +44,7 @@ class AuthService { final AuthTokenService _authTokenService; final VerificationCodeStorageService _verificationCodeStorageService; final EmailRepository _emailRepository; - final DataRepository _userAppSettingsRepository; + final DataRepository _appSettingsRepository; final DataRepository _userContentPreferencesRepository; final PermissionService _permissionService; @@ -450,8 +450,8 @@ class AuthService { // 1. Explicitly delete associated user data. Unlike relational databases // with CASCADE constraints, MongoDB requires manual deletion of related // documents in different collections. - await _userAppSettingsRepository.delete(id: userId, userId: userId); - _log.info('Deleted UserAppSettings for user ${userToDelete.id}.'); + await _appSettingsRepository.delete(id: userId, userId: userId); + _log.info('Deleted AppSettings for user ${userToDelete.id}.'); await _userContentPreferencesRepository.delete( id: userId, @@ -513,14 +513,14 @@ class AuthService { /// who might have been created before these documents were part of the standard /// user creation process. Future _ensureUserDataExists(User user) async { - // Check for UserAppSettings + // Check for AppSettings try { - await _userAppSettingsRepository.read(id: user.id, userId: user.id); + await _appSettingsRepository.read(id: user.id, userId: user.id); } on NotFoundException { _log.info( - 'UserAppSettings not found for user ${user.id}. Creating with defaults.', + 'AppSettings not found for user ${user.id}. Creating with defaults.', ); - final defaultAppSettings = UserAppSettings( + final defaultAppSettings = AppSettings( id: user.id, displaySettings: const DisplaySettings( baseTheme: AppBaseTheme.system, @@ -535,14 +535,13 @@ class AuthService { 'Default language "en" not found in language fixtures.', ), ), - feedPreferences: const FeedDisplayPreferences( - headlineDensity: HeadlineDensity.standard, - headlineImageStyle: HeadlineImageStyle.smallThumbnail, - showSourceInHeadlineFeed: true, - showPublishDateInHeadlineFeed: true, + feedSettings: const FeedSettings( + feedItemDensity: FeedItemDensity.standard, + feedItemImageStyle: FeedItemImageStyle.smallThumbnail, + feedItemClickBehavior: FeedItemClickBehavior.internalNavigation, ), ); - await _userAppSettingsRepository.create( + await _appSettingsRepository.create( item: defaultAppSettings, userId: user.id, ); diff --git a/lib/src/services/database_seeding_service.dart b/lib/src/services/database_seeding_service.dart index 7180980c..2d1eff25 100644 --- a/lib/src/services/database_seeding_service.dart +++ b/lib/src/services/database_seeding_service.dart @@ -122,14 +122,16 @@ class DatabaseSeedingService { // Ensure primaryAdPlatform is not 'demo' for initial setup // since its not intended for any use outside the mobile client. - final productionReadyAdConfig = initialConfig.adConfig.copyWith( + final productionReadyAdConfig = initialConfig.features.ads.copyWith( primaryAdPlatform: AdPlatformType.admob, ); final productionReadyConfig = initialConfig.copyWith( - adConfig: productionReadyAdConfig, createdAt: DateTime.now(), updatedAt: DateTime.now(), + features: initialConfig.features.copyWith( + ads: productionReadyAdConfig, + ), ); await remoteConfigCollection.insertOne({ @@ -357,9 +359,7 @@ class DatabaseSeedingService { /// Deletes a user and their associated sub-documents. Future _deleteUserAndData(ObjectId userId) async { await _db.collection('users').deleteOne(where.eq('_id', userId)); - await _db - .collection('user_app_settings') - .deleteOne(where.eq('_id', userId)); + await _db.collection('app_settings').deleteOne(where.eq('_id', userId)); await _db .collection('user_content_preferences') .deleteOne(where.eq('_id', userId)); @@ -368,7 +368,7 @@ class DatabaseSeedingService { /// Creates the default sub-documents (settings, preferences) for a new user. Future _createUserSubDocuments(ObjectId userId) async { - final defaultAppSettings = UserAppSettings( + final defaultAppSettings = AppSettings( id: userId.oid, displaySettings: const DisplaySettings( baseTheme: AppBaseTheme.system, @@ -383,14 +383,13 @@ class DatabaseSeedingService { 'Default language "en" not found in language fixtures.', ), ), - feedPreferences: const FeedDisplayPreferences( - headlineDensity: HeadlineDensity.standard, - headlineImageStyle: HeadlineImageStyle.smallThumbnail, - showSourceInHeadlineFeed: true, - showPublishDateInHeadlineFeed: true, + feedSettings: const FeedSettings( + feedItemDensity: FeedItemDensity.standard, + feedItemImageStyle: FeedItemImageStyle.smallThumbnail, + feedItemClickBehavior: FeedItemClickBehavior.internalNavigation, ), ); - await _db.collection('user_app_settings').insertOne({ + await _db.collection('app_settings').insertOne({ '_id': userId, ...defaultAppSettings.toJson()..remove('id'), }); diff --git a/lib/src/services/default_user_preference_limit_service.dart b/lib/src/services/default_user_preference_limit_service.dart index e182cca8..8b952802 100644 --- a/lib/src/services/default_user_preference_limit_service.dart +++ b/lib/src/services/default_user_preference_limit_service.dart @@ -33,7 +33,7 @@ class DefaultUserPreferenceLimitService implements UserPreferenceLimitService { final remoteConfig = await _remoteConfigRepository.read( id: _remoteConfigId, ); - final limits = remoteConfig.userPreferenceConfig; + final limits = remoteConfig.user.limits; // Retrieve all relevant limits for the user's role from the remote configuration. final ( @@ -204,26 +204,26 @@ class DefaultUserPreferenceLimitService implements UserPreferenceLimitService { ) _getLimitsForRole( AppUserRole role, - UserPreferenceConfig limits, + UserLimitsConfig limits, ) { - final followedItemsLimit = limits.followedItemsLimit[role]; + final followedItemsLimit = limits.followedItems[role]; if (followedItemsLimit == null) { throw StateError('Followed items limit not configured for role: $role'); } - final savedHeadlinesLimit = limits.savedHeadlinesLimit[role]; + final savedHeadlinesLimit = limits.savedHeadlines[role]; if (savedHeadlinesLimit == null) { throw StateError('Saved headlines limit not configured for role: $role'); } - final savedHeadlineFiltersLimit = limits.savedHeadlineFiltersLimit[role]; + final savedHeadlineFiltersLimit = limits.savedHeadlineFilters[role]; if (savedHeadlineFiltersLimit == null) { throw StateError( 'Saved headline filters limit not configured for role: $role', ); } - final savedSourceFiltersLimit = limits.savedSourceFiltersLimit[role]; + final savedSourceFiltersLimit = limits.savedSourceFilters[role]; if (savedSourceFiltersLimit == null) { throw StateError( 'Saved source filters limit not configured for role: $role', diff --git a/lib/src/services/firebase_push_notification_client.dart b/lib/src/services/firebase_push_notification_client.dart index d4135e6f..bba02380 100644 --- a/lib/src/services/firebase_push_notification_client.dart +++ b/lib/src/services/firebase_push_notification_client.dart @@ -1,4 +1,5 @@ import 'package:core/core.dart'; +import 'package:flutter_news_app_api_server_full_source_code/src/models/models.dart'; import 'package:flutter_news_app_api_server_full_source_code/src/services/push_notification_client.dart'; import 'package:http_client/http_client.dart'; import 'package:logging/logging.dart'; @@ -110,17 +111,16 @@ class FirebasePushNotificationClient implements IPushNotificationClient { // Create a list of futures, one for each notification to be sent. final sendFutures = deviceTokens.map((token) { - final requestBody = { - 'message': { - 'token': token, - 'notification': { - 'title': payload.title, - 'body': payload.body, - if (payload.imageUrl != null) 'image': payload.imageUrl, - }, - 'data': payload.data, - }, - }; + final requestBody = FirebaseRequestBody( + message: FirebaseMessage( + token: token, + notification: FirebaseNotification( + title: payload.title, + image: payload.imageUrl, + ), + data: payload, + ), + ); // Return the future from the post request. return _httpClient.post(url, data: requestBody); diff --git a/lib/src/services/onesignal_push_notification_client.dart b/lib/src/services/onesignal_push_notification_client.dart index 47f3251c..1a49350f 100644 --- a/lib/src/services/onesignal_push_notification_client.dart +++ b/lib/src/services/onesignal_push_notification_client.dart @@ -1,4 +1,5 @@ import 'package:core/core.dart'; +import 'package:flutter_news_app_api_server_full_source_code/src/models/models.dart'; import 'package:flutter_news_app_api_server_full_source_code/src/services/push_notification_client.dart'; import 'package:http_client/http_client.dart'; import 'package:logging/logging.dart'; @@ -94,20 +95,18 @@ class OneSignalPushNotificationClient implements IPushNotificationClient { // app_dependencies.dart. The final URL will be: `https://onesignal.com/api/v1/notifications` const url = 'notifications'; - // Construct the OneSignal API request body. - final requestBody = { - 'app_id': appId, - 'include_player_ids': deviceTokens, - 'headings': {'en': payload.title}, - 'contents': {'en': payload.body}, - if (payload.imageUrl != null) 'big_picture': payload.imageUrl, - 'data': payload.data, - }; - _log.finer( 'Sending OneSignal batch of ${deviceTokens.length} notifications.', ); + final requestBody = OneSignalRequestBody( + appId: appId, + includePlayerIds: deviceTokens, + headings: {'en': payload.title}, + bigPicture: payload.imageUrl, + data: payload, + ); + try { // The OneSignal API returns a JSON object with details about the send, // including errors for invalid player IDs. diff --git a/lib/src/services/push_notification_service.dart b/lib/src/services/push_notification_service.dart index dcdf0395..35230993 100644 --- a/lib/src/services/push_notification_service.dart +++ b/lib/src/services/push_notification_service.dart @@ -75,7 +75,7 @@ class DefaultPushNotificationService implements IPushNotificationService { final remoteConfig = await _remoteConfigRepository.read( id: _remoteConfigId, ); - final pushConfig = remoteConfig.pushNotificationConfig; + final pushConfig = remoteConfig.features.pushNotifications; // Check if push notifications are globally enabled. if (!pushConfig.enabled) { @@ -211,16 +211,14 @@ class DefaultPushNotificationService implements IPushNotificationService { id: notificationId.oid, userId: userId, payload: PushNotificationPayload( + // Corrected payload structure title: headline.title, - body: headline.excerpt, imageUrl: headline.imageUrl, - data: { - 'notificationType': - PushNotificationSubscriptionDeliveryType.breakingOnly.name, - 'contentType': 'headline', - 'headlineId': headline.id, - 'notificationId': notificationId.oid, - }, + notificationId: notificationId.oid, + notificationType: + PushNotificationSubscriptionDeliveryType.breakingOnly, + contentType: ContentType.headline, + contentId: headline.id, ), createdAt: DateTime.now(), ); diff --git a/pubspec.lock b/pubspec.lock index 44a50785..780b3c05 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -5,10 +5,10 @@ packages: dependency: transitive description: name: _fe_analyzer_shared - sha256: c209688d9f5a5f26b2fb47a188131a6fb9e876ae9e47af3737c0b4f58a93470d + sha256: "5b7468c326d2f8a4f630056404ca0d291ade42918f4a3c6233618e724f39da8e" url: "https://pub.dev" source: hosted - version: "91.0.0" + version: "92.0.0" adaptive_number: dependency: transitive description: @@ -21,10 +21,10 @@ packages: dependency: transitive description: name: analyzer - sha256: f51c8499b35f9b26820cfe914828a6a98a94efd5cc78b37bb7d03debae3a1d08 + sha256: "70e4b1ef8003c64793a9e268a551a82869a8a96f39deb73dea28084b0e8bf75e" url: "https://pub.dev" source: hosted - version: "8.4.1" + version: "9.0.0" archive: dependency: transitive description: @@ -81,6 +81,62 @@ packages: url: "https://pub.dev" source: hosted version: "1.2.3" + build: + dependency: transitive + description: + name: build + sha256: c1668065e9ba04752570ad7e038288559d1e2ca5c6d0131c0f5f55e39e777413 + url: "https://pub.dev" + source: hosted + version: "4.0.3" + build_config: + dependency: transitive + description: + name: build_config + sha256: "4f64382b97504dc2fcdf487d5aae33418e08b4703fc21249e4db6d804a4d0187" + url: "https://pub.dev" + source: hosted + version: "1.2.0" + build_daemon: + dependency: transitive + description: + name: build_daemon + sha256: bf05f6e12cfea92d3c09308d7bcdab1906cd8a179b023269eed00c071004b957 + url: "https://pub.dev" + source: hosted + version: "4.1.1" + build_runner: + dependency: "direct dev" + description: + name: build_runner + sha256: "110c56ef29b5eb367b4d17fc79375fa8c18a6cd7acd92c05bb3986c17a079057" + url: "https://pub.dev" + source: hosted + version: "2.10.4" + built_collection: + dependency: transitive + description: + name: built_collection + sha256: "376e3dd27b51ea877c28d525560790aee2e6fbb5f20e2f85d5081027d94e2100" + url: "https://pub.dev" + source: hosted + version: "5.1.1" + built_value: + dependency: transitive + description: + name: built_value + sha256: "426cf75afdb23aa74bd4e471704de3f9393f3c7b04c1e2d9c6f1073ae0b8b139" + url: "https://pub.dev" + source: hosted + version: "8.12.1" + checked_yaml: + dependency: transitive + description: + name: checked_yaml + sha256: "959525d3162f249993882720d52b7e0c833978df229be20702b33d48d91de70f" + url: "https://pub.dev" + source: hosted + version: "2.0.4" cli_config: dependency: transitive description: @@ -97,6 +153,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.1.2" + code_builder: + dependency: transitive + description: + name: code_builder + sha256: "11654819532ba94c34de52ff5feb52bd81cba1de00ef2ed622fd50295f9d4243" + url: "https://pub.dev" + source: hosted + version: "4.11.0" collection: dependency: "direct main" description: @@ -117,8 +181,8 @@ packages: dependency: "direct main" description: path: "." - ref: "064c4387b3f7df835565c41c918dc2d80dd2f49a" - resolved-ref: "064c4387b3f7df835565c41c918dc2d80dd2f49a" + ref: c0c41c069b885f0c16d1b134269aa06861634186 + resolved-ref: c0c41c069b885f0c16d1b134269aa06861634186 url: "https://github.com/flutter-news-app-full-source-code/core.git" source: git version: "1.3.1" @@ -154,6 +218,14 @@ packages: url: "https://pub.dev" source: hosted version: "3.3.1" + dart_style: + dependency: transitive + description: + name: dart_style + sha256: a9c30492da18ff84efe2422ba2d319a89942d93e58eb0b73d32abe822ef54b7b + url: "https://pub.dev" + source: hosted + version: "3.1.3" data_client: dependency: "direct main" description: @@ -296,6 +368,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.3" + graphs: + dependency: transitive + description: + name: graphs + sha256: "741bbf84165310a68ff28fe9e727332eef1407342fca52759cb21ad8177bb8d0" + url: "https://pub.dev" + source: hosted + version: "2.3.2" hotreloader: dependency: transitive description: @@ -361,14 +441,6 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.5" - js: - dependency: transitive - description: - name: js - sha256: "53385261521cc4a0c4658fd0ad07a7d14591cf8fc33abbceae306ddb974888dc" - url: "https://pub.dev" - source: hosted - version: "0.7.2" json_annotation: dependency: "direct main" description: @@ -377,6 +449,14 @@ packages: url: "https://pub.dev" source: hosted version: "4.9.0" + json_serializable: + dependency: "direct dev" + description: + name: json_serializable + sha256: "6b253f7851cf1626a05c8b49c792e04a14897349798c03798137f2b5f7e0b5b1" + url: "https://pub.dev" + source: hosted + version: "6.11.3" logging: dependency: "direct main" description: @@ -505,6 +585,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.2.0" + pubspec_parse: + dependency: transitive + description: + name: pubspec_parse + sha256: "0560ba233314abbed0a48a2956f7f022cce7c3e1e73df540277da7544cad4082" + url: "https://pub.dev" + source: hosted + version: "1.5.0" rational: dependency: transitive description: @@ -577,6 +665,22 @@ packages: url: "https://pub.dev" source: hosted version: "3.0.0" + source_gen: + dependency: transitive + description: + name: source_gen + sha256: "07b277b67e0096c45196cbddddf2d8c6ffc49342e88bf31d460ce04605ddac75" + url: "https://pub.dev" + source: hosted + version: "4.1.1" + source_helper: + dependency: transitive + description: + name: source_helper + sha256: e82b1996c63da42aa3e6a34cc1ec17427728a1baf72ed017717a5669a7123f0d + url: "https://pub.dev" + source: hosted + version: "1.3.9" source_map_stack_trace: dependency: transitive description: @@ -653,26 +757,26 @@ packages: dependency: "direct dev" description: name: test - sha256: "75906bf273541b676716d1ca7627a17e4c4070a3a16272b7a3dc7da3b9f3f6b7" + sha256: "77cc98ea27006c84e71a7356cf3daf9ddbde2d91d84f77dbfe64cf0e4d9611ae" url: "https://pub.dev" source: hosted - version: "1.26.3" + version: "1.28.0" test_api: dependency: transitive description: name: test_api - sha256: ab2726c1a94d3176a45960b6234466ec367179b87dd74f1611adb1f3b5fb9d55 + sha256: "19a78f63e83d3a61f00826d09bc2f60e191bf3504183c001262be6ac75589fb8" url: "https://pub.dev" source: hosted - version: "0.7.7" + version: "0.7.8" test_core: dependency: transitive description: name: test_core - sha256: "0cc24b5ff94b38d2ae73e1eb43cc302b77964fbf67abad1e296025b78deb53d0" + sha256: f1072617a6657e5fc09662e721307f7fb009b4ed89b19f47175d11d5254a62d4 url: "https://pub.dev" source: hosted - version: "0.6.12" + version: "0.6.14" typed_data: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index d4eb70db..98ed31a7 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -52,6 +52,8 @@ dependencies: shelf_cors_headers: ^0.1.5 dev_dependencies: + build_runner: ^2.10.4 + json_serializable: ^6.11.3 mocktail: ^1.0.3 test: ^1.25.5 very_good_analysis: ^9.0.0 @@ -60,7 +62,7 @@ dependency_overrides: core: git: url: https://github.com/flutter-news-app-full-source-code/core.git - ref: 064c4387b3f7df835565c41c918dc2d80dd2f49a + ref: c0c41c069b885f0c16d1b134269aa06861634186 http_client: git: url: https://github.com/flutter-news-app-full-source-code/http-client.git diff --git a/routes/_middleware.dart b/routes/_middleware.dart index a0dfa9b3..21607b8c 100644 --- a/routes/_middleware.dart +++ b/routes/_middleware.dart @@ -115,8 +115,8 @@ Handler middleware(Handler handler) { provider>((_) => deps.userRepository), ) // .use( - provider>( - (_) => deps.userAppSettingsRepository, + provider>( + (_) => deps.appSettingsRepository, ), ) .use(