Skip to content

Chore/3034123 - #664

Merged
leofelix077 merged 44 commits into
mainfrom
chore/3034123
Feb 19, 2026
Merged

leofelix077 merged 44 commits into
mainfrom
chore/3034123

Conversation

@leofelix077

@leofelix077 leofelix077 commented Jan 13, 2026 •

Copy link
Copy Markdown
Contributor

Main pain points:

  • Login taking forever - users waiting ~10-13s just to get into the app. Found two main culprits: network account discovery running on every login (~6-10s depending on network), and upfront derivation of all private keys (~7-9s)

  • Unnecessary network calls - account discovery was checking for funded accounts every single login even though the accounts are deterministic (same seed = same accounts). Extension only does this on wallet import, not login

  • Logout wiped everything - hitting logout cleared all the decrypted data, so next login had to redo all the expensive crypto operations from scratch. No "lock" concept like the extension has

Related findings while adjusting

  • Account switching showed stale data - when switching accounts you'd briefly see the old account's balances/history before new data loaded. Confusing and looked broken

  • Unfunded accounts spamming errors - newly created accounts that don't exist on-chain yet were still making history API calls that would just 400


Main Changes

  • Only discover accounts on first wallet import - cuts it from ~10s (depends on network) to under 1s for normal logins. Discovery still runs once when importing a wallet to find your funded accounts, just skips it on regular logins

  • Added LOCKED state - logout now locks instead of nuking everything. Encrypted keys stay on disk so unlocking takes ~0.5s instead of the full re-auth (~10-13s). Stays locked even if you close/reopen the app

  • Lazy key loading - private keys load on-demand when you first open an account instead of deriving all of them at login. Saves ~7-9s, and once cached switching back is instant

  • Clears stale data on account switch - immediately wipes old balances/history/prices when switching so you don't see the wrong account's data for a second while new stuff loads

  • Skips history calls for unfunded accounts - new accounts don't exist on-chain yet so we just skip the API call that would 400 anyway

  • Overall login - went from ~10-13s to ~0.5-1s (roughly 90-95% faster)

  • Account switching - stays fast at ~0.5-0.6s with cached keys, no network calls (apart from fetching balances and history) or re-derivation


Checklist

PR structure

  • This PR does not mix refactoring changes with feature changes (break it down into smaller PRs if not).
  • This PR has reasonably narrow scope (break it down into smaller PRs if not).
  • This PR includes relevant before and after screenshots/videos highlighting these changes.
  • I took the time to review my own PR.

Testing

  • These changes have been tested and confirmed to work as intended on Android.
  • These changes have been tested and confirmed to work as intended on iOS.
  • These changes have been tested and confirmed to work as intended on small iOS screens.
  • These changes have been tested and confirmed to work as intended on small Android screens.
  • I have tried to break these changes while extensively testing them.
  • This PR adds tests for the new functionality or fixes.

Release

  • This is not a breaking change.
  • This PR updates existing JSDocs when applicable.
  • This PR adds JSDocs to new functionalities.
  • I've checked with the product team if we should add metrics to these changes.
  • I've shared relevant before and after screenshots/videos highlighting these changes with the design team and they've approved the changes.

@leofelix077 leofelix077 self-assigned this Jan 13, 2026
@leofelix077 leofelix077 added wip work in progress don't review yet Work in Progress / Draft PR / Code Review adjustments being worked on labels Jan 13, 2026
@leofelix077
leofelix077 marked this pull request as ready for review January 14, 2026 14:27
Copilot AI review requested due to automatic review settings January 14, 2026 14:27

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

This pull request introduces a new LOCKED authentication status to improve the user experience when locking/unlocking the wallet app. The change distinguishes between session expiration (HASH_KEY_EXPIRED) and intentional app locking (LOCKED), preserving the temporary store and private keys during lock to enable faster unlocking.

Changes:

  • Added LOCKED auth status that preserves encrypted temporary store during logout
  • Implemented lazy loading of private keys on-demand instead of pre-loading all keys during sign-in
  • Optimized account discovery to skip network calls for accounts already stored locally
  • Added loading state during account switching to prevent showing stale data

Reviewed changes

Copilot reviewed 11 out of 11 changed files in this pull request and generated 15 comments.

