Files
buzz/mobile/lib/shared/push/push_relay_capability_provider.dart
Tom Brow c09730e874 fix(mobile): keep notification toggle usable and match row height (#8223)
Turning Notifications off invalidated the relay capability provider
because it watched the entire saved community. If the redundant NIP-11
request stalled or failed, the switch became disabled and reopening
Settings did not recover it. Discovery now follows community identity
and connection status, so preference and lease bookkeeping retain the
validated capability while a real reconnect still refreshes it. Each
opt-in also starts a fresh active lease immediately instead of waiting
for the old renewal timer. Failed attempts retain their gate until the
retry delay expires, preventing rebuilds from bypassing backoff.

Notifications now has the same single-line height as Set status, with
the full switch hit area and one combined accessibility label. This
follows [#8201](https://github.com/block/buzz/pull/8201); no duplicate
open PR was found.

### Validation

Added iOS widget regressions mount the production settings page and push
bootstrap with production storage logic, signing, and lease publication.
They verify a higher-generation active replacement after off/on with
accepted, failed, and pending cleanup, plus closing/reopening Settings,
switch and row taps, 2× text, accessibility, and rediscovery on
reconnect. A gate test verifies that rebuilds cannot bypass failure
backoff. Both UI failures and the missing active replacement were
reproduced before their fixes.

Mobile dependency installation, formatting/static checks, 3,234
configured tests, and three unconfigured-build tests passed. Captures
use the real Flutter settings widget at 375pt width, with synthetic data
and mocked native/network boundaries. Native navigation is outside the
capture. No device installation or deployment was performed.

After merge, validate the candidate on iOS: compare Notifications with
Set status, toggle off then immediately on, close/reopen Settings while
off and turn on, and repeat after reconnect. Confirm delivery resumes
after opt-in.

### Layout

Flutter widget captures with synthetic data; native navigation is
excluded.

| Before | After |
| --- | --- |
| ![Notifications row
before](https://raw.githubusercontent.com/block/buzz/b2cf4b8b1f0abaa95893af0e5a4e194c6af41333/pr-8223--01-before.png)
| ![Notifications row
after](https://raw.githubusercontent.com/block/buzz/b2cf4b8b1f0abaa95893af0e5a4e194c6af41333/pr-8223--02-after.png)
|

---------

Signed-off-by: Tom Brow <tomb@block.xyz>
2026-10-12 09:39:53 -07:00

71 lines
2.5 KiB
Dart

import 'package:flutter/foundation.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import '../community/community_provider.dart';
import '../relay/relay_provider.dart';
import '../relay/relay_session.dart';
import 'dev_push_lease.dart';
typedef BuzzPushDescriptorFetcher =
Future<BuzzPushLeaseDescriptor> Function(String relayBaseUrl);
final buzzPushDescriptorFetcherProvider = Provider<BuzzPushDescriptorFetcher>(
(ref) => fetchBuzzPushLeaseDescriptor,
);
/// The fully validated push capability advertised by the current relay.
///
/// An unconfigured artifact has no capability and never starts discovery.
/// Discovery fails closed. An absent, malformed, or unreachable NIP-11 push
/// descriptor is represented as no capability, so no notification permission,
/// APNs registration, gateway enrollment, or relay lease can begin.
final currentRelayPushDescriptorProvider =
FutureProvider.autoDispose<BuzzPushLeaseDescriptor?>((ref) async {
if (!Env.pushGatewayConfigured) return null;
final sessionStatus = ref.watch(
relaySessionProvider.select((session) => session.status),
);
final config = ref.watch(relayConfigProvider);
// Preference and lease bookkeeping do not change relay capability.
// Watching the whole community rediscovered NIP-11 after every toggle,
// disabling the off switch when that redundant request could not finish.
final communityId = ref.watch(
activeCommunityProvider.select((value) => value.value?.id),
);
final memberPubkey = ref.watch(myPubkeyProvider);
if (sessionStatus != SessionStatus.connected ||
communityId == null ||
config.nsec == null ||
config.nsec!.isEmpty ||
memberPubkey == null ||
memberPubkey.isEmpty) {
return null;
}
return discoverBuzzPushRelayCapability(
config.baseUrl,
fetchDescriptor: ref.read(buzzPushDescriptorFetcherProvider),
);
});
Future<BuzzPushLeaseDescriptor?> discoverBuzzPushRelayCapability(
String relayBaseUrl, {
required BuzzPushDescriptorFetcher fetchDescriptor,
}) async {
try {
return await fetchDescriptor(relayBaseUrl);
} catch (error, stackTrace) {
debugPrint('Current relay does not advertise valid push: $error');
debugPrintStack(stackTrace: stackTrace);
return null;
}
}
Future<void> startBuzzPushRegistrationIfCapable(
BuzzPushLeaseDescriptor? descriptor, {
required Future<void> Function() startRegistration,
}) async {
if (descriptor == null) return;
await startRegistration();
}