Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Ipfs implementation #6968

Merged
merged 18 commits into from
Aug 23, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,11 @@ import {
TEST_REMOTE_IMAGE_SOURCE,
} from './AvatarToken.constants';

jest.mock('react-redux', () => ({
...jest.requireActual('react-redux'),
useSelector: jest.fn(),
}));

describe('AvatarToken', () => {
/* eslint-disable-next-line */

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@ import { useStyles } from '../../../../../hooks';
import { AvatarTokenProps } from './AvatarToken.types';
import stylesheet from './AvatarToken.styles';
import { TOKEN_AVATAR_IMAGE_ID } from './AvatarToken.constants';
import { useSelector } from 'react-redux';
import { selectIsIpfsGatewayEnabled } from '../../../../../../selectors/preferencesController';
import { isIPFSUri } from '../../../../../../util/general';

const AvatarToken = ({
size,
Expand All @@ -28,6 +31,7 @@ const AvatarToken = ({
isHaloEnabled,
showFallback,
});
const isIpfsGatewayEnabled = useSelector(selectIsIpfsGatewayEnabled);
sethkfman marked this conversation as resolved.
Show resolved Hide resolved

const textVariant =
size === AvatarSize.Sm || size === AvatarSize.Xs
Expand All @@ -37,9 +41,13 @@ const AvatarToken = ({

const onError = () => setShowFallback(true);

const isIpfsDisabledAndUriIsIpfs = imageSource
? !isIpfsGatewayEnabled && isIPFSUri(imageSource)
: false;

const tokenImage = () => (
<AvatarBase size={size} style={styles.base}>
{showFallback ? (
{showFallback || isIpfsDisabledAndUriIsIpfs ? (
<Text style={styles.label} variant={textVariant}>
{tokenNameFirstLetter}
</Text>
Expand All @@ -55,7 +63,7 @@ const AvatarToken = ({
</AvatarBase>
);

return !isHaloEnabled || showFallback ? (
return !isHaloEnabled || showFallback || isIpfsDisabledAndUriIsIpfs ? (
tokenImage()
) : (
<ImageBackground
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ exports[`ActionView should render correctly 1`] = `
},
]
}
disabled={false}
disabledContainerStyle={
Object {
"opacity": 0.6,
Expand Down
11 changes: 8 additions & 3 deletions app/components/UI/ActionView/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ export default function ActionView({
showConfirmButton,
confirmed,
confirmDisabled,
loading = false,
keyboardShouldPersistTaps = 'never',
style = undefined,
}) {
Expand Down Expand Up @@ -91,12 +92,12 @@ export default function ActionView({
type={confirmButtonMode}
onPress={onConfirmPress}
containerStyle={[styles.button, styles.confirm]}
disabled={confirmed || confirmDisabled}
disabled={confirmed || confirmDisabled || loading}
>
{confirmed ? (
{confirmed || loading ? (
<ActivityIndicator
size="small"
color={colors.primary.inverse}
color={colors.primary.default}
/>
) : (
confirmText
Expand Down Expand Up @@ -174,6 +175,10 @@ ActionView.propTypes = {
* Whether confirm button is shown
*/
showConfirmButton: PropTypes.bool,
/**
* Loading after confirm
*/
loading: PropTypes.bool,
/**
* Determines if the keyboard should stay visible after a tap
*/
Expand Down
17 changes: 13 additions & 4 deletions app/components/UI/AddCustomCollectible/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ const AddCustomCollectible = ({
const [inputWidth, setInputWidth] = useState<string | undefined>(
Device.isAndroid() ? '99%' : undefined,
);
const [loading, setLoading] = useState(false);
const assetTokenIdInput = React.createRef() as any;
const { colors, themeAppearance } = useTheme();
const styles = createStyles(colors);
Expand Down Expand Up @@ -175,8 +176,15 @@ const AddCustomCollectible = ({
};

const addNft = async (): Promise<void> => {
if (!(await validateCustomCollectible())) return;
if (!(await validateCollectibleOwnership())) return;
setLoading(true);
if (!(await validateCustomCollectible())) {
setLoading(false);
return;
}
if (!(await validateCollectibleOwnership())) {
setLoading(false);
return;
}

const { NftController } = Engine.context as any;
NftController.addNft(address, tokenId);
Expand All @@ -185,7 +193,7 @@ const AddCustomCollectible = ({
MetaMetricsEvents.COLLECTIBLE_ADDED,
getAnalyticsParams(),
);

setLoading(false);
navigation.goBack();
};

Expand Down Expand Up @@ -217,7 +225,8 @@ const AddCustomCollectible = ({
confirmText={strings('add_asset.collectibles.add_collectible')}
onCancelPress={cancelAddCollectible}
onConfirmPress={addNft}
confirmDisabled={!address && !tokenId}
confirmDisabled={!address || !tokenId}
loading={loading}
>
<View>
<View style={styles.rowWrapper}>
Expand Down
66 changes: 53 additions & 13 deletions app/components/UI/CollectibleContracts/index.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { useState, useEffect, useCallback } from 'react';
import React, { useState, useEffect, useCallback, useMemo } from 'react';
import PropTypes from 'prop-types';
import {
TouchableOpacity,
Expand Down Expand Up @@ -26,7 +26,7 @@ import { removeFavoriteCollectible } from '../../../actions/collectibles';
import { setNftDetectionDismissed } from '../../../actions/user';
import Text from '../../Base/Text';
import AppConstants from '../../../core/AppConstants';
import { toLowerCaseEquals } from '../../../util/general';
import { isIPFSUri, toLowerCaseEquals } from '../../../util/general';
import { compareTokenIds } from '../../../util/tokens';
import CollectibleDetectionModal from '../CollectibleDetectionModal';
import { useTheme } from '../../../util/theme';
Expand All @@ -37,13 +37,15 @@ import {
selectProviderType,
} from '../../../selectors/networkController';
import {
selectIsIpfsGatewayEnabled,
selectSelectedAddress,
selectUseNftDetection,
} from '../../../selectors/preferencesController';
import {
IMPORT_NFT_BUTTON_ID,
NFT_TAB_CONTAINER_ID,
} from '../../../../wdio/screen-objects/testIDs/Screens/WalletView.testIds';
import Logger from '../../../util/Logger';

const createStyles = (colors) =>
StyleSheet.create({
Expand Down Expand Up @@ -105,6 +107,7 @@ const CollectibleContracts = ({
useNftDetection,
setNftDetectionDismissed,
nftDetectionDismissed,
isIpfsGatewayEnabled,
}) => {
const { colors } = useTheme();
const styles = createStyles(colors);
Expand Down Expand Up @@ -135,16 +138,19 @@ const CollectibleContracts = ({
* @param address - Collectible address.
* @param tokenId - Collectible token ID.
*/
const updateCollectibleMetadata = (collectible) => {
const { NftController } = Engine.context;
const { address, tokenId } = collectible;
NftController.removeNft(address, tokenId);
if (String(tokenId).includes('e+')) {
removeFavoriteCollectible(selectedAddress, chainId, collectible);
} else {
NftController.addNft(address, String(tokenId));
}
};
const updateCollectibleMetadata = useCallback(
async (collectible) => {
const { NftController } = Engine.context;
const { address, tokenId } = collectible;
NftController.removeNft(address, tokenId);
if (String(tokenId).includes('e+')) {
removeFavoriteCollectible(selectedAddress, chainId, collectible);
} else {
await NftController.addNft(address, String(tokenId));
}
},
[chainId, removeFavoriteCollectible, selectedAddress],
);

useEffect(() => {
// TO DO: Move this fix to the controllers layer
Expand All @@ -153,7 +159,36 @@ const CollectibleContracts = ({
updateCollectibleMetadata(collectible);
}
});
});
}, [collectibles, updateCollectibleMetadata]);
const memoizedCollectibles = useMemo(() => collectibles, [collectibles]);

const updateAllUnfetchedIPFSNftsMetadata = useCallback(async () => {
try {
if (isIpfsGatewayEnabled) {
const promises = memoizedCollectibles.map(async (collectible) => {
if (
!collectible.image &&
!collectible.name &&
!collectible.description &&
isIPFSUri(collectible.tokenURI)
) {
await updateCollectibleMetadata(collectible);
}
});

await Promise.all(promises);
}
} catch (error) {
Logger.error(
error,
'error while trying to update metadata of ipfs stored nfts',
);
}
}, [updateCollectibleMetadata, isIpfsGatewayEnabled, memoizedCollectibles]);

useEffect(() => {
updateAllUnfetchedIPFSNftsMetadata();
}, [updateAllUnfetchedIPFSNftsMetadata, isIpfsGatewayEnabled]);

const goToAddCollectible = useCallback(() => {
setIsAddNFTEnabled(false);
Expand Down Expand Up @@ -361,6 +396,10 @@ CollectibleContracts.propTypes = {
* State to manage display of modal
*/
nftDetectionDismissed: PropTypes.bool,
/**
* Boolean to show if NFT detection is enabled
*/
isIpfsGatewayEnabled: PropTypes.bool,
};

const mapStateToProps = (state) => ({
Expand All @@ -372,6 +411,7 @@ const mapStateToProps = (state) => ({
collectibleContracts: collectibleContractsSelector(state),
collectibles: collectiblesSelector(state),
favoriteCollectibles: favoritesCollectiblesSelector(state),
isIpfsGatewayEnabled: selectIsIpfsGatewayEnabled(state),
});

const mapDispatchToProps = (dispatch) => ({
Expand Down
2 changes: 1 addition & 1 deletion app/components/UI/CollectibleDetectionModal/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ const CollectibleDetectionModal = ({ onDismiss, navigation }: Props) => {
navigation.navigate('SettingsView', {
screen: 'SecuritySettings',
params: {
scrollToBottom: true,
scrollToDetectNFTs: true,
},
});
};
Expand Down
4 changes: 4 additions & 0 deletions app/components/UI/CollectibleMedia/Collectible.constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import Device from '../../../util/device';

// eslint-disable-next-line import/prefer-default-export
export const MEDIA_WIDTH_MARGIN = Device.isMediumDevice() ? 32 : 0;
69 changes: 69 additions & 0 deletions app/components/UI/CollectibleMedia/CollectibleMedia.styles.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import { StyleSheet } from 'react-native';
import scaling from '../../../util/scaling';
import Device from '../../../util/device';

import { MEDIA_WIDTH_MARGIN } from './Collectible.constants';

const createStyles = (colors: any) =>
StyleSheet.create({
container(backgroundColor: string) {
return {
flex: 0,
borderRadius: 12,
backgroundColor: `#${backgroundColor}`,
};
},
tinyImage: {
width: 32,
height: 32,
},
smallImage: {
width: 50,
height: 50,
},
bigImage: {
height: 260,
width: 260,
},
cover: {
height: scaling.scale(Device.getDeviceWidth() - MEDIA_WIDTH_MARGIN, {
baseModel: 2,
}),
},
image: {
borderRadius: 12,
},
textContainer: {
alignItems: 'center',
justifyContent: 'flex-start',
backgroundColor: colors.background.alternative,
borderRadius: 8,
},
textWrapper: {
textAlign: 'center',
marginTop: 16,
},
textWrapperIcon: {
textAlign: 'center',
fontSize: 18,
marginTop: 16,
},
mediaPlayer: {
minHeight: 10,
},
imageFallBackTextContainer: StyleSheet.absoluteFillObject,
imageFallBackShowContainer: {
bottom: 32,
},
// eslint-disable-next-line react-native/no-color-literals
imageFallBackText: {
textAlign: 'center',
marginTop: 16,
color: 'black',
},
imageFallBackShowText: {
textAlign: 'center',
},
});

export default createStyles;
Loading