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, + } ), + ], +} );