diff --git a/__tests__/components/home/LatestDropNextMintSubscribe.test.tsx b/__tests__/components/home/LatestDropNextMintSubscribe.test.tsx index 2d0553e19c..58f65e8d0f 100644 --- a/__tests__/components/home/LatestDropNextMintSubscribe.test.tsx +++ b/__tests__/components/home/LatestDropNextMintSubscribe.test.tsx @@ -7,16 +7,21 @@ jest.mock("@tanstack/react-query", () => ({ useQuery: jest.fn(), })); +jest.mock("@/components/cookies/CookieConsentContext", () => ({ + useCookieConsent: () => ({ country: "US" }), +})); + jest.mock( "@/components/user/subscriptions/MemeSubscriptionRow", () => function MockMemeSubscriptionRow(props: any) { return (
- token:{props.subscription.token_id} eligibility:{props.eligibilityCount} + token:{props.subscription.token_id} eligibility: + {props.eligibilityCount} minting_today:{String(props.minting_today)} readonly: - {String(props.readonly)} variant:{props.variant ?? "default"} date: - {String(props.date)} + {String(props.readonly)} balance:{props.balanceLabel} variant: + {props.variant ?? "default"} date:{String(props.date)}
); } @@ -86,6 +91,9 @@ describe("LatestDropNextMintSubscribe", () => { expect(screen.getByTestId("meme-subscription-row")).toHaveTextContent( /readonly:false/ ); + expect(screen.getByTestId("meme-subscription-row")).toHaveTextContent( + /balance:0/ + ); expect(screen.getByTestId("meme-subscription-row")).toHaveTextContent( /variant:compact/ ); @@ -124,27 +132,92 @@ describe("LatestDropNextMintSubscribe", () => { ); }); - it("does not render when there is no connected profile", () => { - const { container } = renderWithAuth( - , - { connectedProfile: null } + it("falls back to zero balance for non-finite details", () => { + useQueryMock.mockImplementation(({ queryKey }) => { + if (queryKey[0] === "next-mint-subscription-details") { + return { + data: { + balance: Number.NaN, + subscription_eligibility_count: 3, + }, + }; + } + + if (queryKey[0] === "next-mint-subscription-status") { + return { + data: { + subscribed: true, + eligibility: 2, + count: 1, + }, + refetch: jest.fn(), + }; + } + + return { + data: null, + refetch: jest.fn(), + }; + }); + + renderWithAuth(); + + expect(screen.getByTestId("meme-subscription-row")).toHaveTextContent( + "balance:0" ); + }); + + it("does not render when there is no connected profile", () => { + const { container } = renderWithAuth(, { + connectedProfile: null, + }); expect(container).toBeEmptyDOMElement(); }); it("does not render during an active proxy session", () => { - const { container } = renderWithAuth( - , - { - activeProfileProxy: { - id: "proxy-1", - granted_to: {} as any, - created_at: Date.now(), - created_by: {} as any, - actions: [], - } as any, + const { container } = renderWithAuth(, { + activeProfileProxy: { + id: "proxy-1", + granted_to: {} as any, + created_at: Date.now(), + created_by: {} as any, + actions: [], + } as any, + }); + + expect(container).toBeEmptyDOMElement(); + }); + + it("does not render in latest-drop mode when the profile is not subscribed", () => { + useQueryMock.mockImplementation(({ queryKey }) => { + if (queryKey[0] === "next-mint-subscription-details") { + return { + data: { + subscription_eligibility_count: 3, + }, + }; } + + if (queryKey[0] === "next-mint-subscription-status") { + return { + data: { + subscribed: false, + eligibility: 2, + count: 1, + }, + refetch: jest.fn(), + }; + } + + return { + data: null, + refetch: jest.fn(), + }; + }); + + const { container } = renderWithAuth( + ); expect(container).toBeEmptyDOMElement(); diff --git a/__tests__/components/home/NowMintingDetails.test.tsx b/__tests__/components/home/NowMintingDetails.test.tsx index 56c808dac0..ed124dd517 100644 --- a/__tests__/components/home/NowMintingDetails.test.tsx +++ b/__tests__/components/home/NowMintingDetails.test.tsx @@ -20,6 +20,11 @@ jest.mock("@/components/home/now-minting/NowMintingStatsGrid", () => ({ default: () =>
, })); +jest.mock("@/components/home/now-minting/LatestDropNextMintSubscribe", () => ({ + __esModule: true, + default: () =>
, +})); + jest.mock("@/components/home/now-minting/NowMintingCountdown", () => ({ __esModule: true, default: () =>
, @@ -53,6 +58,7 @@ describe("NowMintingDetails", () => { expect(screen.queryByText("Dimensions")).not.toBeInTheDocument(); expect(screen.getByText("Collection")).toBeInTheDocument(); expect(screen.getByText("Season")).toBeInTheDocument(); + expect(screen.getByTestId("subscribe-section")).toBeInTheDocument(); }); it("renders file metadata rows when image metadata is present", () => { diff --git a/__tests__/components/user/subscriptions/MemeSubscriptionRow.test.tsx b/__tests__/components/user/subscriptions/MemeSubscriptionRow.test.tsx new file mode 100644 index 0000000000..ff31e23182 --- /dev/null +++ b/__tests__/components/user/subscriptions/MemeSubscriptionRow.test.tsx @@ -0,0 +1,149 @@ +import { renderWithAuth } from "@/__tests__/utils/testContexts"; +import MemeSubscriptionRow from "@/components/user/subscriptions/MemeSubscriptionRow"; +import { useQuery } from "@tanstack/react-query"; +import { screen } from "@testing-library/react"; + +jest.mock("@tanstack/react-query", () => ({ + useQuery: jest.fn(), + useQueryClient: () => ({ + invalidateQueries: jest.fn(), + }), +})); + +jest.mock("@/services/api/common-api", () => ({ + commonApiFetch: jest.fn(), + commonApiPost: jest.fn(), +})); + +const useQueryMock = useQuery as jest.Mock; + +describe("MemeSubscriptionRow", () => { + beforeEach(() => { + useQueryMock.mockImplementation(({ queryKey }) => { + if (queryKey[0] === "consolidation-final-subscription") { + return { + data: { + phase: "Phase 1", + phase_position: 4, + phase_subscriptions: 12, + airdrop_address: "0xabc123", + subscribed_count: 2, + }, + }; + } + + return { + data: null, + }; + }); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + it("shows the subscribed compact view without toggle controls", () => { + renderWithAuth( + + ); + + expect(screen.getByText("Subscribed")).toBeInTheDocument(); + expect(screen.getByText("2 / 3")).toBeInTheDocument(); + expect( + screen.queryByLabelText(/Toggle subscription for The Memes #478/) + ).not.toBeInTheDocument(); + expect( + screen.queryByLabelText(/Select subscription quantity for The Memes/) + ).not.toBeInTheDocument(); + expect( + screen.getByText(/Phase: Phase 1 - Subscription Position: 4 \/ 12/) + ).toBeInTheDocument(); + }); + + it("renders the compact balance label", () => { + renderWithAuth( + + ); + + expect(screen.getByText("Balance")).toBeInTheDocument(); + expect(screen.getByText("0.5")).toBeInTheDocument(); + }); + + it("omits phase metadata when the final subscription is unavailable", () => { + useQueryMock.mockImplementation(({ queryKey }) => { + if (queryKey[0] === "consolidation-final-subscription") { + return { data: null }; + } + + return { + data: null, + }; + }); + + renderWithAuth( + + ); + + expect(screen.queryByText(/Phase:/)).not.toBeInTheDocument(); + }); +}); diff --git a/components/home/now-minting/LatestDropNextMintSubscribe.tsx b/components/home/now-minting/LatestDropNextMintSubscribe.tsx index 632c50257d..da6fde33b3 100644 --- a/components/home/now-minting/LatestDropNextMintSubscribe.tsx +++ b/components/home/now-minting/LatestDropNextMintSubscribe.tsx @@ -27,7 +27,12 @@ function getProfileKey( ); } -export default function LatestDropNextMintSubscribe() { +export default function LatestDropNextMintSubscribe( + props: Readonly<{ + showOnlyWhenSubscribed?: boolean; + readonly?: boolean; + }> = {} +) { const { connectedProfile, activeProfileProxy } = useContext(AuthContext); const { country } = useCookieConsent(); const { isIos } = useCapacitor(); @@ -53,17 +58,15 @@ export default function LatestDropNextMintSubscribe() { enabled: !hideSubscriptions && !!profileKey, }); - const { - data: status, - refetch: refetchStatus, - } = useQuery({ - queryKey: ["next-mint-subscription-status", profileKey, tokenId], - queryFn: async () => - await commonApiFetch({ - endpoint: `subscriptions/consolidation/upcoming-memes/${tokenId}/${profileKey}`, - }), - enabled: !hideSubscriptions && !!profileKey && hasTokenId, - }); + const { data: status, refetch: refetchStatus } = + useQuery({ + queryKey: ["next-mint-subscription-status", profileKey, tokenId], + queryFn: async () => + await commonApiFetch({ + endpoint: `subscriptions/consolidation/upcoming-memes/${tokenId}/${profileKey}`, + }), + enabled: !hideSubscriptions && !!profileKey && hasTokenId, + }); const subscription = useMemo(() => { if (!profileKey || !hasTokenId || !status) { @@ -79,7 +82,20 @@ export default function LatestDropNextMintSubscribe() { } as NFTSubscription; }, [hasTokenId, profileKey, status, tokenId]); - if (hideSubscriptions || !profileKey || !subscription) { + const balanceLabel = useMemo(() => { + const balance = details?.balance ?? 0; + const safeBalance = Number.isFinite(balance) ? balance : 0; + return new Intl.NumberFormat(undefined, { + maximumFractionDigits: 6, + }).format(Math.round(safeBalance * 1_000_000) / 1_000_000); + }, [details?.balance]); + + if ( + hideSubscriptions || + !profileKey || + !subscription || + (props.showOnlyWhenSubscribed && !subscription.subscribed) + ) { return null; } @@ -93,7 +109,8 @@ export default function LatestDropNextMintSubscribe() { eligibilityCount={ details?.subscription_eligibility_count ?? status?.eligibility ?? 1 } - readonly={false} + balanceLabel={balanceLabel} + readonly={props.readonly ?? false} refresh={() => { refetchStatus(); }} diff --git a/components/home/now-minting/NowMintingDetails.tsx b/components/home/now-minting/NowMintingDetails.tsx index a630bbad3f..7a58a2c108 100644 --- a/components/home/now-minting/NowMintingDetails.tsx +++ b/components/home/now-minting/NowMintingDetails.tsx @@ -10,6 +10,7 @@ import NowMintingCountdown from "./NowMintingCountdown"; import NowMintingDetailsAccordion from "./NowMintingDetailsAccordion"; import NowMintingHeader from "./NowMintingHeader"; import NowMintingStatsGrid from "./NowMintingStatsGrid"; +import LatestDropNextMintSubscribe from "./LatestDropNextMintSubscribe"; interface NowMintingDetailsProps { readonly nft: NFTWithMemesExtendedData; @@ -34,6 +35,7 @@ export default function NowMintingDetails({ nft }: NowMintingDetailsProps) { mediaMimeType={fileMimeType} /> + void; variant?: "default" | "compact"; + balanceLabel?: string; + subscribedView?: boolean; }> ) { const id = `subscription-${props.subscription.token_id}`; @@ -263,31 +266,53 @@ export default function MemeSubscriptionRow( }; if (isCompact) { + const isSubscribedView = !!props.subscribedView; return (
- Subscribe -
- {isSubmitting && } - - - {renderCountSelector({ - selectClassName: - "tw-rounded tw-border tw-border-iron-400 tw-bg-transparent tw-px-1 tw-text-iron-400", - disableWhenSingleOption: false, - })} + + + {isSubscribedView ? "Subscribed" : "Subscribe"} -
+ {props.balanceLabel && ( + + Balance + + {props.balanceLabel} + + + + + + )} + + {isSubscribedView ? ( + + {subscribedCount} / {props.eligibilityCount} + + ) : ( +
+ {isSubmitting && } + + + {renderCountSelector({ + selectClassName: + "tw-rounded tw-border tw-border-iron-400 tw-bg-transparent tw-px-1 tw-text-iron-400", + disableWhenSingleOption: false, + })} + +
+ )}
{finalWithMetadata && ( -
+
Phase: {finalWithMetadata.phase} - Subscription Position:{" "} {finalWithMetadata.phasePosition.toLocaleString()} /{" "} {finalWithMetadata.phaseSubscriptions.toLocaleString()} - Airdrop @@ -324,7 +349,7 @@ export default function MemeSubscriptionRow( -
+
{props.title} #{props.subscription.token_id}{" "} @@ -368,8 +393,8 @@ export default function MemeSubscriptionRow( {finalWithMetadata.phasePosition.toLocaleString()} /{" "} {finalWithMetadata.phaseSubscriptions.toLocaleString()} - Airdrop Address:{" "} - {formatAddress(finalWithMetadata.airdropAddress)} - - Subscription Count: x{finalWithMetadata.subscribedCount} + {formatAddress(finalWithMetadata.airdropAddress)} - Subscription + Count: x{finalWithMetadata.subscribedCount} )}
@@ -383,7 +408,7 @@ export default function MemeSubscriptionRow( onChange={submit} aria-label={`Toggle subscription for ${props.title} #${props.subscription.token_id}`} /> - + {renderCountSelector({ selectClassName: "tw-text-iron-400 tw-bg-transparent tw-border tw-border-iron-400 tw-rounded tw-px-1",