diff --git a/client/components/blank-canvas/style.scss b/client/components/blank-canvas/style.scss
index bfe5162f609b5..de731a0353924 100644
--- a/client/components/blank-canvas/style.scss
+++ b/client/components/blank-canvas/style.scss
@@ -142,6 +142,11 @@
.has-blank-canvas {
overscroll-behavior: none;
touch-action: none;
+
+ .global-notices {
+ // we need to show the global notices above the blank canvas
+ z-index: z-index( "root", ".masterbar" ) + 1;
+ }
}
.has-blank-canvas .layout {
max-height: 100vh;
diff --git a/client/me/account-close/closed.jsx b/client/me/account-close/closed.jsx
index 19d70fb346152..8df03a2ce116b 100644
--- a/client/me/account-close/closed.jsx
+++ b/client/me/account-close/closed.jsx
@@ -1,46 +1,98 @@
-import { Spinner } from '@automattic/components';
-import { localize } from 'i18n-calypso';
-import { Component } from 'react';
-import { connect } from 'react-redux';
-import EmptyContent from 'calypso/components/empty-content';
-import getPreviousRoute from 'calypso/state/selectors/get-previous-route';
+import config from '@automattic/calypso-config';
+import { Button, Spinner } from '@wordpress/components';
+import { useTranslate } from 'i18n-calypso';
+import { useEffect } from 'react';
+import { useDispatch, useSelector } from 'react-redux';
+import { BlankCanvas } from 'calypso/components/blank-canvas';
+import FormattedHeader from 'calypso/components/formatted-header';
+import { restoreAccount } from 'calypso/state/account/actions';
+import { getIsRestoring, getRestoreToken } from 'calypso/state/account/selectors';
import isAccountClosed from 'calypso/state/selectors/is-account-closed';
import './closed.scss';
-class AccountSettingsClosedComponent extends Component {
- onClick = () => {
- window.location = '/';
- };
+function AccountDeletedPage() {
+ const translate = useTranslate();
+ const dispatch = useDispatch();
- render() {
- const { isUserAccountClosed, translate } = this.props;
+ const isRestoring = useSelector( getIsRestoring );
+ const isUserAccountClosed = useSelector( isAccountClosed );
- if ( ! isUserAccountClosed ) {
- return (
-
-
-
- { translate( 'Your account is being deleted' ) }
-
-
- );
+ // restore token is either in the URL or in the reducer
+ const params = new URLSearchParams( window.location.search );
+ const urlToken = params.get( 'token' );
+ const storedToken = useSelector( getRestoreToken );
+ const restoreToken = urlToken || storedToken;
+
+ // Sync token to URL if not already there
+ useEffect( () => {
+ if ( storedToken && ! urlToken ) {
+ const newUrl = new URL( window.location.href );
+ newUrl.searchParams.set( 'token', storedToken );
+ window.history.replaceState( {}, '', newUrl.toString() );
}
+ }, [ storedToken, urlToken ] );
+
+ const onCancelClick = () => {
+ window.location.href = '/';
+ };
+ const onRestoreClick = () => {
+ dispatch( restoreAccount( restoreToken ) );
+ };
+
+ if ( ( ! isUserAccountClosed && ! config.isEnabled( 'me/account-restore' ) ) || ! restoreToken ) {
return (
-
+
+
+
+ }
+ />
+
+
);
}
+
+ return (
+
+
+
+
+
+
+
+
+ { config.isEnabled( 'me/account-restore' ) && (
+
+ ) }
+
+
+
+ );
}
-export default connect( ( state ) => {
- return {
- previousRoute: getPreviousRoute( state ),
- isUserAccountClosed: isAccountClosed( state ),
- };
-} )( localize( AccountSettingsClosedComponent ) );
+export default AccountDeletedPage;
diff --git a/client/me/account-close/closed.scss b/client/me/account-close/closed.scss
index 0d26903ae2992..d2cedd7f9b7ef 100644
--- a/client/me/account-close/closed.scss
+++ b/client/me/account-close/closed.scss
@@ -1,10 +1,45 @@
-.account-close__spinner {
- padding-top: 120px;
- text-align: center;
+@import "@wordpress/base-styles/breakpoints";
+@import "@wordpress/base-styles/mixins";
- .account-close__spinner-text {
- font-size: $font-body-small;
- font-weight: 400;
- margin-top: 8px;
+.account-deleted {
+ &.blank-canvas {
+ display: flex;
+ flex-direction: column;
+ }
+ &.blank-canvas .formatted-header .formatted-header__title {
+ font-size: 2rem;
+ }
+ &.blank-canvas .formatted-header .formatted-header__subtitle {
+ font-size: 0.875rem;
+ }
+ .blank-canvas__content {
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+ flex-grow: 1;
+ @include break-small {
+ max-width: 450px;
+ margin: -4rem auto 0;
+ }
+ }
+ .blank-canvas__header {
+ justify-content: space-between;
+ }
+ .blank-canvas__header-title {
+ position: relative;
+ }
+
+ .account-deleted__button-link {
+ color: var( --studio-gray-100 );
+ font-weight: 500;
+ }
+ .account-deleted__buttons {
+ display: flex;
+ flex-direction: column;
+ align-items: flex-start;
+ gap: 1rem;
+ @include break-small {
+ align-items: center;
+ }
}
}
diff --git a/client/state/account/actions.js b/client/state/account/actions.js
index 9eaf2d5054f48..848102f3d4ef6 100644
--- a/client/state/account/actions.js
+++ b/client/state/account/actions.js
@@ -1,5 +1,6 @@
-import { ACCOUNT_CLOSE, ACCOUNT_CLOSE_SUCCESS } from 'calypso/state/action-types';
+import { ACCOUNT_CLOSE, ACCOUNT_CLOSE_SUCCESS, ACCOUNT_RESTORE } from 'calypso/state/action-types';
import 'calypso/state/data-layer/wpcom/me/account/close';
+import 'calypso/state/data-layer/wpcom/me/account/restore';
import 'calypso/state/account/init';
export function closeAccount() {
@@ -8,8 +9,16 @@ export function closeAccount() {
};
}
-export function closeAccountSuccess() {
+export function closeAccountSuccess( response ) {
return {
type: ACCOUNT_CLOSE_SUCCESS,
+ payload: response,
+ };
+}
+
+export function restoreAccount( token ) {
+ return {
+ type: ACCOUNT_RESTORE,
+ payload: { token },
};
}
diff --git a/client/state/account/reducer.js b/client/state/account/reducer.js
index b6778207849a4..bafa93eeecce7 100644
--- a/client/state/account/reducer.js
+++ b/client/state/account/reducer.js
@@ -1,5 +1,10 @@
import { withStorageKey } from '@automattic/state-utils';
-import { ACCOUNT_CLOSE_SUCCESS } from 'calypso/state/action-types';
+import {
+ ACCOUNT_CLOSE_SUCCESS,
+ ACCOUNT_RESTORE,
+ ACCOUNT_RESTORE_FAILED,
+ ACCOUNT_RESTORE_SUCCESS,
+} from 'calypso/state/action-types';
import { combineReducers } from 'calypso/state/utils';
export const isClosed = ( state = false, action ) => {
@@ -12,5 +17,29 @@ export const isClosed = ( state = false, action ) => {
return state;
};
-const combinedReducer = combineReducers( { isClosed } );
+export const restoreToken = ( state = null, action ) => {
+ switch ( action.type ) {
+ case ACCOUNT_CLOSE_SUCCESS: {
+ return action.payload.token;
+ }
+ }
+
+ return state;
+};
+
+export const isRestoring = ( state = false, action ) => {
+ switch ( action.type ) {
+ case ACCOUNT_RESTORE: {
+ return true;
+ }
+ case ACCOUNT_RESTORE_SUCCESS:
+ case ACCOUNT_RESTORE_FAILED: {
+ return false;
+ }
+ }
+
+ return state;
+};
+
+const combinedReducer = combineReducers( { isClosed, restoreToken, isRestoring } );
export default withStorageKey( 'account', combinedReducer );
diff --git a/client/state/account/selectors.tsx b/client/state/account/selectors.tsx
new file mode 100644
index 0000000000000..b1fb0caae4421
--- /dev/null
+++ b/client/state/account/selectors.tsx
@@ -0,0 +1,7 @@
+import { AccountState } from './types';
+
+export const getRestoreToken = ( state: { account?: AccountState } ) =>
+ state.account?.restoreToken || null;
+
+export const getIsRestoring = ( state: { account?: AccountState } ) =>
+ state.account?.isRestoring || false;
diff --git a/client/state/account/types.ts b/client/state/account/types.ts
new file mode 100644
index 0000000000000..c27cebdd49bc7
--- /dev/null
+++ b/client/state/account/types.ts
@@ -0,0 +1,14 @@
+import { Action } from 'redux';
+import { ACCOUNT_RESTORE } from '../action-types';
+
+export interface AccountState {
+ restoreToken?: string | null;
+ isClosed?: boolean;
+ isRestoring?: boolean;
+}
+
+export type AccountRestoreActionType = Action< typeof ACCOUNT_RESTORE > & {
+ payload: {
+ token: string;
+ };
+};
diff --git a/client/state/action-types.ts b/client/state/action-types.ts
index 1aacbd1bc3915..8a47158263a85 100644
--- a/client/state/action-types.ts
+++ b/client/state/action-types.ts
@@ -29,6 +29,9 @@ export const ACCOUNT_RECOVERY_SETTINGS_VALIDATE_PHONE_FAILED =
'ACCOUNT_RECOVERY_SETTINGS_VALIDATE_PHONE_FAILED';
export const ACCOUNT_RECOVERY_SETTINGS_VALIDATE_PHONE_SUCCESS =
'ACCOUNT_RECOVERY_SETTINGS_VALIDATE_PHONE_SUCCESS';
+export const ACCOUNT_RESTORE = 'ACCOUNT_RESTORE';
+export const ACCOUNT_RESTORE_SUCCESS = 'ACCOUNT_RESTORE_SUCCESS';
+export const ACCOUNT_RESTORE_FAILED = 'ACCOUNT_RESTORE_FAILED';
export const ACTIVE_PROMOTIONS_RECEIVE = 'ACTIVE_PROMOTIONS_RECEIVE';
export const ACTIVE_PROMOTIONS_REQUEST = 'ACTIVE_PROMOTIONS_REQUEST';
export const ACTIVE_PROMOTIONS_REQUEST_FAILURE = 'ACTIVE_PROMOTIONS_REQUEST_FAILURE';
diff --git a/client/state/data-layer/wpcom/me/account/close/index.js b/client/state/data-layer/wpcom/me/account/close/index.js
index 9d47d88940c22..2abbf57af28c5 100644
--- a/client/state/data-layer/wpcom/me/account/close/index.js
+++ b/client/state/data-layer/wpcom/me/account/close/index.js
@@ -30,12 +30,12 @@ export function fromApi( response ) {
return response;
}
-export function receiveAccountCloseSuccess() {
+export function receiveAccountCloseSuccess( _action, response ) {
recordTracksEvent( 'calypso_account_closed' );
- return closeAccountSuccess();
+ return closeAccountSuccess( response );
}
-export function receiveAccountCloseError( action, error ) {
+export function receiveAccountCloseError( _action, error ) {
if ( error.error === 'active-subscriptions' ) {
return errorNotice(
translate( 'This user account cannot be closed while it has active subscriptions.' )
diff --git a/client/state/data-layer/wpcom/me/account/restore/index.ts b/client/state/data-layer/wpcom/me/account/restore/index.ts
new file mode 100644
index 0000000000000..ff3fab02c6102
--- /dev/null
+++ b/client/state/data-layer/wpcom/me/account/restore/index.ts
@@ -0,0 +1,68 @@
+import { translate } from 'i18n-calypso';
+import { AccountRestoreActionType } from 'calypso/state/account/types';
+import {
+ ACCOUNT_RESTORE,
+ ACCOUNT_RESTORE_SUCCESS,
+ ACCOUNT_RESTORE_FAILED,
+} from 'calypso/state/action-types';
+import { registerHandlers } from 'calypso/state/data-layer/handler-registry';
+import { http } from 'calypso/state/data-layer/wpcom-http/actions';
+import { dispatchRequest } from 'calypso/state/data-layer/wpcom-http/utils';
+import { errorNotice, successNotice } from 'calypso/state/notices/actions';
+
+export function requestAccountRestore( action: AccountRestoreActionType ) {
+ const { token } = action.payload;
+ return http(
+ {
+ method: 'POST',
+ apiVersion: '1.1',
+ path: `/me/account/restore`,
+ body: {
+ token,
+ },
+ },
+ action
+ );
+}
+
+function receiveAccountRestoreSuccess() {
+ return [
+ { type: ACCOUNT_RESTORE_SUCCESS },
+ () => {
+ // wait before redirecting to let the user see the success notice
+ setTimeout( () => {
+ window.location.href = '/sites?restored=true';
+ }, 2000 );
+ },
+ successNotice( translate( 'Your account has been restored. Redirecting back to login.' ) ),
+ ];
+}
+
+function receiveAccountRestoreError( action: AccountRestoreActionType, error: { error: string } ) {
+ if ( error.error === 'invalid_token' ) {
+ return [
+ { type: ACCOUNT_RESTORE_FAILED },
+ errorNotice(
+ translate(
+ 'Invalid token. Please check your account deleted email for the correct link or contact support.'
+ )
+ ),
+ ];
+ }
+ return [
+ { type: ACCOUNT_RESTORE_FAILED },
+ errorNotice(
+ translate( 'Sorry, there was a problem restoring your account. Please contact support.' )
+ ),
+ ];
+}
+
+registerHandlers( 'state/data-layer/wpcom/me/account/restore/index.js', {
+ [ ACCOUNT_RESTORE ]: [
+ dispatchRequest( {
+ fetch: requestAccountRestore,
+ onSuccess: receiveAccountRestoreSuccess,
+ onError: receiveAccountRestoreError,
+ } ),
+ ],
+} );