Skip to content

Commit

Permalink
feat: New nft details page (#10277)
Browse files Browse the repository at this point in the history
## **Description**

This PR modifies the design of the NFT details page.
Designs:
https://www.figma.com/design/TfVzSMJA8KwpWX8TTWQ2iO/Asset-list-and-details?node-id=1242-143952&m=dev


## **Related issues**

Fixes:
Related: MetaMask/core#4522
Related: MetaMask/core#4443

## **Manual testing steps**

1. Go to this NFT tab
2. Click and browse through your NFTs
3. You should be able to see basic things like tokenId, contract
address, description. Other fields will be displayed if they exist.
4. Click on the contract address and you should be redirected to
etherscan.
5. Click on the image in the NFT details page and you should see the
image in a new page without any of the details.
6. Try clicking on the navbar and go to "See on opensea"
7. Removing NFT flow and Sending NFT flow should not be affected


## **Screenshots/Recordings**

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

### **Before**

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


https://github.com/user-attachments/assets/97679ba6-c8bb-483d-adb1-f3ebfdfb4337



### **After**

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



https://github.com/user-attachments/assets/024f389a-9feb-4fef-8b20-81fa59f232fd

Also we added a new bottom sheet when token ID is too long



https://github.com/user-attachments/assets/1434b08b-ab1a-4e56-acde-189b303b88e9



## **Pre-merge author checklist**

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

## **Pre-merge reviewer checklist**

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

---------

Co-authored-by: Curtis <[email protected]>
  • Loading branch information
sahar-fehri and cortisiko authored Aug 1, 2024
1 parent 5835f66 commit 0c2befd
Show file tree
Hide file tree
Showing 44 changed files with 4,094 additions and 60 deletions.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
162 changes: 157 additions & 5 deletions app/components/Base/RemoteImage/index.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
import React, { useMemo, useState } from 'react';
import React, { useEffect, useMemo, useState } from 'react';
import PropTypes from 'prop-types';
import { Image, ViewPropTypes, View, StyleSheet } from 'react-native';
import {
Image,
ViewPropTypes,
View,
StyleSheet,
Dimensions,
} from 'react-native';
import FadeIn from 'react-native-fade-in-image';
// eslint-disable-next-line import/default
import resolveAssetSource from 'react-native/Libraries/Image/resolveAssetSource';
Expand All @@ -10,13 +16,44 @@ import ComponentErrorBoundary from '../../UI/ComponentErrorBoundary';
import useIpfsGateway from '../../hooks/useIpfsGateway';
import { getFormattedIpfsUrl } from '@metamask/assets-controllers';
import Identicon from '../../UI/Identicon';
import BadgeWrapper from '../../../component-library/components/Badges/BadgeWrapper';
import Badge, {
BadgeVariant,
} from '../../../component-library/components/Badges/Badge';
import { useSelector } from 'react-redux';
import {
selectChainId,
selectTicker,
} from '../../../selectors/networkController';
import {
getTestNetImageByChainId,
isLineaMainnet,
isMainNet,
isTestNet,
} from '../../../util/networks';
import images from 'images/image-icons';
import { selectNetworkName } from '../../../selectors/networkInfos';

import { BadgeAnchorElementShape } from '../../../component-library/components/Badges/BadgeWrapper/BadgeWrapper.types';
import useSvgUriViewBox from '../../hooks/useSvgUriViewBox';
import { AvatarSize } from '../../../component-library/components/Avatars/Avatar';

const createStyles = () =>
StyleSheet.create({
svgContainer: {
overflow: 'hidden',
},
badgeWrapper: {
flex: 1,
},
imageStyle: {
width: '100%',
height: '100%',
borderRadius: 8,
},
detailedImageStyle: {
borderRadius: 8,
},
});

const RemoteImage = (props) => {
Expand All @@ -26,6 +63,9 @@ const RemoteImage = (props) => {
const isImageUrl = isUrl(props?.source?.uri);
const ipfsGateway = useIpfsGateway();
const styles = createStyles();
const chainId = useSelector(selectChainId);
const ticker = useSelector(selectTicker);
const networkName = useSelector(selectNetworkName);
const resolvedIpfsUrl = useMemo(() => {
try {
const url = new URL(props.source.uri);
Expand All @@ -41,6 +81,51 @@ const RemoteImage = (props) => {

const onError = ({ nativeEvent: { error } }) => setError(error);

const [dimensions, setDimensions] = useState(null);

useEffect(() => {
const calculateImageDimensions = (imageWidth, imageHeight) => {
const deviceWidth = Dimensions.get('window').width;
const maxWidth = deviceWidth - 32;
const maxHeight = 0.75 * maxWidth;

if (imageWidth > imageHeight) {
// Horizontal image
const width = maxWidth;
const height = (imageHeight / imageWidth) * maxWidth;
return { width, height };
} else if (imageHeight > imageWidth) {
// Vertical image
const height = maxHeight;
const width = (imageWidth / imageHeight) * maxHeight;
return { width, height };
}
// Square image
return { width: maxHeight, height: maxHeight };
};

Image.getSize(
uri,
(width, height) => {
const { width: calculatedWidth, height: calculatedHeight } =
calculateImageDimensions(width, height);
setDimensions({ width: calculatedWidth, height: calculatedHeight });
},
() => {
console.error('Failed to get image dimensions');
},
);
}, [uri]);

const NetworkBadgeSource = () => {
if (isTestNet(chainId)) return getTestNetImageByChainId(chainId);

if (isMainNet(chainId)) return images.ETHEREUM;

if (isLineaMainnet(chainId)) return images['LINEA-MAINNET'];

return ticker ? images[ticker] : undefined;
};
const isSVG =
source &&
source.uri &&
Expand Down Expand Up @@ -83,12 +168,75 @@ const RemoteImage = (props) => {
}

if (props.fadeIn) {
const { style, ...restProps } = props;
const badge = {
top: -4,
right: -4,
};
return (
<FadeIn placeholderStyle={props.placeholderStyle}>
<Image {...props} source={{ uri }} onError={onError} />
</FadeIn>
<>
{props.isTokenImage ? (
<FadeIn placeholderStyle={props.placeholderStyle}>
<View>
{props.isFullRatio && dimensions ? (
<BadgeWrapper
badgePosition={badge}
anchorElementShape={BadgeAnchorElementShape.Rectangular}
badgeElement={
<Badge
variant={BadgeVariant.Network}
imageSource={NetworkBadgeSource()}
name={networkName}
isScaled={false}
size={AvatarSize.Md}
/>
}
>
<Image
source={{ uri }}
style={{
width: dimensions.width,
height: dimensions.height,
...styles.detailedImageStyle,
}}
/>
</BadgeWrapper>
) : (
<BadgeWrapper
badgePosition={badge}
anchorElementShape={BadgeAnchorElementShape.Rectangular}
badgeElement={
<Badge
variant={BadgeVariant.Network}
imageSource={NetworkBadgeSource()}
name={networkName}
isScaled={false}
size={AvatarSize.Xs}
/>
}
>
<View style={style}>
<Image
style={styles.imageStyle}
{...restProps}
source={{ uri }}
onError={onError}
resizeMode={'cover'}
/>
</View>
</BadgeWrapper>
)}
</View>
</FadeIn>
) : (
<FadeIn placeholderStyle={props.placeholderStyle}>
<Image {...props} source={{ uri }} onError={onError} />
</FadeIn>
)}
</>
);
}

return <Image {...props} source={{ uri }} onError={onError} />;
};

Expand Down Expand Up @@ -121,6 +269,10 @@ RemoteImage.propTypes = {
* Token address
*/
address: PropTypes.string,

isTokenImage: PropTypes.bool,

isFullRatio: PropTypes.bool,
};

export default RemoteImage;
8 changes: 8 additions & 0 deletions app/components/Nav/App/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,8 @@ import BasicFunctionalityModal from '../../UI/BasicFunctionality/BasicFunctional
import SmartTransactionsOptInModal from '../../Views/SmartTransactionsOptInModal/SmartTranactionsOptInModal';
import ProfileSyncingModal from '../../UI/ProfileSyncing/ProfileSyncingModal/ProfileSyncingModal';
import NFTAutoDetectionModal from '../../../../app/components/Views/NFTAutoDetectionModal/NFTAutoDetectionModal';
import NftOptions from '../../../components/Views/NftOptions';
import ShowTokenIdSheet from '../../../components/Views/ShowTokenIdSheet';
import OriginSpamModal from '../../Views/OriginSpamModal/OriginSpamModal';
///: BEGIN:ONLY_INCLUDE_IF(preinstalled-snaps,external-snaps)
import { SnapsExecutionWebView } from '../../../lib/snaps';
Expand Down Expand Up @@ -682,6 +684,7 @@ const App = ({ userLoggedIn }) => {
/>
<Stack.Screen name={'DetectedTokens'} component={DetectedTokensFlow} />
<Stack.Screen name={'AssetOptions'} component={AssetOptions} />
<Stack.Screen name={'NftOptions'} component={NftOptions} />
<Stack.Screen
name={Routes.MODAL.UPDATE_NEEDED}
component={UpdateNeeded}
Expand Down Expand Up @@ -715,6 +718,11 @@ const App = ({ userLoggedIn }) => {
name={Routes.MODAL.NFT_AUTO_DETECTION_MODAL}
component={NFTAutoDetectionModal}
/>
<Stack.Screen
name={Routes.SHEET.SHOW_TOKEN_ID}
component={ShowTokenIdSheet}
/>

<Stack.Screen
name={Routes.SHEET.ORIGIN_SPAM_MODAL}
component={OriginSpamModal}
Expand Down
34 changes: 34 additions & 0 deletions app/components/Nav/Main/MainNavigator.js
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,9 @@ import { AesCryptoTestForm } from '../../Views/AesCryptoTestForm';
import { isTest } from '../../../util/test/utils';
import { selectPermissionControllerState } from '../../../selectors/snaps/permissionController';

import NftDetails from '../../Views/NftDetails';
import NftDetailsFullImage from '../../Views/NftDetails/NFtDetailsFullImage';

const Stack = createStackNavigator();
const Tab = createBottomTabNavigator();

Expand Down Expand Up @@ -541,6 +544,32 @@ const SendView = () => (
</Stack.Navigator>
);

/* eslint-disable react/prop-types */
const NftDetailsModeView = (props) => (
<Stack.Navigator>
<Stack.Screen
name=" " // No name here because this title will be displayed in the header of the page
component={NftDetails}
initialParams={{
collectible: props.route.params?.collectible,
}}
/>
</Stack.Navigator>
);

/* eslint-disable react/prop-types */
const NftDetailsFullImageModeView = (props) => (
<Stack.Navigator>
<Stack.Screen
name=" " // No name here because this title will be displayed in the header of the page
component={NftDetailsFullImage}
initialParams={{
collectible: props.route.params?.collectible,
}}
/>
</Stack.Navigator>
);

const SendFlowView = () => (
<Stack.Navigator>
<Stack.Screen
Expand Down Expand Up @@ -728,6 +757,11 @@ const MainNavigator = () => (
name={Routes.NOTIFICATIONS.VIEW}
component={NotificationsModeView}
/>
<Stack.Screen name="NftDetails" component={NftDetailsModeView} />
<Stack.Screen
name="NftDetailsFullImage"
component={NftDetailsFullImageModeView}
/>
<Stack.Screen name={Routes.QR_SCANNER} component={QrScanner} />
<Stack.Screen name="PaymentRequestView" component={PaymentRequestView} />
<Stack.Screen name={Routes.RAMP.BUY}>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import React, { useState } from 'react';
import { View } from 'react-native';
import { TextStyle, View } from 'react-native';
import ButtonLink from '../../../../component-library/components/Buttons/Button/variants/ButtonLink';
import { useStyles } from '../../../../component-library/hooks';
import Text, {
Expand All @@ -13,12 +13,14 @@ interface ContentDisplayProps {
content: string;
numberOfLines?: number;
disclaimer?: string;
textStyle?: TextStyle;
}

const ContentDisplay = ({
content,
numberOfLines = 3,
disclaimer,
textStyle,
}: ContentDisplayProps) => {
const { styles } = useStyles(styleSheet, {});

Expand All @@ -33,6 +35,7 @@ const ContentDisplay = ({
<Text
numberOfLines={isExpanded ? undefined : numberOfLines}
color={TextColor.Alternative}
style={[textStyle]}
>
{content}
</Text>
Expand Down
1 change: 1 addition & 0 deletions app/components/UI/CollectibleContractElement/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,7 @@ function CollectibleContractElement({
style={styles.collectibleIcon}
collectible={{ ...collectible, name }}
onPressColectible={onPress}
isTokenImage
/>
</View>
</TouchableOpacity>
Expand Down
9 changes: 7 additions & 2 deletions app/components/UI/CollectibleContracts/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ import { selectSelectedInternalAccountChecksummedAddress } from '../../../select
import { WalletViewSelectorsIDs } from '../../../../e2e/selectors/wallet/WalletView.selectors';
import { useMetrics } from '../../../components/hooks/useMetrics';
import { RefreshTestId, SpinnerTestId } from './constants';
import { debounce } from 'lodash';

const createStyles = (colors) =>
StyleSheet.create({
Expand Down Expand Up @@ -89,6 +90,10 @@ const createStyles = (colors) =>
},
});

const debouncedNavigation = debounce((navigation, collectible) => {
navigation.navigate('NftDetails', { collectible });
}, 200);

/**
* View that renders a list of CollectibleContract
* ERC-721 and ERC-1155
Expand Down Expand Up @@ -120,8 +125,8 @@ const CollectibleContracts = ({
networkType === MAINNET && !useNftDetection;

const onItemPress = useCallback(
(collectible, contractName) => {
navigation.navigate('CollectiblesDetails', { collectible, contractName });
(collectible) => {
debouncedNavigation(navigation, collectible);
},
[navigation],
);
Expand Down
Loading

0 comments on commit 0c2befd

Please sign in to comment.