[NEW] Delete my account (#4219)

* create new delete account button

Co-Authored-By: Danish Ahmed Mirza <danishmirza30602@gmail.com>

* change modal to action sheet

* better naming

* remove ? from translation

* update translations

* change to new figma layout

* fix export

* remove unused state

* add new text input to base input

* clean up

* update bottom sheet and create a mock

* remove unecessary bracket and fix type

* fix header

* migrate buttons to action sheet

* fix imports

* update yarn.lock

* add separator to styles

* add ternary verification

* minor tweaks: keyboard for landscape android tablet, interface IactionSheetProvider and remove navigation options to get ismasterdetail from redux, fix jest setup

* fix colors

* disconnect from sdk when delete the account

* update snapshot

Co-authored-by: Danish Ahmed Mirza <danishmirza30602@gmail.com>
Co-authored-by: Reinaldo Neto <reinaldonetof@hotmail.com>
This commit is contained in:
Gleidson Daniel Silva 2022-06-22 09:24:25 -03:00 committed by GitHub
parent cd7e9e22f8
commit 5f248ebeb5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
26 changed files with 420 additions and 49 deletions

View File

@ -54,6 +54,7 @@ export const SERVER = createRequestTypes('SERVER', [
]); ]);
export const METEOR = createRequestTypes('METEOR_CONNECT', [...defaultTypes, 'DISCONNECT']); export const METEOR = createRequestTypes('METEOR_CONNECT', [...defaultTypes, 'DISCONNECT']);
export const LOGOUT = 'LOGOUT'; // logout is always success export const LOGOUT = 'LOGOUT'; // logout is always success
export const DELETE_ACCOUNT = 'DELETE_ACCOUNT';
export const SNIPPETED_MESSAGES = createRequestTypes('SNIPPETED_MESSAGES', ['OPEN', 'READY', 'CLOSE', 'MESSAGES_RECEIVED']); export const SNIPPETED_MESSAGES = createRequestTypes('SNIPPETED_MESSAGES', ['OPEN', 'READY', 'CLOSE', 'MESSAGES_RECEIVED']);
export const DEEP_LINKING = createRequestTypes('DEEP_LINKING', ['OPEN']); export const DEEP_LINKING = createRequestTypes('DEEP_LINKING', ['OPEN']);
export const SORT_PREFERENCES = createRequestTypes('SORT_PREFERENCES', ['SET_ALL', 'SET']); export const SORT_PREFERENCES = createRequestTypes('SORT_PREFERENCES', ['SET_ALL', 'SET']);

View File

@ -121,3 +121,9 @@ export function setLocalAuthenticated(isLocalAuthenticated: boolean): ISetLocalA
isLocalAuthenticated isLocalAuthenticated
}; };
} }
export function deleteAccount(): Action {
return {
type: types.DELETE_ACCOUNT
};
}

View File

@ -116,6 +116,10 @@ const ActionSheet = React.memo(
const bottomSheet = isLandscape || isTablet ? styles.bottomSheet : {}; const bottomSheet = isLandscape || isTablet ? styles.bottomSheet : {};
// Must need this prop to avoid keyboard dismiss
// when is android tablet and the input text is focused
const androidTablet: any = isTablet && isLandscape && !isIOS ? { android_keyboardInputMode: 'adjustResize' } : {};
return ( return (
<> <>
{children} {children}
@ -130,7 +134,8 @@ const ActionSheet = React.memo(
enablePanDownToClose enablePanDownToClose
style={{ ...styles.container, ...bottomSheet }} style={{ ...styles.container, ...bottomSheet }}
backgroundStyle={{ backgroundColor: colors.focusedBackground }} backgroundStyle={{ backgroundColor: colors.focusedBackground }}
onChange={index => index === -1 && toggleVisible()}> onChange={index => index === -1 && toggleVisible()}
{...androidTablet}>
<BottomSheetContent options={data?.options} hide={hide} children={data?.children} hasCancel={data?.hasCancel} /> <BottomSheetContent options={data?.options} hide={hide} children={data?.children} hasCancel={data?.hasCancel} />
</BottomSheet> </BottomSheet>
)} )}

View File

@ -0,0 +1,36 @@
import React from 'react';
import { View } from 'react-native';
import Button from '../Button';
import { useTheme } from '../../theme';
import styles from './styles';
const FooterButtons = ({
cancelAction = () => {},
confirmAction = () => {},
cancelTitle = '',
confirmTitle = '',
disabled = false,
cancelBackgroundColor = '',
confirmBackgroundColor = ''
}): React.ReactElement => {
const { colors } = useTheme();
return (
<View style={styles.footerButtonsContainer}>
<Button
style={[styles.buttonSeparator, { flex: 1, backgroundColor: cancelBackgroundColor || colors.cancelButton }]}
color={colors.backdropColor}
title={cancelTitle}
onPress={cancelAction}
/>
<Button
style={{ flex: 1, backgroundColor: confirmBackgroundColor || colors.dangerColor }}
title={confirmTitle}
onPress={confirmAction}
disabled={disabled}
/>
</View>
);
};
export default FooterButtons;

View File

@ -21,7 +21,7 @@ export type TActionSheetOptions = {
children?: React.ReactElement | null; children?: React.ReactElement | null;
snaps?: string[] | number[]; snaps?: string[] | number[];
}; };
interface IActionSheetProvider { export interface IActionSheetProvider {
showActionSheet: (item: TActionSheetOptions) => void; showActionSheet: (item: TActionSheetOptions) => void;
hideActionSheet: () => void; hideActionSheet: () => void;
} }