Show a summary per file
File Description
src/config/types.ts Added new LOCKED auth status constant
src/config/constants.ts Added AUTH_STATUS storage key for persisting lock state
src/ducks/auth.ts Core authentication logic changes including LOCKED state handling, lazy private key loading, account switching improvements, and optimized account discovery
src/navigators/RootNavigator.tsx Updated routing logic to handle LOCKED status (includes debugging console.log statements)
src/providers/WalletKitProvider.tsx Added LOCKED status checks for wallet connection and transaction signing
src/hooks/useAuthCheck.ts Updated to skip auth checks when in LOCKED state
src/hooks/useWelcomeBanner.ts Added account switching tracking to prevent showing welcome banner during transitions
src/ducks/history.ts Skip history fetching for unfunded accounts (performance optimization)
src/components/screens/HomeScreen/HomeScreen.tsx Display loading screen during account switching
src/i18n/locales/en/translations.json Added error message for missing mnemonic phrase
src/i18n/locales/pt/translations.json Added Portuguese translation for missing mnemonic phrase error

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/navigators/RootNavigator.tsx Outdated
Comment thread src/ducks/auth.ts Outdated
Comment thread src/hooks/useWelcomeBanner.ts
Comment thread src/i18n/locales/pt/translations.json Outdated
Comment thread src/ducks/auth.ts
Comment thread src/ducks/auth.ts
Comment thread src/config/types.ts
Comment on lines +61 to +62
// User is locked. Hash key is expired but temporary store with private keys is preserved.
LOCKED: "LOCKED",

Copilot AI Jan 14, 2026

Copy link

Choose a reason for hiding this comment

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

The new LOCKED auth status and its associated behaviors (preserving temporary store on logout, handling unlock flow with shouldCreateTempStore parameter, lazy loading of private keys) lack test coverage. The existing test file tests/ducks/auth.test.ts should be updated to include tests for: 1) logout with LOCKED state preservation, 2) sign-in from LOCKED state, 3) getAuthStatus returning LOCKED when persisted, 4) lazy loading of private keys in getActiveAccount, and 5) account switching with the isSwitchingAccount flag.

Copilot uses AI. Check for mistakes.
Comment on lines +56 to +63
// Track when account switching completes and balances are loaded
useEffect(() => {
if (!isSwitchingAccount && !isLoadingBalances) {
setAccountSwitchCompleted(true);
} else if (isSwitchingAccount) {
setAccountSwitchCompleted(false);
}
}, [isSwitchingAccount, isLoadingBalances]);

Copilot AI Jan 14, 2026

Copy link

Choose a reason for hiding this comment

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

The new isSwitchingAccount parameter and accountSwitchCompleted state logic in useWelcomeBanner lack test coverage. Since other hooks in this codebase have comprehensive tests (as evidenced by tests/hooks/ directory), tests should be added to verify: 1) accountSwitchCompleted is set correctly based on isSwitchingAccount and isLoadingBalances, 2) welcome banner is not shown during account switching, and 3) welcome banner is shown after account switch completes.

Copilot uses AI. Check for mistakes.
Comment thread src/ducks/auth.ts
Comment thread src/ducks/auth.ts Outdated
@leofelix077

Copy link
Copy Markdown
Contributor Author

@CassioMG I adjusted the comments above. was gonna reply one by one, but decided to keep it in a single place

Adjusted the history refresh on account switch and importing new wallets. it was not clearing in all cases before;

added a UI block when user presses to switch accounts + auto dismiss the modal, so it cannot be pressed multiple times causing this weird in-between state;

adjusted the code to only clear the session data from the snippet only during full logout / new wallet import

no new findings from my side after testing on iOS Simulator + android device. let me know if can check again and if you find anything else. thanks a lot for checking it as well!

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 16 out of 16 changed files in this pull request and generated 4 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/ducks/auth.ts Outdated
Comment on lines +70 to +77
// Wait for all async operations to complete
await act(async () => {
await new Promise((resolve) => {
setTimeout(() => {
resolve(undefined);
}, 100);
});
});

Copilot AI Feb 10, 2026

Copy link

Choose a reason for hiding this comment

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

These tests rely on real setTimeout(..., 100/200) delays to let effects run. This slows the suite and can be flaky under CI load. The repo already uses jest.useFakeTimers() in other tests; consider switching to fake timers here and advancing time deterministically (and/or using waitFor).

