From b8c00ec0a1ede182a5bb6fdd82b3486949036c1d Mon Sep 17 00:00:00 2001 From: Tushar Pardhe Date: Fri, 17 Mar 2023 11:40:00 +0000 Subject: [PATCH 01/47] Initial commit for NFTs --- .vscode/settings.json | 2 +- .../constants/src/payload/payload.types.ts | 4 + packages/constants/src/xrpl/nft.types.ts | 17 +++++ packages/extension/package.json | 3 + .../components/molecules/NftCard/NftCard.tsx | 73 +++++++++++++++++++ .../src/components/molecules/NftCard/index.ts | 1 + .../organisms/NftListing/NftListing.tsx | 39 ++++++++++ .../components/organisms/NftListing/index.ts | 1 + .../src/components/pages/Nfts/Nfts.tsx | 51 +++++++++++++ .../src/components/pages/Nfts/index.ts | 1 + .../extension/src/components/pages/index.ts | 1 + .../extension/src/constants/navigation.tsx | 8 +- packages/extension/src/constants/paths.ts | 1 + .../contexts/LedgerContext/LedgerContext.tsx | 37 +++++++++- packages/extension/src/mocks/ledgerContext.ts | 3 +- yarn.lock | 33 +++++++++ 16 files changed, 270 insertions(+), 5 deletions(-) create mode 100644 packages/extension/src/components/molecules/NftCard/NftCard.tsx create mode 100644 packages/extension/src/components/molecules/NftCard/index.ts create mode 100644 packages/extension/src/components/organisms/NftListing/NftListing.tsx create mode 100644 packages/extension/src/components/organisms/NftListing/index.ts create mode 100644 packages/extension/src/components/pages/Nfts/Nfts.tsx create mode 100644 packages/extension/src/components/pages/Nfts/index.ts diff --git a/.vscode/settings.json b/.vscode/settings.json index 958f79ab6..d3e35c6bd 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -15,6 +15,6 @@ }, "cSpell.words": ["gemwallet"], "[typescriptreact]": { - "editor.defaultFormatter": "esbenp.prettier-vscode" + "editor.defaultFormatter": "vscode.typescript-language-features" } } diff --git a/packages/constants/src/payload/payload.types.ts b/packages/constants/src/payload/payload.types.ts index 9ee9c6349..4341d7808 100644 --- a/packages/constants/src/payload/payload.types.ts +++ b/packages/constants/src/payload/payload.types.ts @@ -240,6 +240,10 @@ export interface SubmitTransactionRequest { transaction: Transaction; } +export interface NftImageRequestPayload { + nft: AccountNFToken; +} + export type RequestPayload = | AcceptNFTOfferRequest | BurnNFTRequest diff --git a/packages/constants/src/xrpl/nft.types.ts b/packages/constants/src/xrpl/nft.types.ts index cc92fdcae..562deb13a 100644 --- a/packages/constants/src/xrpl/nft.types.ts +++ b/packages/constants/src/xrpl/nft.types.ts @@ -6,3 +6,20 @@ export interface AccountNFToken { URI?: string; nft_serial: number; } + +export interface AccountNFTokenResponse { + account_nfts: AccountNFToken[]; + marker: unknown; +} + +export interface NFTData { + schema: string; + nftType: string; + name?: string; + description?: string; + image?: string; + collection?: { + name?: string; + family?: string; + }; +} diff --git a/packages/extension/package.json b/packages/extension/package.json index 259523ff6..f73804fcd 100644 --- a/packages/extension/package.json +++ b/packages/extension/package.json @@ -20,6 +20,8 @@ "react-content-loader": "^6.2.0", "react-dom": "^17.0.2", "react-json-view": "^1.21.3", + "react-infinite-scroll-component": "^6.1.0", + "react-lazy-load-image-component": "^1.5.6", "react-router-dom": "6", "react-scripts": "4.0.3", "ripple-keypairs": "^1.1.5", @@ -41,6 +43,7 @@ "@types/node": "^12.0.0", "@types/react": "^17.0.0", "@types/react-dom": "^17.0.0", + "@types/react-lazy-load-image-component": "^1.5.2", "cypress": "^12.9.0", "jest-canvas-mock": "^2.4.0", "ts-loader": "~8.2.0" diff --git a/packages/extension/src/components/molecules/NftCard/NftCard.tsx b/packages/extension/src/components/molecules/NftCard/NftCard.tsx new file mode 100644 index 000000000..5a85e4569 --- /dev/null +++ b/packages/extension/src/components/molecules/NftCard/NftCard.tsx @@ -0,0 +1,73 @@ +import { FC, useContext, useEffect, useState } from 'react'; + +import { CircularProgress, ListItem, Paper } from '@mui/material'; +import { LazyLoadImage } from 'react-lazy-load-image-component'; + +import { AccountNFToken, NFTData } from '@gemwallet/constants'; + +import { LedgerContext } from '../../../contexts'; + +export interface NftCardProps { + nft: AccountNFToken; +} + +export const NftCard: FC = ({ nft }) => { + const { getNFTData } = useContext(LedgerContext); + const [nftData, setNftData] = useState(null); + const [loading, setLoading] = useState(true); + + useEffect(() => { + const fetchNftImg = async () => { + try { + setLoading(true); + const nftData = await getNFTData({ nft }); + setNftData(nftData); + } catch (error) { + setNftData(null); + } finally { + setLoading(false); + } + }; + fetchNftImg(); + }, [getNFTData, nft]); + + return ( + + + {loading ? ( + + ) : ( + } + effect="blur" + src={nftData?.image} // use normal attributes as props + width={150} + /> + // nft + )} +
{nftData?.name}
+
+ {nftData?.description} +
+ {/* */} +
+
+ ); +}; diff --git a/packages/extension/src/components/molecules/NftCard/index.ts b/packages/extension/src/components/molecules/NftCard/index.ts new file mode 100644 index 000000000..c436a8c68 --- /dev/null +++ b/packages/extension/src/components/molecules/NftCard/index.ts @@ -0,0 +1 @@ +export * from './NftCard'; diff --git a/packages/extension/src/components/organisms/NftListing/NftListing.tsx b/packages/extension/src/components/organisms/NftListing/NftListing.tsx new file mode 100644 index 000000000..978d37407 --- /dev/null +++ b/packages/extension/src/components/organisms/NftListing/NftListing.tsx @@ -0,0 +1,39 @@ +import { FC } from 'react'; + +import { List } from '@mui/material'; +import InfiniteScroll from 'react-infinite-scroll-component'; + +import { AccountNFTokenResponse } from '@gemwallet/constants'; + +import { InformationMessage } from '../../molecules'; +import { NftCard } from '../../molecules/NftCard'; + +export interface NftListingProps extends AccountNFTokenResponse { + onLoadMoreClick: () => void; +} + +export const NftListing: FC = ({ account_nfts, marker, onLoadMoreClick }) => { + if (account_nfts.length === 0) { + return ( + +
There are no NFTs found in this wallet.
+
+ ); + } + + return ( + Loading...} + > + + {account_nfts.map((nft) => ( + + ))} + + + ); +}; diff --git a/packages/extension/src/components/organisms/NftListing/index.ts b/packages/extension/src/components/organisms/NftListing/index.ts new file mode 100644 index 000000000..b4a8c722f --- /dev/null +++ b/packages/extension/src/components/organisms/NftListing/index.ts @@ -0,0 +1 @@ +export * from './NftListing'; diff --git a/packages/extension/src/components/pages/Nfts/Nfts.tsx b/packages/extension/src/components/pages/Nfts/Nfts.tsx new file mode 100644 index 000000000..2c7778059 --- /dev/null +++ b/packages/extension/src/components/pages/Nfts/Nfts.tsx @@ -0,0 +1,51 @@ +import { FC, useContext, useEffect, useState } from 'react'; + +import { AccountNFTokenResponse } from '@gemwallet/constants'; + +import { LedgerContext } from '../../../contexts'; +import { NftListing } from '../../organisms/NftListing'; +import { PageWithHeader } from '../../templates'; + +const initalState = { + account_nfts: [], + marker: null +}; + +export const Nfts: FC = () => { + const { getNFTs } = useContext(LedgerContext); + + const [nfts, setNfts] = useState(initalState); + + const fetchNfts = async () => { + try { + const payload = { + limit: 20, + marker: nfts.marker + }; + + if (!nfts.marker) { + delete payload.marker; + } + + const response = await getNFTs(payload); + + setNfts({ + marker: response.marker, + account_nfts: nfts.account_nfts.concat(response.account_nfts) + }); + } catch (error) { + setNfts(initalState); + } + }; + + useEffect(() => { + fetchNfts(); + // eslint-disable-next-line react-hooks/exhaustive-deps -- we only want to fetch once + }, []); + + return ( + + + + ); +}; diff --git a/packages/extension/src/components/pages/Nfts/index.ts b/packages/extension/src/components/pages/Nfts/index.ts new file mode 100644 index 000000000..27ba9af8a --- /dev/null +++ b/packages/extension/src/components/pages/Nfts/index.ts @@ -0,0 +1 @@ +export * from './Nfts'; diff --git a/packages/extension/src/components/pages/index.ts b/packages/extension/src/components/pages/index.ts index ce8b68d8f..c4764d208 100644 --- a/packages/extension/src/components/pages/index.ts +++ b/packages/extension/src/components/pages/index.ts @@ -31,3 +31,4 @@ export * from './SubmitTransaction'; export * from './Transaction'; export * from './TrustedApps'; export * from './Welcome'; +export * from './Nfts'; diff --git a/packages/extension/src/constants/navigation.tsx b/packages/extension/src/constants/navigation.tsx index 9fba01c14..f03f02d83 100644 --- a/packages/extension/src/constants/navigation.tsx +++ b/packages/extension/src/constants/navigation.tsx @@ -1,8 +1,9 @@ import AccountBalanceWalletIcon from '@mui/icons-material/AccountBalanceWallet'; import HistoryIcon from '@mui/icons-material/History'; +import PhotoCameraBackIcon from '@mui/icons-material/PhotoCameraBack'; import SettingsIcon from '@mui/icons-material/Settings'; -import { HISTORY_PATH, HOME_PATH, SETTINGS_PATH } from './paths'; +import { HISTORY_PATH, HOME_PATH, NFTS_PATH, SETTINGS_PATH } from './paths'; export const navigation = [ { @@ -15,6 +16,11 @@ export const navigation = [ pathname: HISTORY_PATH, icon: }, + { + label: 'Nfts', + pathname: NFTS_PATH, + icon: + }, { label: 'Settings', pathname: SETTINGS_PATH, diff --git a/packages/extension/src/constants/paths.ts b/packages/extension/src/constants/paths.ts index 5456d4969..0a5d0b381 100644 --- a/packages/extension/src/constants/paths.ts +++ b/packages/extension/src/constants/paths.ts @@ -31,3 +31,4 @@ export const SHARE_PUBLIC_KEY_PATH = '/share-public-key'; export const TRANSACTION_PATH = '/transaction'; export const TRUSTED_APPS_PATH = '/trusted-apps'; export const WELCOME_PATH = '/welcome'; +export const NFTS_PATH = '/nfts'; diff --git a/packages/extension/src/contexts/LedgerContext/LedgerContext.tsx b/packages/extension/src/contexts/LedgerContext/LedgerContext.tsx index adc921a5e..3e6e8ea8f 100644 --- a/packages/extension/src/contexts/LedgerContext/LedgerContext.tsx +++ b/packages/extension/src/contexts/LedgerContext/LedgerContext.tsx @@ -4,6 +4,7 @@ import * as Sentry from '@sentry/react'; import { sign } from 'ripple-keypairs'; import { AccountSet, + convertHexToString, NFTokenAcceptOffer, NFTokenBurn, NFTokenCancelOffer, @@ -32,6 +33,8 @@ import { CreateOfferRequest, GetNFTRequest, MintNFTRequest, + NFTData, + NftImageRequestPayload, SendPaymentRequest, SetAccountRequest, SetTrustlineRequest, @@ -111,6 +114,7 @@ export interface LedgerContextType { cancelOffer: (payload: CancelOfferRequest) => Promise; submitTransaction: (payload: SubmitTransactionRequest) => Promise; getAccountInfo: () => Promise; + getNFTData: (payload: NftImageRequestPayload) => Promise; } const LedgerContext = createContext({ @@ -139,7 +143,8 @@ const LedgerContext = createContext({ createOffer: () => new Promise(() => {}), cancelOffer: () => new Promise(() => {}), submitTransaction: () => new Promise(() => {}), - getAccountInfo: () => new Promise(() => {}) + getAccountInfo: () => new Promise(() => {}), + getNFTData: () => new Promise(() => {}) }); const LedgerProvider: FC = ({ children }) => { @@ -795,6 +800,33 @@ const LedgerProvider: FC = ({ children }) => { ...(payload.txnSignature && { TxnSignature: payload.txnSignature }) }); + const getNFTData = useCallback(async ({ nft }: NftImageRequestPayload) => { + try { + const { URI } = nft; + let url = URI ? await convertHexToString(String(URI)) : ''; + + if (url.length === 0) { + throw new Error('URI is empty'); + } + + url = url.replace('ipfs://', 'https://ipfs.io/ipfs/'); + const nftData = await fetch(url) + .then((res) => res.json()) + .catch(() => ({ + name: '-', + description: '-', + image: null + })); + nftData.image = nftData.image + ? nftData.image.replace('ipfs://', 'https://ipfs.io/ipfs/') + : url.replace('.json', '.png'); + return nftData; + } catch (e) { + Sentry.captureException(e); + throw e; + } + }, []); + const value: LedgerContextType = { sendPayment, setTrustline, @@ -812,7 +844,8 @@ const LedgerProvider: FC = ({ children }) => { createOffer, cancelOffer, submitTransaction, - getAccountInfo + getAccountInfo, + getNFTData }; return {children}; diff --git a/packages/extension/src/mocks/ledgerContext.ts b/packages/extension/src/mocks/ledgerContext.ts index fb35f52ce..8f5934546 100644 --- a/packages/extension/src/mocks/ledgerContext.ts +++ b/packages/extension/src/mocks/ledgerContext.ts @@ -17,5 +17,6 @@ export const valueLedgerContext: LedgerContextType = { createOffer: jest.fn(), cancelOffer: jest.fn(), submitTransaction: jest.fn(), - getAccountInfo: jest.fn() + getAccountInfo: jest.fn(), + getNFTData: jest.fn() }; diff --git a/yarn.lock b/yarn.lock index 000ab4e66..fdb720853 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2436,6 +2436,14 @@ dependencies: "@types/react" "*" +"@types/react-lazy-load-image-component@^1.5.2": + version "1.5.2" + resolved "https://registry.yarnpkg.com/@types/react-lazy-load-image-component/-/react-lazy-load-image-component-1.5.2.tgz#b87e814b6b91853b802f04465364ff1e913dce6a" + integrity sha512-4NLJsMJVrMv18FuMIkUUBVj/PH9A+BvLKrZC75EWiEFn1IsMrZHgL1tVKw5QBfoa0Qjz6SkWIzEvwcyZ8PgnIg== + dependencies: + "@types/react" "*" + csstype "^3.0.2" + "@types/react-transition-group@^4.4.5": version "4.4.5" resolved "https://registry.yarnpkg.com/@types/react-transition-group/-/react-transition-group-4.4.5.tgz#aae20dcf773c5aa275d5b9f7cdbca638abc5e416" @@ -8793,6 +8801,11 @@ lodash.templatesettings@^4.0.0: dependencies: lodash._reinterpolate "^3.0.0" +lodash.throttle@^4.1.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/lodash.throttle/-/lodash.throttle-4.1.1.tgz#c23e91b710242ac70c37f1e1cda9274cc39bf2f4" + integrity sha512-wIkUCfVKpVsWo3JSZlc+8MB5it+2AN5W8J7YVMST30UrvcQNZ1Okbj+rbVniijTWE6FGYy4XJq/rHkas8qJMLQ== + lodash.truncate@^4.4.2: version "4.4.2" resolved "https://registry.yarnpkg.com/lodash.truncate/-/lodash.truncate-4.4.2.tgz#5a350da0b1113b837ecfffd5812cbe58d6eae193" @@ -11028,6 +11041,13 @@ react-error-overlay@^6.0.9: resolved "https://registry.yarnpkg.com/react-error-overlay/-/react-error-overlay-6.0.10.tgz#0fe26db4fa85d9dbb8624729580e90e7159a59a6" integrity sha512-mKR90fX7Pm5seCOfz8q9F+66VCc1PGsWSBxKbITjfKVQHMNF2zudxHnMdJiB1fRCb+XsbQV9sO9DCkgsMQgBIA== +react-infinite-scroll-component@^6.1.0: + version "6.1.0" + resolved "https://registry.yarnpkg.com/react-infinite-scroll-component/-/react-infinite-scroll-component-6.1.0.tgz#7e511e7aa0f728ac3e51f64a38a6079ac522407f" + integrity sha512-SQu5nCqy8DxQWpnUVLx7V7b7LcA37aM7tvoWjTLZp1dk6EJibM5/4EJKzOnl07/BsM1Y40sKLuqjCwwH/xV0TQ== + dependencies: + throttle-debounce "^2.1.0" + react-is@^16.13.1, react-is@^16.7.0: version "16.13.1" resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4" @@ -11058,6 +11078,14 @@ react-lifecycles-compat@^3.0.4: resolved "https://registry.yarnpkg.com/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz#4f1a273afdfc8f3488a8c516bfda78f872352362" integrity sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA== +react-lazy-load-image-component@^1.5.6: + version "1.5.6" + resolved "https://registry.yarnpkg.com/react-lazy-load-image-component/-/react-lazy-load-image-component-1.5.6.tgz#a4b84257be21b1825680b4e158d167c08aeda5ff" + integrity sha512-M0jeJtOlTHgThOfgYM9krSqYbR6ShxROy/KVankwbw9/amPKG1t5GSGN1sei6Cyu8+QJVuyAUvQ+LFtCVTTlKw== + dependencies: + lodash.debounce "^4.0.8" + lodash.throttle "^4.1.1" + react-refresh@^0.8.3: version "0.8.3" resolved "https://registry.yarnpkg.com/react-refresh/-/react-refresh-0.8.3.tgz#721d4657672d400c5e3c75d063c4a85fb2d5d68f" @@ -12740,6 +12768,11 @@ throat@^5.0.0: resolved "https://registry.yarnpkg.com/throat/-/throat-5.0.0.tgz#c5199235803aad18754a667d659b5e72ce16764b" integrity sha512-fcwX4mndzpLQKBS1DVYhGAcYaYt7vsHNIvQV+WXMvnow5cgjPphq5CaayLaGsjRdSCKZFNGt7/GYAuXaNOiYCA== +throttle-debounce@^2.1.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/throttle-debounce/-/throttle-debounce-2.3.0.tgz#fd31865e66502071e411817e241465b3e9c372e2" + integrity sha512-H7oLPV0P7+jgvrk+6mwwwBDmxTaxnu9HMXmloNLXwnNO0ZxZ31Orah2n8lU1eMPvsaowP2CX+USCgyovXfdOFQ== + throttleit@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/throttleit/-/throttleit-1.0.0.tgz#9e785836daf46743145a5984b6268d828528ac6c" From 8e63ed321286ddf59a8f69137ca2910bd15e19c5 Mon Sep 17 00:00:00 2001 From: Tushar Pardhe Date: Fri, 14 Apr 2023 13:48:10 +0100 Subject: [PATCH 02/47] Added a comment --- yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/yarn.lock b/yarn.lock index fdb720853..040cd5c63 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4182,9 +4182,9 @@ caniuse-api@^3.0.0: lodash.uniq "^4.5.0" caniuse-lite@^1.0.0, caniuse-lite@^1.0.30000981, caniuse-lite@^1.0.30001109, caniuse-lite@^1.0.30001125, caniuse-lite@^1.0.30001286: - version "1.0.30001299" - resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001299.tgz#d753bf6444ed401eb503cbbe17aa3e1451b5a68c" - integrity sha512-iujN4+x7QzqA2NCSrS5VUy+4gLmRd4xv6vbBBsmfVqTx8bLAD8097euLqQgKxSVLvxjSDcvF1T/i9ocgnUFexw== + version "1.0.30001478" + resolved "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001478.tgz" + integrity sha512-gMhDyXGItTHipJj2ApIvR+iVB5hd0KP3svMWWXDvZOmjzJJassGLMfxRkQCSYgGd2gtdL/ReeiyvMSFD1Ss6Mw== capture-exit@^2.0.0: version "2.0.0" From 3bcc9cac9f485bf6fe6d2c1e8a85bfde66807ade Mon Sep 17 00:00:00 2001 From: Tushar Pardhe Date: Tue, 18 Apr 2023 18:21:38 +0100 Subject: [PATCH 03/47] Added view more button --- .../components/molecules/NftCard/NftCard.tsx | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/packages/extension/src/components/molecules/NftCard/NftCard.tsx b/packages/extension/src/components/molecules/NftCard/NftCard.tsx index 5a85e4569..5ec50c506 100644 --- a/packages/extension/src/components/molecules/NftCard/NftCard.tsx +++ b/packages/extension/src/components/molecules/NftCard/NftCard.tsx @@ -1,6 +1,7 @@ import { FC, useContext, useEffect, useState } from 'react'; -import { CircularProgress, ListItem, Paper } from '@mui/material'; +import { OpenInNewOutlined } from '@mui/icons-material'; +import { Button, CircularProgress, ListItem, Paper } from '@mui/material'; import { LazyLoadImage } from 'react-lazy-load-image-component'; import { AccountNFToken, NFTData } from '@gemwallet/constants'; @@ -31,6 +32,10 @@ export const NftCard: FC = ({ nft }) => { fetchNftImg(); }, [getNFTData, nft]); + const handleViewNftClick = () => { + window.open('someurl', '_blank'); // TODO: Add redirection url (Potential collaboration with NFT marketplace)? + }; + return ( = ({ nft }) => { style={{ borderRadius: '4px', boxShadow: '4px 4px 0px black' }} beforeLoad={() => } effect="blur" - src={nftData?.image} // use normal attributes as props + src={nftData?.image} width={150} /> - // nft )}
{nftData?.name}
{nftData?.description}
- {/* */} +
); From 6707aa4e72d7a8b8992a634bd4174749a4a0ae53 Mon Sep 17 00:00:00 2001 From: Tushar Pardhe Date: Wed, 19 Apr 2023 23:33:35 +0100 Subject: [PATCH 04/47] Added test cases --- .../molecules/NftCard/NftCard.test.tsx | 90 +++++++++++++++++++ .../components/molecules/NftCard/NftCard.tsx | 19 ++-- 2 files changed, 104 insertions(+), 5 deletions(-) create mode 100644 packages/extension/src/components/molecules/NftCard/NftCard.test.tsx diff --git a/packages/extension/src/components/molecules/NftCard/NftCard.test.tsx b/packages/extension/src/components/molecules/NftCard/NftCard.test.tsx new file mode 100644 index 000000000..2acc6b4e9 --- /dev/null +++ b/packages/extension/src/components/molecules/NftCard/NftCard.test.tsx @@ -0,0 +1,90 @@ +import { render, screen, waitFor, fireEvent, act } from '@testing-library/react'; + +import { LedgerContext, LedgerContextType } from '../../../contexts'; +import { NftCard, NftCardProps } from './NftCard'; + +const mockNft = { + Flags: 11, + Issuer: 'rDGh681kc6V1GKkQB378XhiM1tzkYrnFwQ', + NFTokenID: '000B0000867AD7165A812436FBFA175555413C26162BCF380000099A00000000', + NFTokenTaxon: 1, + URI: '697066733A2F2F6261667962656965356A626736336A6164676979736337796B6B6A64737170777732746A6D786D7935377077716F6A697666356562366B6D6D79612F312E6A736F6E', + nft_serial: 0 +}; + +const mockNFTData = { + schema: 'ipfs://QmNpi8rcXEkohca8iXu7zysKKSJYqCvBJn3xJwga8jXqWU', + nftType: 'art.v0', + name: "Ekiserrepe's Oniric Lo-Fi Rooms Vol.1 NFT #1", + description: "Room #1 of Ekiserrepe's Oniric Lo-Fi Rooms Vol.1", + image: 'ipfs://bafybeie6pmuddco552t4u7oc7anryqohuj6vl42ngct6ve3q4bjet5piam/1.png', + collection: { + name: "Ekiserrepe's Oniric Lo-Fi Rooms Vol.1", + family: 'Oniric Lo-Fi Rooms' + } +}; + +const mockGetNFTData = jest.fn(async () => mockNFTData); + +const mockContext: LedgerContextType = { + getNFTData: mockGetNFTData, + sendPayment: jest.fn(), + addTrustline: jest.fn(), + signMessage: jest.fn(), + estimateNetworkFees: jest.fn(), + getNFTs: jest.fn(), + getTransactions: jest.fn() +}; + +const renderNftCard = (props: NftCardProps) => { + act(() => { + render( + + + + ); + }); +}; + +describe('NftCard', () => { + afterEach(() => { + jest.clearAllMocks(); + }); + + test('renders NftCard component correctly', async () => { + renderNftCard({ nft: mockNft }); + + await waitFor(() => expect(mockGetNFTData).toHaveBeenCalled()); + expect(screen.getByTestId('OpenInNewOutlinedIcon')).toBeInTheDocument(); + }); + + test('displays CircularProgress while fetching data', () => { + renderNftCard({ nft: mockNft }); + expect(screen.getByTestId('progressbar')).toBeInTheDocument(); + }); + + test('handles error when fetching NFT data', async () => { + // Temporarily change the behavior of getNFTData to throw an error + mockGetNFTData.mockImplementationOnce(() => { + throw new Error('Failed to fetch NFT data'); + }); + + renderNftCard({ nft: mockNft }); + + await waitFor(() => expect(mockGetNFTData).toHaveBeenCalled()); + + // Check that the NFT data is not displayed when an error occurs + expect(screen.queryByTestId('nft_name')).not.toBeInTheDocument(); + }); + + test('button redirects to the correct URL', async () => { + const windowOpenSpy = jest.spyOn(window, 'open').mockImplementation(); + + renderNftCard({ nft: mockNft }); + await waitFor(() => expect(mockGetNFTData).toHaveBeenCalled()); + + fireEvent.click(screen.getByRole('button')); + + expect(windowOpenSpy).toHaveBeenCalledWith('someurl', '_blank'); + }); +}); diff --git a/packages/extension/src/components/molecules/NftCard/NftCard.tsx b/packages/extension/src/components/molecules/NftCard/NftCard.tsx index 5ec50c506..984f0c1e5 100644 --- a/packages/extension/src/components/molecules/NftCard/NftCard.tsx +++ b/packages/extension/src/components/molecules/NftCard/NftCard.tsx @@ -54,7 +54,7 @@ export const NftCard: FC = ({ nft }) => { }} > {loading ? ( - + ) : ( = ({ nft }) => { width={150} /> )} -
{nftData?.name}
-
- {nftData?.description} -
+ {nftData && ( + <> +
+ {nftData.name} +
+
+ {nftData.description} +
+ + )} diff --git a/packages/extension/src/components/organisms/NFTListing/NFTListing.tsx b/packages/extension/src/components/organisms/NFTListing/NFTListing.tsx index 8815cd549..ce6f63c17 100644 --- a/packages/extension/src/components/organisms/NFTListing/NFTListing.tsx +++ b/packages/extension/src/components/organisms/NFTListing/NFTListing.tsx @@ -7,6 +7,7 @@ import { AccountNFTokenResponse } from '@gemwallet/constants'; import { InformationMessage } from '../../molecules'; import { NFTCard } from '../../molecules/NFTCard'; +import { MAX_FETCHED_NFTS } from '../../pages'; export interface NFTListingProps extends AccountNFTokenResponse { onLoadMoreClick: () => void; @@ -26,14 +27,16 @@ export const NFTListing: FC = ({ loading, account_nfts, onLoadM = MAX_FETCHED_NFTS} height={450} loader={

Loading...

} > - {account_nfts.map((nft) => ( - - ))} + {account_nfts + .filter((NFT) => NFT.URI) // Do not display NFTs without URI + .map((NFT) => ( + + ))}
); diff --git a/packages/extension/src/components/pages/NFTs/NFTs.tsx b/packages/extension/src/components/pages/NFTs/NFTs.tsx index 6f16ddf53..7b70817d4 100644 --- a/packages/extension/src/components/pages/NFTs/NFTs.tsx +++ b/packages/extension/src/components/pages/NFTs/NFTs.tsx @@ -6,6 +6,8 @@ import { LedgerContext } from '../../../contexts'; import { NFTListing } from '../../organisms/NFTListing'; import { PageWithHeader } from '../../templates'; +export const MAX_FETCHED_NFTS = 20; + const initalState = { account_nfts: [], marker: null, @@ -24,7 +26,7 @@ export const NFTs: FC = () => { const fetchNFTs = async () => { try { const payload = { - limit: 20, + limit: MAX_FETCHED_NFTS, marker: NFTs.marker ?? undefined }; diff --git a/packages/extension/src/contexts/LedgerContext/LedgerContext.tsx b/packages/extension/src/contexts/LedgerContext/LedgerContext.tsx index ca1815232..90a6fe584 100644 --- a/packages/extension/src/contexts/LedgerContext/LedgerContext.tsx +++ b/packages/extension/src/contexts/LedgerContext/LedgerContext.tsx @@ -805,7 +805,7 @@ const LedgerProvider: FC = ({ children }) => { const getNFTData = useCallback(async ({ NFT }: NFTImageRequest) => { try { - const { URI } = NFT; + const { NFTokenID, URI } = NFT; let URL = URI ? await convertHexToString(String(URI)) : ''; if (URL.length === 0) { @@ -813,17 +813,31 @@ const LedgerProvider: FC = ({ children }) => { } URL = URL.replace('ipfs://', 'https://ipfs.io/ipfs/'); - const NFTData = await fetch(URL) - .then((res) => res.json()) - .catch(() => ({ - name: '-', - description: '-', - image: null - })); - NFTData.image = NFTData.image - ? NFTData.image.replace('ipfs://', 'https://ipfs.io/ipfs/') - : URL.replace('.json', '.png'); - return NFTData; + + if (URL.includes('.json')) { + // Parse the JSON, in order to display the NFT in the UI + const NFTData = await fetch(URL) + .then((res) => res.json()) + .catch(() => { + throw new Error('Error fetching NFT data'); + }); + + const image = NFTData.image + ? NFTData.image.replace('ipfs://', 'https://ipfs.io/ipfs/') + : URL.replace('.json', '.png'); + + return { + ...NFTData, + NFTokenID, + image + }; + } else { + // No JSON to parse, just display the raw NFT attributes + return { + NFTokenID, + description: URL + }; + } } catch (e) { Sentry.captureException(e); throw e; From e100ae14f4a7181625e79a52f09c1cea96d99125 Mon Sep 17 00:00:00 2001 From: thibautbremand Date: Wed, 28 Jun 2023 18:34:51 +0200 Subject: [PATCH 10/47] Add NFT details modal --- .../molecules/NFTCard/NFTCard.test.tsx | 13 +- .../components/molecules/NFTCard/NFTCard.tsx | 150 ++++++++++-------- .../organisms/NFTDetails/NFTDetails.tsx | 79 +++++++++ .../components/organisms/NFTDetails/index.ts | 1 + .../src/components/organisms/index.ts | 2 + .../src/components/pages/NFTs/NFTs.tsx | 2 +- 6 files changed, 172 insertions(+), 75 deletions(-) create mode 100644 packages/extension/src/components/organisms/NFTDetails/NFTDetails.tsx create mode 100644 packages/extension/src/components/organisms/NFTDetails/index.ts diff --git a/packages/extension/src/components/molecules/NFTCard/NFTCard.test.tsx b/packages/extension/src/components/molecules/NFTCard/NFTCard.test.tsx index f3b546825..e6c3aba08 100644 --- a/packages/extension/src/components/molecules/NFTCard/NFTCard.test.tsx +++ b/packages/extension/src/components/molecules/NFTCard/NFTCard.test.tsx @@ -1,4 +1,4 @@ -import { render, screen, waitFor, fireEvent, act } from '@testing-library/react'; +import { render, screen, waitFor, act } from '@testing-library/react'; import { LedgerContext } from '../../../contexts'; import { valueLedgerContext } from '../../../mocks'; @@ -72,15 +72,4 @@ describe('NFTCard', () => { // Check that the NFT data is not displayed when an error occurs expect(screen.queryByTestId('nft_name')).not.toBeInTheDocument(); }); - - test('button redirects to the correct URL', async () => { - const windowOpenSpy = jest.spyOn(window, 'open').mockImplementation(); - - renderNFTCard({ NFT: mockNFT }); - await waitFor(() => expect(mockGetNFTData).toHaveBeenCalled()); - - fireEvent.click(screen.getByRole('button')); - - expect(windowOpenSpy).toHaveBeenCalledWith('someurl', '_blank'); - }); }); diff --git a/packages/extension/src/components/molecules/NFTCard/NFTCard.tsx b/packages/extension/src/components/molecules/NFTCard/NFTCard.tsx index 3d4a622a7..5897021b7 100644 --- a/packages/extension/src/components/molecules/NFTCard/NFTCard.tsx +++ b/packages/extension/src/components/molecules/NFTCard/NFTCard.tsx @@ -1,22 +1,42 @@ -import { FC, useContext, useEffect, useState } from 'react'; +import { FC, forwardRef, useCallback, useContext, useEffect, useState } from 'react'; import { OpenInNewOutlined } from '@mui/icons-material'; -import { Button, CircularProgress, ListItem, Paper, Tooltip } from '@mui/material'; +import { Button, CircularProgress, Dialog, ListItem, Paper, Slide, Tooltip } from '@mui/material'; +import { TransitionProps } from '@mui/material/transitions'; import { LazyLoadImage } from 'react-lazy-load-image-component'; import { AccountNFToken, NFTData } from '@gemwallet/constants'; import { LedgerContext } from '../../../contexts'; import { GemWallet } from '../../atoms'; +import { NFTDetails } from '../../organisms'; export interface NFTCardProps { NFT: AccountNFToken; } +const Transition = forwardRef(function Transition( + props: TransitionProps & { + children: React.ReactElement; + }, + ref: React.Ref +) { + return ; +}); + export const NFTCard: FC = ({ NFT }) => { const { getNFTData } = useContext(LedgerContext); const [NFTData, setNFTData] = useState(null); const [loading, setLoading] = useState(true); + const [dialogOpen, setDialogOpen] = useState(false); + + const handleViewNFTClick = useCallback(() => { + setDialogOpen(true); + }, []); + + const handleCloseDialog = useCallback(() => { + setDialogOpen(false); + }, []); useEffect(() => { const fetchNFTImg = async () => { @@ -33,77 +53,83 @@ export const NFTCard: FC = ({ NFT }) => { fetchNFTImg(); }, [getNFTData, NFT]); - const handleViewNFTClick = () => { - window.open('someurl', '_blank'); // TODO: Add redirection url (Potential collaboration with NFT marketplace)? - }; - const handleTokenIdClick = (tokenId: string) => { navigator.clipboard.writeText(tokenId); }; return ( - - + + {NFTData && } + + - {loading ? ( - - ) : NFTData?.image ? ( - } - effect="blur" - src={NFTData?.image} - width={150} - /> - ) : ( - - )} - {NFTData ? ( - + + {loading ? ( + + ) : NFTData?.image ? ( + } + effect="blur" + src={NFTData?.image} + width={150} + /> + ) : ( + + )} + {NFTData ? ( + +
handleTokenIdClick(NFTData.NFTokenID)} + > + {NFTData.NFTokenID.substring(0, 10) + '...'} +
+
+ ) : null} + {NFTData?.name ? (
handleTokenIdClick(NFTData.NFTokenID)} + style={{ fontSize: '16px', color: 'white', marginTop: '10px' }} + data-testid="nft_name" > - {NFTData.NFTokenID.substring(0, 10) + '...'} + {NFTData.name}
-
- ) : null} - {NFTData?.name ? ( -
+ {NFTData.description} +
+ ) : null} + -
-
+ View + + + + ); }; diff --git a/packages/extension/src/components/organisms/NFTDetails/NFTDetails.tsx b/packages/extension/src/components/organisms/NFTDetails/NFTDetails.tsx new file mode 100644 index 000000000..6851b3d7c --- /dev/null +++ b/packages/extension/src/components/organisms/NFTDetails/NFTDetails.tsx @@ -0,0 +1,79 @@ +import React, { FC } from 'react'; + +import CloseIcon from '@mui/icons-material/Close'; +import { + AppBar, + CircularProgress, + IconButton, + List, + ListItem, + ListItemText, + Toolbar, + Typography +} from '@mui/material'; +import { LazyLoadImage } from 'react-lazy-load-image-component'; + +import { NFTData } from '@gemwallet/constants'; + +import { GemWallet } from '../../atoms'; + +interface NFTDetailsProps { + NFTData: NFTData; + handleClose: () => void; +} + +export const NFTDetails: FC = ({ NFTData, handleClose }) => { + return ( + <> + + + + + + + NFT Details + + + + + {NFTData.image ? ( +
+ } + effect="blur" + src={NFTData?.image} + width={250} + /> +
+ ) : ( +
+ +
+ )} + + + + {NFTData.name ? ( + + + + ) : null} + {NFTData.description ? ( + + + + ) : null} +
+ + ); +}; diff --git a/packages/extension/src/components/organisms/NFTDetails/index.ts b/packages/extension/src/components/organisms/NFTDetails/index.ts new file mode 100644 index 000000000..f73711124 --- /dev/null +++ b/packages/extension/src/components/organisms/NFTDetails/index.ts @@ -0,0 +1 @@ +export * from './NFTDetails'; diff --git a/packages/extension/src/components/organisms/index.ts b/packages/extension/src/components/organisms/index.ts index 829259c41..49dbc69df 100644 --- a/packages/extension/src/components/organisms/index.ts +++ b/packages/extension/src/components/organisms/index.ts @@ -2,5 +2,7 @@ export * from './BaseTransaction'; export * from './DisplayXRPLTransaction'; export * from './Header'; export * from './NavMenu'; +export * from './NFTDetails'; +export * from './NFTListing'; export * from './TokenListing'; export * from './TransactionListing'; diff --git a/packages/extension/src/components/pages/NFTs/NFTs.tsx b/packages/extension/src/components/pages/NFTs/NFTs.tsx index 7b70817d4..5c2459a11 100644 --- a/packages/extension/src/components/pages/NFTs/NFTs.tsx +++ b/packages/extension/src/components/pages/NFTs/NFTs.tsx @@ -3,7 +3,7 @@ import { FC, useContext, useEffect, useState } from 'react'; import { AccountNFTokenResponse } from '@gemwallet/constants'; import { LedgerContext } from '../../../contexts'; -import { NFTListing } from '../../organisms/NFTListing'; +import { NFTListing } from '../../organisms'; import { PageWithHeader } from '../../templates'; export const MAX_FETCHED_NFTS = 20; From 473c57129962efbf2ec64ea58d9f16a718bf82f2 Mon Sep 17 00:00:00 2001 From: thibautbremand Date: Thu, 29 Jun 2023 16:58:27 +0200 Subject: [PATCH 11/47] Do not filter out NFTs without URIs --- .../src/components/organisms/NFTListing/NFTListing.tsx | 8 +++----- .../src/contexts/LedgerContext/LedgerContext.tsx | 5 ++++- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/packages/extension/src/components/organisms/NFTListing/NFTListing.tsx b/packages/extension/src/components/organisms/NFTListing/NFTListing.tsx index ce6f63c17..df87948c6 100644 --- a/packages/extension/src/components/organisms/NFTListing/NFTListing.tsx +++ b/packages/extension/src/components/organisms/NFTListing/NFTListing.tsx @@ -32,11 +32,9 @@ export const NFTListing: FC = ({ loading, account_nfts, onLoadM loader={

Loading...

} > - {account_nfts - .filter((NFT) => NFT.URI) // Do not display NFTs without URI - .map((NFT) => ( - - ))} + {account_nfts.map((NFT) => ( + + ))} ); diff --git a/packages/extension/src/contexts/LedgerContext/LedgerContext.tsx b/packages/extension/src/contexts/LedgerContext/LedgerContext.tsx index 90a6fe584..3e92ef93b 100644 --- a/packages/extension/src/contexts/LedgerContext/LedgerContext.tsx +++ b/packages/extension/src/contexts/LedgerContext/LedgerContext.tsx @@ -809,7 +809,10 @@ const LedgerProvider: FC = ({ children }) => { let URL = URI ? await convertHexToString(String(URI)) : ''; if (URL.length === 0) { - throw new Error('URI is empty'); + return { + NFTokenID, + description: 'No data' + }; } URL = URL.replace('ipfs://', 'https://ipfs.io/ipfs/'); From c8ac57d1eeac694c68909ccb48a2265edac5379a Mon Sep 17 00:00:00 2001 From: thibautbremand Date: Thu, 29 Jun 2023 16:58:48 +0200 Subject: [PATCH 12/47] Add cypress e2e tests --- packages/extension/cypress/e2e/NFT.cy.ts | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/packages/extension/cypress/e2e/NFT.cy.ts b/packages/extension/cypress/e2e/NFT.cy.ts index a469277a6..a629ae758 100644 --- a/packages/extension/cypress/e2e/NFT.cy.ts +++ b/packages/extension/cypress/e2e/NFT.cy.ts @@ -7,7 +7,7 @@ import { formatFlags } from '../../src/utils'; describe('Mint', () => { // deepcode ignore NoHardcodedPasswords: password used for testing purposes const PASSWORD = 'SECRET_PASSWORD'; - const MINT_NFT_URL = `http://localhost:3000?mint-nft?URI=4d696e746564207468726f7567682047656d57616c6c657421&flags=%7B%22tfOnlyXRP%22%3Afalse%2C%22tfTransferable%22%3Atrue%7D&fee=199&transferFee=3000&NFTokenTaxon=0&memos=%5B%7B%22memo%22%3A%7B%22memoType%22%3A%224465736372697074696f6e%22%2C%22memoData%22%3A%2254657374206d656d6f%22%7D%7D%5D&id=210324818&requestMessage=undefined&transaction=mintNFT`; + const MINT_NFT_URL = `http://localhost:3000?mint-nft&URI=4d696e746564207468726f7567682047656d57616c6c657421&flags=%7B%22tfOnlyXRP%22%3Afalse%2C%22tfTransferable%22%3Atrue%7D&fee=199&transferFee=3000&NFTokenTaxon=0&memos=%5B%7B%22memo%22%3A%7B%22memoType%22%3A%224465736372697074696f6e%22%2C%22memoData%22%3A%2254657374206d656d6f%22%7D%7D%5D&id=210324818&requestMessage=undefined&transaction=mintNFT`; beforeEach(() => { // Mock the localStorage with a wallet already loaded @@ -54,7 +54,22 @@ describe('Mint', () => { cy.get('p[data-testid="transaction-subtitle"]').should('have.text', 'Transaction Successful'); }); - it('Read the minted NFT Token ID', () => { + it('View a NFT in the NFT Viewer', () => { + navigate('localhost:3000', PASSWORD); + + // Go to NFT Viewer + cy.contains('button', 'NFTs').click(); + + // Find a NFT and open the details + cy.get('[data-testid="OpenInNewOutlinedIcon"]').parent('button').first().click(); + + // Check that the details view is open + cy.contains('NFT Details'); + cy.contains('Token ID'); + cy.contains('Description'); + }); + + it('Read the minted NFT Token ID from the transaction history', () => { navigate('localhost:3000', PASSWORD); // Wait for the wallet to load From f2f3748a1ef8f046ee3fbc970b1f016fe9d03de7 Mon Sep 17 00:00:00 2001 From: thibautbremand Date: Tue, 4 Jul 2023 10:50:04 +0200 Subject: [PATCH 13/47] Fix dependencies post rebase --- packages/extension/package.json | 4 ++-- yarn.lock | 16 ++++++++-------- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/packages/extension/package.json b/packages/extension/package.json index f73804fcd..2bff34868 100644 --- a/packages/extension/package.json +++ b/packages/extension/package.json @@ -19,9 +19,9 @@ "react": "^17.0.2", "react-content-loader": "^6.2.0", "react-dom": "^17.0.2", - "react-json-view": "^1.21.3", "react-infinite-scroll-component": "^6.1.0", - "react-lazy-load-image-component": "^1.5.6", + "react-json-view": "^1.21.3", + "react-lazy-load-image-component": "^1.6.0", "react-router-dom": "6", "react-scripts": "4.0.3", "ripple-keypairs": "^1.1.5", diff --git a/yarn.lock b/yarn.lock index 040cd5c63..1f8bda6d0 100644 --- a/yarn.lock +++ b/yarn.lock @@ -11073,19 +11073,19 @@ react-json-view@^1.21.3: react-lifecycles-compat "^3.0.4" react-textarea-autosize "^8.3.2" +react-lazy-load-image-component@^1.6.0: + version "1.6.0" + resolved "https://registry.yarnpkg.com/react-lazy-load-image-component/-/react-lazy-load-image-component-1.6.0.tgz#f262c2f163052d71011e282031fd60aafa6494ac" + integrity sha512-8KFkDTgjh+0+PVbH+cx0AgxLGbdTsxWMnxXzU5HEUztqewk9ufQAu8cstjZhyvtMIPsdMcPZfA0WAa7HtjQbBQ== + dependencies: + lodash.debounce "^4.0.8" + lodash.throttle "^4.1.1" + react-lifecycles-compat@^3.0.4: version "3.0.4" resolved "https://registry.yarnpkg.com/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz#4f1a273afdfc8f3488a8c516bfda78f872352362" integrity sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA== -react-lazy-load-image-component@^1.5.6: - version "1.5.6" - resolved "https://registry.yarnpkg.com/react-lazy-load-image-component/-/react-lazy-load-image-component-1.5.6.tgz#a4b84257be21b1825680b4e158d167c08aeda5ff" - integrity sha512-M0jeJtOlTHgThOfgYM9krSqYbR6ShxROy/KVankwbw9/amPKG1t5GSGN1sei6Cyu8+QJVuyAUvQ+LFtCVTTlKw== - dependencies: - lodash.debounce "^4.0.8" - lodash.throttle "^4.1.1" - react-refresh@^0.8.3: version "0.8.3" resolved "https://registry.yarnpkg.com/react-refresh/-/react-refresh-0.8.3.tgz#721d4657672d400c5e3c75d063c4a85fb2d5d68f" From d28cfc81cf24d0496cd8395d0d21abcefada497a Mon Sep 17 00:00:00 2001 From: Florian Bouron Date: Sun, 9 Jul 2023 09:16:33 +0200 Subject: [PATCH 14/47] Clean up types --- packages/constants/src/payload/payload.types.ts | 12 ++---------- packages/constants/src/xrpl/nft.types.ts | 5 +++++ .../src/contexts/LedgerContext/LedgerContext.tsx | 16 ++++------------ 3 files changed, 11 insertions(+), 22 deletions(-) diff --git a/packages/constants/src/payload/payload.types.ts b/packages/constants/src/payload/payload.types.ts index 417b84bfa..6d184f29a 100644 --- a/packages/constants/src/payload/payload.types.ts +++ b/packages/constants/src/payload/payload.types.ts @@ -12,7 +12,7 @@ import { SetAccountFlags, CreateOfferFlags } from '../xrpl/basic.types'; -import { AccountNFToken, AccountNFTokenResponse } from './../xrpl/nft.types'; +import { AccountNFToken, AccountNFTokenResponse, NFTokenIDResponse } from './../xrpl/nft.types'; /* * Request Payloads @@ -240,10 +240,6 @@ export interface SubmitTransactionRequest { transaction: Transaction; } -export interface NftImageRequestPayload { - nft: AccountNFToken; -} - export type RequestPayload = | AcceptNFTOfferRequest | BurnNFTRequest @@ -333,11 +329,7 @@ export interface GetNFTResponseDeprecated { nfts: AccountNFToken[] | null | undefined; } -export interface MintNFTResponse - extends BaseResponse<{ - NFTokenID: string; - hash: string; - }> {} +export interface MintNFTResponse extends BaseResponse {} export interface CreateNFTOfferResponse extends BaseResponse<{ diff --git a/packages/constants/src/xrpl/nft.types.ts b/packages/constants/src/xrpl/nft.types.ts index 48042b493..ab8f60c61 100644 --- a/packages/constants/src/xrpl/nft.types.ts +++ b/packages/constants/src/xrpl/nft.types.ts @@ -12,6 +12,11 @@ export interface AccountNFTokenResponse { marker?: unknown; } +export interface NFTokenIDResponse { + hash: string; + NFTokenID: string; +} + export interface NFTData { NFTokenID: string; NFType?: string; diff --git a/packages/extension/src/contexts/LedgerContext/LedgerContext.tsx b/packages/extension/src/contexts/LedgerContext/LedgerContext.tsx index 3e92ef93b..cb01c0450 100644 --- a/packages/extension/src/contexts/LedgerContext/LedgerContext.tsx +++ b/packages/extension/src/contexts/LedgerContext/LedgerContext.tsx @@ -25,6 +25,7 @@ import { BaseTransaction } from 'xrpl/dist/npm/models/transactions/common'; import { AcceptNFTOfferRequest, AccountNFToken, + AccountNFTokenResponse, BaseTransactionRequest, BurnNFTRequest, CancelNFTOfferRequest, @@ -34,6 +35,7 @@ import { GetNFTRequest, MintNFTRequest, NFTData, + NFTokenIDResponse, SendPaymentRequest, SetAccountRequest, SetTrustlineRequest, @@ -45,16 +47,6 @@ import { toXRPLMemos, toXRPLSigners } from '../../utils'; import { useNetwork } from '../NetworkContext'; import { useWallet } from '../WalletContext'; -interface GetNFTsResponse { - account_nfts: AccountNFToken[]; - marker?: unknown; -} - -interface MintNFTResponse { - hash: string; - NFTokenID: string; -} - interface FundWalletResponse { wallet: Wallet; balance: number; @@ -104,10 +96,10 @@ export interface LedgerContextType { setTrustline: (payload: SetTrustlineRequest) => Promise; signMessage: (message: string) => string | undefined; estimateNetworkFees: (payload: Transaction) => Promise; - getNFTs: (payload?: GetNFTRequest) => Promise; + getNFTs: (payload?: GetNFTRequest) => Promise; getTransactions: () => Promise; fundWallet: () => Promise; - mintNFT: (payload: MintNFTRequest) => Promise; + mintNFT: (payload: MintNFTRequest) => Promise; createNFTOffer: (payload: CreateNFTOfferRequest) => Promise; cancelNFTOffer: (payload: CancelNFTOfferRequest) => Promise; acceptNFTOffer: (payload: AcceptNFTOfferRequest) => Promise; From 9daba3d8824146abb935ca4c88fdbd6e0859067d Mon Sep 17 00:00:00 2001 From: thibautbremand Date: Mon, 10 Jul 2023 21:20:29 +0200 Subject: [PATCH 15/47] Fix build --- packages/extension/src/contexts/LedgerContext/LedgerContext.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/extension/src/contexts/LedgerContext/LedgerContext.tsx b/packages/extension/src/contexts/LedgerContext/LedgerContext.tsx index cb01c0450..0512b0ab1 100644 --- a/packages/extension/src/contexts/LedgerContext/LedgerContext.tsx +++ b/packages/extension/src/contexts/LedgerContext/LedgerContext.tsx @@ -172,7 +172,7 @@ const LedgerProvider: FC = ({ children }) => { ); const getNFTs = useCallback( - async (payload?: GetNFTRequest): Promise => { + async (payload?: GetNFTRequest): Promise => { const wallet = getCurrentWallet(); if (!client) { throw new Error('You need to be connected to a ledger to get the NFTs'); From a9b6b7f2d57b6f79022c7fefa275d8e7b448aeb9 Mon Sep 17 00:00:00 2001 From: thibautbremand Date: Sat, 22 Jul 2023 13:07:48 +0200 Subject: [PATCH 16/47] Fix NFT cypress tests --- packages/extension/cypress/e2e/NFT.cy.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/extension/cypress/e2e/NFT.cy.ts b/packages/extension/cypress/e2e/NFT.cy.ts index a629ae758..1871fbe85 100644 --- a/packages/extension/cypress/e2e/NFT.cy.ts +++ b/packages/extension/cypress/e2e/NFT.cy.ts @@ -57,6 +57,9 @@ describe('Mint', () => { it('View a NFT in the NFT Viewer', () => { navigate('localhost:3000', PASSWORD); + // Wait for the wallet to load + cy.get('[data-testid="token-loader"]').should('not.exist'); + // Go to NFT Viewer cy.contains('button', 'NFTs').click(); From 4f2c3211e20ee24a4e3d65ba17f4e45618148c2d Mon Sep 17 00:00:00 2001 From: thibautbremand Date: Sat, 22 Jul 2023 15:10:51 +0200 Subject: [PATCH 17/47] Improve NFTCard UI --- .../components/molecules/NFTCard/NFTCard.tsx | 9 ++++--- packages/extension/src/utils/format.test.ts | 22 ++++++++++++++++ packages/extension/src/utils/format.ts | 25 +++++++++++++++++++ 3 files changed, 53 insertions(+), 3 deletions(-) diff --git a/packages/extension/src/components/molecules/NFTCard/NFTCard.tsx b/packages/extension/src/components/molecules/NFTCard/NFTCard.tsx index 5897021b7..890aaca31 100644 --- a/packages/extension/src/components/molecules/NFTCard/NFTCard.tsx +++ b/packages/extension/src/components/molecules/NFTCard/NFTCard.tsx @@ -8,6 +8,7 @@ import { LazyLoadImage } from 'react-lazy-load-image-component'; import { AccountNFToken, NFTData } from '@gemwallet/constants'; import { LedgerContext } from '../../../contexts'; +import { formatStringToBeDisplayed } from '../../../utils'; import { GemWallet } from '../../atoms'; import { NFTDetails } from '../../organisms'; @@ -25,6 +26,8 @@ const Transition = forwardRef(function Transition( }); export const NFTCard: FC = ({ NFT }) => { + const MAX_STRING_LENGTH = 30; + const { getNFTData } = useContext(LedgerContext); const [NFTData, setNFTData] = useState(null); const [loading, setLoading] = useState(true); @@ -104,7 +107,7 @@ export const NFTCard: FC = ({ NFT }) => { style={{ fontSize: '14px', color: 'grey', marginTop: '10px', cursor: 'pointer' }} onClick={() => handleTokenIdClick(NFTData.NFTokenID)} > - {NFTData.NFTokenID.substring(0, 10) + '...'} + {formatStringToBeDisplayed(NFTData.NFTokenID, MAX_STRING_LENGTH)} ) : null} @@ -113,12 +116,12 @@ export const NFTCard: FC = ({ NFT }) => { style={{ fontSize: '16px', color: 'white', marginTop: '10px' }} data-testid="nft_name" > - {NFTData.name} + {formatStringToBeDisplayed(NFTData.name, MAX_STRING_LENGTH)} ) : null} {NFTData?.description ? (
- {NFTData.description} + {formatStringToBeDisplayed(NFTData.description, MAX_STRING_LENGTH)}
) : null} - - + + {loading ? ( + + ) : NFTData?.image ? ( + } + effect="blur" + src={NFTData?.image} + width={150} + /> + ) : ( + + )} + {NFTData ? ( + +
handleTokenIdClick(NFTData.NFTokenID)} + > + {formatStringToBeDisplayed(NFTData.NFTokenID, MAX_STRING_LENGTH)} +
+
+ ) : null} + {NFTData?.name ? ( +
+ {formatStringToBeDisplayed(NFTData.name, MAX_STRING_LENGTH)} +
+ ) : null} + {NFTData?.description ? ( +
+ {formatStringToBeDisplayed(NFTData.description, MAX_STRING_LENGTH)} +
+ ) : null} + +
+ + + ) : null} ); }; From eb6ee98887ed0161ff81f775ba3b6f58b8cbaac7 Mon Sep 17 00:00:00 2001 From: thibautbremand Date: Wed, 2 Aug 2023 20:41:53 +0200 Subject: [PATCH 26/47] Fix NFTCard unit tests --- .../molecules/NFTCard/NFTCard.test.tsx | 29 ++++++++----------- 1 file changed, 12 insertions(+), 17 deletions(-) diff --git a/packages/extension/src/components/molecules/NFTCard/NFTCard.test.tsx b/packages/extension/src/components/molecules/NFTCard/NFTCard.test.tsx index e6c3aba08..f0cdb6afd 100644 --- a/packages/extension/src/components/molecules/NFTCard/NFTCard.test.tsx +++ b/packages/extension/src/components/molecules/NFTCard/NFTCard.test.tsx @@ -1,7 +1,6 @@ import { render, screen, waitFor, act } from '@testing-library/react'; import { LedgerContext } from '../../../contexts'; -import { valueLedgerContext } from '../../../mocks'; import { NFTCard, NFTCardProps } from './NFTCard'; const mockNFT = { @@ -26,39 +25,35 @@ const mockNFTData = { } }; -const mockGetNFTData = jest.fn(async () => mockNFTData); -const mockContext = { - ...valueLedgerContext, - getNFTData: mockGetNFTData -}; +const mockGetNFTData = jest.fn(); +// @ts-ignore +const LedgerContextMockProvider = ({ children }) => ( + // @ts-ignore + {children} +); const renderNFTCard = (props: NFTCardProps) => { act(() => { render( - + - + ); }); }; describe('NFTCard', () => { - afterEach(() => { - jest.clearAllMocks(); - }); - test('renders NFTCard component correctly', async () => { + mockGetNFTData.mockReturnValue({ + ...mockNFTData + }); + renderNFTCard({ NFT: mockNFT }); await waitFor(() => expect(mockGetNFTData).toHaveBeenCalled()); expect(screen.getByTestId('OpenInNewOutlinedIcon')).toBeInTheDocument(); }); - test('displays CircularProgress while fetching data', () => { - renderNFTCard({ NFT: mockNFT }); - expect(screen.getByTestId('progressbar')).toBeInTheDocument(); - }); - test('handles error when fetching NFT data', async () => { // Temporarily change the behavior of getNFTData to throw an error mockGetNFTData.mockImplementationOnce(() => { From be3a41d786bb4419f167b20e96c410a15b4448f6 Mon Sep 17 00:00:00 2001 From: thibautbremand Date: Fri, 4 Aug 2023 11:35:06 +0200 Subject: [PATCH 27/47] Handle .gif direct links in NFT Viewer --- packages/extension/src/utils/NFTImageResolver.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/packages/extension/src/utils/NFTImageResolver.ts b/packages/extension/src/utils/NFTImageResolver.ts index 6dd33baaa..42302003e 100644 --- a/packages/extension/src/utils/NFTImageResolver.ts +++ b/packages/extension/src/utils/NFTImageResolver.ts @@ -18,7 +18,12 @@ export const resolveNFTImage = async (NFT: AccountNFToken): Promise => URL = URL.replace('ipfs://', 'https://ipfs.io/ipfs/'); // Case 1 - Image URL - if (URL.includes('.png') || URL.includes('.jpg')) { + if ( + URL.includes('.png') || + URL.includes('.jpg') || + URL.includes('.jpeg') || + URL.includes('.gif') + ) { try { // Case 1.1 - The URL is directly an image await fetch(URL); From 88b90c734364c2ddcbbddb4ad4a7d909f76e1073 Mon Sep 17 00:00:00 2001 From: thibautbremand Date: Fri, 4 Aug 2023 12:22:49 +0200 Subject: [PATCH 28/47] Display NFT without data with a fallback description --- .../extension/src/components/molecules/NFTCard/NFTCard.tsx | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/extension/src/components/molecules/NFTCard/NFTCard.tsx b/packages/extension/src/components/molecules/NFTCard/NFTCard.tsx index 6b8696d4e..16d5e534f 100644 --- a/packages/extension/src/components/molecules/NFTCard/NFTCard.tsx +++ b/packages/extension/src/components/molecules/NFTCard/NFTCard.tsx @@ -4,6 +4,7 @@ import { OpenInNewOutlined } from '@mui/icons-material'; import { Button, CircularProgress, Dialog, ListItem, Paper, Slide, Tooltip } from '@mui/material'; import { TransitionProps } from '@mui/material/transitions'; import { LazyLoadImage } from 'react-lazy-load-image-component'; +import { convertHexToString } from 'xrpl'; import { AccountNFToken, NFTData } from '@gemwallet/constants'; @@ -48,7 +49,10 @@ export const NFTCard: FC = ({ NFT }) => { const nftData = await getNFTData({ NFT }); setNFTData(nftData); } catch (error) { - setNFTData(null); + setNFTData({ + NFTokenID: NFT.NFTokenID, + description: NFT.URI ? convertHexToString(NFT.URI) : 'No data' + }); } finally { setLoading(false); } From 20bf66936da1a5a11012c06eae423dd8ac7fc6c0 Mon Sep 17 00:00:00 2001 From: Florian Bouron Date: Fri, 4 Aug 2023 22:00:00 +0200 Subject: [PATCH 29/47] Cleaning up code --- .../pages/{NFTs/NFTs.tsx => NFTViewer/NFTViewer.tsx} | 2 +- packages/extension/src/components/pages/NFTViewer/index.ts | 1 + packages/extension/src/components/pages/NFTs/index.ts | 1 - packages/extension/src/components/pages/index.ts | 2 +- packages/extension/src/constants/navigation.tsx | 4 ++-- packages/extension/src/constants/paths.ts | 2 +- packages/extension/src/utils/format.ts | 6 +++--- 7 files changed, 9 insertions(+), 9 deletions(-) rename packages/extension/src/components/pages/{NFTs/NFTs.tsx => NFTViewer/NFTViewer.tsx} (97%) create mode 100644 packages/extension/src/components/pages/NFTViewer/index.ts delete mode 100644 packages/extension/src/components/pages/NFTs/index.ts diff --git a/packages/extension/src/components/pages/NFTs/NFTs.tsx b/packages/extension/src/components/pages/NFTViewer/NFTViewer.tsx similarity index 97% rename from packages/extension/src/components/pages/NFTs/NFTs.tsx rename to packages/extension/src/components/pages/NFTViewer/NFTViewer.tsx index 5c2459a11..e96a06d72 100644 --- a/packages/extension/src/components/pages/NFTs/NFTs.tsx +++ b/packages/extension/src/components/pages/NFTViewer/NFTViewer.tsx @@ -18,7 +18,7 @@ interface NFTsProps extends AccountNFTokenResponse { loading: boolean; } -export const NFTs: FC = () => { +export const NFTViewer: FC = () => { const { getNFTs } = useContext(LedgerContext); const [NFTs, setNFTs] = useState(initalState); diff --git a/packages/extension/src/components/pages/NFTViewer/index.ts b/packages/extension/src/components/pages/NFTViewer/index.ts new file mode 100644 index 000000000..e91a2b8f1 --- /dev/null +++ b/packages/extension/src/components/pages/NFTViewer/index.ts @@ -0,0 +1 @@ +export * from './NFTViewer'; diff --git a/packages/extension/src/components/pages/NFTs/index.ts b/packages/extension/src/components/pages/NFTs/index.ts deleted file mode 100644 index c838a60d1..000000000 --- a/packages/extension/src/components/pages/NFTs/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './NFTs'; diff --git a/packages/extension/src/components/pages/index.ts b/packages/extension/src/components/pages/index.ts index 6817f6bf4..ee77d7764 100644 --- a/packages/extension/src/components/pages/index.ts +++ b/packages/extension/src/components/pages/index.ts @@ -18,7 +18,7 @@ export * from './ImportWallet'; export * from './ListWallets'; export * from './Login'; export * from './MintNFT'; -export * from './NFTs'; +export * from './NFTViewer'; export * from './ResetPassword'; export * from './SendPayment'; export * from './ReceivePayment'; diff --git a/packages/extension/src/constants/navigation.tsx b/packages/extension/src/constants/navigation.tsx index 91ff9872b..80d7dd115 100644 --- a/packages/extension/src/constants/navigation.tsx +++ b/packages/extension/src/constants/navigation.tsx @@ -3,7 +3,7 @@ import HistoryIcon from '@mui/icons-material/History'; import PhotoCameraBackIcon from '@mui/icons-material/PhotoCameraBack'; import SettingsIcon from '@mui/icons-material/Settings'; -import { HISTORY_PATH, HOME_PATH, NFTS_PATH, SETTINGS_PATH } from './paths'; +import { HISTORY_PATH, HOME_PATH, NFT_VIEWER_PATH, SETTINGS_PATH } from './paths'; export const navigation = [ { @@ -18,7 +18,7 @@ export const navigation = [ }, { label: 'NFTs', - pathname: NFTS_PATH, + pathname: NFT_VIEWER_PATH, icon: }, { diff --git a/packages/extension/src/constants/paths.ts b/packages/extension/src/constants/paths.ts index 0a5d0b381..46c4e2576 100644 --- a/packages/extension/src/constants/paths.ts +++ b/packages/extension/src/constants/paths.ts @@ -31,4 +31,4 @@ export const SHARE_PUBLIC_KEY_PATH = '/share-public-key'; export const TRANSACTION_PATH = '/transaction'; export const TRUSTED_APPS_PATH = '/trusted-apps'; export const WELCOME_PATH = '/welcome'; -export const NFTS_PATH = '/nfts'; +export const NFT_VIEWER_PATH = '/nft-viewer'; diff --git a/packages/extension/src/utils/format.ts b/packages/extension/src/utils/format.ts index 7d4515a80..603a71df1 100644 --- a/packages/extension/src/utils/format.ts +++ b/packages/extension/src/utils/format.ts @@ -104,14 +104,14 @@ export const formatStringToBeDisplayed = (str: string, length: number) => { } // If there is no space within the given length in the string, return the string truncated - let spaceIndex = str.indexOf(' '); + const spaceIndex = str.indexOf(' '); if (spaceIndex === -1 || spaceIndex > length) { return str.substring(0, length) + '...'; } // If there is a word with a length bigger than the given length, return the string truncated - let words = str.split(' '); - let longWordExists = words.some((word) => word.length > length); + const words = str.split(' '); + const longWordExists = words.some((word) => word.length > length); if (longWordExists) { return str.substring(0, length) + '...'; } From 71cb8fde94cedd27efd0910ac2dbf8b84b87ffce Mon Sep 17 00:00:00 2001 From: thibautbremand Date: Sun, 6 Aug 2023 17:35:34 +0200 Subject: [PATCH 30/47] Fix post review --- packages/constants/src/xrpl/nft.constant.ts | 1 + packages/constants/src/xrpl/nft.types.ts | 10 +- .../components/molecules/NFTCard/NFTCard.tsx | 172 +++++++++--------- .../components/pages/NFTViewer/NFTViewer.tsx | 6 +- .../contexts/LedgerContext/LedgerContext.tsx | 32 ++-- .../extension/src/utils/NFTImageResolver.ts | 26 ++- packages/extension/src/utils/NFTViewer.ts | 10 +- packages/extension/src/utils/image.test.ts | 48 +++++ packages/extension/src/utils/image.ts | 3 + packages/extension/src/utils/index.ts | 1 + 10 files changed, 182 insertions(+), 127 deletions(-) create mode 100644 packages/constants/src/xrpl/nft.constant.ts create mode 100644 packages/extension/src/utils/image.test.ts create mode 100644 packages/extension/src/utils/image.ts diff --git a/packages/constants/src/xrpl/nft.constant.ts b/packages/constants/src/xrpl/nft.constant.ts new file mode 100644 index 000000000..7beb48ec9 --- /dev/null +++ b/packages/constants/src/xrpl/nft.constant.ts @@ -0,0 +1 @@ +export const IPFSResolverPrefix = 'https://ipfs.io/ipfs/'; diff --git a/packages/constants/src/xrpl/nft.types.ts b/packages/constants/src/xrpl/nft.types.ts index ab8f60c61..ac4591936 100644 --- a/packages/constants/src/xrpl/nft.types.ts +++ b/packages/constants/src/xrpl/nft.types.ts @@ -12,11 +12,6 @@ export interface AccountNFTokenResponse { marker?: unknown; } -export interface NFTokenIDResponse { - hash: string; - NFTokenID: string; -} - export interface NFTData { NFTokenID: string; NFType?: string; @@ -29,3 +24,8 @@ export interface NFTData { family?: string; }; } + +export interface NFTokenIDResponse { + hash: string; + NFTokenID: string; +} diff --git a/packages/extension/src/components/molecules/NFTCard/NFTCard.tsx b/packages/extension/src/components/molecules/NFTCard/NFTCard.tsx index 16d5e534f..a421c0030 100644 --- a/packages/extension/src/components/molecules/NFTCard/NFTCard.tsx +++ b/packages/extension/src/components/molecules/NFTCard/NFTCard.tsx @@ -1,14 +1,15 @@ -import { FC, forwardRef, useCallback, useContext, useEffect, useState } from 'react'; +import { FC, forwardRef, useCallback, useEffect, useState } from 'react'; import { OpenInNewOutlined } from '@mui/icons-material'; import { Button, CircularProgress, Dialog, ListItem, Paper, Slide, Tooltip } from '@mui/material'; import { TransitionProps } from '@mui/material/transitions'; +import * as Sentry from '@sentry/react'; import { LazyLoadImage } from 'react-lazy-load-image-component'; import { convertHexToString } from 'xrpl'; import { AccountNFToken, NFTData } from '@gemwallet/constants'; -import { LedgerContext } from '../../../contexts'; +import { useLedger } from '../../../contexts'; import { formatStringToBeDisplayed } from '../../../utils'; import { GemWallet } from '../../atoms'; import { NFTDetails } from '../../organisms'; @@ -29,10 +30,10 @@ const Transition = forwardRef(function Transition( export const NFTCard: FC = ({ NFT }) => { const MAX_STRING_LENGTH = 30; - const { getNFTData } = useContext(LedgerContext); + const { getNFTData } = useLedger(); const [NFTData, setNFTData] = useState(null); - const [loading, setLoading] = useState(true); - const [dialogOpen, setDialogOpen] = useState(false); + const [isLoading, setIsLoading] = useState(true); + const [dialogOpen, setDialogOpen] = useState(false); const handleViewNFTClick = useCallback(() => { setDialogOpen(true); @@ -45,7 +46,7 @@ export const NFTCard: FC = ({ NFT }) => { useEffect(() => { const fetchNFTImg = async () => { try { - setLoading(true); + setIsLoading(true); const nftData = await getNFTData({ NFT }); setNFTData(nftData); } catch (error) { @@ -53,8 +54,9 @@ export const NFTCard: FC = ({ NFT }) => { NFTokenID: NFT.NFTokenID, description: NFT.URI ? convertHexToString(NFT.URI) : 'No data' }); + Sentry.captureException(error); } finally { - setLoading(false); + setIsLoading(false); } }; fetchNFTImg(); @@ -64,88 +66,86 @@ export const NFTCard: FC = ({ NFT }) => { navigator.clipboard.writeText(tokenId); }; + if (!NFTData) return null; + return ( <> - {NFTData ? ( - <> - - - - - - {loading ? ( - - ) : NFTData?.image ? ( - } - effect="blur" - src={NFTData?.image} - width={150} - /> - ) : ( - - )} - {NFTData ? ( - -
handleTokenIdClick(NFTData.NFTokenID)} - > - {formatStringToBeDisplayed(NFTData.NFTokenID, MAX_STRING_LENGTH)} -
-
- ) : null} - {NFTData?.name ? ( -
- {formatStringToBeDisplayed(NFTData.name, MAX_STRING_LENGTH)} -
- ) : null} - {NFTData?.description ? ( -
- {formatStringToBeDisplayed(NFTData.description, MAX_STRING_LENGTH)} -
- ) : null} - -
-
- - ) : null} + {formatStringToBeDisplayed(NFTData.NFTokenID, MAX_STRING_LENGTH)} + + + ) : null} + {NFTData?.name ? ( +
+ {formatStringToBeDisplayed(NFTData.name, MAX_STRING_LENGTH)} +
+ ) : null} + {NFTData?.description ? ( +
+ {formatStringToBeDisplayed(NFTData.description, MAX_STRING_LENGTH)} +
+ ) : null} + + + ); }; diff --git a/packages/extension/src/components/pages/NFTViewer/NFTViewer.tsx b/packages/extension/src/components/pages/NFTViewer/NFTViewer.tsx index e96a06d72..c544c46f6 100644 --- a/packages/extension/src/components/pages/NFTViewer/NFTViewer.tsx +++ b/packages/extension/src/components/pages/NFTViewer/NFTViewer.tsx @@ -1,8 +1,8 @@ -import { FC, useContext, useEffect, useState } from 'react'; +import { FC, useEffect, useState } from 'react'; import { AccountNFTokenResponse } from '@gemwallet/constants'; -import { LedgerContext } from '../../../contexts'; +import { useLedger } from '../../../contexts'; import { NFTListing } from '../../organisms'; import { PageWithHeader } from '../../templates'; @@ -19,7 +19,7 @@ interface NFTsProps extends AccountNFTokenResponse { } export const NFTViewer: FC = () => { - const { getNFTs } = useContext(LedgerContext); + const { getNFTs } = useLedger(); const [NFTs, setNFTs] = useState(initalState); diff --git a/packages/extension/src/contexts/LedgerContext/LedgerContext.tsx b/packages/extension/src/contexts/LedgerContext/LedgerContext.tsx index 3d213254c..1187b6c04 100644 --- a/packages/extension/src/contexts/LedgerContext/LedgerContext.tsx +++ b/packages/extension/src/contexts/LedgerContext/LedgerContext.tsx @@ -176,23 +176,25 @@ const LedgerProvider: FC = ({ children }) => { const wallet = getCurrentWallet(); if (!client) { throw new Error('You need to be connected to a ledger to get the NFTs'); - } else if (!wallet) { + } + if (!wallet) { throw new Error('You need to have a wallet connected to get the NFTs'); - } else { - // Prepare the transaction - const prepared = await client.request({ - command: 'account_nfts', - account: wallet.publicAddress, - limit: payload?.limit, - marker: payload?.marker, - ledger_index: 'validated' - }); - if (!prepared.result?.account_nfts) { - throw new Error("Couldn't get the NFTs"); - } else { - return { account_nfts: prepared.result.account_nfts, marker: prepared.result.marker }; - } } + + // Prepare the transaction + const prepared = await client.request({ + command: 'account_nfts', + account: wallet.publicAddress, + limit: payload?.limit, + marker: payload?.marker, + ledger_index: 'validated' + }); + + if (!prepared.result?.account_nfts) { + throw new Error("Couldn't get the NFTs"); + } + + return { account_nfts: prepared.result.account_nfts, marker: prepared.result.marker }; }, [client, getCurrentWallet] ); diff --git a/packages/extension/src/utils/NFTImageResolver.ts b/packages/extension/src/utils/NFTImageResolver.ts index 42302003e..be6775cb3 100644 --- a/packages/extension/src/utils/NFTImageResolver.ts +++ b/packages/extension/src/utils/NFTImageResolver.ts @@ -1,12 +1,15 @@ import { convertHexToString } from 'xrpl'; import { AccountNFToken, NFTData } from '@gemwallet/constants'; +import { IPFSResolverPrefix } from '@gemwallet/constants/src/xrpl/nft.constant'; import { parseJSON } from './NFTViewer'; +import { isImageUrl } from '.'; + export const resolveNFTImage = async (NFT: AccountNFToken): Promise => { const { NFTokenID, URI } = NFT; - let URL = URI ? await convertHexToString(String(URI)) : ''; + let URL = URI ? await convertHexToString(URI) : ''; if (URL.length === 0) { return { @@ -15,15 +18,10 @@ export const resolveNFTImage = async (NFT: AccountNFToken): Promise => }; } - URL = URL.replace('ipfs://', 'https://ipfs.io/ipfs/'); + URL = URL.replace('ipfs://', IPFSResolverPrefix); // Case 1 - Image URL - if ( - URL.includes('.png') || - URL.includes('.jpg') || - URL.includes('.jpeg') || - URL.includes('.gif') - ) { + if (isImageUrl(URL)) { try { // Case 1.1 - The URL is directly an image await fetch(URL); @@ -33,8 +31,8 @@ export const resolveNFTImage = async (NFT: AccountNFToken): Promise => }; } catch (e) { // Case 1.2 - The URL is an IPFS hash - if (!URL.startsWith('https://ipfs.io/ipfs/') && !URL.startsWith('http')) { - URL = `https://ipfs.io/ipfs/${URL}`; + if (!URL.startsWith(IPFSResolverPrefix) && !URL.startsWith('http')) { + URL = `${IPFSResolverPrefix}${URL}`; } try { await fetch(URL); @@ -52,16 +50,16 @@ export const resolveNFTImage = async (NFT: AccountNFToken): Promise => return parseJSON(URL, NFTokenID); } catch (e) {} // Case 2.2 - The URL is an IPFS hash - if (!URL.startsWith('https://ipfs.io/ipfs/') && !URL.startsWith('http')) { + if (!URL.startsWith(IPFSResolverPrefix) && !URL.startsWith('http')) { try { - await fetch(`https://ipfs.io/ipfs/${URL}`); - return parseJSON(`https://ipfs.io/ipfs/${URL}`, NFTokenID); + await fetch(`${IPFSResolverPrefix}${URL}`); + return parseJSON(`${IPFSResolverPrefix}${URL}`, NFTokenID); } catch (e) {} } } // Case 3 - Return the raw NFT attributes return { NFTokenID, - description: URL.replace('https://ipfs.io/ipfs/', 'ipfs://') + description: URL.replace(IPFSResolverPrefix, 'ipfs://') }; }; diff --git a/packages/extension/src/utils/NFTViewer.ts b/packages/extension/src/utils/NFTViewer.ts index 808bd3b98..e47384d1c 100644 --- a/packages/extension/src/utils/NFTViewer.ts +++ b/packages/extension/src/utils/NFTViewer.ts @@ -1,3 +1,5 @@ +import { IPFSResolverPrefix } from '@gemwallet/constants/src/xrpl/nft.constant'; + export const parseImage = (NFTData: any, URL: string): string => { if (NFTData.image) { return replaceIPFS(NFTData.image); @@ -27,11 +29,11 @@ export const parseJSON = async (URL: any, NFTokenID: string): Promise => { }; const replaceIPFS = (inputStr: string): string => { - if (!inputStr.startsWith('ipfs://' || 'https://ipfs.io/ipfs/') && !inputStr.startsWith('http')) { - return `https://ipfs.io/ipfs/${inputStr}`; + if (!inputStr.startsWith('ipfs://' || IPFSResolverPrefix) && !inputStr.startsWith('http')) { + return `${IPFSResolverPrefix}${inputStr}`; } return inputStr - .replace('ipfs://ipfs/', 'https://ipfs.io/ipfs/') - .replace('ipfs://', 'https://ipfs.io/ipfs/'); + .replace('ipfs://ipfs/', IPFSResolverPrefix) + .replace('ipfs://', IPFSResolverPrefix); }; diff --git a/packages/extension/src/utils/image.test.ts b/packages/extension/src/utils/image.test.ts new file mode 100644 index 000000000..d26a81021 --- /dev/null +++ b/packages/extension/src/utils/image.test.ts @@ -0,0 +1,48 @@ +import { isImageUrl } from './image'; + +describe('isImageUrl', () => { + it('returns true for png image URLs', () => { + const url = 'http://example.com/image.png'; + expect(isImageUrl(url)).toBeTruthy(); + }); + + it('returns true for jpg image URLs', () => { + const url = 'http://example.com/image.jpg'; + expect(isImageUrl(url)).toBeTruthy(); + }); + + it('returns true for jpeg image URLs', () => { + const url = 'http://example.com/image.jpeg'; + expect(isImageUrl(url)).toBeTruthy(); + }); + + it('returns true for gif image URLs', () => { + const url = 'http://example.com/image.gif'; + expect(isImageUrl(url)).toBeTruthy(); + }); + + it('returns true for tif image URLs', () => { + const url = 'http://example.com/image.tif'; + expect(isImageUrl(url)).toBeTruthy(); + }); + + it('returns true for tiff image URLs', () => { + const url = 'http://example.com/image.tiff'; + expect(isImageUrl(url)).toBeTruthy(); + }); + + it('returns true for bmp image URLs', () => { + const url = 'http://example.com/image.bmp'; + expect(isImageUrl(url)).toBeTruthy(); + }); + + it('returns false for non-image URLs', () => { + const url = 'http://example.com/image.txt'; + expect(isImageUrl(url)).toBeFalsy(); + }); + + it('returns false for URLs without an extension', () => { + const url = 'http://example.com/image'; + expect(isImageUrl(url)).toBeFalsy(); + }); +}); diff --git a/packages/extension/src/utils/image.ts b/packages/extension/src/utils/image.ts new file mode 100644 index 000000000..8e1fd947d --- /dev/null +++ b/packages/extension/src/utils/image.ts @@ -0,0 +1,3 @@ +export const isImageUrl = (url: string): boolean => { + return /\.(png|jpg|jpeg|gif|tif|tiff|bmp)$/i.test(url); +}; diff --git a/packages/extension/src/utils/index.ts b/packages/extension/src/utils/index.ts index 75c17b339..e7aa0ada0 100644 --- a/packages/extension/src/utils/index.ts +++ b/packages/extension/src/utils/index.ts @@ -4,6 +4,7 @@ export * from './crypto'; export * from './format'; export * from './getLastItemFromArray'; export * from './hexConverter'; +export * from './image'; export * from './link'; export * from './network'; export * from './numbersToSeed'; From 6fc5ad54eeada1a7f9b142d44469909218b97c0e Mon Sep 17 00:00:00 2001 From: thibautbremand Date: Sun, 6 Aug 2023 22:06:55 +0200 Subject: [PATCH 31/47] Simplify formatStringToBeDisplayed function --- .../components/molecules/NFTCard/NFTCard.tsx | 8 ++++---- packages/extension/src/utils/format.test.ts | 14 +++++++------- packages/extension/src/utils/format.ts | 17 +++++++---------- 3 files changed, 18 insertions(+), 21 deletions(-) diff --git a/packages/extension/src/components/molecules/NFTCard/NFTCard.tsx b/packages/extension/src/components/molecules/NFTCard/NFTCard.tsx index a421c0030..6b843a640 100644 --- a/packages/extension/src/components/molecules/NFTCard/NFTCard.tsx +++ b/packages/extension/src/components/molecules/NFTCard/NFTCard.tsx @@ -10,7 +10,7 @@ import { convertHexToString } from 'xrpl'; import { AccountNFToken, NFTData } from '@gemwallet/constants'; import { useLedger } from '../../../contexts'; -import { formatStringToBeDisplayed } from '../../../utils'; +import { truncateStringOnLongWord } from '../../../utils'; import { GemWallet } from '../../atoms'; import { NFTDetails } from '../../organisms'; @@ -120,7 +120,7 @@ export const NFTCard: FC = ({ NFT }) => { }} onClick={() => handleTokenIdClick(NFTData.NFTokenID)} > - {formatStringToBeDisplayed(NFTData.NFTokenID, MAX_STRING_LENGTH)} + {truncateStringOnLongWord(NFTData.NFTokenID, MAX_STRING_LENGTH)} ) : null} @@ -129,12 +129,12 @@ export const NFTCard: FC = ({ NFT }) => { style={{ fontSize: '16px', color: 'white', marginTop: '10px' }} data-testid="nft_name" > - {formatStringToBeDisplayed(NFTData.name, MAX_STRING_LENGTH)} + {truncateStringOnLongWord(NFTData.name, MAX_STRING_LENGTH)} ) : null} {NFTData?.description ? (
- {formatStringToBeDisplayed(NFTData.description, MAX_STRING_LENGTH)} + {truncateStringOnLongWord(NFTData.description, MAX_STRING_LENGTH)}
) : null}