Skip to content

Commit

Permalink
refactor: consolidate accounts references to a single source of truth (
Browse files Browse the repository at this point in the history
…#8664)

## **Description**

1. What is the reason for the change?
The accounts team is working towards the integration of the new
[accounts
controller](https://github.com/MetaMask/core/tree/main/packages/accounts-controller).
This controller will become the source of truth for all things accounts
(name address, methods KeyringType etc). In order to make this
integration easier we must first centralize the source of truth for
accounts.
3. What is the improvement/solution?
- Currently account information is being derived from the preferences
controller. In this PR I removed direct use of
`state.engine.backgroundState.PreferencesController` in favour of the
respective redux selectors. I also migrated the Wallet screen to use the
`useAccounts` hook which achieves the same result as before but without
custom logic.

There should be no visible or functional changes to the app. 

## **Related issues**

Fixes: #8482

## **Notes for code reviewers**
- 🔴 No use of `backgroundState.PreferencesController` `selectedAddress`
and `identities`
- 🟢 Instead they should all be using the `selectIdentities` and the
`selectSelectedAddress` selectors

## **Manual testing steps**
There should be no visible changes after this PR

1. create/import a wallet
2. click the 3 dots on the the account selector 
3. change the account name
4. add an account
5. open the browser
6. navigate to a dapp: https://uniswap.org/
7. connect the dapp
8. ensure that the account names are the same as the ones you input
previously

## **Screenshots/Recordings**

<!-- If applicable, add screenshots and/or recordings to visualize the
before and after of your change. -->

### **Before**

<!-- [screenshots/recordings] -->

### **After**

Creation flow


https://github.com/MetaMask/metamask-mobile/assets/22918444/2afefbfa-55d5-446e-b00a-216b8daa2ec8

Import flow


https://github.com/MetaMask/metamask-mobile/assets/22918444/f9989e24-439a-4986-acd2-87c8229551a0


## **Pre-merge author checklist**

- [ ] I’ve followed [MetaMask Coding
Standards](https://github.com/MetaMask/metamask-mobile/blob/main/.github/guidelines/CODING_GUIDELINES.md).
- [ ] I've clearly explained what problem this PR is solving and how it
is solved.
- [ ] I've linked related issues
- [ ] I've included manual testing steps
- [ ] I've included screenshots/recordings if applicable
- [ ] 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.
- [ ] I’ve properly set the pull request status:
  - [ ] In case it's not yet "ready for review", I've set it to "draft".
- [ ] In case it's "ready for review", I've changed it from "draft" to
"non-draft".

## **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.
  • Loading branch information
owencraston authored Feb 27, 2024
1 parent b536b96 commit 7ef00e1
Show file tree
Hide file tree
Showing 9 changed files with 139 additions and 86 deletions.
85 changes: 67 additions & 18 deletions app/components/UI/WalletAccount/WalletAccount.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ import { createAccountSelectorNavDetails } from '../../../components/Views/Accou
// Internal dependencies
import WalletAccount from './WalletAccount';
import initialBackgroundState from '../../../util/test/initial-background-state.json';
import { Account } from '../../hooks/useAccounts';
import { KeyringTypes } from '@metamask/keyring-controller';

jest.mock('../../../core/Engine', () => ({
context: {
Expand All @@ -25,19 +27,21 @@ jest.mock('../../../core/Engine', () => ({
},
}));

const mockAccount: Account = {
name: 'Test account 1',
address: '0xC4955C0d639D99699Bfd7Ec54d9FaFEe40e4D272',
type: KeyringTypes.hd,
yOffset: 0,
isSelected: true,
};

const mockInitialState = {
settings: {
useBlockieIcon: false,
},
engine: {
backgroundState: {
...initialBackgroundState,
PreferencesController: {
selectedAddress: '0xC4955C0d639D99699Bfd7Ec54d9FaFEe40e4D272',
identities: {
'0xC4955C0d639D99699Bfd7Ec54d9FaFEe40e4D272': { name: 'Account 1' },
},
},
},
},
};
Expand Down Expand Up @@ -69,36 +73,81 @@ jest.mock('react-redux', () => ({

describe('WalletAccount', () => {
it('renders correctly', () => {
const { toJSON } = renderWithProvider(<WalletAccount />, {
state: mockInitialState,
});
const { toJSON } = renderWithProvider(
<WalletAccount account={mockAccount} />,
{
state: mockInitialState,
},
);
expect(toJSON()).toMatchSnapshot();
});

it('shows the account address', () => {
const { getByTestId } = renderWithProvider(<WalletAccount />, {
state: mockInitialState,
});
const { getByTestId } = renderWithProvider(
<WalletAccount account={mockAccount} />,
{
state: mockInitialState,
},
);
expect(getByTestId('wallet-account-address')).toBeDefined();
});

it('copies the account address to the clipboard when the copy button is pressed', async () => {
const { getByTestId } = renderWithProvider(<WalletAccount />, {
state: mockInitialState,
});
const { getByTestId } = renderWithProvider(
<WalletAccount account={mockAccount} />,
{
state: mockInitialState,
},
);

fireEvent.press(getByTestId('wallet-account-copy-button'));
expect(ClipboardManager.setString).toHaveBeenCalledTimes(1);
});

it('should navigate to the account selector screen on account press', () => {
const { getByTestId } = renderWithProvider(<WalletAccount />, {
state: mockInitialState,
});
const { getByTestId } = renderWithProvider(
<WalletAccount account={mockAccount} />,
{
state: mockInitialState,
},
);

fireEvent.press(getByTestId('account-picker'));
expect(mockNavigate).toHaveBeenCalledWith(
...createAccountSelectorNavDetails({}),
);
});
it('displays the correct account name', () => {
const { getByText } = renderWithProvider(
<WalletAccount account={mockAccount} />,
{
state: mockInitialState,
},
);
expect(getByText(mockAccount.name)).toBeDefined();
});
it('displays custom account name when ENS is defined but account name is not the default', () => {
const ensName = 'test.eth';
const { getByText } = renderWithProvider(
<WalletAccount account={mockAccount} ens={ensName} />,
{
state: mockInitialState,
},
);
expect(getByText(mockAccount.name)).toBeDefined();
});
it('displays ENS name when defined and account name is the default', () => {
const ensName = 'test.eth';
const mockAccountWithDefaultName: Account = {
...mockAccount,
name: 'Account 1',
};
const { getByText } = renderWithProvider(
<WalletAccount account={mockAccountWithDefaultName} ens={ensName} />,
{
state: mockInitialState,
},
);
expect(getByText(ensName)).toBeDefined();
});
});
54 changes: 6 additions & 48 deletions app/components/UI/WalletAccount/WalletAccount.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,5 @@
// Third parties dependencies
import React, {
forwardRef,
useCallback,
useEffect,
useImperativeHandle,
useRef,
useState,
} from 'react';
import React, { forwardRef, useImperativeHandle, useRef } from 'react';
import { useSelector } from 'react-redux';
import { useNavigation } from '@react-navigation/native';
import { Platform, View } from 'react-native';
Expand All @@ -18,15 +11,7 @@ import { createAccountSelectorNavDetails } from '../../../components/Views/Accou
import { useStyles } from '../../../component-library/hooks';
import generateTestId from '../../../../wdio/utils/generateTestId';
import AddressCopy from '../AddressCopy';
import {
doENSReverseLookup,
isDefaultAccountName,
} from '../../../util/ENSUtils';
import { selectChainId } from '../../../selectors/networkController';
import {
selectIdentities,
selectSelectedAddress,
} from '../../../selectors/preferencesController';
import { isDefaultAccountName } from '../../../util/ENSUtils';
import ButtonIcon from '../../../component-library/components/Buttons/ButtonIcon/ButtonIcon';
import { ButtonIconSizes } from '../../../component-library/components/Buttons/ButtonIcon';
import Routes from '../../../constants/navigation/Routes';
Expand All @@ -40,53 +25,26 @@ import {
} from '../../../../wdio/screen-objects/testIDs/Screens/WalletView.testIds';
import { getLabelTextByAddress } from '../../../util/address';

const WalletAccount = ({ style }: WalletAccountProps, ref: React.Ref<any>) => {
const WalletAccount = (
{ style, account, ens }: WalletAccountProps,
ref: React.Ref<any>,
) => {
const { styles } = useStyles(styleSheet, { style });

const { navigate } = useNavigation();
const [ens, setEns] = useState<string>();

const yourAccountRef = useRef(null);
const accountActionsRef = useRef(null);

useImperativeHandle(ref, () => ({
yourAccountRef,
accountActionsRef,
}));
/**
* A string that represents the selected address
*/
const selectedAddress = useSelector(selectSelectedAddress);

/**
* An object containing each identity in the format address => account
*/
const identities = useSelector(selectIdentities);

const chainId = useSelector(selectChainId);

const accountAvatarType = useSelector((state: any) =>
state.settings.useBlockieIcon
? AvatarAccountType.Blockies
: AvatarAccountType.JazzIcon,
);
const account = {
...identities[selectedAddress],
address: selectedAddress,
};

const lookupEns = useCallback(async () => {
try {
const accountEns = await doENSReverseLookup(account.address, chainId);

setEns(accountEns);
// eslint-disable-next-line no-empty
} catch {}
}, [account.address, chainId]);

useEffect(() => {
lookupEns();
}, [lookupEns]);

const onNavigateToAccountActions = () => {
navigate(Routes.MODAL.ROOT_MODAL_FLOW, {
Expand Down
3 changes: 3 additions & 0 deletions app/components/UI/WalletAccount/WalletAccount.types.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
import { ViewStyle } from 'react-native';
import { Account } from '../../hooks/useAccounts';

export interface WalletAccountProps {
style?: ViewStyle;
account: Account;
ens?: string;
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -177,7 +177,7 @@ exports[`WalletAccount renders correctly 1`] = `
}
testID="account-label"
>
Account 1
Test account 1
</Text>
</View>
</View>
Expand Down Expand Up @@ -278,9 +278,7 @@ exports[`WalletAccount renders correctly 1`] = `
}
}
testID="wallet-account-address"
>
0xC495...D272
</Text>
/>
<SvgMock
color="#0376C9"
height={16}
Expand Down
4 changes: 2 additions & 2 deletions app/components/Views/ChoosePassword/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ import { LoginOptionsSwitch } from '../../UI/LoginOptionsSwitch';
import navigateTermsOfUse from '../../../util/termsOfUse/termsOfUse';
import { ChoosePasswordSelectorsIDs } from '../../../../e2e/selectors/Onboarding/ChoosePassword.selectors';
import trackOnboarding from '../../../util/metrics/TrackOnboarding/trackOnboarding';
import { selectSelectedAddress } from '../../../selectors/preferencesController';

const createStyles = (colors) =>
StyleSheet.create({
Expand Down Expand Up @@ -789,8 +790,7 @@ class ChoosePassword extends PureComponent {
ChoosePassword.contextType = ThemeContext;

const mapStateToProps = (state) => ({
selectedAddress:
state.engine.backgroundState.PreferencesController?.selectedAddress,
selectedAddress: selectSelectedAddress(state),
});

const mapDispatchToProps = (dispatch) => ({
Expand Down
9 changes: 9 additions & 0 deletions app/components/Views/Wallet/index.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,15 @@ jest.mock('../../../core/Engine', () => ({
AccountTrackerController: {
refresh: jest.fn(),
},
KeyringController: {
state: {
keyrings: [
{
accounts: ['0xC4955C0d639D99699Bfd7Ec54d9FaFEe40e4D272'],
},
],
},
},
},
}));

Expand Down
56 changes: 46 additions & 10 deletions app/components/Views/Wallet/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ import {
import { selectAccountsByChainId } from '../../../selectors/accountTrackerController';
import { selectSelectedAddress } from '../../../selectors/preferencesController';
import { useMetrics } from '../../../components/hooks/useMetrics';
import { useAccounts } from '../../hooks/useAccounts';

const createStyles = ({ colors, typography }: Theme) =>
StyleSheet.create({
Expand Down Expand Up @@ -124,6 +125,32 @@ const Wallet = ({ navigation }: any) => {
*/
const providerConfig = useSelector(selectProviderConfig);

/**
* A list of all the user accounts and a mapping of ENS name to account address if they exist
*/
const { accounts, ensByAccountAddress } = useAccounts();

/**
* An object representing the currently selected account.
*/
const selectedAccount = useMemo(() => {
if (accounts.length > 0) {
return accounts.find((account) => account.isSelected);
}
return undefined;
}, [accounts]);

/**
* ENS name for the currently selected account.
* This value may be undefined if there is no corresponding ENS name for the account.
*/
const ensForSelectedAccount = useMemo(() => {
if (ensByAccountAddress && selectedAccount) {
return ensByAccountAddress[selectedAccount.address];
}
return undefined;
}, [ensByAccountAddress, selectedAccount]);

const networkName = useMemo(
() => getNetworkNameFromProviderConfig(providerConfig),
[providerConfig],
Expand Down Expand Up @@ -274,8 +301,14 @@ const Wallet = ({ navigation }: any) => {
}
return (
<View style={styles.wrapper}>
<WalletAccount style={styles.walletAccount} ref={walletRef} />

{selectedAccount ? (
<WalletAccount
account={selectedAccount}
ens={ensForSelectedAccount}
style={styles.walletAccount}
ref={walletRef}
/>
) : null}
<ScrollableTabView
renderTabBar={renderTabBar}
// eslint-disable-next-line react/jsx-no-bind
Expand Down Expand Up @@ -304,17 +337,20 @@ const Wallet = ({ navigation }: any) => {
</View>
);
}, [
tokens,
accountsByChainId,
providerConfig.chainId,
selectedAddress,
styles.wrapper,
styles.walletAccount,
selectedAccount,
ensForSelectedAccount,
renderTabBar,
conversionRate,
currentCurrency,
navigation,
onChangeTab,
selectedAddress,
navigation,
ticker,
tokens,
styles,
providerConfig.chainId,
accountsByChainId,
conversionRate,
currentCurrency,
]);
const renderLoader = useCallback(
() => (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import withQRHardwareAwareness from '../../../../UI/QRHardware/withQRHardwareAwa
import WebsiteIcon from '../../../../UI/WebsiteIcon';
import { ResultType } from '../BlockaidBanner/BlockaidBanner.types';
import { withMetricsAwareness } from '../../../../../components/hooks/useMetrics';
import { selectSelectedAddress } from '../../../../../selectors/preferencesController';

const createStyles = (colors) =>
StyleSheet.create({
Expand Down Expand Up @@ -417,8 +418,7 @@ class SignatureRequest extends PureComponent {
}

const mapStateToProps = (state) => ({
selectedAddress:
state.engine.backgroundState.PreferencesController.selectedAddress,
selectedAddress: selectSelectedAddress(state),
networkType: selectProviderType(state),
securityAlertResponse: state.signatureRequest.securityAlertResponse,
});
Expand Down
Loading

0 comments on commit 7ef00e1

Please sign in to comment.