From c1deb4a2d711c47f414443b78ff9d46acf9b9597 Mon Sep 17 00:00:00 2001 From: Miduo666 Date: Mon, 4 May 2026 10:26:54 +1000 Subject: [PATCH 1/3] fix_cache_race_condition --- lib/src/widgets/solid_profile_editor.dart | 26 ++++++++++++++++++++++- lib/src/widgets/solid_scaffold.dart | 1 + lib/src/widgets/solid_scaffold_state.dart | 19 +++++++++++++++++ 3 files changed, 45 insertions(+), 1 deletion(-) diff --git a/lib/src/widgets/solid_profile_editor.dart b/lib/src/widgets/solid_profile_editor.dart index 37aaad37..f4b3d79c 100644 --- a/lib/src/widgets/solid_profile_editor.dart +++ b/lib/src/widgets/solid_profile_editor.dart @@ -66,6 +66,11 @@ class _SolidProfileEditorState extends State { bool _isSaving = false; late SolidProfilePrivacy _pendingPrivacy; + /// True once the user explicitly toggles the privacy selector. + /// Prevents async [loadProfile] completions from overwriting a local edit. + + bool _privacyUserEdited = false; + @override void initState() { super.initState(); @@ -74,14 +79,30 @@ class _SolidProfileEditorState extends State { ); _pendingAvatar = solidProfileNotifier.avatarBytes; _pendingPrivacy = solidProfileNotifier.privacy; + solidProfileNotifier.addListener(_onProfileNotifierChanged); } @override void dispose() { + solidProfileNotifier.removeListener(_onProfileNotifierChanged); _nameController.dispose(); super.dispose(); } + /// Keeps [_pendingPrivacy] in sync if [solidProfileNotifier] is updated + /// asynchronously (e.g. [loadProfile] completes after the editor opens). + /// Only updates the pending value when the user has not already explicitly + /// toggled the privacy selector. + + void _onProfileNotifierChanged() { + if (!mounted || _privacyUserEdited) return; + if (_pendingPrivacy != solidProfileNotifier.privacy) { + setState(() { + _pendingPrivacy = solidProfileNotifier.privacy; + }); + } + } + bool get _hasChanges { final nameChanged = _nameController.text.trim() != (solidProfileNotifier.displayName ?? ''); @@ -224,7 +245,10 @@ class _SolidProfileEditorState extends State { selected: {_pendingPrivacy}, onSelectionChanged: _isSaving ? null - : (values) => setState(() => _pendingPrivacy = values.first), + : (values) => setState(() { + _pendingPrivacy = values.first; + _privacyUserEdited = true; + }), ), const SizedBox(height: 6), Text( diff --git a/lib/src/widgets/solid_scaffold.dart b/lib/src/widgets/solid_scaffold.dart index e88af043..ff6f1bcc 100644 --- a/lib/src/widgets/solid_scaffold.dart +++ b/lib/src/widgets/solid_scaffold.dart @@ -35,6 +35,7 @@ import 'package:flutter/material.dart'; import 'package:solidui/src/constants/navigation.dart'; import 'package:solidui/src/handlers/solid_auth_handler.dart'; +import 'package:solidui/src/services/solid_login_status_notifier.dart'; import 'package:solidui/src/services/solid_profile_service.dart'; import 'package:solidui/src/services/solid_security_key_notifier.dart'; import 'package:solidui/src/services/solid_security_key_service.dart'; diff --git a/lib/src/widgets/solid_scaffold_state.dart b/lib/src/widgets/solid_scaffold_state.dart index c68b856f..12de7d75 100644 --- a/lib/src/widgets/solid_scaffold_state.dart +++ b/lib/src/widgets/solid_scaffold_state.dart @@ -56,6 +56,13 @@ class SolidScaffoldState extends State { } }); _loadCurrentWebId(); + // Profile is loaded reactively via _onLoginStatusChangedForProfile when + // solidLoginStatusNotifier confirms the user is logged in. This avoids + // the race condition where isUserLoggedIn() returns false immediately + // after a page refresh before auth state has been restored from storage. + // As a best-effort fallback we also attempt an eager load here; if auth + // is not yet ready loadProfile() will be a no-op and the listener above + // will retry once the login status is resolved. if (widget.enableProfile) { SolidProfileService.instance.loadProfile(); } @@ -83,6 +90,9 @@ class SolidScaffoldState extends State { } solidPreferencesNotifier.addListener(_onPreferencesChanged); widget.controller?.addListener(_onControllerChanged); + if (widget.enableProfile) { + solidLoginStatusNotifier.addListener(_onLoginStatusChangedForProfile); + } } Future _initializeNotifiers() async { @@ -114,6 +124,9 @@ class SolidScaffoldState extends State { } solidPreferencesNotifier.removeListener(_onPreferencesChanged); widget.controller?.removeListener(_onControllerChanged); + if (widget.enableProfile) { + solidLoginStatusNotifier.removeListener(_onLoginStatusChangedForProfile); + } super.dispose(); } @@ -143,6 +156,12 @@ class SolidScaffoldState extends State { widget.statusBar?.securityKeyStatus?.onKeyStatusChanged, ); + void _onLoginStatusChangedForProfile() { + if (solidLoginStatusNotifier.isLoggedIn) { + SolidProfileService.instance.loadProfile(); + } + } + Future refreshSecurityKeyStatus() async => await _securityKeyHelper?.refresh( _isKeySaved, From 378bae66eeaa5a8aac0f05d6367983426be81d4c Mon Sep 17 00:00:00 2001 From: Miduo666 Date: Wed, 6 May 2026 12:39:20 +1000 Subject: [PATCH 2/3] small fix on cache management: read pribacypreference every time to keep consistency --- lib/src/services/solid_profile_service.dart | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/lib/src/services/solid_profile_service.dart b/lib/src/services/solid_profile_service.dart index c634f654..8f3b49ee 100644 --- a/lib/src/services/solid_profile_service.dart +++ b/lib/src/services/solid_profile_service.dart @@ -98,8 +98,6 @@ class SolidProfileService { if (_initialised) return; if (!await isUserLoggedIn()) return; - await _loadPrivacyPreference(); - final dirUrl = await _profileDirUrl(); // Create the folder if it is missing. We deliberately do not swallow @@ -139,7 +137,11 @@ class SolidProfileService { try { await ensureProfileFolder(); - await Future.wait([_loadAvatar(), _loadDisplayName()]); + await Future.wait([ + _loadAvatar(), + _loadDisplayName(), + _loadPrivacyPreference(), + ]); } catch (e) { debugPrint('SolidProfileService.loadProfile: $e'); } finally { From ca4caacebe999f0fc74a8352e93ac67bb9cd0fd6 Mon Sep 17 00:00:00 2001 From: Miduo666 Date: Wed, 6 May 2026 12:55:36 +1000 Subject: [PATCH 3/3] remove SharedPreference to keep consistency --- lib/src/services/solid_profile_service.dart | 52 ++++++++------------- 1 file changed, 20 insertions(+), 32 deletions(-) diff --git a/lib/src/services/solid_profile_service.dart b/lib/src/services/solid_profile_service.dart index 8f3b49ee..1a746003 100644 --- a/lib/src/services/solid_profile_service.dart +++ b/lib/src/services/solid_profile_service.dart @@ -34,7 +34,6 @@ import 'dart:typed_data'; import 'package:flutter/foundation.dart' show debugPrint; import 'package:rdflib/rdflib.dart' show Literal, Namespace, URIRef; -import 'package:shared_preferences/shared_preferences.dart'; import 'package:solidpod/solidpod.dart'; import 'package:solidui/src/services/solid_profile_notifier.dart'; @@ -47,10 +46,6 @@ const int maxProfilePictureBytes = 2 * 1024 * 1024; const Set allowedProfileExtensions = {'.png', '.jpg', '.jpeg'}; -// SharedPreferences key prefix for the per-WebID privacy preference. - -const String _privacyPrefKey = 'solidui_profile_privacy_'; - /// Manages reading, writing, and deleting profile data on the user's POD. class SolidProfileService { @@ -137,11 +132,10 @@ class SolidProfileService { try { await ensureProfileFolder(); - await Future.wait([ - _loadAvatar(), - _loadDisplayName(), - _loadPrivacyPreference(), - ]); + // Privacy must be resolved before avatar/display-name because it + // determines the decryption mode used by readPod. + await _loadPrivacyFromAcl(); + await Future.wait([_loadAvatar(), _loadDisplayName()]); } catch (e) { debugPrint('SolidProfileService.loadProfile: $e'); } finally { @@ -258,7 +252,6 @@ class SolidProfileService { final currentName = solidProfileNotifier.displayName; solidProfileNotifier.setPrivacy(mode); - await _persistPrivacyPreference(mode); if (currentAvatar != null) { await saveAvatar(currentAvatar); @@ -296,30 +289,25 @@ class SolidProfileService { await createResource(aclUrl, content: aclTurtle, replaceIfExist: true); } - Future _loadPrivacyPreference() async { - try { - final webId = await getWebId(); - if (webId == null) return; - final prefs = await SharedPreferences.getInstance(); - final stored = prefs.getString('$_privacyPrefKey$webId'); - if (stored == SolidProfilePrivacy.public.name) { - solidProfileNotifier.setPrivacy(SolidProfilePrivacy.public); - } else { - solidProfileNotifier.setPrivacy(SolidProfilePrivacy.private); - } - } catch (e) { - debugPrint('SolidProfileService._loadPrivacyPreference: $e'); - } - } + /// Reads the profile folder ACL from the POD and updates + /// [solidProfileNotifier] with the derived [SolidProfilePrivacy]. + /// Public mode is inferred from the presence of a public-read grant + /// (`foaf:Agent` + `acl:Read`) in the ACL turtle. - Future _persistPrivacyPreference(SolidProfilePrivacy mode) async { + Future _loadPrivacyFromAcl() async { try { - final webId = await getWebId(); - if (webId == null) return; - final prefs = await SharedPreferences.getInstance(); - await prefs.setString('$_privacyPrefKey$webId', mode.name); + final dirUrl = await _profileDirUrl(); + final aclUrl = '$dirUrl.acl'; + if (await checkResourceStatus(aclUrl) != ResourceStatus.exist) return; + + final content = await readPod(aclUrl, pathType: PathType.absoluteUrl); + final isPublic = + content.contains('foaf:Agent') && content.contains('acl:Read'); + solidProfileNotifier.setPrivacy( + isPublic ? SolidProfilePrivacy.public : SolidProfilePrivacy.private, + ); } catch (e) { - debugPrint('SolidProfileService._persistPrivacyPreference: $e'); + debugPrint('SolidProfileService._loadPrivacyFromAcl: $e'); } }