Skip to content

Conversation

@untreu2
Copy link
Contributor

@untreu2 untreu2 commented Sep 10, 2025

Description

Fixed the settings page showing "No accounts found" for 1-10 seconds by replacing redundant getAccounts() calls with provider-first approach.

Type of Change

  • ✨ New feature (non-breaking change which adds functionality)
  • 🛠️ Bug fix (non-breaking change which fixes an issue)
  • ❌ Breaking change (fix or feature that would cause existing functionality to change)
  • 🧹 Code refactor
  • ✅ Build configuration change
  • 📝 Documentation
  • 🗑️ Chore
  • 🧪 Tests

Checklist

  • Run just precommit to ensure that formatting and linting are correct
  • Run just check-flutter-coverage to ensure that flutter coverage rules are passing
  • Updated the CHANGELOG.md file with your changes (if they affect the user experience)

Fixes #608

Summary by CodeRabbit

  • New Features
    • Added loading indicators and clear error messages when fetching accounts.
    • Introduced lazy/background loading of accounts and contact details for faster startup.
    • Streamlined account switching and logout using cached data for a smoother experience.
  • Bug Fixes
    • Prevented duplicate/concurrent account loads to avoid UI glitches.
    • Reduced unnecessary full reloads when switching accounts.
    • Improved error handling with toasts for clearer feedback.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Sep 10, 2025

Walkthrough

Refactors GeneralSettingsScreen to load account data via providers with lazy loading, adds guarded fallback API loading, introduces loading/error state fields, listens for provider changes to refresh current account and metadata-derived ContactModel, updates UI to show loading/error states, and adjusts account switching/logout to use cached/provider data with toast-based error handling.

Changes

Cohort / File(s) Summary of Changes
Provider-driven account loading and UI state
lib/ui/settings/general_settings_screen.dart
Replaces direct bridge calls with provider-based loading: adds _loadFromProviders, _loadAccountsLazilyIfNeeded, _loadContactModelsForOtherAccounts, _loadAccountsFromApi, _refreshAccountData. Introduces _isLoadingAccounts, _loadingError, _isLoadingInProgress. Listens to active account/metadata providers, constructs ContactModel from metadata, updates UI to show loading/error, and uses toasts for failures. Account switching and logout rely on cached/provider data. No public API changes.

Sequence Diagram(s)

sequenceDiagram
  actor User
  participant Screen as GeneralSettingsScreen
  participant Providers as Account/Metadata Providers
  participant API as Accounts API
  participant UI as UI/Toasts

  User->>Screen: Open Settings
  rect #eef7ff
    note right of Screen: Initial load
    Screen->>Providers: Get active account + metadata
    alt Provider data available
      Screen->>Screen: _refreshAccountData()<br/>Create ContactModel from metadata
      Screen->>Providers: (async) Maybe fetch full accounts list
      note over Screen,Providers: Lazy load other accounts' contact models
    else Missing provider data
      Screen->>API: _loadAccountsFromApi()
      alt Success
        Screen->>Screen: Populate accounts + current account
      else Error
        Screen->>UI: Show error toast
      end
    end
  end

  rect #f6fff0
    note right of Screen: Provider updates
    Providers-->>Screen: Active account / metadata changed
    Screen->>Screen: _refreshAccountData()
    Screen->>UI: Update current account tile
  end

  rect #fff7ee
    note right of Screen: UI states
    Screen->>UI: Show loading indicator or error message as needed
  end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

Pre-merge checks (5 passed)

✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title Check ✅ Passed The pull request title “Fix account loading on general settings page” succinctly captures the main change by indicating that the account loading behavior on the general settings screen is corrected, and it is both concise and directly related to the core fix implemented in the changeset.
Linked Issues Check ✅ Passed The changes overhaul the account loading flow by introducing provider-driven lazy loading and loading-state flags, which prevents the transient “No accounts found” message and ensures accounts are displayed once available, directly addressing the symptom and expected behavior outlined in issue #608.
Out of Scope Changes Check ✅ Passed All modifications in the pull request are confined to the general_settings_screen.dart file and pertain exclusively to the account loading logic required to fix the transient “No accounts found” message, with no unrelated functionality or files affected.
Docstring Coverage ✅ Passed No functions found in the changes. Docstring coverage check skipped.

