(core) Delete my account button

Summary:
Adding new "Delete my account" button to the profile page that allows users to remove completely
their accounts as long as they don't own any team site.

Test Plan: Added

Reviewers: georgegevoian, paulfitz

Reviewed By: georgegevoian, paulfitz

Subscribers: paulfitz

Differential Revision: https://phab.getgrist.com/D4037
This commit is contained in:
Jarosław Sadziński
2023-09-26 10:09:06 +02:00
parent e033889b6a
commit cce185956c
19 changed files with 205 additions and 34 deletions

View File

@@ -87,6 +87,11 @@ export function getLogoutUrl(): string {
return _getLoginLogoutUrl('logout');
}
// Get the URL that users are redirect to after deleting their account.
export function getAccountDeletedUrl(): string {
return _getLoginLogoutUrl('account-deleted', {nextUrl: ''});
}
// Get URL for the signin page.
export function getLoginOrSignupUrl(options: GetLoginOrSignupUrlOptions = {}): string {
return _getLoginLogoutUrl('signin', options);
@@ -96,19 +101,21 @@ export function getWelcomeHomeUrl() {
return _buildUrl('welcome/home').href;
}
const FINAL_PATHS = ['/signed-out', '/account-deleted'];
// Returns the relative URL (i.e. path) of the current page, except when it's the
// "/signed-out" page, in which case it returns the home page ("/").
// "/signed-out" page or "/account-deleted", in which case it returns the home page ("/").
// This is a good URL to use for a post-login redirect.
function _getCurrentUrl(): string {
const {hash, pathname, search} = new URL(window.location.href);
if (pathname.endsWith('/signed-out')) { return '/'; }
if (FINAL_PATHS.some(final => pathname.endsWith(final))) { return '/'; }
return parseFirstUrlPart('o', pathname).path + search + hash;
}
// Returns the URL for the given login page.
function _getLoginLogoutUrl(
page: 'login'|'logout'|'signin'|'signup',
page: 'login'|'logout'|'signin'|'signup'|'account-deleted',
options: GetLoginOrSignupUrlOptions = {}
): string {
const {srcDocId, nextUrl = _getCurrentUrl()} = options;

View File

@@ -1,9 +1,12 @@
import {detectCurrentLang, makeT} from 'app/client/lib/localization';
import {AppModel, reportError} from 'app/client/models/AppModel';
import {urlState} from 'app/client/models/gristUrlState';
import * as css from 'app/client/ui/AccountPageCss';
import {ApiKey} from 'app/client/ui/ApiKey';
import {AppHeader} from 'app/client/ui/AppHeader';
import {buildChangePasswordDialog} from 'app/client/ui/ChangePasswordDialog';
import {DeleteAccountDialog} from 'app/client/ui/DeleteAccountDialog';
import {translateLocale} from 'app/client/ui/LanguageMenu';
import {leftPanelBasic} from 'app/client/ui/LeftPanelCommon';
import {MFAConfig} from 'app/client/ui/MFAConfig';
import {pagePanels} from 'app/client/ui/PagePanels';
@@ -14,11 +17,9 @@ import {cssBreadcrumbs, separator} from 'app/client/ui2018/breadcrumbs';
import {labeledSquareCheckbox} from 'app/client/ui2018/checkbox';
import {cssLink} from 'app/client/ui2018/links';
import {select} from 'app/client/ui2018/menus';
import {getPageTitleSuffix} from 'app/common/gristUrls';
import {getGristConfig} from 'app/common/urlUtils';
import {FullUser} from 'app/common/UserAPI';
import {detectCurrentLang, makeT} from 'app/client/lib/localization';
import {translateLocale} from 'app/client/ui/LanguageMenu';
import {getPageTitleSuffix} from 'app/common/gristUrls';
import {Computed, Disposable, dom, domComputed, makeTestId, Observable, styled, subscribe} from 'grainjs';
const testId = makeTestId('test-account-page-');
@@ -161,7 +162,10 @@ designed to ensure that you're the only person who can access your account, even
inputArgs: [{size: '5'}], // Lower size so that input can shrink below ~152px.
})
)),
),
!getGristConfig().canCloseAccount ? null : [
dom.create(DeleteAccountDialog, user),
],
),
testId('body'),
)));
}

View File

@@ -1,6 +1,6 @@
import {makeT} from 'app/client/lib/localization';
import {AppModel} from 'app/client/models/AppModel';
import {getLoginUrl, getMainOrgUrl, urlState} from 'app/client/models/gristUrlState';
import {getLoginUrl, getMainOrgUrl, getSignupUrl, urlState} from 'app/client/models/gristUrlState';
import {AppHeader} from 'app/client/ui/AppHeader';
import {leftPanelBasic} from 'app/client/ui/LeftPanelCommon';
import {pagePanels} from 'app/client/ui/PagePanels';
@@ -21,7 +21,8 @@ export function createErrPage(appModel: AppModel) {
return gristConfig.errPage === 'signed-out' ? createSignedOutPage(appModel) :
gristConfig.errPage === 'not-found' ? createNotFoundPage(appModel, message) :
gristConfig.errPage === 'access-denied' ? createForbiddenPage(appModel, message) :
createOtherErrorPage(appModel, message);
gristConfig.errPage === 'account-deleted' ? createAccountDeletedPage(appModel) :
createOtherErrorPage(appModel, message);
}
/**
@@ -67,6 +68,20 @@ export function createSignedOutPage(appModel: AppModel) {
]);
}
/**
* Creates a page that shows the user is logged out.
*/
export function createAccountDeletedPage(appModel: AppModel) {
document.title = t("Account deleted{{suffix}}", {suffix: getPageTitleSuffix(getGristConfig())});
return pagePanelsError(appModel, t("Account deleted{{suffix}}", {suffix: ''}), [
cssErrorText(t("Your account has been deleted.")),
cssButtonWrap(bigPrimaryButtonLink(
t("Sign up"), {href: getSignupUrl()}, testId('error-signin')
))
]);
}
/**
* Creates a "Page not found" page.
*/

View File

@@ -168,6 +168,7 @@ export const theme = {
lightText: new CustomProp('theme-text-light', undefined, colors.slate),
darkText: new CustomProp('theme-text-dark', undefined, 'black'),
errorText: new CustomProp('theme-text-error', undefined, colors.error),
errorTextHover: new CustomProp('theme-text-error-hover', undefined, '#BF0A31'),
dangerText: new CustomProp('theme-text-danger', undefined, '#FFA500'),
disabledText: new CustomProp('theme-text-disabled', undefined, colors.slate),

View File

@@ -341,6 +341,8 @@ export interface ConfirmModalOptions {
hideCancel?: boolean;
extraButtons?: DomContents;
modalOptions?: IModalOptions;
saveDisabled?: Observable<boolean>;
width?: ModalWidth;
}
/**
@@ -352,7 +354,7 @@ export function confirmModal(
title: DomElementArg,
btnText: DomElementArg,
onConfirm: () => Promise<void>,
{explanation, hideCancel, extraButtons, modalOptions}: ConfirmModalOptions = {},
{explanation, hideCancel, extraButtons, modalOptions, saveDisabled, width}: ConfirmModalOptions = {},
): void {
return saveModal((ctl, owner): ISaveModalOptions => ({
title,
@@ -360,8 +362,9 @@ export function confirmModal(
saveLabel: btnText,
saveFunc: onConfirm,
hideCancel,
width: 'normal',
width: width ?? 'normal',
extraButtons,
saveDisabled,
}), modalOptions);
}