From 70c13394932b8b35f5b5b53bc7bbe131a5cca2b1 Mon Sep 17 00:00:00 2001 From: fulleni Date: Tue, 25 Nov 2025 05:02:58 +0100 Subject: [PATCH 01/27] build(deps): update core dependency - Update git ref in pubspec.yaml and pubspec.lock - Change ref from 064c4387b3f7df835565c41c918dc2d80dd2f49a to c0c41c069b885f0c16d1b134269aa068616341 --- pubspec.lock | 4 ++-- pubspec.yaml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/pubspec.lock b/pubspec.lock index 44a5078..f2b8a98 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -117,8 +117,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" diff --git a/pubspec.yaml b/pubspec.yaml index d4eb70d..fc72bdc 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -60,7 +60,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 From cbdfa8775b682b780441d5cb110a3f0817036019 Mon Sep 17 00:00:00 2001 From: fulleni Date: Tue, 25 Nov 2025 05:26:54 +0100 Subject: [PATCH 02/27] refactor(dependencies): update userAppSettings repository and model names - Rename UserAppSettings to AppSettings in repository and logger - Adjust repository and logger names for userContentPreferences and pushNotificationDevice --- lib/src/config/app_dependencies.dart | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/lib/src/config/app_dependencies.dart b/lib/src/config/app_dependencies.dart index de78c86..b4ec202 100644 --- a/lib/src/config/app_dependencies.dart +++ b/lib/src/config/app_dependencies.dart @@ -65,11 +65,11 @@ class AppDependencies { late final DataRepository countryRepository; late final DataRepository languageRepository; late final DataRepository userRepository; - late final DataRepository userAppSettingsRepository; + late final DataRepository userAppSettingsRepository; late final DataRepository - userContentPreferencesRepository; + userContentPreferencesRepository; late final DataRepository - pushNotificationDeviceRepository; + pushNotificationDeviceRepository; late final DataRepository remoteConfigRepository; late final DataRepository inAppNotificationRepository; @@ -190,12 +190,12 @@ class AppDependencies { toJson: (item) => item.toJson(), logger: Logger('DataMongodb'), ); - final userAppSettingsClient = DataMongodb( + final userAppSettingsClient = DataMongodb( connectionManager: _mongoDbConnectionManager, modelName: 'user_app_settings', - fromJson: UserAppSettings.fromJson, + fromJson: AppSettings.fromJson, toJson: (item) => item.toJson(), - logger: Logger('DataMongodb'), + logger: Logger('DataMongodb'), ); final userContentPreferencesClient = DataMongodb( connectionManager: _mongoDbConnectionManager, From efd24c3ae6e111779cb984d0c78776e96f7a9d77 Mon Sep 17 00:00:00 2001 From: fulleni Date: Tue, 25 Nov 2025 06:23:17 +0100 Subject: [PATCH 03/27] refactor(data): update UserAppSettings to AppSettings - Replace UserAppSettings with AppSettings in data operation registry - Update related data repository references - Adjust type casting in update operations --- lib/src/registry/data_operation_registry.dart | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/src/registry/data_operation_registry.dart b/lib/src/registry/data_operation_registry.dart index 78047e7..c860c2d 100644 --- a/lib/src/registry/data_operation_registry.dart +++ b/lib/src/registry/data_operation_registry.dart @@ -114,7 +114,7 @@ class DataOperationRegistry { 'user': (c, id) => c.read>().read(id: id, userId: null), 'user_app_settings': (c, id) => - c.read>().read(id: id, userId: null), + c.read>().read(id: id, userId: null), 'user_content_preferences': (c, id) => c .read>() .read(id: id, userId: null), @@ -390,8 +390,8 @@ class DataOperationRegistry { ); }, 'user_app_settings': (c, id, item, uid) => c - .read>() - .update(id: id, item: item as UserAppSettings, userId: uid), + .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.', @@ -455,7 +455,7 @@ class DataOperationRegistry { 'language': (c, id, uid) => c.read>().delete(id: id, userId: uid), 'user_app_settings': (c, id, uid) => - c.read>().delete(id: id, userId: uid), + c.read>().delete(id: id, userId: uid), 'user_content_preferences': (c, id, uid) => c .read>() .delete(id: id, userId: uid), From 9c0a23d0481e7530e1cbbf9a29119e433d657bae Mon Sep 17 00:00:00 2001 From: fulleni Date: Tue, 25 Nov 2025 06:26:56 +0100 Subject: [PATCH 04/27] refactor(registry): update model config for app settings - Rename UserAppSettings to AppSettings - Simplify getOwnerId function - Update permissions configuration --- lib/src/registry/model_registry.dart | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/lib/src/registry/model_registry.dart b/lib/src/registry/model_registry.dart index 58a258b..f91cdf7 100644 --- a/lib/src/registry/model_registry.dart +++ b/lib/src/registry/model_registry.dart @@ -305,11 +305,10 @@ final modelRegistry = >{ type: RequiredPermissionType.unsupported, ), ), - 'user_app_settings': ModelConfig( - fromJson: UserAppSettings.fromJson, + 'user_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, From 8a4b5e530d133550f15a911f9dbf38af4fc0a5c4 Mon Sep 17 00:00:00 2001 From: fulleni Date: Tue, 25 Nov 2025 06:27:08 +0100 Subject: [PATCH 05/27] fix(database): rename user_app_settings collection to app_settings - Update collection name in AppDependencies - Update related permissions in Permissions class - Update DataOperationRegistry to use new collection name - Update ModelRegistry to reflect new collection name - Update DatabaseSeedingService to use new collection name These changes ensure consistency across the codebase and database, using the more appropriate "app_settings" collection name instead of "user_app_settings". --- lib/src/config/app_dependencies.dart | 6 +++--- lib/src/rbac/permissions.dart | 5 ++--- lib/src/registry/data_operation_registry.dart | 6 +++--- lib/src/registry/model_registry.dart | 2 +- lib/src/services/database_seeding_service.dart | 4 ++-- 5 files changed, 11 insertions(+), 12 deletions(-) diff --git a/lib/src/config/app_dependencies.dart b/lib/src/config/app_dependencies.dart index b4ec202..08743d5 100644 --- a/lib/src/config/app_dependencies.dart +++ b/lib/src/config/app_dependencies.dart @@ -67,9 +67,9 @@ class AppDependencies { late final DataRepository userRepository; late final DataRepository userAppSettingsRepository; late final DataRepository - userContentPreferencesRepository; + userContentPreferencesRepository; late final DataRepository - pushNotificationDeviceRepository; + pushNotificationDeviceRepository; late final DataRepository remoteConfigRepository; late final DataRepository inAppNotificationRepository; @@ -192,7 +192,7 @@ class AppDependencies { ); final userAppSettingsClient = DataMongodb( connectionManager: _mongoDbConnectionManager, - modelName: 'user_app_settings', + modelName: 'app_settings', fromJson: AppSettings.fromJson, toJson: (item) => item.toJson(), logger: Logger('DataMongodb'), diff --git a/lib/src/rbac/permissions.dart b/lib/src/rbac/permissions.dart index 307f68c..9e6b40f 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 userAppSettingsReadOwned = 'app_settings.read_owned'; + static const String userAppSettingsUpdateOwned = 'app_settings.update_owned'; // User Content Preferences Permissions (User-owned) static const String userContentPreferencesReadOwned = diff --git a/lib/src/registry/data_operation_registry.dart b/lib/src/registry/data_operation_registry.dart index c860c2d..b4fbc60 100644 --- a/lib/src/registry/data_operation_registry.dart +++ b/lib/src/registry/data_operation_registry.dart @@ -113,7 +113,7 @@ class DataOperationRegistry { c.read>().read(id: id, userId: null), 'user': (c, id) => c.read>().read(id: id, userId: null), - 'user_app_settings': (c, id) => + 'app_settings': (c, id) => c.read>().read(id: id, userId: null), 'user_content_preferences': (c, id) => c .read>() @@ -389,7 +389,7 @@ class DataOperationRegistry { userId: uid, ); }, - 'user_app_settings': (c, id, item, uid) => c + '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 { @@ -454,7 +454,7 @@ 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) => + 'app_settings': (c, id, uid) => c.read>().delete(id: id, userId: uid), 'user_content_preferences': (c, id, uid) => c .read>() diff --git a/lib/src/registry/model_registry.dart b/lib/src/registry/model_registry.dart index f91cdf7..e9575db 100644 --- a/lib/src/registry/model_registry.dart +++ b/lib/src/registry/model_registry.dart @@ -305,7 +305,7 @@ final modelRegistry = >{ type: RequiredPermissionType.unsupported, ), ), - 'user_app_settings': ModelConfig( + 'app_settings': ModelConfig( fromJson: AppSettings.fromJson, getId: (s) => s.id, getOwnerId: (dynamic item) => (item as AppSettings).id, diff --git a/lib/src/services/database_seeding_service.dart b/lib/src/services/database_seeding_service.dart index 7180980..1288c0f 100644 --- a/lib/src/services/database_seeding_service.dart +++ b/lib/src/services/database_seeding_service.dart @@ -358,7 +358,7 @@ class DatabaseSeedingService { Future _deleteUserAndData(ObjectId userId) async { await _db.collection('users').deleteOne(where.eq('_id', userId)); await _db - .collection('user_app_settings') + .collection('app_settings') .deleteOne(where.eq('_id', userId)); await _db .collection('user_content_preferences') @@ -390,7 +390,7 @@ class DatabaseSeedingService { showPublishDateInHeadlineFeed: true, ), ); - await _db.collection('user_app_settings').insertOne({ + await _db.collection('app_settings').insertOne({ '_id': userId, ...defaultAppSettings.toJson()..remove('id'), }); From 84d99dd7d8add1ead117a1ec794f1bf77d0c4eb3 Mon Sep 17 00:00:00 2001 From: fulleni Date: Tue, 25 Nov 2025 06:34:55 +0100 Subject: [PATCH 06/27] refactor(auth): update AuthService and related components - Update UserAppSettings to AppSettings in AuthService - Rename feedPreferences to feedSettings and update related properties - Adjust class names and method calls to reflect new naming conventions --- lib/src/services/auth_service.dart | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/lib/src/services/auth_service.dart b/lib/src/services/auth_service.dart index 1b65484..333d246 100644 --- a/lib/src/services/auth_service.dart +++ b/lib/src/services/auth_service.dart @@ -24,9 +24,10 @@ class AuthService { const AuthService({ required DataRepository userRepository, required AuthTokenService authTokenService, - required VerificationCodeStorageService verificationCodeStorageService, + required VerificationCodeStorageService + verificationCodeStorageService, required EmailRepository emailRepository, - required DataRepository userAppSettingsRepository, + required DataRepository appSettingsRepository, required DataRepository userContentPreferencesRepository, required PermissionService permissionService, @@ -36,7 +37,7 @@ class AuthService { _verificationCodeStorageService = verificationCodeStorageService, _permissionService = permissionService, _emailRepository = emailRepository, - _userAppSettingsRepository = userAppSettingsRepository, + _appSettingsRepository = appSettingsRepository, _userContentPreferencesRepository = userContentPreferencesRepository, _log = log; @@ -44,7 +45,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,7 +451,7 @@ 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); + await _appSettingsRepository.delete(id: userId, userId: userId); _log.info('Deleted UserAppSettings for user ${userToDelete.id}.'); await _userContentPreferencesRepository.delete( @@ -515,12 +516,12 @@ class AuthService { Future _ensureUserDataExists(User user) async { // Check for UserAppSettings 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 +536,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, ); From e7150b374b99d0389eafce5a5c9a0626e5758c2e Mon Sep 17 00:00:00 2001 From: fulleni Date: Tue, 25 Nov 2025 06:36:04 +0100 Subject: [PATCH 07/27] fix(database): update seeding service for app settings and ad config - Update ad config setup in initial configuration - Modify app settings structure for user creation - Refactor user deletion process to remove app settings - Adjust feed preferences to feed settings in default app settings --- .../services/database_seeding_service.dart | 21 +++++++++---------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/lib/src/services/database_seeding_service.dart b/lib/src/services/database_seeding_service.dart index 1288c0f..2d1eff2 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('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,11 +383,10 @@ 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('app_settings').insertOne({ From 5260c71db1657d19032b1cf41e25a75cfcb8edec Mon Sep 17 00:00:00 2001 From: fulleni Date: Tue, 25 Nov 2025 06:36:59 +0100 Subject: [PATCH 08/27] refactor(limit): update user limits configuration retrieval - Modify the retrieval path for user limits from remote configuration - Update variable names to reflect the new structure of limits configuration - Adjust the limits retrieval logic for different user roles --- lib/src/config/app_dependencies.dart | 2 +- .../default_user_preference_limit_service.dart | 12 ++++++------ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/lib/src/config/app_dependencies.dart b/lib/src/config/app_dependencies.dart index 08743d5..e1e5bda 100644 --- a/lib/src/config/app_dependencies.dart +++ b/lib/src/config/app_dependencies.dart @@ -359,7 +359,7 @@ class AppDependencies { verificationCodeStorageService: verificationCodeStorageService, permissionService: permissionService, emailRepository: emailRepository, - userAppSettingsRepository: userAppSettingsRepository, + appSettingsRepository: userAppSettingsRepository, userContentPreferencesRepository: userContentPreferencesRepository, log: Logger('AuthService'), ); diff --git a/lib/src/services/default_user_preference_limit_service.dart b/lib/src/services/default_user_preference_limit_service.dart index e182cca..8b95280 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', From a72e689472dd816961398c862af430c433c0b0f1 Mon Sep 17 00:00:00 2001 From: fulleni Date: Tue, 25 Nov 2025 06:43:22 +0100 Subject: [PATCH 09/27] fix(firebase_push_notification_client): correct notification payload mapping - Replace payload.body with payload.title in notification body - Reconstruct data payload from explicit fields instead of using payload.data directly - Update data payload structure to include specific notification details --- .../services/firebase_push_notification_client.dart | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/lib/src/services/firebase_push_notification_client.dart b/lib/src/services/firebase_push_notification_client.dart index d4135e6..8723398 100644 --- a/lib/src/services/firebase_push_notification_client.dart +++ b/lib/src/services/firebase_push_notification_client.dart @@ -115,10 +115,16 @@ class FirebasePushNotificationClient implements IPushNotificationClient { 'token': token, 'notification': { 'title': payload.title, - 'body': payload.body, + 'body': payload.title, if (payload.imageUrl != null) 'image': payload.imageUrl, }, - 'data': payload.data, + // Reconstruct the data payload from the explicit fields + 'data': { + 'notificationId': payload.notificationId, + 'notificationType': payload.notificationType.name, + 'contentType': payload.contentType.name, + 'contentId': payload.contentId, + }, }, }; From 7e4e07790a1ee33e876cb261e3abaa82ae66c80b Mon Sep 17 00:00:00 2001 From: fulleni Date: Tue, 25 Nov 2025 06:43:39 +0100 Subject: [PATCH 10/27] fix(onesignal): correct payload mapping and enhancement - Map notification title to 'contents' instead of body - Remove direct payload.data mapping and replace with individual fields - Add 'notificationId', 'notificationType', 'contentType', and 'contentId' to data payload --- .../services/onesignal_push_notification_client.dart | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/lib/src/services/onesignal_push_notification_client.dart b/lib/src/services/onesignal_push_notification_client.dart index 47f3251..1c1a129 100644 --- a/lib/src/services/onesignal_push_notification_client.dart +++ b/lib/src/services/onesignal_push_notification_client.dart @@ -99,9 +99,15 @@ class OneSignalPushNotificationClient implements IPushNotificationClient { 'app_id': appId, 'include_player_ids': deviceTokens, 'headings': {'en': payload.title}, - 'contents': {'en': payload.body}, + 'contents': {'en': payload.title}, if (payload.imageUrl != null) 'big_picture': payload.imageUrl, - 'data': payload.data, + // Reconstruct the data payload from the explicit fields + 'data': { + 'notificationId': payload.notificationId, + 'notificationType': payload.notificationType.name, + 'contentType': payload.contentType.name, + 'contentId': payload.contentId, + }, }; _log.finer( From 1349628c20a85a02b04245856c42c172e871bcdc Mon Sep 17 00:00:00 2001 From: fulleni Date: Tue, 25 Nov 2025 06:43:47 +0100 Subject: [PATCH 11/27] refactor(push-notification): update notification payload structure - Corrected the structure of the PushNotificationPayload - Removed unnecessary fields (body, data) - Added new fields (notificationId, notificationType, contentType, contentId) - Simplified the payload object to improve readability and maintainability --- lib/src/services/push_notification_service.dart | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/lib/src/services/push_notification_service.dart b/lib/src/services/push_notification_service.dart index dcdf039..59d0290 100644 --- a/lib/src/services/push_notification_service.dart +++ b/lib/src/services/push_notification_service.dart @@ -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(), ); From ffd81d04b6d5c9d6d277a162650b4a6a060c398e Mon Sep 17 00:00:00 2001 From: fulleni Date: Tue, 25 Nov 2025 06:45:42 +0100 Subject: [PATCH 12/27] fix(push-notification): update push notification configuration path - Change remoteConfig.pushNotificationConfig to remoteConfig.features.pushNotifications - This modification ensures compatibility with the latest remote config structure --- lib/src/services/push_notification_service.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/src/services/push_notification_service.dart b/lib/src/services/push_notification_service.dart index 59d0290..3523099 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) { From d7366ed8bccdaf94fe3523fe1a50f6df151e655b Mon Sep 17 00:00:00 2001 From: fulleni Date: Tue, 25 Nov 2025 06:52:49 +0100 Subject: [PATCH 13/27] feat(models): add push notification request models - Add FirebaseRequestBody and OneSignalRequestBody models - Include necessary JSON serialization and equity comparison - Export new models from push_notification.dart --- lib/src/models/models.dart | 2 + .../firebase_request_body.dart | 79 +++++++++++++++++++ .../onesignal_request_body.dart | 56 +++++++++++++ .../push_notification/push_notification.dart | 2 + 4 files changed, 139 insertions(+) create mode 100644 lib/src/models/models.dart create mode 100644 lib/src/models/push_notification/firebase_request_body.dart create mode 100644 lib/src/models/push_notification/onesignal_request_body.dart create mode 100644 lib/src/models/push_notification/push_notification.dart diff --git a/lib/src/models/models.dart b/lib/src/models/models.dart new file mode 100644 index 0000000..97f2cea --- /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 0000000..275ec74 --- /dev/null +++ b/lib/src/models/push_notification/firebase_request_body.dart @@ -0,0 +1,79 @@ +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) +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) +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 Map 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) +class FirebaseNotification extends Equatable { + /// {@macro firebase_notification} + const FirebaseNotification({ + required this.title, + required 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/onesignal_request_body.dart b/lib/src/models/push_notification/onesignal_request_body.dart new file mode 100644 index 0000000..488a7c2 --- /dev/null +++ b/lib/src/models/push_notification/onesignal_request_body.dart @@ -0,0 +1,56 @@ +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, +) +class OneSignalRequestBody extends Equatable { + /// {@macro onesignal_request_body} + const OneSignalRequestBody({ + required this.appId, + required this.includePlayerIds, + required this.headings, + required this.contents, + required this.data, + 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 Map 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/push_notification.dart b/lib/src/models/push_notification/push_notification.dart new file mode 100644 index 0000000..0d93a13 --- /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'; From e086620ec966f800c2e6d8afbfaca17f1756ded3 Mon Sep 17 00:00:00 2001 From: fulleni Date: Tue, 25 Nov 2025 07:00:10 +0100 Subject: [PATCH 14/27] refactor(push_notification): update FirebaseMessage data payload type - Import core package for PushNotificationPayload - Change data type from Map to PushNotificationPayload --- lib/src/models/push_notification/firebase_request_body.dart | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/src/models/push_notification/firebase_request_body.dart b/lib/src/models/push_notification/firebase_request_body.dart index 275ec74..0bcf33d 100644 --- a/lib/src/models/push_notification/firebase_request_body.dart +++ b/lib/src/models/push_notification/firebase_request_body.dart @@ -1,3 +1,4 @@ +import 'package:core/core.dart'; import 'package:equatable/equatable.dart'; import 'package:json_annotation/json_annotation.dart'; @@ -41,7 +42,7 @@ class FirebaseMessage extends Equatable { final FirebaseNotification notification; /// The custom data payload. - final Map data; + final PushNotificationPayload data; /// Converts this [FirebaseMessage] instance to a JSON map. Map toJson() => _$FirebaseMessageToJson(this); From ecde6b6a777d148040e758d8aacdca8861611167 Mon Sep 17 00:00:00 2001 From: fulleni Date: Tue, 25 Nov 2025 07:00:51 +0100 Subject: [PATCH 15/27] refactor(push_notification): update data payload type in OneSignalRequestBody - Replace `Map` with `PushNotificationPayload` for better type safety - Import `core` package to include `PushNotificationPayload` definition --- lib/src/models/push_notification/onesignal_request_body.dart | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/lib/src/models/push_notification/onesignal_request_body.dart b/lib/src/models/push_notification/onesignal_request_body.dart index 488a7c2..74ecf3c 100644 --- a/lib/src/models/push_notification/onesignal_request_body.dart +++ b/lib/src/models/push_notification/onesignal_request_body.dart @@ -1,3 +1,4 @@ +import 'package:core/core.dart'; import 'package:equatable/equatable.dart'; import 'package:json_annotation/json_annotation.dart'; @@ -35,8 +36,8 @@ class OneSignalRequestBody extends Equatable { /// The notification's content. final Map contents; - /// The custom data payload. - final Map data; + /// The custom data payload + final PushNotificationPayload data; /// The URL of a large image to display in the notification. final String? bigPicture; From d15de37297b66d388d421b111fbdd63039d525b0 Mon Sep 17 00:00:00 2001 From: fulleni Date: Tue, 25 Nov 2025 07:06:38 +0100 Subject: [PATCH 16/27] refactor(push-notification): use FirebaseRequestBody model for notification - Replace manual JSON map construction with FirebaseRequestBody model - Simplify requestBody creation by using FirebaseMessage and FirebaseNotification models - Remove redundant fields and improve code readability --- .../firebase_push_notification_client.dart | 29 ++++++++----------- 1 file changed, 12 insertions(+), 17 deletions(-) diff --git a/lib/src/services/firebase_push_notification_client.dart b/lib/src/services/firebase_push_notification_client.dart index 8723398..e3ff0d5 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,23 +111,17 @@ 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.title, - if (payload.imageUrl != null) 'image': payload.imageUrl, - }, - // Reconstruct the data payload from the explicit fields - 'data': { - 'notificationId': payload.notificationId, - 'notificationType': payload.notificationType.name, - 'contentType': payload.contentType.name, - 'contentId': payload.contentId, - }, - }, - }; + final requestBody = FirebaseRequestBody( + message: FirebaseMessage( + token: token, + notification: FirebaseNotification( + title: payload.title, + body: payload.title, + image: payload.imageUrl, + ), + data: payload, + ), + ); // Return the future from the post request. return _httpClient.post(url, data: requestBody); From ee0483c1175305641ff4ab44be35c96435b0073e Mon Sep 17 00:00:00 2001 From: fulleni Date: Tue, 25 Nov 2025 07:07:03 +0100 Subject: [PATCH 17/27] refactor(push-notification): use OneSignalRequestBody model for API request - Replace manual JSON construction with OneSignalRequestBody model - Simplify request body creation by using a dedicated model class - Improve code readability and maintainability --- .../onesignal_push_notification_client.dart | 26 +++++++------------ 1 file changed, 10 insertions(+), 16 deletions(-) diff --git a/lib/src/services/onesignal_push_notification_client.dart b/lib/src/services/onesignal_push_notification_client.dart index 1c1a129..1fa043c 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,26 +95,19 @@ 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.title}, - if (payload.imageUrl != null) 'big_picture': payload.imageUrl, - // Reconstruct the data payload from the explicit fields - 'data': { - 'notificationId': payload.notificationId, - 'notificationType': payload.notificationType.name, - 'contentType': payload.contentType.name, - 'contentId': payload.contentId, - }, - }; - _log.finer( 'Sending OneSignal batch of ${deviceTokens.length} notifications.', ); + final requestBody = OneSignalRequestBody( + appId: appId, + includePlayerIds: deviceTokens, + headings: {'en': payload.title}, + contents: {'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. From 012443fde7aff7db6ffc6844ee3407cc9ce85569 Mon Sep 17 00:00:00 2001 From: fulleni Date: Tue, 25 Nov 2025 07:11:15 +0100 Subject: [PATCH 18/27] build(dev_tools): update dependencies and add build tools - Update _fe_analyzer_shared from 91.0.0 to 92.0.0 - Update analyzer from 8.4.1 to 9.0.0 - Add build_runner and json_serializable dev dependencies - Update other dependencies including build, build_config, build_daemon, checked_yaml, code_builder, dart_style, graphs, pubspec_parse, source_gen, source_helper - Update test from 1.26.3 to 1.28.0 and related test dependencies --- pubspec.lock | 140 ++++++++++++++++++++++++++++++++++++++++++++------- pubspec.yaml | 2 + 2 files changed, 124 insertions(+), 18 deletions(-) diff --git a/pubspec.lock b/pubspec.lock index f2b8a98..780b3c0 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: @@ -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 fc72bdc..98ed31a 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 From 1b7e0f43914ee922b147b5e7732c825f1d5c894a Mon Sep 17 00:00:00 2001 From: fulleni Date: Tue, 25 Nov 2025 07:14:01 +0100 Subject: [PATCH 19/27] ci: ignore cast_nullable_to_non_nullable lint rule - Add 'cast_nullable_to_non_nullable: ignore' to analysis_options.yaml - This change allows ignoring issues related to casting nullable types to non-nullable types --- analysis_options.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/analysis_options.yaml b/analysis_options.yaml index 6f69771..7b5c1a6 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: From c937a7108ff1e7734f389c467f45a9e2d297a12d Mon Sep 17 00:00:00 2001 From: fulleni Date: Tue, 25 Nov 2025 07:14:13 +0100 Subject: [PATCH 20/27] build(models): add generated JSON serialization code for push notification models - Add firebase_request_body.g.dart: generated code for FirebaseRequestBody serialization - Add onesignal_request_body.g.dart: generated code for OneSignalRequestBody serialization --- .../firebase_request_body.g.dart | 64 +++++++++++++++++++ .../onesignal_request_body.g.dart | 53 +++++++++++++++ 2 files changed, 117 insertions(+) create mode 100644 lib/src/models/push_notification/firebase_request_body.g.dart create mode 100644 lib/src/models/push_notification/onesignal_request_body.g.dart 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 0000000..ad96407 --- /dev/null +++ b/lib/src/models/push_notification/firebase_request_body.g.dart @@ -0,0 +1,64 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'firebase_request_body.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +FirebaseRequestBody _$FirebaseRequestBodyFromJson(Map json) => + $checkedCreate('FirebaseRequestBody', json, ($checkedConvert) { + final val = FirebaseRequestBody( + message: $checkedConvert( + 'message', + (v) => FirebaseMessage.fromJson(v as Map), + ), + ); + return val; + }); + +Map _$FirebaseRequestBodyToJson( + FirebaseRequestBody instance, +) => {'message': instance.message.toJson()}; + +FirebaseMessage _$FirebaseMessageFromJson(Map json) => + $checkedCreate('FirebaseMessage', json, ($checkedConvert) { + final val = FirebaseMessage( + token: $checkedConvert('token', (v) => v as String), + notification: $checkedConvert( + 'notification', + (v) => FirebaseNotification.fromJson(v as Map), + ), + data: $checkedConvert( + 'data', + (v) => PushNotificationPayload.fromJson(v as Map), + ), + ); + return val; + }); + +Map _$FirebaseMessageToJson(FirebaseMessage instance) => + { + 'token': instance.token, + 'notification': instance.notification.toJson(), + 'data': instance.data.toJson(), + }; + +FirebaseNotification _$FirebaseNotificationFromJson( + Map json, +) => $checkedCreate('FirebaseNotification', json, ($checkedConvert) { + final val = FirebaseNotification( + title: $checkedConvert('title', (v) => v as String), + body: $checkedConvert('body', (v) => v as String), + image: $checkedConvert('image', (v) => v as String?), + ); + return val; +}); + +Map _$FirebaseNotificationToJson( + FirebaseNotification instance, +) => { + 'title': instance.title, + 'body': instance.body, + 'image': instance.image, +}; 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 0000000..20b6fa1 --- /dev/null +++ b/lib/src/models/push_notification/onesignal_request_body.g.dart @@ -0,0 +1,53 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'onesignal_request_body.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +OneSignalRequestBody _$OneSignalRequestBodyFromJson( + Map json, +) => $checkedCreate( + 'OneSignalRequestBody', + json, + ($checkedConvert) { + final val = OneSignalRequestBody( + appId: $checkedConvert('app_id', (v) => v as String), + includePlayerIds: $checkedConvert( + 'include_player_ids', + (v) => (v as List).map((e) => e as String).toList(), + ), + headings: $checkedConvert( + 'headings', + (v) => Map.from(v as Map), + ), + contents: $checkedConvert( + 'contents', + (v) => Map.from(v as Map), + ), + data: $checkedConvert( + 'data', + (v) => PushNotificationPayload.fromJson(v as Map), + ), + bigPicture: $checkedConvert('big_picture', (v) => v as String?), + ); + return val; + }, + fieldKeyMap: const { + 'appId': 'app_id', + 'includePlayerIds': 'include_player_ids', + 'bigPicture': 'big_picture', + }, +); + +Map _$OneSignalRequestBodyToJson( + OneSignalRequestBody instance, +) => { + 'app_id': instance.appId, + 'include_player_ids': instance.includePlayerIds, + 'headings': instance.headings, + 'contents': instance.contents, + 'data': instance.data.toJson(), + 'big_picture': ?instance.bigPicture, +}; From 172597f9358b11f7ed1cb2c87d951ec132967bd0 Mon Sep 17 00:00:00 2001 From: fulleni Date: Tue, 25 Nov 2025 07:21:09 +0100 Subject: [PATCH 21/27] fix(models): add createFactory: false to JsonSerializable annotation - Adds createFactory: false to the @JsonSerializable annotation in multiple model classes to prevent the generation of `fromJson` factory methods. - This change affects FirebaseRequestBody, FirebaseMessage, FirebaseNotification, and OneSignalRequestBody classes. --- .../firebase_request_body.dart | 21 ++++++++++++++++--- .../onesignal_request_body.dart | 1 + 2 files changed, 19 insertions(+), 3 deletions(-) diff --git a/lib/src/models/push_notification/firebase_request_body.dart b/lib/src/models/push_notification/firebase_request_body.dart index 0bcf33d..cee0256 100644 --- a/lib/src/models/push_notification/firebase_request_body.dart +++ b/lib/src/models/push_notification/firebase_request_body.dart @@ -8,7 +8,12 @@ part 'firebase_request_body.g.dart'; /// Represents the top-level structure for a Firebase Cloud Messaging /// v1 API request. /// {@endtemplate} -@JsonSerializable(explicitToJson: true, includeIfNull: true, checked: true) +@JsonSerializable( + explicitToJson: true, + includeIfNull: true, + checked: true, + createFactory: false, +) class FirebaseRequestBody extends Equatable { /// {@macro firebase_request_body} const FirebaseRequestBody({required this.message}); @@ -26,7 +31,12 @@ class FirebaseRequestBody extends Equatable { /// {@template firebase_message} /// Represents the message object within a Firebase request. /// {@endtemplate} -@JsonSerializable(explicitToJson: true, includeIfNull: true, checked: true) +@JsonSerializable( + explicitToJson: true, + includeIfNull: true, + checked: true, + createFactory: false, +) class FirebaseMessage extends Equatable { /// {@macro firebase_message} const FirebaseMessage({ @@ -54,7 +64,12 @@ class FirebaseMessage extends Equatable { /// {@template firebase_notification} /// Represents the notification content within a Firebase message. /// {@endtemplate} -@JsonSerializable(explicitToJson: true, includeIfNull: true, checked: true) +@JsonSerializable( + explicitToJson: true, + includeIfNull: true, + checked: true, + createFactory: false, +) class FirebaseNotification extends Equatable { /// {@macro firebase_notification} const FirebaseNotification({ diff --git a/lib/src/models/push_notification/onesignal_request_body.dart b/lib/src/models/push_notification/onesignal_request_body.dart index 74ecf3c..ee65906 100644 --- a/lib/src/models/push_notification/onesignal_request_body.dart +++ b/lib/src/models/push_notification/onesignal_request_body.dart @@ -12,6 +12,7 @@ part 'onesignal_request_body.g.dart'; 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} From f44e32ce06cd9c5e8e380b203c0b4d79b73c7c17 Mon Sep 17 00:00:00 2001 From: fulleni Date: Tue, 25 Nov 2025 07:21:31 +0100 Subject: [PATCH 22/27] build(serialization): generate --- .../firebase_request_body.g.dart | 51 +++++-------------- .../onesignal_request_body.g.dart | 38 ++------------ 2 files changed, 15 insertions(+), 74 deletions(-) diff --git a/lib/src/models/push_notification/firebase_request_body.g.dart b/lib/src/models/push_notification/firebase_request_body.g.dart index ad96407..bba3ed3 100644 --- a/lib/src/models/push_notification/firebase_request_body.g.dart +++ b/lib/src/models/push_notification/firebase_request_body.g.dart @@ -6,59 +6,32 @@ part of 'firebase_request_body.dart'; // JsonSerializableGenerator // ************************************************************************** -FirebaseRequestBody _$FirebaseRequestBodyFromJson(Map json) => - $checkedCreate('FirebaseRequestBody', json, ($checkedConvert) { - final val = FirebaseRequestBody( - message: $checkedConvert( - 'message', - (v) => FirebaseMessage.fromJson(v as Map), - ), - ); - return val; - }); - Map _$FirebaseRequestBodyToJson( FirebaseRequestBody instance, -) => {'message': instance.message.toJson()}; - -FirebaseMessage _$FirebaseMessageFromJson(Map json) => - $checkedCreate('FirebaseMessage', json, ($checkedConvert) { - final val = FirebaseMessage( - token: $checkedConvert('token', (v) => v as String), - notification: $checkedConvert( - 'notification', - (v) => FirebaseNotification.fromJson(v as Map), - ), - data: $checkedConvert( - 'data', - (v) => PushNotificationPayload.fromJson(v as Map), - ), - ); - return val; - }); +) => { + '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, }; -FirebaseNotification _$FirebaseNotificationFromJson( - Map json, -) => $checkedCreate('FirebaseNotification', json, ($checkedConvert) { - final val = FirebaseNotification( - title: $checkedConvert('title', (v) => v as String), - body: $checkedConvert('body', (v) => v as String), - image: $checkedConvert('image', (v) => v as String?), - ); - return val; -}); - 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.g.dart b/lib/src/models/push_notification/onesignal_request_body.g.dart index 20b6fa1..c562e1e 100644 --- a/lib/src/models/push_notification/onesignal_request_body.g.dart +++ b/lib/src/models/push_notification/onesignal_request_body.g.dart @@ -6,48 +6,16 @@ part of 'onesignal_request_body.dart'; // JsonSerializableGenerator // ************************************************************************** -OneSignalRequestBody _$OneSignalRequestBodyFromJson( - Map json, -) => $checkedCreate( - 'OneSignalRequestBody', - json, - ($checkedConvert) { - final val = OneSignalRequestBody( - appId: $checkedConvert('app_id', (v) => v as String), - includePlayerIds: $checkedConvert( - 'include_player_ids', - (v) => (v as List).map((e) => e as String).toList(), - ), - headings: $checkedConvert( - 'headings', - (v) => Map.from(v as Map), - ), - contents: $checkedConvert( - 'contents', - (v) => Map.from(v as Map), - ), - data: $checkedConvert( - 'data', - (v) => PushNotificationPayload.fromJson(v as Map), - ), - bigPicture: $checkedConvert('big_picture', (v) => v as String?), - ); - return val; - }, - fieldKeyMap: const { - 'appId': 'app_id', - 'includePlayerIds': 'include_player_ids', - 'bigPicture': 'big_picture', - }, -); - 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, }; From 0c1a5859f679cf4927149d11eb59c5498309c706 Mon Sep 17 00:00:00 2001 From: fulleni Date: Tue, 25 Nov 2025 07:22:14 +0100 Subject: [PATCH 23/27] refactor(routes): update dependency type in middleware - Change DataRepository to DataRepository --- routes/_middleware.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/routes/_middleware.dart b/routes/_middleware.dart index a0dfa9b..f13dcc0 100644 --- a/routes/_middleware.dart +++ b/routes/_middleware.dart @@ -115,7 +115,7 @@ Handler middleware(Handler handler) { provider>((_) => deps.userRepository), ) // .use( - provider>( + provider>( (_) => deps.userAppSettingsRepository, ), ) From 6a694850e6cdc8990bf4ff02c8ee1d4fef4d3d2b Mon Sep 17 00:00:00 2001 From: fulleni Date: Tue, 25 Nov 2025 07:23:53 +0100 Subject: [PATCH 24/27] style: format --- lib/src/services/auth_service.dart | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/lib/src/services/auth_service.dart b/lib/src/services/auth_service.dart index 333d246..adbd8ca 100644 --- a/lib/src/services/auth_service.dart +++ b/lib/src/services/auth_service.dart @@ -24,8 +24,7 @@ class AuthService { const AuthService({ required DataRepository userRepository, required AuthTokenService authTokenService, - required VerificationCodeStorageService - verificationCodeStorageService, + required VerificationCodeStorageService verificationCodeStorageService, required EmailRepository emailRepository, required DataRepository appSettingsRepository, required DataRepository From 67d249689120c0e2012e48512f787e865fdfa893 Mon Sep 17 00:00:00 2001 From: fulleni Date: Tue, 25 Nov 2025 07:28:07 +0100 Subject: [PATCH 25/27] chore: misc --- lib/src/config/app_dependencies.dart | 10 +++++----- lib/src/rbac/permissions.dart | 4 ++-- lib/src/rbac/role_permissions.dart | 4 ++-- lib/src/registry/model_registry.dart | 8 ++++---- lib/src/services/auth_service.dart | 4 ++-- routes/_middleware.dart | 2 +- 6 files changed, 16 insertions(+), 16 deletions(-) diff --git a/lib/src/config/app_dependencies.dart b/lib/src/config/app_dependencies.dart index e1e5bda..da51e5e 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,7 +190,7 @@ class AppDependencies { toJson: (item) => item.toJson(), logger: Logger('DataMongodb'), ); - final userAppSettingsClient = DataMongodb( + final appSettingsClient = DataMongodb( connectionManager: _mongoDbConnectionManager, modelName: 'app_settings', fromJson: AppSettings.fromJson, @@ -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, - appSettingsRepository: userAppSettingsRepository, + appSettingsRepository: appSettingsRepository, userContentPreferencesRepository: userContentPreferencesRepository, log: Logger('AuthService'), ); diff --git a/lib/src/rbac/permissions.dart b/lib/src/rbac/permissions.dart index 9e6b40f..58761c6 100644 --- a/lib/src/rbac/permissions.dart +++ b/lib/src/rbac/permissions.dart @@ -50,8 +50,8 @@ abstract class Permissions { static const String userUpdate = 'user.update'; // User App Settings Permissions (User-owned) - static const String userAppSettingsReadOwned = 'app_settings.read_owned'; - static const String userAppSettingsUpdateOwned = '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 eb3d581..96b77a7 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/model_registry.dart b/lib/src/registry/model_registry.dart index e9575db..bf07f04 100644 --- a/lib/src/registry/model_registry.dart +++ b/lib/src/registry/model_registry.dart @@ -315,26 +315,26 @@ final modelRegistry = >{ ), 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 adbd8ca..fe70e34 100644 --- a/lib/src/services/auth_service.dart +++ b/lib/src/services/auth_service.dart @@ -451,7 +451,7 @@ class AuthService { // with CASCADE constraints, MongoDB requires manual deletion of related // documents in different collections. await _appSettingsRepository.delete(id: userId, userId: userId); - _log.info('Deleted UserAppSettings for user ${userToDelete.id}.'); + _log.info('Deleted AppSettings for user ${userToDelete.id}.'); await _userContentPreferencesRepository.delete( id: userId, @@ -513,7 +513,7 @@ 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 _appSettingsRepository.read(id: user.id, userId: user.id); } on NotFoundException { diff --git a/routes/_middleware.dart b/routes/_middleware.dart index f13dcc0..21607b8 100644 --- a/routes/_middleware.dart +++ b/routes/_middleware.dart @@ -116,7 +116,7 @@ Handler middleware(Handler handler) { ) // .use( provider>( - (_) => deps.userAppSettingsRepository, + (_) => deps.appSettingsRepository, ), ) .use( From 25466da4d360e0996bde6bbde4789d952262d58e Mon Sep 17 00:00:00 2001 From: fulleni Date: Tue, 25 Nov 2025 07:40:51 +0100 Subject: [PATCH 26/27] fix(firebase_messaging): make notification body optional - Updated FirebaseNotification model to make the body field optional - Fixed a bug in FirebasePushNotificationClient where the notification body was incorrectly set to the payload title --- lib/src/models/push_notification/firebase_request_body.dart | 4 ++-- lib/src/services/firebase_push_notification_client.dart | 1 - 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/lib/src/models/push_notification/firebase_request_body.dart b/lib/src/models/push_notification/firebase_request_body.dart index cee0256..9067141 100644 --- a/lib/src/models/push_notification/firebase_request_body.dart +++ b/lib/src/models/push_notification/firebase_request_body.dart @@ -74,7 +74,7 @@ class FirebaseNotification extends Equatable { /// {@macro firebase_notification} const FirebaseNotification({ required this.title, - required this.body, + this.body, this.image, }); @@ -82,7 +82,7 @@ class FirebaseNotification extends Equatable { final String title; /// The notification's body text. - final String body; + final String? body; /// The URL of an image to be displayed in the notification. final String? image; diff --git a/lib/src/services/firebase_push_notification_client.dart b/lib/src/services/firebase_push_notification_client.dart index e3ff0d5..bba0238 100644 --- a/lib/src/services/firebase_push_notification_client.dart +++ b/lib/src/services/firebase_push_notification_client.dart @@ -116,7 +116,6 @@ class FirebasePushNotificationClient implements IPushNotificationClient { token: token, notification: FirebaseNotification( title: payload.title, - body: payload.title, image: payload.imageUrl, ), data: payload, From 0c96edc628fb86ed86e0f809d76248422f1e8414 Mon Sep 17 00:00:00 2001 From: fulleni Date: Tue, 25 Nov 2025 07:41:09 +0100 Subject: [PATCH 27/27] fix(onesignal): make notification contents optional - Remove requirement for contents in OneSignalRequestBody - Update OneSignalPushNotificationClient to use payload.message instead of payload.title for contents --- lib/src/models/push_notification/onesignal_request_body.dart | 4 ++-- .../models/push_notification/onesignal_request_body.g.dart | 2 +- lib/src/services/onesignal_push_notification_client.dart | 1 - 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/lib/src/models/push_notification/onesignal_request_body.dart b/lib/src/models/push_notification/onesignal_request_body.dart index ee65906..3ee2228 100644 --- a/lib/src/models/push_notification/onesignal_request_body.dart +++ b/lib/src/models/push_notification/onesignal_request_body.dart @@ -20,8 +20,8 @@ class OneSignalRequestBody extends Equatable { required this.appId, required this.includePlayerIds, required this.headings, - required this.contents, required this.data, + this.contents, this.bigPicture, }); @@ -35,7 +35,7 @@ class OneSignalRequestBody extends Equatable { final Map headings; /// The notification's content. - final Map contents; + final Map? contents; /// The custom data payload final PushNotificationPayload data; diff --git a/lib/src/models/push_notification/onesignal_request_body.g.dart b/lib/src/models/push_notification/onesignal_request_body.g.dart index c562e1e..aa0b00a 100644 --- a/lib/src/models/push_notification/onesignal_request_body.g.dart +++ b/lib/src/models/push_notification/onesignal_request_body.g.dart @@ -14,7 +14,7 @@ Map _$OneSignalRequestBodyToJson( 'app_id': instance.appId, 'include_player_ids': instance.includePlayerIds, 'headings': instance.headings, - 'contents': instance.contents, + 'contents': ?instance.contents, 'data': instance.data.toJson(), 'big_picture': ?instance.bigPicture, 'props': instance.props, diff --git a/lib/src/services/onesignal_push_notification_client.dart b/lib/src/services/onesignal_push_notification_client.dart index 1fa043c..1a49350 100644 --- a/lib/src/services/onesignal_push_notification_client.dart +++ b/lib/src/services/onesignal_push_notification_client.dart @@ -103,7 +103,6 @@ class OneSignalPushNotificationClient implements IPushNotificationClient { appId: appId, includePlayerIds: deviceTokens, headings: {'en': payload.title}, - contents: {'en': payload.title}, bigPicture: payload.imageUrl, data: payload, );