Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
3 changes: 2 additions & 1 deletion extension/e2e-tests/test-fixtures.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ export const test = base.extend<{
`--disable-extensions-except=${pathToExtension}`,
`--load-extension=${pathToExtension}`,
],
deviceScaleFactor: 1,
});

await use(context);
Expand Down Expand Up @@ -90,7 +91,7 @@ export const expectPageToHaveScreenshot = async (
options?: any,
) => {
await expect(page).toHaveScreenshot(screenshot, {
maxDiffPixelRatio: threshold || 0.02,
maxDiffPixelRatio: threshold || 0.05,
...options,
});
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,15 @@ import {
} from "@shared/constants/stellar";
import { ROUTES } from "popup/constants/routes";

// Mock functions for hidden collectibles
const mockRefreshHiddenCollectibles = jest.fn().mockResolvedValue(undefined);
const mockIsCollectibleHidden = jest.fn().mockReturnValue(false);

describe("AccountCollectibles", () => {
beforeEach(() => {
jest.clearAllMocks();
});

it("renders collectibles", async () => {
render(
<Wrapper
Expand Down Expand Up @@ -46,7 +54,11 @@ describe("AccountCollectibles", () => {
},
}}
>
<AccountCollectibles collections={mockCollectibles} />
<AccountCollectibles
collections={mockCollectibles}
refreshHiddenCollectibles={mockRefreshHiddenCollectibles}
isCollectibleHidden={mockIsCollectibleHidden}
/>
</Wrapper>,
);
await waitFor(() => screen.getByTestId("account-collectibles"));
Expand Down Expand Up @@ -156,7 +168,11 @@ describe("AccountCollectibles", () => {
},
}}
>
<AccountCollectibles collections={[]} />
<AccountCollectibles
collections={[]}
refreshHiddenCollectibles={mockRefreshHiddenCollectibles}
isCollectibleHidden={mockIsCollectibleHidden}
/>
</Wrapper>,
);
await waitFor(() => screen.getByTestId("account-collectibles"));
Expand Down Expand Up @@ -202,6 +218,8 @@ describe("AccountCollectibles", () => {
collections={[
{ error: { collectionAddress: "test", errorMessage: "test" } },
]}
refreshHiddenCollectibles={mockRefreshHiddenCollectibles}
isCollectibleHidden={mockIsCollectibleHidden}
/>
</Wrapper>,
);
Expand Down Expand Up @@ -317,7 +335,11 @@ describe("AccountCollectibles", () => {
},
}}
>
<AccountCollectibles collections={partialMockCollectibles} />
<AccountCollectibles
collections={partialMockCollectibles}
refreshHiddenCollectibles={mockRefreshHiddenCollectibles}
isCollectibleHidden={mockIsCollectibleHidden}
/>
</Wrapper>,
);
await waitFor(() => screen.getByTestId("account-collectibles"));
Expand Down
159 changes: 107 additions & 52 deletions extension/src/popup/components/account/AccountCollectibles/index.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import React, { useState } from "react";
import { useTranslation } from "react-i18next";
import { useSelector } from "react-redux";
import { Icon } from "@stellar/design-system";

import { Collection } from "@shared/api/types/types";
Expand All @@ -10,17 +9,42 @@ import {
SheetContent,
SheetTitle,
} from "popup/basics/shadcn/Sheet";
import { publicKeySelector } from "popup/ducks/accountServices";
import { getUserCollections } from "popup/helpers/collectibles";

import { CollectibleDetail, SelectedCollectible } from "../CollectibleDetail";
import { CollectibleInfoImage } from "../CollectibleInfo";

import "./styles.scss";
import { CollectibleInfoImage } from "../CollectibleInfo";

const CollectionsList = ({ collections }: { collections: Collection[] }) => {
const [selectedCollectible, setSelectedCollectible] =
useState<SelectedCollectible | null>(null);
const CollectionsList = ({
collections,
showHidden,
isCollectibleHidden,
onCloseCollectible,
}: {
collections: Collection[];
showHidden: boolean;
isCollectibleHidden: (collectionAddress: string, tokenId: string) => boolean;
onCloseCollectible: () => void;
}) => {
const [isDetailOpen, setIsDetailOpen] = useState(false);
const [detailData, setDetailData] = useState<SelectedCollectible | null>(
null,
);

const handleOpenCollectible = (collectible: SelectedCollectible) => {
setDetailData(collectible);
setIsDetailOpen(true);
};

const handleCloseCollectible = () => {
setIsDetailOpen(false);
onCloseCollectible();
};

const handleAnimationEnd = () => {
if (!isDetailOpen) {
setDetailData(null);
}
};

return (
<>
Expand All @@ -30,7 +54,20 @@ const CollectionsList = ({ collections }: { collections: Collection[] }) => {
return null;
}

// render the collection we do have
// filter collectibles based on showHidden toggle
const collectiblesToShow = showHidden
? collection.collectibles.filter((item) =>
isCollectibleHidden(collection.address, item.tokenId),
)
: collection.collectibles.filter(
(item) => !isCollectibleHidden(collection.address, item.tokenId),
);

// if no collectibles to show, don't render the collection
if (collectiblesToShow.length === 0) {
return null;
}

return (
<div
className="AccountCollectibles__collection"
Expand All @@ -49,79 +86,97 @@ const CollectionsList = ({ collections }: { collections: Collection[] }) => {
className="AccountCollectibles__collection__header__count"
data-testid="account-collection-count"
>
{collection.collectibles.length}
{collectiblesToShow.length}
</div>
</div>
<div
className="AccountCollectibles__collection__grid"
data-testid="account-collection-grid"
>
{collection.collectibles.map((item) => (
<Sheet
open={selectedCollectible?.tokenId === item.tokenId}
{collectiblesToShow.map((item) => (
<div
className={`AccountCollectibles__collection__grid__item${
showHidden
? " AccountCollectibles__collection__grid__item--hidden"
: ""
}`}
onClick={() =>
handleOpenCollectible({
collectionAddress: collection.address,
tokenId: item.tokenId,
})
}
key={item.tokenId}
>
<div
className="AccountCollectibles__collection__grid__item"
onClick={() =>
setSelectedCollectible({
collectionAddress: collection.address,
tokenId: item.tokenId,
})
}
>
<CollectibleInfoImage
image={item.metadata?.image}
name={item.tokenId}
/>
</div>
<SheetContent
aria-describedby={undefined}
side="bottom"
className="AccountCollectibles__collectible-detail__sheet"
onOpenAutoFocus={(e) => e.preventDefault()}
>
<ScreenReaderOnly>
<SheetTitle>{item.tokenId}</SheetTitle>
</ScreenReaderOnly>
<CollectibleDetail
selectedCollectible={{
collectionAddress: collection.address,
tokenId: item.tokenId,
}}
handleItemClose={() => setSelectedCollectible(null)}
/>
</SheetContent>
</Sheet>
<CollectibleInfoImage
image={item.metadata?.image}
name={item.tokenId}
/>
</div>
))}
</div>
</div>
);
})}

{/* Sheet rendered outside the map to persist during close animation */}
<Sheet
open={isDetailOpen}
onOpenChange={(open) => {
if (!open) {
handleCloseCollectible();
}
}}
>
<SheetContent
aria-describedby={undefined}
side="bottom"
className="AccountCollectibles__collectible-detail__sheet"
onOpenAutoFocus={(e) => e.preventDefault()}
onAnimationEnd={handleAnimationEnd}
>
<ScreenReaderOnly>
<SheetTitle>{detailData?.tokenId || ""}</SheetTitle>
</ScreenReaderOnly>
{detailData && (
<CollectibleDetail
selectedCollectible={detailData}
handleItemClose={handleCloseCollectible}
isHidden={showHidden}
/>
)}
</SheetContent>
</Sheet>
</>
);
};

interface AccountCollectiblesProps {
collections: Collection[];
refreshHiddenCollectibles: () => Promise<void>;
isCollectibleHidden: (collectionAddress: string, tokenId: string) => boolean;
onClickCollectible?: (selectedCollectible: SelectedCollectible) => void;
}

export const AccountCollectibles = ({
collections,
refreshHiddenCollectibles,
isCollectibleHidden,
}: AccountCollectiblesProps) => {
const { t } = useTranslation();
const publicKey = useSelector(publicKeySelector);

const userCollections = getUserCollections({
collections,
publicKey,
});
// Check if there are any valid collections with collectibles
const hasValidCollections = collections.some((c) => c.collection && !c.error);

return (
<div className="AccountCollectibles" data-testid="account-collectibles">
{userCollections.length ? (
<CollectionsList collections={userCollections} />
{hasValidCollections ? (
<CollectionsList
collections={collections}
showHidden={false}
isCollectibleHidden={isCollectibleHidden}
onCloseCollectible={refreshHiddenCollectibles}
/>
) : (
<div className="AccountCollectibles__empty">
<Icon.Grid01 />
Expand Down
44 changes: 34 additions & 10 deletions extension/src/popup/components/account/CollectibleDetail/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ import { settingsNetworkDetailsSelector } from "popup/ducks/settings";
import { publicKeySelector } from "popup/ducks/accountServices";
import { collectionsSelector } from "popup/ducks/cache";
import { ROUTES } from "popup/constants/routes";
import { changeCollectibleVisibility } from "@shared/api/internal";
import { AssetVisibility } from "@shared/api/types/types";

import { useCollectibleDetail } from "./hooks/useCollectibleDetail";
import {
Expand All @@ -37,7 +39,7 @@ export interface SelectedCollectible {
export const CollectibleDetail = ({
selectedCollectible,
handleItemClose,
isHidden,
isHidden = false,
}: {
selectedCollectible: SelectedCollectible;
handleItemClose: () => void;
Expand Down Expand Up @@ -97,6 +99,19 @@ export const CollectibleDetail = ({
navigateTo(ROUTES.sendPayment, navigate, queryParams);
};

const handleToggleCollectibleVisibility = async () => {
const collectibleKey = `${selectedCollectible.collectionAddress}:${selectedCollectible.tokenId}`;
await changeCollectibleVisibility({
collectibleKey,
collectibleVisibility: isHidden
? "visible"
: ("hidden" as AssetVisibility),
activePublicKey: publicKey || "",
});
setIsPopoverOpen(false);
handleItemClose();
};

return (
<div className="CollectibleDetail" data-testid="CollectibleDetail">
<View>
Expand Down Expand Up @@ -143,12 +158,19 @@ export const CollectibleDetail = ({
{t("View on stellar.expert")}
</div>
</div>
{/* <div className="CollectibleDetail__header__right-button__popover-content__item">
<Icon.EyeOff className="CollectibleDetail__header__right-button__popover-content__item__icon" />
<div
className="CollectibleDetail__header__right-button__popover-content__item"
onClick={handleToggleCollectibleVisibility}
>
{isHidden ? (
<Icon.Eye className="CollectibleDetail__header__right-button__popover-content__item__icon" />
) : (
<Icon.EyeOff className="CollectibleDetail__header__right-button__popover-content__item__icon" />
)}
<div className="CollectibleDetail__header__right-button__popover-content__item__label">
{t("Hide collectible")}
{isHidden ? t("Show collectible") : t("Hide collectible")}
</div>
</div> */}
</div>
</PopoverContent>
</div>
</Popover>
Expand All @@ -158,11 +180,6 @@ export const CollectibleDetail = ({
<Loading />
) : (
<View.Content>
{isHidden && (
<Notification variant="primary" title="">
{t("This collectible is hidden")}
</Notification>
)}
<div className="CollectibleDetail__content">
<CollectibleInfo
name={name}
Expand All @@ -171,6 +188,13 @@ export const CollectibleDetail = ({
image={collectible.metadata?.image}
dataTestIdBase="CollectibleDetail"
/>
{isHidden && (
<Notification
variant="warning"
icon={<Icon.EyeOff />}
title={t("This collectible is hidden")}
/>
)}
<CollectibleDescription
description={collectible.metadata?.description || ""}
dataTestIdBase="CollectibleDetail"
Expand Down
Loading