diff --git a/app/actions/modals/index.js b/app/actions/modals/index.js
index 1e2656eb724..b8088bb9f94 100644
--- a/app/actions/modals/index.js
+++ b/app/actions/modals/index.js
@@ -5,12 +5,6 @@ export function toggleNetworkModal() {
};
}
-export function toggleAccountsModal() {
- return {
- type: 'TOGGLE_ACCOUNT_MODAL',
- };
-}
-
export function toggleCollectibleContractModal() {
return {
type: 'TOGGLE_COLLECTIBLE_CONTRACT_MODAL',
diff --git a/app/component-library/components-temp/Loader/Loader.styles.ts b/app/component-library/components-temp/Loader/Loader.styles.ts
new file mode 100644
index 00000000000..dc27d6226ef
--- /dev/null
+++ b/app/component-library/components-temp/Loader/Loader.styles.ts
@@ -0,0 +1,29 @@
+// Third party dependencies.
+import { StyleSheet } from 'react-native';
+
+// External dependencies.
+import { Theme } from '../../../util/theme/models';
+
+/**
+ * Style sheet function for SheetActions component.
+ *
+ * @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({
+ base: {
+ ...StyleSheet.absoluteFillObject,
+ justifyContent: 'center',
+ alignItems: 'center',
+ backgroundColor: colors.background.default,
+ },
+ });
+};
+
+export default styleSheet;
diff --git a/app/component-library/components-temp/Loader/Loader.tsx b/app/component-library/components-temp/Loader/Loader.tsx
new file mode 100644
index 00000000000..4ce1c711cd4
--- /dev/null
+++ b/app/component-library/components-temp/Loader/Loader.tsx
@@ -0,0 +1,23 @@
+// Third party dependencies.
+import React from 'react';
+import { ActivityIndicator, View } from 'react-native';
+
+// External dependencies.
+import { useStyles } from '../../hooks';
+
+// Internal dependencies.
+import styleSheet from './Loader.styles';
+import { LoaderProps } from './Loader.types';
+
+const Loader = ({ size = 'large' }: LoaderProps) => {
+ const { styles, theme } = useStyles(styleSheet, {});
+ const { colors } = theme;
+
+ return (
+
+
+
+ );
+};
+
+export default Loader;
diff --git a/app/component-library/components-temp/Loader/Loader.types.ts b/app/component-library/components-temp/Loader/Loader.types.ts
new file mode 100644
index 00000000000..8cf3fcd2635
--- /dev/null
+++ b/app/component-library/components-temp/Loader/Loader.types.ts
@@ -0,0 +1,12 @@
+// Third party dependencies.
+import { ActivityIndicatorProps } from 'react-native';
+
+/**
+ * Loader props.
+ */
+export interface LoaderProps {
+ /**
+ * Activity indicator size.
+ */
+ size?: ActivityIndicatorProps['size'];
+}
diff --git a/app/component-library/components-temp/Loader/index.ts b/app/component-library/components-temp/Loader/index.ts
new file mode 100644
index 00000000000..348c02a9870
--- /dev/null
+++ b/app/component-library/components-temp/Loader/index.ts
@@ -0,0 +1 @@
+export { default } from './Loader';
diff --git a/app/component-library/components-temp/SheetActions/SheetActions.styles.ts b/app/component-library/components-temp/SheetActions/SheetActions.styles.ts
new file mode 100644
index 00000000000..496792849aa
--- /dev/null
+++ b/app/component-library/components-temp/SheetActions/SheetActions.styles.ts
@@ -0,0 +1,28 @@
+// Third party dependencies.
+import { StyleSheet } from 'react-native';
+
+// External dependencies.
+import { Theme } from '../../../util/theme/models';
+
+/**
+ * Style sheet function for SheetActions component.
+ *
+ * @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({
+ separator: {
+ height: 1,
+ backgroundColor: colors.border.muted,
+ marginHorizontal: 16,
+ },
+ });
+};
+
+export default styleSheet;
diff --git a/app/component-library/components-temp/SheetActions/SheetActions.tsx b/app/component-library/components-temp/SheetActions/SheetActions.tsx
new file mode 100644
index 00000000000..10e5cc7e698
--- /dev/null
+++ b/app/component-library/components-temp/SheetActions/SheetActions.tsx
@@ -0,0 +1,46 @@
+// Third party dependencies.
+import React, { useCallback } from 'react';
+import { View } from 'react-native';
+
+// External dependencies.
+import { useStyles } from '../../hooks';
+import ButtonTertiary from '../../components/Buttons/ButtonTertiary';
+import { ButtonBaseSize } from '../../components/Buttons/ButtonBase';
+import Loader from '../Loader';
+
+// Internal dependencies.
+import { SheetActionsProps } from './SheetActions.types';
+import styleSheet from './SheetActions.styles';
+
+const SheetActions = ({ actions }: SheetActionsProps) => {
+ const { styles } = useStyles(styleSheet, {});
+
+ const renderActions = useCallback(
+ () =>
+ actions.map(({ label, onPress, testID, isLoading, disabled }, index) => {
+ const key = `${label}-${index}`;
+ return (
+
+ {actions.length > 1 && }
+
+
+ {isLoading && }
+
+
+ );
+ }),
+ [actions, styles.separator],
+ );
+
+ return <>{renderActions()}>;
+};
+
+export default SheetActions;
diff --git a/app/component-library/components-temp/SheetActions/SheetActions.types.ts b/app/component-library/components-temp/SheetActions/SheetActions.types.ts
new file mode 100644
index 00000000000..967d2eabd85
--- /dev/null
+++ b/app/component-library/components-temp/SheetActions/SheetActions.types.ts
@@ -0,0 +1,20 @@
+/**
+ * Sheet action options.
+ */
+export interface Action {
+ label: string;
+ onPress: () => void;
+ testID?: string;
+ disabled?: boolean;
+ isLoading?: boolean;
+}
+
+/**
+ * SheetActionsProps props.
+ */
+export interface SheetActionsProps {
+ /**
+ * List of actions.
+ */
+ actions: Action[];
+}
diff --git a/app/component-library/components-temp/SheetActions/index.ts b/app/component-library/components-temp/SheetActions/index.ts
new file mode 100644
index 00000000000..5513e2ffb64
--- /dev/null
+++ b/app/component-library/components-temp/SheetActions/index.ts
@@ -0,0 +1 @@
+export { default } from './SheetActions';
diff --git a/app/component-library/components/Avatars/AvatarGroup/AvatarGroup.constants.ts b/app/component-library/components/Avatars/AvatarGroup/AvatarGroup.constants.ts
index 1edd74fdc77..003df07110d 100644
--- a/app/component-library/components/Avatars/AvatarGroup/AvatarGroup.constants.ts
+++ b/app/component-library/components/Avatars/AvatarGroup/AvatarGroup.constants.ts
@@ -9,98 +9,84 @@ export const AVAILABLE_TOKEN_LIST: AvatarGroupTokenList = [
imageSource: {
uri: 'https://cryptologos.cc/logos/avalanche-avax-logo.png',
},
- id: '0',
},
{
name: 'Ethereum',
imageSource: {
uri: 'https://cryptologos.cc/logos/avalanche-avax-logo.png',
},
- id: '1',
},
{
name: 'Ethereum',
imageSource: {
uri: 'https://cryptologos.cc/logos/avalanche-avax-logo.png',
},
- id: '2',
},
{
name: 'Ethereum',
imageSource: {
uri: 'https://cryptologos.cc/logos/avalanche-avax-logo.png',
},
- id: '3',
},
{
name: 'Ethereum',
imageSource: {
uri: 'https://cryptologos.cc/logos/avalanche-avax-logo.png',
},
- id: '4',
},
{
name: 'Ethereum',
imageSource: {
uri: 'https://cryptologos.cc/logos/avalanche-avax-logo.png',
},
- id: '5',
},
{
name: 'Ethereum',
imageSource: {
uri: 'https://cryptologos.cc/logos/avalanche-avax-logo.png',
},
- id: '6',
},
{
name: 'Ethereum',
imageSource: {
uri: 'https://cryptologos.cc/logos/avalanche-avax-logo.png',
},
- id: '7',
},
{
name: 'Ethereum',
imageSource: {
uri: 'https://cryptologos.cc/logos/avalanche-avax-logo.png',
},
- id: '8',
},
{
name: 'Ethereum',
imageSource: {
uri: 'https://cryptologos.cc/logos/avalanche-avax-logo.png',
},
- id: '9',
},
{
name: 'Ethereum',
imageSource: {
uri: 'https://cryptologos.cc/logos/avalanche-avax-logo.png',
},
- id: '10',
},
{
name: 'Ethereum',
imageSource: {
uri: 'https://cryptologos.cc/logos/avalanche-avax-logo.png',
},
- id: '11',
},
{
name: 'Ethereum',
imageSource: {
uri: 'https://cryptologos.cc/logos/avalanche-avax-logo.png',
},
- id: '12',
},
{
name: 'Ethereum',
imageSource: {
uri: 'https://cryptologos.cc/logos/avalanche-avax-logo.png',
},
- id: '13',
},
];
diff --git a/app/component-library/components/Avatars/AvatarGroup/AvatarGroup.styles.ts b/app/component-library/components/Avatars/AvatarGroup/AvatarGroup.styles.ts
index 073ef31963d..0130b75b218 100644
--- a/app/component-library/components/Avatars/AvatarGroup/AvatarGroup.styles.ts
+++ b/app/component-library/components/Avatars/AvatarGroup/AvatarGroup.styles.ts
@@ -4,6 +4,9 @@ import { StyleSheet } from 'react-native';
// External dependencies.
import { Theme } from '../../../../util/theme/models';
+// Internal dependencies.
+import { AvatarGroupStyleSheetVars } from './AvatarGroup.types';
+
/**
* Style sheet function for AvatarGroup component.
*
@@ -12,18 +15,25 @@ import { Theme } from '../../../../util/theme/models';
* @param params.vars Inputs that the style sheet depends on.
* @returns StyleSheet object.
*/
-const styleSheet = (params: { theme: Theme; vars: any }) => {
+const styleSheet = (params: {
+ theme: Theme;
+ vars: AvatarGroupStyleSheetVars;
+}) => {
const { theme, vars } = params;
- const { stackWidth } = vars;
-
+ const { stackWidth, stackHeight } = vars;
const borderWidth = 1;
+ const stackHeightWithBorder = stackHeight + borderWidth * 2;
return StyleSheet.create({
- base: { flexDirection: 'row' },
- stack: {
+ base: {
flexDirection: 'row',
alignItems: 'center',
+ height: stackHeightWithBorder,
+ },
+ stack: {
+ flexDirection: 'row',
width: stackWidth + borderWidth * 2,
+ height: stackHeightWithBorder,
},
stackedAvatarWrapper: {
position: 'absolute',
@@ -37,6 +47,7 @@ const styleSheet = (params: { theme: Theme; vars: any }) => {
textStyle: {
color: theme.colors.text.alternative,
marginLeft: 2,
+ bottom: 2,
},
});
};
diff --git a/app/component-library/components/Avatars/AvatarGroup/AvatarGroup.tsx b/app/component-library/components/Avatars/AvatarGroup/AvatarGroup.tsx
index 14405cf7767..40ac62d2b3a 100644
--- a/app/component-library/components/Avatars/AvatarGroup/AvatarGroup.tsx
+++ b/app/component-library/components/Avatars/AvatarGroup/AvatarGroup.tsx
@@ -1,5 +1,5 @@
// Third party dependencies.
-import React, { useMemo } from 'react';
+import React, { useCallback } from 'react';
import { View } from 'react-native';
// External dependencies.
@@ -26,18 +26,21 @@ const AvatarGroup = ({ tokenList }: AvatarGroupProps) => {
const stackWidth = avatarSpacing * (amountOfVisibleAvatars + 1);
const shouldRenderOverflowCounter = overflowCounter > 0;
- const { styles } = useStyles(styleSheet, { stackWidth });
+ const { styles } = useStyles(styleSheet, {
+ stackWidth,
+ stackHeight: Number(extraSmallSize),
+ });
- const renderTokenList = useMemo(
+ const renderTokenList = useCallback(
() =>
tokenList
.slice(0, MAX_STACKED_AVATARS)
- .map(({ name, imageSource, id }, index) => {
+ .map(({ name, imageSource }, index) => {
const leftOffset = avatarSpacing * index;
return (
{
return (
- {renderTokenList}
+ {renderTokenList()}
{shouldRenderOverflowCounter && (
{
const { styles } = useStyles(styleSheet, { style });
@@ -30,6 +31,7 @@ const CellSelect = ({
isSelected={isSelected}
style={styles.base}
testID={CELL_SELECT_TEST_ID}
+ {...props}
>
TYPE | REQUIRED | DEFAULT |
+| :-------------------------------------------------- | :------------------------------------------------------ | :----------------------------------------------------- |
+| number | No | 250 |
+
### `children`
Content to wrap in sheet.
diff --git a/app/component-library/components/Sheet/SheetBottom/SheetBottom.constants.ts b/app/component-library/components/Sheet/SheetBottom/SheetBottom.constants.ts
index 40436e3511a..324db6823da 100644
--- a/app/component-library/components/Sheet/SheetBottom/SheetBottom.constants.ts
+++ b/app/component-library/components/Sheet/SheetBottom/SheetBottom.constants.ts
@@ -17,4 +17,8 @@ export const SWIPE_TRIGGERED_ANIMATION_DURATION = 200;
/**
* The animation duration used for initial render.
*/
-export const INITIAL_RENDER_ANIMATION_DURATION = 500;
+export const INITIAL_RENDER_ANIMATION_DURATION = 350;
+/**
+ * Minimum spacing reserved for the overlay tappable area.
+ */
+export const DEFAULT_MIN_OVERLAY_HEIGHT = 250;
diff --git a/app/component-library/components/Sheet/SheetBottom/SheetBottom.styles.ts b/app/component-library/components/Sheet/SheetBottom/SheetBottom.styles.ts
index 8213f2d00af..8e7480d8e3b 100644
--- a/app/component-library/components/Sheet/SheetBottom/SheetBottom.styles.ts
+++ b/app/component-library/components/Sheet/SheetBottom/SheetBottom.styles.ts
@@ -21,7 +21,7 @@ const styleSheet = (params: {
}) => {
const { vars, theme } = params;
const { colors } = theme;
- const { maxSheetHeight } = vars;
+ const { maxSheetHeight, screenBottomPadding } = vars;
return StyleSheet.create({
base: {
...StyleSheet.absoluteFillObject,
@@ -37,6 +37,7 @@ const styleSheet = (params: {
borderTopRightRadius: 16,
maxHeight: maxSheetHeight,
overflow: 'hidden',
+ paddingBottom: screenBottomPadding,
},
fill: {
flex: 1,
@@ -47,7 +48,7 @@ const styleSheet = (params: {
borderRadius: 2,
backgroundColor: colors.border.muted,
alignSelf: 'center',
- marginVertical: 4,
+ marginTop: 4,
},
});
};
diff --git a/app/component-library/components/Sheet/SheetBottom/SheetBottom.tsx b/app/component-library/components/Sheet/SheetBottom/SheetBottom.tsx
index 96560b57644..29e8d1612e4 100644
--- a/app/component-library/components/Sheet/SheetBottom/SheetBottom.tsx
+++ b/app/component-library/components/Sheet/SheetBottom/SheetBottom.tsx
@@ -4,6 +4,7 @@
import { useNavigation } from '@react-navigation/native';
import React, {
forwardRef,
+ useCallback,
useEffect,
useImperativeHandle,
useMemo,
@@ -30,6 +31,7 @@ import Animated, {
withTiming,
} from 'react-native-reanimated';
import { useSafeAreaInsets } from 'react-native-safe-area-context';
+import { debounce } from 'lodash';
// External dependencies.
import { useStyles } from '../../../hooks';
@@ -41,6 +43,7 @@ import {
TAP_TRIGGERED_ANIMATION_DURATION,
SWIPE_TRIGGERED_ANIMATION_DURATION,
INITIAL_RENDER_ANIMATION_DURATION,
+ DEFAULT_MIN_OVERLAY_HEIGHT,
} from './SheetBottom.constants';
import styleSheet from './SheetBottom.styles';
import {
@@ -50,16 +53,28 @@ import {
} from './SheetBottom.types';
const SheetBottom = forwardRef(
- ({ children, onDismissed, isInteractable = true, ...props }, ref) => {
+ (
+ {
+ children,
+ onDismissed,
+ isInteractable = true,
+ reservedMinOverlayHeight = DEFAULT_MIN_OVERLAY_HEIGHT,
+ ...props
+ },
+ ref,
+ ) => {
const postCallback = useRef();
- const { top: screenTopPadding } = useSafeAreaInsets();
+ const { top: screenTopPadding, bottom: screenBottomPadding } =
+ useSafeAreaInsets();
const { height: screenHeight } = useWindowDimensions();
const { styles } = useStyles(styleSheet, {
- maxSheetHeight: screenHeight - screenTopPadding,
+ maxSheetHeight:
+ screenHeight - screenTopPadding - reservedMinOverlayHeight,
+ screenBottomPadding,
});
const currentYOffset = useSharedValue(screenHeight);
const visibleYOffset = useSharedValue(0);
- const sheetHeight = useSharedValue(0);
+ const sheetHeight = useSharedValue(screenHeight);
const overlayOpacity = useDerivedValue(() =>
interpolate(
currentYOffset.value,
@@ -70,17 +85,12 @@ const SheetBottom = forwardRef(
const navigation = useNavigation();
const isMounted = useRef(false);
- useEffect(() => {
- // Automatically handles animation when content changes
- LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut);
- }, [children]);
-
- const onHidden = () => {
+ const onHidden = useCallback(() => {
// Sheet is automatically unmounted from the navigation stack.
navigation.goBack();
onDismissed?.();
postCallback.current?.();
- };
+ }, [navigation, onDismissed]);
const gestureHandler = useAnimatedGestureHandler<
PanGestureHandlerGestureEvent,
@@ -140,13 +150,27 @@ const SheetBottom = forwardRef(
});
};
- const hide = () => {
+ const hide = useCallback(() => {
currentYOffset.value = withTiming(
sheetHeight.value,
{ duration: TAP_TRIGGERED_ANIMATION_DURATION },
() => runOnJS(onHidden)(),
);
- };
+ // Ref values do not affect deps.
+ /* eslint-disable-next-line */
+ }, [onHidden]);
+
+ const debouncedHide = useMemo(
+ // Prevent hide from being called multiple times. Potentially caused by taps in quick succession.
+ () => debounce(hide, 2000, { leading: true }),
+ [hide],
+ );
+
+ useEffect(() => {
+ // Automatically handles animation when content changes
+ LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut);
+ return () => debouncedHide.cancel();
+ }, [children, debouncedHide]);
const updateSheetHeight = (e: LayoutChangeEvent) => {
const { height } = e.nativeEvent.layout;
@@ -160,7 +184,7 @@ const SheetBottom = forwardRef(
useImperativeHandle(ref, () => ({
hide: (callback) => {
postCallback.current = callback;
- hide();
+ debouncedHide();
},
}));
@@ -199,7 +223,7 @@ const SheetBottom = forwardRef(
void;
/**
- * Boolean that indicates if sheet is swippable. This affects whether or not tapping on the overlay will dismiss the sheet as well.
+ * Optional boolean that indicates if sheet is swippable. This affects whether or not tapping on the overlay will dismiss the sheet as well.
* @default true
*/
isInteractable?: boolean;
+ /**
+ * Optional number for the minimum spacing reserved for the overlay tappable area.
+ * @default 250
+ */
+ reservedMinOverlayHeight?: number;
}
export type SheetBottomPostCallback = () => void;
@@ -28,4 +33,5 @@ export interface SheetBottomRef {
*/
export interface SheetBottomStyleSheetVars {
maxSheetHeight: number;
+ screenBottomPadding: number;
}
diff --git a/app/component-library/components/Sheet/SheetHeader/SheetHeader.styles.ts b/app/component-library/components/Sheet/SheetHeader/SheetHeader.styles.ts
index 0adfc7add07..0b66f0eb888 100644
--- a/app/component-library/components/Sheet/SheetHeader/SheetHeader.styles.ts
+++ b/app/component-library/components/Sheet/SheetHeader/SheetHeader.styles.ts
@@ -20,7 +20,7 @@ const styleSheet = (params: { theme: Theme }) => {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
- paddingHorizontal: 16,
+ margin: 16,
backgroundColor: colors.background.default,
height: 32,
},
diff --git a/app/component-library/components/Sheet/SheetHeader/SheetHeader.tsx b/app/component-library/components/Sheet/SheetHeader/SheetHeader.tsx
index 8ec2effdc5c..750043094c1 100644
--- a/app/component-library/components/Sheet/SheetHeader/SheetHeader.tsx
+++ b/app/component-library/components/Sheet/SheetHeader/SheetHeader.tsx
@@ -35,7 +35,7 @@ const SheetHeader = ({
testID={SHEET_HEADER_BACK_BUTTON_ID}
variant={ButtonIconVariant.Secondary}
onPress={onBack}
- icon={IconName.ArrowLeftOutline}
+ iconName={IconName.ArrowLeftOutline}
/>
)}
diff --git a/app/component-library/components/Sheet/SheetHeader/__snapshots__/SheetHeader.test.tsx.snap b/app/component-library/components/Sheet/SheetHeader/__snapshots__/SheetHeader.test.tsx.snap
index b8f4fd7c8a8..72cb9446be3 100644
--- a/app/component-library/components/Sheet/SheetHeader/__snapshots__/SheetHeader.test.tsx.snap
+++ b/app/component-library/components/Sheet/SheetHeader/__snapshots__/SheetHeader.test.tsx.snap
@@ -9,7 +9,7 @@ exports[`SheetHeader should render correctly 1`] = `
"flexDirection": "row",
"height": 32,
"justifyContent": "space-between",
- "paddingHorizontal": 16,
+ "margin": 16,
}
}
>
diff --git a/app/components/Nav/App/index.js b/app/components/Nav/App/index.js
index 8746a67fe83..8eca004830b 100644
--- a/app/components/Nav/App/index.js
+++ b/app/components/Nav/App/index.js
@@ -54,6 +54,7 @@ import ModalConfirmation from '../../../component-library/components/Modals/Moda
import Toast, {
ToastContext,
} from '../../../component-library/components/Toast';
+import AccountSelector from '../../../components/Views/AccountSelector';
import { TurnOffRememberMeModal } from '../../../components/UI/TurnOffRememberMeModal';
const Stack = createStackNavigator();
@@ -352,6 +353,10 @@ const App = ({ userLoggedIn }) => {
component={ModalConfirmation}
/>
+
- StyleSheet.create({
- account: {
- borderBottomWidth: StyleSheet.hairlineWidth,
- borderColor: colors.border.muted,
- flexDirection: 'row',
- paddingHorizontal: 20,
- paddingVertical: 20,
- height: 80,
- },
- disabledAccount: {
- opacity: 0.5,
- },
- accountInfo: {
- marginLeft: 15,
- marginRight: 0,
- flex: 1,
- flexDirection: 'row',
- },
- accountLabel: {
- fontSize: 18,
- color: colors.text.default,
- ...fontStyles.normal,
- },
- accountBalanceWrapper: {
- display: 'flex',
- flexDirection: 'row',
- },
- accountBalance: {
- paddingTop: 5,
- fontSize: 12,
- color: colors.text.alternative,
- ...fontStyles.normal,
- },
- accountBalanceError: {
- color: colors.error.default,
- marginLeft: 4,
- },
- importedView: {
- flex: 0.5,
- alignItems: 'flex-start',
- marginTop: 2,
- },
- accountMain: {
- flex: 1,
- flexDirection: 'column',
- },
- selectedWrapper: {
- flex: 0.2,
- alignItems: 'flex-end',
- },
- importedText: {
- color: colors.text.alternative,
- fontSize: 10,
- ...fontStyles.bold,
- },
- importedWrapper: {
- paddingHorizontal: 10,
- paddingVertical: 3,
- borderRadius: 10,
- borderWidth: 1,
- borderColor: colors.border.default,
- },
- });
-
-/**
- * View that renders specific account element in AccountList
- */
-class AccountElement extends PureComponent {
- static propTypes = {
- /**
- * Callback to be called onPress
- */
- onPress: PropTypes.func.isRequired,
- /**
- * Callback to be called onLongPress
- */
- onLongPress: PropTypes.func.isRequired,
- /**
- * Current ticker
- */
- ticker: PropTypes.string,
- /**
- * Whether the account element should be disabled (opaque and not clickable)
- */
- disabled: PropTypes.bool,
- item: PropTypes.object,
- /**
- * Updated balance using stored in state
- */
- updatedBalanceFromStore: PropTypes.string,
- };
-
- onPress = () => {
- const { onPress } = this.props;
- const { address } = this.props.item;
- onPress && onPress(address);
- };
-
- onLongPress = () => {
- const { onLongPress } = this.props;
- const { address, isImported, index } = this.props.item;
- onLongPress && onLongPress(address, isImported, index);
- };
-
- render() {
- const { disabled, updatedBalanceFromStore, ticker } = this.props;
- const {
- address,
- name,
- ens,
- isSelected,
- isImported,
- balanceError,
- isQRHardware,
- } = this.props.item;
- const colors = this.context.colors || mockTheme.colors;
- const styles = createStyles(colors);
-
- const selected = isSelected ? (
-
- ) : null;
- const tag =
- isImported || isQRHardware ? (
-
-
-
- {strings(
- isImported ? 'accounts.imported' : 'transaction.hardware',
- )}
-
-
-
- ) : null;
-
- return (
- true}>
-
-
-
-
-
- {isDefaultAccountName(name) && ens ? ens : name}
-
-
-
- {renderFromWei(updatedBalanceFromStore)} {getTicker(ticker)}
-
- {!!balanceError && (
-
- {balanceError}
-
- )}
-
-
- {!!tag && tag}
- {selected}
-
-
-
- );
- }
-}
-
-const mapStateToProps = (
- {
- engine: {
- backgroundState: { PreferencesController, AccountTrackerController },
- },
- },
- { item: { balance, address } },
-) => {
- const { selectedAddress } = PreferencesController;
- const { accounts } = AccountTrackerController;
- const selectedAccount = accounts[selectedAddress];
- const selectedAccountHasBalance =
- selectedAccount &&
- Object.prototype.hasOwnProperty.call(selectedAccount, BALANCE_KEY);
- const updatedBalanceFromStore =
- balance === EMPTY &&
- selectedAddress === address &&
- selectedAccount &&
- selectedAccountHasBalance
- ? selectedAccount[BALANCE_KEY]
- : balance;
- return {
- updatedBalanceFromStore,
- };
-};
-
-AccountElement.contextType = ThemeContext;
-
-export default connect(mapStateToProps)(AccountElement);
diff --git a/app/components/UI/AccountList/__snapshots__/index.test.tsx.snap b/app/components/UI/AccountList/__snapshots__/index.test.tsx.snap
deleted file mode 100644
index ccf4a0e25fe..00000000000
--- a/app/components/UI/AccountList/__snapshots__/index.test.tsx.snap
+++ /dev/null
@@ -1,7 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`Accounts should render correctly 1`] = `
-
-`;
diff --git a/app/components/UI/AccountList/index.js b/app/components/UI/AccountList/index.js
deleted file mode 100644
index aebeb325b52..00000000000
--- a/app/components/UI/AccountList/index.js
+++ /dev/null
@@ -1,476 +0,0 @@
-import React, { PureComponent } from 'react';
-import { KeyringTypes } from '@metamask/controllers';
-import Engine from '../../../core/Engine';
-import PropTypes from 'prop-types';
-import {
- Alert,
- ActivityIndicator,
- InteractionManager,
- FlatList,
- TouchableOpacity,
- StyleSheet,
- Text,
- View,
- SafeAreaView,
-} from 'react-native';
-import { fontStyles } from '../../../styles/common';
-import Device from '../../../util/device';
-import { strings } from '../../../../locales/i18n';
-import { toChecksumAddress } from 'ethereumjs-util';
-import Logger from '../../../util/Logger';
-import Analytics from '../../../core/Analytics/Analytics';
-import AnalyticsV2 from '../../../util/analyticsV2';
-import { ANALYTICS_EVENT_OPTS } from '../../../util/analytics';
-import { doENSReverseLookup } from '../../../util/ENSUtils';
-import AccountElement from './AccountElement';
-import { connect } from 'react-redux';
-import { ThemeContext, mockTheme } from '../../../util/theme';
-
-const createStyles = (colors) =>
- StyleSheet.create({
- wrapper: {
- backgroundColor: colors.background.default,
- borderTopLeftRadius: 10,
- borderTopRightRadius: 10,
- minHeight: 450,
- },
- titleWrapper: {
- width: '100%',
- height: 33,
- alignItems: 'center',
- justifyContent: 'center',
- borderBottomWidth: StyleSheet.hairlineWidth,
- borderColor: colors.border.muted,
- },
- dragger: {
- width: 48,
- height: 5,
- borderRadius: 4,
- backgroundColor: colors.border.default,
- opacity: Device.isAndroid() ? 0.6 : 0.5,
- },
- accountsWrapper: {
- flex: 1,
- },
- footer: {
- height: Device.isIphoneX() ? 200 : 170,
- paddingBottom: Device.isIphoneX() ? 30 : 0,
- justifyContent: 'center',
- flexDirection: 'column',
- alignItems: 'center',
- },
- btnText: {
- fontSize: 14,
- color: colors.primary.default,
- ...fontStyles.normal,
- },
- footerButton: {
- width: '100%',
- height: 55,
- alignItems: 'center',
- justifyContent: 'center',
- borderTopWidth: StyleSheet.hairlineWidth,
- borderColor: colors.border.muted,
- },
- });
-
-/**
- * View that contains the list of all the available accounts
- */
-class AccountList extends PureComponent {
- static propTypes = {
- /**
- * Map of accounts to information objects including balances
- */
- accounts: PropTypes.object,
- /**
- * An object containing each identity in the format address => account
- */
- identities: PropTypes.object,
- /**
- * A string representing the selected address => account
- */
- selectedAddress: PropTypes.string,
- /**
- * An object containing all the keyrings
- */
- keyrings: PropTypes.array,
- /**
- * function to be called when switching accounts
- */
- onAccountChange: PropTypes.func,
- /**
- * function to be called when importing an account
- */
- onImportAccount: PropTypes.func,
- /**
- * function to be called when connect to a QR hardware
- */
- onConnectHardware: PropTypes.func,
- /**
- * Current provider ticker
- */
- ticker: PropTypes.string,
- /**
- * Whether it will show options to create or import accounts
- */
- enableAccountsAddition: PropTypes.bool,
- /**
- * function to generate an error string based on a passed balance
- */
- getBalanceError: PropTypes.func,
- /**
- * Indicates whether third party API mode is enabled
- */
- thirdPartyApiMode: PropTypes.bool,
- /**
- * ID of the current network
- */
- network: PropTypes.string,
- };
-
- state = {
- loading: false,
- orderedAccounts: {},
- accountsENS: {},
- };
-
- flatList = React.createRef();
- lastPosition = 0;
- updating = false;
-
- componentDidMount() {
- this.mounted = true;
- const orderedAccounts = this.getAccounts();
- InteractionManager.runAfterInteractions(() => {
- this.assignENSToAccounts(orderedAccounts);
- if (orderedAccounts.length > 4) {
- const selectedAccountIndex =
- orderedAccounts.findIndex((account) => account.isSelected) || 0;
- this.scrollToCurrentAccount(selectedAccountIndex);
- }
- });
- this.mounted && this.setState({ orderedAccounts });
- }
-
- componentWillUnmount = () => {
- this.mounted = false;
- };
-
- scrollToCurrentAccount(selectedAccountIndex) {
- // eslint-disable-next-line no-unused-expressions
- this.flatList?.current?.scrollToIndex({
- index: selectedAccountIndex,
- animated: true,
- });
- }
-
- onAccountChange = async (newAddress) => {
- const { PreferencesController } = Engine.context;
- const { accounts } = this.props;
-
- requestAnimationFrame(async () => {
- try {
- // If not enabled is used from address book so we don't change accounts
- if (!this.props.enableAccountsAddition) {
- this.props.onAccountChange(newAddress);
- const orderedAccounts = this.getAccounts();
- this.mounted && this.setState({ orderedAccounts });
- return;
- }
-
- PreferencesController.setSelectedAddress(newAddress);
-
- this.props.onAccountChange();
-
- this.props.thirdPartyApiMode &&
- InteractionManager.runAfterInteractions(async () => {
- setTimeout(() => {
- Engine.refreshTransactionHistory();
- }, 1000);
- });
- } catch (e) {
- Logger.error(e, 'error while trying change the selected account'); // eslint-disable-line
- }
- InteractionManager.runAfterInteractions(() => {
- setTimeout(() => {
- // Track Event: "Switched Account"
- AnalyticsV2.trackEvent(
- AnalyticsV2.ANALYTICS_EVENTS.SWITCHED_ACCOUNT,
- {
- number_of_accounts: Object.keys(accounts ?? {}).length,
- },
- );
- }, 1000);
- });
- const orderedAccounts = this.getAccounts();
- this.mounted && this.setState({ orderedAccounts });
- });
- };
-
- importAccount = () => {
- this.props.onImportAccount();
- InteractionManager.runAfterInteractions(() => {
- Analytics.trackEvent(ANALYTICS_EVENT_OPTS.ACCOUNTS_IMPORTED_NEW_ACCOUNT);
- });
- };
-
- connectHardware = () => {
- this.props.onConnectHardware();
- AnalyticsV2.trackEvent(
- AnalyticsV2.ANALYTICS_EVENTS.CONNECT_HARDWARE_WALLET,
- );
- };
-
- addAccount = async () => {
- if (this.state.loading) return;
- this.mounted && this.setState({ loading: true });
- const { KeyringController } = Engine.context;
- requestAnimationFrame(async () => {
- try {
- await KeyringController.addNewAccount();
- const { PreferencesController } = Engine.context;
- const newIndex = Object.keys(this.props.identities).length - 1;
- PreferencesController.setSelectedAddress(
- Object.keys(this.props.identities)[newIndex],
- );
- setTimeout(() => {
- this.flatList &&
- this.flatList.current &&
- this.flatList.current.scrollToEnd();
- this.mounted && this.setState({ loading: false });
- }, 500);
- const orderedAccounts = this.getAccounts();
- this.mounted && this.setState({ orderedAccounts });
- } catch (e) {
- // Restore to the previous index in case anything goes wrong
- Logger.error(e, 'error while trying to add a new account'); // eslint-disable-line
- this.mounted && this.setState({ loading: false });
- }
- });
- InteractionManager.runAfterInteractions(() => {
- Analytics.trackEvent(ANALYTICS_EVENT_OPTS.ACCOUNTS_ADDED_NEW_ACCOUNT);
- });
- };
-
- isImported(allKeyrings, address) {
- let ret = false;
- for (const keyring of allKeyrings) {
- if (keyring.accounts.includes(address)) {
- ret = keyring.type === KeyringTypes.simple;
- break;
- }
- }
-
- return ret;
- }
-
- isQRHardware(allKeyrings, address) {
- let ret = false;
- for (const keyring of allKeyrings) {
- if (keyring.accounts.includes(address)) {
- ret = keyring.type === KeyringTypes.qr;
- break;
- }
- }
-
- return ret;
- }
-
- onLongPress = (address, imported, index) => {
- if (!imported) return;
- Alert.alert(
- strings('accounts.remove_account_title'),
- strings('accounts.remove_account_message'),
- [
- {
- text: strings('accounts.no'),
- onPress: () => false,
- style: 'cancel',
- },
- {
- text: strings('accounts.yes_remove_it'),
- onPress: async () => {
- const { PreferencesController } = Engine.context;
- const { selectedAddress } = this.props;
- const isRemovingCurrentAddress = selectedAddress === address;
- const fallbackAccountIndex = index - 1;
- const fallbackAccountAddress =
- this.state.orderedAccounts[fallbackAccountIndex].address;
-
- // TODO - Refactor logic. onAccountChange is only used for refreshing latest orderedAccounts after account removal. Duplicate call for PreferencesController.setSelectedAddress exists.
- // Set fallback address before removing account if removing current account
- isRemovingCurrentAddress &&
- PreferencesController.setSelectedAddress(fallbackAccountAddress);
- await Engine.context.KeyringController.removeAccount(address);
- // Default to the previous account in the list if removing current account
- this.onAccountChange(
- isRemovingCurrentAddress
- ? fallbackAccountAddress
- : selectedAddress,
- );
- },
- },
- ],
- { cancelable: false },
- );
- };
-
- renderItem = ({ item }) => {
- const { ticker } = this.props;
- const { accountsENS } = this.state;
- return (
-
- );
- };
-
- getAccounts() {
- const { accounts, identities, selectedAddress, keyrings, getBalanceError } =
- this.props;
- // This is a temporary fix until we can read the state from @metamask/controllers
- const allKeyrings =
- keyrings && keyrings.length
- ? keyrings
- : Engine.context.KeyringController.state.keyrings;
-
- const accountsOrdered = allKeyrings.reduce(
- (list, keyring) => list.concat(keyring.accounts),
- [],
- );
- return accountsOrdered
- .filter((address) => !!identities[toChecksumAddress(address)])
- .map((addr, index) => {
- const checksummedAddress = toChecksumAddress(addr);
- const identity = identities[checksummedAddress];
- const { name, address } = identity;
- const identityAddressChecksummed = toChecksumAddress(address);
- const isSelected = identityAddressChecksummed === selectedAddress;
- const isImported = this.isImported(
- allKeyrings,
- identityAddressChecksummed,
- );
- const isQRHardware = this.isQRHardware(
- allKeyrings,
- identityAddressChecksummed,
- );
- let balance = 0x0;
- if (accounts[identityAddressChecksummed]) {
- balance = accounts[identityAddressChecksummed].balance;
- }
-
- const balanceError = getBalanceError ? getBalanceError(balance) : null;
- return {
- index,
- name,
- address: identityAddressChecksummed,
- balance,
- isSelected,
- isImported,
- isQRHardware,
- balanceError,
- };
- });
- }
-
- assignENSToAccounts = (orderedAccounts) => {
- const { network } = this.props;
- orderedAccounts.forEach(async (account) => {
- try {
- const ens = await doENSReverseLookup(account.address, network);
- this.setState((state) => ({
- accountsENS: {
- ...state.accountsENS,
- [account.address]: ens,
- },
- }));
- } catch {
- // Error
- }
- });
- };
-
- keyExtractor = (item) => item.address;
-
- render() {
- const { orderedAccounts } = this.state;
- const { enableAccountsAddition } = this.props;
- const colors = this.context.colors || mockTheme.colors;
- const styles = createStyles(colors);
-
- return (
-
-
-
-
- ({
- length: 80,
- offset: 80 * index,
- index,
- })} // eslint-disable-line
- />
- {enableAccountsAddition && (
-
-
- {this.state.loading ? (
-
- ) : (
-
- {strings('accounts.create_new_account')}
-
- )}
-
-
-
- {strings('accounts.import_account')}
-
-
-
-
- {strings('accounts.connect_hardware')}
-
-
-
- )}
-
- );
- }
-}
-
-AccountList.contextType = ThemeContext;
-
-const mapStateToProps = (state) => ({
- accounts: state.engine.backgroundState.AccountTrackerController.accounts,
- thirdPartyApiMode: state.privacy.thirdPartyApiMode,
- keyrings: state.engine.backgroundState.KeyringController.keyrings,
- network: state.engine.backgroundState.NetworkController.network,
-});
-
-export default connect(mapStateToProps)(AccountList);
diff --git a/app/components/UI/AccountList/index.test.tsx b/app/components/UI/AccountList/index.test.tsx
deleted file mode 100644
index 112a3a07b49..00000000000
--- a/app/components/UI/AccountList/index.test.tsx
+++ /dev/null
@@ -1,30 +0,0 @@
-import React from 'react';
-import { shallow } from 'enzyme';
-import AccountList from './';
-import { Provider } from 'react-redux';
-import configureMockStore from 'redux-mock-store';
-
-const mockStore = configureMockStore();
-const address = '0xe7E125654064EEa56229f273dA586F10DF96B0a1';
-const store = mockStore({
- engine: {
- backgroundState: {
- AccountTrackerController: {
- accounts: {
- [address]: { name: 'account 1', address, balance: 0 },
- },
- },
- },
- },
-});
-
-describe('Accounts', () => {
- it('should render correctly', () => {
- const wrapper = shallow(
-
-
- ,
- );
- expect(wrapper).toMatchSnapshot();
- });
-});
diff --git a/app/components/UI/AccountOverview/index.js b/app/components/UI/AccountOverview/index.js
index 7775cb1a85a..5ba46dbb9ee 100644
--- a/app/components/UI/AccountOverview/index.js
+++ b/app/components/UI/AccountOverview/index.js
@@ -20,10 +20,7 @@ import { strings } from '../../../../locales/i18n';
import { swapsLivenessSelector } from '../../../reducers/swaps';
import { showAlert } from '../../../actions/alert';
import { protectWalletModalVisible } from '../../../actions/user';
-import {
- toggleAccountsModal,
- toggleReceiveModal,
-} from '../../../actions/modals';
+import { toggleReceiveModal } from '../../../actions/modals';
import { newAssetTransaction } from '../../../actions/transaction';
import Device from '../../../util/device';
@@ -164,10 +161,6 @@ class AccountOverview extends PureComponent {
/* Triggers global alert
*/
showAlert: PropTypes.func,
- /**
- * Action that toggles the accounts modal
- */
- toggleAccountsModal: PropTypes.func,
/**
* whether component is being rendered from onboarding wizard
*/
@@ -222,17 +215,12 @@ class AccountOverview extends PureComponent {
scrollViewContainer = React.createRef();
mainView = React.createRef();
- animatingAccountsModal = false;
-
- toggleAccountsModal = () => {
- const { onboardingWizard } = this.props;
- if (!onboardingWizard && !this.animatingAccountsModal) {
- this.animatingAccountsModal = true;
- this.props.toggleAccountsModal();
- setTimeout(() => {
- this.animatingAccountsModal = false;
- }, 500);
- }
+ openAccountSelector = () => {
+ const { onboardingWizard, navigation } = this.props;
+ !onboardingWizard &&
+ navigation.navigate(Routes.MODAL.ROOT_MODAL_FLOW, {
+ screen: Routes.SHEET.ACCOUNT_SELECTOR,
+ });
};
isAccountLabelDefined = (accountLabel) =>
@@ -395,7 +383,7 @@ class AccountOverview extends PureComponent {
({
const mapDispatchToProps = (dispatch) => ({
showAlert: (config) => dispatch(showAlert(config)),
- toggleAccountsModal: () => dispatch(toggleAccountsModal()),
protectWalletModalVisible: () => dispatch(protectWalletModalVisible()),
newAssetTransaction: (selectedAsset) =>
dispatch(newAssetTransaction(selectedAsset)),
diff --git a/app/components/UI/AccountRightButton/__snapshots__/index.test.tsx.snap b/app/components/UI/AccountRightButton/__snapshots__/index.test.tsx.snap
index c654a207d6d..4d9b41a0236 100644
--- a/app/components/UI/AccountRightButton/__snapshots__/index.test.tsx.snap
+++ b/app/components/UI/AccountRightButton/__snapshots__/index.test.tsx.snap
@@ -1,3 +1,3 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
-exports[`AccountRightButton should render correctly 1`] = ``;
+exports[`AccountRightButton should render correctly 1`] = ``;
diff --git a/app/components/UI/AccountRightButton/index.js b/app/components/UI/AccountRightButton/index.js
index 17764bff130..8b0927478a0 100644
--- a/app/components/UI/AccountRightButton/index.js
+++ b/app/components/UI/AccountRightButton/index.js
@@ -3,9 +3,10 @@ import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import { TouchableOpacity, StyleSheet } from 'react-native';
import Identicon from '../Identicon';
-import { toggleAccountsModal } from '../../../actions/modals';
import Device from '../../../util/device';
import AnalyticsV2 from '../../../util/analyticsV2';
+import { withNavigation } from '@react-navigation/compat';
+import Routes from '../../../constants/navigation/Routes';
const styles = StyleSheet.create({
leftButton: {
@@ -28,27 +29,21 @@ class AccountRightButton extends PureComponent {
* Selected address as string
*/
address: PropTypes.string,
- /**
- * Action that toggles the account modal
- */
- toggleAccountsModal: PropTypes.func,
/**
* List of accounts from the AccountTrackerController
*/
accounts: PropTypes.object,
+ /**
+ * Navigation object.
+ */
+ navigation: PropTypes.object,
};
- animating = false;
-
- toggleAccountsModal = () => {
- const { accounts } = this.props;
- if (!this.animating) {
- this.animating = true;
- this.props.toggleAccountsModal();
- setTimeout(() => {
- this.animating = false;
- }, 500);
- }
+ openAccountSelector = () => {
+ const { accounts, navigation } = this.props;
+ navigation.navigate(Routes.MODAL.ROOT_MODAL_FLOW, {
+ screen: Routes.SHEET.ACCOUNT_SELECTOR,
+ });
// Track Event: "Opened Acount Switcher"
AnalyticsV2.trackEvent(
AnalyticsV2.ANALYTICS_EVENTS.BROWSER_OPEN_ACCOUNT_SWITCH,
@@ -63,7 +58,7 @@ class AccountRightButton extends PureComponent {
return (
@@ -77,8 +72,7 @@ const mapStateToProps = (state) => ({
accounts: state.engine.backgroundState.AccountTrackerController.accounts,
});
-const mapDispatchToProps = (dispatch) => ({
- toggleAccountsModal: () => dispatch(toggleAccountsModal()),
-});
-
-export default connect(mapStateToProps, mapDispatchToProps)(AccountRightButton);
+export default connect(
+ mapStateToProps,
+ null,
+)(withNavigation(AccountRightButton));
diff --git a/app/components/UI/AccountSelectorList/AccountSelectorList.styles.ts b/app/components/UI/AccountSelectorList/AccountSelectorList.styles.ts
new file mode 100644
index 00000000000..90c0ffedf6d
--- /dev/null
+++ b/app/components/UI/AccountSelectorList/AccountSelectorList.styles.ts
@@ -0,0 +1,20 @@
+// Third party dependencies.
+import { StyleSheet } from 'react-native';
+
+/**
+ * Style sheet function for AvatarIcon component.
+ *
+ * @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 = () =>
+ StyleSheet.create({
+ balancesContainer: {
+ alignItems: 'flex-end',
+ },
+ balanceLabel: { textAlign: 'right' },
+ });
+
+export default styleSheet;
diff --git a/app/components/UI/AccountSelectorList/AccountSelectorList.tsx b/app/components/UI/AccountSelectorList/AccountSelectorList.tsx
new file mode 100644
index 00000000000..892092f3fa4
--- /dev/null
+++ b/app/components/UI/AccountSelectorList/AccountSelectorList.tsx
@@ -0,0 +1,153 @@
+// Third party dependencies.
+import React, { useCallback, useEffect, useRef } from 'react';
+import { ListRenderItem, View } from 'react-native';
+import { FlatList } from 'react-native-gesture-handler';
+import { useSelector } from 'react-redux';
+import { KeyringTypes } from '@metamask/controllers';
+
+// External dependencies.
+import Cell, {
+ CellVariants,
+} from '../../../component-library/components/Cells/Cell';
+import { useStyles } from '../../../component-library/hooks';
+import Text from '../../../component-library/components/Text';
+import AvatarGroup from '../../../component-library/components/Avatars/AvatarGroup';
+import UntypedEngine from '../../../core/Engine';
+import { formatAddress } from '../../../util/address';
+import { AvatarAccountType } from '../../../component-library/components/Avatars/AvatarAccount';
+import { isDefaultAccountName } from '../../../util/ENSUtils';
+import { strings } from '../../../../locales/i18n';
+import { AvatarVariants } from '../../../component-library/components/Avatars/Avatar.types';
+
+// Internal dependencies.
+import {
+ Account,
+ AccountSelectorListProps,
+ Assets,
+} from './AccountSelectorList.types';
+import styleSheet from './AccountSelectorList.styles';
+import { useAccounts } from './hooks';
+
+const AccountSelectorList = ({
+ onSelectAccount,
+ checkBalanceError,
+ isLoading = false,
+ ...props
+}: AccountSelectorListProps) => {
+ const Engine = UntypedEngine as any;
+ const accountListRef = useRef(null);
+ const { styles } = useStyles(styleSheet, {});
+ const accountAvatarType = useSelector((state: any) =>
+ state.settings.useBlockieIcon
+ ? AvatarAccountType.Blockies
+ : AvatarAccountType.JazzIcon,
+ );
+ const { accounts, ensByAccountAddress } = useAccounts({
+ checkBalanceError,
+ isLoading,
+ });
+
+ useEffect(() => {
+ if (!accounts.length) return;
+ const account = accounts.find(({ isSelected }) => isSelected);
+ if (account) {
+ // Wrap in timeout to provide more time for the list to render.
+ setTimeout(() => {
+ accountListRef?.current?.scrollToOffset({
+ offset: account.yOffset,
+ animated: false,
+ });
+ }, 0);
+ }
+ // eslint-disable-next-line
+ }, [accounts.length]);
+
+ const onPress = useCallback(
+ (address: string) => {
+ const { PreferencesController } = Engine.context;
+ PreferencesController.setSelectedAddress(address);
+ onSelectAccount?.(address);
+ },
+ /* eslint-disable-next-line */
+ [onSelectAccount],
+ );
+
+ const getKeyExtractor = ({ address }: Account) => address;
+
+ const getTagLabel = (type: KeyringTypes) => {
+ let label = '';
+ switch (type) {
+ case KeyringTypes.qr:
+ label = strings('transaction.hardware');
+ break;
+ case KeyringTypes.simple:
+ label = strings('accounts.imported');
+ break;
+ }
+ return label;
+ };
+
+ const renderAccountBalances = useCallback(
+ ({ fiatBalance, tokens }: Assets) => (
+
+ {fiatBalance}
+ {tokens && }
+
+ ),
+ [styles.balancesContainer, styles.balanceLabel],
+ );
+
+ const renderAccountItem: ListRenderItem = useCallback(
+ ({ item: { name, address, assets, type, isSelected, balanceError } }) => {
+ const shortAddress = formatAddress(address, 'short');
+ const tagLabel = getTagLabel(type);
+ const ensName = ensByAccountAddress[address];
+ const accountName =
+ isDefaultAccountName(name) && ensName ? ensName : name;
+ const isDisabled = !!balanceError || isLoading;
+
+ return (
+ onPress(address)}
+ avatarProps={{
+ variant: AvatarVariants.Account,
+ type: accountAvatarType,
+ accountAddress: address,
+ }}
+ tagLabel={tagLabel}
+ disabled={isDisabled}
+ /* eslint-disable-next-line */
+ style={{ opacity: isDisabled ? 0.5 : 1 }}
+ >
+ {assets && renderAccountBalances(assets)}
+ |
+ );
+ },
+ [
+ accountAvatarType,
+ onPress,
+ renderAccountBalances,
+ ensByAccountAddress,
+ isLoading,
+ ],
+ );
+
+ return (
+
+ );
+};
+
+export default AccountSelectorList;
diff --git a/app/components/UI/AccountSelectorList/AccountSelectorList.types.ts b/app/components/UI/AccountSelectorList/AccountSelectorList.types.ts
new file mode 100644
index 00000000000..7d2e985d5cc
--- /dev/null
+++ b/app/components/UI/AccountSelectorList/AccountSelectorList.types.ts
@@ -0,0 +1,75 @@
+// Third party dependencies.
+import { KeyringTypes } from '@metamask/controllers';
+import { FlatListProps } from 'react-native';
+
+// External dependencies.
+import { AvatarGroupToken } from '../../../component-library/components/Avatars/AvatarGroup/AvatarGroup.types';
+
+/**
+ * Asset information associated with the account, which includes both the fiat balance and owned tokens.
+ */
+export interface Assets {
+ /**
+ * Fiat balance in string format.
+ */
+ fiatBalance: string;
+ /**
+ * Tokens owned by this account.
+ */
+ tokens?: AvatarGroupToken[];
+}
+
+/**
+ * Account information.
+ */
+export interface Account {
+ /**
+ * Account name.
+ */
+ name: string;
+ /**
+ * Account address.
+ */
+ address: string;
+ /**
+ * Asset information associated with the account, which includes both the fiat balance and owned tokens.
+ */
+ assets?: Assets;
+ /**
+ * Account type.
+ */
+ type: KeyringTypes;
+ /**
+ * Y offset of the item. Used for scrolling purposes.
+ */
+ yOffset: number;
+ /**
+ * Boolean that indicates if the account is selected.
+ */
+ isSelected: boolean;
+ /**
+ * Optional error that indicates if the account has enough funds. Non-empty string will render the account item non-selectable.
+ */
+ balanceError?: string;
+}
+
+/**
+ * AccountSelectorList props.
+ */
+export interface AccountSelectorListProps
+ extends Partial> {
+ /**
+ * Optional callback to trigger when account is selected.
+ */
+ onSelectAccount?: (address: string) => void;
+ /**
+ * Optional callback that is used to check for a balance requirement. Non-empty string will render the account item non-selectable.
+ * @param balance - The ticker balance of an account in wei and hex string format.
+ */
+ checkBalanceError?: (balance: string) => string;
+ /**
+ * Optional boolean that indicates if accounts are being processed in the background. The accounts will be unselectable as long as this is true.
+ * @default false
+ */
+ isLoading?: boolean;
+}
diff --git a/app/components/UI/AccountSelectorList/hooks/index.ts b/app/components/UI/AccountSelectorList/hooks/index.ts
new file mode 100644
index 00000000000..632be0949b4
--- /dev/null
+++ b/app/components/UI/AccountSelectorList/hooks/index.ts
@@ -0,0 +1,3 @@
+/* eslint-disable import/prefer-default-export */
+
+export { useAccounts } from './useAccounts';
diff --git a/app/components/UI/AccountSelectorList/hooks/useAccounts/index.ts b/app/components/UI/AccountSelectorList/hooks/useAccounts/index.ts
new file mode 100644
index 00000000000..7228467747c
--- /dev/null
+++ b/app/components/UI/AccountSelectorList/hooks/useAccounts/index.ts
@@ -0,0 +1,4 @@
+/* eslint-disable import/prefer-default-export */
+
+export { useAccounts } from './useAccounts';
+export type { EnsByAccountAddress } from './useAccounts.types';
diff --git a/app/components/UI/AccountSelectorList/hooks/useAccounts/useAccounts.ts b/app/components/UI/AccountSelectorList/hooks/useAccounts/useAccounts.ts
new file mode 100644
index 00000000000..4dfba38565b
--- /dev/null
+++ b/app/components/UI/AccountSelectorList/hooks/useAccounts/useAccounts.ts
@@ -0,0 +1,205 @@
+/* eslint-disable import/prefer-default-export */
+
+// Third party dependencies.
+import { useCallback, useEffect, useRef, useState } from 'react';
+import { useSelector } from 'react-redux';
+import { toChecksumAddress } from 'ethereumjs-util';
+import { KeyringTypes } from '@metamask/controllers';
+import { isEqual } from 'lodash';
+
+// External Dependencies.
+import UntypedEngine from '../../../../../core/Engine';
+import { Account } from '../..';
+import { doENSReverseLookup } from '../../../../../util/ENSUtils';
+import { hexToBN, renderFromWei, weiToFiat } from '../../../../../util/number';
+import { getTicker } from '../../../../..//util/transactions';
+
+// Internal dependencies
+import {
+ EnsByAccountAddress,
+ UseAccounts,
+ UseAccountsParams,
+} from './useAccounts.types';
+
+/**
+ * Hook that returns both wallet accounts and ens name information.
+ *
+ * @returns Object that contins both wallet accounts and ens name information.
+ */
+export const useAccounts = ({
+ checkBalanceError,
+ isLoading = false,
+}: UseAccountsParams = {}): UseAccounts => {
+ const Engine = UntypedEngine as any;
+ const isMountedRef = useRef(false);
+ const [accounts, setAccounts] = useState([]);
+ const [ensByAccountAddress, setENSByAccountAddress] =
+ useState({});
+ const identities = useSelector(
+ (state: any) =>
+ state.engine.backgroundState.PreferencesController.identities,
+ );
+ const network = useSelector(
+ (state: any) => state.engine.backgroundState.NetworkController.network,
+ );
+ const selectedAddress = useSelector(
+ (state: any) =>
+ state.engine.backgroundState.PreferencesController.selectedAddress,
+ );
+ const accountInfoByAddress = useSelector(
+ (state: any) =>
+ state.engine.backgroundState.AccountTrackerController.accounts,
+ (left, right) => isEqual(left, right),
+ );
+ const conversionRate = useSelector(
+ (state: any) =>
+ state.engine.backgroundState.CurrencyRateController.conversionRate,
+ );
+ const currentCurrency = useSelector(
+ (state: any) =>
+ state.engine.backgroundState.CurrencyRateController.currentCurrency,
+ );
+ const ticker = useSelector(
+ (state: any) =>
+ state.engine.backgroundState.NetworkController.provider.ticker,
+ );
+
+ const fetchENSNames = useCallback(
+ async ({
+ flattenedAccounts,
+ startingIndex,
+ }: {
+ flattenedAccounts: Account[];
+ startingIndex: number;
+ }) => {
+ // Ensure index exists in account list.
+ let safeStartingIndex = startingIndex;
+ let mirrorIndex = safeStartingIndex - 1;
+ let latestENSbyAccountAddress: EnsByAccountAddress = {};
+
+ if (startingIndex < 0) {
+ safeStartingIndex = 0;
+ } else if (startingIndex > flattenedAccounts.length) {
+ safeStartingIndex = flattenedAccounts.length - 1;
+ }
+
+ const fetchENSName = async (accountIndex: number) => {
+ const { address } = flattenedAccounts[accountIndex];
+ try {
+ const ens: string | undefined = await doENSReverseLookup(
+ address,
+ network,
+ );
+ if (ens) {
+ latestENSbyAccountAddress = {
+ ...latestENSbyAccountAddress,
+ [address]: ens,
+ };
+ }
+ } catch (e) {
+ // ENS either doesn't exists or failed to fetch.
+ }
+ };
+
+ // Iterate outwards in both directions starting at the starting index.
+ while (mirrorIndex >= 0 || safeStartingIndex < flattenedAccounts.length) {
+ if (!isMountedRef.current) return;
+ if (safeStartingIndex < flattenedAccounts.length) {
+ await fetchENSName(safeStartingIndex);
+ }
+ if (mirrorIndex >= 0) {
+ await fetchENSName(mirrorIndex);
+ }
+ mirrorIndex--;
+ safeStartingIndex++;
+ setENSByAccountAddress(latestENSbyAccountAddress);
+ }
+ },
+ [network],
+ );
+
+ const getAccounts = useCallback(() => {
+ // Keep track of the Y position of account item. Used for scrolling purposes.
+ let yOffset = 0;
+ let selectedIndex = 0;
+ // Reading keyrings directly from Redux doesn't work at the momemt.
+ const keyrings: any[] = Engine.context.KeyringController.state.keyrings;
+ const flattenedAccounts: Account[] = keyrings.reduce((result, keyring) => {
+ const {
+ accounts: accountAddresses,
+ type,
+ }: { accounts: string[]; type: KeyringTypes } = keyring;
+ for (const index in accountAddresses) {
+ const address = accountAddresses[index];
+ const isSelected = selectedAddress === address;
+ if (isSelected) {
+ selectedIndex = result.length;
+ }
+ const checksummedAddress = toChecksumAddress(address);
+ const identity = identities[checksummedAddress];
+ if (!identity) continue;
+ const { name } = identity;
+ // TODO - Improve UI to either include loading and/or balance load failures.
+ const balanceWeiHex =
+ accountInfoByAddress?.[checksummedAddress]?.balance || 0x0;
+ const balanceETH = renderFromWei(balanceWeiHex); // Gives ETH
+ const balanceFiat = weiToFiat(
+ hexToBN(balanceWeiHex) as any,
+ conversionRate,
+ currentCurrency,
+ );
+ const balanceTicker = getTicker(ticker);
+ const balanceLabel = `${balanceFiat}\n${balanceETH} ${balanceTicker}`;
+ const balanceError = checkBalanceError?.(balanceWeiHex);
+ const mappedAccount: Account = {
+ name,
+ address: checksummedAddress,
+ type,
+ yOffset,
+ isSelected,
+ // TODO - Also fetch assets. Reference AccountList component.
+ // assets
+ assets: { fiatBalance: balanceLabel },
+ balanceError,
+ };
+ result.push(mappedAccount);
+ // Calculate height of the account item.
+ yOffset += 78;
+ if (balanceError) {
+ yOffset += 22;
+ }
+ if (type !== KeyringTypes.hd) {
+ yOffset += 24;
+ }
+ }
+ return result;
+ }, []);
+ setAccounts(flattenedAccounts);
+ fetchENSNames({ flattenedAccounts, startingIndex: selectedIndex });
+ /* eslint-disable-next-line */
+ }, [
+ selectedAddress,
+ identities,
+ fetchENSNames,
+ accountInfoByAddress,
+ conversionRate,
+ currentCurrency,
+ ticker,
+ checkBalanceError,
+ ]);
+
+ useEffect(() => {
+ if (!isMountedRef.current) {
+ isMountedRef.current = true;
+ }
+ if (isLoading) return;
+ // setTimeout is needed for now to ensure next frame contains updated keyrings.
+ setTimeout(getAccounts, 0);
+ // Once we can pull keyrings from Redux, we will replace the deps with keyrings.
+ return () => {
+ isMountedRef.current = false;
+ };
+ }, [getAccounts, isLoading]);
+
+ return { accounts, ensByAccountAddress };
+};
diff --git a/app/components/UI/AccountSelectorList/hooks/useAccounts/useAccounts.types.ts b/app/components/UI/AccountSelectorList/hooks/useAccounts/useAccounts.types.ts
new file mode 100644
index 00000000000..df6ae658af0
--- /dev/null
+++ b/app/components/UI/AccountSelectorList/hooks/useAccounts/useAccounts.types.ts
@@ -0,0 +1,41 @@
+/* eslint-disable import/prefer-default-export */
+
+import {
+ Account,
+ AccountSelectorListProps,
+} from '../../AccountSelectorList.types';
+
+/**
+ * Mapping of ENS names by account address.
+ */
+export type EnsByAccountAddress = Record;
+
+/**
+ * Optional params that useAccount hook takes.
+ */
+export interface UseAccountsParams {
+ /**
+ * Optional callback that is used to check for a balance requirement. Non-empty string will render the account item non-selectable.
+ * @param balance - The ticker balance of an account in wei and hex string format.
+ */
+ checkBalanceError?: AccountSelectorListProps['checkBalanceError'];
+ /**
+ * Optional boolean that indicates if accounts are being processed in the background. Setting this to true will prevent any unnecessary updates while loading.
+ * @default false
+ */
+ isLoading?: boolean;
+}
+
+/**
+ * Return value for useAccounts hook.
+ */
+export interface UseAccounts {
+ /**
+ * List of account information.
+ */
+ accounts: Account[];
+ /**
+ * Mapping of ENS names by account address.
+ */
+ ensByAccountAddress: EnsByAccountAddress;
+}
diff --git a/app/components/UI/AccountSelectorList/hooks/useAccounts/useAccountsTest.tsx b/app/components/UI/AccountSelectorList/hooks/useAccounts/useAccountsTest.tsx
new file mode 100644
index 00000000000..173b7e17168
--- /dev/null
+++ b/app/components/UI/AccountSelectorList/hooks/useAccounts/useAccountsTest.tsx
@@ -0,0 +1,65 @@
+// TODO - Finish tests
+
+// import { renderHook } from '@testing-library/react-hooks';
+// import { useAccounts } from './useAccounts';
+
+// const initialState = {
+// engine: {
+// backgroundState: {
+// PreferencesController: {
+// selectedAddress: '0xc4800C54cB70E7Dac746C2fA829da0004443613e',
+// identities: {
+// '0xc4800C54cB70E7Dac746C2fA829da0004443613e': { name: 'Account 1' },
+// },
+// },
+// NetworkController: {
+// network: '1',
+// provider: {
+// ticker: 'ETH',
+// },
+// },
+// CurrencyRateController: {
+// conversionRate: 1641.87,
+// currentCurrency: 'usd',
+// },
+// AccountTrackerController: {
+// accounts: {
+// '0xc4800C54cB70E7Dac746C2fA829da0004443613e': {
+// balance: '0x0',
+// },
+// },
+// },
+// },
+// },
+// };
+
+// jest.mock('react-redux', () => ({
+// ...jest.requireActual('react-redux'),
+// useSelector: jest.fn().mockImplementation((cb) => cb(initialState)),
+// }));
+
+// jest.mock('../../../../../core/Engine', () => ({
+// context: {
+// KeyringController: {
+// state: {
+// keyrings: [
+// {
+// accounts: ['0xc4800C54cB70E7Dac746C2fA829da0004443613e'],
+// type: 'HD Key Tree',
+// },
+// ],
+// },
+// },
+// },
+// }));
+
+// describe('useAccounts', () => {
+// test('it should start with a state of "Loading"', async () => {
+// const { result, waitForNextUpdate } = renderHook(() => useAccounts());
+// jest.runAllTimers();
+// await waitForNextUpdate();
+
+// // TODO - Actually test hook
+// expect(true).toBeTruthy();
+// });
+// });
diff --git a/app/components/UI/AccountSelectorList/index.ts b/app/components/UI/AccountSelectorList/index.ts
new file mode 100644
index 00000000000..34f107af8bc
--- /dev/null
+++ b/app/components/UI/AccountSelectorList/index.ts
@@ -0,0 +1,6 @@
+export { default } from './AccountSelectorList';
+export type {
+ Account,
+ AccountSelectorListProps,
+} from './AccountSelectorList.types';
+export * from './hooks';
diff --git a/app/components/UI/DrawerView/index.js b/app/components/UI/DrawerView/index.js
index 9f190e150dc..293115f553d 100644
--- a/app/components/UI/DrawerView/index.js
+++ b/app/components/UI/DrawerView/index.js
@@ -22,7 +22,6 @@ import {
} from '../../../util/networks';
import Identicon from '../Identicon';
import StyledButton from '../StyledButton';
-import AccountList from '../AccountList';
import NetworkList from '../NetworkList';
import { renderFromWei, renderFiat } from '../../../util/number';
import { strings } from '../../../../locales/i18n';
@@ -30,7 +29,6 @@ import Modal from 'react-native-modal';
import SecureKeychain from '../../../core/SecureKeychain';
import {
toggleNetworkModal,
- toggleAccountsModal,
toggleReceiveModal,
} from '../../../actions/modals';
import { showAlert } from '../../../actions/alert';
@@ -351,10 +349,6 @@ class DrawerView extends PureComponent {
* Action that toggles the network modal
*/
toggleNetworkModal: PropTypes.func,
- /**
- * Action that toggles the accounts modal
- */
- toggleAccountsModal: PropTypes.func,
/**
* Action that toggles the receive modal
*/
@@ -375,10 +369,6 @@ class DrawerView extends PureComponent {
* Start transaction with asset
*/
newAssetTransaction: PropTypes.func.isRequired,
- /**
- * Boolean that determines the status of the networks modal
- */
- accountsModalVisible: PropTypes.bool.isRequired,
/**
* Boolean that determines if the user has set a password before
*/
@@ -468,7 +458,6 @@ class DrawerView extends PureComponent {
previousBalance = null;
processedNewBalance = false;
animatingNetworksModal = false;
- animatingAccountsModal = false;
isCurrentAccountImported() {
let ret = false;
@@ -612,16 +601,17 @@ class DrawerView extends PureComponent {
}
};
- toggleAccountsModal = async () => {
- if (!this.animatingAccountsModal) {
- this.animatingAccountsModal = true;
- this.props.toggleAccountsModal();
- setTimeout(() => {
- this.animatingAccountsModal = false;
- }, 500);
- }
- !this.props.accountsModalVisible &&
- this.trackEvent(ANALYTICS_EVENT_OPTS.NAVIGATION_TAPS_ACCOUNT_NAME);
+ openAccountSelector = () => {
+ const { navigation } = this.props;
+ navigation.navigate(Routes.MODAL.ROOT_MODAL_FLOW, {
+ screen: Routes.SHEET.ACCOUNT_SELECTOR,
+ params: {
+ onOpenImportAccount: this.hideDrawer,
+ onOpenConnectHardwareWallet: this.hideDrawer,
+ onSelectAccount: this.hideDrawer,
+ },
+ });
+ this.trackEvent(ANALYTICS_EVENT_OPTS.NAVIGATION_TAPS_ACCOUNT_NAME);
};
toggleReceiveModal = () => {
@@ -831,27 +821,8 @@ class DrawerView extends PureComponent {
this.hideDrawer();
}
- hideDrawer() {
+ hideDrawer = () => {
this.props.onCloseDrawer();
- }
-
- onAccountChange = () => {
- setTimeout(() => {
- this.toggleAccountsModal();
- this.hideDrawer();
- }, 300);
- };
-
- onImportAccount = () => {
- this.toggleAccountsModal();
- this.props.navigation.navigate('ImportPrivateKeyView');
- this.hideDrawer();
- };
-
- onConnectHardware = () => {
- this.toggleAccountsModal();
- this.props.navigation.navigate('ConnectQRHardwareFlow');
- this.hideDrawer();
};
hasBlockExplorer = (providerType) => {
@@ -1160,7 +1131,6 @@ class DrawerView extends PureComponent {
accounts,
identities,
selectedAddress,
- keyrings,
currentCurrency,
ticker,
seedphraseBackedUp,
@@ -1234,7 +1204,7 @@ class DrawerView extends PureComponent {
@@ -1243,7 +1213,7 @@ class DrawerView extends PureComponent {
@@ -1416,28 +1386,6 @@ class DrawerView extends PureComponent {
onClose={this.closeInvalidCustomNetworkAlert}
/>
-
-
-
{this.renderOnboardingWizard()}
({
state.engine.backgroundState.CurrencyRateController.currentCurrency,
keyrings: state.engine.backgroundState.KeyringController.keyrings,
networkModalVisible: state.modals.networkModalVisible,
- accountsModalVisible: state.modals.accountsModalVisible,
receiveModalVisible: state.modals.receiveModalVisible,
passwordSet: state.user.passwordSet,
wizard: state.wizard,
@@ -1493,7 +1440,6 @@ const mapStateToProps = (state) => ({
const mapDispatchToProps = (dispatch) => ({
toggleNetworkModal: () => dispatch(toggleNetworkModal()),
- toggleAccountsModal: () => dispatch(toggleAccountsModal()),
toggleReceiveModal: () => dispatch(toggleReceiveModal()),
showAlert: (config) => dispatch(showAlert(config)),
newAssetTransaction: (selectedAsset) =>
diff --git a/app/components/UI/FiatOnRampAggregator/components/AccountSelector.tsx b/app/components/UI/FiatOnRampAggregator/components/AccountSelector.tsx
index 21c03a43a08..b5ae8f04990 100644
--- a/app/components/UI/FiatOnRampAggregator/components/AccountSelector.tsx
+++ b/app/components/UI/FiatOnRampAggregator/components/AccountSelector.tsx
@@ -1,13 +1,14 @@
-import React, { useCallback } from 'react';
+import React from 'react';
import { StyleSheet } from 'react-native';
-import { useDispatch, useSelector } from 'react-redux';
+import { useSelector } from 'react-redux';
-import { toggleAccountsModal } from '../../../../actions/modals';
import EthereumAddress from '../../EthereumAddress';
import JSIdenticon from '../../Identicon';
import BaseText from '../../../Base/Text';
import JSSelectorButton from '../../../Base/SelectorButton';
+import { useNavigation } from '@react-navigation/native';
+import Routes from '../../../../constants/navigation/Routes';
// TODO: Convert into typescript and correctly type
const SelectorButton = JSSelectorButton as any;
@@ -26,7 +27,7 @@ const styles = StyleSheet.create({
});
const AccountSelector = () => {
- const dispatch = useDispatch();
+ const navigation = useNavigation();
const selectedAddress = useSelector(
(state: any) =>
state.engine.backgroundState.PreferencesController.selectedAddress,
@@ -37,11 +38,13 @@ const AccountSelector = () => {
state.engine.backgroundState.PreferencesController.identities,
);
- const handleToggleAccountsModal = useCallback(() => {
- dispatch(toggleAccountsModal());
- }, [dispatch]);
+ const openAccountSelector = () =>
+ navigation.navigate(Routes.MODAL.ROOT_MODAL_FLOW, {
+ screen: Routes.SHEET.ACCOUNT_SELECTOR,
+ });
+
return (
-
+
{identities[selectedAddress]?.name} (
diff --git a/app/components/UI/FiatOrders/components/AccountSelector.js b/app/components/UI/FiatOrders/components/AccountSelector.js
index 2257c5ed348..e3500c1938d 100644
--- a/app/components/UI/FiatOrders/components/AccountSelector.js
+++ b/app/components/UI/FiatOrders/components/AccountSelector.js
@@ -2,12 +2,13 @@ import React from 'react';
import PropTypes from 'prop-types';
import { StyleSheet } from 'react-native';
import { connect } from 'react-redux';
+import { useNavigation } from '@react-navigation/native';
-import { toggleAccountsModal } from '../../../../actions/modals';
import EthereumAddress from '../../EthereumAddress';
import Identicon from '../../Identicon';
import Text from '../../../Base/Text';
import SelectorButton from '../../../Base/SelectorButton';
+import Routes from '../../../../constants/navigation/Routes';
const styles = StyleSheet.create({
selector: {
@@ -19,22 +20,26 @@ const styles = StyleSheet.create({
marginHorizontal: 5,
},
});
-const AccountSelector = ({
- toggleAccountsModal,
- selectedAddress,
- identities,
-}) => (
-
-
-
- {identities[selectedAddress]?.name} (
- )
-
-
-);
+const AccountSelector = ({ selectedAddress, identities }) => {
+ const navigation = useNavigation();
+
+ const openAccountSelector = () =>
+ navigation.navigate(Routes.MODAL.ROOT_MODAL_FLOW, {
+ screen: Routes.SHEET.ACCOUNT_SELECTOR,
+ });
+
+ return (
+
+
+
+ {identities[selectedAddress]?.name} (
+ )
+
+
+ );
+};
AccountSelector.propTypes = {
- toggleAccountsModal: PropTypes.func.isRequired,
selectedAddress: PropTypes.string.isRequired,
identities: PropTypes.object.isRequired,
};
@@ -45,7 +50,4 @@ const mapStateToProps = (state) => ({
identities: state.engine.backgroundState.PreferencesController.identities,
});
-const mapDispatchToProps = (dispatch) => ({
- toggleAccountsModal: () => dispatch(toggleAccountsModal()),
-});
-export default connect(mapStateToProps, mapDispatchToProps)(AccountSelector);
+export default connect(mapStateToProps, null)(AccountSelector);
diff --git a/app/components/Views/AccountSelector/AccountSelector.constants.ts b/app/components/Views/AccountSelector/AccountSelector.constants.ts
new file mode 100644
index 00000000000..b41f8b4becb
--- /dev/null
+++ b/app/components/Views/AccountSelector/AccountSelector.constants.ts
@@ -0,0 +1,5 @@
+/* eslint-disable import/prefer-default-export */
+
+export const ACCOUNT_LIST_ID = 'account-list';
+export const CREATE_ACCOUNT_BUTTON_ID = 'create-account-button';
+export const IMPORT_ACCOUNT_BUTTON_ID = 'import-account-button';
diff --git a/app/components/Views/AccountSelector/AccountSelector.tsx b/app/components/Views/AccountSelector/AccountSelector.tsx
new file mode 100644
index 00000000000..706bede0b6a
--- /dev/null
+++ b/app/components/Views/AccountSelector/AccountSelector.tsx
@@ -0,0 +1,130 @@
+// Third party dependencies.
+import React, { useCallback, useRef, useState } from 'react';
+import { useNavigation } from '@react-navigation/native';
+
+// External dependencies.
+import AccountSelectorList from '../../UI/AccountSelectorList';
+import SheetActions from '../../../component-library/components-temp/SheetActions';
+import SheetBottom, {
+ SheetBottomRef,
+} from '../../../component-library/components/Sheet/SheetBottom';
+import SheetHeader from '../../../component-library/components/Sheet/SheetHeader';
+import UntypedEngine from '../../../core/Engine';
+import Logger from '../../../util/Logger';
+import AnalyticsV2 from '../../../util/analyticsV2';
+import { ANALYTICS_EVENT_OPTS } from '../../../util/analytics';
+import { strings } from '../../../../locales/i18n';
+import {
+ ACCOUNT_LIST_ID,
+ CREATE_ACCOUNT_BUTTON_ID,
+ IMPORT_ACCOUNT_BUTTON_ID,
+} from './AccountSelector.constants';
+
+// Internal dependencies.
+import { AccountSelectorProps } from './AccountSelector.types';
+
+const AccountSelector = ({ route }: AccountSelectorProps) => {
+ const {
+ onCreateNewAccount,
+ onOpenImportAccount,
+ onOpenConnectHardwareWallet,
+ onSelectAccount,
+ checkBalanceError,
+ isSelectOnly,
+ } = route.params || {};
+ const Engine = UntypedEngine as any;
+ const [isLoading, setIsLoading] = useState(false);
+ const sheetRef = useRef(null);
+ const navigation = useNavigation();
+
+ const _onSelectAccount = (address: string) => {
+ sheetRef.current?.hide();
+ onSelectAccount?.(address);
+ };
+
+ const createNewAccount = useCallback(async () => {
+ const { KeyringController } = Engine.context;
+ try {
+ setIsLoading(true);
+ await KeyringController.addNewAccount();
+ AnalyticsV2.trackEvent(ANALYTICS_EVENT_OPTS.ACCOUNTS_ADDED_NEW_ACCOUNT);
+ } catch (e: any) {
+ Logger.error(e, 'error while trying to add a new account');
+ } finally {
+ setIsLoading(false);
+ }
+ onCreateNewAccount?.();
+ /* eslint-disable-next-line */
+ }, [onCreateNewAccount, setIsLoading]);
+
+ const openImportAccount = useCallback(() => {
+ sheetRef.current?.hide(() => {
+ navigation.navigate('ImportPrivateKeyView');
+ // Is this where we want to track importing an account or within ImportPrivateKeyView screen?
+ AnalyticsV2.trackEvent(
+ ANALYTICS_EVENT_OPTS.ACCOUNTS_IMPORTED_NEW_ACCOUNT,
+ );
+ });
+ onOpenImportAccount?.();
+ }, [onOpenImportAccount, navigation]);
+
+ const openConnectHardwareWallet = useCallback(() => {
+ sheetRef.current?.hide(() => {
+ navigation.navigate('ConnectQRHardwareFlow');
+ // Is this where we want to track connecting a hardware wallet or within ConnectQRHardwareFlow screen?
+ AnalyticsV2.trackEvent(
+ AnalyticsV2.ANALYTICS_EVENTS.CONNECT_HARDWARE_WALLET,
+ );
+ });
+ onOpenConnectHardwareWallet?.();
+ }, [onOpenConnectHardwareWallet, navigation]);
+
+ const renderSheetActions = useCallback(
+ () =>
+ !isSelectOnly && (
+
+ ),
+ [
+ isSelectOnly,
+ isLoading,
+ createNewAccount,
+ openImportAccount,
+ openConnectHardwareWallet,
+ ],
+ );
+
+ return (
+
+
+
+ {renderSheetActions()}
+
+ );
+};
+
+export default AccountSelector;
diff --git a/app/components/Views/AccountSelector/AccountSelector.types.ts b/app/components/Views/AccountSelector/AccountSelector.types.ts
new file mode 100644
index 00000000000..3e9758cd7a7
--- /dev/null
+++ b/app/components/Views/AccountSelector/AccountSelector.types.ts
@@ -0,0 +1,40 @@
+// External dependencies.
+import { AccountSelectorListProps } from '../../../components/UI/AccountSelectorList';
+
+/**
+ * AccountSelectorProps props.
+ */
+export interface AccountSelectorProps {
+ /**
+ * Props that are passed in while navigating to screen.
+ */
+ route: {
+ params?: {
+ /**
+ * Optional callback that is called whenever a new account is being created.
+ */
+ onCreateNewAccount?: () => void;
+ /**
+ * Optional callback that is called whenever import account is being opened.
+ */
+ onOpenImportAccount?: () => void;
+ /**
+ * Optional callback that is called whenever connect hardware wallet is being opened.
+ */
+ onOpenConnectHardwareWallet?: () => void;
+ /**
+ * Optional callback that is called whenever an account is selected.
+ */
+ onSelectAccount?: (address: string) => void;
+ /**
+ * Optional boolean that indicates if the sheet is for selection only. Other account actions are disabled when this is true.
+ */
+ isSelectOnly?: boolean;
+ /**
+ * Optional callback that is used to check for a balance requirement. Non-empty string will render the account item non-selectable.
+ * @param balance - The ticker balance of an account in wei and hex string format.
+ */
+ checkBalanceError?: AccountSelectorListProps['checkBalanceError'];
+ };
+ };
+}
diff --git a/app/components/Views/AccountSelector/index.ts b/app/components/Views/AccountSelector/index.ts
new file mode 100644
index 00000000000..ef2b9e5a0de
--- /dev/null
+++ b/app/components/Views/AccountSelector/index.ts
@@ -0,0 +1 @@
+export { default } from './AccountSelector';
diff --git a/app/components/Views/SendFlow/Confirm/__snapshots__/index.test.tsx.snap b/app/components/Views/SendFlow/Confirm/__snapshots__/index.test.tsx.snap
index 91d863c8289..f82e4403280 100644
--- a/app/components/Views/SendFlow/Confirm/__snapshots__/index.test.tsx.snap
+++ b/app/components/Views/SendFlow/Confirm/__snapshots__/index.test.tsx.snap
@@ -14,16 +14,6 @@ exports[`Confirm should render correctly 1`] = `
conversionRate={1}
currentCurrency="USD"
identities={Object {}}
- keyrings={
- Array [
- Object {
- "accounts": Array [
- "0x",
- ],
- "type": "HD Key Tree",
- },
- ]
- }
network="1"
networkType="mainnet"
prepareTransaction={[Function]}
diff --git a/app/components/Views/SendFlow/Confirm/index.js b/app/components/Views/SendFlow/Confirm/index.js
index bada08f1072..2f6961f787d 100644
--- a/app/components/Views/SendFlow/Confirm/index.js
+++ b/app/components/Views/SendFlow/Confirm/index.js
@@ -46,7 +46,6 @@ import {
import { getGasLimit } from '../../../../util/custom-gas';
import Engine from '../../../../core/Engine';
import Logger from '../../../../util/Logger';
-import AccountList from '../../../UI/AccountList';
import CustomNonceModal from '../../../UI/CustomNonceModal';
import { doENSReverseLookup } from '../../../../util/ENSUtils';
import NotificationManager from '../../../../core/NotificationManager';
@@ -324,10 +323,6 @@ class Confirm extends PureComponent {
* List of accounts from the PreferencesController
*/
identities: PropTypes.object,
- /**
- * List of keyrings
- */
- keyrings: PropTypes.array,
/**
* Selected asset from current transaction state
*/
@@ -387,7 +382,6 @@ class Confirm extends PureComponent {
transactionTotalAmount: undefined,
transactionTotalAmountFiat: undefined,
errorMessage: undefined,
- fromAccountModalVisible: false,
warningModalVisible: false,
mode: REVIEW,
gasSelected: AppConstants.GAS_OPTIONS.MEDIUM,
@@ -1030,13 +1024,11 @@ class Confirm extends PureComponent {
return balanceIsInsufficient ? strings('transaction.insufficient') : null;
};
- onAccountChange = async (accountAddress) => {
+ onSelectAccount = async (accountAddress) => {
const { identities, accounts } = this.props;
const { name } = identities[accountAddress];
- const { PreferencesController } = Engine.context;
const ens = await doENSReverseLookup(accountAddress);
const fromAccountName = ens || name;
- PreferencesController.setSelectedAddress(accountAddress);
// If new account doesn't have the asset
this.setState({
fromAccountName,
@@ -1044,7 +1036,18 @@ class Confirm extends PureComponent {
balanceIsZero: hexToBN(accounts[accountAddress].balance).isZero(),
});
this.parseTransactionDataHeader();
- this.toggleFromAccountModal();
+ };
+
+ openAccountSelector = () => {
+ const { navigation } = this.props;
+ navigation.navigate(Routes.MODAL.ROOT_MODAL_FLOW, {
+ screen: Routes.SHEET.ACCOUNT_SELECTOR,
+ params: {
+ isSelectOnly: true,
+ onSelectAccount: this.onSelectAccount,
+ checkBalanceError: this.getBalanceError,
+ },
+ });
};
toggleHexDataModal = () => {
@@ -1052,11 +1055,6 @@ class Confirm extends PureComponent {
this.setState({ hexDataModalVisible: !hexDataModalVisible });
};
- toggleFromAccountModal = () => {
- const { fromAccountModalVisible } = this.state;
- this.setState({ fromAccountModalVisible: !fromAccountModalVisible });
- };
-
cancelGasEdition = () => {
this.setState({
EIP1559TransactionDataTemp: { ...this.state.EIP1559TransactionData },
@@ -1286,37 +1284,6 @@ class Confirm extends PureComponent {
);
};
- renderFromAccountModal = () => {
- const { identities, keyrings, ticker } = this.props;
- const { fromAccountModalVisible, fromSelectedAddress } = this.state;
- const colors = this.context.colors || mockTheme.colors;
- const styles = createStyles(colors);
-
- return (
-
-
-
- );
- };
-
buyEth = () => {
const { navigation } = this.props;
try {
@@ -1464,7 +1431,7 @@ class Confirm extends PureComponent {
>
- {this.renderFromAccountModal()}
{mode === EDIT && this.renderCustomGasModalLegacy()}
{mode === EDIT_NONCE && this.renderCustomNonceModal()}
{mode === EDIT_EIP1559 && this.renderCustomGasModalEIP1559()}
@@ -1666,7 +1632,6 @@ const mapStateToProps = (state) => ({
showCustomNonce: state.settings.showCustomNonce,
chainId: state.engine.backgroundState.NetworkController.provider.chainId,
ticker: state.engine.backgroundState.NetworkController.provider.ticker,
- keyrings: state.engine.backgroundState.KeyringController.keyrings,
transaction: getNormalizedTxState(state),
selectedAsset: state.transaction.selectedAsset,
transactionState: state.transaction,
diff --git a/app/components/Views/SendFlow/SendTo/__snapshots__/index.test.tsx.snap b/app/components/Views/SendFlow/SendTo/__snapshots__/index.test.tsx.snap
index 01d047181fd..2d985d0e51f 100644
--- a/app/components/Views/SendFlow/SendTo/__snapshots__/index.test.tsx.snap
+++ b/app/components/Views/SendFlow/SendTo/__snapshots__/index.test.tsx.snap
@@ -29,16 +29,6 @@ exports[`SendTo should render correctly 1`] = `
},
}
}
- keyrings={
- Array [
- Object {
- "accounts": Array [
- "0x",
- ],
- "type": "HD Key Tree",
- },
- ]
- }
network="1"
newAssetTransaction={[Function]}
providerType="mainnet"
diff --git a/app/components/Views/SendFlow/SendTo/index.js b/app/components/Views/SendFlow/SendTo/index.js
index 86d6c118cfd..b8663c8f314 100644
--- a/app/components/Views/SendFlow/SendTo/index.js
+++ b/app/components/Views/SendFlow/SendTo/index.js
@@ -12,8 +12,6 @@ import {
ScrollView,
} from 'react-native';
import { AddressFrom, AddressTo } from './../AddressInputs';
-import Modal from 'react-native-modal';
-import AccountList from '../../../UI/AccountList';
import { connect } from 'react-redux';
import { renderFromWei } from '../../../../util/number';
import ActionModal from '../../../UI/ActionModal';
@@ -218,10 +216,6 @@ class SendFlow extends PureComponent {
* List of accounts from the PreferencesController
*/
identities: PropTypes.object,
- /**
- * List of keyrings
- */
- keyrings: PropTypes.array,
/**
* Current provider ticker
*/
@@ -257,7 +251,6 @@ class SendFlow extends PureComponent {
state = {
addressError: undefined,
balanceIsZero: false,
- fromAccountModalVisible: false,
addToAddressBookModalVisible: false,
fromSelectedAddress: this.props.selectedAddress,
fromAccountName: this.props.identities[this.props.selectedAddress].name,
@@ -331,11 +324,6 @@ class SendFlow extends PureComponent {
this.updateNavBar();
};
- toggleFromAccountModal = () => {
- const { fromAccountModalVisible } = this.state;
- this.setState({ fromAccountModalVisible: !fromAccountModalVisible });
- };
-
toggleAddToAddressBookModal = () => {
const { addToAddressBookModalVisible } = this.state;
this.setState({
@@ -343,16 +331,14 @@ class SendFlow extends PureComponent {
});
};
- onAccountChange = async (accountAddress) => {
- const { identities, ticker, accounts } = this.props;
+ onSelectAccount = async (accountAddress) => {
+ const { ticker, accounts, identities } = this.props;
const { name } = identities[accountAddress];
- const { PreferencesController } = Engine.context;
const fromAccountBalance = `${renderFromWei(
accounts[accountAddress].balance,
)} ${getTicker(ticker)}`;
const ens = await doENSReverseLookup(accountAddress);
const fromAccountName = ens || name;
- PreferencesController.setSelectedAddress(accountAddress);
// If new account doesn't have the asset
this.props.setSelectedAsset(getEther(ticker));
this.setState({
@@ -361,8 +347,19 @@ class SendFlow extends PureComponent {
fromSelectedAddress: accountAddress,
balanceIsZero: hexToBN(accounts[accountAddress].balance).isZero(),
});
- this.toggleFromAccountModal();
};
+
+ openAccountSelector = () => {
+ const { navigation } = this.props;
+ navigation.navigate(Routes.MODAL.ROOT_MODAL_FLOW, {
+ screen: Routes.SHEET.ACCOUNT_SELECTOR,
+ params: {
+ isSelectOnly: true,
+ onSelectAccount: this.onSelectAccount,
+ },
+ });
+ };
+
/**
* This returns the address name from the address book or user accounts if the selectedAddress exist there
* @param {String} toAccount - Address input
@@ -673,36 +670,6 @@ class SendFlow extends PureComponent {
);
};
- renderFromAccountModal = () => {
- const { identities, keyrings, ticker } = this.props;
- const { fromAccountModalVisible, fromSelectedAddress } = this.state;
- const colors = this.context.colors || mockTheme.colors;
- const styles = createStyles(colors);
-
- return (
-
-
-
- );
- };
-
onToInputFocus = () => {
const { toInputHighlighted } = this.state;
this.setState({ toInputHighlighted: !toInputHighlighted });
@@ -784,7 +751,7 @@ class SendFlow extends PureComponent {
>
)}
-
- {this.renderFromAccountModal()}
{this.renderAddToAddressBookModal()}
);
@@ -937,7 +902,6 @@ const mapStateToProps = (state) => ({
state.engine.backgroundState.PreferencesController.selectedAddress,
selectedAsset: state.transaction.selectedAsset,
identities: state.engine.backgroundState.PreferencesController.identities,
- keyrings: state.engine.backgroundState.KeyringController.keyrings,
ticker: state.engine.backgroundState.NetworkController.provider.ticker,
network: state.engine.backgroundState.NetworkController.network,
providerType: state.engine.backgroundState.NetworkController.provider.type,
diff --git a/app/constants/navigation/Routes.ts b/app/constants/navigation/Routes.ts
index 03d27d4f5ba..261f12fbf5f 100644
--- a/app/constants/navigation/Routes.ts
+++ b/app/constants/navigation/Routes.ts
@@ -28,6 +28,9 @@ const Routes = {
LOGIN: 'Login',
NAV: 'OnboardingNav',
},
+ SHEET: {
+ ACCOUNT_SELECTOR: 'AccountSelector',
+ },
};
export default Routes;
diff --git a/app/reducers/modals/index.js b/app/reducers/modals/index.js
index bae979c7904..1f2d416d1d0 100644
--- a/app/reducers/modals/index.js
+++ b/app/reducers/modals/index.js
@@ -1,6 +1,5 @@
const initialState = {
networkModalVisible: false,
- accountsModalVisible: false,
collectibleContractModalVisible: false,
receiveModalVisible: false,
receiveAsset: undefined,
@@ -22,11 +21,6 @@ const modalsReducer = (state = initialState, action) => {
receiveAsset: action.asset,
};
}
- case 'TOGGLE_ACCOUNT_MODAL':
- return {
- ...state,
- accountsModalVisible: !state.accountsModalVisible,
- };
case 'TOGGLE_COLLECTIBLE_CONTRACT_MODAL':
return {
...state,
diff --git a/app/util/ENSUtils.js b/app/util/ENSUtils.js
index 47030c42f43..7da3a063b02 100644
--- a/app/util/ENSUtils.js
+++ b/app/util/ENSUtils.js
@@ -7,6 +7,10 @@ import { ethers } from 'ethers';
import { toLowerCaseEquals } from '../util/general';
import { getEthersNetworkTypeById } from './networks';
import Logger from './Logger';
+const ENS_NAME_NOT_DEFINED_ERROR = 'ENS name not defined';
+const INVALID_ENS_NAME_ERROR = 'invalid ENS name';
+// One hour cache threshold.
+const CACHE_REFRESH_THRESHOLD = 60 * 60 * 1000;
/**
* Utility class with the single responsibility
@@ -29,22 +33,31 @@ export function getEnsProvider(network, provider) {
}
export async function doENSReverseLookup(address, network) {
- const cache = ENSCache.cache[network + address];
const { provider } = Engine.context.NetworkController;
- if (cache) {
- return Promise.resolve(cache);
+ const { name: cachedName, timestamp } =
+ ENSCache.cache[network + address] || {};
+ const nowTimestamp = Date.now();
+ if (timestamp && nowTimestamp - timestamp < CACHE_REFRESH_THRESHOLD) {
+ return Promise.resolve(cachedName);
}
- const ensProvider = await getEnsProvider(network, provider);
- if (ensProvider) {
- try {
+
+ try {
+ const ensProvider = await getEnsProvider(network, provider);
+ if (ensProvider) {
const name = await ensProvider.lookupAddress(address);
const resolvedAddress = await ensProvider.resolveName(name);
if (toLowerCaseEquals(address, resolvedAddress)) {
- ENSCache.cache[network + address] = name;
+ ENSCache.cache[network + address] = { name, timestamp: Date.now() };
return name;
}
- // eslint-disable-next-line no-empty
- } catch (e) {}
+ }
+ } catch (e) {
+ if (
+ e.message.includes(ENS_NAME_NOT_DEFINED_ERROR) ||
+ e.message.includes(INVALID_ENS_NAME_ERROR)
+ ) {
+ ENSCache.cache[network + address] = { timestamp: Date.now() };
+ }
}
}
diff --git a/app/util/analyticsV2.js b/app/util/analyticsV2.js
index c1f4887ddcc..b20cd3a48de 100644
--- a/app/util/analyticsV2.js
+++ b/app/util/analyticsV2.js
@@ -186,7 +186,7 @@ export const ANALYTICS_EVENTS_V2 = {
* @param {Object} eventName
* @param {Object} params
*/
-export const trackEventV2 = (eventName, params) => {
+export const trackEventV2 = (eventName, params = undefined) => {
InteractionManager.runAfterInteractions(() => {
let anonymousEvent = false;
try {
diff --git a/locales/languages/en.json b/locales/languages/en.json
index c5653996282..4a27b3aeb6d 100644
--- a/locales/languages/en.json
+++ b/locales/languages/en.json
@@ -437,14 +437,17 @@
"receive": "RECEIVE"
},
"accounts": {
- "create_new_account": "Create New Account",
- "import_account": "Import an Account",
- "connect_hardware": "Connect Hardware Wallet",
- "imported": "IMPORTED",
+ "create_new_account": "Create a new account",
+ "import_account": "Import an account",
+ "connect_hardware": "Connect hardware wallet",
+ "imported": "Imported",
"remove_account_title": "Account removal",
"remove_account_message": "Do you really want to remove this account?",
"no": "No",
- "yes_remove_it": "Yes, remove it"
+ "yes_remove_it": "Yes, remove it",
+ "account_selector": {
+ "title": "Accounts"
+ }
},
"connect_qr_hardware": {
"title": "Connect a QR-based hardware wallet",
diff --git a/package.json b/package.json
index bdbf3f6c008..733703f1462 100644
--- a/package.json
+++ b/package.json
@@ -135,6 +135,7 @@
"@sentry/integrations": "6.3.1",
"@sentry/react-native": "2.4.2",
"@tradle/react-native-http": "2.0.1",
+ "@types/lodash": "^4.14.184",
"@walletconnect/client": "^1.7.1",
"@walletconnect/utils": "^1.7.1",
"asyncstorage-down": "4.2.0",
@@ -169,6 +170,7 @@
"is-url": "^1.2.4",
"json-rpc-engine": "^6.1.0",
"json-rpc-middleware-stream": "3.0.0",
+ "lodash": "^4.17.21",
"lottie-react-native": "git+https://github.com/MetaMask/lottie-react-native.git#7ce6a78ac4ac7b9891bc513cb3f12f8b9c9d9106",
"metro-config": "^0.71.1",
"multihashes": "0.4.14",
diff --git a/yarn.lock b/yarn.lock
index 8356a4715eb..803e5c129fc 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -3967,6 +3967,11 @@
dependencies:
"@types/node" "*"
+"@types/lodash@^4.14.184":
+ version "4.14.184"
+ resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.184.tgz#23f96cd2a21a28e106dc24d825d4aa966de7a9fe"
+ integrity sha512-RoZphVtHbxPZizt4IcILciSWiC6dcn+eZ8oX9IWEYfDMcocdd42f7NPI6fQj+6zI8y4E0L7gu2pcZKLGTRaV9Q==
+
"@types/node@*":
version "16.0.1"
resolved "https://registry.yarnpkg.com/@types/node/-/node-16.0.1.tgz#70cedfda26af7a2ca073fdcc9beb2fff4aa693f8"
@@ -11736,7 +11741,7 @@ lodash-es@^4.17.15:
lodash.debounce@^4.0.8:
version "4.0.8"
resolved "https://registry.yarnpkg.com/lodash.debounce/-/lodash.debounce-4.0.8.tgz#82d79bff30a67c4005ffd5e2515300ad9ca4d7af"
- integrity sha1-gteb/zCmfEAF/9XiUVMArZyk168=
+ integrity sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==
lodash.escape@^4.0.1:
version "4.0.1"
@@ -11751,7 +11756,7 @@ lodash.flattendeep@^4.4.0:
lodash.isequal@^4.5.0:
version "4.5.0"
resolved "https://registry.yarnpkg.com/lodash.isequal/-/lodash.isequal-4.5.0.tgz#415c4478f2bcc30120c22ce10ed3226f7d3e18e0"
- integrity sha1-QVxEePK8wwEgwizhDtMib30+GOA=
+ integrity sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==
lodash.isplainobject@^4.0.6:
version "4.0.6"