Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Update account deleted page to allow restore behind feature flag #96446

Merged
merged 9 commits into from
Dec 7, 2024
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,
} ),
],
} );
Loading