diff --git a/.changeset/dry-otters-shop.md b/.changeset/dry-otters-shop.md new file mode 100644 index 000000000000..187563b2688a --- /dev/null +++ b/.changeset/dry-otters-shop.md @@ -0,0 +1,6 @@ +--- +"ledger-live-desktop": patch +"@ledgerhq/live-nft": patch +--- + +Add detail drawers for inscriptions of ordinals protocol diff --git a/.changeset/rare-yaks-deny.md b/.changeset/rare-yaks-deny.md new file mode 100644 index 000000000000..e5c8877cb941 --- /dev/null +++ b/.changeset/rare-yaks-deny.md @@ -0,0 +1,8 @@ +--- +"@ledgerhq/coin-near": patch +"ledger-live-desktop": patch +"live-mobile": patch +"@ledgerhq/live-common": patch +--- + +remove rewards feature from near diff --git a/.changeset/six-hairs-raise.md b/.changeset/six-hairs-raise.md new file mode 100644 index 000000000000..4ad083d7a294 --- /dev/null +++ b/.changeset/six-hairs-raise.md @@ -0,0 +1,5 @@ +--- +"live-mobile": patch +--- + +Run tests files at `e2e/specs/*.spec.ts` diff --git a/.github/workflows/build-and-test-external.yml b/.github/workflows/build-and-test-external.yml index af5ed98ceefc..96769045a1fd 100644 --- a/.github/workflows/build-and-test-external.yml +++ b/.github/workflows/build-and-test-external.yml @@ -14,7 +14,7 @@ permissions: jobs: determine-affected: name: "Turbo Affected" - if: ${{contains(needs.determine-affected.outputs.paths, 'ledger-live-desktop') && github.event.pull_request.head.repo.full_name != github.repository }} + if: ${{github.event.pull_request.head.repo.full_name != github.repository }} uses: LedgerHQ/ledger-live/.github/workflows/turbo-affected-reusable.yml@develop with: head_branch: ${{ github.event.pull_request.head.ref || github.event.merge_group.head_ref }} @@ -26,18 +26,30 @@ jobs: needs: determine-affected if: ${{contains(needs.determine-affected.outputs.paths, 'ledger-live-desktop') && github.event.pull_request.head.repo.full_name != github.repository }} uses: LedgerHQ/ledger-live/.github/workflows/build-desktop-external-reusable.yml@develop + with: + ref: ${{ github.event.pull_request.head.ref }} + repository: ${{ github.event.pull_request.head.repo.full_name }} + secrets: inherit test-desktop-external: name: "[External] Test Desktop" needs: determine-affected if: ${{contains(needs.determine-affected.outputs.paths, 'ledger-live-desktop') && github.event.pull_request.head.repo.full_name != github.repository }} uses: LedgerHQ/ledger-live/.github/workflows/test-desktop-external-reusable.yml@develop + with: + ref: ${{ github.event.pull_request.head.ref }} + repository: ${{ github.event.pull_request.head.repo.full_name }} + secrets: inherit build-mobile-external: name: "[External] Build Mobile" needs: determine-affected if: ${{contains(needs.determine-affected.outputs.paths, 'ledger-live-mobile') && github.event.pull_request.head.repo.full_name != github.repository}} uses: LedgerHQ/ledger-live/.github/workflows/build-mobile-external-reusable.yml@develop + with: + ref: ${{ github.event.pull_request.head.ref }} + repository: ${{ github.event.pull_request.head.repo.full_name }} + secrets: inherit # Final Check required ok: diff --git a/.github/workflows/build-and-test-pr.yml b/.github/workflows/build-and-test-pr.yml index cf2b38919b9e..37c55935c2e0 100644 --- a/.github/workflows/build-and-test-pr.yml +++ b/.github/workflows/build-and-test-pr.yml @@ -14,6 +14,7 @@ permissions: jobs: determine-affected: name: "Turbo Affected" + if: ${{github.event.pull_request.head.repo.full_name == github.repository }} uses: LedgerHQ/ledger-live/.github/workflows/turbo-affected-reusable.yml@develop with: head_branch: ${{ github.event.pull_request.head.ref || github.event.merge_group.head_ref }} diff --git a/.github/workflows/build-desktop-external-reusable.yml b/.github/workflows/build-desktop-external-reusable.yml index e22f9a170bca..67bc572856df 100644 --- a/.github/workflows/build-desktop-external-reusable.yml +++ b/.github/workflows/build-desktop-external-reusable.yml @@ -2,20 +2,18 @@ name: "@Desktop • Build App (external)" on: workflow_call: - workflow_dispatch: inputs: ref: + type: string description: | If you run this manually, and want to run on a PR, the correct ref should be refs/pull/{PR_NUMBER}/merge to have the "normal" scenario involving checking out a merge commit between your branch and the base branch. If you want to run only on a branch or specific commit, you can use either the sha or the branch name instead (prefer the first verion for PRs). - required: false - login: - description: The GitHub username that triggered the workflow - required: false - base_ref: - description: The base branch to merge the head into when checking out the code - required: false + required: true + repository: + description: The repository to checkout the code from + type: string + required: true jobs: build-desktop-app: @@ -46,7 +44,9 @@ jobs: } - uses: actions/checkout@v4 with: - ref: ${{ github.ref_name }} + ref: ${{ inputs.ref }} + repository: ${{ inputs.repository }} + token: ${{ secrets.GITHUB_TOKEN }} persist-credentials: false - name: Setup git user uses: LedgerHQ/ledger-live/tools/actions/composites/setup-git-user@develop diff --git a/.github/workflows/build-mobile-external-reusable.yml b/.github/workflows/build-mobile-external-reusable.yml index 30b9dc52fd37..fd729a3cae3e 100644 --- a/.github/workflows/build-mobile-external-reusable.yml +++ b/.github/workflows/build-mobile-external-reusable.yml @@ -2,23 +2,18 @@ name: "@Mobile • Build App (external)" on: workflow_call: - workflow_dispatch: inputs: ref: + type: string description: | If you run this manually, and want to run on a PR, the correct ref should be refs/pull/{PR_NUMBER}/merge to have the "normal" scenario involving checking out a merge commit between your branch and the base branch. If you want to run only on a branch or specific commit, you can use either the sha or the branch name instead (prefer the first verion for PRs). - login: - description: The GitHub username that triggered the workflow - required: false - base_ref: - description: The base branch to merge the head into when checking out the code - required: false - -concurrency: - group: ${{ github.workflow }}-${{ github.ref_name != 'develop' && github.ref || github.run_id }} - cancel-in-progress: true + required: true + repository: + description: The repository to checkout the code from + type: string + required: true permissions: contents: read @@ -26,13 +21,15 @@ permissions: jobs: build-mobile-app-android: name: "Build Ledger Live Mobile (Android)" - runs-on: ubuntu-latest + runs-on: ubuntu-22.04 env: NODE_OPTIONS: "--max-old-space-size=7168" steps: - uses: actions/checkout@v4 with: - ref: ${{ github.ref_name }} + ref: ${{ inputs.ref }} + repository: ${{ inputs.repository }} + token: ${{ secrets.GITHUB_TOKEN }} persist-credentials: false - name: Setup git user uses: LedgerHQ/ledger-live/tools/actions/composites/setup-git-user@develop @@ -92,7 +89,9 @@ jobs: steps: - uses: actions/checkout@v4 with: - ref: ${{ github.ref_name }} + ref: ${{ inputs.ref }} + repository: ${{ inputs.repository }} + token: ${{ secrets.GITHUB_TOKEN }} persist-credentials: false - name: Setup git user uses: LedgerHQ/ledger-live/tools/actions/composites/setup-git-user@develop @@ -105,7 +104,7 @@ jobs: run: pnpm i --filter="live-mobile..." --filter="ledger-live" --no-frozen-lockfile --unsafe-perm report: - runs-on: ubuntu-latest + runs-on: ubuntu-22.04 needs: [build-mobile-app-android, build-mobile-app-ios] if: ${{ !cancelled() && github.event_name == 'workflow_dispatch' }} steps: diff --git a/.github/workflows/test-desktop-external-reusable.yml b/.github/workflows/test-desktop-external-reusable.yml index 3069cf438fd1..d39b21a02ecb 100644 --- a/.github/workflows/test-desktop-external-reusable.yml +++ b/.github/workflows/test-desktop-external-reusable.yml @@ -2,20 +2,18 @@ name: "@Desktop • Test App (external)" on: workflow_call: - workflow_dispatch: inputs: ref: + type: string description: | If you run this manually, and want to run on a PR, the correct ref should be refs/pull/{PR_NUMBER}/merge to have the "normal" scenario involving checking out a merge commit between your branch and the base branch. If you want to run only on a branch or specific commit, you can use either the sha or the branch name instead (prefer the first verion for PRs). - required: false - login: - description: The GitHub username that triggered the workflow - required: false - base_ref: - description: The base branch to merge the head into when checking out the code - required: false + required: true + repository: + description: The repository to checkout the code from + type: string + required: true permissions: id-token: write @@ -27,12 +25,14 @@ jobs: env: NODE_OPTIONS: "--max-old-space-size=7168" FORCE_COLOR: 3 - CI_OS: ubuntu-latest - runs-on: ubuntu-latest + CI_OS: ubuntu-22.04 + runs-on: ubuntu-22.04 steps: - uses: actions/checkout@v4 with: - ref: ${{ github.ref_name }} + ref: ${{ inputs.ref }} + repository: ${{ inputs.repository }} + token: ${{ secrets.GITHUB_TOKEN }} persist-credentials: false - name: Setup the toolchain uses: LedgerHQ/ledger-live/tools/actions/composites/setup-toolchain@develop @@ -63,12 +63,14 @@ jobs: env: NODE_OPTIONS: "--max-old-space-size=7168" FORCE_COLOR: 3 - CI_OS: ubuntu-latest - runs-on: ubuntu-latest + CI_OS: ubuntu-22.04 + runs-on: ubuntu-22.04 steps: - uses: actions/checkout@v4 with: - ref: ${{ github.ref_name }} + ref: ${{ inputs.ref }} + repository: ${{ inputs.repository }} + token: ${{ secrets.GITHUB_TOKEN }} persist-credentials: false - name: Setup the toolchain uses: LedgerHQ/ledger-live/tools/actions/composites/setup-toolchain@develop @@ -84,17 +86,19 @@ jobs: env: NODE_OPTIONS: "--max-old-space-size=7168" FORCE_COLOR: 3 - CI_OS: ubuntu-latest + CI_OS: ubuntu-22.04 PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: 1 # DEBUG: "pw:browser*" # DEBUG_LOGS: 1 - runs-on: ubuntu-latest + runs-on: ubuntu-22.04 outputs: status: ${{ steps.tests.outcome }} steps: - uses: actions/checkout@v4 with: - ref: ${{ github.ref_name }} + ref: ${{ inputs.ref }} + repository: ${{ inputs.repository }} + token: ${{ secrets.GITHUB_TOKEN }} persist-credentials: false - name: Setup the toolchain uses: LedgerHQ/ledger-live/tools/actions/composites/setup-toolchain@develop @@ -147,12 +151,14 @@ jobs: report: needs: [codechecks, unit-tests, e2e-tests-linux] - runs-on: ubuntu-latest + runs-on: ubuntu-22.04 if: ${{ !cancelled() && github.event_name == 'workflow_dispatch' }} steps: - uses: actions/checkout@v4 with: - ref: ${{ github.ref_name }} + ref: ${{ inputs.ref }} + repository: ${{ inputs.repository }} + token: ${{ secrets.GITHUB_TOKEN }} persist-credentials: false - name: "download linter results" uses: actions/download-artifact@v4 @@ -318,22 +324,3 @@ jobs: with: name: summary.json path: ${{ github.workspace }}/summary.json - - allure-report: - name: "Allure Reports Export on Server" - needs: [e2e-tests-linux] - runs-on: [ledger-live-medium] - if: ${{ !cancelled() && github.ref_name == 'develop' }} - steps: - - name: checkout - uses: actions/checkout@v4 - with: - ref: ${{ github.ref_name }} - - name: Send Results and Generate Allure Report - Linux - uses: LedgerHQ/ledger-live/tools/actions/composites/upload-allure-report@develop - if: ${{ !cancelled() }} - with: - platform: linux - login: ${{ vars.ALLURE_USERNAME }} - password: ${{ secrets.ALLURE_LEDGER_LIVE_PASSWORD }} - path: allure-results-linux diff --git a/apps/ledger-live-desktop/index-types.d.ts b/apps/ledger-live-desktop/index-types.d.ts index 64587364fbb3..d1e41cc52d9f 100644 --- a/apps/ledger-live-desktop/index-types.d.ts +++ b/apps/ledger-live-desktop/index-types.d.ts @@ -63,6 +63,7 @@ interface Window { getAllFeatureFlags: (appLanguage: string) => Partial<{ [key in FeatureId]: Feature }>; getAllEnvs: () => { [key in EnvName]: unknown }; + saveLogs: (path: string) => void; // for mocking purposes apparently? // eslint-disable-next-line diff --git a/apps/ledger-live-desktop/src/newArch/features/Collectibles/Ordinals/components/Inscriptions/DetailsDrawer/Actions.tsx b/apps/ledger-live-desktop/src/newArch/features/Collectibles/Ordinals/components/Inscriptions/DetailsDrawer/Actions.tsx new file mode 100644 index 000000000000..f81c2a28202b --- /dev/null +++ b/apps/ledger-live-desktop/src/newArch/features/Collectibles/Ordinals/components/Inscriptions/DetailsDrawer/Actions.tsx @@ -0,0 +1,32 @@ +import React from "react"; +import { Flex, Icons, IconsLegacy, Text } from "@ledgerhq/react-ui"; +import Button from "~/renderer/components/Button"; +import { useTranslation } from "react-i18next"; + +const Actions = () => { + const { t } = useTranslation(); + return ( + + + + + ); +}; +export default Actions; diff --git a/apps/ledger-live-desktop/src/newArch/features/Collectibles/Ordinals/components/Inscriptions/DetailsDrawer/SubTitle.tsx b/apps/ledger-live-desktop/src/newArch/features/Collectibles/Ordinals/components/Inscriptions/DetailsDrawer/SubTitle.tsx new file mode 100644 index 000000000000..21c43cae7fe2 --- /dev/null +++ b/apps/ledger-live-desktop/src/newArch/features/Collectibles/Ordinals/components/Inscriptions/DetailsDrawer/SubTitle.tsx @@ -0,0 +1,25 @@ +import React from "react"; +import { Flex, Text } from "@ledgerhq/react-ui"; +import IconContainer from "LLD/features/Collectibles/components/Collection/TableRow/IconContainer"; +import { IconProps } from "LLD/features/Collectibles/types/Collection"; +import { MappingKeys } from "LLD/features/Collectibles/types/Ordinals"; +import { useTranslation } from "react-i18next"; + +type Props = { + icons: (({ size, color, style }: IconProps) => JSX.Element)[]; + names: MappingKeys[]; +}; + +const SubTitle: React.FC = ({ icons, names }) => { + const { t } = useTranslation(); + return ( + + + + {t("ordinals.inscriptions.detailsDrawer.storedOnChain")} + + + ); +}; + +export default SubTitle; diff --git a/apps/ledger-live-desktop/src/newArch/features/Collectibles/Ordinals/components/Inscriptions/DetailsDrawer/index.tsx b/apps/ledger-live-desktop/src/newArch/features/Collectibles/Ordinals/components/Inscriptions/DetailsDrawer/index.tsx index eb7ff49015cf..abedd3a803ae 100644 --- a/apps/ledger-live-desktop/src/newArch/features/Collectibles/Ordinals/components/Inscriptions/DetailsDrawer/index.tsx +++ b/apps/ledger-live-desktop/src/newArch/features/Collectibles/Ordinals/components/Inscriptions/DetailsDrawer/index.tsx @@ -1,18 +1,66 @@ import React from "react"; -import { SideDrawer } from "~/renderer/components/SideDrawer"; import { SimpleHashNft } from "@ledgerhq/live-nft/api/types"; +import { DetailDrawer } from "LLD/features/Collectibles/components"; +import { CollectibleTypeEnum } from "LLD/features/Collectibles/types/enum/Collectibles"; +import useInscriptionDetailDrawer from "./useInscriptionDetailDrawer"; +import Actions from "./Actions"; +import SubTitle from "./SubTitle"; + +type ViewProps = ReturnType & { + onClose: () => void; +}; type Props = { inscription: SimpleHashNft; + correspondingRareSat: SimpleHashNft | null | undefined; + isLoading: boolean; onClose: () => void; }; -const InscriptionDetailsDrawer: React.FC = ({ inscription, onClose }) => { - // will be replaced by DetailsDrawer from collectibles + +const View: React.FC = ({ data, rareSat, onClose }) => ( + + {rareSat?.icons && ( + + + + )} + + + + +); + +const InscriptionDetailDrawer = ({ + inscription, + isLoading, + correspondingRareSat, + onClose, +}: Props) => { return ( - - {inscription.name || inscription.contract.name} - + ); }; -export default InscriptionDetailsDrawer; +export default InscriptionDetailDrawer; diff --git a/apps/ledger-live-desktop/src/newArch/features/Collectibles/Ordinals/components/Inscriptions/DetailsDrawer/useInscriptionDetailDrawer.ts b/apps/ledger-live-desktop/src/newArch/features/Collectibles/Ordinals/components/Inscriptions/DetailsDrawer/useInscriptionDetailDrawer.ts new file mode 100644 index 000000000000..c82cae275514 --- /dev/null +++ b/apps/ledger-live-desktop/src/newArch/features/Collectibles/Ordinals/components/Inscriptions/DetailsDrawer/useInscriptionDetailDrawer.ts @@ -0,0 +1,64 @@ +import { useMemo, useState } from "react"; +import { SimpleHashNft } from "@ledgerhq/live-nft/api/types"; +import useCollectibles from "LLD/features/Collectibles/hooks/useCollectibles"; +import { createDetails } from "LLD/features/Collectibles/utils/createInscriptionDetailsArrays"; +import { useCalendarFormatted } from "~/renderer/hooks/useDateFormatter"; +import { Tag } from "LLD/features/Collectibles/types/DetailDrawer"; +import { processRareSat } from "../helpers"; + +type Props = { + isLoading: boolean; + inscription: SimpleHashNft; + correspondingRareSat: SimpleHashNft | null | undefined; +}; + +const useInscriptionDetailDrawer = ({ isLoading, inscription, correspondingRareSat }: Props) => { + const [useFallback, setUseFallback] = useState(false); + const imageUri = + inscription.video_url || inscription.previews?.image_large_url || inscription.image_url; + + const isVideo = !!inscription.video_url; + + const contentType = isVideo ? "video" : imageUri ? "image" : ""; + + const { isPanAndZoomOpen, openCollectiblesPanAndZoom, closeCollectiblesPanAndZoom } = + useCollectibles(); + + const createdDateFromTimestamp = new Date(inscription.first_created?.timestamp || 0); + const formattedCreatedDate = useCalendarFormatted(createdDateFromTimestamp); + const createdDate = createdDateFromTimestamp === new Date(0) ? "" : formattedCreatedDate; + const details = createDetails(inscription, createdDate); + + const tags: Tag[] = + inscription.extra_metadata?.attributes?.map(attr => ({ + key: attr.trait_type, + value: attr.value, + })) || []; + + const rareSat = useMemo(() => { + if (correspondingRareSat) return processRareSat(correspondingRareSat); + }, [correspondingRareSat]); + + const data = { + areFieldsLoading: isLoading, + collectibleName: inscription.name || inscription.contract.name || "", + contentType, + collectionName: inscription.collection.name || "", + details: details, + previewUri: imageUri, + originalUri: imageUri, + isPanAndZoomOpen, + mediaType: inscription.video_properties?.mime_type || "image", + tags: tags, + useFallback: useFallback, + tokenId: inscription.nft_id, + isOpened: true, + closeCollectiblesPanAndZoom, + openCollectiblesPanAndZoom, + setUseFallback: setUseFallback, + }; + + return { inscription, data, rareSat }; +}; + +export default useInscriptionDetailDrawer; diff --git a/apps/ledger-live-desktop/src/newArch/features/Collectibles/Ordinals/components/Inscriptions/DiscoveryDrawer/ProtectBox.tsx b/apps/ledger-live-desktop/src/newArch/features/Collectibles/Ordinals/components/Inscriptions/DiscoveryDrawer/ProtectBox.tsx index 482a46ebcbff..8c428e9f8a18 100644 --- a/apps/ledger-live-desktop/src/newArch/features/Collectibles/Ordinals/components/Inscriptions/DiscoveryDrawer/ProtectBox.tsx +++ b/apps/ledger-live-desktop/src/newArch/features/Collectibles/Ordinals/components/Inscriptions/DiscoveryDrawer/ProtectBox.tsx @@ -4,9 +4,10 @@ import Switch from "~/renderer/components/Switch"; import { hasProtectedOrdinalsAssetsSelector } from "~/renderer/reducers/settings"; import { setHasProtectedOrdinalsAssets } from "~/renderer/actions/settings"; import { useDispatch, useSelector } from "react-redux"; -import { t } from "i18next"; +import { useTranslation } from "react-i18next"; const ProtectBox: React.FC = () => { + const { t } = useTranslation(); const dispatch = useDispatch(); const hasProtectedOrdinals = useSelector(hasProtectedOrdinalsAssetsSelector); diff --git a/apps/ledger-live-desktop/src/newArch/features/Collectibles/Ordinals/components/Inscriptions/Item.tsx b/apps/ledger-live-desktop/src/newArch/features/Collectibles/Ordinals/components/Inscriptions/Item.tsx deleted file mode 100644 index dc3f2ab8ffd8..000000000000 --- a/apps/ledger-live-desktop/src/newArch/features/Collectibles/Ordinals/components/Inscriptions/Item.tsx +++ /dev/null @@ -1,29 +0,0 @@ -import { InscriptionsItemProps } from "LLD/features/Collectibles/types/Inscriptions"; -import TableRow from "LLD/features/Collectibles/components/Collection/TableRow"; -import React from "react"; - -type ItemProps = { - isLoading: boolean; -} & InscriptionsItemProps; - -const Item: React.FC = ({ - isLoading, - tokenName, - collectionName, - tokenIcons, - media, - rareSatName, - onClick, -}) => ( - -); - -export default Item; diff --git a/apps/ledger-live-desktop/src/newArch/features/Collectibles/Ordinals/components/Inscriptions/Item/index.tsx b/apps/ledger-live-desktop/src/newArch/features/Collectibles/Ordinals/components/Inscriptions/Item/index.tsx new file mode 100644 index 000000000000..b9c404527346 --- /dev/null +++ b/apps/ledger-live-desktop/src/newArch/features/Collectibles/Ordinals/components/Inscriptions/Item/index.tsx @@ -0,0 +1,40 @@ +import React, { useMemo } from "react"; +import { InscriptionsItemProps } from "LLD/features/Collectibles/types/Inscriptions"; +import TableRow from "LLD/features/Collectibles/components/Collection/TableRow"; +import { GroupedNftOrdinals } from "@ledgerhq/live-nft-react/index"; +import { findCorrespondingSat } from "LLD/features/Collectibles/utils/findCorrespondingSat"; +import { processRareSat } from "../helpers"; + +type ItemProps = { + isLoading: boolean; + inscriptionsGroupedWithRareSats: GroupedNftOrdinals[]; +} & InscriptionsItemProps; + +const Item: React.FC = ({ + isLoading, + tokenName, + collectionName, + media, + nftId, + inscriptionsGroupedWithRareSats, + onClick, +}) => { + const correspondingRareSat = findCorrespondingSat(inscriptionsGroupedWithRareSats, nftId); + const rareSat = useMemo(() => { + if (correspondingRareSat) return processRareSat(correspondingRareSat.rareSat); + }, [correspondingRareSat]); + + return ( + + ); +}; + +export default Item; diff --git a/apps/ledger-live-desktop/src/newArch/features/Collectibles/Ordinals/components/Inscriptions/helpers.ts b/apps/ledger-live-desktop/src/newArch/features/Collectibles/Ordinals/components/Inscriptions/helpers.ts index 7ea111c9c9c9..695f0a214b1d 100644 --- a/apps/ledger-live-desktop/src/newArch/features/Collectibles/Ordinals/components/Inscriptions/helpers.ts +++ b/apps/ledger-live-desktop/src/newArch/features/Collectibles/Ordinals/components/Inscriptions/helpers.ts @@ -1,40 +1,14 @@ import { SimpleHashNft } from "@ledgerhq/live-nft/api/types"; -import { IconProps } from "LLD/features/Collectibles/types/Collection"; -import { mappingKeysWithIconAndName } from "../Icons"; -import { MappingKeys } from "LLD/features/Collectibles/types/Ordinals"; - -function matchCorrespondingIcon( - rareSats: SimpleHashNft[], -): Array JSX.Element> }> { - return rareSats.map(rareSat => { - const iconKeys: string[] = []; - const rarity = rareSat.extra_metadata?.ordinal_details?.sat_rarity?.toLowerCase(); - - if (rarity && rarity !== "common") { - iconKeys.push(rarity.replace(" ", "_")); - } - - const icons = iconKeys - .map( - iconKey => - mappingKeysWithIconAndName[iconKey as keyof typeof mappingKeysWithIconAndName]?.icon, - ) - .filter(Boolean) as Array<({ size, color, style }: IconProps) => JSX.Element>; - - return { ...rareSat, icons }; - }); -} +import { createRareSatObject, matchCorrespondingIcon } from "../helpers"; export function getInscriptionsData( inscriptions: SimpleHashNft[], onInscriptionClick: (inscription: SimpleHashNft) => void, ) { - const inscriptionsWithIcons = matchCorrespondingIcon(inscriptions); - return inscriptionsWithIcons.map(item => ({ + return inscriptions.map(item => ({ tokenName: item.name || item.contract.name || "", + nftId: item.nft_id, collectionName: item.collection.name, - tokenIcons: item.icons, - rareSatName: [item.extra_metadata?.ordinal_details?.sat_rarity] as MappingKeys[], media: { uri: item.image_url || item.previews?.image_small_url, isLoading: false, @@ -45,3 +19,9 @@ export function getInscriptionsData( onClick: () => onInscriptionClick(item), })); } + +export function processRareSat(inscription: SimpleHashNft) { + const matchedRareSatsIcons = matchCorrespondingIcon(inscription); + const rareSatObject = createRareSatObject({ rareSat: matchedRareSatsIcons }); + return rareSatObject.rareSat[0]; +} diff --git a/apps/ledger-live-desktop/src/newArch/features/Collectibles/Ordinals/components/Inscriptions/index.tsx b/apps/ledger-live-desktop/src/newArch/features/Collectibles/Ordinals/components/Inscriptions/index.tsx index 3e88f98b0e75..e6269cb4034b 100644 --- a/apps/ledger-live-desktop/src/newArch/features/Collectibles/Ordinals/components/Inscriptions/index.tsx +++ b/apps/ledger-live-desktop/src/newArch/features/Collectibles/Ordinals/components/Inscriptions/index.tsx @@ -10,14 +10,16 @@ import Loader from "../Loader"; import Error from "../Error"; import Item from "./Item"; import EmptyCollection from "LLD/features/Collectibles/components/Collection/EmptyCollection"; -import { CollectibleTypeEnum } from "../../../types/enum/Collectibles"; +import { CollectibleTypeEnum } from "LLD/features/Collectibles/types/enum/Collectibles"; import Button from "~/renderer/components/Button"; import { useTranslation } from "react-i18next"; +import { GroupedNftOrdinals } from "@ledgerhq/live-nft-react/index"; type ViewProps = ReturnType & { isLoading: boolean; isError: boolean; error: Error | null; + inscriptionsGroupedWithRareSats: GroupedNftOrdinals[]; onReceive: () => void; }; @@ -26,6 +28,7 @@ type Props = { isLoading: boolean; isError: boolean; error: Error | null; + inscriptionsGroupedWithRareSats: GroupedNftOrdinals[]; onReceive: () => void; onInscriptionClick: (inscription: SimpleHashNft) => void; }; @@ -36,6 +39,7 @@ const View: React.FC = ({ isError, inscriptions, error, + inscriptionsGroupedWithRareSats, onShowMore, onReceive, }) => { @@ -55,12 +59,8 @@ const View: React.FC = ({ ))} {nothingToShow && ( @@ -84,6 +84,7 @@ const Inscriptions: React.FC = ({ isLoading, isError, error, + inscriptionsGroupedWithRareSats, onReceive, onInscriptionClick, }) => ( @@ -92,6 +93,7 @@ const Inscriptions: React.FC = ({ isError={isError} error={error} onReceive={onReceive} + inscriptionsGroupedWithRareSats={inscriptionsGroupedWithRareSats} {...useInscriptionsModel({ inscriptions, onInscriptionClick })} /> ); diff --git a/apps/ledger-live-desktop/src/newArch/features/Collectibles/Ordinals/components/RareSats/helpers.ts b/apps/ledger-live-desktop/src/newArch/features/Collectibles/Ordinals/components/RareSats/helpers.ts deleted file mode 100644 index 9a7423bbe53b..000000000000 --- a/apps/ledger-live-desktop/src/newArch/features/Collectibles/Ordinals/components/RareSats/helpers.ts +++ /dev/null @@ -1,62 +0,0 @@ -import { mappingKeysWithIconAndName } from "../Icons"; -import { SimpleHashNft } from "@ledgerhq/live-nft/api/types"; -import { - SimpleHashNftWithIcons, - RareSat, - MappingKeys, -} from "LLD/features/Collectibles/types/Ordinals"; - -export function matchCorrespondingIcon(rareSats: SimpleHashNft[]): SimpleHashNftWithIcons[] { - return rareSats.map(rareSat => { - const iconKeys: string[] = []; - if (rareSat.name) { - iconKeys.push(rareSat.name.toLowerCase().replace(" ", "_")); - } - - if (rareSat.extra_metadata?.utxo_details?.satributes) { - iconKeys.push(...Object.keys(rareSat.extra_metadata.utxo_details.satributes)); - } - - const icons = iconKeys - .map( - iconKey => - mappingKeysWithIconAndName[iconKey as keyof typeof mappingKeysWithIconAndName]?.icon, - ) - .filter(Boolean) as RareSat["icons"]; - - return { ...rareSat, icons }; - }); -} - -export function createRareSatObject( - rareSats: Record, -): Record { - const result: Record = {}; - - for (const [key, value] of Object.entries(rareSats)) { - result[key] = value.map(rareSat => { - const { icons, extra_metadata } = rareSat; - const year = extra_metadata?.utxo_details?.sat_ranges?.[0]?.year || ""; - const displayed_names = - Object.values(extra_metadata?.utxo_details?.satributes || {}) - .map(attr => attr.display_name) - .join(" / ") || ""; - const names = rareSat.extra_metadata?.utxo_details?.satributes - ? (Object.keys(rareSat.extra_metadata.utxo_details.satributes) as MappingKeys[]) - : []; - const count = extra_metadata?.utxo_details?.value || 0; - const isMultipleRow = value.length > 1; - - return { - year, - displayed_names, - names, - count: `${count} ${count > 1 ? "sats" : "sat"}`, - isMultipleRow, - icons, - }; - }); - } - - return result; -} diff --git a/apps/ledger-live-desktop/src/newArch/features/Collectibles/Ordinals/components/RareSats/useRareSatsModel.ts b/apps/ledger-live-desktop/src/newArch/features/Collectibles/Ordinals/components/RareSats/useRareSatsModel.ts index a59b1eafc054..e73078536530 100644 --- a/apps/ledger-live-desktop/src/newArch/features/Collectibles/Ordinals/components/RareSats/useRareSatsModel.ts +++ b/apps/ledger-live-desktop/src/newArch/features/Collectibles/Ordinals/components/RareSats/useRareSatsModel.ts @@ -1,4 +1,4 @@ -import { matchCorrespondingIcon, createRareSatObject } from "./helpers"; +import { matchCorrespondingIcon, createRareSatObject } from "../helpers"; import { SimpleHashNft } from "@ledgerhq/live-nft/api/types"; import { regroupRareSatsByContractAddress } from "@ledgerhq/live-nft-react"; diff --git a/apps/ledger-live-desktop/src/newArch/features/Collectibles/Ordinals/components/helpers.ts b/apps/ledger-live-desktop/src/newArch/features/Collectibles/Ordinals/components/helpers.ts new file mode 100644 index 000000000000..c323b37e8bd9 --- /dev/null +++ b/apps/ledger-live-desktop/src/newArch/features/Collectibles/Ordinals/components/helpers.ts @@ -0,0 +1,78 @@ +import { mappingKeysWithIconAndName } from "./Icons"; +import { SimpleHashNft } from "@ledgerhq/live-nft/api/types"; +import { + SimpleHashNftWithIcons, + RareSat, + MappingKeys, +} from "LLD/features/Collectibles/types/Ordinals"; + +function processSingleRareSat(rareSat: SimpleHashNft): SimpleHashNftWithIcons { + const iconKeys: string[] = []; + if (rareSat.name) { + iconKeys.push(rareSat.name.toLowerCase().replace(" ", "_")); + } + + if (rareSat.extra_metadata?.utxo_details?.satributes) { + iconKeys.push(...Object.keys(rareSat.extra_metadata.utxo_details.satributes)); + } + + const icons = iconKeys + .map( + iconKey => + mappingKeysWithIconAndName[iconKey as keyof typeof mappingKeysWithIconAndName]?.icon, + ) + .filter(Boolean) as RareSat["icons"]; + + return { ...rareSat, icons }; +} + +function mapToRareSat(rareSat: SimpleHashNftWithIcons, isMultipleRow: boolean): RareSat { + const { icons, extra_metadata } = rareSat; + const year = extra_metadata?.utxo_details?.sat_ranges?.[0]?.year || ""; + const displayed_names = + Object.values(extra_metadata?.utxo_details?.satributes || {}) + .map(attr => attr.display_name) + .join(" / ") || ""; + const names = rareSat.extra_metadata?.utxo_details?.satributes + ? (Object.keys(rareSat.extra_metadata.utxo_details.satributes) as MappingKeys[]) + : []; + const count = extra_metadata?.utxo_details?.value || 0; + + return { + year, + displayed_names, + names, + count: `${count} ${count > 1 ? "sats" : "sat"}`, + isMultipleRow, + icons, + }; +} + +function processRareSatObject( + rareSats: Record, +): Record { + const result: Record = {}; + + for (const [key, value] of Object.entries(rareSats)) { + result[key] = value.map(rareSat => mapToRareSat(rareSat, value.length > 1)); + } + + return result; +} + +export function matchCorrespondingIcon( + rareSats: SimpleHashNft | SimpleHashNft[], +): SimpleHashNftWithIcons[] { + const rareSatArray = Array.isArray(rareSats) ? rareSats : [rareSats]; + return rareSatArray.map(processSingleRareSat); +} + +export function createRareSatObject( + rareSats: Record | SimpleHashNftWithIcons[], +): Record { + if (Array.isArray(rareSats)) { + return { default: rareSats.map(rareSat => mapToRareSat(rareSat, rareSats.length > 1)) }; + } else { + return processRareSatObject(rareSats); + } +} diff --git a/apps/ledger-live-desktop/src/newArch/features/Collectibles/Ordinals/screens/Account/index.tsx b/apps/ledger-live-desktop/src/newArch/features/Collectibles/Ordinals/screens/Account/index.tsx index f1683da348f9..cbec04e52909 100644 --- a/apps/ledger-live-desktop/src/newArch/features/Collectibles/Ordinals/screens/Account/index.tsx +++ b/apps/ledger-live-desktop/src/newArch/features/Collectibles/Ordinals/screens/Account/index.tsx @@ -19,6 +19,8 @@ const View: React.FC = ({ rareSats, isDrawerOpen, selectedInscription, + correspondingRareSat, + inscriptionsGroupedWithRareSats, handleDrawerClose, onReceive, onInscriptionClick, @@ -26,15 +28,21 @@ const View: React.FC = ({ }) => ( {selectedInscription && ( - + )} ); diff --git a/apps/ledger-live-desktop/src/newArch/features/Collectibles/Ordinals/screens/Account/useBitcoinAccountModel.ts b/apps/ledger-live-desktop/src/newArch/features/Collectibles/Ordinals/screens/Account/useBitcoinAccountModel.ts index b6bbe99d37d3..c1c8d0ad620b 100644 --- a/apps/ledger-live-desktop/src/newArch/features/Collectibles/Ordinals/screens/Account/useBitcoinAccountModel.ts +++ b/apps/ledger-live-desktop/src/newArch/features/Collectibles/Ordinals/screens/Account/useBitcoinAccountModel.ts @@ -6,6 +6,7 @@ import { useDispatch, useSelector } from "react-redux"; import { openModal } from "~/renderer/actions/modals"; import { setHasSeenOrdinalsDiscoveryDrawer } from "~/renderer/actions/settings"; import { hasSeenOrdinalsDiscoveryDrawerSelector } from "~/renderer/reducers/settings"; +import { findCorrespondingSat } from "LLD/features/Collectibles/utils/findCorrespondingSat"; interface Props { account: BitcoinAccount; @@ -15,8 +16,13 @@ export const useBitcoinAccountModel = ({ account }: Props) => { const dispatch = useDispatch(); const hasSeenDiscoveryDrawer = useSelector(hasSeenOrdinalsDiscoveryDrawerSelector); const [selectedInscription, setSelectedInscription] = useState(null); + const [correspondingRareSat, setCorrespondingRareSat] = useState< + SimpleHashNft | null | undefined + >(null); - const { rareSats, inscriptions, ...rest } = useFetchOrdinals({ account }); + const { rareSats, inscriptions, inscriptionsGroupedWithRareSats, ...rest } = useFetchOrdinals({ + account, + }); const [isDrawerOpen, setIsDrawerOpen] = useState(!hasSeenDiscoveryDrawer); @@ -40,7 +46,11 @@ export const useBitcoinAccountModel = ({ account }: Props) => { ); }, [dispatch, account]); - const onInscriptionClick = (inscription: SimpleHashNft) => setSelectedInscription(inscription); + const onInscriptionClick = (inscription: SimpleHashNft) => { + const groupedNft = findCorrespondingSat(inscriptionsGroupedWithRareSats, inscription.nft_id); + setCorrespondingRareSat(groupedNft?.rareSat ?? null); + setSelectedInscription(inscription); + }; const onDetailsDrawerClose = () => setSelectedInscription(null); @@ -50,6 +60,8 @@ export const useBitcoinAccountModel = ({ account }: Props) => { rest, isDrawerOpen, selectedInscription, + correspondingRareSat, + inscriptionsGroupedWithRareSats, onReceive, handleDrawerClose, onInscriptionClick, diff --git a/apps/ledger-live-desktop/src/newArch/features/Collectibles/__integration__/bitcoinPage.test.tsx b/apps/ledger-live-desktop/src/newArch/features/Collectibles/__integration__/bitcoinPage.test.tsx index e1046b592629..66797fa60c8e 100644 --- a/apps/ledger-live-desktop/src/newArch/features/Collectibles/__integration__/bitcoinPage.test.tsx +++ b/apps/ledger-live-desktop/src/newArch/features/Collectibles/__integration__/bitcoinPage.test.tsx @@ -18,24 +18,24 @@ jest.mock("~/renderer/linking", () => ({ describe("displayBitcoinPage", () => { it("should display Bitcoin page with rare sats and inscriptions", async () => { - const { user } = render(); + const { user } = render(, { + initialState: { + settings: { + hasSeenOrdinalsDiscoveryDrawer: true, + }, + }, + }); - await waitFor(() => expect(screen.getByText(/inscription #63691311/i)).toBeVisible()); - await waitFor(() => expect(screen.getByTestId(/raresaticon-palindrome-0/i)).toBeVisible()); - await user.hover(screen.getByTestId(/raresaticon-palindrome-0/i)); - await waitFor(() => expect(screen.getByText(/in a playful twist/i)).toBeVisible()); - await waitFor(() => - expect( - screen.getByText(/block 9 \/ first transaction \/ nakamoto \/ vintage/i), - ).toBeVisible(), - ); + await waitFor(() => expect(screen.getByText(/the great war #3695/i)).toBeVisible()); await waitFor(() => expect(screen.getByText(/see more inscriptions/i)).toBeVisible()); await user.click(screen.getByText(/see more inscriptions/i)); - await waitFor(() => expect(screen.getByText(/timechain #136/i)).toBeVisible()); - await waitFor(() => expect(screen.getByTestId(/raresaticon-jpeg-0/i)).toBeVisible()); - await user.hover(screen.getByTestId(/raresaticon-jpeg-0/i)); - await waitFor(() => expect(screen.getByText(/journey into the past with jpeg/i)).toBeVisible()); + await user.click(screen.getByText(/see more inscriptions/i)); + await waitFor(() => expect(screen.getByText(/bitcoin puppet #71/i)).toBeVisible()); + await waitFor(() => expect(screen.queryAllByTestId(/raresaticon-pizza-0/i)).toHaveLength(2)); + await user.hover(screen.queryAllByTestId(/raresaticon-pizza-0/i)[0]); + await waitFor(() => expect(screen.getByText(/papa john's pizza/i)).toBeVisible()); }); + it("should open discovery drawer when it is the first time feature is activated", async () => { const { user } = render(); @@ -43,4 +43,20 @@ describe("displayBitcoinPage", () => { await user.click(screen.getByText(/learn more/i)); expect(openURL).toHaveBeenCalledWith("https://www.ledger.com/academy/bitcoin-ordinals"); }); + + it("should open inscription detail drawer", async () => { + const { user } = render(, { + initialState: { + settings: { + hasSeenOrdinalsDiscoveryDrawer: true, + }, + }, + }); + + await waitFor(() => expect(screen.getByText(/the great war #3695/i)).toBeVisible()); + await user.click(screen.getByText(/the great war #3695/i)); + await expect(screen.getByText(/hide/i)).toBeVisible(); + // sat name + await expect(screen.getByText(/dlngbapxjdv/i)).toBeVisible(); + }); }); diff --git a/apps/ledger-live-desktop/src/newArch/features/Collectibles/components/Collection/HeaderActions.tsx b/apps/ledger-live-desktop/src/newArch/features/Collectibles/components/Collection/HeaderActions.tsx index 5a3074491d6d..8f993d1b3d24 100644 --- a/apps/ledger-live-desktop/src/newArch/features/Collectibles/components/Collection/HeaderActions.tsx +++ b/apps/ledger-live-desktop/src/newArch/features/Collectibles/components/Collection/HeaderActions.tsx @@ -1,7 +1,7 @@ -import Button from "~/renderer/components/Button"; -import { t } from "i18next"; import React from "react"; +import Button from "~/renderer/components/Button"; import Box from "~/renderer/components/Box"; +import { useTranslation } from "react-i18next"; type Props = { children?: JSX.Element; @@ -9,6 +9,7 @@ type Props = { }; const HeaderActions: React.FC = ({ children, textKey }) => { + const { t } = useTranslation(); return (