Skip to content

Commit

Permalink
Update account deleted page to allow restore behind feature flag (#96446
Browse files Browse the repository at this point in the history
)

* Update layout for account deleted page

* Add account restore functionality

* Update layout for deleting state

* rename ACCOUNT_RESTORE_ERROR to ACCOUNT_RESTORE_FAILED

* Prefix unused variable

* Use translate for Create an account

* Align click handler names

* Replace redux connect with useSelector and rename component

* Update redirect copy
  • Loading branch information
candy02058912 authored Dec 7, 2024
1 parent 9ffb111 commit b7fffd6
Show file tree
Hide file tree
Showing 10 changed files with 269 additions and 47 deletions.
5 changes: 5 additions & 0 deletions client/components/blank-canvas/style.scss
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
118 changes: 85 additions & 33 deletions client/me/account-close/closed.jsx
Original file line number Diff line number Diff line change
@@ -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 (
<div className="account-close__spinner">
<Spinner size={ 32 } />
<p className="account-close__spinner-text">
{ translate( 'Your account is being deleted' ) }
</p>
</div>
);
// 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 (
<EmptyContent
title={ translate( 'Your account has been closed' ) }
line={ translate( 'Thanks for flying with WordPress.com' ) }
secondaryAction={ translate( 'Return to WordPress.com' ) }
secondaryActionCallback={ this.onClick }
/>
<BlankCanvas className="account-deleted">
<BlankCanvas.Header />
<BlankCanvas.Content>
<FormattedHeader
brandFont
headerText={ translate( 'Your account is being deleted' ) }
subHeaderText={ <Spinner style={ { width: '32px', height: '32px' } } /> }
/>
</BlankCanvas.Content>
</BlankCanvas>
);
}

return (
<BlankCanvas className="account-deleted">
<BlankCanvas.Header>
<Button variant="link" className="account-deleted__button-link" href="/">
{ translate( 'Create an account' ) }
</Button>
</BlankCanvas.Header>
<BlankCanvas.Content>
<FormattedHeader
brandFont
headerText={ translate( 'Your account has been deleted' ) }
subHeaderText={
config.isEnabled( 'me/account-restore' )
? translate(
'Thanks for flying with WordPress.com. You have 30 days to restore your account if you change your mind.'
)
: translate( 'Thanks for flying with WordPress.com.' )
}
/>
<div className="account-deleted__buttons">
<Button variant="secondary" onClick={ onCancelClick }>
{ translate( 'Return to WordPress.com' ) }
</Button>
{ config.isEnabled( 'me/account-restore' ) && (
<Button
variant="link"
className="account-deleted__button-link"
onClick={ onRestoreClick }
isBusy={ isRestoring }
>
{ translate( 'I made a mistake! Restore my account' ) }
</Button>
) }
</div>
</BlankCanvas.Content>
</BlankCanvas>
);
}

export default connect( ( state ) => {
return {
previousRoute: getPreviousRoute( state ),
isUserAccountClosed: isAccountClosed( state ),
};
} )( localize( AccountSettingsClosedComponent ) );
export default AccountDeletedPage;
49 changes: 42 additions & 7 deletions client/me/account-close/closed.scss
Original file line number Diff line number Diff line change
@@ -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;
}
}
}
13 changes: 11 additions & 2 deletions client/state/account/actions.js
Original file line number Diff line number Diff line change
@@ -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() {
Expand All @@ -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 },
};
}
33 changes: 31 additions & 2 deletions client/state/account/reducer.js
Original file line number Diff line number Diff line change
@@ -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 ) => {
Expand All @@ -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 );
7 changes: 7 additions & 0 deletions client/state/account/selectors.tsx
Original file line number Diff line number Diff line change
@@ -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;
14 changes: 14 additions & 0 deletions client/state/account/types.ts
Original file line number Diff line number Diff line change
@@ -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;
};
};
3 changes: 3 additions & 0 deletions client/state/action-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down
6 changes: 3 additions & 3 deletions client/state/data-layer/wpcom/me/account/close/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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.' )
Expand Down
68 changes: 68 additions & 0 deletions client/state/data-layer/wpcom/me/account/restore/index.ts
Original file line number Diff line number Diff line change
@@ -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,
} ),
],
} );

0 comments on commit b7fffd6

Please sign in to comment.