Files
buzz/mobile/lib/shared/hosted/use_known_communities.dart
klopez4212 9c3ba83adb Simplify signed-in mobile community onboarding (#8852)
Replace the signed-in mobile welcome screen with **Add a community**: QR
scan first, then grey secondary actions for community links, finding
saved communities, and creation. Hide **Find my communities** when none
are known; refresh on entry/resume and every 30 seconds while active,
retaining results on transient failure.

Community switcher → Add keeps its two-action flow and uses the same
grey secondary link button.

Validation: Flutter analysis, full mobile suite (4,014 tests),
Builderlab-enabled full suite (4,036 tests), widget interactions, and
independent agent review. Draft pending native sign-in and human
testing. Snapshots render the production Flutter widgets; native
navigation/status bars are omitted.

| Communities available | No communities | Switcher → Add |
|---|---|---|
| ![Onboarding with
communities](https://github.com/user-attachments/assets/444b05c5-711f-41df-9c50-293ad9e13767)
| ![Onboarding without
communities](https://github.com/user-attachments/assets/db78a490-bd8e-4d78-97b4-a391b52290d8)
| ![Switcher Add with grey secondary
button](https://github.com/user-attachments/assets/de9e42d2-dd53-462b-98c7-da87223e5dd4)
|

---------

Signed-off-by: kenny lopez <klopez4212@gmail.com>
Co-authored-by: Terp <5856f6900e8e0a4e0ea1f4f8d2ee33e4753d2119bc122f94ecafc660b46c0511@buzz.block.builderlab.xyz>
2026-10-31 07:46:44 -07:00

57 lines
1.8 KiB
Dart

import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
/// Refreshes account destinations only while the entry screen is active.
/// A failed refresh preserves the last successful result for this account.
bool useKnownCommunities({
required String? accountKey,
required bool active,
required Future<bool> Function() load,
}) {
final available = useMemoized(() => ValueNotifier(false), [accountKey]);
useEffect(() => available.dispose, [available]);
useListenable(available);
final latestLoad = useRef(load)..value = load;
useEffect(() {
if (accountKey == null || !active) return null;
var disposed = false;
var loading = false;
Future<void> refresh() async {
if (loading) return;
loading = true;
try {
final result = await latestLoad.value();
if (!disposed) available.value = result;
} catch (_) {
// Discovery is optional; an unavailable service is not an empty list.
} finally {
loading = false;
}
}
Timer? timer;
void lifecycleChanged(AppLifecycleState? state) {
timer?.cancel();
timer = null;
if (state != null && state != AppLifecycleState.resumed) return;
unawaited(refresh());
timer = Timer.periodic(const Duration(seconds: 30), (_) {
unawaited(refresh());
});
}
// Lifecycle callbacks must stop timers immediately: paused apps do not
// render a frame, so a rebuild-driven effect may never run its cleanup.
final listener = AppLifecycleListener(onStateChange: lifecycleChanged);
lifecycleChanged(WidgetsBinding.instance.lifecycleState);
return () {
disposed = true;
timer?.cancel();
listener.dispose();
};
}, [accountKey, active, available]);
return available.value;
}