Copilot uses AI. Check for mistakes.
Comment thread src/ducks/auth.ts
Comment on lines +1289 to +1306
if (shouldCreateTempStore) {
// Fresh login: Generate new hash key and create new temporary store
const newHashKey = await generateHashKey(password);
await secureDataStorage.setItem(
SENSITIVE_STORAGE_KEYS.HASH_KEY,
JSON.stringify(newHashKey),
);

await createTemporaryStore({
password,
mnemonicPhrase: keyExtraData.mnemonicPhrase,
activeKeyPair: {
publicKey: loadedKey.publicKey,
privateKey: loadedKey.privateKey,
accountName: account.name,
id: loadedKey.id,
},
});

Copilot AI Feb 10, 2026

Copy link

Choose a reason for hiding this comment

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

In signIn (fresh login path), a new hash key is generated and persisted, but createTemporaryStore() will generate and persist another hash key again (default shouldRefreshHashKey=true). This results in redundant/expensive key derivation on every login and makes the first newHashKey write unused. Consider letting createTemporaryStore be the single place that generates+persists the hash key, or pass shouldRefreshHashKey: false and reuse the already-generated hash key (by extending the helper signature).

Copilot uses AI. Check for mistakes.
Comment thread src/ducks/auth.ts
Comment thread src/components/screens/HomeScreen/AccountItemRow.tsx Outdated
@CassioMG

Copy link
Copy Markdown
Contributor

@leofelix077 it looks like the main issues are fixed now, the only thing I noticed is that now the account switch is feeling "laggy" on iOS because of the fullscreen spinner that appears for a fraction of a second while I believe the destination account data is being loaded. On my Android device it looks better because it is slower and consequentelly takes a few seconds to load the data before the fullscreen spinner disappears.

Could we fix it somehow? I have 2 possible suggestions in mind:

  1. Instead of immediately dismissing the accounts list sheet on tap we leave the account list sheet open while the other account is being loaded in the background and display the spinner inside the accounts list sheet, either as a "fullsheet" spinner or as a local spinner on top of the destination account row while making all rows disabled at the same time. Then only when the account data is loaded we dismiss the sheet.

  2. Immediately dismiss the accounts list sheet as it happens today but wait 0.5s before trying to show the fullscreen spinner. If the account loads in less than 0.5s then the spinner could be skipped. We should still always disable all account rows while the switch happens even if it takes less than 0.5s to load the account.

Wdyt? Of course those are only suggestions from the top of my head, feel free to use another solution you may think is a better fit

ScreenRecording_02-10-2026.14-06-30_1.MP4

@leofelix077

Copy link
Copy Markdown
Contributor Author

@CassioMG I tried with the full screen loading and the inline, directly on the row. spinner on the entire modal level dosnt look too good, kind of off centered. I liked the 2nd one best. wdyt?

Screen.Recording.2026-02-17.at.14.36.14.mov
Screen.Recording.2026-02-17.at.15.39.27.mov

@CassioMG

Copy link
Copy Markdown
Contributor

@CassioMG I tried with the full screen loading and the inline, directly on the row. spinner on the entire modal level dosnt look too good, kind of off centered. I liked the 2nd one best. wdyt?

@leofelix077 agree, I think the second option is looking good. In addition to displaying the spinner over the selected row, could we also decrease opacity of the selected row? Indicating that it has been selected/disabled

E.g. the row in orange below should have lower opacity:
Screenshot 2026-02-17 at 11 14 24

Wdyt?

@leofelix077

Copy link
Copy Markdown
Contributor Author

oh yes. think it looks smoother imo

Screen.Recording.2026-02-17.at.16.39.19.mov

@CassioMG

Copy link
Copy Markdown
Contributor

oh yes. think it looks smoother imo

@leofelix077 nice, thanks for adjusting it 👌

Could you review this Copilot comment to check if it's applicable?

I'll request Copilot review again to see if it finds something else

Other than that, I think we should be good to merge this PR

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 21 out of 21 changed files in this pull request and generated 7 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/ducks/auth.ts
Comment thread src/components/screens/HomeScreen/ManageAccounts.tsx
modalRef={bottomSheetRef}
handleCloseModal={handleCloseModal}
enablePanDownToClose={false}
enablePanDownToClose

Copilot AI Feb 17, 2026

Copy link

Choose a reason for hiding this comment

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

The enablePanDownToClose property was changed from false to true (line 135). This means users can now swipe down to dismiss the account management bottom sheet even while an account switch is in progress.

However, the dismissal logic at lines 109-112 already handles the case where account switching completes, so allowing manual dismissal during switching could create a race condition where the sheet is dismissed before the switch completes. Consider keeping enablePanDownToClose={false} or conditionally disabling it based on isAccountSwitching: enablePanDownToClose={!isSwitchingAccount}

Suggested change
enablePanDownToClose
enablePanDownToClose={!isSwitchingAccount}

Copilot uses AI. Check for mistakes.
Comment thread src/ducks/auth.ts
Comment on lines +1309 to +1320
// LOCKED state unlock: Generate new hash key and re-encrypt existing temporary store
// IMPORTANT: Generate key first but don't store it yet - reEncryptTemporaryStore
// needs the OLD hash key to decrypt before we can re-encrypt with the new one
const newHashKey = await generateHashKey(password);
await reEncryptTemporaryStore(newHashKey);

// Now store the new hash key (reEncryptTemporaryStore used it for encryption
// but retrieved the old key itself via getHashKey() for decryption)
await secureDataStorage.setItem(
SENSITIVE_STORAGE_KEYS.HASH_KEY,
JSON.stringify(newHashKey),
);

Copilot AI Feb 17, 2026

Copy link

Choose a reason for hiding this comment

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

The shouldCreateTempStore parameter logic has an issue. When unlocking from LOCKED state (shouldCreateTempStore = false), the code calls reEncryptTemporaryStore(newHashKey) at line 1313. However, reEncryptTemporaryStore needs the OLD hash key from storage to decrypt the temporary store. The problem is that at line 1312, a new hash key has been generated but not stored yet, which is correct. But there's no guarantee that the old hash key is still in storage and not expired, especially if the user took a long time to unlock.

Consider adding a validation check to ensure the old hash key exists and is valid before attempting re-encryption. If it's missing or expired, fall back to creating a new temporary store instead.

Suggested change
// LOCKED state unlock: Generate new hash key and re-encrypt existing temporary store
// IMPORTANT: Generate key first but don't store it yet - reEncryptTemporaryStore
// needs the OLD hash key to decrypt before we can re-encrypt with the new one
const newHashKey = await generateHashKey(password);
await reEncryptTemporaryStore(newHashKey);
// Now store the new hash key (reEncryptTemporaryStore used it for encryption
// but retrieved the old key itself via getHashKey() for decryption)
await secureDataStorage.setItem(
SENSITIVE_STORAGE_KEYS.HASH_KEY,
JSON.stringify(newHashKey),
);
// LOCKED state unlock: attempt to re-encrypt existing temporary store
// using the old hash key (from storage) and a newly generated hash key.
// If the old hash key is missing or expired, fall back to creating a new
// temporary store instead.
const existingHashKey = await getHashKey();
const isExistingHashKeyValid =
!!existingHashKey &&
typeof existingHashKey.createdAt === "number" &&
Date.now() - existingHashKey.createdAt < HASH_KEY_EXPIRATION_MS;
if (isExistingHashKeyValid) {
// Old hash key is still valid: re-encrypt the existing temporary store
// with a newly generated hash key.
const newHashKey = await generateHashKey(password);
await reEncryptTemporaryStore(newHashKey);
// Now store the new hash key (reEncryptTemporaryStore used it for encryption
// but retrieved the old key itself via getHashKey() for decryption)
await secureDataStorage.setItem(
SENSITIVE_STORAGE_KEYS.HASH_KEY,
JSON.stringify(newHashKey),
);
} else {
// Old hash key missing or expired: fall back to creating a new temporary store,
// same behavior as the fresh login path.
const newHashKey = await generateHashKey(password);
await secureDataStorage.setItem(
SENSITIVE_STORAGE_KEYS.HASH_KEY,
JSON.stringify(newHashKey),
);
await createTemporaryStore({
password,
mnemonicPhrase: keyExtraData.mnemonicPhrase,
activeKeyPair: {
publicKey: loadedKey.publicKey,
privateKey: loadedKey.privateKey,
accountName: account.name,
id: loadedKey.id,
},
shouldRefreshHashKey: false,
});
}

Copilot uses AI. Check for mistakes.
Comment thread src/ducks/auth.ts
Comment thread src/ducks/auth.ts
Comment thread src/components/sds/Button/index.tsx
@leofelix077
leofelix077 merged commit 8c10872 into main Feb 19, 2026
5 checks passed
@leofelix077
leofelix077 deleted the chore/3034123 branch February 19, 2026 00:08
@github-actions github-actions Bot mentioned this pull request Feb 24, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants