Skip to content

Commit dc4cdc4

Browse files
authored
fix(mobile): reset community notification preferences (#8455)
Community notification choices now use `pushNotificationsEnabledV2`, defaulting ON when absent. This gives every installed community a fresh enabled preference, including communities previously disabled. Subsequent explicit OFF choices persist per community through reloads and reconnects. Previous values are deliberately discarded, not migrated or preserved. There is no migration or compatibility read. Enabled communities enter the existing APNs enrollment and active relay lease publication path when connected to a push-capable relay. iOS still controls notification display authorization. Notification eligibility and lease addressing are unchanged. Related work: [#8425](#8425). ### Validation - Extended the settings integration test to load a saved community with the previous preference OFF and no new preference and verify native enrollment plus decrypted active lease publication, then OFF across reconnect and ON again. - The focused model test proves both previous values are ignored and new-key OFF wins even when the previous key says ON. A temporary compatibility-fallback mutation failed this test as expected; the implementation was restored unchanged. - Added storage coverage for independent community choices across reloads; updated existing model round-trip coverage for the new key. - Mobile dependency setup, formatting, analysis, and the full mobile suite passed, including gateway-unconfigured coverage. - Agent review found no blockers. Enrollment coverage uses the real Flutter bootstrap, storage, signing, and lease construction with mocked native and relay boundaries; no physical-device or live APNs delivery test was run. The fresh enabled preference is an intentional product exception to the general zero-notification default in VISION.md. Subsequent user choices and existing eligibility filters remain authoritative. <!-- buzz-review-completed --> --------- Signed-off-by: Tom Brow <[email protected]>
1 parent 5730c83 commit dc4cdc4

4 files changed

Lines changed: 73 additions & 8 deletions

File tree

‎mobile/lib/shared/community/community.dart‎

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@ enum SensitiveActionPolicy { enabled, disabledByUser }
2121
class Community {
2222
static const _defaultPushNotificationsEnabled = true;
2323

24+
static const _pushNotificationsEnabledKey = 'pushNotificationsEnabledV2';
25+
2426
final String id;
2527
final String name;
2628
final String relayUrl;
@@ -115,7 +117,7 @@ class Community {
115117
if (pubkey != null) 'pubkey': pubkey,
116118
if (nsec != null) 'nsec': nsec,
117119
'sensitiveActionPolicy': sensitiveActionPolicy.name,
118-
'pushNotificationsEnabled': pushNotificationsEnabled,
120+
_pushNotificationsEnabledKey: pushNotificationsEnabled,
119121
'pushSubscriptionState': pushSubscriptionState.toJson(),
120122
if (pushLeaseInstallationId != null)
121123
'pushLeaseInstallationId': pushLeaseInstallationId,
@@ -132,7 +134,7 @@ class Community {
132134
);
133135
}
134136
final pushNotificationsEnabled =
135-
json['pushNotificationsEnabled'] as bool? ??
137+
json[_pushNotificationsEnabledKey] as bool? ??
136138
_defaultPushNotificationsEnabled;
137139
var pushSubscriptionState = json['pushSubscriptionState'] == null
138140
? const BuzzPushLeaseSubscriptionState.desired()

‎mobile/test/features/settings/notification_toggle_test.dart‎

Lines changed: 27 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,13 @@ void main() {
3737
..addFont(rootBundle.load('assets/fonts/InterVariable.ttf'));
3838
await fonts.load();
3939
});
40-
for (final outcome in ['accepted', 'failed', 'pending', 'scaled']) {
40+
for (final outcome in [
41+
'accepted',
42+
'failed',
43+
'pending',
44+
'scaled',
45+
'unset preference',
46+
]) {
4147
testWidgets('off then on remains usable with $outcome cleanup', (
4248
tester,
4349
) async {
@@ -61,7 +67,8 @@ void main() {
6167
);
6268
SharedPreferences.setMockInitialValues({});
6369
final prefs = await SharedPreferences.getInstance();
64-
final storage = CommunityStorage(secure: FakeSecureStorage());
70+
final secure = FakeSecureStorage();
71+
final storage = CommunityStorage(secure: secure);
6572
final subscriptions = [
6673
BuzzPushSubscription(
6774
filter: BuzzPushFilter(kinds: const [9], pTags: ['a' * 64]),
@@ -110,7 +117,13 @@ void main() {
110117
desired: subscriptions,
111118
).withAccepted(subscriptions: subscriptions, generation: 7),
112119
);
113-
await storage.save(community);
120+
if (outcome == 'unset preference') {
121+
final saved = community.toJson()..remove('pushNotificationsEnabledV2');
122+
saved['pushNotificationsEnabled'] = false;
123+
secure['buzz_communities'] = jsonEncode([saved]);
124+
} else {
125+
await storage.save(community);
126+
}
114127
await storage.saveActiveId(community.id);
115128

116129
final cleanup = Completer<void>();
@@ -236,6 +249,15 @@ void main() {
236249
reason: 'failed cleanup must not defeat the retry backoff',
237250
);
238251
expect(discoveries, 2);
252+
session.setStatus(SessionStatus.disconnected);
253+
await tester.pumpAndSettle();
254+
session.setStatus(SessionStatus.connected);
255+
await tester.pumpAndSettle();
256+
expect(
257+
session.leases.where((lease) => lease['active'] == true),
258+
hasLength(1),
259+
);
260+
expect(discoveries, 3);
239261
expect(snapshots.last.single.pushNotificationsEnabled, isFalse);
240262
final offSwitch = tester.widget<Switch>(find.byType(Switch));
241263
expect(offSwitch.value, isFalse);
@@ -273,7 +295,7 @@ void main() {
273295
isTrue,
274296
reason: 'a delayed old tombstone cannot supersede the new active lease',
275297
);
276-
expect(discoveries, 3);
298+
expect(discoveries, 4);
277299
// A real reconnect must still discover capability again.
278300
(container.read(relaySessionProvider.notifier) as _Connected).setStatus(
279301
SessionStatus.disconnected,
@@ -284,7 +306,7 @@ void main() {
284306
SessionStatus.connected,
285307
);
286308
await tester.pumpAndSettle();
287-
expect(discoveries, 4);
309+
expect(discoveries, 5);
288310
expect(tester.widget<Switch>(find.byType(Switch)).onChanged, isNotNull);
289311
expect(tester.takeException(), isNull);
290312
await tester.pumpWidget(const SizedBox.shrink());

‎mobile/test/shared/community/community_storage_test.dart‎

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,33 @@ void main() {
127127
);
128128
});
129129

130+
test('push choices persist independently across storage reloads', () async {
131+
final first = Community.create(
132+
name: 'First',
133+
relayUrl: 'https://first.example',
134+
pubkey: 'a' * 64,
135+
);
136+
final second = Community.create(
137+
name: 'Second',
138+
relayUrl: 'https://second.example',
139+
pubkey: 'b' * 64,
140+
);
141+
await storage.saveAll([first, second]);
142+
await storage.save(first.copyWith(pushNotificationsEnabled: false));
143+
144+
final reloaded = CommunityStorage(secure: fakeSecure);
145+
final communities = await reloaded.loadAll();
146+
expect(communities.map((c) => c.pushNotificationsEnabled), [false, true]);
147+
expect(communities.map((c) => c.pubkey), [first.pubkey, second.pubkey]);
148+
await reloaded.save(communities.last.copyWith(name: 'Renamed'));
149+
expect(
150+
(await CommunityStorage(
151+
secure: fakeSecure,
152+
).loadAll()).map((c) => c.pushNotificationsEnabled),
153+
[false, true],
154+
);
155+
});
156+
130157
test('round-trips desired push subscription state', () async {
131158
final pubkey = 'a' * 64;
132159
final subscriptions = buildDesiredBuzzPushSubscriptions(

‎mobile/test/shared/community/community_test.dart‎

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,8 @@ void main() {
5959
'id': 'one',
6060
'name': 'Buzz',
6161
'relayUrl': 'https://relay.test',
62-
'pushNotificationsEnabled': enabled,
62+
'pushNotificationsEnabledV2': enabled,
63+
'pushNotificationsEnabled': !enabled,
6364
'addedAt': '2026-08-05T00:00:00.000Z',
6465
});
6566
expect(community.pushNotificationsEnabled, enabled);
@@ -78,6 +79,19 @@ void main() {
7879
});
7980
}
8081

82+
test('previous preference values are intentionally not preserved', () {
83+
for (final previous in [false, true]) {
84+
final saved = Community.create(
85+
name: 'Team',
86+
relayUrl: 'https://relay.test',
87+
).toJson()..remove('pushNotificationsEnabledV2');
88+
saved['pushNotificationsEnabled'] = previous;
89+
final community = Community.fromJson(saved);
90+
expect(community.pushNotificationsEnabled, isTrue);
91+
expect(community.toJson(), isNot(contains('pushNotificationsEnabled')));
92+
}
93+
});
94+
8195
test('malformed stored push lease address id is rejected', () {
8296
expect(
8397
() => Community.fromJson({

0 commit comments

Comments
 (0)