Skip to content

Commit

Permalink
feat: custom names for snap accounts (Flask only) (#12198)
Browse files Browse the repository at this point in the history
## **Description**

This PR allows enables the following...
1. Keyring snaps to suggest account names
2. Users to set custom account names during the snap account creation
flow
3. deny adding an account

This will be used to create Bitcoin accounts in the future.

Equivalent extension PR:
MetaMask/metamask-extension#25191


## **Related issues**

Fixes: MetaMask/accounts-planning#604

## **Manual testing steps**

1. open `.js.env` in your editor and set `export
METAMASK_BUILD_TYPE="flask"`
2. run the app and build the create a wallet
3. in the in app browser open this url:
https://metamask.github.io/snap-simple-keyring/latest/
4. click connect and approve the permissions and install confirmation
5. now that you are connected, click the create account button in the
dapp
6. EXPECT: a popup with the suggested account name should appear.
Assuming this is your first snap account the suggested name should be
`SSK Account`
7. click `Add account`
8. EXPECT: `SSK Account` should be added to the account list in the
wallet view and be the currently selected account.
9. navigate back to the browser and click the create account button
again.
10. EXPECT: a new popup should appear with the suggested name being `SSK
Account 1`
11. If you remove the `1` from `SSK Account 1` you should see a an error
popup on the screen saying `This account name already exists` AND the
`Add account` button should be disabled.
12. Change the text input value to something unique and click `Add
account`
13. again this account should be added to the wallet and set as the
currently selected account.

#### Testing Add account denial
1. open `.js.env` in your editor and set `export
METAMASK_BUILD_TYPE="flask"`
2. run the app and build the create a wallet
3. in the in app browser open this url:
https://metamask.github.io/snap-simple-keyring/latest/
4. click connect and approve the permissions and install confirmation
5. now that you are connected, click the create account button in the
dapp
6. EXPECT: a popup with the suggested account name should appear
7. click cancel
8. EXPECT: the an error should appear in the SSK dapp indicating that
the user denied the account creation and the account should not have
been added to metamask.

## **Screenshots/Recordings**

Extension version

<img width="363" alt="Screenshot 2024-11-06 at 3 47 12 PM"
src="https://github.com/user-attachments/assets/2a73fd12-bdba-425c-87ef-d98e1b0e1307">

Mobile:

<img width="363" alt="Screenshot 2024-11-06 at 3 47 12 PM"
src="https://github.com/user-attachments/assets/7934e7fe-4575-4ee6-af99-6950c070087c">


### **Before**


https://github.com/user-attachments/assets/2f8dbdfd-92bc-4a68-8e19-68b295e67f88


### **After**



https://github.com/user-attachments/assets/1139fa9f-af31-497d-9680-c71f5abd1a84



## **Pre-merge author checklist**

- [ ] I’ve followed [MetaMask Contributor
Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Mobile
Coding
Standards](https://github.com/MetaMask/metamask-mobile/blob/main/.github/guidelines/CODING_GUIDELINES.md).
- [ ] I've completed the PR template to the best of my ability
- [ ] I’ve included tests if applicable
- [ ] I’ve documented my code using [JSDoc](https://jsdoc.app/) format
if applicable
- [ ] I’ve applied the right labels on the PR (see [labeling
guidelines](https://github.com/MetaMask/metamask-mobile/blob/main/.github/guidelines/LABELING_GUIDELINES.md)).
Not required for external contributors.

## **Pre-merge reviewer checklist**

- [ ] I've manually tested the PR (e.g. pull and build branch, run the
app, test code being changed).
- [ ] I confirm that this PR addresses all acceptance criteria described
in the ticket it closes and includes the necessary testing evidence such
as recordings and or screenshots.

---------

Co-authored-by: Daniel Cross <dan.s.cross@icloud.com>
  • Loading branch information
owencraston and Daniel-Cross authored Nov 19, 2024
1 parent fb69962 commit 2e10cf4
Show file tree
Hide file tree
Showing 11 changed files with 686 additions and 28 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
///: BEGIN:ONLY_INCLUDE_IF(keyring-snaps)
export const SNAP_ACCOUNT_CUSTOM_NAME_APPROVAL =
'snap-account-custom-name-approval';
export const SNAP_ACCOUNT_CUSTOM_NAME_CANCEL_BUTTON =
'snap-account-custom-name-approval-cancel-button';
export const SNAP_ACCOUNT_CUSTOM_NAME_ADD_ACCOUNT_BUTTON =
'snap-account-custom-name-approval-add-account-button';
export const SNAP_ACCOUNT_CUSTOM_NAME_INPUT =
'snap-account-custom-name-approval-input';
///: END:ONLY_INCLUDE_IF
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
///: BEGIN:ONLY_INCLUDE_IF(keyring-snaps)
import { StyleSheet } from 'react-native';
import { Theme } from '../../../util/theme/models';
import Device from '../../../util/device';

/**
*
* @param params Style sheet params.
* @param params.theme App theme from ThemeContext.
* @param params.vars Inputs that the style sheet depends on.
* @returns StyleSheet object.
*/
const styleSheet = (params: { theme: Theme }) => {
const { theme } = params;
const { colors } = theme;
return StyleSheet.create({
root: {
backgroundColor: colors.background.default,
paddingTop: 24,
paddingHorizontal: 16,
borderTopLeftRadius: 20,
borderTopRightRadius: 20,
minHeight: 200,
paddingBottom: Device.isIphoneX() ? 20 : 0,
},
actionContainer: {
flex: 0,
paddingVertical: 16,
justifyContent: 'center',
},
inputTitle: {
textAlign: 'left',
},
input: {
borderWidth: 1,
borderColor: colors.border.default,
borderRadius: 4,
padding: 10,
marginVertical: 10,
},
});
};

export default styleSheet;
///: END:ONLY_INCLUDE_IF
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
///: BEGIN:ONLY_INCLUDE_IF(keyring-snaps)
import React, { useEffect, useState } from 'react';
import { TextInput, View } from 'react-native';
import ApprovalModal from '../ApprovalModal';
import useApprovalRequest from '../../Views/confirmations/hooks/useApprovalRequest';
import { SNAP_MANAGE_ACCOUNTS_CONFIRMATION_TYPES } from '../../../core/RPCMethods/RPCMethodMiddleware';
import {
SNAP_ACCOUNT_CUSTOM_NAME_ADD_ACCOUNT_BUTTON,
SNAP_ACCOUNT_CUSTOM_NAME_APPROVAL,
SNAP_ACCOUNT_CUSTOM_NAME_CANCEL_BUTTON,
SNAP_ACCOUNT_CUSTOM_NAME_INPUT,
} from './SnapAccountCustomNameApproval.constants';
import styleSheet from './SnapAccountCustomNameApproval.styles';
import { useStyles } from '../../hooks/useStyles';
import BottomSheetFooter, {
ButtonsAlignment,
} from '../../../component-library/components/BottomSheets/BottomSheetFooter';
import SheetHeader from '../../../component-library/components/Sheet/SheetHeader';
import { strings } from '../../../../locales/i18n';
import Text, {
TextColor,
TextVariant,
} from '../../../component-library/components/Texts/Text';
import {
ButtonProps,
ButtonSize,
ButtonVariants,
} from '../../../component-library/components/Buttons/Button/Button.types';
import { useSelector } from 'react-redux';
import { selectInternalAccounts } from '../../../selectors/accountsController';
import { KeyringTypes } from '@metamask/keyring-controller';
import Engine from '../../../core/Engine';

const SnapAccountCustomNameApproval = () => {
const { approvalRequest, onConfirm, onReject } = useApprovalRequest();
const internalAccounts = useSelector(selectInternalAccounts);
const [accountName, setAccountName] = useState<string>('');
const [isNameTaken, setIsNameTaken] = useState<boolean>(false);

const { styles } = useStyles(styleSheet, {});

const onAddAccountPressed = () => {
if (!isNameTaken) {
onConfirm(undefined, { success: true, name: accountName });
}
};

const checkIfNameTaken = (name: string) =>
internalAccounts.some((account) => account.metadata.name === name);

useEffect(() => {
function generateUniqueNameWithSuffix(baseName: string): string {
let suffix = 1;
let candidateName = baseName;
while (
internalAccounts.some(
// eslint-disable-next-line no-loop-func
(account) => account.metadata.name === candidateName,
)
) {
suffix += 1;
candidateName = `${baseName} ${suffix}`;
}
return candidateName;
}

const suggestedName = approvalRequest?.requestData.snapSuggestedAccountName;
const initialName = suggestedName
? generateUniqueNameWithSuffix(suggestedName)
: Engine.context.AccountsController.getNextAvailableAccountName(
KeyringTypes.snap,
);
setAccountName(initialName);
}, [approvalRequest, internalAccounts]);

const cancelButtonProps: ButtonProps = {
variant: ButtonVariants.Secondary,
label: strings('accountApproval.cancel'),
size: ButtonSize.Lg,
onPress: onReject,
testID: SNAP_ACCOUNT_CUSTOM_NAME_CANCEL_BUTTON,
};

const addAccountButtonProps: ButtonProps = {
variant: ButtonVariants.Primary,
label: strings('snap_account_custom_name_approval.add_account_button'),
size: ButtonSize.Lg,
onPress: onAddAccountPressed,
testID: SNAP_ACCOUNT_CUSTOM_NAME_ADD_ACCOUNT_BUTTON,
isDisabled: isNameTaken,
};

const handleNameChange = (text: string) => {
setAccountName(text);
setIsNameTaken(checkIfNameTaken(text));
};

return (
<ApprovalModal
isVisible={
approvalRequest?.type ===
SNAP_MANAGE_ACCOUNTS_CONFIRMATION_TYPES.showNameSnapAccount
}
onCancel={onReject}
>
<View testID={SNAP_ACCOUNT_CUSTOM_NAME_APPROVAL} style={styles.root}>
<SheetHeader
title={strings('snap_account_custom_name_approval.title')}
/>
<Text style={styles.inputTitle} variant={TextVariant.BodyMDBold}>
{strings('snap_account_custom_name_approval.input_title')}
</Text>
<TextInput
style={styles.input}
value={accountName}
onChangeText={handleNameChange}
testID={SNAP_ACCOUNT_CUSTOM_NAME_INPUT}
/>
{isNameTaken && (
<Text variant={TextVariant.BodySM} color={TextColor.Error}>
{strings('snap_account_custom_name_approval.name_taken_message')}
</Text>
)}
<View style={styles.actionContainer}>
<BottomSheetFooter
buttonsAlignment={ButtonsAlignment.Horizontal}
buttonPropsArray={[cancelButtonProps, addAccountButtonProps]}
/>
</View>
</View>
</ApprovalModal>
);
};

export default SnapAccountCustomNameApproval;
///: 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 } from './SnapAccountCustomNameApproval';
///: END:ONLY_INCLUDE_IF
Loading

0 comments on commit 2e10cf4

Please sign in to comment.