View File

@ -63,5 +63,12 @@ export default StyleSheet.create({
}, },
rightContainer: { rightContainer: {
paddingLeft: 12 paddingLeft: 12
},
footerButtonsContainer: {
flexDirection: 'row',
paddingTop: 16
},
buttonSeparator: {
marginRight: 8
} }
}); });

View File

@ -1,13 +1,14 @@
import { BottomSheetTextInput } from '@gorhom/bottom-sheet';
import React from 'react'; import React from 'react';
import { StyleProp, StyleSheet, Text, TextInputProps, TextInput as RNTextInput, TextStyle, View, ViewStyle } from 'react-native'; import { StyleProp, StyleSheet, Text, TextInput as RNTextInput, TextInputProps, TextStyle, View, ViewStyle } from 'react-native';
import Touchable from 'react-native-platform-touchable'; import Touchable from 'react-native-platform-touchable';
import sharedStyles from '../../views/Styles';
import TextInput from './index';
import { themes } from '../../lib/constants'; import { themes } from '../../lib/constants';
import { CustomIcon, TIconsName } from '../CustomIcon';
import ActivityIndicator from '../ActivityIndicator';
import { TSupportedThemes } from '../../theme'; import { TSupportedThemes } from '../../theme';
import sharedStyles from '../../views/Styles';
import ActivityIndicator from '../ActivityIndicator';
import { CustomIcon, TIconsName } from '../CustomIcon';
import TextInput from './index';
const styles = StyleSheet.create({ const styles = StyleSheet.create({
error: { error: {
@ -63,6 +64,7 @@ export interface IRCTextInputProps extends TextInputProps {
iconRight?: TIconsName; iconRight?: TIconsName;
left?: JSX.Element; left?: JSX.Element;
theme: TSupportedThemes; theme: TSupportedThemes;
bottomSheet?: boolean;
onClearInput?: () => void; onClearInput?: () => void;
} }
@ -153,16 +155,18 @@ export default class FormTextInput extends React.PureComponent<IRCTextInputProps
testID, testID,
placeholder, placeholder,
theme, theme,
bottomSheet,
...inputProps ...inputProps
} = this.props; } = this.props;
const { dangerColor } = themes[theme]; const { dangerColor } = themes[theme];
const Input = bottomSheet ? BottomSheetTextInput : TextInput;
return ( return (
<View style={[styles.inputContainer, containerStyle]}> <View style={[styles.inputContainer, containerStyle]}>
{label ? ( {label ? (
<Text style={[styles.label, { color: themes[theme].titleText }, error?.error && { color: dangerColor }]}>{label}</Text> <Text style={[styles.label, { color: themes[theme].titleText }, error?.error && { color: dangerColor }]}>{label}</Text>
) : null} ) : null}
<View style={styles.wrap}> <View style={styles.wrap}>
<TextInput <Input
style={[ style={[
styles.input, styles.input,
iconLeft && styles.inputIconLeft, iconLeft && styles.inputIconLeft,
@ -178,7 +182,8 @@ export default class FormTextInput extends React.PureComponent<IRCTextInputProps
}, },
inputStyle inputStyle
]} ]}
ref={inputRef} // @ts-ignore
ref={inputRef} // bottomSheetRef overlap default ref
autoCorrect={false} autoCorrect={false}
autoCapitalize='none' autoCapitalize='none'
underlineColorAndroid='transparent' underlineColorAndroid='transparent'

View File

@ -61,4 +61,7 @@ export type UsersEndpoints = {
success: boolean; success: boolean;
}; };
}; };
'users.deleteOwnAccount': {
POST: (params: { password: string; confirmRelinquish: boolean }) => { success: boolean };
};
}; };

View File

