From e9269cbdf66b0e2fdb588aa20aad65bcba3ca622 Mon Sep 17 00:00:00 2001 From: Tom Brow Date: Thu, 8 Oct 2026 17:47:42 -0700 Subject: [PATCH] fix(mobile): simplify notification settings (#8199) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Keep notification settings as a separate card with a single “Notifications” toggle. Remove the section heading, bell, all subtitles (including permission/capability and pending opt-out messages), and the iOS settings button. Toggle callbacks, capability visibility, persisted opt-in/out, and relay synchronization are unchanged. The disable failure is being investigated separately. This intentionally removes the pending warning rather than claiming to fix synchronization. Tom explicitly requested removal of the immediate opt-out synchronization warning and all other status text; this is an intentional exception to the pending-feedback guidance in VISION_MOBILE.md. [Codex’s finding is answered against that agreed scope](https://github.com/block/buzz/pull/8199#discussion_r4225426842). No new tests. Delete tests for the removed permission/settings UI and remove obsolete display assertions from existing recovery tests while preserving their opt-out synchronization coverage. Closest related issue: [#7826](https://github.com/block/buzz/issues/7826). No duplicate cleanup PR found. ### Validation Mobile formatting/static analysis passed. The full existing mobile suite passed (3,216 tests; 4 skipped), plus the three unconfigured-build tests and gateway recipe checks. Existing opt-out recovery tests exercised the switch, persisted tombstone, snapshot synchronization, and successful relay retry. Before/after renders use the existing Flutter widget fixture with synthetic data and mocked native navigation. No simulator or TestFlight run was performed. After merge, validate the settings row and on/off flow in the next iOS candidate; remote disable failure remains a separate investigation. GitHub CI also passed the mobile tests, unconfigured-build tests, and gateway recipe checks. The [security review](https://github.com/block/buzz/actions/runs/37863885299) completed successfully. The [Android build](https://github.com/block/buzz/actions/runs/37863554077/job/113605025305) passed. The iOS simulator build, native accessibility checks, and unsigned iOS release build passed. [All required CI checks](https://github.com/block/buzz/actions/runs/37863554077) are green. ### Before and after Headless Flutter widget fixture, with synthetic data and mocked native navigation. | Before | After | | --- | --- | | ![Notification settings before](https://raw.githubusercontent.com/block/buzz/8334767435a2896656df7bcc497dcb64a1846d92/pr-8199--before.png) | ![Notification settings after](https://raw.githubusercontent.com/block/buzz/8334767435a2896656df7bcc497dcb64a1846d92/pr-8199--after.png) | Signed-off-by: Tom Brow --- .../lib/features/settings/settings_page.dart | 1 - .../settings_page/notifications_section.dart | 46 +------- .../settings/notification_recovery_test.dart | 27 ----- .../features/settings/settings_page_test.dart | 103 ------------------ 4 files changed, 1 insertion(+), 176 deletions(-) diff --git a/mobile/lib/features/settings/settings_page.dart b/mobile/lib/features/settings/settings_page.dart index 174310a27d..4d9199be74 100644 --- a/mobile/lib/features/settings/settings_page.dart +++ b/mobile/lib/features/settings/settings_page.dart @@ -12,7 +12,6 @@ import 'package:package_info_plus/package_info_plus.dart'; import '../../shared/auth/auth.dart'; import '../../shared/clipboard_utils.dart'; import '../../shared/success_haptic.dart'; -import '../../shared/push/push_bridge.dart'; import '../../shared/push/push_relay_capability_provider.dart'; import '../../shared/relay/relay.dart'; import '../../shared/utils/string_utils.dart'; diff --git a/mobile/lib/features/settings/settings_page/notifications_section.dart b/mobile/lib/features/settings/settings_page/notifications_section.dart index 8383b90403..34b46a4cd0 100644 --- a/mobile/lib/features/settings/settings_page/notifications_section.dart +++ b/mobile/lib/features/settings/settings_page/notifications_section.dart @@ -22,49 +22,14 @@ class _NotificationsSection extends ConsumerWidget { !optOutPending) { return const SizedBox.shrink(); } - final authorization = ref.watch(buzzPushAuthorizationStatusProvider); - final status = authorization.value; - final permissionUnavailable = authorization.hasError; - final permissionDenied = status == BuzzPushAuthorizationStatus.denied; - final showSettingsRecovery = - optOutPending || - (community.pushNotificationsEnabled && - (permissionDenied || permissionUnavailable)); final canToggle = hasCapability || community.pushNotificationsEnabled; - final subtitle = optOutPending - ? 'Waiting for relay confirmation; notifications may continue' - : !community.pushNotificationsEnabled - ? 'Off for this community' - : !hasCapability - ? 'Push support unavailable; you can still turn notifications off' - : switch (status) { - BuzzPushAuthorizationStatus.notDetermined => - 'Waiting for iOS notification permission', - BuzzPushAuthorizationStatus.denied => - 'Enabled in Buzz, but disabled in iOS Settings', - BuzzPushAuthorizationStatus.authorized || - BuzzPushAuthorizationStatus.provisional || - BuzzPushAuthorizationStatus.ephemeral => - 'Receive message notifications from this community', - null when authorization.isLoading => - 'Checking iOS notification permission', - null => 'Enabled in Buzz; iOS permission status unavailable', - }; return AppListCard( - label: 'Notifications', verticalPadding: Grid.twelve, children: [ AppListRow( key: const ValueKey('push-notifications-enabled'), - icon: BuzzIcons.bell, - title: 'Push notifications', - subtitle: subtitle, - subtitleStyle: showSettingsRecovery - ? context.textTheme.bodySmall?.copyWith( - color: context.colors.error, - ) - : null, + title: 'Notifications', trailing: Switch.adaptive( value: community.pushNotificationsEnabled, onChanged: !canToggle @@ -86,15 +51,6 @@ class _NotificationsSection extends ConsumerWidget { ), ), ), - if (showSettingsRecovery) - AppListRow( - key: const ValueKey('push-notifications-open-settings'), - icon: BuzzIcons.settings, - title: 'Open iOS Notification Settings', - onTap: () => unawaited( - ref.read(buzzPushNotificationSettingsOpenerProvider)(), - ), - ), ], ); } diff --git a/mobile/test/features/settings/notification_recovery_test.dart b/mobile/test/features/settings/notification_recovery_test.dart index 7f95680686..25cd70ecc1 100644 --- a/mobile/test/features/settings/notification_recovery_test.dart +++ b/mobile/test/features/settings/notification_recovery_test.dart @@ -61,7 +61,6 @@ void main() { final pending = Completer(); final snapshots = >[]; final tombstones = []; - var settingsOpened = 0; var relayAvailable = false; await tester.pumpWidget( ProviderScope( @@ -90,12 +89,6 @@ void main() { buzzPushAuthorizationStatusReaderProvider.overrideWithValue( () async => BuzzPushAuthorizationStatus.denied, ), - buzzPushNotificationSettingsOpenerProvider.overrideWithValue( - () async { - settingsOpened++; - return true; - }, - ), ], child: MaterialApp( theme: AppTheme.light(), @@ -117,17 +110,6 @@ void main() { '${container.read(activeCommunityProvider)} / ${container.read(communityListProvider)}', ); expect(tester.widget(find.byType(Switch)).value, isTrue); - expect( - find.text( - 'Push support unavailable; you can still turn notifications off', - ), - findsOneWidget, - ); - await tester.tap( - find.byKey(const ValueKey('push-notifications-open-settings')), - ); - await tester.pump(); - expect(settingsOpened, 1); await tester.tap(find.byType(Switch)); await tester.pumpAndSettle(); final stored = (await storage.loadAll()).single; @@ -135,18 +117,9 @@ void main() { expect(stored.pushSubscriptionState.pendingTombstoneGeneration, 8); expect(tombstones, [8]); expect(snapshots.last.single.pushNotificationsEnabled, isFalse); - expect( - find.text('Waiting for relay confirmation; notifications may continue'), - findsOneWidget, - ); final offSwitch = tester.widget(find.byType(Switch)); expect(offSwitch.value, isFalse); expect(offSwitch.onChanged, isNull); - await tester.tap( - find.byKey(const ValueKey('push-notifications-open-settings')), - ); - await tester.pump(); - expect(settingsOpened, 2); relayAvailable = true; await container .read(communityListProvider.notifier) diff --git a/mobile/test/features/settings/settings_page_test.dart b/mobile/test/features/settings/settings_page_test.dart index d587159e3d..5cc2f78b38 100644 --- a/mobile/test/features/settings/settings_page_test.dart +++ b/mobile/test/features/settings/settings_page_test.dart @@ -194,109 +194,6 @@ void main() { debugDefaultTargetPlatformOverride = null; }); - testWidgets('shows denied display permission and opens iOS settings', ( - tester, - ) async { - debugDefaultTargetPlatformOverride = TargetPlatform.iOS; - addTearDown(() => debugDefaultTargetPlatformOverride = null); - SharedPreferences.setMockInitialValues({}); - final prefs = await SharedPreferences.getInstance(); - final community = Community.create( - name: 'Team', - relayUrl: 'wss://relay.example', - ).copyWith(pushNotificationsEnabled: true); - var openSettingsCalls = 0; - - await tester.pumpWidget( - ProviderScope( - overrides: [ - savedPrefsProvider.overrideWithValue(prefs), - activeCommunityProvider.overrideWith((ref) async => community), - currentRelayPushDescriptorProvider.overrideWith( - (ref) async => _pushDescriptor, - ), - appLifecycleProvider.overrideWith(_SettingsLifecycleNotifier.new), - buzzPushAuthorizationStatusReaderProvider.overrideWithValue( - () async => BuzzPushAuthorizationStatus.denied, - ), - buzzPushNotificationSettingsOpenerProvider.overrideWithValue( - () async { - openSettingsCalls += 1; - return true; - }, - ), - ], - child: MaterialApp( - theme: AppTheme.light(), - home: SettingsPage( - profileHeader: const SizedBox.shrink(), - identityRecoveryPageBuilder: (_) => const SizedBox.shrink(), - ), - ), - ), - ); - await tester.pumpAndSettle(); - - expect(tester.widget(find.byType(Switch)).value, isTrue); - expect( - find.text('Enabled in Buzz, but disabled in iOS Settings'), - findsOneWidget, - ); - await tester.tap( - find.byKey(const ValueKey('push-notifications-open-settings')), - ); - await tester.pump(); - expect(openSettingsCalls, 1); - debugDefaultTargetPlatformOverride = null; - }); - - testWidgets('shows permission lookup errors with settings recovery', ( - tester, - ) async { - debugDefaultTargetPlatformOverride = TargetPlatform.iOS; - addTearDown(() => debugDefaultTargetPlatformOverride = null); - SharedPreferences.setMockInitialValues({}); - final prefs = await SharedPreferences.getInstance(); - final community = Community.create( - name: 'Team', - relayUrl: 'wss://relay.example', - ).copyWith(pushNotificationsEnabled: true); - - await tester.pumpWidget( - ProviderScope( - overrides: [ - savedPrefsProvider.overrideWithValue(prefs), - activeCommunityProvider.overrideWith((ref) async => community), - currentRelayPushDescriptorProvider.overrideWith( - (ref) async => _pushDescriptor, - ), - appLifecycleProvider.overrideWith(_SettingsLifecycleNotifier.new), - buzzPushAuthorizationStatusReaderProvider.overrideWithValue( - () async => throw StateError('authorization unavailable'), - ), - ], - child: MaterialApp( - theme: AppTheme.light(), - home: SettingsPage( - profileHeader: const SizedBox.shrink(), - identityRecoveryPageBuilder: (_) => const SizedBox.shrink(), - ), - ), - ), - ); - await tester.pumpAndSettle(); - - expect( - find.text('Enabled in Buzz; iOS permission status unavailable'), - findsOneWidget, - ); - expect( - find.byKey(const ValueKey('push-notifications-open-settings')), - findsOneWidget, - ); - debugDefaultTargetPlatformOverride = null; - }); - for (final brightness in Brightness.values) { testWidgets( 'shows status above profile editing and routes photo directly in ${brightness.name}',