diff --git a/__tests__/components/distribution-plan-tool/ReviewDistributionPlanTableSubscription.test.tsx b/__tests__/components/distribution-plan-tool/ReviewDistributionPlanTableSubscription.test.tsx index 207c5b0f3c..ed4f099b21 100644 --- a/__tests__/components/distribution-plan-tool/ReviewDistributionPlanTableSubscription.test.tsx +++ b/__tests__/components/distribution-plan-tool/ReviewDistributionPlanTableSubscription.test.tsx @@ -6,26 +6,9 @@ jest.mock("@/services/api/common-api", () => ({ import * as ReviewDistributionPlanTableSubscriptionModule from "@/components/distribution-plan-tool/review-distribution-plan/table/ReviewDistributionPlanTableSubscription"; import { ApiIdentity } from "@/generated/models/ApiIdentity"; -const { download, isSubscriptionsAdmin, SubscriptionConfirm } = +const { download, isSubscriptionsAdmin } = ReviewDistributionPlanTableSubscriptionModule; -jest.mock("react-bootstrap", () => ({ - __esModule: true, - Modal: Object.assign( - ({ show, children }: any) => (show ?
{children}
: null), - { - Header: ({ children }: any) =>
{children}
, - Title: ({ children }: any) =>
{children}
, - Body: ({ children }: any) =>
{children}
, - Footer: ({ children }: any) =>
{children}
, - } - ), - Button: (p: any) => , - Col: (p: any) =>
{p.children}
, - Container: (p: any) =>
{p.children}
, - Row: (p: any) =>
{p.children}
, -})); - describe("ReviewDistributionPlanTableSubscription utilities", () => { it("checks subscriptions admin wallets", () => { const profile: Partial = { wallets: [{ wallet: "0xabc" }] }; @@ -70,50 +53,6 @@ jest.mock("@/components/distribution-plan-tool/common/CircleLoader", () => ({ default: () =>
Loading...
, })); -test("SubscriptionConfirm extracts token id from plan name", () => { - render( - - ); - const input = screen.getByRole("spinbutton") as HTMLInputElement; - expect(input.value).toBe("123"); -}); - -test("SubscriptionConfirm displays token id as text when confirmedTokenId is provided", () => { - render( - - ); - expect(screen.queryByRole("spinbutton")).not.toBeInTheDocument(); - expect(screen.getByText("456")).toBeInTheDocument(); -}); - -test("SubscriptionConfirm shows input when confirmedTokenId is not provided", () => { - render( - - ); - const input = screen.getByRole("spinbutton") as HTMLInputElement; - expect(input).toBeInTheDocument(); - expect(input.value).toBe("123"); -}); - describe("SubscriptionLinks", () => { const mockSetToast = jest.fn(); const mockAuthCtx = { @@ -124,7 +63,11 @@ describe("SubscriptionLinks", () => { }; const mockPlan = { id: "plan1", name: "Meme 123 drop" } as any; - const mockDistCtx = { + const mockDistCtxWithTokenId = { + confirmedTokenId: "123", + } as any; + + const mockDistCtxNoTokenId = { confirmedTokenId: null, } as any; @@ -146,7 +89,7 @@ describe("SubscriptionLinks", () => { jest.restoreAllMocks(); }); - it("renders Public Subscriptions button for public phase", () => { + it("renders Public Subscriptions button for public phase when token is confirmed", () => { const publicPhase = { id: "phase1", name: "public", @@ -155,7 +98,7 @@ describe("SubscriptionLinks", () => { } as any; render( - + @@ -165,8 +108,7 @@ describe("SubscriptionLinks", () => { expect(screen.getByText("Public Subscriptions")).toBeInTheDocument(); }); - it("shows confirm modal when Public Subscriptions button is clicked", async () => { - const user = userEvent.setup(); + it("does not render when token is not confirmed", () => { const publicPhase = { id: "phase1", name: "public", @@ -174,24 +116,18 @@ describe("SubscriptionLinks", () => { phaseId: PUBLIC_SUBSCRIPTIONS_PHASE_ID, } as any; - render( - + const { container } = render( + ); - await user.click(screen.getByText("Public Subscriptions")); - - await waitFor(() => { - expect( - screen.getByText("Confirm Download Public Subscriptions Info") - ).toBeInTheDocument(); - }); + expect(container.innerHTML).toBe(""); }); - it("calls download and shows toast when Public Subscriptions is confirmed", async () => { + it("calls download directly when button is clicked", async () => { const user = userEvent.setup(); jest @@ -215,7 +151,7 @@ describe("SubscriptionLinks", () => { } as any; render( - + @@ -224,14 +160,6 @@ describe("SubscriptionLinks", () => { await user.click(screen.getByText("Public Subscriptions")); - await waitFor(() => { - expect( - screen.getByText("Confirm Download Public Subscriptions Info") - ).toBeInTheDocument(); - }); - - await user.click(screen.getByText("Looks good")); - await waitFor(() => { expect(mockSetToast).toHaveBeenCalledWith({ type: "success", @@ -256,7 +184,7 @@ describe("SubscriptionLinks", () => { } as any; const { container } = render( - + @@ -266,11 +194,27 @@ describe("SubscriptionLinks", () => { expect(container.innerHTML).toBe(""); }); - it("displays token id as text when confirmedTokenId is provided in context", async () => { + it("shows loading state during download", async () => { const user = userEvent.setup(); - const distCtxWithTokenId = { - confirmedTokenId: "789", - } as any; + + jest + .spyOn( + ReviewDistributionPlanTableSubscriptionModule, + "isSubscriptionsAdmin" + ) + .mockReturnValue(true); + + let resolveDownload: (value: { success: boolean; message: string }) => void; + const downloadPromise = new Promise<{ success: boolean; message: string }>( + (resolve) => { + resolveDownload = resolve; + } + ); + + jest + .spyOn(ReviewDistributionPlanTableSubscriptionModule, "download") + .mockReturnValue(downloadPromise); + const publicPhase = { id: "phase1", name: "public", @@ -279,7 +223,7 @@ describe("SubscriptionLinks", () => { } as any; render( - + @@ -289,12 +233,13 @@ describe("SubscriptionLinks", () => { await user.click(screen.getByText("Public Subscriptions")); await waitFor(() => { - expect( - screen.getByText("Confirm Download Public Subscriptions Info") - ).toBeInTheDocument(); + expect(screen.getByText("Downloading")).toBeInTheDocument(); }); - expect(screen.queryByRole("spinbutton")).not.toBeInTheDocument(); - expect(screen.getByText("789")).toBeInTheDocument(); + resolveDownload!({ success: true, message: "Done" }); + + await waitFor(() => { + expect(screen.getByText("Public Subscriptions")).toBeInTheDocument(); + }); }); }); diff --git a/__tests__/components/distribution-plan-tool/ReviewDistributionPlanTableSubscriptionFooter.test.tsx b/__tests__/components/distribution-plan-tool/ReviewDistributionPlanTableSubscriptionFooter.test.tsx index 44ae12ce50..e66d8dff0d 100644 --- a/__tests__/components/distribution-plan-tool/ReviewDistributionPlanTableSubscriptionFooter.test.tsx +++ b/__tests__/components/distribution-plan-tool/ReviewDistributionPlanTableSubscriptionFooter.test.tsx @@ -8,13 +8,6 @@ import { useMemo, useState } from "react"; jest.mock( "@/components/distribution-plan-tool/review-distribution-plan/table/ReviewDistributionPlanTableSubscription", () => ({ - SubscriptionConfirm: (props: any) => - props.show ? ( - @@ -138,7 +131,7 @@ test("displays contract and token id after confirmation", async () => { }); }); -test("shows confirm modals when buttons clicked", async () => { +test("shows upload modals when upload buttons clicked", async () => { const user = userEvent.setup(); (isSubscriptionsAdmin as jest.Mock).mockReturnValue(true); render(); @@ -155,14 +148,7 @@ test("shows confirm modals when buttons clicked", async () => { screen.getByTestId("Upload Distribution Photos") ).toBeInTheDocument(); }); - await user.click(screen.getByText("Reset Subscriptions")); - await waitFor(() => { - expect(screen.getByTestId("Reset Subscriptions")).toBeInTheDocument(); - }); - await user.click(screen.getByText("Finalize Distribution")); - await waitFor(() => { - expect(screen.getByTestId("Finalize Distribution")).toBeInTheDocument(); - }); + await user.click(screen.getByText("Upload Automatic Airdrops")); await waitFor(() => { expect(screen.getByTestId("Automatic Airdrops")).toBeInTheDocument(); @@ -187,7 +173,7 @@ test("change token id button opens confirm modal", async () => { }); }); -test("resetSubscriptions posts data and shows toast", async () => { +test("resetSubscriptions posts data and shows toast directly", async () => { const user = userEvent.setup(); (isSubscriptionsAdmin as jest.Mock).mockReturnValue(true); const commonApiPost = jest.fn().mockResolvedValue({}); @@ -208,13 +194,54 @@ test("resetSubscriptions posts data and shows toast", async () => { }); await user.click(screen.getByText("Reset Subscriptions")); + + await waitFor(() => { + expect(commonApiPost).toHaveBeenCalledWith( + expect.objectContaining({ + endpoint: expect.stringContaining("/reset"), + }) + ); + expect(authCtx.setToast).toHaveBeenCalledWith({ + type: "success", + message: "Subscriptions reset successfully.", + }); + }); +}); + +test("finalizeDistribution posts data and shows toast directly", async () => { + const user = userEvent.setup(); + (isSubscriptionsAdmin as jest.Mock).mockReturnValue(true); + const commonApiPost = jest + .fn() + .mockResolvedValue({ success: true, message: "Normalized" }); + jest + .spyOn(require("@/services/api/common-api"), "commonApiPost") + .mockImplementation(commonApiPost); + + jest + .spyOn(require("@/services/api/common-api"), "commonApiFetch") + .mockResolvedValue({ photos_count: 0, is_normalized: false }); + + render(); + + await user.click(screen.getByTestId("confirm-token-id-button")); + await waitFor(() => { - expect(screen.getByTestId("Reset Subscriptions")).toBeInTheDocument(); + expect(screen.getByText("Finalize Distribution")).toBeInTheDocument(); }); - await user.click(screen.getByTestId("Reset Subscriptions")); + + await user.click(screen.getByText("Finalize Distribution")); + await waitFor(() => { - expect(commonApiPost).toHaveBeenCalled(); - expect(authCtx.setToast).toHaveBeenCalled(); + expect(commonApiPost).toHaveBeenCalledWith( + expect.objectContaining({ + endpoint: expect.stringContaining("/normalize"), + }) + ); + expect(authCtx.setToast).toHaveBeenCalledWith({ + type: "success", + message: "Normalized", + }); }); }); diff --git a/__tests__/components/distribution-plan-tool/ReviewDistributionPlanTableSubscriptionFooterConfirmTokenId.test.tsx b/__tests__/components/distribution-plan-tool/ReviewDistributionPlanTableSubscriptionFooterConfirmTokenId.test.tsx index 9436d02b0d..6b54093828 100644 --- a/__tests__/components/distribution-plan-tool/ReviewDistributionPlanTableSubscriptionFooterConfirmTokenId.test.tsx +++ b/__tests__/components/distribution-plan-tool/ReviewDistributionPlanTableSubscriptionFooterConfirmTokenId.test.tsx @@ -50,7 +50,7 @@ describe("ConfirmTokenIdModal", () => { ); const confirmButton = screen.getByText("Confirm"); await user.click(confirmButton); - expect(mockOnConfirm).toHaveBeenCalledWith(expect.any(String), "123"); + expect(mockOnConfirm).toHaveBeenCalledWith("123"); }); it("calls onConfirm when Enter key is pressed with valid token id", async () => { @@ -65,7 +65,7 @@ describe("ConfirmTokenIdModal", () => { const input = screen.getByRole("spinbutton"); input.focus(); await user.keyboard("{Enter}"); - expect(mockOnConfirm).toHaveBeenCalledWith(expect.any(String), "123"); + expect(mockOnConfirm).toHaveBeenCalledWith("123"); }); it("does not call onConfirm when Enter key is pressed with invalid token id", async () => { diff --git a/__tests__/components/distribution/Distribution.test.tsx b/__tests__/components/distribution/Distribution.test.tsx index 7b981e84d2..178f28a128 100644 --- a/__tests__/components/distribution/Distribution.test.tsx +++ b/__tests__/components/distribution/Distribution.test.tsx @@ -11,6 +11,19 @@ jest.mock("@/components/not-found/NotFound", () => ({ ), })); +const mockSetTitle = jest.fn(); +jest.mock("@/contexts/TitleContext", () => ({ + __esModule: true, + useTitle: () => ({ + title: "Test Title", + setTitle: mockSetTitle, + notificationCount: 0, + setNotificationCount: jest.fn(), + setWaveData: jest.fn(), + setStreamHasNewItems: jest.fn(), + }), +})); + // Mock useParams to return different values for different tests const mockUseParams = jest.fn(() => ({ id: "123" })); jest.mock("next/navigation", () => ({ @@ -756,4 +769,42 @@ describe("DistributionPage", () => { }); }); }); + + describe("Title Management", () => { + it("sets title when valid nft id is present", async () => { + mockUseParams.mockReturnValue({ id: "123" }); + mockFetchAllPages.mockResolvedValue([]); + mockFetchUrl.mockResolvedValue({ count: 0, data: [] }); + + render( + + ); + + await waitFor(() => { + expect(mockSetTitle).toHaveBeenCalledWith( + "Test Collection #123 | DISTRIBUTION" + ); + }); + }); + + it("does not set title when nft id is invalid", () => { + mockUseParams.mockReturnValue({ id: "invalid" }); + mockFetchAllPages.mockResolvedValue([]); + mockFetchUrl.mockResolvedValue({ count: 0, data: [] }); + + render(); + + expect(mockSetTitle).not.toHaveBeenCalled(); + }); + + it("does not set title when nft id is missing", () => { + mockUseParams.mockReturnValue({ id: "" }); + mockFetchAllPages.mockResolvedValue([]); + mockFetchUrl.mockResolvedValue({ count: 0, data: [] }); + + render(); + + expect(mockSetTitle).not.toHaveBeenCalled(); + }); + }); }); diff --git a/components/distribution-plan-tool/review-distribution-plan/table/ReviewDistributionPlanTableSubscription.tsx b/components/distribution-plan-tool/review-distribution-plan/table/ReviewDistributionPlanTableSubscription.tsx index 571b7fb863..47ea33ca1c 100644 --- a/components/distribution-plan-tool/review-distribution-plan/table/ReviewDistributionPlanTableSubscription.tsx +++ b/components/distribution-plan-tool/review-distribution-plan/table/ReviewDistributionPlanTableSubscription.tsx @@ -6,14 +6,9 @@ import CircleLoader from "@/components/distribution-plan-tool/common/CircleLoade import { DistributionPlanToolContext } from "@/components/distribution-plan-tool/DistributionPlanToolContext"; import { MEMES_CONTRACT, SUBSCRIPTIONS_ADMIN_WALLETS } from "@/constants"; import { ApiIdentity } from "@/generated/models/ApiIdentity"; -import { - areEqualAddresses, - extractAllNumbers, - formatAddress, -} from "@/helpers/Helpers"; +import { areEqualAddresses } from "@/helpers/Helpers"; import { commonApiFetch } from "@/services/api/common-api"; import { useContext, useState } from "react"; -import { Button, Col, Container, Modal, Row } from "react-bootstrap"; import { PUBLIC_SUBSCRIPTIONS_PHASE_ID } from "./constants"; import { ReviewDistributionPlanTableItem, @@ -40,156 +35,59 @@ export function SubscriptionLinks( const { connectedProfile, setToast } = useContext(AuthContext); const { confirmedTokenId } = useContext(DistributionPlanToolContext); - const [showConfirm, setShowConfirm] = useState(false); const [downloading, setDownloading] = useState(false); if ( !isSubscriptionsAdmin(connectedProfile) || - props.phase.type !== ReviewDistributionPlanTableItemType.PHASE + props.phase.type !== ReviewDistributionPlanTableItemType.PHASE || + !confirmedTokenId ) { return <>; } const isPublic = props.phase.phaseId === PUBLIC_SUBSCRIPTIONS_PHASE_ID; - - return ( - <> - - setShowConfirm(false)} - confirmedTokenId={confirmedTokenId} - onConfirm={async (contract: string, tokenId: string) => { - setShowConfirm(false); - setDownloading(true); - try { - const downloadResponse = await download( - contract, - tokenId, - props.plan.id, - isPublic ? "public" : props.phase.id, - isPublic ? "public" : props.phase.name - ); - setToast({ - type: downloadResponse.success ? "success" : "error", - message: downloadResponse.message, - }); - } catch (error: any) { - console.error("Download failed", error); - setToast({ - type: "error", - message: "Something went wrong.", - }); - } finally { - setDownloading(false); - } - }} - /> - - ); -} - -export function SubscriptionConfirm( - props: Readonly<{ - title: string; - plan: AllowlistDescription; - show: boolean; - handleClose(): void; - isNormalized?: boolean | undefined; - confirmedTokenId?: string | null | undefined; - onConfirm(contract: string, tokenId: string): void; - }> -) { const contract = MEMES_CONTRACT; - const numbers = extractAllNumbers(props.plan.name); - const defaultTokenId = numbers.length > 0 ? numbers[0]!.toString() : ""; - const [tokenId, setTokenId] = useState( - props.confirmedTokenId ?? defaultTokenId - ); - const displayTokenId = props.confirmedTokenId ?? tokenId; + const handleDownload = async () => { + setDownloading(true); + try { + const downloadResponse = await download( + contract, + confirmedTokenId, + props.plan.id, + isPublic ? "public" : props.phase.id, + isPublic ? "public" : props.phase.name + ); + setToast({ + type: downloadResponse.success ? "success" : "error", + message: downloadResponse.message, + }); + } catch (error) { + console.error("Download failed", error); + setToast({ + type: "error", + message: "Something went wrong.", + }); + } finally { + setDownloading(false); + } + }; return ( - - - - Confirm {props.title} Info - - -
- - - - - Contract: The Memes - {formatAddress(contract)} - - - - - Token ID:{" "} - {props.confirmedTokenId !== undefined && - props.confirmedTokenId !== null ? ( - {displayTokenId} - ) : ( - { - setTokenId(e.target.value); - }} - /> - )} - - - {props.isNormalized !== undefined && props.isNormalized && ( - - -
- ⚠️ Distribution is already normalized. This will recalculate - and overwrite existing normalized data. -
- -
- )} -
-
- - - - -
+ ); } diff --git a/components/distribution-plan-tool/review-distribution-plan/table/ReviewDistributionPlanTableSubscriptionFooter.tsx b/components/distribution-plan-tool/review-distribution-plan/table/ReviewDistributionPlanTableSubscriptionFooter.tsx index 04294236ac..8a698b5787 100644 --- a/components/distribution-plan-tool/review-distribution-plan/table/ReviewDistributionPlanTableSubscriptionFooter.tsx +++ b/components/distribution-plan-tool/review-distribution-plan/table/ReviewDistributionPlanTableSubscriptionFooter.tsx @@ -4,25 +4,24 @@ import { AuthContext } from "@/components/auth/Auth"; import CircleLoader from "@/components/distribution-plan-tool/common/CircleLoader"; import { DistributionPlanToolContext } from "@/components/distribution-plan-tool/DistributionPlanToolContext"; import { MEMES_CONTRACT } from "@/constants"; +import { DistributionOverview } from "@/generated/models/DistributionOverview"; import { formatAddress } from "@/helpers/Helpers"; -import { - commonApiFetch, - commonApiPost, - commonApiPostForm, -} from "@/services/api/common-api"; +import { commonApiFetch, commonApiPost } from "@/services/api/common-api"; +import { uploadDistributionPhotos } from "@/services/distribution/distributionPhotoUpload"; import { useCallback, useContext, useEffect, useState } from "react"; -import { - SubscriptionConfirm, - isSubscriptionsAdmin, -} from "./ReviewDistributionPlanTableSubscription"; +import { isSubscriptionsAdmin } from "./ReviewDistributionPlanTableSubscription"; import { AutomaticAirdropsModal } from "./ReviewDistributionPlanTableSubscriptionFooterAutomaticAirdrops"; import { ConfirmTokenIdModal } from "./ReviewDistributionPlanTableSubscriptionFooterConfirmTokenId"; import { UploadDistributionPhotosModal } from "./ReviewDistributionPlanTableSubscriptionFooterUploadPhotos"; -interface DistributionOverview { - photos_count: number; - is_normalized: boolean; - automatic_airdrops: number; +function getErrorMessage(error: unknown): string { + if (typeof error === "string") { + return error; + } + if (error instanceof Error) { + return error.message; + } + return "Something went wrong."; } export function ReviewDistributionPlanTableSubscriptionFooter() { @@ -30,11 +29,8 @@ export function ReviewDistributionPlanTableSubscriptionFooter() { useContext(DistributionPlanToolContext); const { connectedProfile, setToast } = useContext(AuthContext); - const [showSubscriptionsReset, setShowSubscriptionsReset] = useState(false); const [showUploadPhotos, setShowUploadPhotos] = useState(false); const [showAutomaticAirdrops, setShowAutomaticAirdrops] = useState(false); - const [showFinalizeDistribution, setShowFinalizeDistribution] = - useState(false); const [showConfirmTokenId, setShowConfirmTokenId] = useState(false); const [isResetting, setIsResetting] = useState(false); const [isUploading, setIsUploading] = useState(false); @@ -92,7 +88,6 @@ export function ReviewDistributionPlanTableSubscriptionFooter() { tokenId: string, planId: string ) => { - setShowSubscriptionsReset(false); setIsResetting(true); try { await commonApiPost({ @@ -105,16 +100,53 @@ export function ReviewDistributionPlanTableSubscriptionFooter() { }); await refreshOverview(contract, tokenId); - } catch (error: any) { + } catch (error: unknown) { setToast({ type: "error", - message: `Reset failed: ${error}`, + message: `Reset failed: ${getErrorMessage(error)}`, }); } finally { setIsResetting(false); } }; + const finalizeDistribution = async (contract: string, tokenId: string) => { + setIsFinalizing(true); + try { + const response = await commonApiPost< + Record, + { + success: boolean; + message?: string; + error?: string; + } + >({ + endpoint: `distributions/${contract}/${tokenId}/normalize`, + body: {}, + }); + + if (response.success) { + setToast({ + type: "success", + message: response.message || "Distribution normalized successfully", + }); + await refreshOverview(contract, tokenId); + } else { + setToast({ + type: "error", + message: response.error || "Normalization failed", + }); + } + } catch (error: unknown) { + setToast({ + type: "error", + message: getErrorMessage(error), + }); + } finally { + setIsFinalizing(false); + } + }; + if (!isSubscriptionsAdmin(connectedProfile)) { return <>; } @@ -151,7 +183,10 @@ export function ReviewDistributionPlanTableSubscriptionFooter() { Change Token ID