Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
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 @@ -7,7 +7,8 @@ import { useIpcListener } from 'src/hooks/use-ipc-listener';
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 { 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 @@ -45,21 +46,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 );
} catch ( err ) {
console.error( 'Failed to handle invalid token:', err );
Sentry.captureException( err );
}
}, [] );
}, [ dispatch ] );

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

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

useEffect( () => {
async function run() {
Expand Down
13 changes: 12 additions & 1 deletion apps/studio/src/hooks/use-import-export.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import * as Sentry from '@sentry/electron/renderer';
import { __, sprintf } from '@wordpress/i18n';
import { createContext, useMemo, useState, useCallback, useContext } from 'react';
import { createContext, useEffect, useMemo, useState, useCallback, useContext } from 'react';
import { WP_CLI_IMPORT_EXPORT_RESPONSE_TIMEOUT_IN_HRS } from 'src/constants';
import { useAuth } from 'src/hooks/use-auth';
import { useIpcListener } from 'src/hooks/use-ipc-listener';
import { useSiteDetails } from 'src/hooks/use-site-details';
import { simplifyErrorForDisplay } from 'src/lib/error-formatting';
Expand All @@ -26,6 +27,7 @@ export type ImportProgressState = {
statusMessage: string;
progress: number;
isNewSite?: boolean;
isError?: boolean;
};
};

Expand Down Expand Up @@ -76,6 +78,14 @@ export const ImportExportProvider = ( { children }: { children: React.ReactNode
const [ importState, setImportState ] = useState< ImportProgressState >( {} );
const [ exportState, setExportState ] = useState< ExportProgressState >( {} );
const { startServer, stopServer, updateSite } = useSiteDetails();
const { isAuthenticated } = useAuth();

useEffect( () => {
if ( ! isAuthenticated ) {
setImportState( {} );
setExportState( {} );
}
}, [ isAuthenticated ] );

const importFile = useCallback(
async (
Expand Down Expand Up @@ -339,6 +349,7 @@ export const ImportExportProvider = ( { children }: { children: React.ReactNode
[ siteId ]: {
...currentProgress,
statusMessage: __( 'Import failed. Please try again.' ),
isError: true,
},
} ) );
break;
Expand Down
12 changes: 8 additions & 4 deletions apps/studio/src/modules/sync/components/sync-connected-sites.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -225,11 +225,15 @@ const SyncConnectedSitesSectionItem = ( {
const sitePullState = useRootSelector(
syncOperationsSelectors.selectPullState( selectedSite.id, connectedSite.id )
);
const pullImportFailed =
sitePullState?.status.key === 'importing' &&
importState[ connectedSite.localSiteId ]?.isError === true;
const isPulling =
sitePullState?.status.key === 'in-progress' ||
sitePullState?.status.key === 'downloading' ||
sitePullState?.status.key === 'importing';
const isPullError = sitePullState?.status.key === 'failed';
( sitePullState?.status.key === 'in-progress' ||
sitePullState?.status.key === 'downloading' ||
sitePullState?.status.key === 'importing' ) &&
! pullImportFailed;
const isPullError = sitePullState?.status.key === 'failed' || pullImportFailed;
const hasPullFinished = sitePullState?.status.key === 'finished';
const hasPullCancelled = sitePullState?.status.key === 'cancelled';
const pullImportState = importState[ connectedSite.localSiteId ];
Expand Down
3 changes: 3 additions & 0 deletions apps/studio/src/stores/auth-actions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { createAction } from '@reduxjs/toolkit';

export const userLoggedOut = createAction( 'auth/userLoggedOut' );
57 changes: 43 additions & 14 deletions apps/studio/src/stores/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import {
} from 'src/hooks/use-sync-states-progress-info';
import { getIpcApi } from 'src/lib/get-ipc-api';
import { appVersionApi } from 'src/stores/app-version-api';
import { userLoggedOut } from 'src/stores/auth-actions';
import { betaFeaturesReducer, loadBetaFeatures } from 'src/stores/beta-features-slice';
import { certificateTrustApi } from 'src/stores/certificate-trust-api';
import { reducer as chatReducer } from 'src/stores/chat-slice';
Expand All @@ -32,9 +33,15 @@ import {
syncOperationsSelectors,
syncOperationsThunks,
} from 'src/stores/sync/sync-operations-slice';
import {
PUSH_POLLERS,
PULL_POLLERS,
stopPushPoller,
stopPullPoller,
} from 'src/stores/sync/sync-pollers';
import { wpcomSitesApi } from 'src/stores/sync/wpcom-sites';
import uiReducer from 'src/stores/ui-slice';
import { getWpcomClient, wpcomApi, wpcomPublicApi } from 'src/stores/wpcom-api';
import { getWpcomClient, setWpcomClient, wpcomApi, wpcomPublicApi } from 'src/stores/wpcom-api';
import { wordpressVersionsApi } from './wordpress-versions-api';
import type { SupportedLocale } from '@studio/common/lib/locale';

Expand Down Expand Up @@ -182,12 +189,44 @@ startAppListening( {
},
} );

// Clear all sync state when user logs out
startAppListening( {
actionCreator: userLoggedOut,
effect( action, listenerApi ) {
setWpcomClient( undefined );

// Use getOriginalState() to read state BEFORE the reducer cleared pullStates/pushStates
const state = listenerApi.getOriginalState();

for ( const pullState of Object.values( state.syncOperations.pullStates ) ) {
void store.dispatch(
syncOperationsThunks.cancelPull( {
selectedSiteId: pullState.selectedSite.id,
remoteSiteId: pullState.remoteSiteId,
displayNotification: false,
} )
);
}

for ( const pushState of Object.values( state.syncOperations.pushStates ) ) {
void store.dispatch(
syncOperationsThunks.cancelPush( {
selectedSiteId: pushState.selectedSite.id,
remoteSiteId: pushState.remoteSiteId,
displayNotification: false,
} )
);
}

// Reset authenticated RTK Query caches
listenerApi.dispatch( wpcomSitesApi.util.resetApiState() );
listenerApi.dispatch( wpcomApi.util.resetApiState() );
},
} );

const PUSH_POLLING_KEYS = [ 'creatingRemoteBackup', 'applyingChanges', 'finishing' ];
const SYNC_POLLING_INTERVAL = 3000;

const PUSH_POLLERS = new Map< string, AbortController >();
const PULL_POLLERS = new Map< string, AbortController >();

function isPushPollable( selectedSiteId: string, remoteSiteId: number ) {
const pushState = syncOperationsSelectors.selectPushState(
selectedSiteId,
Expand All @@ -204,16 +243,6 @@ function isPullPollable( selectedSiteId: string, remoteSiteId: number ) {
return pullState?.status.key === 'in-progress' && !! pullState.backupId;
}

function stopPushPoller( stateId: string ) {
PUSH_POLLERS.get( stateId )?.abort();
PUSH_POLLERS.delete( stateId );
}

function stopPullPoller( stateId: string ) {
PULL_POLLERS.get( stateId )?.abort();
PULL_POLLERS.delete( stateId );
}

async function startPushPoller( selectedSiteId: string, remoteSiteId: number ) {
const stateId = generateStateId( selectedSiteId, remoteSiteId );
if ( PUSH_POLLERS.has( stateId ) ) {
Expand Down
4 changes: 4 additions & 0 deletions apps/studio/src/stores/sync/connected-sites.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { createSlice, PayloadAction } from '@reduxjs/toolkit';
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react';
import { getIpcApi } from 'src/lib/get-ipc-api';
import { RootState } from 'src/stores';
import { userLoggedOut } from 'src/stores/auth-actions';
import type { SyncSite, SyncModalMode } from 'src/modules/sync/types';

type ConnectedSitesState = {
Expand Down Expand Up @@ -60,6 +61,9 @@ const connectedSitesSlice = createSlice( {
delete state.loadingSiteIds[ action.payload ];
},
},
extraReducers: ( builder ) => {
builder.addCase( userLoggedOut, () => getInitialState() );
},
} );

export const connectedSitesActions = connectedSitesSlice.actions;
Expand Down
36 changes: 26 additions & 10 deletions apps/studio/src/stores/sync/sync-operations-slice.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,9 @@ import { generateStateId } from 'src/hooks/sync-sites/use-pull-push-states';
import { getIpcApi } from 'src/lib/get-ipc-api';
import { getHostnameFromUrl } from 'src/lib/url-utils';
import { store } from 'src/stores';
import { userLoggedOut } from 'src/stores/auth-actions';
import { connectedSitesApi } from 'src/stores/sync/connected-sites';
import { stopPullPoller, stopPushPoller } from 'src/stores/sync/sync-pollers';
import type {
PullStateProgressInfo,
PushStateProgressInfo,
Expand Down Expand Up @@ -182,6 +184,7 @@ const syncOperationsSlice = createSlice( {
},
extraReducers: ( builder ) => {
builder
.addCase( userLoggedOut, () => initialState )
.addCase( pushSiteThunk.rejected, ( state, action ) => {
const { connectedSite, selectedSite } = action.meta.arg;
const stateId = generateStateId( selectedSite.id, connectedSite.id );
Expand Down Expand Up @@ -297,14 +300,19 @@ const createTypedAsyncThunk = createAsyncThunk.withTypes< {
type CancelOperationPayload = {
selectedSiteId: string;
remoteSiteId: number;
displayNotification?: boolean;
};

const cancelPushThunk = createTypedAsyncThunk(
'syncOperations/cancelPush',
async ( { selectedSiteId, remoteSiteId }: CancelOperationPayload, { dispatch } ) => {
async (
{ selectedSiteId, remoteSiteId, displayNotification = true }: CancelOperationPayload,
{ dispatch }
) => {
const operationId = generateStateId( selectedSiteId, remoteSiteId );
const abortCallback = PUSH_SITE_ABORT_CALLBACKS.get( operationId );

stopPushPoller( operationId );
Copy link
Contributor

Choose a reason for hiding this comment

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

Maybe it's better to call both start{Pull,Push}Poller and stop{Pull,Push}Poller in this file… 🤔 I feel like we ended up jumping through some hoops doing it in the middleware functions in stores/index.ts.

Thoughts, @gcsecsey?

abortCallback?.();
getIpcApi().cancelSyncOperation( operationId );

Expand All @@ -316,17 +324,23 @@ const cancelPushThunk = createTypedAsyncThunk(
} )
);

getIpcApi().showNotification( {
title: __( 'Push cancelled' ),
body: __( 'The push operation has been cancelled.' ),
} );
if ( displayNotification ) {
getIpcApi().showNotification( {
title: __( 'Push cancelled' ),
body: __( 'The push operation has been cancelled.' ),
} );
}
}
);

const cancelPullThunk = createTypedAsyncThunk(
'syncOperations/cancelPull',
async ( { selectedSiteId, remoteSiteId }: CancelOperationPayload, { dispatch } ) => {
async (
{ selectedSiteId, remoteSiteId, displayNotification = true }: CancelOperationPayload,
{ dispatch }
) => {
const operationId = generateStateId( selectedSiteId, remoteSiteId );
stopPullPoller( operationId );
getIpcApi().cancelSyncOperation( operationId );

dispatch(
Expand All @@ -343,10 +357,12 @@ const cancelPullThunk = createTypedAsyncThunk(
// Ignore errors if file doesn't exist
} );

getIpcApi().showNotification( {
title: __( 'Pull cancelled' ),
body: __( 'The pull operation has been cancelled.' ),
} );
if ( displayNotification ) {
getIpcApi().showNotification( {
title: __( 'Pull cancelled' ),
body: __( 'The pull operation has been cancelled.' ),
} );
}
}
);

Expand Down
20 changes: 20 additions & 0 deletions apps/studio/src/stores/sync/sync-pollers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
/**
* Shared poller abort controllers for sync push/pull operations.
*
* Extracted into its own module so both the poller loops (stores/index.ts)
* and the cancel thunks (sync-operations-slice.ts) can abort in-flight
* poll requests without circular imports.
*/

export const PUSH_POLLERS = new Map< string, AbortController >();
export const PULL_POLLERS = new Map< string, AbortController >();

export function stopPushPoller( stateId: string ) {
PUSH_POLLERS.get( stateId )?.abort();
PUSH_POLLERS.delete( stateId );
}

export function stopPullPoller( stateId: string ) {
PULL_POLLERS.get( stateId )?.abort();
PULL_POLLERS.delete( stateId );
}
2 changes: 2 additions & 0 deletions apps/studio/src/stores/sync/sync-slice.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { createSlice } from '@reduxjs/toolkit';
import { TreeNode } from 'src/components/tree-view';
import { userLoggedOut } from 'src/stores/auth-actions';
import { fetchRemoteFileTree } from './sync-api';

type RemoteFileTreeState = {
Expand Down Expand Up @@ -33,6 +34,7 @@ const syncSlice = createSlice( {
},
extraReducers: ( builder ) => {
builder
.addCase( userLoggedOut, () => initialState )
.addCase( fetchRemoteFileTree.pending, ( state ) => {
state.remoteFileTrees.loading = true;
state.remoteFileTrees.error = null;
Expand Down
Loading