Skip to content

Clear sync state on user logout#2707

Open
gcsecsey wants to merge 29 commits intotrunkfrom
gcsecsey/stu-166-clear-sync-on-logout
Open

Clear sync state on user logout#2707
gcsecsey wants to merge 29 commits intotrunkfrom
gcsecsey/stu-166-clear-sync-on-logout

Conversation

@gcsecsey
Copy link
Copy Markdown
Contributor

@gcsecsey gcsecsey commented Mar 4, 2026

Related issues

Fixes STU-166

How AI was used in this PR

I used AI to research the existing logout flow, then implemented and iterated on the changes using it.

Proposed Changes

When a user logged out, sync-related Redux state was not cleared, and the stale data remained in memory. If a different user logged in, they could see leftover sync UI state from the previous session.

This PR adds a userLoggedOut Redux action that is dispatched either when logging out explicitly or when an invalid token is detected. Multiple slices and a store listener react to this:

  • syncOperations slice resets pullStates and pushStates to empty
  • sync slice clears the remote file tree cache
  • connectedSites slice resets modal and selection state
  • A store listener cancels active main-process sync operations, stops renderer-side push/pull pollers, and resets authenticated RTK Query caches

Testing Instructions

  • Log in to a WordPress.com account and connect a remote site
  • Start a push or pull operation
  • Log out
  • Log in again with the same user or a different one
  • Check that the Sync tab shows the logged out state
  • Log in again (same or different account) and check that initial pull/push states are shown
CleanShot.2026-03-04.at.16.41.21.mp4

Pre-merge Checklist

  • Have you checked for TypeScript, React or other console errors?

Dispatch a shared userLoggedOut action on logout and invalid token detection
that resets sync slices, cancels active operations, stops pollers, and clears
authenticated RTK Query caches.
@gcsecsey gcsecsey self-assigned this Mar 4, 2026
@gcsecsey gcsecsey requested a review from a team March 4, 2026 16:54
@gcsecsey gcsecsey marked this pull request as ready for review March 4, 2026 16:54
Copy link
Copy Markdown
Contributor

@katinthehatsite katinthehatsite left a comment

Choose a reason for hiding this comment

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

Thanks for making the changes @gcsecsey - nice work!

I found some issues on my end when testing this PR. This is what I saw on my end: I started the pull operation and waited until the downloading backup step. Then, I logged out and noticed that some of the buttons were stuck in the "Importing" state:

Image

(see the top right corner)

Then, when I log in as a different user, I see the following issue on the import screen:

Image

When importSite fails during a sync pull, the pull Redux state stays at
'importing' while the import context fires an error event. Since
canCancelPull only allows cancellation for 'in-progress' and
'downloading' states, the user gets stuck with a disabled dismiss button.

Add an isError flag to ImportProgressState and set it on import errors.
In the sync UI, detect when a pull is in 'importing' state but the
import has failed, and treat it as a pull error so the user can dismiss
it and retry.
@gcsecsey
Copy link
Copy Markdown
Contributor Author

Thanks for making the changes @gcsecsey - nice work!

I found some issues on my end when testing this PR. This is what I saw on my end: I started the pull operation and waited until the downloading backup step. Then, I logged out and noticed that some of the buttons were stuck in the "Importing" state:

Image (see the top right corner)

Then, when I log in as a different user, I see the following issue on the import screen:

Image

Thanks for the review @katinthehatsite, great catch! This was caused by the import/export also being tracked in the ImportExportProvider context, not just Redux. I set this to also be reset on logout.

I also encountered a stuck state when trying to pull a site that caused a db import issue:

image

I updated the component to treat this state as a pull error as well, to unblock this state. However, I'm not sure if this is the best approach to handle this. I think ideally we'd show the pull operation as completed, and separately also show the db import error in a dialog, similarly to how it's done when importing, right?

@wpmobilebot
Copy link
Copy Markdown
Collaborator

wpmobilebot commented Mar 13, 2026

📊 Performance Test Results

Comparing d0ffd83 vs trunk

app-size

Metric trunk d0ffd83 Diff Change
App Size (Mac) 1409.57 MB 1409.59 MB +0.01 MB ⚪ 0.0%

site-editor

