Skip to content

Commit

Permalink
feat: Implement staking advanced details
Browse files Browse the repository at this point in the history
  • Loading branch information
pedronfigueiredo committed Feb 18, 2025
1 parent a075903 commit d24fc39
Show file tree
Hide file tree
Showing 9 changed files with 342 additions and 1 deletion.
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { StyleSheet } from 'react-native';

const styleSheet = () =>
StyleSheet.create({
container:{
paddingVertical: 8
},
networkContainer: {
display: 'flex',
flexDirection: 'row',
alignItems: 'center',
}
});

export default styleSheet;
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { fireEvent } from '@testing-library/react-native';
import React from 'react';
import { stakingDepositConfirmationState } from '../../../../../../util/test/confirm-data-helpers';
import renderWithProvider from '../../../../../../util/test/renderWithProvider';
import AdvancedDetails from './AdvancedDetails';

describe('AdvancedDetails', () => {
it('contains values for staking deposit', async () => {
const { getByText } = renderWithProvider(<AdvancedDetails />, {
state: stakingDepositConfirmationState,
});

expect(getByText('Advanced details')).toBeDefined();

fireEvent(getByText('Advanced details'), 'onPress');

expect(getByText('Advanced details')).toBeDefined();

expect(getByText('Staking from')).toBeDefined();
expect(getByText('0xDc477...0c164')).toBeDefined();

expect(getByText('Interacting with')).toBeDefined();
expect(getByText('0x4FEF9...d47Df')).toBeDefined();

expect(getByText('Network')).toBeDefined();
expect(getByText('Ethereum Mainnet')).toBeDefined();
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import { CHAIN_IDS, TransactionMeta } from '@metamask/transaction-controller';
import React from 'react';
import { View } from 'react-native';
import { strings } from '../../../../../../../locales/i18n';
import { AvatarSize } from '../../../../../../component-library/components/Avatars/Avatar';
import Badge, { BadgeVariant } from '../../../../../../component-library/components/Badges/Badge';
import Text from '../../../../../../component-library/components/Texts/Text';
import { useStyles } from '../../../../../../component-library/hooks';
import images from '../../../../../../images/image-icons';
import Name from '../../../../../UI/Name';
import { NameType } from '../../../../../UI/Name/Name.types';
import { useTransactionMetadataRequest } from '../../../hooks/useTransactionMetadataRequest';
import InfoRow from '../../UI/InfoRow';
import InfoSectionAccordion from '../../UI/InfoSectionAccordion';
import styleSheet from './AdvancedDetails.styles';

const AdvancedDetails = () => {
const { styles } = useStyles(styleSheet, {});
const transactionMeta = useTransactionMetadataRequest();

return (
<View style={styles.container}>
<InfoSectionAccordion header={strings('stake.advanced_details')}>
<InfoRow
label={strings('confirm.staking_from')}
>
<Name
type={NameType.EthereumAddress}
value={(transactionMeta as TransactionMeta).txParams.from}
variation={CHAIN_IDS.MAINNET}
/>
</InfoRow>
<InfoRow
label={strings('confirm.interacting_with')}
>
<Name
type={NameType.EthereumAddress}
value={(transactionMeta as TransactionMeta).txParams.to as string}
variation={CHAIN_IDS.MAINNET}
/>
</InfoRow>
<InfoRow
label={strings('confirm.network')}
>
<View style={styles.networkContainer}>
<Badge
size={AvatarSize.Xs}
imageSource={images.ETHEREUM}
variant={BadgeVariant.Network}
isScaled={false}
/>
<Text>{' '}</Text>
<Text>{strings('stake.ethereum_mainnet')}</Text>
</View>
</InfoRow>
</InfoSectionAccordion>
</View>
);
};

export default AdvancedDetails;
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import React from 'react';

import { strings } from '../../../../../../../../locales/i18n';
import AdvancedDetails from '../../AdvancedDetails/AdvancedDetails';
import FlatNavHeader from '../../FlatNavHeader';
import StakingDetails from '../../StakingDetails';
import TokenHero from '../../TokenHero';
Expand All @@ -10,6 +10,7 @@ const StakingDeposit = () => (
<FlatNavHeader title={strings('stake.stake')} />
<TokenHero />
<StakingDetails />
<AdvancedDetails />
</>
);
export default StakingDeposit;
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { StyleSheet } from 'react-native';
import { Theme } from '../../../../../../util/theme/models';

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

return StyleSheet.create({
container: {
overflow: 'hidden',
backgroundColor: theme.colors.background.default,
borderRadius: 8,
padding: 8,
marginBottom: 8,
},
header: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
padding: 8,
},
headerTitle: {
color: theme.colors.text.default,
fontSize: 14,
fontWeight: '500',
},
icon: {
color: theme.colors.text.muted,
},
content: {},
iconContainer: {},
});
};

export default styleSheet;
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import { fireEvent } from '@testing-library/react-native';
import React from 'react';
import Text from '../../../../../../component-library/components/Texts/Text';
import { stakingDepositConfirmationState } from '../../../../../../util/test/confirm-data-helpers';
import renderWithProvider from '../../../../../../util/test/renderWithProvider';
import InfoSectionAccordion from './InfoSectionAccordion';

describe('InfoSectionAccordion', () => {
it("opens and closes the accordion when it's collapsed by default", async () => {
const testHeader = 'Test Header';
const testContent = 'Test Content';

const { getByText, queryByText } = renderWithProvider(
<InfoSectionAccordion header={testHeader}>
<Text>{testContent}</Text>
</InfoSectionAccordion>,
{
state: stakingDepositConfirmationState,
},
);

expect(getByText(testHeader)).toBeDefined();
expect(queryByText(testContent)).toBeNull();

fireEvent(getByText(testHeader), 'onPress');

expect(getByText(testHeader)).toBeDefined();
expect(getByText(testContent)).toBeDefined();

fireEvent(getByText(testHeader), 'onPress');

expect(getByText(testHeader)).toBeDefined();
expect(queryByText(testContent)).toBeNull();
});

it("opens and closes the accordion when it's expanded by default", async () => {
const testHeader = 'Test Header';
const testContent = 'Test Content';

const { getByText, queryByText } = renderWithProvider(
<InfoSectionAccordion header={testHeader} initiallyExpanded>
<Text>{testContent}</Text>
</InfoSectionAccordion>,
{
state: stakingDepositConfirmationState,
},
);

expect(getByText(testHeader)).toBeDefined();
expect(getByText(testContent)).toBeDefined();

fireEvent(getByText(testHeader), 'onPress');

expect(getByText(testHeader)).toBeDefined();
expect(queryByText(testContent)).toBeNull();

fireEvent(getByText(testHeader), 'onPress');

expect(getByText(testHeader)).toBeDefined();
expect(getByText(testContent)).toBeDefined();
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
import React, { useCallback, useState } from 'react';
import {
LayoutAnimation,
Platform,
StyleProp,
TouchableOpacity,
UIManager,
View,
ViewStyle
} from 'react-native';
import Animated, {
interpolate,
useAnimatedStyle,
useSharedValue,
withTiming,
} from 'react-native-reanimated';
import Icon, {
IconName,
IconSize,
} from '../../../../../../component-library/components/Icons/Icon';
import Text from '../../../../../../component-library/components/Texts/Text';
import { useStyles } from '../../../../../../component-library/hooks';
import styleSheet from './InfoSectionAccordion.styles';

if (Platform.OS === 'android') {
if (UIManager.setLayoutAnimationEnabledExperimental) {
UIManager.setLayoutAnimationEnabledExperimental(true);
}
}

interface InfoRowAccordionProps {
/**
* Header component or title string
*/
header: React.ReactNode | string;
/**
* Content to be shown when accordion is expanded
*/
children: React.ReactNode;
/**
* Initial expanded state
*/
initiallyExpanded?: boolean;
/**
* Optional styles for the container
*/
style?: StyleProp<ViewStyle>;
/**
* Optional styles for the header container
*/
headerStyle?: StyleProp<ViewStyle>;
/**
* Optional styles for the content container
*/
contentStyle?: StyleProp<ViewStyle>;
/**
* Optional callback when accordion state changes
*/
onStateChange?: (isExpanded: boolean) => void;
/**
* Test ID for component
*/
testID?: string;
}

const ANIMATION_DURATION_MS = 300;

const InfoRowAccordion: React.FC<InfoRowAccordionProps> = ({
header,
children,
initiallyExpanded = false,
style,
headerStyle,
contentStyle,
onStateChange,
testID,
}) => {
const { styles } = useStyles(styleSheet, {});
const [isExpanded, setIsExpanded] = useState(initiallyExpanded);
const rotationValue = useSharedValue(initiallyExpanded ? 1 : 0);

const toggleAccordion = useCallback(() => {
LayoutAnimation.configureNext(
LayoutAnimation.create(
ANIMATION_DURATION_MS,
LayoutAnimation.Types.easeInEaseOut,
LayoutAnimation.Properties.opacity,
),
);

const newExpandedState = !isExpanded;
setIsExpanded(newExpandedState);
rotationValue.value = withTiming(newExpandedState ? 1 : 0, {
duration: ANIMATION_DURATION_MS,
});
onStateChange?.(newExpandedState);
}, [isExpanded, onStateChange, rotationValue]);

const arrowStyle = useAnimatedStyle(() => {
const rotation = interpolate(rotationValue.value, [0, 1], [0, 180]);
return {
transform: [{ rotate: `${rotation}deg` }],
};
});

return (
<View style={[styles.container, style]} testID={testID}>
<TouchableOpacity
style={[styles.header, headerStyle]}
onPress={toggleAccordion}
activeOpacity={0.7}
testID={`${testID}-header`}
>
{typeof header === 'string' ? (
<Animated.Text style={styles.headerTitle}><Text style={styles.headerTitle}>{header}</Text></Animated.Text>
) : (
header
)}
<Animated.View style={[styles.iconContainer, arrowStyle]}>
<Icon
name={IconName.ArrowDown}
size={IconSize.Sm}
color={styles.icon.color}
testID={`${testID}-arrow`}
/>
</Animated.View>
</TouchableOpacity>
{isExpanded && <View style={[styles.content, contentStyle]}>{children}</View>}
</View>
);
};

export default InfoRowAccordion;
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { default } from './InfoSectionAccordion';
6 changes: 6 additions & 0 deletions locales/languages/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -3508,6 +3508,8 @@
"unstake_input_banner_description":"On average, it takes less than 3 days for the unstaked ETH to be claimable, but can take up to 11 days.",
"max": "Max",
"staking_from": "Staking from",
"advanced_details": "Advanced details",
"ethereum_mainnet": "Ethereum Mainnet",
"interacting_with": "Interacting with",
"12_hours": "12 hours",
"terms_of_service": "Terms of service",
Expand Down Expand Up @@ -3655,6 +3657,10 @@
"signature": "Review request details before you confirm.",
"signature_siwe": "A site wants you to sign in to prove you own this account."
},
"request_from": "Request from",
"staking_from": "Staking from",
"interacting_with": "Interacting with",
"signing_in_with": "Signing in with",
"message": "Message",
"personal_sign_tooltip": "This site is asking for your signature",
"details": "Details",
Expand Down

0 comments on commit d24fc39

Please sign in to comment.