@ -1,4 +1,5 @@
{ { "__count__empty_rooms_will_be_removed_automatically": "{{count}} empty rooms will be deleted.",
"__count__empty_room_will_be_removed_automatically": "{{count}} empty room will be deleted.",
"1_person_reacted": "1 person reacted", "1_person_reacted": "1 person reacted",
"1_user": "1 user", "1_user": "1 user",
"error-action-not-allowed": "{{action}} is not allowed", "error-action-not-allowed": "{{action}} is not allowed",
@ -81,6 +82,8 @@
"error-user-registration-secret": "User registration is only allowed via Secret URL", "error-user-registration-secret": "User registration is only allowed via Secret URL",
"error-you-are-last-owner": "You are the last owner. Please set new owner before leaving the room.", "error-you-are-last-owner": "You are the last owner. Please set new owner before leaving the room.",
"error-status-not-allowed": "Invisible status is disabled", "error-status-not-allowed": "Invisible status is disabled",
"A_new_owner_will_be_assigned_automatically_to__count__rooms": "A new owner will be assigned automatically to {{count}} rooms.",
"A_new_owner_will_be_assigned_automatically_to__count__room": "A new owner will be assigned automatically to {{count}} room.",
"Actions": "Actions", "Actions": "Actions",
"Activity": "Activity", "Activity": "Activity",
"Add_Reaction": "Add Reaction", "Add_Reaction": "Add Reaction",
@ -107,6 +110,8 @@
"archive": "archive", "archive": "archive",
"are_typing": "are typing", "are_typing": "are typing",
"Are_you_sure_question_mark": "Are you sure?", "Are_you_sure_question_mark": "Are you sure?",
"Are_you_sure_you_want_to_delete_your_account":"Are you sure you want to delete your account?",
"Deleting_a_user_will_delete_all_messages":"Deleting a user will delete all messages, rooms and teams from that user as well. This cannot be undone.",
"Are_you_sure_you_want_to_leave_the_room": "Are you sure you want to leave the room {{room}}?", "Are_you_sure_you_want_to_leave_the_room": "Are you sure you want to leave the room {{room}}?",
"Audio": "Audio", "Audio": "Audio",
"Authenticating": "Authenticating", "Authenticating": "Authenticating",
@ -185,6 +190,8 @@
"delete": "delete", "delete": "delete",
"Delete": "Delete", "Delete": "Delete",
"DELETE": "DELETE", "DELETE": "DELETE",
"Delete_Account": "Delete account",
"Delete_Account_confirm": "Yes, Delete",
"move": "move", "move": "move",
"deleting_room": "deleting room", "deleting_room": "deleting room",
"description": "description", "description": "description",
@ -741,6 +748,8 @@
"Last_owner_team_room": "You are the last owner of this channel. Once you leave the team, the channel will be kept inside the team but you will be managing it from outside.", "Last_owner_team_room": "You are the last owner of this channel. Once you leave the team, the channel will be kept inside the team but you will be managing it from outside.",
"last-owner-can-not-be-removed": "Last owner cannot be removed", "last-owner-can-not-be-removed": "Last owner cannot be removed",
"Remove_User_Teams": "Select channels you want the user to be removed from.", "Remove_User_Teams": "Select channels you want the user to be removed from.",
"Deleting_account": "Deleting account",
"Delete_my_account": "Delete my account",
"Delete_Team": "Delete Team", "Delete_Team": "Delete Team",
"Select_channels_to_delete": "This can't be undone. Once you delete a team, all chat content and configuration will be deleted. \n\nSelect the channels you would like to delete. The ones you decide to keep will be available on your workspace. Notice that public channels will still be public and visible to everyone.", "Select_channels_to_delete": "This can't be undone. Once you delete a team, all chat content and configuration will be deleted. \n\nSelect the channels you would like to delete. The ones you decide to keep will be available on your workspace. Notice that public channels will still be public and visible to everyone.",
"You_are_deleting_the_team": "You are deleting this team.", "You_are_deleting_the_team": "You are deleting this team.",

View File

@ -1,4 +1,5 @@
{ {"__count__empty_rooms_will_be_removed_automatically": "{{count}} salas vazias serão excluídas.",
"__count__empty_room_will_be_removed_automatically": "{{count}} sala vazia será excluída.",
"1_person_reacted": "1 pessoa reagiu", "1_person_reacted": "1 pessoa reagiu",
"1_user": "1 usuário", "1_user": "1 usuário",
"error-action-not-allowed": "{{action}} não é permitido", "error-action-not-allowed": "{{action}} não é permitido",
@ -78,6 +79,8 @@
"error-user-registration-secret": "O registro de usuário é permitido somente via URL secreta", "error-user-registration-secret": "O registro de usuário é permitido somente via URL secreta",
"error-you-are-last-owner": "Você é o último proprietário da sala. Por favor defina um novo proprietário antes de sair.", "error-you-are-last-owner": "Você é o último proprietário da sala. Por favor defina um novo proprietário antes de sair.",
"error-status-not-allowed": "O status invisível está desativado", "error-status-not-allowed": "O status invisível está desativado",
"A_new_owner_will_be_assigned_automatically_to__count__rooms": "Um novo proprietário será atribuído automaticamente a {{count}} salas.",
"A_new_owner_will_be_assigned_automatically_to__count__room": "Um novo proprietário será atribuído automaticamente a {{count}} sala.",
"Actions": "Ações", "Actions": "Ações",
"Activity": "Atividade", "Activity": "Atividade",
"Add_Reaction": "Reagir", "Add_Reaction": "Reagir",
@ -102,6 +105,8 @@
"archive": "arquivar", "archive": "arquivar",
"are_typing": "estão digitando", "are_typing": "estão digitando",
"Are_you_sure_question_mark": "Você tem certeza?", "Are_you_sure_question_mark": "Você tem certeza?",
"Are_you_sure_you_want_to_delete_your_account":"Tem certeza de que deseja excluir sua conta?",
"Deleting_a_user_will_delete_all_messages":"A exclusão de um usuário também excluirá todas as mensagens, salas e equipes desse usuário. Isto não pode ser desfeito.",
"Are_you_sure_you_want_to_leave_the_room": "Tem certeza de que deseja sair da sala {{room}}?", "Are_you_sure_you_want_to_leave_the_room": "Tem certeza de que deseja sair da sala {{room}}?",
"Audio": "Áudio", "Audio": "Áudio",
"Authenticating": "Autenticando", "Authenticating": "Autenticando",
@ -174,6 +179,8 @@
"delete": "excluir", "delete": "excluir",
"Delete": "Excluir", "Delete": "Excluir",
"DELETE": "EXCLUIR", "DELETE": "EXCLUIR",
"Delete_Account": "Apagar conta",
"Delete_Account_confirm": "Sim, apagar conta",
"move": "mover", "move": "mover",
"deleting_room": "excluindo sala", "deleting_room": "excluindo sala",
"description": "descrição", "description": "descrição",
@ -684,6 +691,8 @@
"last-owner-can-not-be-removed": "O último dono não pode ser removido", "last-owner-can-not-be-removed": "O último dono não pode ser removido",
"Remove_User_Teams": "Selecione os canais dos quais você deseja que o usuário seja removido.", "Remove_User_Teams": "Selecione os canais dos quais você deseja que o usuário seja removido.",
"Delete_Team": "Excluir Time", "Delete_Team": "Excluir Time",
"Deleting_account": "Apagando conta",
"Delete_my_account": "Apagar minha conta",
"Select_channels_to_delete": "Isto não pode ser desfeito. Assim que você apagar um time, todo o conteúdo e configuração do chat serão apagados.\n\nSelecione os canais que você gostaria de excluir. Os que você decidir manter estarão disponíveis no seu espaço de trabalho. Note que os canais públicos continuarão a ser públicos e visíveis para todos.", "Select_channels_to_delete": "Isto não pode ser desfeito. Assim que você apagar um time, todo o conteúdo e configuração do chat serão apagados.\n\nSelecione os canais que você gostaria de excluir. Os que você decidir manter estarão disponíveis no seu espaço de trabalho. Note que os canais públicos continuarão a ser públicos e visíveis para todos.",
"You_are_deleting_the_team": "Você está apagando este time.", "You_are_deleting_the_team": "Você está apagando este time.",
"Removing_user_from_this_team": "Você está removendo {{user}} deste time", "Removing_user_from_this_team": "Você está removendo {{user}} deste time",

View File

@ -69,6 +69,7 @@ export const colors = {
attachmentLoadingOpacity: 0.7, attachmentLoadingOpacity: 0.7,
collapsibleQuoteBorder: '#CBCED1', collapsibleQuoteBorder: '#CBCED1',
collapsibleChevron: '#6C727A', collapsibleChevron: '#6C727A',
cancelButton: '#E4E7EA',
...mentions ...mentions
}, },
dark: { dark: {
@ -120,6 +121,7 @@ export const colors = {
attachmentLoadingOpacity: 0.3, attachmentLoadingOpacity: 0.3,
collapsibleQuoteBorder: '#CBCED1', collapsibleQuoteBorder: '#CBCED1',
collapsibleChevron: '#6C727A', collapsibleChevron: '#6C727A',
cancelButton: '#E4E7EA',
...mentions ...mentions
}, },
black: { black: {
@ -171,6 +173,7 @@ export const colors = {
attachmentLoadingOpacity: 0.3, attachmentLoadingOpacity: 0.3,
collapsibleQuoteBorder: '#CBCED1', collapsibleQuoteBorder: '#CBCED1',
collapsibleChevron: '#6C727A', collapsibleChevron: '#6C727A',
cancelButton: '#E4E7EA',
...mentions ...mentions
} }
}; };

View File

@ -211,5 +211,8 @@ export const defaultSettings = {
}, },
Accounts_AvatarExternalProviderUrl: { Accounts_AvatarExternalProviderUrl: {
type: 'valueAsString' type: 'valueAsString'
},
Accounts_AllowDeleteOwnAccount: {
type: 'valueAsBoolean'
} }
} as const; } as const;

