diff --git a/client/__tests__/CreateFormIntegration.test.tsx b/client/__tests__/CreateFormIntegration.test.tsx index bada14c..3fce980 100644 --- a/client/__tests__/CreateFormIntegration.test.tsx +++ b/client/__tests__/CreateFormIntegration.test.tsx @@ -144,7 +144,11 @@ const CreateFormLogic = () => { }; describe("Create Form Integration", () => { - const user = userEvent.setup(); + let user: ReturnType; + + beforeEach(() => { + user = userEvent.setup(); + }); it("validates complete form correctly", async () => { render(); diff --git a/client/__tests__/MetadataParser.test.tsx b/client/__tests__/MetadataParser.test.tsx index c456aa8..9fe41d9 100644 --- a/client/__tests__/MetadataParser.test.tsx +++ b/client/__tests__/MetadataParser.test.tsx @@ -74,13 +74,15 @@ describe("parseMetadata", () => { render(); expect(screen.getByTestId("name")).toHaveTextContent("My Special Jar"); - expect(screen.getByTestId("description")).toHaveTextContent( - "A detailed description", - ); - expect(screen.getByTestId("image")).toHaveAttribute( - "src", - "https://example.com/image.png", - ); + expect(screen.getByTestId("description")).toHaveTextContent( + "A detailed description", + ); + expect(screen.getByTestId("image")).toHaveAttribute( + "src", + expect.stringContaining( + encodeURIComponent("https://example.com/image.png"), + ), + ); expect(screen.getByTestId("link")).toHaveAttribute( "href", "https://example.com/project", @@ -96,11 +98,13 @@ describe("parseMetadata", () => { render(); expect(screen.getByTestId("name")).toHaveTextContent("Partial Jar"); - expect(screen.getByTestId("description")).toHaveTextContent(metadata); // fallback to raw string - expect(screen.getByTestId("image")).toHaveAttribute( - "src", - "https://example.com/image.png", - ); + expect(screen.getByTestId("description")).toHaveTextContent(metadata); // fallback to raw string + expect(screen.getByTestId("image")).toHaveAttribute( + "src", + expect.stringContaining( + encodeURIComponent("https://example.com/image.png"), + ), + ); expect(screen.getByTestId("link")).toHaveAttribute("href", ""); }); @@ -150,13 +154,15 @@ describe("parseMetadata", () => { render(); expect(screen.getByTestId("name")).toHaveTextContent("Complete Jar"); - expect(screen.getByTestId("description")).toHaveTextContent( - "Full description", - ); - expect(screen.getByTestId("image")).toHaveAttribute( - "src", - "https://example.com/complete.png", - ); + expect(screen.getByTestId("description")).toHaveTextContent( + "Full description", + ); + expect(screen.getByTestId("image")).toHaveAttribute( + "src", + expect.stringContaining( + encodeURIComponent("https://example.com/complete.png"), + ), + ); expect(screen.getByTestId("link")).toHaveAttribute( "href", "https://complete-project.com", diff --git a/client/__tests__/hooks/jar/createV2CreateArgs.test.ts b/client/__tests__/hooks/jar/createV2CreateArgs.test.ts new file mode 100644 index 0000000..8a49ccf --- /dev/null +++ b/client/__tests__/hooks/jar/createV2CreateArgs.test.ts @@ -0,0 +1,213 @@ +import { decodeFunctionData, encodeFunctionData } from "viem"; +import { describe, expect, it, vi } from "vitest"; +import { cookieJarFactoryAbi } from "@/generated"; +import { + FACTORY_DEFAULT_FEE_SENTINEL, + buildV2CreateCookieJarArgs, + getFeePercentageOnDeposit, +} from "@/hooks/jar/createV2CreateArgs"; +import { ETH_ADDRESS, HATS_PROTOCOL_ADDRESS, POAP_TOKEN_ADDRESS } from "@/lib/blockchain/constants"; + +vi.mock("@/hooks/jar/schemas/jarCreationSchema", () => ({ + AccessType: { + Allowlist: 0, + NFTGated: 1, + POAP: 2, + Unlock: 3, + Hypercert: 4, + Hats: 5, + }, + NFTType: { + None: 0, + ERC721: 1, + ERC1155: 2, + }, +})); + +const AccessType = { + Allowlist: 0, + NFTGated: 1, + POAP: 2, + Unlock: 3, + Hypercert: 4, + Hats: 5, +} as const; + +const NFTType = { + None: 0, + ERC721: 1, + ERC1155: 2, +} as const; + +type JarCreationFormData = Parameters[0]["values"]; +type ProtocolConfig = JarCreationFormData["protocolConfig"]; + +type MakeValuesOverrides = Partial> & { + protocolConfig?: Partial; +}; + +function makeValues(overrides: MakeValuesOverrides = {}): JarCreationFormData { + const baseValues: JarCreationFormData = { + jarName: "Test Jar", + jarOwnerAddress: "0x1234567890123456789012345678901234567890", + supportedCurrency: ETH_ADDRESS, + metadata: "Test metadata", + imageUrl: "", + externalLink: "", + showCustomCurrency: false, + customCurrencyAddress: "", + withdrawalOption: 0, + fixedAmount: "1", + maxWithdrawal: "2", + withdrawalInterval: "7", + strictPurpose: true, + emergencyWithdrawalEnabled: true, + oneTimeWithdrawal: false, + accessType: AccessType.Allowlist, + nftAddresses: [] as string[], + nftTypes: [] as number[], + protocolConfig: { accessType: "Allowlist" }, + enableCustomFee: false, + customFee: "", + streamingEnabled: false, + requireStreamApproval: true, + maxStreamRate: "1.0", + minStreamDuration: "1", + autoSwapEnabled: false, + }; + + return { + ...baseValues, + ...overrides, + protocolConfig: { + ...baseValues.protocolConfig, + ...overrides.protocolConfig, + }, + }; +} + +describe("buildV2CreateCookieJarArgs", () => { + it("encodes createCookieJar args compatible with current factory ABI", () => { + const args = buildV2CreateCookieJarArgs({ + values: makeValues(), + metadata: "metadata", + parseAmount: (amount) => BigInt(Math.floor(Number.parseFloat(amount) * 1e18)), + }); + + const data = encodeFunctionData({ + abi: cookieJarFactoryAbi, + functionName: "createCookieJar", + args, + }); + expect(data.startsWith("0x")).toBe(true); + + const decoded = decodeFunctionData({ + abi: cookieJarFactoryAbi, + data, + }); + expect(decoded.functionName).toBe("createCookieJar"); + expect(decoded.args?.length).toBe(3); + }); + + it("uses default fee sentinel when custom fee is disabled", () => { + const fee = getFeePercentageOnDeposit(makeValues({ enableCustomFee: false })); + expect(fee).toBe(FACTORY_DEFAULT_FEE_SENTINEL); + }); + + it("uses explicit custom fee when provided", () => { + const fee = getFeePercentageOnDeposit( + makeValues({ enableCustomFee: true, customFee: "2.5" }), + ); + expect(fee).toBe(250n); + }); + + it("supports explicit zero-percent fee", () => { + const fee = getFeePercentageOnDeposit( + makeValues({ enableCustomFee: true, customFee: "0" }), + ); + expect(fee).toBe(0n); + }); + + it("maps NFT ERC721 access to contract enum ERC721", () => { + const [jarConfig, accessConfig] = buildV2CreateCookieJarArgs({ + values: makeValues({ + accessType: AccessType.NFTGated, + nftAddresses: ["0x1111111111111111111111111111111111111111"], + nftTypes: [NFTType.ERC721], + }), + metadata: "metadata", + parseAmount: () => 1n, + }); + expect(jarConfig.accessType).toBe(1); + expect(accessConfig.nftRequirement.nftContract).toBe( + "0x1111111111111111111111111111111111111111", + ); + }); + + it("maps NFT ERC1155 access to contract enum ERC1155", () => { + const [jarConfig] = buildV2CreateCookieJarArgs({ + values: makeValues({ + accessType: AccessType.NFTGated, + nftAddresses: ["0x1111111111111111111111111111111111111111"], + nftTypes: [NFTType.ERC1155], + }), + metadata: "metadata", + parseAmount: () => 1n, + }); + expect(jarConfig.accessType).toBe(2); + }); + + it("maps POAP, Unlock, Hypercert, and Hats to supported contract enum domain", () => { + const [poapJar, poapAccess] = buildV2CreateCookieJarArgs({ + values: makeValues({ + accessType: AccessType.POAP, + protocolConfig: { accessType: "POAP", eventId: "1234" }, + }), + metadata: "metadata", + parseAmount: () => 1n, + }); + expect(poapJar.accessType).toBe(1); + expect(poapAccess.nftRequirement.nftContract).toBe(POAP_TOKEN_ADDRESS); + expect(poapAccess.nftRequirement.tokenId).toBe(1234n); + expect(poapAccess.nftRequirement.minBalance).toBe(1n); + + const [unlockJar] = buildV2CreateCookieJarArgs({ + values: makeValues({ + accessType: AccessType.Unlock, + protocolConfig: { + accessType: "Unlock", + unlockAddress: "0x2222222222222222222222222222222222222222", + }, + }), + metadata: "metadata", + parseAmount: () => 1n, + }); + expect(unlockJar.accessType).toBe(1); + + const [hypercertJar] = buildV2CreateCookieJarArgs({ + values: makeValues({ + accessType: AccessType.Hypercert, + protocolConfig: { + accessType: "Hypercert", + hypercertAddress: "0x3333333333333333333333333333333333333333", + hypercertTokenId: "42", + hypercertMinBalance: 5, + }, + }), + metadata: "metadata", + parseAmount: () => 1n, + }); + expect(hypercertJar.accessType).toBe(2); + + const [hatsJar, hatsAccess] = buildV2CreateCookieJarArgs({ + values: makeValues({ + accessType: AccessType.Hats, + protocolConfig: { accessType: "Hats", hatsId: 99 }, + }), + metadata: "metadata", + parseAmount: () => 1n, + }); + expect(hatsJar.accessType).toBe(2); + expect(hatsAccess.nftRequirement.nftContract).toBe(HATS_PROTOCOL_ADDRESS); + }); +}); diff --git a/client/__tests__/hooks/useJarCreation.test.ts b/client/__tests__/hooks/useJarCreation.test.ts index cdae29c..8bcd423 100644 --- a/client/__tests__/hooks/useJarCreation.test.ts +++ b/client/__tests__/hooks/useJarCreation.test.ts @@ -18,7 +18,6 @@ vi.mock("@/config/deployments.auto", () => ({ }, })); -import { useJarCreation } from "@/hooks/jar/useJarCreation"; import { ETH_ADDRESS } from "@/lib/blockchain/constants"; // Mock wagmi hooks @@ -60,7 +59,7 @@ vi.mock("@/config/supported-networks", () => ({ })); // Mock toast -vi.mock("@/hooks/useToast", () => ({ +vi.mock("@/hooks/app/useToast", () => ({ useToast: () => ({ toast: vi.fn(), }), @@ -73,6 +72,12 @@ const describeOrSkip = describeOrSkip("useJarCreation", () => { let queryClient: QueryClient; + let useJarCreation: typeof import("@/hooks/jar/useJarCreation").useJarCreation; + + beforeAll(async () => { + const module = await import("@/hooks/jar/useJarCreation"); + useJarCreation = module.useJarCreation; + }); beforeEach(() => { vi.clearAllMocks(); @@ -85,6 +90,9 @@ describeOrSkip("useJarCreation", () => { }); const renderHookWithProviders = () => { + if (!useJarCreation) { + throw new Error("useJarCreation hook not loaded"); + } return renderHook(() => useJarCreation(), { wrapper: ({ children }: { children: React.ReactNode }) => React.createElement( @@ -95,7 +103,7 @@ describeOrSkip("useJarCreation", () => { }); }; - describe("๐Ÿ”ง ETH Address Fix", () => { + describe("ETH Address Fix", () => { it("should use address(3) for ETH address", () => { const { result } = renderHookWithProviders(); @@ -105,11 +113,13 @@ describeOrSkip("useJarCreation", () => { it("should initialize supportedCurrency with correct ETH address", () => { const { result } = renderHookWithProviders(); - expect(result.current.supportedCurrency).toBe(ETH_ADDRESS); + expect(result.current.form.getValues("supportedCurrency")).toBe( + ETH_ADDRESS, + ); }); }); - describe("๐Ÿš€ V1 vs V2 Logic", () => { + describe("V1 vs V2 Logic", () => { it("should detect v1 contracts correctly", () => { const { result } = renderHookWithProviders(); @@ -118,13 +128,13 @@ describeOrSkip("useJarCreation", () => { }); it("should detect v2 contracts correctly", () => { - // Mock the deployments.auto module to return v2 for chain 31337 vi.doMock("@/config/deployments.auto", () => ({ isV2Chain: vi.fn().mockReturnValue(true), DEPLOYMENTS: { 31337: { chainId: 31337, - factoryAddress: "0xa2Cc1f3479E194B1aa16BeCc975aA25618f8d3AD", + factoryAddress: + "0xa2Cc1f3479E194B1aa16BeCc975aA25618f8d3AD", isV2: true, blockNumber: 0, timestamp: 1759019328, @@ -140,26 +150,28 @@ describeOrSkip("useJarCreation", () => { const { result } = renderHookWithProviders(); act(() => { - result.current.setEnableCustomFee(true); + result.current.form.setValue("enableCustomFee", true); }); - // Should automatically disable custom fees for v1 when isV2Contract is false - expect(result.current.enableCustomFee).toBe(false); + // v1 effect should reset to false + expect(result.current.form.getValues("enableCustomFee")).toBe( + false, + ); }); it("should force allowlist access type for v1 contracts", () => { const { result } = renderHookWithProviders(); act(() => { - result.current.setAccessType(1); // NFTGated + result.current.form.setValue("accessType", 1); // NFTGated }); - // Should automatically revert to allowlist for v1 - expect(result.current.accessType).toBe(0); // Allowlist + // v1 effect should reset to Allowlist + expect(result.current.form.getValues("accessType")).toBe(0); }); }); - describe("๐Ÿ“ Form Validation", () => { + describe("Form Validation", () => { it("should validate required jar name", () => { const { result } = renderHookWithProviders(); @@ -173,8 +185,8 @@ describeOrSkip("useJarCreation", () => { const { result } = renderHookWithProviders(); act(() => { - result.current.setWithdrawalOption(0); // Fixed - result.current.setFixedAmount("0"); + result.current.form.setValue("withdrawalOption", 0); // Fixed + result.current.form.setValue("fixedAmount", "0"); }); const validation = result.current.validateStep2(); @@ -189,7 +201,7 @@ describeOrSkip("useJarCreation", () => { const { result } = renderHookWithProviders(); act(() => { - result.current.setWithdrawalInterval("0"); + result.current.form.setValue("withdrawalInterval", "0"); }); const validation = result.current.validateStep2(); @@ -204,8 +216,8 @@ describeOrSkip("useJarCreation", () => { const { result } = renderHookWithProviders(); act(() => { - result.current.setEnableCustomFee(true); - result.current.setCustomFee("150"); // Over 100% + result.current.form.setValue("enableCustomFee", true); + result.current.form.setValue("customFee", "150"); // Over 100% }); const validation = result.current.validateStep4(); @@ -217,50 +229,50 @@ describeOrSkip("useJarCreation", () => { }); }); - describe("๐Ÿ’ฑ Currency Handling", () => { - it("should handle custom currency selection", () => { + describe("Currency Handling", () => { + it("should initialize with custom currency hidden", () => { const { result } = renderHookWithProviders(); - act(() => { - result.current.handleCurrencyChange("CUSTOM"); - }); - - expect(result.current.showCustomCurrency).toBe(true); + expect(result.current.form.getValues("showCustomCurrency")).toBe( + false, + ); }); - it("should validate ERC20 addresses", async () => { + it("should allow setting custom currency via form", () => { const { result } = renderHookWithProviders(); act(() => { - result.current.setCustomCurrencyAddress( + result.current.form.setValue("showCustomCurrency", true); + result.current.form.setValue( + "customCurrencyAddress", "0x1234567890123456789012345678901234567890", ); }); - await act(async () => { - await result.current.handleCustomCurrencySubmit(); - }); - - // Should handle the submission (testing the function executes without throwing) - expect(result.current.handleCustomCurrencySubmit).toBeDefined(); + expect(result.current.form.getValues("showCustomCurrency")).toBe( + true, + ); + expect( + result.current.form.getValues("customCurrencyAddress"), + ).toBe("0x1234567890123456789012345678901234567890"); }); }); - describe("๐Ÿงน Form Reset", () => { + describe("Form Reset", () => { it("should reset all form fields", () => { const { result } = renderHookWithProviders(); // Set some values act(() => { - result.current.setJarName("Test Jar"); - result.current.setFixedAmount("0.5"); - result.current.setEnableCustomFee(true); + result.current.form.setValue("jarName", "Test Jar"); + result.current.form.setValue("fixedAmount", "0.5"); + result.current.form.setValue("enableCustomFee", true); }); // Verify values are set - expect(result.current.jarName).toBe("Test Jar"); - expect(result.current.fixedAmount).toBe("0.5"); - expect(result.current.enableCustomFee).toBe(true); + expect(result.current.form.getValues("jarName")).toBe("Test Jar"); + expect(result.current.form.getValues("fixedAmount")).toBe("0.5"); + expect(result.current.form.getValues("enableCustomFee")).toBe(true); // Reset form act(() => { @@ -268,31 +280,11 @@ describeOrSkip("useJarCreation", () => { }); // Verify values are reset - expect(result.current.jarName).toBe(""); - expect(result.current.fixedAmount).toBe("0"); - expect(result.current.enableCustomFee).toBe(false); - }); - }); - - describe("๐ŸŽฒ Development Helpers", () => { - beforeEach(() => { - vi.stubEnv("NODE_ENV", "development"); - }); - - afterEach(() => { - vi.unstubAllEnvs(); - }); - - it("should populate random data in development", () => { - const { result } = renderHookWithProviders(); - - act(() => { - result.current.prepopulateRandomData(); - }); - - // Should have populated some data - expect(result.current.jarName).toBeTruthy(); - expect(result.current.fixedAmount).not.toBe("0"); + expect(result.current.form.getValues("jarName")).toBe(""); + expect(result.current.form.getValues("fixedAmount")).toBe("0"); + expect(result.current.form.getValues("enableCustomFee")).toBe( + false, + ); }); }); }); diff --git a/client/app/create/page.tsx b/client/app/create/page.tsx index 01d50c4..3767623 100644 --- a/client/app/create/page.tsx +++ b/client/app/create/page.tsx @@ -1,17 +1,16 @@ "use client"; import { lazy, Suspense, useEffect, useState } from "react"; +import { FormProvider } from "react-hook-form"; import { useAccount, useChainId } from "wagmi"; import { ProtocolErrorBoundary } from "@/components/app/ProtocolErrorBoundary"; import { CreateJarForm } from "@/components/create/CreateJarForm"; -// Import extracted components import { CreateJarHeader } from "@/components/create/CreateJarHeader"; import { ProgressIndicator } from "@/components/create/ProgressIndicator"; import { isV2Chain } from "@/config/supported-networks"; import { useStepNavigation } from "@/hooks/app/useStepNavigation"; import { useJarCreation } from "@/hooks/jar/useJarCreation"; -// Lazy load heavy components for better bundle splitting const StatusCards = lazy(() => import("@/components/create/StatusCards").then((module) => ({ default: module.StatusCards, @@ -28,29 +27,37 @@ export default function CreateCookieJarForm() { const chainId = useChainId(); const isV2Contract = isV2Chain(chainId); - // All form state and logic moved to custom hook - const formData = useJarCreation(); + const { + form, + confirmSubmit, + validateStep1, + validateStep2, + validateStep3, + validateStep4, + isCreating, + isWaitingForTx, + newJarPreview, + formErrors, + isFormError, + ETH_ADDRESS, + } = useJarCreation(); - // Step navigation moved to custom hook const { currentStep, totalSteps, nextStep, prevStep } = useStepNavigation(isV2Contract); - // Modal state (keep minimal state in main component) const [showWalletModal, setShowWalletModal] = useState(false); const [pendingSubmission, setPendingSubmission] = useState(false); - // Check if current step is valid const isCurrentStepValid = () => { switch (currentStep) { case 1: - return formData.validateStep1().isValid; + return validateStep1().isValid; case 2: - return formData.validateStep2().isValid; + return validateStep2().isValid; case 3: - // Skip validation for step 3 on v1 chains - return isV2Contract ? formData.validateStep3().isValid : true; + return isV2Contract ? validateStep3().isValid : true; case 4: - return formData.validateStep4().isValid; + return validateStep4().isValid; default: return false; } @@ -61,21 +68,19 @@ export default function CreateCookieJarForm() { if (isConnected && address && pendingSubmission && showWalletModal) { setShowWalletModal(false); setPendingSubmission(false); - // Retry the submission setTimeout(() => { - formData.confirmSubmit(); - }, 100); // Small delay to ensure modal closes first + confirmSubmit(); + }, 100); } - }, [isConnected, address, pendingSubmission, showWalletModal, formData]); + }, [isConnected, address, pendingSubmission, showWalletModal, confirmSubmit]); - // Handle wallet connection modal trigger const handleSubmit = () => { if (!isConnected) { setShowWalletModal(true); setPendingSubmission(true); return; } - formData.confirmSubmit(); + confirmSubmit(); }; return ( @@ -84,46 +89,49 @@ export default function CreateCookieJarForm() { maxRetries={2} showDetails={process.env.NODE_ENV === "development"} > -
- - - - + +
+ + - - } - > - - -
+ + + } + > + + +
+ diff --git a/client/components/create/CreateJarForm.tsx b/client/components/create/CreateJarForm.tsx index f5febef..95eb970 100644 --- a/client/components/create/CreateJarForm.tsx +++ b/client/components/create/CreateJarForm.tsx @@ -15,22 +15,24 @@ interface CreateJarFormProps { currentStep: number; totalSteps: number; isV2Contract: boolean; - formData: any; // Type this based on your useJarCreation hook return type nextStep: () => void; prevStep: () => void; handleSubmit: () => void; isCurrentStepValid: () => boolean; + isCreating: boolean; + isWaitingForTx: boolean; } export function CreateJarForm({ currentStep, totalSteps, isV2Contract, - formData, nextStep, prevStep, handleSubmit, isCurrentStepValid, + isCreating, + isWaitingForTx, }: CreateJarFormProps) { const { isConnected } = useAccount(); @@ -75,7 +77,6 @@ export function CreateJarForm({ > @@ -110,12 +111,12 @@ export function CreateJarForm({ onClick={handleSubmit} disabled={ (!isCurrentStepValid() && isConnected) || - formData.isCreating || - formData.isWaitingForTx + isCreating || + isWaitingForTx } className="w-full md:w-auto cj-btn-primary flex items-center justify-center gap-2" > - {formData.isCreating || formData.isWaitingForTx ? ( + {isCreating || isWaitingForTx ? ( <> Creating... diff --git a/client/components/create/MobileOptimizedForm.tsx b/client/components/create/MobileOptimizedForm.tsx index d4c408c..9728268 100644 --- a/client/components/create/MobileOptimizedForm.tsx +++ b/client/components/create/MobileOptimizedForm.tsx @@ -26,7 +26,6 @@ interface MobileOptimizedFormProps { currentStep: number; totalSteps: number; isV2Contract: boolean; - formData: any; nextStep: () => void; prevStep: () => void; handleSubmit: () => void; @@ -37,7 +36,6 @@ export function MobileOptimizedForm({ currentStep, totalSteps, isV2Contract, - formData, nextStep, prevStep, handleSubmit, @@ -204,7 +202,6 @@ export function MobileOptimizedForm({
diff --git a/client/components/create/StepContent.tsx b/client/components/create/StepContent.tsx index 3e166ab..13d5910 100644 --- a/client/components/create/StepContent.tsx +++ b/client/components/create/StepContent.tsx @@ -2,8 +2,10 @@ import { Trash2 } from "lucide-react"; import type React from "react"; +import { useCallback, useEffect, useMemo } from "react"; +import { useFormContext } from "react-hook-form"; import { isAddress } from "viem"; -// import { NFTGateInput } from "@/components/nft/NFTGateInput"; // TODO: Component not found +import { useChainId } from "wagmi"; import { NFTSelector, type SelectedNFT } from "@/components/nft/NFTSelector"; import { ProtocolSelector } from "@/components/nft/ProtocolSelector"; import { Button } from "@/components/ui/button"; @@ -20,39 +22,200 @@ import { import { Textarea } from "@/components/ui/textarea"; import { AccessType, + METHOD_TO_ACCESS_TYPE, NFTType, WithdrawalTypeOptions, -} from "@/hooks/jar/useJarCreation"; + type JarCreationFormData, +} from "@/hooks/jar/schemas/jarCreationSchema"; +import type { + AccessMethod, + ProtocolConfig as SelectorProtocolConfig, +} from "@/components/nft/ProtocolSelector"; +import { useToast } from "@/hooks/app/useToast"; +import { ETH_ADDRESS } from "@/lib/blockchain/token-utils"; import { shortenAddress } from "@/lib/app/utils"; +import { isPoapSupportedChain } from "@/config/supported-networks"; interface StepContentProps { step: number; - formData: any; // Using any to avoid complex type imports for now isV2Contract: boolean; } +const VISIBLE_METHODS_WITHOUT_POAP: AccessMethod[] = [ + "Allowlist", + "NFT", + "Hats", + "Hypercert", + "Unlock", +]; + export const StepContent: React.FC = ({ step, - formData, isV2Contract, }) => { switch (step) { case 1: - return ; + return ; case 2: - return ; + return ; case 3: - return isV2Contract ? : null; + return isV2Contract ? : null; case 4: - return ( - - ); + return ; default: return null; } }; -const BasicConfigStep: React.FC<{ formData: any }> = ({ formData }) => { +// โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +// Step 1: Basic Configuration +// โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + +const BasicConfigStep: React.FC = () => { + const { register, watch, setValue, formState: { errors } } = + useFormContext(); + const { toast } = useToast(); + const chainId = useChainId(); + + const showCustomCurrency = watch("showCustomCurrency"); + const supportedCurrency = watch("supportedCurrency"); + const customCurrencyAddress = watch("customCurrencyAddress"); + const jarOwnerAddress = watch("jarOwnerAddress"); + + const currencyOptions = useMemo(() => { + const options: Array<{ + value: string; + label: string; + description: string; + }> = [ + { + value: ETH_ADDRESS, + label: "ETH (Native)", + description: "Use native Ethereum", + }, + ]; + + if (chainId === 31337) { + options.push({ + value: "0x036CbD53842c5426634e7929541eC2318f3dCF7e", + label: "DEMO Token", + description: "Local development token", + }); + } else if (chainId === 84532) { + options.push({ + value: "0x4200000000000000000000000000000000000006", + label: "WETH", + description: "Wrapped ETH on Base Sepolia", + }); + } + + options.push({ + value: "CUSTOM", + label: "Custom ERC-20", + description: "Enter your own ERC-20 token address", + }); + + return options; + }, [chainId]); + + const handleCurrencyChange = useCallback( + (value: string) => { + if (value === "CUSTOM") { + setValue("showCustomCurrency", true); + } else { + setValue("showCustomCurrency", false); + setValue("supportedCurrency", value); + setValue("customCurrencyAddress", ""); + } + }, + [setValue], + ); + + const handleCustomCurrencySubmit = useCallback(async () => { + if (customCurrencyAddress && isAddress(customCurrencyAddress)) { + setValue("supportedCurrency", customCurrencyAddress); + toast({ + title: "Custom currency set", + description: "ERC-20 token address has been set successfully", + }); + } else { + toast({ + title: "Invalid address", + description: "Please enter a valid Ethereum address", + variant: "destructive", + }); + } + }, [customCurrencyAddress, setValue, toast]); + + const handlePrepopulate = useCallback(() => { + if (process.env.NODE_ENV !== "development") return; + + const randomNames = [ + "Cookie Fund", + "Dev Grants", + "Community Pool", + "Test Jar", + "Demo Fund", + "Alpha Pool", + ]; + const randomDescriptions = [ + "A fund for supporting cookie development", + "Grants for innovative projects", + "Community-driven funding pool", + "Testing new jar functionality", + "Demonstration of jar capabilities", + "Early access funding", + ]; + const randomImages = [ + "https://picsum.photos/400/300?random=1", + "https://picsum.photos/400/300?random=2", + "https://picsum.photos/400/300?random=3", + ]; + const randomLinks = [ + "https://example.com/project1", + "https://github.com/test/repo", + "https://docs.example.com", + ]; + + setValue( + "jarName", + randomNames[Math.floor(Math.random() * randomNames.length)], + ); + setValue( + "metadata", + randomDescriptions[ + Math.floor(Math.random() * randomDescriptions.length) + ], + ); + setValue( + "imageUrl", + randomImages[Math.floor(Math.random() * randomImages.length)], + ); + setValue( + "externalLink", + randomLinks[Math.floor(Math.random() * randomLinks.length)], + ); + + if (Math.random() > 0.5) { + setValue("enableCustomFee", true); + setValue("customFee", (Math.random() * 1.9 + 0.1).toFixed(2)); + } + + if (Math.random() > 0.7) { + setValue( + "supportedCurrency", + "0x036CbD53842c5426634e7929541eC2318f3dCF7e", + ); + } + + setValue("fixedAmount", (Math.random() * 0.5).toFixed(3)); + setValue("maxWithdrawal", (Math.random() * 2).toFixed(3)); + setValue( + "withdrawalInterval", + String(Math.floor(Math.random() * 30 + 1)), + ); + }, [setValue]); + return (
@@ -60,10 +223,10 @@ const BasicConfigStep: React.FC<{ formData: any }> = ({ formData }) => { )}
@@ -74,11 +237,17 @@ const BasicConfigStep: React.FC<{ formData: any }> = ({ formData }) => { formData.setJarName(e.target.value)} placeholder="e.g., Community Fund, Dev Grants" aria-label="Enter a name for your cookie jar" + aria-invalid={!!errors.jarName} + aria-describedby={errors.jarName ? "jarName-error" : undefined} + {...register("jarName")} /> + {errors.jarName && ( +

+ {errors.jarName.message} +

+ )}
@@ -87,13 +256,11 @@ const BasicConfigStep: React.FC<{ formData: any }> = ({ formData }) => { - formData.setJarOwnerAddress(e.target.value as `0x${string}`) - } placeholder="0x... (defaults to your connected wallet)" className="pr-12" aria-label="Enter the Ethereum address that will own this jar" + aria-invalid={!!errors.jarOwnerAddress} + {...register("jarOwnerAddress")} />

- {formData.jarOwnerAddress && - formData.jarOwnerAddress !== + {jarOwnerAddress && + jarOwnerAddress !== "0x0000000000000000000000000000000000000000" - ? `Currently set to: ${shortenAddress(formData.jarOwnerAddress, 10)}` + ? `Currently set to: ${shortenAddress(jarOwnerAddress, 10)}` : "The address that will own and manage this jar"}

@@ -139,12 +305,8 @@ const BasicConfigStep: React.FC<{ formData: any }> = ({ formData }) => {
- {formData.showCustomCurrency && ( + {showCustomCurrency && (
- formData.setCustomCurrencyAddress(e.target.value) - } placeholder="0x... (ERC-20 contract address)" className="flex-1" + {...register("customCurrencyAddress")} />
- {formData.customCurrencyAddress && - !isAddress(formData.customCurrencyAddress) && ( + {customCurrencyAddress && + !isAddress(customCurrencyAddress) && (

Please enter a valid Ethereum address

)} - {formData.supportedCurrency && - formData.supportedCurrency !== formData.ETH_ADDRESS && - isAddress(formData.supportedCurrency) && ( + {supportedCurrency && + supportedCurrency !== ETH_ADDRESS && + isAddress(supportedCurrency) && (

- โœ“ Custom ERC-20 set:{" "} - {shortenAddress(formData.supportedCurrency, 10)} + Custom ERC-20 set:{" "} + {shortenAddress(supportedCurrency, 10)}

)}
@@ -214,10 +373,9 @@ const BasicConfigStep: React.FC<{ formData: any }> = ({ formData }) => {