diff --git a/mobile/lib/shared/community/community.dart b/mobile/lib/shared/community/community.dart index 426a34198b..96c2b5f7a2 100644 --- a/mobile/lib/shared/community/community.dart +++ b/mobile/lib/shared/community/community.dart @@ -21,6 +21,8 @@ enum SensitiveActionPolicy { enabled, disabledByUser } class Community { static const _defaultPushNotificationsEnabled = true; + static const _pushNotificationsEnabledKey = 'pushNotificationsEnabledV2'; + final String id; final String name; final String relayUrl; @@ -115,7 +117,7 @@ class Community { if (pubkey != null) 'pubkey': pubkey, if (nsec != null) 'nsec': nsec, 'sensitiveActionPolicy': sensitiveActionPolicy.name, - 'pushNotificationsEnabled': pushNotificationsEnabled, + _pushNotificationsEnabledKey: pushNotificationsEnabled, 'pushSubscriptionState': pushSubscriptionState.toJson(), if (pushLeaseInstallationId != null) 'pushLeaseInstallationId': pushLeaseInstallationId, @@ -132,7 +134,7 @@ class Community { ); } final pushNotificationsEnabled = - json['pushNotificationsEnabled'] as bool? ?? + json[_pushNotificationsEnabledKey] as bool? ?? _defaultPushNotificationsEnabled; var pushSubscriptionState = json['pushSubscriptionState'] == null ? const BuzzPushLeaseSubscriptionState.desired() diff --git a/mobile/test/features/settings/notification_toggle_test.dart b/mobile/test/features/settings/notification_toggle_test.dart index 19c80e2522..84f78e536f 100644 --- a/mobile/test/features/settings/notification_toggle_test.dart +++ b/mobile/test/features/settings/notification_toggle_test.dart @@ -37,7 +37,13 @@ void main() { ..addFont(rootBundle.load('assets/fonts/InterVariable.ttf')); await fonts.load(); }); - for (final outcome in ['accepted', 'failed', 'pending', 'scaled']) { + for (final outcome in [ + 'accepted', + 'failed', + 'pending', + 'scaled', + 'unset preference', + ]) { testWidgets('off then on remains usable with $outcome cleanup', ( tester, ) async { @@ -61,7 +67,8 @@ void main() { ); SharedPreferences.setMockInitialValues({}); final prefs = await SharedPreferences.getInstance(); - final storage = CommunityStorage(secure: FakeSecureStorage()); + final secure = FakeSecureStorage(); + final storage = CommunityStorage(secure: secure); final subscriptions = [ BuzzPushSubscription( filter: BuzzPushFilter(kinds: const [9], pTags: ['a' * 64]), @@ -110,7 +117,13 @@ void main() { desired: subscriptions, ).withAccepted(subscriptions: subscriptions, generation: 7), ); - await storage.save(community); + if (outcome == 'unset preference') { + final saved = community.toJson()..remove('pushNotificationsEnabledV2'); + saved['pushNotificationsEnabled'] = false; + secure['buzz_communities'] = jsonEncode([saved]); + } else { + await storage.save(community); + } await storage.saveActiveId(community.id); final cleanup = Completer(); @@ -236,6 +249,15 @@ void main() { reason: 'failed cleanup must not defeat the retry backoff', ); expect(discoveries, 2); + session.setStatus(SessionStatus.disconnected); + await tester.pumpAndSettle(); + session.setStatus(SessionStatus.connected); + await tester.pumpAndSettle(); + expect( + session.leases.where((lease) => lease['active'] == true), + hasLength(1), + ); + expect(discoveries, 3); expect(snapshots.last.single.pushNotificationsEnabled, isFalse); final offSwitch = tester.widget(find.byType(Switch)); expect(offSwitch.value, isFalse); @@ -273,7 +295,7 @@ void main() { isTrue, reason: 'a delayed old tombstone cannot supersede the new active lease', ); - expect(discoveries, 3); + expect(discoveries, 4); // A real reconnect must still discover capability again. (container.read(relaySessionProvider.notifier) as _Connected).setStatus( SessionStatus.disconnected, @@ -284,7 +306,7 @@ void main() { SessionStatus.connected, ); await tester.pumpAndSettle(); - expect(discoveries, 4); + expect(discoveries, 5); expect(tester.widget(find.byType(Switch)).onChanged, isNotNull); expect(tester.takeException(), isNull); await tester.pumpWidget(const SizedBox.shrink()); diff --git a/mobile/test/shared/community/community_storage_test.dart b/mobile/test/shared/community/community_storage_test.dart index 93874aef25..be9c404dbf 100644 --- a/mobile/test/shared/community/community_storage_test.dart +++ b/mobile/test/shared/community/community_storage_test.dart @@ -127,6 +127,33 @@ void main() { ); }); + test('push choices persist independently across storage reloads', () async { + final first = Community.create( + name: 'First', + relayUrl: 'https://first.example', + pubkey: 'a' * 64, + ); + final second = Community.create( + name: 'Second', + relayUrl: 'https://second.example', + pubkey: 'b' * 64, + ); + await storage.saveAll([first, second]); + await storage.save(first.copyWith(pushNotificationsEnabled: false)); + + final reloaded = CommunityStorage(secure: fakeSecure); + final communities = await reloaded.loadAll(); + expect(communities.map((c) => c.pushNotificationsEnabled), [false, true]); + expect(communities.map((c) => c.pubkey), [first.pubkey, second.pubkey]); + await reloaded.save(communities.last.copyWith(name: 'Renamed')); + expect( + (await CommunityStorage( + secure: fakeSecure, + ).loadAll()).map((c) => c.pushNotificationsEnabled), + [false, true], + ); + }); + test('round-trips desired push subscription state', () async { final pubkey = 'a' * 64; final subscriptions = buildDesiredBuzzPushSubscriptions( diff --git a/mobile/test/shared/community/community_test.dart b/mobile/test/shared/community/community_test.dart index a5304f643e..93c9881849 100644 --- a/mobile/test/shared/community/community_test.dart +++ b/mobile/test/shared/community/community_test.dart @@ -59,7 +59,8 @@ void main() { 'id': 'one', 'name': 'Buzz', 'relayUrl': 'https://relay.test', - 'pushNotificationsEnabled': enabled, + 'pushNotificationsEnabledV2': enabled, + 'pushNotificationsEnabled': !enabled, 'addedAt': '2026-08-05T00:00:00.000Z', }); expect(community.pushNotificationsEnabled, enabled); @@ -78,6 +79,19 @@ void main() { }); } + test('previous preference values are intentionally not preserved', () { + for (final previous in [false, true]) { + final saved = Community.create( + name: 'Team', + relayUrl: 'https://relay.test', + ).toJson()..remove('pushNotificationsEnabledV2'); + saved['pushNotificationsEnabled'] = previous; + final community = Community.fromJson(saved); + expect(community.pushNotificationsEnabled, isTrue); + expect(community.toJson(), isNot(contains('pushNotificationsEnabled'))); + } + }); + test('malformed stored push lease address id is rejected', () { expect( () => Community.fromJson({