Files
buzz/mobile/lib/shared/push/push_bootstrap.dart
Tom Brow 3b3e536bd2 refactor(push): simplify internal notification stack
Signed-off-by: Tom Brow <tomb@block.xyz>
Co-authored-by: Codex <noreply@openai.com>
Ai-assisted: true
2026-08-24 20:25:40 -07:00

161 lines
5.0 KiB
Dart

import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import '../community/community.dart';
import '../community/community_provider.dart';
import '../relay/relay_provider.dart';
import '../relay/relay_session.dart';
import '../relay/signed_event_relay.dart';
import 'dev_push_lease.dart';
import 'push_bridge.dart';
import 'push_relay_capability_provider.dart';
import 'push_subscription.dart';
/// Starts the push lifecycle only after authenticated relay connectivity and a
/// push-capable NIP-11 descriptor are both present.
class BuzzPushBootstrap extends HookConsumerWidget {
const BuzzPushBootstrap({required this.child, super.key});
final Widget child;
@override
Widget build(BuildContext context, WidgetRef ref) {
useListenable(apnsDeviceToken);
final registrationAttempt = useRef<String?>(null);
final publicationAttempt = useRef<String?>(null);
final session = ref.watch(relaySessionProvider);
final config = ref.watch(relayConfigProvider);
final community = ref.watch(activeCommunityProvider).value;
final memberPubkey = ref.watch(myPubkeyProvider);
final descriptor = ref.watch(currentRelayPushDescriptorProvider).value;
useEffect(
() {
if (!_ready(session, config, community, memberPubkey) ||
descriptor == null) {
return null;
}
final attempt = '${community!.id}|${config.baseUrl}';
if (registrationAttempt.value == attempt) return null;
registrationAttempt.value = attempt;
unawaited(() async {
try {
await startBuzzPushRegistrationIfCapable(
descriptor,
startRegistration: startBuzzPushRegistration,
);
} catch (error, stack) {
registrationAttempt.value = null;
debugPrint('Push registration bootstrap failed: $error');
debugPrintStack(stackTrace: stack);
}
}());
return null;
},
[session.status, config.baseUrl, community?.id, memberPubkey, descriptor],
);
final token = apnsDeviceToken.value;
useEffect(
() {
if (!_ready(session, config, community, memberPubkey) ||
descriptor == null ||
token == null) {
return null;
}
final state = community!.pushSubscriptionState;
if (state.desired.isEmpty) return null;
final attempt = [
community.id,
config.baseUrl,
token,
buzzPushSubscriptionsFingerprint(state.desired),
].join('|');
if (publicationAttempt.value == attempt) return null;
publicationAttempt.value = attempt;
final relay = SignedEventRelay(
session: ref.read(relaySessionProvider.notifier),
nsec: config.nsec!,
);
unawaited(
_publish(ref, config, community, memberPubkey!, relay).catchError((
Object error,
StackTrace stack,
) {
publicationAttempt.value = null;
debugPrint('Push lease bootstrap failed: $error');
debugPrintStack(stackTrace: stack);
}),
);
return null;
},
[
session.status,
config.baseUrl,
community?.id,
community?.pushSubscriptionState,
memberPubkey,
descriptor,
token,
],
);
return child;
}
static bool _ready(
SessionState session,
RelayConfig config,
Community? community,
String? memberPubkey,
) =>
session.status == SessionStatus.connected &&
community != null &&
config.nsec != null &&
config.nsec!.isNotEmpty &&
memberPubkey != null &&
memberPubkey.isNotEmpty;
static Future<void> _publish(
WidgetRef ref,
RelayConfig config,
Community community,
String memberPubkey,
SignedEventRelay relay,
) async {
final state = community.pushSubscriptionState;
final desired = state.desired;
final descriptor = await fetchBuzzPushLeaseDescriptor(config.baseUrl);
final grant = await enrollBuzzPush(
config.wsUrl,
Env.pushGatewayUrl,
communitiesForSnapshotRefresh:
ref.read(communityListProvider).value ?? [community],
);
// Relay lease replacement and gateway delegation are independent state
// machines. Subscription changes advance only the kind-30350 generation;
// the opaque grant remains reusable until its own authority changes.
final leaseGeneration = (state.acceptedGeneration ?? 0) + 1;
await publishBuzzDevPushLeaseThroughRelay(
grant: grant,
leaseGeneration: leaseGeneration,
descriptor: descriptor,
nsec: config.nsec!,
memberPubkey: memberPubkey,
subscriptions: desired,
relay: relay,
);
await ref
.read(communityListProvider.notifier)
.markPushLeaseAccepted(
community.id,
subscriptions: desired,
generation: leaseGeneration,
);
}
}