View File

@ -349,5 +349,9 @@ export default {
TC_DELETE_ROOM: 'tc_delete_room', TC_DELETE_ROOM: 'tc_delete_room',
TC_DELETE_ROOM_F: 'tc_delete_room_f', TC_DELETE_ROOM_F: 'tc_delete_room_f',
TC_TOGGLE_AUTOJOIN: 'tc_toggle_autojoin', TC_TOGGLE_AUTOJOIN: 'tc_toggle_autojoin',
TC_TOGGLE_AUTOJOIN_F: 'tc_toggle_autojoin_f' TC_TOGGLE_AUTOJOIN_F: 'tc_toggle_autojoin_f',
// DELETE OWN ACCOUNT ACCOUNT
DELETE_OWN_ACCOUNT: 'delete_own_account',
DELETE_OWN_ACCOUNT_F: 'delete_own_account_f'
}; };

View File

@ -39,7 +39,7 @@ async function removeSharedCredentials({ server }: { server: string }) {
} }
} }
async function removeServerData({ server }: { server: string }) { export async function removeServerData({ server }: { server: string }): Promise<void> {
try { try {
const batch: Model[] = []; const batch: Model[] = [];
const serversDB = database.servers; const serversDB = database.servers;
@ -66,7 +66,7 @@ function removeCurrentServer() {
UserPreferences.removeItem(CURRENT_SERVER); UserPreferences.removeItem(CURRENT_SERVER);
} }
async function removeServerDatabase({ server }: { server: string }) { export async function removeServerDatabase({ server }: { server: string }): Promise<void> {
try { try {
const db = getDatabase(server); const db = getDatabase(server);
await db.write(() => db.unsafeResetDatabase()); await db.write(() => db.unsafeResetDatabase());

View File

@ -917,3 +917,7 @@ export function getUserInfo(userId: string) {
} }
export const toggleFavorite = (roomId: string, favorite: boolean) => sdk.post('rooms.favorite', { roomId, favorite }); export const toggleFavorite = (roomId: string, favorite: boolean) => sdk.post('rooms.favorite', { roomId, favorite });
export const deleteOwnAccount = (password: string, confirmRelinquish = false): any =>
// RC 0.67.0
sdk.post('users.deleteOwnAccount', { password, confirmRelinquish });

View File

@ -113,6 +113,11 @@ export default function login(state = initialState, action: TActionsLogin): ILog
...state, ...state,
isLocalAuthenticated: action.isLocalAuthenticated isLocalAuthenticated: action.isLocalAuthenticated
}; };
case types.DELETE_ACCOUNT:
return {
...state,
isLocalAuthenticated: false
};
default: default:
return state; return state;
} }

View File

@ -30,6 +30,8 @@ import {
getUserPresence, getUserPresence,
isOmnichannelModuleAvailable, isOmnichannelModuleAvailable,
logout, logout,
removeServerData,
removeServerDatabase,
subscribeSettings, subscribeSettings,
subscribeUsersPresence subscribeUsersPresence
} from '../lib/methods'; } from '../lib/methods';
@ -247,10 +249,46 @@ const handleSetUser = function* handleSetUser({ user }) {
} }
}; };
const handleDeleteAccount = function* handleDeleteAccount() {
yield put(encryptionStop());
yield put(appStart({ root: RootEnum.ROOT_LOADING, text: I18n.t('Deleting_account') }));
const server = yield select(getServer);
if (server) {
try {
yield call(removeServerData, { server });
yield call(removeServerDatabase, { server });
const serversDB = database.servers;
// all servers
const serversCollection = serversDB.get('servers');
const servers = yield serversCollection.query().fetch();
// see if there're other logged in servers and selects first one
if (servers.length > 0) {
for (let i = 0; i < servers.length; i += 1) {
const newServer = servers[i].id;
const token = UserPreferences.getString(`${TOKEN_KEY}-${newServer}`);
if (token) {
yield put(selectServerRequest(newServer));
return;
}
}
}
// if there's no servers, go outside
sdk.disconnect();
yield put(appStart({ root: RootEnum.ROOT_OUTSIDE }));
} catch (e) {
sdk.disconnect();
yield put(appStart({ root: RootEnum.ROOT_OUTSIDE }));
log(e);
}
}
};
const root = function* root() { const root = function* root() {
yield takeLatest(types.LOGIN.REQUEST, handleLoginRequest); yield takeLatest(types.LOGIN.REQUEST, handleLoginRequest);
yield takeLatest(types.LOGOUT, handleLogout); yield takeLatest(types.LOGOUT, handleLogout);
yield takeLatest(types.USER.SET, handleSetUser); yield takeLatest(types.USER.SET, handleSetUser);
yield takeLatest(types.DELETE_ACCOUNT, handleDeleteAccount);
while (true) { while (true) {
const params = yield take(types.LOGIN.SUCCESS); const params = yield take(types.LOGIN.SUCCESS);

View File

@ -191,11 +191,7 @@ const ModalStackNavigator = React.memo(({ navigation }: INavigation) => {
options={ScreenLockConfigView.navigationOptions} options={ScreenLockConfigView.navigationOptions}
/> />
<ModalStack.Screen name='StatusView' component={StatusView} /> <ModalStack.Screen name='StatusView' component={StatusView} />
<ModalStack.Screen <ModalStack.Screen name='ProfileView' component={ProfileView} />
name='ProfileView'
component={ProfileView}
options={props => ProfileView.navigationOptions!({ ...props, isMasterDetail: true })}
/>
<ModalStack.Screen name='DisplayPrefsView' component={DisplayPrefsView} /> <ModalStack.Screen name='DisplayPrefsView' component={DisplayPrefsView} />
<ModalStack.Screen name='AdminPanelView' component={AdminPanelView} /> <ModalStack.Screen name='AdminPanelView' component={AdminPanelView} />
<ModalStack.Screen name='NewMessageView' component={NewMessageView} options={NewMessageView.navigationOptions} /> <ModalStack.Screen name='NewMessageView' component={NewMessageView} options={NewMessageView.navigationOptions} />

View File

@ -0,0 +1,36 @@
import i18n from '../../../../i18n';
export const getTranslations = ({
shouldChangeOwner,
shouldBeRemoved
}: {
shouldChangeOwner: string[];
shouldBeRemoved: string[];
}): { changeOwnerRooms: string; removedRooms: string } => {
let changeOwnerRooms = '';
if (shouldChangeOwner.length) {
if (shouldChangeOwner.length === 1) {
changeOwnerRooms = i18n.t('A_new_owner_will_be_assigned_automatically_to__count__room', {
count: shouldChangeOwner.length
});
} else {
changeOwnerRooms = i18n.t('A_new_owner_will_be_assigned_automatically_to__count__rooms', {
count: shouldChangeOwner.length
});
}
}
let removedRooms = '';
if (shouldBeRemoved.length) {
if (shouldBeRemoved.length === 1) {
removedRooms = i18n.t('__count__empty_room_will_be_removed_automatically', {
count: shouldBeRemoved.length
});
} else {
removedRooms = i18n.t('__count__empty_rooms_will_be_removed_automatically', {
count: shouldBeRemoved.length
});
}
}
return { changeOwnerRooms, removedRooms };
};

View File

@ -0,0 +1,128 @@
import { sha256 } from 'js-sha256';
import React, { useState } from 'react';
import { Keyboard, Text, View } from 'react-native';
import { useSafeAreaInsets } from 'react-native-safe-area-context';
import { useDispatch } from 'react-redux';
import { deleteAccount } from '../../../../actions/login';
import { useActionSheet } from '../../../../containers/ActionSheet';
import FooterButtons from '../../../../containers/ActionSheet/FooterButtons';
import { CustomIcon } from '../../../../containers/CustomIcon';
import FormTextInput from '../../../../containers/TextInput/FormTextInput';
import i18n from '../../../../i18n';
import { showErrorAlert } from '../../../../lib/methods/helpers';
import { events, logEvent } from '../../../../lib/methods/helpers/log';
import { deleteOwnAccount } from '../../../../lib/services/restApi';
import { useTheme } from '../../../../theme';
import { getTranslations } from './getTranslations';
import styles from './styles';
const AlertHeader = ({ title = '', subTitle = '' }) => {
const { colors } = useTheme();
return (
<>
<View style={styles.titleContainer}>
<CustomIcon name='warning' size={32} color={colors.dangerColor} />
<Text style={[styles.titleContainerText, { color: colors.passcodePrimary }]}>{title}</Text>
</View>
<Text style={[styles.subTitleContainerText, { color: colors.passcodePrimary }]}>{subTitle}</Text>
</>
);
};
export function DeleteAccountActionSheetContent(): React.ReactElement {
const [password, setPassword] = useState('');
const { theme } = useTheme();
const { hideActionSheet, showActionSheet } = useActionSheet();
const dispatch = useDispatch();
const insets = useSafeAreaInsets();
const handleDeleteAccount = async () => {
Keyboard.dismiss();
try {
await deleteOwnAccount(sha256(password));
hideActionSheet();
} catch (error: any) {
hideActionSheet();
if (error.data.errorType === 'user-last-owner') {
const { shouldChangeOwner, shouldBeRemoved } = error.data.details;
const { changeOwnerRooms, removedRooms } = getTranslations({ shouldChangeOwner, shouldBeRemoved });
setTimeout(() => {
showActionSheet({
children: (
<ConfirmDeleteAccountActionSheetContent
changeOwnerRooms={changeOwnerRooms}
removedRooms={removedRooms}
password={sha256(password)}
/>
),
headerHeight: 225 + insets.bottom
});
}, 250); // timeout for hide effect
} else if (error.data.errorType === 'error-invalid-password') {
logEvent(events.DELETE_OWN_ACCOUNT_F);
showErrorAlert(i18n.t('error-invalid-password'));
} else {
logEvent(events.DELETE_OWN_ACCOUNT_F);
showErrorAlert(i18n.t(error.data.details));
}
return;
}
dispatch(deleteAccount());
};
return (
<View style={styles.container}>
<AlertHeader
title={i18n.t('Are_you_sure_you_want_to_delete_your_account')}
subTitle={i18n.t('For_your_security_you_must_enter_your_current_password_to_continue')}
/>
<FormTextInput
value={password}
placeholder={i18n.t('Password')}
onChangeText={value => setPassword(value)}
onSubmitEditing={handleDeleteAccount}
theme={theme}
testID='room-info-edit-view-name'
secureTextEntry
inputStyle={{ borderWidth: 2 }}
bottomSheet
/>
<FooterButtons
cancelTitle={i18n.t('Cancel')}
cancelAction={hideActionSheet}
confirmTitle={i18n.t('Delete_Account')}
confirmAction={handleDeleteAccount}
disabled={!password}
/>
</View>
);
}
function ConfirmDeleteAccountActionSheetContent({ changeOwnerRooms = '', removedRooms = '', password = '' }) {
const { colors } = useTheme();
const { hideActionSheet } = useActionSheet();
const dispatch = useDispatch();
const handleDeleteAccount = async () => {
hideActionSheet();
await deleteOwnAccount(password, true);
dispatch(deleteAccount());
};
return (
<View style={styles.container}>
<AlertHeader title={i18n.t('Are_you_sure_question_mark')} subTitle={i18n.t('Deleting_a_user_will_delete_all_messages')} />
{!!changeOwnerRooms && (
<Text style={{ ...styles.subTitleContainerText, color: colors.dangerColor }}>{changeOwnerRooms}</Text>
)}
{!!removedRooms && <Text style={{ ...styles.subTitleContainerText, color: colors.dangerColor }}>{removedRooms}</Text>}
<FooterButtons
cancelTitle={i18n.t('Cancel')}
cancelAction={hideActionSheet}
confirmTitle={i18n.t('Delete_Account_confirm')}
confirmAction={handleDeleteAccount}
/>
</View>
);
}

View File

@ -0,0 +1,28 @@
import { StyleSheet } from 'react-native';
import sharedStyles from '../../../Styles';
export default StyleSheet.create({
container: {
paddingTop: 16,
paddingLeft: 16,
paddingRight: 16,
paddingBottom: 32
},
titleContainer: {
paddingRight: 80,
marginBottom: 16,
flexDirection: 'row',
alignItems: 'center'
},
titleContainerText: {
fontSize: 16,
...sharedStyles.textSemibold,
paddingLeft: 16
},
subTitleContainerText: {
fontSize: 14,
...sharedStyles.textRegular,
marginBottom: 10
}
});

View File

@ -41,8 +41,10 @@ import {
IProfileParams, IProfileParams,
IUser IUser
} from '../../definitions'; } from '../../definitions';
import { withActionSheet, IActionSheetProvider } from '../../containers/ActionSheet';
import { DeleteAccountActionSheetContent } from './components/DeleteAccountActionSheetContent';
interface IProfileViewProps extends IBaseScreen<ProfileStackParamList, 'ProfileView'> { interface IProfileViewProps extends IActionSheetProvider, IBaseScreen<ProfileStackParamList, 'ProfileView'> {
user: IUser; user: IUser;
baseUrl: string; baseUrl: string;
Accounts_AllowEmailChange: boolean; Accounts_AllowEmailChange: boolean;
@ -52,6 +54,8 @@ interface IProfileViewProps extends IBaseScreen<ProfileStackParamList, 'ProfileV
Accounts_AllowUsernameChange: boolean; Accounts_AllowUsernameChange: boolean;
Accounts_CustomFields: string; Accounts_CustomFields: string;
theme: TSupportedThemes; theme: TSupportedThemes;
Accounts_AllowDeleteOwnAccount: boolean;
isMasterDetail: boolean;
} }
interface IProfileViewState { interface IProfileViewState {
@ -76,7 +80,8 @@ class ProfileView extends React.Component<IProfileViewProps, IProfileViewState>
private avatarUrl?: TextInput; private avatarUrl?: TextInput;
private newPassword?: TextInput; private newPassword?: TextInput;
static navigationOptions = ({ navigation, isMasterDetail }: IProfileViewProps) => { setHeader = () => {
const { navigation, isMasterDetail } = this.props;
const options: StackNavigationOptions = { const options: StackNavigationOptions = {
title: I18n.t('Profile') title: I18n.t('Profile')
}; };
@ -86,9 +91,15 @@ class ProfileView extends React.Component<IProfileViewProps, IProfileViewState>
options.headerRight = () => ( options.headerRight = () => (
<HeaderButton.Preferences onPress={() => navigation?.navigate('UserPreferencesView')} testID='preferences-view-open' /> <HeaderButton.Preferences onPress={() => navigation?.navigate('UserPreferencesView')} testID='preferences-view-open' />
); );
return options;
navigation.setOptions(options);
}; };
constructor(props: IProfileViewProps) {
super(props);
this.setHeader();
}
state: IProfileViewState = { state: IProfileViewState = {
saving: false, saving: false,
name: '', name: '',
@ -475,6 +486,14 @@ class ProfileView extends React.Component<IProfileViewProps, IProfileViewState>
}); });
}; };
deleteOwnAccount = () => {
logEvent(events.DELETE_OWN_ACCOUNT);
this.props.showActionSheet({
children: <DeleteAccountActionSheetContent />,
headerHeight: 225
});
};
render() { render() {
const { name, username, email, newPassword, avatarUrl, customFields, avatar, saving } = this.state; const { name, username, email, newPassword, avatarUrl, customFields, avatar, saving } = this.state;
const { const {
@ -485,7 +504,8 @@ class ProfileView extends React.Component<IProfileViewProps, IProfileViewState>
Accounts_AllowRealNameChange, Accounts_AllowRealNameChange,
Accounts_AllowUserAvatarChange, Accounts_AllowUserAvatarChange,
Accounts_AllowUsernameChange, Accounts_AllowUsernameChange,
Accounts_CustomFields Accounts_CustomFields,
Accounts_AllowDeleteOwnAccount
} = this.props; } = this.props;
return ( return (
@ -605,6 +625,15 @@ class ProfileView extends React.Component<IProfileViewProps, IProfileViewState>
onPress={this.logoutOtherLocations} onPress={this.logoutOtherLocations}
testID='profile-view-logout-other-locations' testID='profile-view-logout-other-locations'
/> />
{Accounts_AllowDeleteOwnAccount ? (
<Button
title={I18n.t('Delete_my_account')}
type='primary'
backgroundColor={themes[theme].dangerColor}
onPress={this.deleteOwnAccount}
testID='profile-view-delete-my-account'
/>
) : null}
</ScrollView> </ScrollView>
</SafeAreaView> </SafeAreaView>
</KeyboardView> </KeyboardView>
@ -620,7 +649,9 @@ const mapStateToProps = (state: IApplicationState) => ({
Accounts_AllowUserAvatarChange: state.settings.Accounts_AllowUserAvatarChange as boolean, Accounts_AllowUserAvatarChange: state.settings.Accounts_AllowUserAvatarChange as boolean,
Accounts_AllowUsernameChange: state.settings.Accounts_AllowUsernameChange as boolean, Accounts_AllowUsernameChange: state.settings.Accounts_AllowUsernameChange as boolean,
Accounts_CustomFields: state.settings.Accounts_CustomFields as string, Accounts_CustomFields: state.settings.Accounts_CustomFields as string,
baseUrl: state.server.server baseUrl: state.server.server,
Accounts_AllowDeleteOwnAccount: state.settings.Accounts_AllowDeleteOwnAccount as boolean,
isMasterDetail: state.app.isMasterDetail
}); });
export default connect(mapStateToProps)(withTheme(ProfileView)); export default connect(mapStateToProps)(withTheme(withActionSheet(ProfileView)));

View File

@ -59,3 +59,12 @@ jest.mock('react-native-notifications', () => ({
}) })
} }
})); }));
jest.mock('@gorhom/bottom-sheet', () => {
const react = require('react-native');
return {
__esModule: true,
default: react.View,
BottomSheetScrollView: react.ScrollView
};
});

