Skip to content
4 changes: 3 additions & 1 deletion src/modules/user-settings/components/snapshot-info.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,9 @@ export const SnapshotInfo = ( {
isDeleting?: boolean;
} ) => {
const { __ } = useI18n();
const { data: snapshotUsage } = useGetSnapshotUsage();
const { data: snapshotUsage } = useGetSnapshotUsage( undefined, {
refetchOnMountOrArgChange: true,
} );
const snapshotCreationBlocked = snapshotUsage?.siteCreationBlocked ?? false;
const menuItemStyles = cx(
'[&_span]:min-w-0 [&_span]:p-[1px]',
Expand Down
10 changes: 3 additions & 7 deletions src/modules/user-settings/components/usage-tab.tsx
Original file line number Diff line number Diff line change
@@ -1,33 +1,29 @@
import { Snapshot } from 'common/types/snapshot';
import { PromptInfo } from './prompt-info';
import { SnapshotInfo } from './snapshot-info';

export const UsageTab = ( {
loadingDeletingAllSnapshots,
activeSnapshotCount,
isLoadingSnapshotUsage,
allSnapshots,
isOffline,
snapshotQuota,
onRemoveSnapshots,
}: {
loadingDeletingAllSnapshots: boolean;
activeSnapshotCount: number;
isLoadingSnapshotUsage: boolean;
allSnapshots: Pick< Snapshot, 'atomicSiteId' >[] | null;
isOffline: boolean;
snapshotQuota: number;
onRemoveSnapshots: () => void;
} ) => (
<>
<SnapshotInfo
isDeleting={ loadingDeletingAllSnapshots }
isDeleting={ loadingDeletingAllSnapshots || isLoadingSnapshotUsage }
isDisabled={
activeSnapshotCount === 0 ||
isOffline ||
loadingDeletingAllSnapshots ||
isLoadingSnapshotUsage ||
allSnapshots?.length === 0 ||
isOffline
isLoadingSnapshotUsage
}
siteCount={ activeSnapshotCount }
siteLimit={ snapshotQuota }
Expand Down
32 changes: 19 additions & 13 deletions src/modules/user-settings/components/user-settings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,19 +11,14 @@ import { AccountTab } from 'src/modules/user-settings/components/account-tab';
import { NonAuthenticatedAccountTab } from 'src/modules/user-settings/components/non-authenticated-account-tab';
import { PreferencesTab } from 'src/modules/user-settings/components/preferences-tab';
import { UsageTab } from 'src/modules/user-settings/components/usage-tab';
import { useRootSelector, useAppDispatch } from 'src/stores';
import { snapshotSelectors, snapshotThunks } from 'src/stores/snapshot-slice';
import { useGetSnapshotUsage } from 'src/stores/wpcom-api';
import { UserSettingsTab } from '../user-settings-types';
import { UserSettingsTab } from 'src/modules/user-settings/user-settings-types';
import { useRootSelector } from 'src/stores';
import { snapshotSelectors } from 'src/stores/snapshot-slice';
import { useDeleteAllSnapshots, useGetSnapshotUsage } from 'src/stores/wpcom-api';

export default function UserSettings() {
const { __ } = useI18n();
const dispatch = useAppDispatch();
const { isAuthenticated, logout, user } = useAuth();

const activeBulkOperationForUser = useRootSelector( ( state ) =>
snapshotSelectors.selectActiveBulkOperationForUser( state, user?.id ?? 0 )
);
const snapshotsByUser = useRootSelector( ( state ) =>
snapshotSelectors.selectSnapshotsByUser( state, user?.id ?? 0 )
);
Expand All @@ -34,6 +29,8 @@ export default function UserSettings() {
const [ needsToOpenUserSettings, setNeedsToOpenUserSettings ] = useState( false );
const [ selectedTabName, setSelectedTabName ] = useState< string | undefined >();

const [ deleteAllSnapshots, { isLoading: isDeletingAllSnapshots } ] = useDeleteAllSnapshots();

const isOffline = useOffline();

const resetLocalState = useCallback( () => {
Expand Down Expand Up @@ -61,9 +58,19 @@ export default function UserSettings() {
} );

if ( response === DELETE_BUTTON_INDEX ) {
await dispatch( snapshotThunks.deleteAllSnapshotsForUser( { userId: user?.id ?? 0 } ) );
try {
await deleteAllSnapshots().unwrap();
await getIpcApi().saveSnapshotsToStorage( [] );
} catch ( error ) {
await getIpcApi().showMessageBox( {
type: 'warning',
message: __( 'Failed to delete all preview sites' ),
detail: __( 'An error occurred while deleting all preview sites. Please try again.' ),
buttons: [ __( 'OK' ) ],
} );
}
}
}, [ __, dispatch, user?.id ] );
}, [ __, deleteAllSnapshots ] );

const tabs: UserSettingsTab[] = [
{
Expand Down Expand Up @@ -113,10 +120,9 @@ export default function UserSettings() {
{ name === 'preferences' && <PreferencesTab onClose={ resetLocalState } /> }
{ name === 'usage' && isAuthenticated && (
<UsageTab
loadingDeletingAllSnapshots={ !! activeBulkOperationForUser }
loadingDeletingAllSnapshots={ isDeletingAllSnapshots }
activeSnapshotCount={ definitiveSnapshotCount }
isLoadingSnapshotUsage={ isLoadingSnapshotUsage }
allSnapshots={ snapshotsByUser }
isOffline={ isOffline }
snapshotQuota={ snapshotQuota }
onRemoveSnapshots={ onRemoveSnapshots }
Expand Down
1 change: 1 addition & 0 deletions src/storage/user-data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,7 @@ type UserDataSafeKeys =
| 'devToolsOpen'
| 'windowBounds'
| 'authToken'
| 'snapshots'
| 'onboardingCompleted'
| 'locale'
| 'promptWindowsSpeedUpResult'
Expand Down
22 changes: 21 additions & 1 deletion src/stores/utils/with-offline-check.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { useOffline } from 'src/hooks/use-offline';
import type { BaseQueryFn } from '@reduxjs/toolkit/query';
import type { TypedUseQuery } from '@reduxjs/toolkit/query/react';
import type { TypedUseQuery, TypedUseMutation } from '@reduxjs/toolkit/query/react';

export function withOfflineCheck< TResult, TArg, TBaseQuery extends BaseQueryFn >(
useQueryHook: TypedUseQuery< TResult, TArg, TBaseQuery >
Expand All @@ -13,3 +13,23 @@ export function withOfflineCheck< TResult, TArg, TBaseQuery extends BaseQueryFn
} );
};
}

export function withOfflineCheckMutation< TResult, TArg, TBaseQuery extends BaseQueryFn >(

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.

We already disable the button to delete preview sites when the user isOffline, so I am wondering if we need this?

Image

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.

That’s a great point. I agree it’s not needed right now, but I added it just in case we need the offline check elsewhere while using another API. No strong opinion here, though I would slightly prefer keeping it since withOfflineCheckMutation could be useful later.

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.

Okay, we can keep it then 👍 If it is not something that we use often, we can clean this up later.

useMutationHook: TypedUseMutation< TResult, TArg, TBaseQuery >
): TypedUseMutation< TResult, TArg, TBaseQuery > {
return ( options = {} ) => {
const isOffline = useOffline();
const [ trigger, result ] = useMutationHook( options );

const wrappedTrigger = ( ( ...args: Parameters< typeof trigger > ) => {
if ( isOffline ) {
return Promise.reject( new Error( 'Cannot perform mutation while offline' ) ) as ReturnType<
typeof trigger
>;
}
return trigger( ...args );
} ) as typeof trigger;

return [ wrappedTrigger, result ] as const;
};
}
30 changes: 28 additions & 2 deletions src/stores/wpcom-api.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import { createApi, TypedUseQuery } from '@reduxjs/toolkit/query/react';
import { createApi, TypedUseQuery, TypedUseMutation } from '@reduxjs/toolkit/query/react';
import * as Sentry from '@sentry/electron/renderer';
import { z } from 'zod';
import { DAY_MS } from 'common/constants';
import wpcomFactory from 'src/lib/wpcom-factory';
import wpcomXhrRequest from 'src/lib/wpcom-xhr-request-factory';
import { withOfflineCheck } from 'src/stores/utils/with-offline-check';
import { withOfflineCheck, withOfflineCheckMutation } from 'src/stores/utils/with-offline-check';
import type { BaseQueryFn, FetchBaseQueryError } from '@reduxjs/toolkit/query';

type WPCOM = ReturnType< typeof wpcomFactory >;
Expand Down Expand Up @@ -152,6 +152,13 @@ export const wpcomApi = createApi( {
keepUnusedDataFor: 60 * 60,
providesTags: [ 'SnapshotUsage' ],
} ),
deleteAllSnapshots: builder.mutation< void, void >( {
query: () => ( {
path: '/jurassic-ninja/delete/all',
apiNamespace: 'wpcom/v2',
method: 'POST',
} ),
} ),
} ),
} );

