From 14194f166702634db8b6a22d406d4e3377319bd4 Mon Sep 17 00:00:00 2001 From: Hongkuan Zhou <6771308+tedzhouhk@users.noreply.github.com> Date: Tue, 11 Aug 2026 20:18:30 -0700 Subject: [PATCH 1/3] refactor: share BeeCount Cloud provider instance --- lib/pages/cloud/devices_page.dart | 22 ++++-------- lib/providers/sync_providers.dart | 25 +++++++------ .../beecount_cloud_auth_provider_test.dart | 36 +++++++++++++++++++ 3 files changed, 58 insertions(+), 25 deletions(-) create mode 100644 test/providers/beecount_cloud_auth_provider_test.dart diff --git a/lib/pages/cloud/devices_page.dart b/lib/pages/cloud/devices_page.dart index 7d1923949..9db47439a 100644 --- a/lib/pages/cloud/devices_page.dart +++ b/lib/pages/cloud/devices_page.dart @@ -82,17 +82,11 @@ class _DevicesPageState extends ConsumerState { /// 获取 BeeCountCloudProvider 实例(仅 beecountCloud 后端可用) Future _getCloudProvider() async { - final config = await ref.read(activeCloudConfigProvider.future); - if (!config.valid || config.type != CloudBackendType.beecountCloud) { - throw StateError( - AppLocalizations.of(context).cloudCollabUnavailableMessage); - } - final services = await createCloudServices(config); - if (services.provider == null || services.provider is! BeeCountCloudProvider) { - throw StateError( - AppLocalizations.of(context).cloudCollabUnavailableMessage); - } - return services.provider as BeeCountCloudProvider; + final unavailableMessage = + AppLocalizations.of(context).cloudCollabUnavailableMessage; + final provider = await ref.read(beecountCloudProviderInstance.future); + if (provider == null) throw StateError(unavailableMessage); + return provider; } Future _reload({bool keepLoadingState = true}) async { @@ -104,11 +98,9 @@ class _DevicesPageState extends ConsumerState { _scopeDenied = false; }); try { - final auth = await ref.read(authServiceProvider.future); - final user = await auth.currentUser; - final currentDeviceId = user?.metadata?['deviceId']?.toString(); - final provider = await _getCloudProvider(); + final user = await provider.auth.currentUser; + final currentDeviceId = user?.metadata?['deviceId']?.toString(); final devices = await provider.listDevices( view: _showAllSessions ? 'sessions' : 'deduped', activeWithinDays: 30, diff --git a/lib/providers/sync_providers.dart b/lib/providers/sync_providers.dart index 7950d531c..d72ccb20f 100644 --- a/lib/providers/sync_providers.dart +++ b/lib/providers/sync_providers.dart @@ -141,19 +141,26 @@ final s3ConfigProvider = FutureProvider((ref) async { }); final authServiceProvider = FutureProvider((ref) async { - final activeAsync = ref.watch(activeCloudConfigProvider); - if (!activeAsync.hasValue) { - return NoopAuthService(); - } - - final config = activeAsync.value!; + final config = await ref.watch(activeCloudConfigProvider.future); if (!config.valid || config.type == CloudBackendType.local) { return NoopAuthService(); } try { + // BeeCount Cloud 必须复用同步引擎持有的唯一 provider/auth 实例。 + // 多个实例虽然共用 SharedPreferences,却各自缓存 session;这会让 2FA + // 登录成功后同步实例仍停留在未登录状态。 + if (config.type == CloudBackendType.beecountCloud) { + final provider = await ref.watch(beecountCloudProviderInstance.future); + return provider?.auth ?? NoopAuthService(); + } + final services = await createCloudServices(config); if (services.auth != null) { + final provider = services.provider; + if (provider != null) { + ref.onDispose(() => unawaited(provider.dispose())); + } return services.auth!; } } catch (e) { @@ -488,10 +495,7 @@ final syncServiceProvider = Provider((ref) { /// 用于 SyncEngine 和其他需要直接访问 BeeCount Cloud API 的场景 final beecountCloudProviderInstance = FutureProvider((ref) async { - final configAsync = ref.watch(activeCloudConfigProvider); - if (!configAsync.hasValue) return null; - - final config = configAsync.value!; + final config = await ref.watch(activeCloudConfigProvider.future); if (!config.valid || config.type != CloudBackendType.beecountCloud) { return null; } @@ -500,6 +504,7 @@ final beecountCloudProviderInstance = final services = await createCloudServices(config); if (services.provider is! BeeCountCloudProvider) return null; final provider = services.provider as BeeCountCloudProvider; + ref.onDispose(() => unawaited(provider.dispose())); final email = config.beecountCloudEmail; final password = config.beecountCloudPassword; diff --git a/test/providers/beecount_cloud_auth_provider_test.dart b/test/providers/beecount_cloud_auth_provider_test.dart new file mode 100644 index 000000000..3a43ea036 --- /dev/null +++ b/test/providers/beecount_cloud_auth_provider_test.dart @@ -0,0 +1,36 @@ +import 'package:flutter_cloud_sync/flutter_cloud_sync.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:shared_preferences/shared_preferences.dart'; + +import 'package:beecount/providers/sync_providers.dart'; + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + + setUp(() { + SharedPreferences.setMockInitialValues({}); + }); + + test('BeeCount Cloud UI auth and SyncEngine share one auth instance', + () async { + const config = CloudServiceConfig( + type: CloudBackendType.beecountCloud, + name: 'BeeCount Cloud', + beecountCloudBaseUrl: 'https://cloud.example.com', + beecountCloudApiPrefix: '/api/v1', + ); + final container = ProviderContainer( + overrides: [ + activeCloudConfigProvider.overrideWith((ref) async => config), + ], + ); + addTearDown(container.dispose); + + final provider = await container.read(beecountCloudProviderInstance.future); + final auth = await container.read(authServiceProvider.future); + + expect(provider, isNotNull); + expect(identical(auth, provider!.auth), isTrue); + }); +} From ec2f2f2388ab6864d7ab49d2367fecd2ff67ab99 Mon Sep 17 00:00:00 2001 From: Hongkuan Zhou <6771308+tedzhouhk@users.noreply.github.com> Date: Tue, 11 Aug 2026 20:50:00 -0700 Subject: [PATCH 2/3] fix: avoid disposing providers before cached engines --- lib/providers/sync_providers.dart | 5 ----- 1 file changed, 5 deletions(-) diff --git a/lib/providers/sync_providers.dart b/lib/providers/sync_providers.dart index d72ccb20f..d48a8d5dc 100644 --- a/lib/providers/sync_providers.dart +++ b/lib/providers/sync_providers.dart @@ -157,10 +157,6 @@ final authServiceProvider = FutureProvider((ref) async { final services = await createCloudServices(config); if (services.auth != null) { - final provider = services.provider; - if (provider != null) { - ref.onDispose(() => unawaited(provider.dispose())); - } return services.auth!; } } catch (e) { @@ -504,7 +500,6 @@ final beecountCloudProviderInstance = final services = await createCloudServices(config); if (services.provider is! BeeCountCloudProvider) return null; final provider = services.provider as BeeCountCloudProvider; - ref.onDispose(() => unawaited(provider.dispose())); final email = config.beecountCloudEmail; final password = config.beecountCloudPassword; From 0052464514a61f3f849e0d96212a6db94f61e1b0 Mon Sep 17 00:00:00 2001 From: Hongkuan Zhou <6771308+tedzhouhk@users.noreply.github.com> Date: Sat, 15 Aug 2026 13:22:56 -0700 Subject: [PATCH 3/3] refactor: narrow shared provider change --- lib/pages/cloud/devices_page.dart | 22 +++++++++++++++------- 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/lib/pages/cloud/devices_page.dart b/lib/pages/cloud/devices_page.dart index 9db47439a..7d1923949 100644 --- a/lib/pages/cloud/devices_page.dart +++ b/lib/pages/cloud/devices_page.dart @@ -82,11 +82,17 @@ class _DevicesPageState extends ConsumerState { /// 获取 BeeCountCloudProvider 实例(仅 beecountCloud 后端可用) Future _getCloudProvider() async { - final unavailableMessage = - AppLocalizations.of(context).cloudCollabUnavailableMessage; - final provider = await ref.read(beecountCloudProviderInstance.future); - if (provider == null) throw StateError(unavailableMessage); - return provider; + final config = await ref.read(activeCloudConfigProvider.future); + if (!config.valid || config.type != CloudBackendType.beecountCloud) { + throw StateError( + AppLocalizations.of(context).cloudCollabUnavailableMessage); + } + final services = await createCloudServices(config); + if (services.provider == null || services.provider is! BeeCountCloudProvider) { + throw StateError( + AppLocalizations.of(context).cloudCollabUnavailableMessage); + } + return services.provider as BeeCountCloudProvider; } Future _reload({bool keepLoadingState = true}) async { @@ -98,9 +104,11 @@ class _DevicesPageState extends ConsumerState { _scopeDenied = false; }); try { - final provider = await _getCloudProvider(); - final user = await provider.auth.currentUser; + final auth = await ref.read(authServiceProvider.future); + final user = await auth.currentUser; final currentDeviceId = user?.metadata?['deviceId']?.toString(); + + final provider = await _getCloudProvider(); final devices = await provider.listDevices( view: _showAllSessions ? 'sessions' : 'deduped', activeWithinDays: 30,