diff --git a/mobile/lib/shared/community/community.dart b/mobile/lib/shared/community/community.dart index 86db62bb62..426a34198b 100644 --- a/mobile/lib/shared/community/community.dart +++ b/mobile/lib/shared/community/community.dart @@ -19,6 +19,8 @@ String _newPushLeaseInstallationId() { enum SensitiveActionPolicy { enabled, disabledByUser } class Community { + static const _defaultPushNotificationsEnabled = true; + final String id; final String name; final String relayUrl; @@ -45,7 +47,7 @@ class Community { this.pubkey, this.nsec, this.sensitiveActionPolicy = SensitiveActionPolicy.disabledByUser, - this.pushNotificationsEnabled = false, + this.pushNotificationsEnabled = _defaultPushNotificationsEnabled, this.pushSubscriptionState = const BuzzPushLeaseSubscriptionState.desired(), this.pushLeaseInstallationId, this.starterSetupIncomplete = false, @@ -130,7 +132,8 @@ class Community { ); } final pushNotificationsEnabled = - json['pushNotificationsEnabled'] as bool? ?? false; + json['pushNotificationsEnabled'] as bool? ?? + _defaultPushNotificationsEnabled; var pushSubscriptionState = json['pushSubscriptionState'] == null ? const BuzzPushLeaseSubscriptionState.desired() : BuzzPushLeaseSubscriptionState.fromJson( diff --git a/mobile/test/shared/community/community_provider_test.dart b/mobile/test/shared/community/community_provider_test.dart index fa977786f2..d79d1620f6 100644 --- a/mobile/test/shared/community/community_provider_test.dart +++ b/mobile/test/shared/community/community_provider_test.dart @@ -122,7 +122,7 @@ void main() { }); test( - 'push notifications default off and opt-in survives restart', + 'push notifications default on and opt-out survives restart', () async { container = createContainer(); await container.read(communityListProvider.future); @@ -137,12 +137,12 @@ void main() { (await container.read( communityListProvider.future, )).single.pushNotificationsEnabled, - isFalse, + isTrue, ); await container .read(communityListProvider.notifier) - .setPushNotificationsEnabled(community.id, true); + .setPushNotificationsEnabled(community.id, false); container.dispose(); container = createContainer(); @@ -150,7 +150,7 @@ void main() { (await container.read( communityListProvider.future, )).single.pushNotificationsEnabled, - isTrue, + isFalse, ); }, ); diff --git a/mobile/test/shared/community/community_test.dart b/mobile/test/shared/community/community_test.dart index 0d229d2366..a5304f643e 100644 --- a/mobile/test/shared/community/community_test.dart +++ b/mobile/test/shared/community/community_test.dart @@ -16,6 +16,8 @@ void main() { ); expect(community.starterSetupIncomplete, isFalse); expect(community.pushLeaseInstallationId, isNull); + expect(community.pushNotificationsEnabled, isTrue); + expect(community.pushSubscriptionState.pendingTombstoneGeneration, isNull); }); test('new community gets a unique canonical push lease address id', () { @@ -25,6 +27,8 @@ void main() { relayUrl: 'https://relay.test', ); + expect(first.pushNotificationsEnabled, isTrue); + expect(Community.fromJson(first.toJson()).pushNotificationsEnabled, isTrue); expect(first.pushLeaseInstallationId, matches(RegExp(r'^[0-9a-f]{32}$'))); expect( second.pushLeaseInstallationId, @@ -49,6 +53,31 @@ void main() { expect(roundTrip.pushLeaseInstallationId, 'a' * 32); }); + for (final enabled in [true, false]) { + test('saved push preference $enabled survives loading and copying', () { + final community = Community.fromJson({ + 'id': 'one', + 'name': 'Buzz', + 'relayUrl': 'https://relay.test', + 'pushNotificationsEnabled': enabled, + 'addedAt': '2026-08-05T00:00:00.000Z', + }); + expect(community.pushNotificationsEnabled, enabled); + expect( + community.copyWith(name: 'Renamed').pushNotificationsEnabled, + enabled, + ); + expect( + Community.fromJson(community.toJson()).pushNotificationsEnabled, + enabled, + ); + expect( + community.pushSubscriptionState.pendingTombstoneGeneration, + isNull, + ); + }); + } + test('malformed stored push lease address id is rejected', () { expect( () => Community.fromJson({ diff --git a/mobile/test/shared/push/push_bootstrap_test.dart b/mobile/test/shared/push/push_bootstrap_test.dart index 6770ad546e..2fd37bfbbe 100644 --- a/mobile/test/shared/push/push_bootstrap_test.dart +++ b/mobile/test/shared/push/push_bootstrap_test.dart @@ -26,6 +26,33 @@ void main() { expect(registrations, 1); }); + testWidgets('new community starts registration once capability arrives', ( + tester, + ) async { + final community = Community.create( + name: 'Team', + relayUrl: 'wss://relay.example', + ); + var registrations = 0; + Widget bootstrap(BuzzPushLeaseDescriptor? descriptor) => + BuzzPushRegistrationBootstrap( + shouldRegister: buzzPushLifecycleEnabled( + community: community, + descriptor: descriptor, + ), + attemptKey: '${community.id}|${community.relayUrl}', + startRegistration: () async => registrations += 1, + child: const SizedBox(), + ); + await tester.pumpWidget(bootstrap(null)); + expect(registrations, 0); + final descriptor = _descriptor(keyId: 'relay-v1', pubkey: _hex('b')); + await tester.pumpWidget(bootstrap(descriptor)); + expect(registrations, 1); + await tester.pumpWidget(bootstrap(descriptor)); + expect(registrations, 1); + }); + test( 'superseded lease acceptance is a retryable publication failure', () async { @@ -141,12 +168,12 @@ void main() { ); }); - test('relay capability alone does not activate push without opt-in', () { - final disabled = Community.create( + test('default push requires relay capability and respects opt-out', () { + final enabled = Community.create( name: 'Team', relayUrl: 'wss://relay.example', ); - final enabled = disabled.copyWith(pushNotificationsEnabled: true); + final disabled = enabled.copyWith(pushNotificationsEnabled: false); expect( buzzPushLifecycleEnabled( diff --git a/mobile/test/shared/push/push_unconfigured_build_test.dart b/mobile/test/shared/push/push_unconfigured_build_test.dart index c5cf8161ad..3c54d3fcb4 100644 --- a/mobile/test/shared/push/push_unconfigured_build_test.dart +++ b/mobile/test/shared/push/push_unconfigured_build_test.dart @@ -73,43 +73,56 @@ void main() { expect(calls, isEmpty); }); - testWidgets('Settings hides notifications without push capability', ( - tester, - ) async { - debugDefaultTargetPlatformOverride = TargetPlatform.iOS; - addTearDown(() => debugDefaultTargetPlatformOverride = null); - SharedPreferences.setMockInitialValues({}); - final prefs = await SharedPreferences.getInstance(); - await tester.pumpWidget( - ProviderScope( - overrides: [ - savedPrefsProvider.overrideWithValue(prefs), - activeCommunityProvider.overrideWith( - (ref) async => Community.create( - name: 'Team', - relayUrl: 'wss://relay.example', + for (final enabled in [true, false]) { + testWidgets( + 'Settings preserves preference $enabled without push capability', + (tester) async { + debugDefaultTargetPlatformOverride = TargetPlatform.iOS; + addTearDown(() => debugDefaultTargetPlatformOverride = null); + SharedPreferences.setMockInitialValues({}); + final prefs = await SharedPreferences.getInstance(); + await tester.pumpWidget( + ProviderScope( + overrides: [ + savedPrefsProvider.overrideWithValue(prefs), + activeCommunityProvider.overrideWith( + (ref) async => Community.create( + name: 'Team', + relayUrl: 'wss://relay.example', + ).copyWith(pushNotificationsEnabled: enabled), + ), + buzzPushAuthorizationStatusReaderProvider.overrideWithValue( + () async => throw StateError( + 'must not query notification permission', + ), + ), + ], + child: MaterialApp( + theme: AppTheme.light(), + home: SettingsPage( + profileHeader: const SizedBox.shrink(), + identityRecoveryPageBuilder: (_) => const SizedBox.shrink(), + ), ), ), - buzzPushAuthorizationStatusReaderProvider.overrideWithValue( - () async => - throw StateError('must not query notification permission'), - ), - ], - child: MaterialApp( - theme: AppTheme.light(), - home: SettingsPage( - profileHeader: const SizedBox.shrink(), - identityRecoveryPageBuilder: (_) => const SizedBox.shrink(), - ), - ), - ), + ); + await tester.pumpAndSettle(); + expect( + find.text('Notifications'), + enabled ? findsOneWidget : findsNothing, + ); + expect(find.text('Push notifications'), findsNothing); + if (enabled) { + final toggle = tester.widget(find.byType(Switch)); + expect(toggle.value, isTrue); + expect(toggle.onChanged, isNotNull); + } else { + expect(find.byType(Switch), findsNothing); + } + expect(tester.takeException(), isNull); + debugDefaultTargetPlatformOverride = null; + }, ); - await tester.pumpAndSettle(); - expect(find.text('Notifications'), findsNothing); - expect(find.text('Push notifications'), findsNothing); - expect(find.byType(Switch), findsNothing); - expect(tester.takeException(), isNull); - debugDefaultTargetPlatformOverride = null; - }); + } }, skip: Env.pushGatewayConfigured); }