mirror of
https://github.com/block/buzz.git
synced 2026-11-03 16:01:35 +00:00
fix(mobile/ios): synchronize push snapshot lifecycle
Co-authored-by: npub15uvq03jgm52chkxa5x7f2ud28xh9v48287f5fj5fsuu0wqpm63ns8hn2er <a71807c648dd158bd8dda1bc9571aa39ae5654ea3f9344ca898738f7003bd467@buzz.block.builderlab.xyz> Signed-off-by: npub15uvq03jgm52chkxa5x7f2ud28xh9v48287f5fj5fsuu0wqpm63ns8hn2er <a71807c648dd158bd8dda1bc9571aa39ae5654ea3f9344ca898738f7003bd467@buzz.block.builderlab.xyz>
This commit is contained in:
parent
14a529d4af
commit
e067a394dc
@@ -24,6 +24,7 @@ class AuthNotifier extends AsyncNotifier<AuthState> {
|
||||
final storage = ref.read(communityStorageProvider);
|
||||
final communities = await storage.loadAll();
|
||||
if (communities.isEmpty) {
|
||||
await ref.read(communitySnapshotWriterProvider)(communities);
|
||||
return const AuthState(status: AuthStatus.unauthenticated);
|
||||
}
|
||||
|
||||
@@ -36,6 +37,7 @@ class AuthNotifier extends AsyncNotifier<AuthState> {
|
||||
await storage.saveActiveId(active.id);
|
||||
|
||||
if (_hasValidNsec(active.nsec)) {
|
||||
await ref.read(communitySnapshotWriterProvider)(communities);
|
||||
return AuthState(status: AuthStatus.authenticated, community: active);
|
||||
}
|
||||
|
||||
@@ -47,6 +49,7 @@ class AuthNotifier extends AsyncNotifier<AuthState> {
|
||||
}
|
||||
|
||||
await storage.clearActiveId();
|
||||
await ref.read(communitySnapshotWriterProvider)(communities);
|
||||
return const AuthState(status: AuthStatus.unauthenticated);
|
||||
}
|
||||
|
||||
@@ -57,6 +60,7 @@ class AuthNotifier extends AsyncNotifier<AuthState> {
|
||||
final storage = ref.read(communityStorageProvider);
|
||||
await storage.save(community);
|
||||
await storage.saveActiveId(community.id);
|
||||
await syncStoredCommunitySnapshot(ref);
|
||||
|
||||
// Invalidate community providers so other consumers pick up the new data.
|
||||
ref.invalidate(communityListProvider);
|
||||
@@ -76,8 +80,10 @@ class AuthNotifier extends AsyncNotifier<AuthState> {
|
||||
}
|
||||
|
||||
// Check if other communities remain — switch to the next one instead of
|
||||
// forcing the user back to the pairing screen.
|
||||
// forcing the user back to the pairing screen. Refreshing the complete
|
||||
// snapshot also removes revoked keys from the extension Keychain.
|
||||
final remaining = await storage.loadAll();
|
||||
await ref.read(communitySnapshotWriterProvider)(remaining);
|
||||
|
||||
// Invalidate community providers so other consumers pick up the change.
|
||||
ref.invalidate(communityListProvider);
|
||||
|
||||
@@ -9,11 +9,30 @@ final communityStorageProvider = Provider<CommunityStorage>((ref) {
|
||||
return CommunityStorage();
|
||||
});
|
||||
|
||||
typedef CommunitySnapshotWriter =
|
||||
Future<void> Function(List<Community> communities);
|
||||
|
||||
/// Writes the complete persisted community set to storage shared with the iOS
|
||||
/// notification service extension. Tests override this provider to verify that
|
||||
/// every persistence path refreshes (or clears) the native snapshot.
|
||||
final communitySnapshotWriterProvider = Provider<CommunitySnapshotWriter>((
|
||||
ref,
|
||||
) {
|
||||
return registerBuzzPushCommunitySnapshot;
|
||||
});
|
||||
|
||||
Future<void> syncStoredCommunitySnapshot(Ref ref) async {
|
||||
final communities = await ref.read(communityStorageProvider).loadAll();
|
||||
await ref.read(communitySnapshotWriterProvider)(communities);
|
||||
}
|
||||
|
||||
class CommunityListNotifier extends AsyncNotifier<List<Community>> {
|
||||
@override
|
||||
Future<List<Community>> build() async {
|
||||
final storage = ref.read(communityStorageProvider);
|
||||
return storage.loadAll();
|
||||
final communities = await storage.loadAll();
|
||||
await ref.read(communitySnapshotWriterProvider)(communities);
|
||||
return communities;
|
||||
}
|
||||
|
||||
/// Add a community. If one with the same relay URL already exists, update
|
||||
|
||||
@@ -20,8 +20,16 @@ void main() {
|
||||
);
|
||||
await storage.save(invalid);
|
||||
await storage.saveActiveId(invalid.id);
|
||||
final snapshots = <List<Community>>[];
|
||||
final container = ProviderContainer(
|
||||
overrides: [communityStorageProvider.overrideWithValue(storage)],
|
||||
overrides: [
|
||||
communityStorageProvider.overrideWithValue(storage),
|
||||
communitySnapshotWriterProvider.overrideWithValue((
|
||||
communities,
|
||||
) async {
|
||||
snapshots.add(List.of(communities));
|
||||
}),
|
||||
],
|
||||
);
|
||||
addTearDown(container.dispose);
|
||||
|
||||
@@ -30,6 +38,87 @@ void main() {
|
||||
expect(auth.status, AuthStatus.unauthenticated);
|
||||
expect(await storage.loadAll(), isEmpty);
|
||||
expect(await storage.loadActiveId(), isNull);
|
||||
expect(snapshots.last, isEmpty);
|
||||
},
|
||||
);
|
||||
|
||||
test('authenticate exports the complete stored community snapshot', () async {
|
||||
final storage = CommunityStorage(secure: FakeSecureStorage());
|
||||
final existing = Community.create(
|
||||
name: 'Existing',
|
||||
relayUrl: 'https://existing.example',
|
||||
nsec: nostr.Keys.generate().nsec,
|
||||
);
|
||||
final added = Community.create(
|
||||
name: 'Added',
|
||||
relayUrl: 'https://added.example',
|
||||
nsec: nostr.Keys.generate().nsec,
|
||||
);
|
||||
await storage.save(existing);
|
||||
final snapshots = <List<Community>>[];
|
||||
final container = ProviderContainer(
|
||||
overrides: [
|
||||
communityStorageProvider.overrideWithValue(storage),
|
||||
communitySnapshotWriterProvider.overrideWithValue((communities) async {
|
||||
snapshots.add(List.of(communities));
|
||||
}),
|
||||
],
|
||||
);
|
||||
addTearDown(container.dispose);
|
||||
|
||||
await container
|
||||
.read(authProvider.notifier)
|
||||
.authenticateWithCommunity(added);
|
||||
|
||||
expect(snapshots.last.map((community) => community.id), {
|
||||
existing.id,
|
||||
added.id,
|
||||
});
|
||||
});
|
||||
|
||||
test(
|
||||
'sign out removes the active community from the shared snapshot',
|
||||
() async {
|
||||
final storage = CommunityStorage(secure: FakeSecureStorage());
|
||||
final first = Community.create(
|
||||
name: 'First',
|
||||
relayUrl: 'https://first.example',
|
||||
nsec: nostr.Keys.generate().nsec,
|
||||
);
|
||||
final second = Community.create(
|
||||
name: 'Second',
|
||||
relayUrl: 'https://second.example',
|
||||
nsec: nostr.Keys.generate().nsec,
|
||||
);
|
||||
await storage.save(first);
|
||||
await storage.save(second);
|
||||
await storage.saveActiveId(first.id);
|
||||
final snapshots = <List<Community>>[];
|
||||
final container = ProviderContainer(
|
||||
overrides: [
|
||||
communityStorageProvider.overrideWithValue(storage),
|
||||
communitySnapshotWriterProvider.overrideWithValue((
|
||||
communities,
|
||||
) async {
|
||||
snapshots.add(List.of(communities));
|
||||
}),
|
||||
],
|
||||
);
|
||||
addTearDown(container.dispose);
|
||||
await container.read(authProvider.future);
|
||||
|
||||
await container.read(authProvider.notifier).signOut();
|
||||
|
||||
expect(
|
||||
snapshots.any((snapshot) {
|
||||
return snapshot.length == 1 && snapshot.single.id == second.id;
|
||||
}),
|
||||
isTrue,
|
||||
);
|
||||
expect(
|
||||
snapshots.last.map((community) => community.id),
|
||||
isNot(contains(first.id)),
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:buzz/shared/community/community.dart';
|
||||
import 'package:buzz/shared/community/community_provider.dart';
|
||||
import 'package:buzz/shared/community/community_storage.dart';
|
||||
import 'package:nostr/nostr.dart' as nostr;
|
||||
|
||||
import 'community_storage_test.dart';
|
||||
|
||||
@@ -10,17 +13,24 @@ void main() {
|
||||
late FakeSecureStorage fakeSecure;
|
||||
late CommunityStorage communityStorage;
|
||||
late ProviderContainer container;
|
||||
late List<List<Community>> snapshots;
|
||||
|
||||
setUp(() {
|
||||
fakeSecure = FakeSecureStorage();
|
||||
communityStorage = CommunityStorage(secure: fakeSecure);
|
||||
snapshots = [];
|
||||
});
|
||||
|
||||
tearDown(() => container.dispose());
|
||||
|
||||
ProviderContainer createContainer() {
|
||||
return ProviderContainer(
|
||||
overrides: [communityStorageProvider.overrideWithValue(communityStorage)],
|
||||
overrides: [
|
||||
communityStorageProvider.overrideWithValue(communityStorage),
|
||||
communitySnapshotWriterProvider.overrideWithValue((communities) async {
|
||||
snapshots.add(List.of(communities));
|
||||
}),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
@@ -29,6 +39,24 @@ void main() {
|
||||
container = createContainer();
|
||||
final communities = await container.read(communityListProvider.future);
|
||||
expect(communities, isEmpty);
|
||||
expect(snapshots, [isEmpty]);
|
||||
});
|
||||
|
||||
test('exports migrated communities on startup', () async {
|
||||
final community = Community.create(
|
||||
name: 'Migrated',
|
||||
relayUrl: 'https://migrated.example.com',
|
||||
nsec: nostr.Keys.generate().nsec,
|
||||
);
|
||||
// Seed legacy storage to exercise the same migration path as an app
|
||||
// upgrade.
|
||||
fakeSecure['buzz_workspaces'] = jsonEncode([community.toJson()]);
|
||||
|
||||
container = createContainer();
|
||||
await container.read(communityListProvider.future);
|
||||
|
||||
expect(snapshots.single.single.id, community.id);
|
||||
expect(fakeSecure['buzz_workspaces'], isNull);
|
||||
});
|
||||
|
||||
test('addCommunity adds to list', () async {
|
||||
|
||||
Reference in New Issue
Block a user