Poem

A whisk of code, a hop of state,
I nibble providers—loadings abate.
Lazy lists sprout where APIs once grew,
Errors go “poof!” with a toast or two.
Accounts now bloom without a fuss—
Happy hops from this diligent cuss. 🥕🐇

Tip

👮 Agentic pre-merge checks are now available in preview!

Pro plan users can now enable pre-merge checks in their settings to enforce checklists before merging PRs.

  • Built-in checks – Quickly apply ready-made checks to enforce title conventions, require pull request descriptions that follow templates, validate linked issues for compliance, and more.
  • Custom agentic checks – Define your own rules using CodeRabbit’s advanced agentic capabilities to enforce organization-specific policies and workflows. For example, you can instruct CodeRabbit’s agent to verify that API documentation is updated whenever API schema files are modified in a PR. Note: Upto 5 custom checks are currently allowed during the preview period. Pricing for this feature will be announced in a few weeks.

Please see the documentation for more information.

Example:

reviews:
  pre_merge_checks:
    custom_checks:
      - name: "Undocumented Breaking Changes"
        mode: "warning"
        instructions: |
          Pass/fail criteria: All breaking changes to public APIs, CLI flags, environment variables, configuration keys, database schemas, or HTTP/GraphQL endpoints must be documented in the "Breaking Change" section of the PR description and in CHANGELOG.md. Exclude purely internal or private changes (e.g., code not exported from package entry points or explicitly marked as internal).

Please share your feedback with us on this Discord post.

✨ Finishing Touches
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch fix-acc-loading

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@untreu2 untreu2 requested review from erskingardner and josefinalliende and removed request for erskingardner September 10, 2025 13:12
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
lib/ui/settings/general_settings_screen.dart (2)

373-403: Fix “multiple accounts” logic and stale list after logout

  • Condition > 2 is incorrect; should be > 1.
  • Using cached _accounts before logout gives the wrong count post-logout. Fetch the remaining accounts after logout to decide the flow and refresh the cache.
@@
-    // Use cached accounts instead of making another bridge call
-    final hasMultipleAccounts = _accounts.length > 2;
+    // Determine remaining accounts after logout
@@
-    await authNotifier.logoutCurrentAccount();
+    await authNotifier.logoutCurrentAccount();
@@
-    // Check the final auth state after logout
+    // Check the final auth state after logout
     final finalAuthState = ref.read(authProvider);
