Skip to content
Merged
Show file tree
Hide file tree
Changes from 29 commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
d47341d
Clear sync-related Redux state when user logs out
gcsecsey Mar 4, 2026
c680af7
Treat failed import during sync pull as a pull error
gcsecsey Mar 13, 2026
e667920
Simplify logout cleanup in ImportExportProvider to use direct isAuthe…
gcsecsey Mar 13, 2026
99884e0
Merge branch 'trunk' of github.com:Automattic/studio into gcsecsey/st…
gcsecsey Mar 13, 2026
7cf6787
Merge branch 'trunk' into gcsecsey/stu-166-clear-sync-on-logout
gcsecsey Mar 13, 2026
f816995
Use cancel thunks for sync cleanup on logout instead of manual cancel…
gcsecsey Mar 16, 2026
0c6e92f
Merge branch 'trunk' into gcsecsey/stu-166-clear-sync-on-logout
gcsecsey Mar 16, 2026
0d93adc
Merge branch 'trunk' into gcsecsey/stu-166-clear-sync-on-logout
gcsecsey Mar 17, 2026
625fc86
Use getOriginalState() in userLoggedOut listener to fix cancel thunks…
gcsecsey Mar 19, 2026
677d49d
Clear wpcomClient before dispatching userLoggedOut to prevent error m…
gcsecsey Mar 19, 2026
20bde45
Merge branch 'trunk' into gcsecsey/stu-166-clear-sync-on-logout
gcsecsey Mar 19, 2026
34bd922
keep connected sites cache on logout
gcsecsey Mar 19, 2026
11a1a05
Merge branch 'gcsecsey/stu-166-clear-sync-on-logout' of github.com:Au…
gcsecsey Mar 19, 2026
58e5ecf
Abort poller signals in cancel thunks to prevent error modals during …
gcsecsey Mar 19, 2026
e09fa02
Move setWpcomClient into userLoggedOut listener
gcsecsey Mar 20, 2026
42fcee6
Merge branch 'trunk' of github.com:Automattic/studio into gcsecsey/st…
gcsecsey Apr 9, 2026
86cb91a
Merge branch 'trunk' of github.com:Automattic/studio into gcsecsey/st…
gcsecsey Apr 9, 2026
ad7bc86
Move sync poller logic from stores/index.ts into sync-operations-slic…
gcsecsey Apr 9, 2026
a833912
Merge branch 'trunk' into gcsecsey/stu-166-clear-sync-on-logout
gcsecsey Apr 10, 2026
d94ad64
Merge branch 'trunk' of github.com:Automattic/studio into gcsecsey/st…
gcsecsey Apr 10, 2026
b4603f5
Merge branch 'gcsecsey/stu-166-clear-sync-on-logout' of github.com:Au…
gcsecsey Apr 10, 2026
8e1a594
Avoid setting cancelled state when logging out
gcsecsey Apr 15, 2026
e32c175
Enable import/export while push is running remotely
gcsecsey Apr 15, 2026
1ea5e8f
Saving uncommitted changes before archiving
Apr 17, 2026
e5f06f8
Merge branch 'trunk' into gcsecsey/stu-166-clear-sync-on-logout
gcsecsey Apr 22, 2026
9490492
Merge branch 'gcsecsey/stu-166-clear-sync-on-logout' of github.com:Au…
gcsecsey May 6, 2026
8a04a4b
Abort sync operations directly from userLoggedOut listener
gcsecsey May 6, 2026
2b4a33e
Skip import/export reset on logout if a pull is in flight
gcsecsey May 6, 2026
d0ffd83
Merge branch 'trunk' into gcsecsey/stu-166-clear-sync-on-logout
gcsecsey May 6, 2026
3149b6b
Merge branch 'trunk' of github.com:Automattic/studio into gcsecsey/st…
gcsecsey Jun 1, 2026
337d9e3
Remove protocol-handler leftovers accidentally swept in by Conductor …
gcsecsey Jun 1, 2026
f57a08e
Move local-sync-work selectors into syncOperationsSelectors
gcsecsey Jun 1, 2026
dceb859
Merge branch 'trunk' of github.com:Automattic/studio into gcsecsey/st…
gcsecsey Jun 2, 2026
912d789
Handle logout during a sync pull cleanly (no error dialog, no orphane…
gcsecsey Jun 2, 2026
e852ab8
Move logic into Redux slice selectors
fredrikekelund Jun 3, 2026
76977ec
Corrections
fredrikekelund Jun 3, 2026
831efb6
Correction
fredrikekelund Jun 3, 2026
a73a48e
Merge branch 'trunk' into gcsecsey/stu-166-clear-sync-on-logout
fredrikekelund Jun 3, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions apps/studio/__mocks__/electron.ts
Comment thread
gcsecsey marked this conversation as resolved.
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ export const app = {
on: vi.fn(),
off: vi.fn(),
setAppLogsPath: vi.fn(),
removeAsDefaultProtocolClient: vi.fn(),
setAsDefaultProtocolClient: vi.fn(),
enableSandbox: vi.fn(),
commandLine: {
Expand Down
12 changes: 7 additions & 5 deletions apps/studio/src/components/auth-provider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@ import { useOffline } from 'src/hooks/use-offline';
import { getIpcApi } from 'src/lib/get-ipc-api';
import { isInvalidTokenError } from 'src/lib/is-invalid-oauth-token-error';
import { setSentryWpcomUserIdRenderer } from 'src/lib/renderer-sentry-utils';
import { useI18nLocale } from 'src/stores';
import { useAppDispatch, useI18nLocale } from 'src/stores';
import { userLoggedOut } from 'src/stores/auth-actions';
import { setWpcomClient } from 'src/stores/wpcom-api';
import type { WPCOM } from 'wpcom/types';

Expand Down Expand Up @@ -46,21 +47,22 @@ const AuthProvider: React.FC< AuthProviderProps > = ( { children } ) => {
const { __ } = useI18n();
const isOffline = useOffline();

const dispatch = useAppDispatch();
const authenticate = useCallback( () => getIpcApi().authenticate(), [] );

const handleInvalidToken = useCallback( async () => {
try {
void getIpcApi().logRendererMessage( 'info', 'Detected invalid token. Logging out.' );
dispatch( userLoggedOut() );
await getIpcApi().clearAuthenticationToken();
setIsAuthenticated( false );
setClient( undefined );
setWpcomClient( undefined );
setUser( undefined );
setSentryWpcomUserIdRenderer( undefined );
} catch ( err ) {
console.error( 'Failed to handle invalid token:', err );
}
}, [] );
}, [ dispatch ] );

useIpcListener( 'auth-updated', ( _event, payload ) => {
if ( 'error' in payload ) {
Expand Down Expand Up @@ -121,16 +123,16 @@ const AuthProvider: React.FC< AuthProviderProps > = ( { children } ) => {
}

try {
dispatch( userLoggedOut() );
await getIpcApi().clearAuthenticationToken();
setIsAuthenticated( false );
setClient( undefined );
setWpcomClient( undefined );
setUser( undefined );
setSentryWpcomUserIdRenderer( undefined );
} catch ( err ) {
console.error( err );
}
}, [ client, isOffline ] );
}, [ client, dispatch, isOffline ] );

useEffect( () => {
async function run() {
Expand Down
32 changes: 23 additions & 9 deletions apps/studio/src/components/content-tab-import-export.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,10 @@ import { useConfirmationDialog } from 'src/hooks/use-confirmation-dialog';
import { useDragAndDropFile } from 'src/hooks/use-drag-and-drop-file';
import { useImportExport } from 'src/hooks/use-import-export';
import { useSiteDetails } from 'src/hooks/use-site-details';
import {
pullBackupIsDownloadingOrImporting,
pushBackupIsUploading,
} from 'src/lib/active-sync-operations';
import { cx } from 'src/lib/cx';
import { getIpcApi } from 'src/lib/get-ipc-api';
import { useRootSelector } from 'src/stores';
Expand Down Expand Up @@ -344,17 +348,27 @@ export function ContentTabImportExport( { selectedSite }: ContentTabImportExport
localSiteId: selectedSite.id,
userId: user?.id,
} );
const isPulling = useRootSelector( ( state ) =>
connectedSites.some( ( site ) =>
syncOperationsSelectors.selectIsSiteIdPulling( selectedSite.id, site.id )( state )
)
const isPullingLocally = useRootSelector( ( state ) =>
connectedSites.some( ( site ) => {
const pullState = syncOperationsSelectors.selectPullState(
selectedSite.id,
site.id
)( state );
return pullBackupIsDownloadingOrImporting( pullState?.status.key );
} )
);
const isPushing = useRootSelector( ( state ) =>
connectedSites.some( ( site ) =>
syncOperationsSelectors.selectIsSiteIdPushing( selectedSite.id, site.id )( state )
)
// Only block import/export while the local machine is actively involved in sync.
// After the backup upload completes, the push continues remotely and should not block import/export.
const isUploadingPushBackup = useRootSelector( ( state ) =>
connectedSites.some( ( site ) => {
const pushState = syncOperationsSelectors.selectPushState(
selectedSite.id,
site.id
)( state );
return pushBackupIsUploading( pushState?.status.key );
} )
);
const isThisSiteSyncing = isPulling || isPushing;
const isThisSiteSyncing = isPullingLocally || isUploadingPushBackup;

useEffect( () => {
getIpcApi()
Expand Down
20 changes: 15 additions & 5 deletions apps/studio/src/components/publish-site-button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,10 @@ import { useI18n } from '@wordpress/react-i18n';
import { useCallback } from 'react';
import { useAuth } from 'src/hooks/use-auth';
import { useSiteDetails } from 'src/hooks/use-site-details';
import {
pullBackupIsDownloadingOrImporting,
pushBackupIsUploading,
} from 'src/lib/active-sync-operations';
import { generateCheckoutUrl } from 'src/lib/generate-checkout-url';
import { getIpcApi } from 'src/lib/get-ipc-api';
import { ConnectButton } from 'src/modules/sync/components/connect-button';
Expand All @@ -18,9 +22,15 @@ export const PublishSiteButton = () => {
localSiteId: selectedSite?.id,
userId: user?.id,
} );
const isAnySitePulling = useRootSelector( syncOperationsSelectors.selectIsAnySitePulling );
const isAnySitePushing = useRootSelector( syncOperationsSelectors.selectIsAnySitePushing );
const isAnySiteSyncing = isAnySitePulling || isAnySitePushing;
const isAnySiteDoingLocalSyncWork = useRootSelector( ( state ) => {
const anyPullLocal = Object.values( syncOperationsSelectors.selectPullStates( state ) ).some(
( pullState ) => pullBackupIsDownloadingOrImporting( pullState.status.key )
);
const anyPushLocal = Object.values( syncOperationsSelectors.selectPushStates( state ) ).some(
( pushState ) => pushBackupIsUploading( pushState.status.key )
);
return anyPullLocal || anyPushLocal;
} );

const handlePublishClick = useCallback( () => {
if ( ! selectedSite ) return;
Expand All @@ -36,9 +46,9 @@ export const PublishSiteButton = () => {
variant="primary"
icon={ cloudUpload }
connectSite={ handlePublishClick }
disabled={ isAnySiteSyncing }
disabled={ isAnySiteDoingLocalSyncWork }
tooltipText={
isAnySiteSyncing
isAnySiteDoingLocalSyncWork
? __(
'Another site is syncing. Please wait for the sync to finish before you publish your site.'
)
Expand Down
32 changes: 24 additions & 8 deletions apps/studio/src/components/settings-site-menu.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
import { MenuItem } from '@wordpress/components';
import { useSiteDetails } from 'src/hooks/use-site-details';
import {
pullBackupIsDownloadingOrImporting,
pushBackupIsUploading,
} from 'src/lib/active-sync-operations';
import { useRootSelector } from 'src/stores';
import { syncOperationsSelectors } from 'src/stores/sync';

Expand All @@ -15,18 +19,30 @@ export const SettingsMenuItem = ( {
isDestructive = false,
}: SettingsMenuItemProps ) => {
const { isDeleting, sites, selectedSite } = useSiteDetails();
const isPulling = useRootSelector(
syncOperationsSelectors.selectIsSiteIdPulling( selectedSite?.id ?? '' )
);
const isPushing = useRootSelector(
syncOperationsSelectors.selectIsSiteIdPushing( selectedSite?.id ?? '' )
);
const selectedSiteId = selectedSite?.id;
const isThisSiteDoingLocalSyncWork = useRootSelector( ( state ) => {
if ( ! selectedSiteId ) {
return false;
}
const pullStates = syncOperationsSelectors.selectPullStates( state );
const pushStates = syncOperationsSelectors.selectPushStates( state );
const anyPullLocal = Object.values( pullStates ).some(
( pullState ) =>
pullState.selectedSite?.id === selectedSiteId &&
pullBackupIsDownloadingOrImporting( pullState.status.key )
);
const anyPushLocal = Object.values( pushStates ).some(
( pushState ) =>
pushState.selectedSite?.id === selectedSiteId &&
pushBackupIsUploading( pushState.status.key )
);
return anyPullLocal || anyPushLocal;
} );
if ( ! selectedSite ) {
return null;
}
const isThisSiteSyncing = isPulling || isPushing;
const isAddingSite = sites.some( ( site ) => site.isAddingSite );
const isDisabled = isDeleting || isThisSiteSyncing || isAddingSite;
const isDisabled = isDeleting || isThisSiteDoingLocalSyncWork || isAddingSite;

return (
<MenuItem
Expand Down
18 changes: 13 additions & 5 deletions apps/studio/src/components/site-management-actions.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { ActionButton } from 'src/components/action-button';
import { PublishSiteButton } from 'src/components/publish-site-button';
import { Tooltip } from 'src/components/tooltip';
import { useImportExport } from 'src/hooks/use-import-export';
import { pullBackupIsDownloadingOrImporting } from 'src/lib/active-sync-operations';
import { useRootSelector } from 'src/stores';
import { syncOperationsSelectors } from 'src/stores/sync';

Expand All @@ -22,19 +23,26 @@ export const SiteManagementActions = ( {
}: SiteManagementActionProps ) => {
const { __ } = useI18n();
const { isSiteImporting } = useImportExport();
const isPulling = useRootSelector(
syncOperationsSelectors.selectIsSiteIdPulling( selectedSite?.id ?? '' )
);
const isPullingLocally = useRootSelector( ( state ) => {
if ( ! selectedSite ) {
return false;
}
return Object.values( syncOperationsSelectors.selectPullStates( state ) ).some(
( pullState ) =>
pullState.selectedSite?.id === selectedSite.id &&
pullBackupIsDownloadingOrImporting( pullState.status.key )
);
} );

if ( ! selectedSite ) {
return null;
}

const isImporting = isSiteImporting( selectedSite.id );
const disabled = isImporting || isPulling;
const disabled = isImporting || isPullingLocally;

let buttonLabelOnDisabled: string = __( 'Importing…' );
if ( isPulling ) {
if ( isPullingLocally ) {
buttonLabelOnDisabled = __( 'Pulling…' );
}

Expand Down
34 changes: 33 additions & 1 deletion apps/studio/src/hooks/use-import-export.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,20 @@ import {
ValidatorEvents,
} from '@studio/common/lib/import-export-events';
import { __, sprintf } from '@wordpress/i18n';
import { createContext, useMemo, useState, useCallback, useContext } from 'react';
import {
createContext,
useEffect,
useMemo,
useRef,
useState,
useCallback,
useContext,
} from 'react';
import { useAuth } from 'src/hooks/use-auth';
import { useIpcListener } from 'src/hooks/use-ipc-listener';
import { getIpcApi } from 'src/lib/get-ipc-api';
import { useRootSelector } from 'src/stores';
import { syncOperationsSelectors } from 'src/stores/sync/sync-operations-slice';

export type ImportProgressState = {
[ siteId: string ]: {
Expand Down Expand Up @@ -65,6 +76,27 @@ const WP_CONTENT_TYPE_LABELS: Record< string, string > = {
export const ImportExportProvider = ( { children }: { children: React.ReactNode } ) => {
const [ importState, setImportState ] = useState< ImportProgressState >( {} );
const [ exportState, setExportState ] = useState< ExportProgressState >( {} );
const { isAuthenticated } = useAuth();
const isAnyPullActive = useRootSelector( syncOperationsSelectors.selectIsAnySitePulling );

// Snapshot the latest pre-logout pull-active value while still authenticated.
// pullStates is reset synchronously on userLoggedOut, so by the time the effect
// below sees `isAuthenticated === false`, isAnyPullActive is already false.
const hadActivePullRef = useRef( false );
if ( isAuthenticated ) {
hadActivePullRef.current = isAnyPullActive;
}

useEffect( () => {
// On logout, only clear import/export state if no pull was in flight.
// An active pull's import phase is driven by main-process events that
// would just repopulate this state, so leave it alone and let the
// import finish.
if ( ! isAuthenticated && ! hadActivePullRef.current ) {
setImportState( {} );
setExportState( {} );
}
}, [ isAuthenticated ] );
Comment thread
gcsecsey marked this conversation as resolved.

const importFile = useCallback(
async (
Expand Down
10 changes: 6 additions & 4 deletions apps/studio/src/index.ts
Comment thread
gcsecsey marked this conversation as resolved.
Original file line number Diff line number Diff line change
Expand Up @@ -157,13 +157,15 @@ async function appBoot() {

setupUpdates();

const appArgs = process.argv.length >= 2 ? [ path.resolve( process.argv[ 1 ] ) ] : [];

if ( process.defaultApp ) {
if ( process.argv.length >= 2 ) {
app.setAsDefaultProtocolClient( PROTOCOL_PREFIX, process.execPath, [
path.resolve( process.argv[ 1 ] ),
] );
if ( appArgs.length > 0 ) {
app.removeAsDefaultProtocolClient( PROTOCOL_PREFIX, process.execPath, appArgs );
app.setAsDefaultProtocolClient( PROTOCOL_PREFIX, process.execPath, appArgs );
}
} else {
app.removeAsDefaultProtocolClient( PROTOCOL_PREFIX );
app.setAsDefaultProtocolClient( PROTOCOL_PREFIX );
}

Expand Down
16 changes: 16 additions & 0 deletions apps/studio/src/lib/active-sync-operations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,22 @@ export function pushBackupIsUploading( key: PushStateProgressInfo[ 'key' ] | und
return uploadingStateKeys.includes( key );
}

/**
* Check if a pull operation is doing local work (download/import).
*
* The initial "in-progress" phase is remote backup creation and should not be treated as local work
* when deciding whether to block unrelated local operations.
*/
export function pullBackupIsDownloadingOrImporting(
key: PullStateProgressInfo[ 'key' ] | undefined
): boolean {
const localPullKeys: PullStateProgressInfo[ 'key' ][] = [ 'downloading', 'importing' ];
if ( ! key ) {
return false;
}
return localPullKeys.includes( key );
}

export function hasUploadingPushOperations(): boolean {
// Iterate over all the sites and check if any operation is cancelable
let result = false;
Expand Down
Loading
Loading