View File

@ -30,7 +30,7 @@
"dependencies": { "dependencies": {
"@bugsnag/react-native": "^7.10.5", "@bugsnag/react-native": "^7.10.5",
"@codler/react-native-keyboard-aware-scroll-view": "^1.0.1", "@codler/react-native-keyboard-aware-scroll-view": "^1.0.1",
"@gorhom/bottom-sheet": "^4", "@gorhom/bottom-sheet": "^4.3.1",
"@nozbe/watermelondb": "0.23.0", "@nozbe/watermelondb": "0.23.0",
"@react-native-clipboard/clipboard": "^1.8.5", "@react-native-clipboard/clipboard": "^1.8.5",
"@react-native-community/art": "^1.2.0", "@react-native-community/art": "^1.2.0",

File diff suppressed because one or more lines are too long

View File

@ -3018,22 +3018,22 @@
resolved "https://registry.yarnpkg.com/@expo/sdk-runtime-versions/-/sdk-runtime-versions-1.0.0.tgz#d7ebd21b19f1c6b0395e50d78da4416941c57f7c" resolved "https://registry.yarnpkg.com/@expo/sdk-runtime-versions/-/sdk-runtime-versions-1.0.0.tgz#d7ebd21b19f1c6b0395e50d78da4416941c57f7c"
integrity sha512-Doz2bfiPndXYFPMRwPyGa1k5QaKDVpY806UJj570epIiMzWaYyCtobasyfC++qfIXVb5Ocy7r3tP9d62hAQ7IQ== integrity sha512-Doz2bfiPndXYFPMRwPyGa1k5QaKDVpY806UJj570epIiMzWaYyCtobasyfC++qfIXVb5Ocy7r3tP9d62hAQ7IQ==
"@gorhom/bottom-sheet@^4": "@gorhom/bottom-sheet@^4.3.1":
version "4.1.5" version "4.3.2"
resolved "https://registry.yarnpkg.com/@gorhom/bottom-sheet/-/bottom-sheet-4.1.5.tgz#35341d45799de28082c380db6639537b04fa0b26" resolved "https://registry.yarnpkg.com/@gorhom/bottom-sheet/-/bottom-sheet-4.3.2.tgz#83de68387f54f6e3cc9419329b8e4f925a3426c8"
integrity sha512-3F5P8jK3NXwT2lGwkAdkdLwDVHaRvMZalUTXjK6Ogf0Tki6idffJ3TNlQZlg8k6+OnXAx0+i80f4XaI+J4GFrA== integrity sha512-kOnxKz3TuxbwagxhHvAyo2b1fgUCzw/Xs3OQD2lTE7vjGEdibsYnRaBxdpKKLV25VOQA6zVFmZq9apICj9foPw==
dependencies: dependencies:
"@gorhom/portal" "^1.0.11" "@gorhom/portal" "1.0.13"
invariant "^2.2.4" invariant "^2.2.4"
nanoid "^3.1.20" nanoid "^3.3.3"
react-native-redash "^16.1.1" react-native-redash "^16.1.1"
"@gorhom/portal@^1.0.11": "@gorhom/portal@1.0.13":
version "1.0.12" version "1.0.13"
resolved "https://registry.yarnpkg.com/@gorhom/portal/-/portal-1.0.12.tgz#1c0deabb3f9057c736352a88bae9ca891a100346" resolved "https://registry.yarnpkg.com/@gorhom/portal/-/portal-1.0.13.tgz#da3af4d427e1fa68d264107de4b3072a4adf35ce"
integrity sha512-JOYe85RUwiksgdMbhLWDCLpH3kgFFz+LCu1lnxOMMBQSfAKtL5kkTKVrhtmQ3Lq3lJM2paGnLc4wJrlVuaC5Jw== integrity sha512-ViClKPkyGnj8HVMW45OGQSnGbWBVh8i3tgMOkGqpm6Cv0WVcDfUL7SER6zyGQy8Wdoj3GUDpAJFMqVOxpmRpzw==
dependencies: dependencies:
nanoid "^3.1.23" nanoid "^3.3.1"
"@hapi/hoek@^9.0.0": "@hapi/hoek@^9.0.0":
version "9.2.1" version "9.2.1"
@ -13485,11 +13485,16 @@ nanoid@3.1.23:
resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.1.23.tgz#f744086ce7c2bc47ee0a8472574d5c78e4183a81" resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.1.23.tgz#f744086ce7c2bc47ee0a8472574d5c78e4183a81"
integrity sha512-FiB0kzdP0FFVGDKlRLEQ1BgDzU87dy5NnzjeW9YZNt+/c3+q82EQDUwniSAUxp/F0gFNI1ZhKU1FqYsMuqZVnw== integrity sha512-FiB0kzdP0FFVGDKlRLEQ1BgDzU87dy5NnzjeW9YZNt+/c3+q82EQDUwniSAUxp/F0gFNI1ZhKU1FqYsMuqZVnw==
nanoid@^3.1.20, nanoid@^3.1.23: nanoid@^3.1.23:
version "3.3.1" version "3.3.1"
resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.1.tgz#6347a18cac88af88f58af0b3594b723d5e99bb35" resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.1.tgz#6347a18cac88af88f58af0b3594b723d5e99bb35"
integrity sha512-n6Vs/3KGyxPQd6uO0eH4Bv0ojGSUvuLlIHtC3Y0kEO23YRge8H9x1GCzLn28YX0H66pMkxuaeESFq4tKISKwdw== integrity sha512-n6Vs/3KGyxPQd6uO0eH4Bv0ojGSUvuLlIHtC3Y0kEO23YRge8H9x1GCzLn28YX0H66pMkxuaeESFq4tKISKwdw==
nanoid@^3.3.1, nanoid@^3.3.3:
version "3.3.4"
resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.4.tgz#730b67e3cd09e2deacf03c027c81c9d9dbc5e8ab"
integrity sha512-MqBkQh/OHTS2egovRtLk45wEyNXwF+cokD+1YPf9u5VfJiRdAiRwB2froX5Co9Rh20xs4siNPm8naNotSD6RBw==
nanomatch@^1.2.9: nanomatch@^1.2.9:
version "1.2.13" version "1.2.13"
resolved "https://registry.yarnpkg.com/nanomatch/-/nanomatch-1.2.13.tgz#b87a8aa4fc0de8fe6be88895b38983ff265bd119" resolved "https://registry.yarnpkg.com/nanomatch/-/nanomatch-1.2.13.tgz#b87a8aa4fc0de8fe6be88895b38983ff265bd119"