diff --git a/.github/workflows/web.unit.yml b/.github/workflows/web.unit.yml index 5e3dc059b6..ad169d54a2 100644 --- a/.github/workflows/web.unit.yml +++ b/.github/workflows/web.unit.yml @@ -12,5 +12,7 @@ jobs: node-version: 16.18.1 cache: 'yarn' - run: 'yarn install --immutable' - - name: Run unit tests + - name: Run web unit tests run: node_modules/.bin/moon run web:codegen && node_modules/.bin/moon run web:test + - name: Run shared package tests + run: node_modules/.bin/moon run shared:relay-codegen && node_modules/.bin/moon run shared:test diff --git a/apps/mobile/src/screens/NftDetailScreen/NftAdditionalDetailsEth.tsx b/apps/mobile/src/screens/NftDetailScreen/NftAdditionalDetailsEth.tsx index 6a19f43a0c..f488d681ae 100644 --- a/apps/mobile/src/screens/NftDetailScreen/NftAdditionalDetailsEth.tsx +++ b/apps/mobile/src/screens/NftDetailScreen/NftAdditionalDetailsEth.tsx @@ -4,6 +4,7 @@ import { useFragment } from 'react-relay'; import { graphql } from 'relay-runtime'; import { NftAdditionalDetailsEthFragment$key } from '~/generated/NftAdditionalDetailsEthFragment.graphql'; +import { extractMirrorXyzUrl } from '~/shared/utils/extractMirrorXyzUrl'; import { getOpenseaExternalUrl, hexHandler } from '~/shared/utils/getOpenseaExternalUrl'; import { EnsOrAddress } from '../../components/EnsOrAddress'; @@ -24,6 +25,7 @@ export function NftAdditionalDetailsEth({ tokenRef, showDetails }: NftAdditional tokenId chain + tokenMetadata contract { creatorAddress { @@ -42,15 +44,23 @@ export function NftAdditionalDetailsEth({ tokenRef, showDetails }: NftAdditional tokenRef ); - const { tokenId, contract, externalUrl } = token; + const { tokenId, contract, externalUrl, chain, tokenMetadata } = token; const openSeaExternalUrl = useMemo(() => { - if (contract?.contractAddress?.address && tokenId) { - return getOpenseaExternalUrl(contract.contractAddress.address, tokenId); + if (chain && contract?.contractAddress?.address && tokenId) { + return getOpenseaExternalUrl(chain, contract.contractAddress.address, tokenId); } return null; - }, [contract?.contractAddress?.address, tokenId]); + }, [chain, contract?.contractAddress?.address, tokenId]); + + const mirrorXyzUrl = useMemo(() => { + if (tokenMetadata) { + return extractMirrorXyzUrl(tokenMetadata); + } + + return null; + }, [tokenMetadata]); return ( @@ -91,6 +101,14 @@ export function NftAdditionalDetailsEth({ tokenRef, showDetails }: NftAdditional )} + {mirrorXyzUrl && ( + + + View on Mirror + + + )} + {openSeaExternalUrl && ( diff --git a/apps/web/src/components/CommunityHolderGrid/CommunityHolderGridItem.tsx b/apps/web/src/components/CommunityHolderGrid/CommunityHolderGridItem.tsx index 4e40cce594..fbff31c13e 100644 --- a/apps/web/src/components/CommunityHolderGrid/CommunityHolderGridItem.tsx +++ b/apps/web/src/components/CommunityHolderGrid/CommunityHolderGridItem.tsx @@ -30,6 +30,7 @@ export default function CommunityHolderGridItem({ holderRef, queryRef }: Props) fragment CommunityHolderGridItemFragment on Token { name tokenId + chain contract { contractAddress { address @@ -64,7 +65,7 @@ export default function CommunityHolderGridItem({ holderRef, queryRef }: Props) const { showModal } = useModalActions(); - const { tokenId, contract, owner } = token; + const { tokenId, contract, owner, chain } = token; const usernameWithFallback = owner ? graphqlTruncateUniversalUsername(owner) : null; @@ -81,12 +82,12 @@ export default function CommunityHolderGridItem({ holderRef, queryRef }: Props) } const openSeaExternalUrl = useMemo(() => { - if (contract?.contractAddress?.address && tokenId) { - return getOpenseaExternalUrl(contract.contractAddress.address, tokenId); + if (chain && contract?.contractAddress?.address && tokenId) { + return getOpenseaExternalUrl(chain, contract.contractAddress.address, tokenId); } return ''; - }, [contract?.contractAddress?.address, tokenId]); + }, [chain, contract?.contractAddress?.address, tokenId]); const handleClick = useCallback(() => { if (owner?.universal) { diff --git a/apps/web/src/scenes/NftDetailPage/NftAdditionalDetails/NftAdditionalDetailsEth.tsx b/apps/web/src/scenes/NftDetailPage/NftAdditionalDetails/NftAdditionalDetailsEth.tsx index 41bea8337f..03ce6fdcc7 100644 --- a/apps/web/src/scenes/NftDetailPage/NftAdditionalDetails/NftAdditionalDetailsEth.tsx +++ b/apps/web/src/scenes/NftDetailPage/NftAdditionalDetails/NftAdditionalDetailsEth.tsx @@ -9,6 +9,7 @@ import { EnsOrAddress } from '~/components/EnsOrAddress'; import { LinkableAddress } from '~/components/LinkableAddress'; import { NftAdditionalDetailsEthFragment$key } from '~/generated/NftAdditionalDetailsEthFragment.graphql'; import { useRefreshMetadata } from '~/scenes/NftDetailPage/NftAdditionalDetails/useRefreshMetadata'; +import { extractMirrorXyzUrl } from '~/shared/utils/extractMirrorXyzUrl'; import { getOpenseaExternalUrl, hexHandler } from '~/shared/utils/getOpenseaExternalUrl'; type NftAdditionaDetailsNonPOAPProps = { @@ -22,6 +23,8 @@ export function NftAdditionalDetailsEth({ tokenRef }: NftAdditionaDetailsNonPOAP fragment NftAdditionalDetailsEthFragment on Token { externalUrl tokenId + chain + tokenMetadata contract { creatorAddress { address @@ -40,17 +43,25 @@ export function NftAdditionalDetailsEth({ tokenRef }: NftAdditionaDetailsNonPOAP tokenRef ); - const { tokenId, contract, externalUrl } = token; + const { tokenId, contract, externalUrl, tokenMetadata, chain } = token; const [refresh, isRefreshing] = useRefreshMetadata(token); const openSeaExternalUrl = useMemo(() => { - if (contract?.contractAddress?.address && tokenId) { - return getOpenseaExternalUrl(contract.contractAddress.address, tokenId); + if (chain && contract?.contractAddress?.address && tokenId) { + return getOpenseaExternalUrl(chain, contract.contractAddress.address, tokenId); } return null; - }, [contract?.contractAddress?.address, tokenId]); + }, [chain, contract?.contractAddress?.address, tokenId]); + + const mirrorXyzUrl = useMemo(() => { + if (tokenMetadata) { + return extractMirrorXyzUrl(tokenMetadata); + } + + return null; + }, [tokenMetadata]); return ( @@ -76,6 +87,7 @@ export function NftAdditionalDetailsEth({ tokenRef }: NftAdditionaDetailsNonPOAP )} + {mirrorXyzUrl && View on Mirror} {openSeaExternalUrl && ( <> View on OpenSea diff --git a/apps/web/src/scenes/NftDetailPage/NftDetailText.tsx b/apps/web/src/scenes/NftDetailPage/NftDetailText.tsx index fe82ca19ae..8bb6161a25 100644 --- a/apps/web/src/scenes/NftDetailPage/NftDetailText.tsx +++ b/apps/web/src/scenes/NftDetailPage/NftDetailText.tsx @@ -88,12 +88,12 @@ function NftDetailText({ tokenRef, queryRef }: Props) { const horizontalLayout = breakpoint === size.desktop || breakpoint === size.tablet; const openseaExternalUrl = useMemo(() => { - if (token.contract?.contractAddress?.address && token.tokenId) { - getOpenseaExternalUrl(token.contract.contractAddress.address, token.tokenId); + if (token.chain && token.contract?.contractAddress?.address && token.tokenId) { + getOpenseaExternalUrl(token.chain, token.contract.contractAddress.address, token.tokenId); } return ''; - }, [token.contract?.contractAddress?.address, token.tokenId]); + }, [token.chain, token.contract?.contractAddress?.address, token.tokenId]); const handleBuyNowClick = useCallback(() => { track('Buy Now Button Click', { diff --git a/packages/shared/src/utils/extractMirrorXyz.test.tsx b/packages/shared/src/utils/extractMirrorXyz.test.tsx new file mode 100644 index 0000000000..102225494e --- /dev/null +++ b/packages/shared/src/utils/extractMirrorXyz.test.tsx @@ -0,0 +1,18 @@ +import { extractMirrorXyzUrl } from './extractMirrorXyzUrl'; + +function generateData(description: string) { + return `{"animation_url":"https://mirror.xyz/10/0x9fe9f4b985234ff185ecddb980e7567262b71b6f/render","description":"${description}","external_url":"","format":"png","image_url":"ipfs://QmPj5kFNhugP1V7s6yMRJtKSQUagMb2iDcNsKXc2miJpdy","media_type":"png","name":"When the sun hits 3"}`; +} + +describe('extractMirrorXyzUrl', () => { + test('extracts correctly', () => { + expect(extractMirrorXyzUrl(generateData('https://mirror.xyz/test/post'))).toEqual( + 'https://mirror.xyz/test/post' + ); + expect(extractMirrorXyzUrl(generateData('https://mirror.xyz/'))).toEqual('https://mirror.xyz/'); + expect(extractMirrorXyzUrl(generateData('https://mirror'))).toEqual(''); + expect(extractMirrorXyzUrl(generateData('https://mirror.xzy'))).toEqual(''); + expect(extractMirrorXyzUrl(generateData('https://mirror.xyz/posts/235 is the link'))).toEqual(''); + expect(extractMirrorXyzUrl(generateData('mirror.xyz'))).toEqual(''); + }); +}); diff --git a/packages/shared/src/utils/extractMirrorXyzUrl.ts b/packages/shared/src/utils/extractMirrorXyzUrl.ts new file mode 100644 index 0000000000..2878b0970f --- /dev/null +++ b/packages/shared/src/utils/extractMirrorXyzUrl.ts @@ -0,0 +1,10 @@ +export const extractMirrorXyzUrl = (tokenMetadata: string) => { + const metadataObj = JSON.parse(tokenMetadata); + const tokenDesc = metadataObj.description; + const startsWithMirrorXYZ = tokenDesc?.startsWith('https://mirror.xyz'); + const hasWhitespaceInMiddle = /\s/.test(tokenDesc); + if (startsWithMirrorXYZ && !hasWhitespaceInMiddle) { + return tokenDesc; + } + return ''; +}; diff --git a/packages/shared/src/utils/getOpenseaExternalUrl.ts b/packages/shared/src/utils/getOpenseaExternalUrl.ts index e636fd256c..f7f65adb8f 100644 --- a/packages/shared/src/utils/getOpenseaExternalUrl.ts +++ b/packages/shared/src/utils/getOpenseaExternalUrl.ts @@ -12,11 +12,16 @@ export const hexHandler = (str: string) => { return d; }; -export const getOpenseaExternalUrl = (contractAddress: string, tokenId: string) => { +export const getOpenseaExternalUrl = ( + chainStr: string, + contractAddress: string, + tokenId: string +) => { + const chain = chainStr.toLocaleLowerCase(); const hexTokenId = hexHandler(tokenId); // Allows us to get referral credit const ref = GALLERY_OS_ADDRESS; - return `https://opensea.io/assets/ethereum/${contractAddress}/${hexTokenId}?ref=${ref}`; + return `https://opensea.io/assets/${chain}/${contractAddress}/${hexTokenId}?ref=${ref}`; };