+    // Refresh accounts and compute remaining count
+    final remainingAccounts = await getAccounts();
+    final hasMultipleAccounts = remainingAccounts.length > 1;
+    if (mounted) {
+      setState(() {
+        _accounts = remainingAccounts;
+      });
+    }
@@
-      if (hasMultipleAccounts) {
+      if (hasMultipleAccounts) {
         await _loadFromProviders();

302-310: Add package:collection import or bump SDK for Iterable.firstOrNull

In lib/ui/settings/general_settings_screen.dart (lines 302 & 309), you call .firstOrNull without importing the extension from package:collection. This will fail on CI unless your Dart SDK lower bound includes the built-in firstOrNull. Either:

  • Add
    import 'package:collection/collection.dart';
    to that file, or
  • Raise your SDK constraint in pubspec.yaml to a version that natively provides Iterable.firstOrNull.
🧹 Nitpick comments (9)
lib/ui/settings/general_settings_screen.dart (9)

44-48: Consolidate loading state into a single UI state (Freezed union) managed by Riverpod

Three flags (_isLoadingAccounts, _loadingError, _isLoadingInProgress) are easy to desync. Prefer a GeneralSettingsState union (loading | ready | error) in a provider; the widget subscribes to that state. Keeps logic out of the widget and follows our “Use Riverpod + Freezed to manage UI states” guideline.


58-71: Guard noisy refreshes when metadata instances change but values don’t

previous.value.metadata != newMetadata may trigger on instance inequality even if content is equal. If metadata is a Map or data class without deep equality, this will cause redundant _refreshAccountData() calls. Consider value equality (e.g., a hash or package:collection DeepCollectionEquality) or expose a version/timestamp from the provider to compare.


83-118: Make loading flag transitions explicit inside provider-first path

When provider has no active account, _loadAccountsFromApi() sets loading=true. When the provider already has an account, we should also ensure loading=false (see prior diff). Additionally, consider returning early if activeAccountState is AsyncLoading to avoid kicking the API fallback too eagerly.


120-139: Add a simple reentrancy guard and error surfacing for lazy load

If this method is called twice quickly, it can race. Mirror the _isLoadingInProgress guard here or use a local _isLoadingAccountsList flag. Also consider bubbling errors to _loadingError to render an error state rather than silently debugPrint.

@@
-  Future<void> _loadAccountsLazilyIfNeeded() async {
-    if (_accounts.isEmpty) {
+  Future<void> _loadAccountsLazilyIfNeeded() async {
+    if (_accounts.isEmpty && !_isLoadingInProgress) {
+      _isLoadingInProgress = true;
       try {
         final accounts = await getAccounts();
         if (mounted) {
           setState(() {
             _accounts = accounts;
           });
           _loadContactModelsForOtherAccounts(accounts);
         }
       } catch (e) {
-        // Silently handle - account switching might not work but current account will show
-        debugPrint('Failed to load accounts list: $e');
+        debugPrint('Failed to load accounts list: $e');
+        if (mounted) {
+          setState(() => _loadingError = e.toString());
+        }
       }
+      _isLoadingInProgress = false;
     }
   }

140-163: Explicitly mark fire-and-forget futures

Use unawaited(...) (dart:async) or a comment to silence lints and document intent for the background loads.

@@
-        userProfileDataNotifier
+        // unawaited: background prefetch
+        userProfileDataNotifier
             .getUserProfileData(account.pubkey)

201-233: Remove unnecessary async and ensure loading flag is cleared after refresh

Method doesn’t await. Also ensure we end any loading state after applying fresh provider data.

-  void _refreshAccountData() async {
+  void _refreshAccountData() {
@@
-      setState(() {
-        _currentAccount = activeAccount;
-      });
+      setState(() {
+        _currentAccount = activeAccount;
+        _isLoadingAccounts = false;
+      });

264-281: Name should start with a verb

Follow guidelines: rename _accountToContactModel to buildContactModelForAccount and update call site.

-  ContactModel _accountToContactModel(Account account) {
+  ContactModel buildContactModelForAccount(Account account) {
@@
-    final contactModels = _accounts.map(_accountToContactModel).toList();
+    final contactModels = _accounts.map(buildContactModelForAccount).toList();

Also applies to: 280-281


316-320: Localize user-facing strings

Replace hardcoded English strings with AppLocalizations (e.g., error toasts and error label).

-              ref.showErrorToast('Account not found. Please try switching account again.');
+              ref.showErrorToast(AppLocalizations.of(context)!.accountNotFoundTryAgain);
@@
-                      const Center(child: Text('Error loading account'))
+                      Center(child: Text(AppLocalizations.of(context)!.errorLoadingAccount))

Also applies to: 451-453


279-327: Disable “Switch Account” when accounts not yet loaded

If _accounts is empty, the sheet opens blank and may lead to the “Account not found” toast. Consider disabling the button or showing a loading indicator until _accounts.isNotEmpty.

-                    WnFilledButton(
+                    WnFilledButton(
                       label: 'Switch Account',
                       size: WnButtonSize.small,
                       visualState: WnButtonVisualState.secondary,
-                      onPressed: () => _showAccountSwitcher(),
+                      onPressed: _accounts.isEmpty ? null : () => _showAccountSwitcher(),
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 3f96cf3 and 7ae93bb.

⛔ Files ignored due to path filters (1)
  • ios/Podfile.lock is excluded by !**/*.lock
📒 Files selected for processing (1)
  • lib/ui/settings/general_settings_screen.dart (6 hunks)
🧰 Additional context used
📓 Path-based instructions (1)
**/*.dart

📄 CodeRabbit inference engine (.cursor/rules/flutter.mdc)

**/*.dart: Always declare the type of each variable and function (parameters and return value). Avoid using 'any'. Create necessary types.
Don't leave blank lines within a function.
One export per file.
Use PascalCase for classes.
Use camelCase for variables, functions, and methods.
Use underscores_case for file and directory names.
Use UPPERCASE for environment variables. Avoid magic numbers and define constants.
Start each function with a verb.
Use verbs for boolean variables. Example: isLoading, hasError, canDelete, etc.
Use complete words instead of abbreviations and correct spelling, except for standard and well-known abbreviations (API, URL, i, j, err, ctx, req, res, next).
Write short functions with a single purpose. Less than 20 instructions.
Name functions with a verb and something else. If it returns a boolean, use isX or hasX, canX, etc. If it doesn't return anything, use executeX or saveX, etc.
Avoid nesting blocks by early checks and returns, or extraction to utility functions.
Use higher-order functions (map, filter, reduce, etc.) to avoid function nesting. Use arrow functions for simple functions (less than 3 instructions). Use named functions for non-simple functions.
Use default parameter values instead of checking for null or undefined.
Reduce function parameters using RO-RO: use an object to pass multiple parameters and to return results. Declare necessary types for input arguments and output.
Use a single level of abstraction in functions.
Don't abuse primitive types and encapsulate data in composite types.
Avoid data validations in functions and use classes with internal validation.
Prefer immutability for data. Use readonly for data that doesn't change. Use 'as const' for literals that don't change.
Declare interfaces to define contracts.
Write small classes with a single purpose. Less than 200 instructions, less than 10 public methods, less than 10 properties.
Use exceptions to handle errors you don't expect. If you catch an exception, it sh...

Files:

  • lib/ui/settings/general_settings_screen.dart
🔇 Additional comments (1)
lib/ui/settings/general_settings_screen.dart (1)

40-40: Good addition: cache contact models per account pubkey

This avoids repeat profile fetches and reduces UI jank when switching.

Comment on lines +53 to 55
_loadFromProviders();
_loadPackageInfo();
_activeAccountSubscription = ref.listenManual(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Prevent the initial “No accounts found” flash on first frame

Because loading starts in a post-frame callback, the first build shows “No accounts found” until _isLoadingAccounts flips. Start in loading=true and clear it as soon as provider data sets _currentAccount.

Apply this diff:

@@
-  bool _isLoadingAccounts = false;
+  bool _isLoadingAccounts = true;
@@
-      setState(() {
-        _currentAccount = activeAccount;
-      });
+      setState(() {
+        _currentAccount = activeAccount;
+        _isLoadingAccounts = false;
+      });

Also applies to: 449-454

Comment on lines +166 to +199
Future<void> _loadAccountsFromApi() async {
if (!mounted || _isLoadingInProgress) return;

_isLoadingInProgress = true;

setState(() {
_isLoadingAccounts = true;
_loadingError = null;
});

try {
final accounts = await getAccounts();
final activeAccountState = ref.read(activeAccountProvider);
final activeAccount = activeAccountState.value?.account;

if (mounted) {
setState(() {
_accounts = accounts;
_currentAccount = activeAccount;
_isLoadingAccounts = false;
});
}
} catch (e) {
if (mounted) {
setState(() {
_loadingError = e.toString();
_isLoadingAccounts = false;
});
ref.showErrorToast('Failed to load accounts: $e');
}
} finally {}
} finally {
_isLoadingInProgress = false;
}
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Fallback should still show a valid account instead of “No accounts found”

If the provider hasn’t resolved yet, _currentAccount stays null even when the accounts list exists, which reintroduces the “No accounts found” UI. Default _currentAccount to the first account as a temporary selection until the provider catches up.

@@
-        setState(() {
-          _accounts = accounts;
-          _currentAccount = activeAccount;
-          _isLoadingAccounts = false;
-        });
+        setState(() {
+          _accounts = accounts;
+          _currentAccount = activeAccount ?? (accounts.isNotEmpty ? accounts.first : null);
+          _isLoadingAccounts = false;
+        });
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
Future<void> _loadAccountsFromApi() async {
if (!mounted || _isLoadingInProgress) return;
_isLoadingInProgress = true;
setState(() {
_isLoadingAccounts = true;
_loadingError = null;
});
try {
final accounts = await getAccounts();
final activeAccountState = ref.read(activeAccountProvider);
final activeAccount = activeAccountState.value?.account;
if (mounted) {
setState(() {
_accounts = accounts;
_currentAccount = activeAccount;
_isLoadingAccounts = false;
});
}
} catch (e) {
if (mounted) {
setState(() {
_loadingError = e.toString();
_isLoadingAccounts = false;
});
ref.showErrorToast('Failed to load accounts: $e');
}
} finally {}
} finally {
_isLoadingInProgress = false;
}
}
Future<void> _loadAccountsFromApi() async {
if (!mounted || _isLoadingInProgress) return;
_isLoadingInProgress = true;
setState(() {
_isLoadingAccounts = true;
_loadingError = null;
});
try {
final accounts = await getAccounts();
final activeAccountState = ref.read(activeAccountProvider);
final activeAccount = activeAccountState.value?.account;
if (mounted) {
setState(() {
_accounts = accounts;
_currentAccount = activeAccount ?? (accounts.isNotEmpty ? accounts.first : null);
_isLoadingAccounts = false;
});
}
} catch (e) {
if (mounted) {
setState(() {
_loadingError = e.toString();
_isLoadingAccounts = false;
});
ref.showErrorToast('Failed to load accounts: $e');
}
} finally {
_isLoadingInProgress = false;
}
}
🤖 Prompt for AI Agents
In lib/ui/settings/general_settings_screen.dart around lines 166 to 199, the
current implementation leaves _currentAccount null when the provider hasn’t
resolved yet, causing the UI to show “No accounts found” even though accounts
were fetched; update the success path after fetching accounts so that if
activeAccount is null but the returned accounts list is non-empty, set
_currentAccount = accounts.first as a temporary/default selection (and keep
updating when the provider resolves), ensure this assignment happens inside the
mounted setState block and only when _currentAccount is null to avoid
overwriting a user selection.

Copy link
Contributor

@josefinalliende josefinalliende left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I hate to say this, but found it a bit strange that it required lazy loading, so I took a deeper look into the bug, and here are my thoughts: I think the real issue isn’t with loading the accounts for the switcher, but with maintaining a separate current account state instead of relying directly on the active account provider.

Fetching all accounts when the screen first renders, or when switching between accounts, isn’t an expensive operation. It doesn’t hit a server, it only queries the Rust crate with data already stored locally on the device. So, requesting the accounts every now and then shouldn’t be a problem.

The real problem iI think s that we’re waiting to load all accounts just to display the current one, when we could simply read the active account directly from its provider.

It took me a bit to review because I wanted to be sure I wasn’t talking nonsense 😅. I tested it locally in a branch, and it worked for me without changing the account loads. I left my explorations in a branch called explore/no-accounts-bug. #616 Could you take a look? I don’t want to step over your work, just sharing in case it helps, and maybe I’m missing something.

@josefinalliende
Copy link
Contributor

Another idea that comes to mind is that loading all accounts in the general settings screen might be unnecessary. It should be enough to load them within the switcher widget, only when that widget is rendered... But that could be handled as a refactor after the release, it’s not a priority right now, but it would be nice to keep this screen simpler and move the logic into the components that actually use the data.

Copy link
Contributor

@josefinalliende josefinalliende left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ Tried it locally and works, so lets merge this fix. I was wrong about that only watching the active account was needed, it was still needed to load accounts before rendering the switcher, thanks for taking the time to check my draft!

@untreu2 untreu2 merged commit 8c495b5 into master Sep 10, 2025
2 checks passed
@untreu2 untreu2 deleted the fix-acc-loading branch September 21, 2025 15:10
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

No Accounts Found

3 participants