Metric trunk d0ffd83 Diff Change
load 1483 ms 1482 ms 1 ms ⚪ 0.0%

site-startup

Metric trunk d0ffd83 Diff Change
siteCreation 8084 ms 8079 ms 5 ms ⚪ 0.0%
siteStartup 4941 ms 4941 ms 0 ms ⚪ 0.0%

Results are median values from multiple test runs.

Legend: 🟢 Improvement (faster) | 🔴 Regression (slower) | ⚪ No change (<50ms diff)

@gcsecsey gcsecsey requested review from a team and katinthehatsite March 13, 2026 15:10
Comment thread apps/studio/src/stores/index.ts Outdated
Comment thread apps/studio/src/stores/index.ts Outdated
@gcsecsey gcsecsey requested a review from fredrikekelund March 17, 2026 10:54
Copy link
Copy Markdown
Contributor

@katinthehatsite katinthehatsite left a comment

Choose a reason for hiding this comment

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

Thanks for making the changes @gcsecsey ! I was testing the flow a bit more but now I am getting strange session expired messages:

Image

If I do manage to log in, I am getting the errors related to not being logged in while doing the push or pull:

Image

I did not debug the code for this but wanted to point this out early in the review.

@gcsecsey
Copy link
Copy Markdown
Contributor Author

Thanks for making the changes @gcsecsey ! I was testing the flow a bit more but now I am getting strange session expired messages:

Image If I do manage to log in, I am getting the errors related to not being logged in while doing the push or pull: Image I did not debug the code for this but wanted to point this out early in the review.

Thanks for testing again @katinthehatsite! I could reproduce this issue, and found that it was caused by the wpcomClient being cleared after the logout event being dispatched, causing refetches to run into the error.

I also found that even though I added the listener earlier for clearing the state, these weren't firing because the listener iterated on the updated (empty) state.

I fixed both of these issues now and the whole pull flow seems to be working well on my end:

CleanShot.2026-03-19.at.13.54.07.mp4

I'm still working to fix an issue with cancelling pushes, then I'll ask for another review.

…tomattic/studio into gcsecsey/stu-166-clear-sync-on-logout
…logout

Extracted PUSH_POLLERS/PULL_POLLERS maps and stop functions into a shared
sync-pollers module. Cancel thunks now call stopPushPoller/stopPullPoller
directly, so in-flight poll requests see signal.aborted and exit silently
instead of showing error modals.
@gcsecsey
Copy link
Copy Markdown
Contributor Author

@katinthehatsite @fredrikekelund I think this is ready for another review. I fixed these issues for the push flow;

I reverted emptying the connected sites cache on logout to avoid resetting the connected site list when logging back in:

CleanShot.2026-03-19.at.14.02.16.mp4

I also updated the pollers to avoid receiving errors when logging out at a later step in the push process, when the changes are being processed on the server:

CleanShot.2026-03-19.at.14.40.35.mp4

Copy link
Copy Markdown
Contributor

@fredrikekelund fredrikekelund left a comment

Choose a reason for hiding this comment

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

I took another look at the Linear issue, and it doesn't appear like these changes fully address the issue.

If I have two users with access to the same WP.com site, initiate a push to the site with my first account, log out in Studio, then log in with my second account (which has also connected the same WP.com site to my local site), then I still see Push cancelled in the interface. One step better than seeing an error message, but the UI should really be completely cleared.

Comment thread apps/studio/src/components/auth-provider.tsx Outdated
Comment thread apps/studio/src/stores/sync/sync-operations-slice.ts
Copy link
Copy Markdown
Contributor

@katinthehatsite katinthehatsite left a comment

Choose a reason for hiding this comment

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

I tried testing the changes again but I am still seeing the error with pushing a site when I log in and try to do the push operation:

Image

I tried with a couple of different users but I keep on running into the same error.

gcsecsey and others added 6 commits April 9, 2026 13:40
…u-166-clear-sync-on-logout

# Conflicts:
#	apps/studio/src/components/auth-provider.tsx
…u-166-clear-sync-on-logout

# Conflicts:
#	apps/studio/src/stores/sync/connected-sites.ts
#	apps/studio/src/stores/sync/sync-operations-slice.ts
…e.ts