Expand Down Expand Up @@ -220,6 +227,21 @@ function withWpcomClientCheck< TResult, TArg >(
};
}

function withWpcomClientCheckMutation< TResult, TArg >(
useMutationHook: TypedUseMutation< TResult, TArg, typeof wpcomBaseQuery >
): TypedUseMutation< TResult, TArg, typeof wpcomBaseQuery > {
return ( options = {} ) => {
const [ trigger, result ] = useMutationHook( options );
const wrappedTrigger = ( ( ...args: Parameters< typeof trigger > ) => {
if ( ! wpcomClient ) {
return Promise.reject( new Error( 'Not authenticated' ) ) as ReturnType< typeof trigger >;
}
return trigger( ...args );
} ) as typeof trigger;
return [ wrappedTrigger, result ] as const;
};
}

export const useGetWelcomeMessages = withWpcomClientCheck(
withOfflineCheck( wpcomApi.useGetWelcomeMessagesQuery )
);
Expand All @@ -232,5 +254,9 @@ export const useGetSnapshotUsage = withWpcomClientCheck(
withOfflineCheck( wpcomApi.useGetSnapshotUsageQuery )
);

export const useDeleteAllSnapshots = withWpcomClientCheckMutation(
withOfflineCheckMutation( wpcomApi.useDeleteAllSnapshotsMutation )
);

// Blueprints use the public API and don't require authentication
export const useGetBlueprints = withOfflineCheck( wpcomPublicApi.useGetBlueprintsQuery );