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

feat: Remove Account Snap Warning (Flask) #11451

Merged
merged 20 commits into from
Oct 23, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
62 changes: 61 additions & 1 deletion app/components/Views/AccountActions/AccountActions.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,12 @@ import { protectWalletModalVisible } from '../../../actions/user';
import Routes from '../../../constants/navigation/Routes';
import { AccountActionsModalSelectorsIDs } from '../../../../e2e/selectors/Modals/AccountActionsModal.selectors';
import { useMetrics } from '../../../components/hooks/useMetrics';
import { isHardwareAccount } from '../../../util/address';
import {
isHardwareAccount,
///: BEGIN:ONLY_INCLUDE_IF(keyring-snaps)
isSnapAccount,
///: END:ONLY_INCLUDE_IF
} from '../../../util/address';
import { removeAccountsFromPermissions } from '../../../core/Permissions';
import ExtendedKeyringTypes, {
HardwareDeviceTypes,
Expand Down Expand Up @@ -189,6 +194,49 @@ const AccountActions = () => {
}
}, [controllers.KeyringController]);

///: BEGIN:ONLY_INCLUDE_IF(keyring-snaps)

/**
* Remove the snap account from the keyring
*/
const removeSnapAccount = useCallback(async () => {
owencraston marked this conversation as resolved.
Show resolved Hide resolved
if (selectedAddress) {
await controllers.KeyringController.removeAccount(selectedAddress as Hex);
await removeAccountsFromPermissions([selectedAddress]);
trackEvent(MetaMetricsEvents.ACCOUNT_REMOVED, {
accountType: keyring?.type,
selectedAddress,
});
}
}, [
controllers.KeyringController,
keyring?.type,
selectedAddress,
trackEvent,
]);

const showRemoveSnapAccountAlert = useCallback(() => {
Alert.alert(
strings('accounts.remove_snap_account'),
strings('accounts.remove_snap_account_alert_description'),
[
{
text: strings('accounts.remove_account_alert_cancel_btn'),
style: 'cancel',
},
{
text: strings('accounts.remove_account_alert_remove_btn'),
onPress: async () => {
sheetRef.current?.onCloseBottomSheet(async () => {
await removeSnapAccount();
});
},
},
],
);
}, [removeSnapAccount]);
///: END:ONLY_INCLUDE_IF