Consolidates all poller infrastructure (AbortController maps, start/stop
functions, pollable checks) into the slice alongside the thunks that use
them. Pollers are now started explicitly from pushSiteThunk, pullSiteThunk,
and initializeSyncStatesThunk instead of reactively via listener middleware.
Removes sync-pollers.ts and ~130 lines from stores/index.ts.
…tomattic/studio into gcsecsey/stu-166-clear-sync-on-logout
@gcsecsey
Copy link
Copy Markdown
Contributor Author

I tried testing the changes again but I am still seeing the error with pushing a site when I log in and try to do the push operation:
Image

I tried with a couple of different users but I keep on running into the same error.

I could reproduce this issue, but I could also reproduce it on trunk and found that it's unrelated to these changes. We're fetching the latest WP-CLI phar and sqlite-command release, but the latest sqlite-command requires a newer WP-CLI version than the latest.

Temporarily, I could resolve this by updating the post-install script to fetch the nightly version:

diff --git a/scripts/download-wp-server-files.ts b/scripts/download-wp-server-files.ts
index fe9ca6cc..41b07da7 100644
--- a/scripts/download-wp-server-files.ts
+++ b/scripts/download-wp-server-files.ts
@@ -35,7 +35,7 @@ const FILES_TO_DOWNLOAD: FileToDownload[] = [
        {
                name: 'wp-cli',
                description: 'WP-CLI phar file',
-               getUrl: () => 'https://raw.githubusercontent.com/wp-cli/builds/gh-pages/phar/wp-cli.phar',
+               getUrl: () => 'https://raw.githubusercontent.com/wp-cli/builds/gh-pages/phar/wp-cli-nightly.phar',
                destinationPath: path.join( WP_SERVER_FILES_PATH, 'wp-cli' ),
        },
        {

@katinthehatsite could you try testing again using this diff? After applying that, I didn't run into any other issues regarding logout.

Copy link
Copy Markdown
Contributor

@katinthehatsite katinthehatsite left a comment

Choose a reason for hiding this comment

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

Log in again (same or different account) and check that initial pull/push states are shown

I am re-testing this again and when I log back in, I am seeing Push cancelled even though it was in progress when I logged out:

Image

Just to confirm, is this the expected behaviour?

Copy link
Copy Markdown
Contributor

@katinthehatsite katinthehatsite left a comment

Choose a reason for hiding this comment

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

And another sidenote is that file it shows Push cancelled on the Sync tab, I am seeing in Import/Export tab that the sync is in progress:

Image

@gcsecsey
Copy link
Copy Markdown
Contributor Author

Just to confirm, is this the expected behaviour?

Good catch, sorry for missing this earlier. No, I think it'd be better behavior to completely clear the state in this case, and not show the success message.

And another sidenote is that file it shows Push cancelled on the Sync tab, I am seeing in Import/Export tab that the sync is in progress:

Thanks for catching, this is also clearly a bug. I'll also address this one.

@gcsecsey
Copy link
Copy Markdown
Contributor Author

I fixed both issues, now the state is completely reset on logout:

CleanShot.2026-04-15.at.17.22.46.mp4

@gcsecsey
Copy link
Copy Markdown
Contributor Author

@katinthehatsite @fredrikekelund could you please give this another look when you have some time? Thanks! 🙏

@fredrikekelund
Copy link
Copy Markdown
Contributor

@gcsecsey, yep, I'll take another look later today. Sorry for the delay

Copy link
Copy Markdown
Contributor

@fredrikekelund fredrikekelund left a comment

Choose a reason for hiding this comment

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

This issue is really surprisingly complex… 🤔 I raised a point about how we handle active import operations when a user logs out. I think it's OK if this PR doesn't have a perfect solution to that problem, but we should at least be explicit in the code and acknowledge that we don't/can't abort those operations.

Also, I'd like to clarify how the sync operation state gets updated when displayNotification=true in the cancel{Push,Pull}Thunk functions.

Comment thread apps/studio/src/components/content-tab-import-export.tsx Outdated
Comment on lines +82 to +89
const { isAuthenticated } = useAuth();

useEffect( () => {
if ( ! isAuthenticated ) {
setImportState( {} );
setExportState( {} );
}
}, [ isAuthenticated ] );
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.

The cancelPull thunk doesn't abort import operations – it only aborts the initial API call, the polling, and the downloading. So if the backup archive has already been fetched from the server when the user logs out, the userLoggedOut Redux middleware does nothing.

That means there's no real point in resetting the import state in the renderer in this way (if there's an ongoing import operation), because the renderer process will continue to receive on-import events from the main process that repopulate the state.

This logic might be needed to get rid of the Error pulling changes message described in the original issue, but it seems sensible to only run this when there's no active pull operation.

We could actually take this discussion further, too, because I feel like we haven't really discussed this constraint before, and that it kind of breaks our existing logic that groups connected sites by WPCOM user. It effectively means we cannot (or shouldn't) always stop a pull operation when a user logs out.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Thanks for the additional context here @fredrikekelund! I get that it doesn't accomplish much to only reset the state in the renderer when we have an ongoing import, but I'd also be hesitant to completely cancel the import. Regarding grouping sites by WPCOM user, I think we should let the import continue if it's already started by the time the user logs out, as the import doesn't rely on authentication.