/**
* Forget the device if there are no more accounts in the keyring
* @param keyringType - The keyring type
Expand Down Expand Up @@ -306,6 +354,18 @@ const AccountActions = () => {
testID={AccountActionsModalSelectorsIDs.REMOVE_HARDWARE_ACCOUNT}
/>
)}
{
///: BEGIN:ONLY_INCLUDE_IF(keyring-snaps)
selectedAddress && isSnapAccount(selectedAddress) && (
<AccountAction
actionTitle={strings('accounts.remove_snap_account')}
iconName={IconName.Close}
onPress={showRemoveSnapAccountAlert}
testID={AccountActionsModalSelectorsIDs.REMOVE_SNAP_ACCOUNT}
/>
)
///: END:ONLY_INCLUDE_IF
}
</View>
<BlockingActionModal
modalVisible={blockingModalVisible}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
///: BEGIN:ONLY_INCLUDE_IF(keyring-snaps)
export const KEYRING_SNAP_REMOVAL_WARNING = 'keyring-snap-removal-warning';
export const KEYRING_SNAP_REMOVAL_WARNING_CONTINUE =
'keyring-snap-removal-warning-continue';
export const KEYRING_SNAP_REMOVAL_WARNING_CANCEL =
'keyring-snap-removal-warning-cancel';
export const KEYRING_SNAP_REMOVAL_WARNING_TEXT_INPUT =
'keyring-snap-removal-warning-text-input';
///: END:ONLY_INCLUDE_IF
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
///: BEGIN:ONLY_INCLUDE_IF(keyring-snaps)
import { StyleSheet } from 'react-native';
import { Theme } from '../../../../util/theme/models';

const styleSheet = (params: { theme: Theme }) => {
const { theme } = params;
const { colors } = theme;

return StyleSheet.create({
bottomSheet: {
flex: 1,
},
container: {
paddingHorizontal: 16,
},
description: {
paddingVertical: 8,
},
buttonContainer: {
paddingTop: 16,
},
input: {
borderWidth: 1,
borderColor: colors.border.default,
borderRadius: 4,
padding: 10,
marginVertical: 10,
},
errorText: {
color: colors.error.default,
},
placeholderText: {
color: colors.text.muted,
},
scrollView: {
flexGrow: 1,
maxHeight: 300,
},
});
};

export default styleSheet;
///: END:ONLY_INCLUDE_IF
Original file line number Diff line number Diff line change
@@ -0,0 +1,226 @@
///: BEGIN:ONLY_INCLUDE_IF(keyring-snaps)
import React, {
useEffect,
useRef,
useState,
useCallback,
useMemo,
} from 'react';
import { View, TextInput, ScrollView } from 'react-native';
import { NativeViewGestureHandler } from 'react-native-gesture-handler';
import { Snap } from '@metamask/snaps-utils';
import BottomSheet, {
BottomSheetRef,
} from '../../../../component-library/components/BottomSheets/BottomSheet';
import Text, {
TextVariant,
} from '../../../../component-library/components/Texts/Text';
import { InternalAccount } from '@metamask/keyring-api';
import BannerAlert from '../../../../component-library/components/Banners/Banner/variants/BannerAlert';
import { BannerAlertSeverity } from '../../../../component-library/components/Banners/Banner';
import BottomSheetHeader from '../../../../component-library/components/BottomSheets/BottomSheetHeader';
import { useStyles } from '../../../hooks/useStyles';
import stylesheet from './KeyringSnapRemovalWarning.styles';
import { strings } from '../../../../../locales/i18n';
import { KeyringAccountListItem } from '../components/KeyringAccountListItem';
import { getAccountLink } from '@metamask/etherscan-link';
import { useSelector } from 'react-redux';
import { selectProviderConfig } from '../../../../selectors/networkController';
import BottomSheetFooter, {
ButtonsAlignment,
} from '../../../../component-library/components/BottomSheets/BottomSheetFooter';
import {
ButtonProps,
ButtonSize,
ButtonVariants,
} from '../../../../component-library/components/Buttons/Button/Button.types';
import {
KEYRING_SNAP_REMOVAL_WARNING,
KEYRING_SNAP_REMOVAL_WARNING_CANCEL,
KEYRING_SNAP_REMOVAL_WARNING_CONTINUE,
KEYRING_SNAP_REMOVAL_WARNING_TEXT_INPUT,
} from './KeyringSnapRemovalWarning.constants';
import Logger from '../../../../util/Logger';

interface KeyringSnapRemovalWarningProps {
snap: Snap;
keyringAccounts: InternalAccount[];
onCancel: () => void;
onClose: () => void;
onSubmit: () => void;
}

export default function KeyringSnapRemovalWarning({
snap,
keyringAccounts,
onCancel,
onClose,
onSubmit,
}: KeyringSnapRemovalWarningProps) {
const [showConfirmation, setShowConfirmation] = useState(false);
const [confirmedRemoval, setConfirmedRemoval] = useState(false);
const [confirmationInput, setConfirmationInput] = useState('');
const [error, setError] = useState(false);
const { chainId } = useSelector(selectProviderConfig);
const { styles } = useStyles(stylesheet, {});
const bottomSheetRef = useRef<BottomSheetRef>(null);

useEffect(() => {
setShowConfirmation(keyringAccounts.length === 0);
}, [keyringAccounts]);

const validateConfirmationInput = useCallback(
(input: string): boolean => input === snap.manifest.proposedName,
[snap.manifest.proposedName],
);

const handleConfirmationInputChange = useCallback(
(text: string) => {
setConfirmationInput(text);
setConfirmedRemoval(validateConfirmationInput(text));
},
[validateConfirmationInput],
);

const handleContinuePress = useCallback(() => {
if (!showConfirmation) {
setShowConfirmation(true);
} else if (confirmedRemoval) {
try {
onSubmit();
} catch (e) {
Logger.error(
e as Error,
'KeyringSnapRemovalWarning: error while removing snap',
);
setError(true);
owencraston marked this conversation as resolved.
Show resolved Hide resolved
}
}
}, [showConfirmation, confirmedRemoval, onSubmit]);

const cancelButtonProps: ButtonProps = useMemo(
() => ({
variant: ButtonVariants.Secondary,
label: strings(
'app_settings.snaps.snap_settings.remove_account_snap_warning.cancel_button',
),
size: ButtonSize.Lg,
onPress: onCancel,
testID: KEYRING_SNAP_REMOVAL_WARNING_CANCEL,
}),
[onCancel],
);

const continueButtonProps: ButtonProps = useMemo(
() => ({
variant: ButtonVariants.Primary,
label: showConfirmation
? strings(
'app_settings.snaps.snap_settings.remove_account_snap_warning.remove_snap_button',
)
: strings(
'app_settings.snaps.snap_settings.remove_account_snap_warning.continue_button',
),
size: ButtonSize.Lg,
onPress: handleContinuePress,
isDisabled: showConfirmation && !confirmedRemoval,
isDanger: showConfirmation,
testID: KEYRING_SNAP_REMOVAL_WARNING_CONTINUE,
}),
[showConfirmation, confirmedRemoval, handleContinuePress],
);

const buttonPropsArray = useMemo(
() => [cancelButtonProps, continueButtonProps],
[cancelButtonProps, continueButtonProps],
);

const accountListItems = useMemo(
() =>
keyringAccounts.map((account, index) => (
<KeyringAccountListItem
key={index}
account={account}
blockExplorerUrl={getAccountLink(account.address, chainId)}
/>
)),
[keyringAccounts, chainId],
);

return (
<BottomSheet
ref={bottomSheetRef}
isFullscreen={false}
onClose={onClose}
shouldNavigateBack={false}
testID={KEYRING_SNAP_REMOVAL_WARNING}
style={styles.bottomSheet}
>
<View style={styles.container}>
<BottomSheetHeader>
<Text variant={TextVariant.HeadingMD}>
{strings(
'app_settings.snaps.snap_settings.remove_account_snap_warning.title',
)}
</Text>
</BottomSheetHeader>
<BannerAlert
severity={BannerAlertSeverity.Warning}
title={strings(
'app_settings.snaps.snap_settings.remove_account_snap_warning.banner_title',
)}
/>
{showConfirmation ? (
<>
<Text variant={TextVariant.BodyMD} style={styles.description}>
{`${strings(
'app_settings.snaps.snap_settings.remove_account_snap_warning.remove_account_snap_alert_description_1',
)} `}
<Text variant={TextVariant.BodyMDBold}>
{snap.manifest.proposedName}
</Text>
{` ${strings(
'app_settings.snaps.snap_settings.remove_account_snap_warning.remove_account_snap_alert_description_2',
)}`}
</Text>
<TextInput
style={styles.input}
value={confirmationInput}
onChangeText={handleConfirmationInputChange}
testID={KEYRING_SNAP_REMOVAL_WARNING_TEXT_INPUT}
/>
{error && (
<Text variant={TextVariant.BodySM} style={styles.errorText}>
{strings(
'app_settings.snaps.snap_settings.remove_account_snap_warning.remove_snap_error',
{
snapName: snap.manifest.proposedName,
},
)}
</Text>
)}
</>
) : (
<>
<Text variant={TextVariant.BodyMD} style={styles.description}>
{strings(
'app_settings.snaps.snap_settings.remove_account_snap_warning.description',
)}
</Text>
<NativeViewGestureHandler disallowInterruption>
<ScrollView style={styles.scrollView}>
{accountListItems}
</ScrollView>
</NativeViewGestureHandler>
</>
)}
</View>
<BottomSheetFooter
style={styles.buttonContainer}
buttonsAlignment={ButtonsAlignment.Horizontal}
buttonPropsArray={buttonPropsArray}
/>
</BottomSheet>
);
}
///: END:ONLY_INCLUDE_IF
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
///: BEGIN:ONLY_INCLUDE_IF(keyring-snaps)
export { default as KeyringSnapRemovalWarning } from './KeyringSnapRemovalWarning';
///: END:ONLY_INCLUDE_IF
Loading
Loading