I think your suggestion makes sense, to only run this when there's no active pull operation. That'd mean that we let the import complete, and then clear the state.

Comment on lines +464 to +473
// See cancelPushThunk note: skip "cancelled" UI state for silent cancels (logout cleanup).
if ( displayNotification ) {
dispatch(
syncOperationsActions.updatePullState( {
selectedSiteId,
remoteSiteId,
state: { status: getPullStatesProgressInfo().cancelled },
} )
);
}
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.

So what happens instead..? Which part of the code updates this slice when a user logs out? Maybe it would be clearer if displayNotification didn't determine how we update the state, and we added an explicit call to reset the sync operation state in the userLoggedOut middleware. Thoughts, @gcsecsey?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

The slice is reset via a reducer here on logout. I agree that using the displayNotification both for controlling the notification and the state update is a bit confusing. In the original commit of this PR, we had the explicit cancellation in the middleware, and later moved it to the thunks. The PUSH/PULL pollers were exported in the meantime to stopPushPoller/stopPullPoller, so we could use those and move the cancellation back to the middleware.

Comment on lines +437 to +444
if ( displayNotification ) {
dispatch(
syncOperationsActions.updatePushState( {
selectedSiteId,
remoteSiteId,
state: { status: getPushStatesProgressInfo().cancelled },
} )
);
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.

Same question here as in cancelPullThunk: which part of the code updates the sync operation state when a user logs out?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Yes, this is the same as above, it's actually cleared here.

gcsecsey added 4 commits May 6, 2026 10:31
…tomattic/studio into gcsecsey/stu-166-clear-sync-on-logout
Replace per-operation cancel thunk dispatches with a single
abortAllSyncOperations() helper. The slice's addCase(userLoggedOut)
already resets pullStates/pushStates, so the listener only needs to
abort in-flight side effects (pollers, push upload aborts, main-process
operations) — no state dispatches required.

This removes the displayNotification param from cancelPush/cancelPull,
which was overloaded to gate both the toast and the post-logout state
re-population. The thunks are now purely user-initiated cancel paths.
An active pull's import phase is driven by main-process events that
keep emitting after logout, so resetting importState here was futile —
it would just get repopulated. Snapshot the pre-logout pull-active
flag via a ref (pullStates is reset synchronously on userLoggedOut, so
the post-logout selector value is already false), and only clear when
no pull was active.

The hook now reads from the Redux store, so the test wrapper needs a
Provider too.
Conflicts in apps/studio/src/hooks/use-import-export.tsx and the deletion
of its test file: trunk's #3129 refactored the hook to depend on the CLI
for import/export, removing Sentry, useSiteDetails, error formatting,
and the BackupArchiveInfo path. Took trunk's simpler structure and
re-applied the logout cleanup block (useAuth + useRootSelector +
useRef-based pre-logout pull-active snapshot driving the
setImportState/setExportState reset).

Dropped the isError flag on ImportProgressState and the matching
pullImportFailed workaround in sync-connected-sites.tsx: trunk's pull
thunk now propagates importSite errors via pullStates.failed, so the
"dismiss button stuck disabled" bug those addressed is no longer
reachable. Accepted trunk's deletion of use-import-export.test.tsx as
part of the legacy-test cleanup.
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.

4 participants