From 51770b80fcf280019fee9b5391af60e68864efd2 Mon Sep 17 00:00:00 2001 From: Paolo Guerra Date: Tue, 24 Jun 2025 18:52:42 +0200 Subject: [PATCH 01/16] feat(frontend): add setup share and form auto complete for amm campaign --- packages/frontend/SUPPORTED_PROTOCOLS.md | 24 +++---- packages/frontend/messages/en.json | 2 + .../form/amm-pool-liquidity-form/index.tsx | 53 ++++++++++++++- .../components/create-campaign/form/index.tsx | 16 ++--- .../create-campaign/preview/index.tsx | 47 ++++++++++++- .../create-campaign/preview/styles.module.css | 8 +-- .../create-campaign/steps/dex-step/index.tsx | 15 ++++- .../steps/end-date-step/index.tsx | 17 +++-- .../create-campaign/steps/kpi-step/index.tsx | 63 +++++++++++------ .../steps/liquity-v2-action-step/index.tsx | 9 ++- .../steps/liquity-v2-brand-step/index.tsx | 9 ++- .../liquity-v2-collateral-step/index.tsx | 9 ++- .../create-campaign/steps/pool-step/index.tsx | 13 ++-- .../steps/range-step/index.tsx | 67 ++++++++++++------- .../steps/restrictions-step/index.tsx | 46 +++++++------ .../steps/rewards-step/index.tsx | 4 +- .../steps/start-date-step/index.tsx | 17 +++-- .../create-campaign/steps/weighting/index.tsx | 57 +++++++++++----- .../weighting/weighting-inputs/index.tsx | 29 -------- packages/frontend/src/types/campaign.ts | 5 ++ 20 files changed, 346 insertions(+), 164 deletions(-) diff --git a/packages/frontend/SUPPORTED_PROTOCOLS.md b/packages/frontend/SUPPORTED_PROTOCOLS.md index 978fa55d9..416c74e4e 100644 --- a/packages/frontend/SUPPORTED_PROTOCOLS.md +++ b/packages/frontend/SUPPORTED_PROTOCOLS.md @@ -1,12 +1,12 @@ -||Telos|Gnosis|Sonic|Lens|Sei Network|LightLink|Swell|Base|Hemi|Taiko|Scroll|Lumia| -|---|---|---|---|---|---|---|---|---|---|---|---|--- -|[Uniswap v3](https://app.uniswap.org/)|✅|✅|✅|✅|-|✅|-|✅|✅|✅|✅|-| -|[Swapr](https://swapr.eth.link/)|-|✅|-|-|-|-|-|-|-|-|-|-| -|[SilverSwap](https://silverswap.io/)|-|-|✅|-|-|-|-|-|-|-|-|-| -|[Carbon DeFi](https://carbondefi.xyz/)|-|-|-|-|✅|-|-|-|-|-|-|-| -|[Velodrome](https://velodrome.finance/)|-|-|-|-|-|-|✅|-|-|-|-|-| -|[Kim](https://www.kim.exchange)|-|-|-|-|-|-|-|✅|-|-|-|-| -|[BaseSwap](https://baseswap.fi/)|-|-|-|-|-|-|-|✅|-|-|-|-| -|[Unagi](https://unagiswap.xyz/)|-|-|-|-|-|-|-|-|-|✅|-|-| -|[Scribe](https://scribe.exchange)|-|-|-|-|-|-|-|-|-|-|✅|-| -|[Morphex](https://morphex.exchange/)|-|-|-|-|-|-|-|-|-|-|-|✅| +| | Telos | Gnosis | Sonic | Lens | Sei Network | LightLink | Swell | Base | Hemi | Taiko | Scroll | Lumia | +| --------------------------------------- | ----- | ------ | ----- | ---- | ----------- | --------- | ----- | ---- | ---- | ----- | ------ | ----- | +| [Uniswap v3](https://app.uniswap.org/) | ✅ | ✅ | ✅ | ✅ | - | ✅ | - | ✅ | ✅ | ✅ | ✅ | - | +| [Swapr](https://swapr.eth.link/) | - | ✅ | - | - | - | - | - | - | - | - | - | - | +| [SilverSwap](https://silverswap.io/) | - | - | ✅ | - | - | - | - | - | - | - | - | - | +| [Carbon DeFi](https://carbondefi.xyz/) | - | - | - | - | ✅ | - | - | - | - | - | - | - | +| [Velodrome](https://velodrome.finance/) | - | - | - | - | - | - | ✅ | - | - | - | - | - | +| [Kim](https://www.kim.exchange) | - | - | - | - | - | - | - | ✅ | - | - | - | - | +| [BaseSwap](https://baseswap.fi/) | - | - | - | - | - | - | - | ✅ | - | - | - | - | +| [Unagi](https://unagiswap.xyz/) | - | - | - | - | - | - | - | - | - | ✅ | - | - | +| [Scribe](https://scribe.exchange) | - | - | - | - | - | - | - | - | - | - | ✅ | - | +| [Morphex](https://morphex.exchange/) | - | - | - | - | - | - | - | - | - | - | - | ✅ | diff --git a/packages/frontend/messages/en.json b/packages/frontend/messages/en.json index 58da2322c..2ab2558ea 100644 --- a/packages/frontend/messages/en.json +++ b/packages/frontend/messages/en.json @@ -397,6 +397,8 @@ "errors": { "specification": "An error occurred while processing the campaign specification" }, + "share": "Share campaign setup", + "linkCopied": "Link copied to clipboard", "deploy": "Launch campaign", "connectWallet": "Connect wallet", "congratulations": "Congratulations!", diff --git a/packages/frontend/src/components/create-campaign/form/amm-pool-liquidity-form/index.tsx b/packages/frontend/src/components/create-campaign/form/amm-pool-liquidity-form/index.tsx index 412cd094a..ee383c20f 100644 --- a/packages/frontend/src/components/create-campaign/form/amm-pool-liquidity-form/index.tsx +++ b/packages/frontend/src/components/create-campaign/form/amm-pool-liquidity-form/index.tsx @@ -6,8 +6,14 @@ import { type CampaignPreviewDistributables, } from "@/src/types/campaign"; import { useTranslations } from "next-intl"; -import { useCallback, useEffect, useMemo, useState } from "react"; -import { useChainId } from "wagmi"; +import { + useCallback, + useEffect, + useLayoutEffect, + useMemo, + useState, +} from "react"; +import { useAccount, useChainId, useSwitchChain } from "wagmi"; import { AmmPoolLiquidityType, DistributablesType, @@ -28,6 +34,8 @@ import { AMM_SUPPORTS_TOKENS_RATIO, } from "@/src/commons"; import { WeightingStep } from "../../steps/weighting"; +import { useSearchParams } from "next/navigation"; +import dayjs from "dayjs"; import styles from "./styles.module.css"; @@ -89,10 +97,41 @@ export function AmmPoolLiquidityForm({ }: AmmPoolLiquidityFormProps) { const t = useTranslations("newCampaign"); const chainId = useChainId(); + const { address } = useAccount(); + const { switchChainAsync } = useSwitchChain(); + const searchParams = useSearchParams(); const [payload, setPayload] = useState(initialPayload); + const [autoCompleted, setAutoCompleted] = useState(false); const [errors, setErrors] = useState({}); + // This hook auto fills the form state when a 'payload' query parameter is found in the url, + // indicating that the campaign setup was shared via a link. + useLayoutEffect(() => { + const parsePayload = async () => { + const encodedPayload = searchParams.get("payload"); + if (!encodedPayload) return undefined; + + const payload = JSON.parse(atob(encodedPayload), (key, value) => { + if (typeof value === "string" && /^\d+n$/.test(value)) + return BigInt(value.slice(0, -1)); + + if (key === "startDate" || key === "endDate") + return dayjs(value); + + return value; + }) as AmmPoolLiquidityCampaignPayload; + + if (!payload.pool) return undefined; + + await switchChainAsync({ chainId: payload.pool.chainId }); + setPayload(payload); + }; + + parsePayload(); + setAutoCompleted(true); + }, [address, searchParams, switchChainAsync]); + const previewPayload = useMemo(() => { if (Object.values(errors).some((error) => !!error)) return null; return validatePayload(payload); @@ -159,11 +198,13 @@ export function AmmPoolLiquidityForm({
{tokensRatioSupported && ( )} )} ({ const [view, setView] = useState(View.Form); const [payload, setPayload] = useState(null); + const unsupportedChain = useMemo(() => { + return ( + isConnected && + (!connectedChain || + !chains.some((chain) => chain.id === selectedChain)) + ); + }, [chains, connectedChain, isConnected, selectedChain]); + function handlePreviewOnClick(payload: CampaignPreviewPayload | null) { setPayload(payload); setView(View.Preview); @@ -62,14 +70,6 @@ export function CreateCampaignForm({ router.push("/campaigns/create"); } - const unsupportedChain = useMemo(() => { - return ( - isConnected && - (!connectedChain || - !chains.some((chain) => chain.id === selectedChain)) - ); - }, [chains, connectedChain, isConnected, selectedChain]); - return (
{dexesProtocols.length > 0 && liquityV2Protocols.length > 0 && ( diff --git a/packages/frontend/src/components/create-campaign/preview/index.tsx b/packages/frontend/src/components/create-campaign/preview/index.tsx index 527daf106..d9b7a5b78 100644 --- a/packages/frontend/src/components/create-campaign/preview/index.tsx +++ b/packages/frontend/src/components/create-campaign/preview/index.tsx @@ -1,4 +1,10 @@ -import { Button, Typography, TextField, ErrorText } from "@metrom-xyz/ui"; +import { + Button, + Typography, + TextField, + ErrorText, + ToastNotification, +} from "@metrom-xyz/ui"; import { AmmPoolLiquidityCampaignPreviewPayload, type CampaignPreviewPayload, @@ -45,6 +51,8 @@ import { useChainData } from "@/src/hooks/useChainData"; import { Weighting } from "./weighting"; import { Restrictions } from "./restrictions"; import { useLiquidityByAddresses } from "@/src/hooks/useLiquidityByAddresses"; +import { LinkIcon } from "@/src/assets/link-icon"; +import { toast } from "sonner"; import styles from "./styles.module.css"; @@ -108,6 +116,17 @@ export function CampaignPreview({ enabled: true, }; }, [ammPoolLiquidityCampaign, payload]); + + const sharablePreviewUrl = useMemo(() => { + const data = JSON.stringify(payload, (_, value) => + typeof value === "bigint" ? `${value.toString()}n` : value, + ); + + const url = new URL(window.location.href); + url.searchParams.set("payload", btoa(data)); + + return url.toString(); + }, [payload]); const { loading: loadingLiquidityInRange, liquidityInRange } = useLiquidityInRange(liquidityInRangeParams); @@ -255,6 +274,18 @@ export function CampaignPreview({ setTokensApproved(true); } + const handleShareOnClick = useCallback(() => { + navigator.clipboard.writeText(sharablePreviewUrl).then(() => { + toast.custom((toastId) => ( + + )); + }); + }, [sharablePreviewUrl, t]); + const handleOnStandardDeploy = useCallback(() => { if (simulateCreateErrored) { console.warn( @@ -435,12 +466,22 @@ export function CampaignPreview({ {restrictions && ( )} -
+
{error && ( {t(error)} )} + {tokensApproved && ( diff --git a/packages/frontend/src/components/create-campaign/steps/dex-step/index.tsx b/packages/frontend/src/components/create-campaign/steps/dex-step/index.tsx index 959d2bd72..b32c375ca 100644 --- a/packages/frontend/src/components/create-campaign/steps/dex-step/index.tsx +++ b/packages/frontend/src/components/create-campaign/steps/dex-step/index.tsx @@ -22,6 +22,7 @@ interface DexStepProps extends FormStepBaseProps { } export function DexStep({ + loading, autoCompleted, disabled, dex, @@ -51,12 +52,12 @@ export function DexStep({ }, [autoCompleted]); useEffect(() => { - if (!!dex || availableDexes.length !== 1) return; + if (autoCompleted || !!dex || availableDexes.length !== 1) return; onDexChange({ dex: availableDexes[0], }); setOpen(false); - }, [availableDexes, dex, onDexChange]); + }, [autoCompleted, availableDexes, dex, onDexChange]); const getDexChangeHandler = useCallback( (newDex: DexProtocol) => { @@ -77,6 +78,7 @@ export function DexStep({ return ( ; // TODO: make KPI step work with liquityv2 campaigns export function KpiStep({ + loading, autoCompleted, disabled, pool, @@ -121,7 +122,12 @@ export function KpiStep({ useEffect(() => { setEnabled(false); - }, [chainId]); + onKpiChange({ kpiSpecification: undefined }); + setMinimumPayoutPercentage(0); + setLowerUsdTargetRaw(undefined); + setUpperUsdTargetRaw(undefined); + setError(""); + }, [chainId, onKpiChange]); useEffect(() => { if (autoCompleted && !!kpiSpecification) { @@ -242,6 +248,7 @@ export function KpiStep({ return ( { setOpen(false); }, [chainId]); @@ -47,17 +51,18 @@ export function PoolStep({ }, [autoCompleted]); useEffect(() => { - if (autoCompleted || disabled || !!pool?.id) return; + if (disabled || !!pool?.id) return; setOpen(true); - }, [autoCompleted, disabled, pool]); + }, [disabled, pool]); useEffect(() => { onError({ pool: !!error }); }, [onError, error]); useEffect(() => { - onPoolChange({ pool: undefined }); - }, [onPoolChange, dex?.slug]); + if (!!prevDex && !!dex && prevDex.slug !== dex.slug) + onPoolChange({ pool: undefined }); + }, [onPoolChange, prevDex, dex]); const handlePoolOnChange = useCallback( (newPool: AmmPoolWithTvl) => { @@ -76,6 +81,7 @@ export function PoolStep({ return ( ; export function RestrictionsStep({ + loading, autoCompleted, disabled, restrictions, @@ -63,6 +65,7 @@ export function RestrictionsStep({ const [address, setAddress] = useState(""); const [addresses, setAddresses] = useState([]); + const chainId = useChainId(); const prevRestrictions = usePrevious(restrictions); const unsavedChanges = useMemo(() => { @@ -87,7 +90,14 @@ export function RestrictionsStep({ }, [autoCompleted, enabled, restrictions, prevRestrictions, disabled]); useEffect(() => { - if (restrictions) { + onRestrictionsChange({ restrictions: undefined }); + setAddress(""); + setType(RestrictionType.Blacklist); + setAddresses([]); + }, [chainId, onRestrictionsChange]); + + useEffect(() => { + if (!!restrictions) { const { type, list } = restrictions; setType(type); @@ -185,6 +195,7 @@ export function RestrictionsStep({ return ( diff --git a/packages/frontend/src/components/step/styles.module.css b/packages/frontend/src/components/step/styles.module.css index 14b4cf5fd..b39ac5c06 100644 --- a/packages/frontend/src/components/step/styles.module.css +++ b/packages/frontend/src/components/step/styles.module.css @@ -8,6 +8,10 @@ ease-in-out; } +.root.loading { + @apply animate-pulse; +} + .root.disabled { @apply hover:cursor-not-allowed; } diff --git a/packages/frontend/src/hooks/useActivities.ts b/packages/frontend/src/hooks/useActivities.ts index 1dc2f2935..f73f5e56e 100644 --- a/packages/frontend/src/hooks/useActivities.ts +++ b/packages/frontend/src/hooks/useActivities.ts @@ -1,5 +1,4 @@ import { type Activity } from "@metrom-xyz/sdk"; -import { SupportedChain } from "@metrom-xyz/contracts"; import { METROM_API_CLIENT } from "../commons"; import { useAccount, useChainId } from "wagmi"; import { useQuery } from "@tanstack/react-query"; diff --git a/packages/frontend/src/hooks/useCampaignSetup.ts b/packages/frontend/src/hooks/useCampaignSetup.ts new file mode 100644 index 000000000..689cd4986 --- /dev/null +++ b/packages/frontend/src/hooks/useCampaignSetup.ts @@ -0,0 +1,45 @@ +import { SERVICE_URLS } from "@metrom-xyz/sdk"; +import { useQuery } from "@tanstack/react-query"; +import { ENVIRONMENT } from "../commons/env"; +import type { HookBaseParams } from "../types/hooks"; + +interface UseCampaignSetupParams extends HookBaseParams { + hash?: string | null; +} + +export function useCampaignSetup({ hash, enabled }: UseCampaignSetupParams): { + loading: boolean; + error?: boolean; + setup?: string; +} { + const { + data: setup, + isLoading: loading, + isError: error, + } = useQuery({ + queryKey: ["temporary-data", hash], + queryFn: async () => { + try { + const response = await fetch( + `${SERVICE_URLS[ENVIRONMENT].dataManager}/data?hash=${hash}`, + { + method: "GET", + headers: { + "Content-Type": "application/json", + }, + }, + ); + + if (!response.ok) throw new Error(await response.text()); + + return await response.text(); + } catch (error) { + console.error(`Could not fetch campaign setup: ${error}`); + throw error; + } + }, + enabled: enabled && !!hash, + }); + + return { loading, error, setup }; +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f343ae802..70d8b30a3 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -11084,7 +11084,7 @@ snapshots: '@eslint/config-array@0.21.0': dependencies: '@eslint/object-schema': 2.1.6 - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.1 minimatch: 3.1.2 transitivePeerDependencies: - supports-color @@ -11102,7 +11102,7 @@ snapshots: '@eslint/eslintrc@3.3.1': dependencies: ajv: 6.12.6 - debug: 4.4.0(supports-color@8.1.1) + debug: 4.4.0 espree: 10.3.0 globals: 14.0.0 ignore: 5.3.2 @@ -11859,7 +11859,7 @@ snapshots: bufferutil: 4.0.9 cross-fetch: 4.1.0(encoding@0.1.13) date-fns: 2.30.0 - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.1 eciesjs: 0.4.15 eventemitter2: 6.4.9 readable-stream: 3.6.2 @@ -11883,7 +11883,7 @@ snapshots: '@paulmillr/qr': 0.2.1 bowser: 2.11.0 cross-fetch: 4.1.0(encoding@0.1.13) - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.1 eciesjs: 0.4.15 eth-rpc-errors: 4.0.3 eventemitter2: 6.4.9 @@ -11906,7 +11906,7 @@ snapshots: dependencies: '@ethereumjs/tx': 4.2.0 '@types/debug': 4.1.12 - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.1 semver: 7.7.2 superstruct: 1.0.4 transitivePeerDependencies: @@ -11919,7 +11919,7 @@ snapshots: '@noble/hashes': 1.8.0 '@scure/base': 1.2.6 '@types/debug': 4.1.12 - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.1 pony-cause: 2.1.11 semver: 7.7.2 uuid: 9.0.1 @@ -11933,7 +11933,7 @@ snapshots: '@noble/hashes': 1.8.0 '@scure/base': 1.2.6 '@types/debug': 4.1.12 - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.1 pony-cause: 2.1.11 semver: 7.7.2 uuid: 9.0.1 @@ -13090,7 +13090,7 @@ snapshots: '@rollup/pluginutils@5.1.4(rollup@4.45.1)': dependencies: - '@types/estree': 1.0.7 + '@types/estree': 1.0.8 estree-walker: 2.0.2 picomatch: 4.0.2 optionalDependencies: @@ -15799,7 +15799,7 @@ snapshots: capnp-ts@0.7.0: dependencies: - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.1 tslib: 2.8.1 transitivePeerDependencies: - supports-color @@ -16248,12 +16248,20 @@ snapshots: dependencies: ms: 2.1.3 + debug@4.4.0: + dependencies: + ms: 2.1.3 + debug@4.4.0(supports-color@8.1.1): dependencies: ms: 2.1.3 optionalDependencies: supports-color: 8.1.1 + debug@4.4.1: + dependencies: + ms: 2.1.3 + debug@4.4.1(supports-color@8.1.1): dependencies: ms: 2.1.3 @@ -17118,7 +17126,7 @@ snapshots: ajv: 6.12.6 chalk: 4.1.2 cross-spawn: 7.0.6 - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.1 escape-string-regexp: 4.0.0 eslint-scope: 8.4.0 eslint-visitor-keys: 4.2.1 From f15200b3b80f128d514cdc453aa5d98659548ccf Mon Sep 17 00:00:00 2001 From: Paolo Guerra Date: Wed, 25 Jun 2025 18:52:29 +0200 Subject: [PATCH 05/16] fix(frontend): add missing type --- packages/frontend/src/types/campaign.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/frontend/src/types/campaign.ts b/packages/frontend/src/types/campaign.ts index 1a2a4a30d..c4925426e 100644 --- a/packages/frontend/src/types/campaign.ts +++ b/packages/frontend/src/types/campaign.ts @@ -316,6 +316,7 @@ export interface TargetedNamedCampaign extends Campaign { } export interface FormStepBaseProps { + loading?: boolean; autoCompleted?: boolean; disabled?: boolean; } From 9fc45465044cb422614dc92716ffbc0f3e048577 Mon Sep 17 00:00:00 2001 From: Paolo Guerra Date: Wed, 25 Jun 2025 19:07:05 +0200 Subject: [PATCH 06/16] feat(frontend): update icon and translation --- packages/frontend/messages/en.json | 2 +- .../create-campaign/form/notifications/setup-success.tsx | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/frontend/messages/en.json b/packages/frontend/messages/en.json index 74aaa2cc7..254770981 100644 --- a/packages/frontend/messages/en.json +++ b/packages/frontend/messages/en.json @@ -347,7 +347,7 @@ "message": "Something went wrong" }, "setupSuccess": { - "title": "Setup completed", + "title": "Setup successful", "message": "Campaign imported" } } diff --git a/packages/frontend/src/components/create-campaign/form/notifications/setup-success.tsx b/packages/frontend/src/components/create-campaign/form/notifications/setup-success.tsx index bc4a9e75e..b7bb6a5aa 100644 --- a/packages/frontend/src/components/create-campaign/form/notifications/setup-success.tsx +++ b/packages/frontend/src/components/create-campaign/form/notifications/setup-success.tsx @@ -1,4 +1,4 @@ -import { NewCampaignIcon } from "@/src/assets/new-campaign-icon"; +import { TickIcon } from "@/src/assets/tick-icon"; import { ToastNotification, Typography } from "@metrom-xyz/ui"; import { useTranslations } from "next-intl"; @@ -13,7 +13,7 @@ export function SetupSuccess({ toastId }: SetupSuccessProps) { {t("message")} From a03312de6727bb944db2aa2e38bc941b57dd293c Mon Sep 17 00:00:00 2001 From: Paolo Guerra Date: Thu, 26 Jun 2025 14:42:19 +0200 Subject: [PATCH 07/16] fix(frontend): fix issue with optional steps, add fail notification --- packages/frontend/messages/en.json | 13 +++++++-- .../form/amm-pool-liquidity-form/index.tsx | 12 ++++++-- .../create-campaign/preview/index.tsx | 29 +++++++++---------- .../preview/notifications/link-copied.tsx | 22 ++++++++++++++ .../preview/notifications/link-error.tsx | 22 ++++++++++++++ .../create-campaign/steps/kpi-step/index.tsx | 9 +++--- .../create-campaign/steps/pool-step/index.tsx | 10 ++----- .../steps/range-step/index.tsx | 9 +++--- .../steps/restrictions-step/index.tsx | 9 +++--- .../create-campaign/steps/weighting/index.tsx | 9 +++--- 10 files changed, 101 insertions(+), 43 deletions(-) create mode 100644 packages/frontend/src/components/create-campaign/preview/notifications/link-copied.tsx create mode 100644 packages/frontend/src/components/create-campaign/preview/notifications/link-error.tsx diff --git a/packages/frontend/messages/en.json b/packages/frontend/messages/en.json index 254770981..11c39da49 100644 --- a/packages/frontend/messages/en.json +++ b/packages/frontend/messages/en.json @@ -409,7 +409,6 @@ "setup": "An error occurred while processing the campaign setup" }, "share": "Share campaign setup", - "linkCopied": "Link copied to clipboard", "deploy": "Launch campaign", "connectWallet": "Connect wallet", "congratulations": "Congratulations!", @@ -421,7 +420,17 @@ } }, "allCampaigns": "All campaigns", - "newCampaign": "New campaign" + "newCampaign": "New campaign", + "notifications": { + "linkCopied": { + "title": "Link copied to clipboard", + "message": "Expires in 2 days" + }, + "linkError": { + "title": "Link generation failed", + "message": "Something went wrong" + } + } }, "campaignDuration": { "startDate": "Start date", diff --git a/packages/frontend/src/components/create-campaign/form/amm-pool-liquidity-form/index.tsx b/packages/frontend/src/components/create-campaign/form/amm-pool-liquidity-form/index.tsx index 0b34ead38..9c39c1563 100644 --- a/packages/frontend/src/components/create-campaign/form/amm-pool-liquidity-form/index.tsx +++ b/packages/frontend/src/components/create-campaign/form/amm-pool-liquidity-form/index.tsx @@ -148,8 +148,16 @@ export function AmmPoolLiquidityForm({ }; parsePayload(); - setAutoCompleted(true); - }, [searchParams, chainId, setup, pathname, router, switchChainAsync]); + if (loadingSetup) setAutoCompleted(true); + }, [ + loadingSetup, + searchParams, + chainId, + setup, + pathname, + router, + switchChainAsync, + ]); useEffect(() => { if (!loadingSetup && !!setupError) diff --git a/packages/frontend/src/components/create-campaign/preview/index.tsx b/packages/frontend/src/components/create-campaign/preview/index.tsx index fd3b68c54..575cce88c 100644 --- a/packages/frontend/src/components/create-campaign/preview/index.tsx +++ b/packages/frontend/src/components/create-campaign/preview/index.tsx @@ -53,6 +53,8 @@ import { Restrictions } from "./restrictions"; import { useLiquidityByAddresses } from "@/src/hooks/useLiquidityByAddresses"; import { LinkIcon } from "@/src/assets/link-icon"; import { toast } from "sonner"; +import { LinkCopied } from "./notifications/link-copied"; +import { LinkError } from "./notifications/link-error"; import styles from "./styles.module.css"; @@ -81,6 +83,7 @@ export function CampaignPreview({ const [deploying, setDeploying] = useState(false); const [uploadingSpecification, setUploadingSpecification] = useState(false); const [uploadingSetup, setUploadingSetup] = useState(false); + const [setupError, setSetupError] = useState(false); const [created, setCreated] = useState(false); const [tokensApproved, setTokensApproved] = useState(false); const [specificationHash, setSpecificationHash] = useState(zeroHash); @@ -215,18 +218,13 @@ export function CampaignPreview({ const uploadSetup = useCallback(async () => { if (!!shareUrl) { navigator.clipboard.writeText(shareUrl).then(() => { - toast.custom((toastId) => ( - - )); + toast.custom((toastId) => ); }); return; } + setSetupError(false); setUploadingSetup(true); const setup = JSON.stringify(payload, (_, value) => @@ -254,24 +252,23 @@ export function CampaignPreview({ setShareUrl(url.toString()); navigator.clipboard.writeText(url.toString()).then(() => { - toast.custom((toastId) => ( - - )); + toast.custom((toastId) => ); }); } catch (error) { console.error( `Could not upload setup to data-manager: ${setup}`, error, ); - setError("errors.setup"); + setSetupError(true); } finally { setUploadingSetup(false); } - }, [shareUrl, payload, t]); + }, [shareUrl, payload]); + + useEffect(() => { + if (setupError) + toast.custom((toastId) => ); + }, [setupError]); useEffect(() => { const specification = buildSpecificationBundle(payload); diff --git a/packages/frontend/src/components/create-campaign/preview/notifications/link-copied.tsx b/packages/frontend/src/components/create-campaign/preview/notifications/link-copied.tsx new file mode 100644 index 000000000..47253fa2f --- /dev/null +++ b/packages/frontend/src/components/create-campaign/preview/notifications/link-copied.tsx @@ -0,0 +1,22 @@ +import { LinkIcon } from "@/src/assets/link-icon"; +import { ToastNotification, Typography } from "@metrom-xyz/ui"; +import { useTranslations } from "next-intl"; + +interface LinkCopiedProps { + toastId: string | number; +} + +export function LinkCopied({ toastId }: LinkCopiedProps) { + const t = useTranslations("campaignPreview.notifications.linkCopied"); + + return ( + + {t("message")} + + ); +} diff --git a/packages/frontend/src/components/create-campaign/preview/notifications/link-error.tsx b/packages/frontend/src/components/create-campaign/preview/notifications/link-error.tsx new file mode 100644 index 000000000..51a6ab942 --- /dev/null +++ b/packages/frontend/src/components/create-campaign/preview/notifications/link-error.tsx @@ -0,0 +1,22 @@ +import { ErrorIcon } from "@/src/assets/error-icon"; +import { ToastNotification, Typography } from "@metrom-xyz/ui"; +import { useTranslations } from "next-intl"; + +interface LinkErrorProps { + toastId: string | number; +} + +export function LinkError({ toastId }: LinkErrorProps) { + const t = useTranslations("campaignPreview.notifications.linkError"); + + return ( + + {t("message")} + + ); +} diff --git a/packages/frontend/src/components/create-campaign/steps/kpi-step/index.tsx b/packages/frontend/src/components/create-campaign/steps/kpi-step/index.tsx index 822629cb1..615a68b33 100644 --- a/packages/frontend/src/components/create-campaign/steps/kpi-step/index.tsx +++ b/packages/frontend/src/components/create-campaign/steps/kpi-step/index.tsx @@ -133,8 +133,8 @@ export function KpiStep({ if (autoCompleted && !!kpiSpecification) { setEnabled(true); setOpen(false); - } else setOpen(enabled); - }, [autoCompleted, kpiSpecification, enabled]); + } + }, [autoCompleted, kpiSpecification]); // This hooks is used to disable and close the step when // the kpi specification gets disabled, after the campaign creation. @@ -200,13 +200,14 @@ export function KpiStep({ }, [distributables?.type, onKpiChange]); function handleSwitchOnClick( - _: boolean, + checked: boolean, event: | React.MouseEvent | React.KeyboardEvent, ) { event.stopPropagation(); - setEnabled((enabled) => !enabled); + setEnabled(checked); + setOpen(checked); if (kpiSpecification) { onKpiChange({ kpiSpecification: undefined }); diff --git a/packages/frontend/src/components/create-campaign/steps/pool-step/index.tsx b/packages/frontend/src/components/create-campaign/steps/pool-step/index.tsx index 3d164ca11..ee1006796 100644 --- a/packages/frontend/src/components/create-campaign/steps/pool-step/index.tsx +++ b/packages/frontend/src/components/create-campaign/steps/pool-step/index.tsx @@ -47,13 +47,9 @@ export function PoolStep({ }, [chainId]); useEffect(() => { - if (autoCompleted) setOpen(false); - }, [autoCompleted]); - - useEffect(() => { - if (disabled || !!pool?.id) return; - setOpen(true); - }, [disabled, pool]); + if (autoCompleted || disabled || !!pool?.id) setOpen(false); + else setOpen(true); + }, [autoCompleted, disabled, pool]); useEffect(() => { onError({ pool: !!error }); diff --git a/packages/frontend/src/components/create-campaign/steps/range-step/index.tsx b/packages/frontend/src/components/create-campaign/steps/range-step/index.tsx index 0d280a951..0d9fe8441 100644 --- a/packages/frontend/src/components/create-campaign/steps/range-step/index.tsx +++ b/packages/frontend/src/components/create-campaign/steps/range-step/index.tsx @@ -111,8 +111,8 @@ export function RangeStep({ if (autoCompleted && priceRangeSpecification) { setEnabled(true); setOpen(false); - } else setOpen(enabled); - }, [autoCompleted, priceRangeSpecification, enabled]); + } + }, [autoCompleted, priceRangeSpecification]); // This hooks is used to disable and close the step when // the range specification gets disabled, after the campaign creation. @@ -171,13 +171,14 @@ export function RangeStep({ }, [distributablesType, onRangeChange]); function handleSwitchOnClick( - _: boolean, + checked: boolean, event: | React.MouseEvent | React.KeyboardEvent, ) { event.stopPropagation(); - setEnabled((enabled) => !enabled); + setEnabled(checked); + setOpen(checked); if (priceRangeSpecification) { onRangeChange({ priceRangeSpecification: undefined }); diff --git a/packages/frontend/src/components/create-campaign/steps/restrictions-step/index.tsx b/packages/frontend/src/components/create-campaign/steps/restrictions-step/index.tsx index aea40bdc3..c6dd80dbc 100644 --- a/packages/frontend/src/components/create-campaign/steps/restrictions-step/index.tsx +++ b/packages/frontend/src/components/create-campaign/steps/restrictions-step/index.tsx @@ -138,17 +138,18 @@ export function RestrictionsStep({ if (autoCompleted && !!restrictions) { setEnabled(true); setOpen(false); - } else setOpen(enabled); - }, [autoCompleted, restrictions, enabled]); + } + }, [autoCompleted, restrictions]); function handleSwitchOnClick( - _: boolean, + checked: boolean, event: | React.MouseEvent | React.KeyboardEvent, ) { event.stopPropagation(); - setEnabled((enabled) => !enabled); + setEnabled(checked); + setOpen(checked); if (restrictions) { onRestrictionsChange({ restrictions: undefined }); diff --git a/packages/frontend/src/components/create-campaign/steps/weighting/index.tsx b/packages/frontend/src/components/create-campaign/steps/weighting/index.tsx index f5d5b6d54..cd5ef7fea 100644 --- a/packages/frontend/src/components/create-campaign/steps/weighting/index.tsx +++ b/packages/frontend/src/components/create-campaign/steps/weighting/index.tsx @@ -80,8 +80,8 @@ export function WeightingStep({ if (autoCompleted && !!weighting) { setEnabled(true); setOpen(false); - } else setOpen(enabled); - }, [autoCompleted, weighting, enabled]); + } + }, [autoCompleted, weighting]); useEffect(() => { if (enabled && !open && unsavedChanges) setWarning("notApplied"); @@ -112,13 +112,14 @@ export function WeightingStep({ }, [autoCompleted, enabled, prevWeighting, weighting]); function handleSwitchOnClick( - _: boolean, + checked: boolean, event: | React.MouseEvent | React.KeyboardEvent, ) { event.stopPropagation(); - setEnabled((enabled) => !enabled); + setEnabled(checked); + setOpen(checked); if (weighting) { onWeightingChange({ weighting: undefined }); From c8231687a65d5abba6693c83d7956540cb045cba Mon Sep 17 00:00:00 2001 From: Paolo Guerra Date: Thu, 26 Jun 2025 17:03:24 +0200 Subject: [PATCH 08/16] fix(frontend): fix reward balance check after campaign import, remove autoCompleted state --- .../form/amm-pool-liquidity-form/index.tsx | 16 ++------ .../preview/notifications/link-error.tsx | 4 +- .../create-campaign/steps/dex-step/index.tsx | 4 +- .../steps/end-date-step/index.tsx | 5 --- .../create-campaign/steps/kpi-step/index.tsx | 14 ++----- .../create-campaign/steps/pool-step/index.tsx | 7 ++-- .../steps/range-step/index.tsx | 38 +++++++------------ .../steps/restrictions-step/index.tsx | 10 ++--- .../rewards-step/tokens/preview/reward.tsx | 14 +++++-- .../steps/start-date-step/index.tsx | 5 --- .../create-campaign/steps/weighting/index.tsx | 10 ++--- 11 files changed, 47 insertions(+), 80 deletions(-) diff --git a/packages/frontend/src/components/create-campaign/form/amm-pool-liquidity-form/index.tsx b/packages/frontend/src/components/create-campaign/form/amm-pool-liquidity-form/index.tsx index 9c39c1563..fff15ab5e 100644 --- a/packages/frontend/src/components/create-campaign/form/amm-pool-liquidity-form/index.tsx +++ b/packages/frontend/src/components/create-campaign/form/amm-pool-liquidity-form/index.tsx @@ -117,7 +117,6 @@ export function AmmPoolLiquidityForm({ }); const [payload, setPayload] = useState(initialPayload); - const [autoCompleted, setAutoCompleted] = useState(false); const [errors, setErrors] = useState({}); // This hook auto fills the form state when a campaign setup is available. @@ -142,13 +141,14 @@ export function AmmPoolLiquidityForm({ const params = new URLSearchParams(searchParams.toString()); params.delete("setup"); - router.replace(`${pathname}?${params.toString()}`); + router.replace(`${pathname}?${params.toString()}`, { + scroll: false, + }); setPayload(payload); }; parsePayload(); - if (loadingSetup) setAutoCompleted(true); }, [ loadingSetup, searchParams, @@ -162,6 +162,7 @@ export function AmmPoolLiquidityForm({ useEffect(() => { if (!loadingSetup && !!setupError) toast.custom((toastId) => ); + if (!loadingSetup && !setupError && !!setup) toast.custom((toastId) => ); }, [loadingSetup, setupError, setup]); @@ -235,14 +236,12 @@ export function AmmPoolLiquidityForm({
{tokensRatioSupported && ( {t("message")} diff --git a/packages/frontend/src/components/create-campaign/steps/dex-step/index.tsx b/packages/frontend/src/components/create-campaign/steps/dex-step/index.tsx index b32c375ca..029e36835 100644 --- a/packages/frontend/src/components/create-campaign/steps/dex-step/index.tsx +++ b/packages/frontend/src/components/create-campaign/steps/dex-step/index.tsx @@ -52,12 +52,12 @@ export function DexStep({ }, [autoCompleted]); useEffect(() => { - if (autoCompleted || !!dex || availableDexes.length !== 1) return; + if (loading || !!dex || availableDexes.length !== 1) return; onDexChange({ dex: availableDexes[0], }); setOpen(false); - }, [autoCompleted, availableDexes, dex, onDexChange]); + }, [loading, availableDexes, dex, onDexChange]); const getDexChangeHandler = useCallback( (newDex: DexProtocol) => { diff --git a/packages/frontend/src/components/create-campaign/steps/end-date-step/index.tsx b/packages/frontend/src/components/create-campaign/steps/end-date-step/index.tsx index 25257010e..4902f3042 100644 --- a/packages/frontend/src/components/create-campaign/steps/end-date-step/index.tsx +++ b/packages/frontend/src/components/create-campaign/steps/end-date-step/index.tsx @@ -60,7 +60,6 @@ const DURATION_PRESETS: DurationPreset[] = [ export function EndDateStep({ loading, - autoCompleted, disabled, startDate, endDate, @@ -92,10 +91,6 @@ export function EndDateStep({ setOpen(false); }, [chainId]); - useEffect(() => { - if (autoCompleted) setOpen(false); - }, [autoCompleted]); - useEffect(() => { if (disabled || !!endDate) return; setOpen(true); diff --git a/packages/frontend/src/components/create-campaign/steps/kpi-step/index.tsx b/packages/frontend/src/components/create-campaign/steps/kpi-step/index.tsx index 615a68b33..451e615f7 100644 --- a/packages/frontend/src/components/create-campaign/steps/kpi-step/index.tsx +++ b/packages/frontend/src/components/create-campaign/steps/kpi-step/index.tsx @@ -37,7 +37,6 @@ type ErrorMessage = LocalizedMessage<"newCampaign.form.base.kpi">; // TODO: make KPI step work with liquityv2 campaigns export function KpiStep({ loading, - autoCompleted, disabled, pool, distributables, @@ -130,23 +129,18 @@ export function KpiStep({ }, [chainId, onKpiChange]); useEffect(() => { - if (autoCompleted && !!kpiSpecification) { + if (!!kpiSpecification) { setEnabled(true); setOpen(false); } - }, [autoCompleted, kpiSpecification]); + }, [kpiSpecification]); // This hooks is used to disable and close the step when // the kpi specification gets disabled, after the campaign creation. useEffect(() => { - if ( - !autoCompleted && - enabled && - !!prevKpiSpecification && - !kpiSpecification - ) + if (enabled && !!prevKpiSpecification && !kpiSpecification) setEnabled(false); - }, [autoCompleted, enabled, kpiSpecification, prevKpiSpecification]); + }, [enabled, kpiSpecification, prevKpiSpecification]); useEffect(() => { if ( diff --git a/packages/frontend/src/components/create-campaign/steps/pool-step/index.tsx b/packages/frontend/src/components/create-campaign/steps/pool-step/index.tsx index ee1006796..68d3df950 100644 --- a/packages/frontend/src/components/create-campaign/steps/pool-step/index.tsx +++ b/packages/frontend/src/components/create-campaign/steps/pool-step/index.tsx @@ -1,4 +1,4 @@ -import { useCallback, useEffect, useState } from "react"; +import { useCallback, useEffect, useLayoutEffect, useState } from "react"; import { useChainId } from "wagmi"; import { useTranslations } from "next-intl"; import { type AmmPoolWithTvl } from "@metrom-xyz/sdk"; @@ -28,7 +28,6 @@ interface PoolStepProps extends FormStepBaseProps { export function PoolStep({ loading, - autoCompleted, disabled, dex, pool, @@ -47,9 +46,9 @@ export function PoolStep({ }, [chainId]); useEffect(() => { - if (autoCompleted || disabled || !!pool?.id) setOpen(false); + if (disabled || !!pool?.id) setOpen(false); else setOpen(true); - }, [autoCompleted, disabled, pool]); + }, [disabled, pool]); useEffect(() => { onError({ pool: !!error }); diff --git a/packages/frontend/src/components/create-campaign/steps/range-step/index.tsx b/packages/frontend/src/components/create-campaign/steps/range-step/index.tsx index 0d9fe8441..8112fe9f1 100644 --- a/packages/frontend/src/components/create-campaign/steps/range-step/index.tsx +++ b/packages/frontend/src/components/create-campaign/steps/range-step/index.tsx @@ -46,7 +46,6 @@ interface RangeStepProps extends FormStepBaseProps { type ErrorMessage = LocalizedMessage<"newCampaign.form.ammPoolLiquidity.range">; export function RangeStep({ - autoCompleted, disabled, distributablesType, pool, @@ -65,6 +64,7 @@ export function RangeStep({ const [to, setTo] = useState(); const prevRangeSpecification = usePrevious(priceRangeSpecification); + const prevPoolId = usePrevious(pool?.id); const chainId = useChainId(); const { liquidityDensity, loading: loadingLiquidityDensity } = useLiquidityDensity({ @@ -89,47 +89,37 @@ export function RangeStep({ return tickToScaledPrice(activeTickIdx, pool, token0To1); }, [liquidityDensity, token0To1, pool]); + // FIXME: changing dex doesn't reset range it it's enabled useEffect(() => { + if (!prevPoolId || prevPoolId === pool?.id) return; + + onRangeChange({ priceRangeSpecification: undefined }); setFrom(undefined); setTo(undefined); - }, [pool?.id]); + }, [prevPoolId, pool?.id]); useEffect(() => { setOpen(false); }, [chainId]); useEffect(() => { - if (priceRangeSpecification) { - const { from, to } = priceRangeSpecification; - - setFrom(from); - setTo(to); - } - }, [priceRangeSpecification]); + setFrom(priceRangeSpecification?.from); + setTo(priceRangeSpecification?.to); + }, [priceRangeSpecification?.from, priceRangeSpecification?.to]); useEffect(() => { - if (autoCompleted && priceRangeSpecification) { + if (!!priceRangeSpecification) { setEnabled(true); setOpen(false); } - }, [autoCompleted, priceRangeSpecification]); + }, [priceRangeSpecification]); // This hooks is used to disable and close the step when // the range specification gets disabled, after the campaign creation. useEffect(() => { - if ( - !autoCompleted && - enabled && - !!prevRangeSpecification && - !priceRangeSpecification - ) + if (enabled && !!prevRangeSpecification && !priceRangeSpecification) setEnabled(false); - }, [ - autoCompleted, - enabled, - prevRangeSpecification, - priceRangeSpecification, - ]); + }, [enabled, prevRangeSpecification, priceRangeSpecification]); useEffect(() => { if (!from && !to) setError(""); @@ -180,7 +170,7 @@ export function RangeStep({ setEnabled(checked); setOpen(checked); - if (priceRangeSpecification) { + if (!checked) { onRangeChange({ priceRangeSpecification: undefined }); setFrom(undefined); setTo(undefined); diff --git a/packages/frontend/src/components/create-campaign/steps/restrictions-step/index.tsx b/packages/frontend/src/components/create-campaign/steps/restrictions-step/index.tsx index c6dd80dbc..f2455d000 100644 --- a/packages/frontend/src/components/create-campaign/steps/restrictions-step/index.tsx +++ b/packages/frontend/src/components/create-campaign/steps/restrictions-step/index.tsx @@ -50,7 +50,6 @@ type ErrorMessage = LocalizedMessage<"newCampaign.form.base.restrictions">; export function RestrictionsStep({ loading, - autoCompleted, disabled, restrictions, onRestrictionsChange, @@ -84,10 +83,9 @@ export function RestrictionsStep({ // This hooks is used to disable and close the step when // the restrictions gets disabled, after the campaign creation. useEffect(() => { - if (!autoCompleted && enabled && !!prevRestrictions && !restrictions) - setEnabled(false); + if (enabled && !!prevRestrictions && !restrictions) setEnabled(false); if (disabled) setEnabled(false); - }, [autoCompleted, enabled, restrictions, prevRestrictions, disabled]); + }, [enabled, restrictions, prevRestrictions, disabled]); useEffect(() => { onRestrictionsChange({ restrictions: undefined }); @@ -135,11 +133,11 @@ export function RestrictionsStep({ }, [enabled, restrictions, error, onError]); useEffect(() => { - if (autoCompleted && !!restrictions) { + if (!!restrictions) { setEnabled(true); setOpen(false); } - }, [autoCompleted, restrictions]); + }, [restrictions]); function handleSwitchOnClick( checked: boolean, diff --git a/packages/frontend/src/components/create-campaign/steps/rewards-step/tokens/preview/reward.tsx b/packages/frontend/src/components/create-campaign/steps/rewards-step/tokens/preview/reward.tsx index b7936a077..05feabf2f 100644 --- a/packages/frontend/src/components/create-campaign/steps/rewards-step/tokens/preview/reward.tsx +++ b/packages/frontend/src/components/create-campaign/steps/rewards-step/tokens/preview/reward.tsx @@ -13,7 +13,6 @@ import type { Address } from "viem"; import classNames from "classnames"; import { formatUsdAmount } from "@/src/utils/format"; import { RemoteLogo } from "@/src/components/remote-logo"; -import { MAX_U256 } from "@/src/commons"; import type { WhitelistedErc20TokenAmount } from "@/src/types/common"; import type { TokensErrorMessage } from ".."; @@ -61,10 +60,10 @@ export function Reward({ const distributionRate = (rewardAmount.formatted * 3_600) / campaignDuration; - const balance = rewardTokenBalance || MAX_U256; + const balance = rewardTokenBalance || 0n; const error = - rewardAmount.raw > balance + !!address && rewardAmount.raw > balance ? "errors.insufficientBalance" : distributionRate < reward.token.minimumRate.formatted ? "errors.lowDistributionRate" @@ -72,7 +71,14 @@ export function Reward({ onError(reward.token.address, error); setError(!!error); - }, [campaignDuration, onError, reward, rewardAmount, rewardTokenBalance]); + }, [ + address, + campaignDuration, + onError, + reward, + rewardAmount, + rewardTokenBalance, + ]); useEffect(() => { if (!rewardRawValue) return; diff --git a/packages/frontend/src/components/create-campaign/steps/start-date-step/index.tsx b/packages/frontend/src/components/create-campaign/steps/start-date-step/index.tsx index 8109848a7..e19739cdb 100644 --- a/packages/frontend/src/components/create-campaign/steps/start-date-step/index.tsx +++ b/packages/frontend/src/components/create-campaign/steps/start-date-step/index.tsx @@ -29,7 +29,6 @@ interface StartDateStepProps extends FormStepBaseProps { export function StartDateStep({ loading, - autoCompleted, disabled, startDate, endDate, @@ -60,10 +59,6 @@ export function StartDateStep({ setOpen(false); }, [chainId]); - useEffect(() => { - if (autoCompleted) setOpen(false); - }, [autoCompleted]); - useEffect(() => { if (disabled || !!startDate) return; setOpen(true); diff --git a/packages/frontend/src/components/create-campaign/steps/weighting/index.tsx b/packages/frontend/src/components/create-campaign/steps/weighting/index.tsx index cd5ef7fea..5e00206d1 100644 --- a/packages/frontend/src/components/create-campaign/steps/weighting/index.tsx +++ b/packages/frontend/src/components/create-campaign/steps/weighting/index.tsx @@ -31,7 +31,6 @@ interface WeightingStepProps extends FormStepBaseProps { type ErrorMessage = LocalizedMessage<"newCampaign.form.base.weighting">; export function WeightingStep({ - autoCompleted, disabled, pool, distributablesType, @@ -77,11 +76,11 @@ export function WeightingStep({ }, [weighting]); useEffect(() => { - if (autoCompleted && !!weighting) { + if (!!weighting) { setEnabled(true); setOpen(false); } - }, [autoCompleted, weighting]); + }, [weighting]); useEffect(() => { if (enabled && !open && unsavedChanges) setWarning("notApplied"); @@ -107,9 +106,8 @@ export function WeightingStep({ // This hooks is used to disable and close the step when // the weighting gets disabled, after the campaign creation. useEffect(() => { - if (!autoCompleted && enabled && !!prevWeighting && !weighting) - setEnabled(false); - }, [autoCompleted, enabled, prevWeighting, weighting]); + if (enabled && !!prevWeighting && !weighting) setEnabled(false); + }, [enabled, prevWeighting, weighting]); function handleSwitchOnClick( checked: boolean, From d4eac74fe92aaec22cca35d4427b0a1bb353a06a Mon Sep 17 00:00:00 2001 From: Paolo Guerra Date: Thu, 26 Jun 2025 17:32:12 +0200 Subject: [PATCH 09/16] fix(frontend): minor fix for kpi and restrictions steps --- .../create-campaign/steps/kpi-step/index.tsx | 12 +++++------- .../steps/restrictions-step/index.tsx | 8 ++------ 2 files changed, 7 insertions(+), 13 deletions(-) diff --git a/packages/frontend/src/components/create-campaign/steps/kpi-step/index.tsx b/packages/frontend/src/components/create-campaign/steps/kpi-step/index.tsx index 451e615f7..d8e748bd3 100644 --- a/packages/frontend/src/components/create-campaign/steps/kpi-step/index.tsx +++ b/packages/frontend/src/components/create-campaign/steps/kpi-step/index.tsx @@ -110,13 +110,11 @@ export function KpiStep({ : undefined; useEffect(() => { - if (!!kpiSpecification) { - const { goal, minimumPayoutPercentage } = kpiSpecification; - - setLowerUsdTargetRaw(goal.lowerUsdTarget); - setUpperUsdTargetRaw(goal.upperUsdTarget); - setMinimumPayoutPercentage(minimumPayoutPercentage ?? 0); - } + setLowerUsdTargetRaw(kpiSpecification?.goal.lowerUsdTarget); + setUpperUsdTargetRaw(kpiSpecification?.goal.upperUsdTarget); + setMinimumPayoutPercentage( + kpiSpecification?.minimumPayoutPercentage ?? 0, + ); }, [kpiSpecification]); useEffect(() => { diff --git a/packages/frontend/src/components/create-campaign/steps/restrictions-step/index.tsx b/packages/frontend/src/components/create-campaign/steps/restrictions-step/index.tsx index f2455d000..0133b5ca7 100644 --- a/packages/frontend/src/components/create-campaign/steps/restrictions-step/index.tsx +++ b/packages/frontend/src/components/create-campaign/steps/restrictions-step/index.tsx @@ -95,12 +95,8 @@ export function RestrictionsStep({ }, [chainId, onRestrictionsChange]); useEffect(() => { - if (!!restrictions) { - const { type, list } = restrictions; - - setType(type); - setAddresses(list); - } + setType(restrictions?.type || RestrictionType.Blacklist); + setAddresses(restrictions?.list || []); }, [restrictions]); useEffect(() => { From d0de6e48a256ff12b9467057082438c7320852fc Mon Sep 17 00:00:00 2001 From: Paolo Guerra Date: Fri, 27 Jun 2025 12:25:49 +0200 Subject: [PATCH 10/16] feat(frontend): remove scrolls on campaigns filters change, minor css change --- packages/frontend/messages/en.json | 2 +- packages/frontend/src/components/campaigns/index.tsx | 6 ++++-- .../components/create-campaign/preview/styles.module.css | 2 +- .../components/create-campaign/steps/range-step/index.tsx | 2 +- 4 files changed, 7 insertions(+), 5 deletions(-) diff --git a/packages/frontend/messages/en.json b/packages/frontend/messages/en.json index 11c39da49..ba3ddfbc9 100644 --- a/packages/frontend/messages/en.json +++ b/packages/frontend/messages/en.json @@ -424,7 +424,7 @@ "notifications": { "linkCopied": { "title": "Link copied to clipboard", - "message": "Expires in 2 days" + "message": "Expires in 1 day" }, "linkError": { "title": "Link generation failed", diff --git a/packages/frontend/src/components/campaigns/index.tsx b/packages/frontend/src/components/campaigns/index.tsx index f2b202abd..e43cddc52 100644 --- a/packages/frontend/src/components/campaigns/index.tsx +++ b/packages/frontend/src/components/campaigns/index.tsx @@ -304,7 +304,9 @@ export function Campaigns() { const params = new URLSearchParams(searchParams.toString()); if (!chain.query) params.delete("chain"); else params.set("chain", chain.query); - router.replace(`${pathname}?${params.toString()}`); + router.replace(`${pathname}?${params.toString()}`, { + scroll: false, + }); }, [pathname, router, searchParams], ); @@ -329,7 +331,7 @@ export function Campaigns() { const params = new URLSearchParams(searchParams.toString()); params.delete("chain"); - router.replace(`${pathname}?${params.toString()}`); + router.replace(`${pathname}?${params.toString()}`, { scroll: false }); }, [pathname, searchParams, router]); function handlePreviousPage() { diff --git a/packages/frontend/src/components/create-campaign/preview/styles.module.css b/packages/frontend/src/components/create-campaign/preview/styles.module.css index b87356187..625da288b 100644 --- a/packages/frontend/src/components/create-campaign/preview/styles.module.css +++ b/packages/frontend/src/components/create-campaign/preview/styles.module.css @@ -28,7 +28,7 @@ } .buttonsContainer { - @apply w-full flex gap-4 items-center justify-center; + @apply w-full flex flex-col sm:flex-row gap-4 items-center justify-center; } .button { diff --git a/packages/frontend/src/components/create-campaign/steps/range-step/index.tsx b/packages/frontend/src/components/create-campaign/steps/range-step/index.tsx index 8112fe9f1..e5a50a530 100644 --- a/packages/frontend/src/components/create-campaign/steps/range-step/index.tsx +++ b/packages/frontend/src/components/create-campaign/steps/range-step/index.tsx @@ -96,7 +96,7 @@ export function RangeStep({ onRangeChange({ priceRangeSpecification: undefined }); setFrom(undefined); setTo(undefined); - }, [prevPoolId, pool?.id]); + }, [prevPoolId, pool?.id, onRangeChange]); useEffect(() => { setOpen(false); From 8e6c21c305d16d9fed874666f3a83586bffd8913 Mon Sep 17 00:00:00 2001 From: Paolo Guerra Date: Fri, 27 Jun 2025 15:10:22 +0200 Subject: [PATCH 11/16] feat(frontend): reset range on dex or pool change, autocomplete fix --- .../form/amm-pool-liquidity-form/index.tsx | 3 +++ .../create-campaign/steps/kpi-step/index.tsx | 17 +++-------------- .../create-campaign/steps/range-step/index.tsx | 15 ++++++++++----- .../steps/restrictions-step/index.tsx | 17 ++++++----------- packages/frontend/src/types/campaign.ts | 2 +- 5 files changed, 23 insertions(+), 31 deletions(-) diff --git a/packages/frontend/src/components/create-campaign/form/amm-pool-liquidity-form/index.tsx b/packages/frontend/src/components/create-campaign/form/amm-pool-liquidity-form/index.tsx index fff15ab5e..5c72716b8 100644 --- a/packages/frontend/src/components/create-campaign/form/amm-pool-liquidity-form/index.tsx +++ b/packages/frontend/src/components/create-campaign/form/amm-pool-liquidity-form/index.tsx @@ -139,6 +139,8 @@ export function AmmPoolLiquidityForm({ if (payload.pool.chainId !== chainId) await switchChainAsync({ chainId: payload.pool.chainId }); + // Remove the 'setup' parameter from the URL after parsing. + // This ensures the form behaves correctly after autocomplete completes. const params = new URLSearchParams(searchParams.toString()); params.delete("setup"); router.replace(`${pathname}?${params.toString()}`, { @@ -306,6 +308,7 @@ export function AmmPoolLiquidityForm({ payload.pool.liquidityType === AmmPoolLiquidityType.Concentrated && ( (""); const [minimumPayoutPercentage, setMinimumPayoutPercentage] = - useState(0); + useState(kpiSpecification?.minimumPayoutPercentage || 0); const [lowerUsdTargetRaw, setLowerUsdTargetRaw] = useState< number | undefined - >(); + >(kpiSpecification?.goal.lowerUsdTarget); const [upperUsdTargetRaw, setUpperUsdTargetRaw] = useState< number | undefined - >(); + >(kpiSpecification?.goal.upperUsdTarget); const prevKpiSpecification = usePrevious(kpiSpecification); - const chainId = useChainId(); const totalRewardsUsdAmount = useMemo(() => { if (!distributables || !distributables.tokens) return 0; @@ -117,15 +115,6 @@ export function KpiStep({ ); }, [kpiSpecification]); - useEffect(() => { - setEnabled(false); - onKpiChange({ kpiSpecification: undefined }); - setMinimumPayoutPercentage(0); - setLowerUsdTargetRaw(undefined); - setUpperUsdTargetRaw(undefined); - setError(""); - }, [chainId, onKpiChange]); - useEffect(() => { if (!!kpiSpecification) { setEnabled(true); diff --git a/packages/frontend/src/components/create-campaign/steps/range-step/index.tsx b/packages/frontend/src/components/create-campaign/steps/range-step/index.tsx index e5a50a530..0a9ebb6db 100644 --- a/packages/frontend/src/components/create-campaign/steps/range-step/index.tsx +++ b/packages/frontend/src/components/create-campaign/steps/range-step/index.tsx @@ -46,6 +46,7 @@ interface RangeStepProps extends FormStepBaseProps { type ErrorMessage = LocalizedMessage<"newCampaign.form.ammPoolLiquidity.range">; export function RangeStep({ + autoCompleting, disabled, distributablesType, pool, @@ -60,8 +61,12 @@ export function RangeStep({ const [error, setError] = useState(""); const [warning, setWarning] = useState(""); - const [from, setFrom] = useState(); - const [to, setTo] = useState(); + const [from, setFrom] = useState( + priceRangeSpecification?.from, + ); + const [to, setTo] = useState( + priceRangeSpecification?.to, + ); const prevRangeSpecification = usePrevious(priceRangeSpecification); const prevPoolId = usePrevious(pool?.id); @@ -89,14 +94,14 @@ export function RangeStep({ return tickToScaledPrice(activeTickIdx, pool, token0To1); }, [liquidityDensity, token0To1, pool]); - // FIXME: changing dex doesn't reset range it it's enabled useEffect(() => { - if (!prevPoolId || prevPoolId === pool?.id) return; + // Avoid resetting the range if the form is autocompleting + if (autoCompleting || prevPoolId === pool?.id) return; onRangeChange({ priceRangeSpecification: undefined }); setFrom(undefined); setTo(undefined); - }, [prevPoolId, pool?.id, onRangeChange]); + }, [autoCompleting, prevPoolId, pool?.id, onRangeChange]); useEffect(() => { setOpen(false); diff --git a/packages/frontend/src/components/create-campaign/steps/restrictions-step/index.tsx b/packages/frontend/src/components/create-campaign/steps/restrictions-step/index.tsx index 0133b5ca7..e5d9cd684 100644 --- a/packages/frontend/src/components/create-campaign/steps/restrictions-step/index.tsx +++ b/packages/frontend/src/components/create-campaign/steps/restrictions-step/index.tsx @@ -34,7 +34,6 @@ import { CsvAddressesImport } from "./csv-addresses-import"; import { Avatar } from "@/src/components/avatar/avatar"; import { Account } from "@/src/components/account"; import { InfoMessage } from "@/src/components/info-message"; -import { useChainId } from "wagmi"; import styles from "./styles.module.css"; @@ -60,11 +59,14 @@ export function RestrictionsStep({ const [enabled, setEnabled] = useState(false); const [error, setError] = useState(""); const [warning, setWarning] = useState(""); - const [type, setType] = useState(RestrictionType.Blacklist); + const [type, setType] = useState( + restrictions?.type || RestrictionType.Blacklist, + ); const [address, setAddress] = useState(""); - const [addresses, setAddresses] = useState([]); + const [addresses, setAddresses] = useState( + restrictions?.list || [], + ); - const chainId = useChainId(); const prevRestrictions = usePrevious(restrictions); const unsavedChanges = useMemo(() => { @@ -87,13 +89,6 @@ export function RestrictionsStep({ if (disabled) setEnabled(false); }, [enabled, restrictions, prevRestrictions, disabled]); - useEffect(() => { - onRestrictionsChange({ restrictions: undefined }); - setAddress(""); - setType(RestrictionType.Blacklist); - setAddresses([]); - }, [chainId, onRestrictionsChange]); - useEffect(() => { setType(restrictions?.type || RestrictionType.Blacklist); setAddresses(restrictions?.list || []); diff --git a/packages/frontend/src/types/campaign.ts b/packages/frontend/src/types/campaign.ts index c4925426e..411979acf 100644 --- a/packages/frontend/src/types/campaign.ts +++ b/packages/frontend/src/types/campaign.ts @@ -317,6 +317,6 @@ export interface TargetedNamedCampaign extends Campaign { export interface FormStepBaseProps { loading?: boolean; - autoCompleted?: boolean; + autoCompleting?: boolean; disabled?: boolean; } From 304fc3fef6d2aaeaef659436f0d899c199867faf Mon Sep 17 00:00:00 2001 From: Paolo Guerra Date: Fri, 27 Jun 2025 17:33:10 +0200 Subject: [PATCH 12/16] feat(frontend/chains): add autocomplete support for liquityv2 form --- packages/chains/src/types/protocol.ts | 5 + .../form/amm-pool-liquidity-form/index.tsx | 30 +++--- .../create-campaign/form/header/index.tsx | 2 +- .../form/liquity-v2-forks-form/index.tsx | 91 ++++++++++++++++++- .../steps/liquity-v2-action-step/index.tsx | 17 ++-- .../steps/liquity-v2-brand-step/index.tsx | 17 ++-- .../liquity-v2-collateral-step/index.tsx | 26 +++--- .../frontend/src/hooks/useProtocolsInChain.ts | 10 +- packages/frontend/src/types/campaign.ts | 6 +- packages/frontend/src/utils/campaign.ts | 12 +++ 10 files changed, 166 insertions(+), 50 deletions(-) diff --git a/packages/chains/src/types/protocol.ts b/packages/chains/src/types/protocol.ts index 94666d6e8..d5296f43b 100644 --- a/packages/chains/src/types/protocol.ts +++ b/packages/chains/src/types/protocol.ts @@ -6,6 +6,7 @@ import { SupportedLiquityV2, TargetType, } from "@metrom-xyz/sdk"; +import { SupportedChain } from "@metrom-xyz/contracts"; export enum ProtocolType { Dex = "dex", @@ -34,6 +35,10 @@ export interface DexProtocol extends ProtocolBase { supportsFetchAllPools: boolean; } +export type WithChain = T & { + chainId: SupportedChain; +}; + export interface LiquityV2Protocol extends ProtocolBase { type: ProtocolType.LiquityV2; debtToken: Erc20Token; diff --git a/packages/frontend/src/components/create-campaign/form/amm-pool-liquidity-form/index.tsx b/packages/frontend/src/components/create-campaign/form/amm-pool-liquidity-form/index.tsx index 5c72716b8..51e4aa576 100644 --- a/packages/frontend/src/components/create-campaign/form/amm-pool-liquidity-form/index.tsx +++ b/packages/frontend/src/components/create-campaign/form/amm-pool-liquidity-form/index.tsx @@ -4,6 +4,7 @@ import { AmmPoolLiquidityCampaignPreviewPayload, type AmmPoolLiquidityCampaignPayloadPart, type CampaignPreviewDistributables, + CampaignKind, } from "@/src/types/campaign"; import { useTranslations } from "next-intl"; import { @@ -34,15 +35,16 @@ import { AMM_SUPPORTS_TOKENS_RATIO, } from "@/src/commons"; import { WeightingStep } from "../../steps/weighting"; -import { usePathname, useSearchParams } from "next/navigation"; +import { usePathname } from "@/i18n/routing"; import dayjs from "dayjs"; import { useCampaignSetup } from "@/src/hooks/useCampaignSetup"; import { toast } from "sonner"; import { SetupFail } from "../notifications/setup-fail"; import { SetupSuccess } from "../notifications/setup-success"; -import { useRouter } from "next/navigation"; +import { useRouter, useSearchParams } from "next/navigation"; import styles from "./styles.module.css"; +import { decodeCampaignSetup } from "@/src/utils/campaign"; function validatePayload( payload: AmmPoolLiquidityCampaignPayload, @@ -121,23 +123,19 @@ export function AmmPoolLiquidityForm({ // This hook auto fills the form state when a campaign setup is available. useLayoutEffect(() => { - const parsePayload = async () => { - if (!setup) return undefined; + if (!setup) return; - const payload = JSON.parse(setup, (key, value) => { - if (typeof value === "string" && /^\d+n$/.test(value)) - return BigInt(value.slice(0, -1)); + const autocompletePayload = async () => { + const decodedSetup = decodeCampaignSetup(setup); + if (decodedSetup.kind !== CampaignKind.AmmPoolLiquidity) return; - if (key === "startDate" || key === "endDate") - return dayjs(value); + const payload = decodedSetup as AmmPoolLiquidityCampaignPayload; + if (!payload.dex?.chainId) return; - return value; - }) as AmmPoolLiquidityCampaignPayload; + const { dex } = payload; - if (!payload.pool) return undefined; - - if (payload.pool.chainId !== chainId) - await switchChainAsync({ chainId: payload.pool.chainId }); + if (dex.chainId !== chainId) + await switchChainAsync({ chainId: dex.chainId }); // Remove the 'setup' parameter from the URL after parsing. // This ensures the form behaves correctly after autocomplete completes. @@ -150,7 +148,7 @@ export function AmmPoolLiquidityForm({ setPayload(payload); }; - parsePayload(); + autocompletePayload(); }, [ loadingSetup, searchParams, diff --git a/packages/frontend/src/components/create-campaign/form/header/index.tsx b/packages/frontend/src/components/create-campaign/form/header/index.tsx index ee98262fe..f2b21b32d 100644 --- a/packages/frontend/src/components/create-campaign/form/header/index.tsx +++ b/packages/frontend/src/components/create-campaign/form/header/index.tsx @@ -23,7 +23,7 @@ export function FormHeader({ type }: FormHeaderProps) { const router = useRouter(); function handleBackOnClick() { - router.back(); + router.push("/campaigns/create"); } return ( diff --git a/packages/frontend/src/components/create-campaign/form/liquity-v2-forks-form/index.tsx b/packages/frontend/src/components/create-campaign/form/liquity-v2-forks-form/index.tsx index 8c7d3d90e..15968c12b 100644 --- a/packages/frontend/src/components/create-campaign/form/liquity-v2-forks-form/index.tsx +++ b/packages/frontend/src/components/create-campaign/form/liquity-v2-forks-form/index.tsx @@ -4,10 +4,17 @@ import { type LiquityV2CampaignPayload, type LiquityV2CampaignPayloadPart, type CampaignPreviewDistributables, + CampaignKind, } from "@/src/types/campaign"; import { useTranslations } from "next-intl"; -import { useCallback, useEffect, useMemo, useState } from "react"; -import { useChainId } from "wagmi"; +import { + useCallback, + useEffect, + useLayoutEffect, + useMemo, + useState, +} from "react"; +import { useChainId, useSwitchChain } from "wagmi"; import { DistributablesType } from "@metrom-xyz/sdk"; import { StartDateStep } from "../../steps/start-date-step"; import { EndDateStep } from "../../steps/end-date-step"; @@ -18,6 +25,13 @@ import { ArrowRightIcon } from "@/src/assets/arrow-right-icon"; import { LiquityV2BrandStep } from "../../steps/liquity-v2-brand-step"; import { LiquityV2ActionStep } from "../../steps/liquity-v2-action-step"; import { LiquityV2CollateralStep } from "../../steps/liquity-v2-collateral-step"; +import { useRouter, useSearchParams } from "next/navigation"; +import { usePathname } from "@/src/i18n/routing"; +import { useCampaignSetup } from "@/src/hooks/useCampaignSetup"; +import { decodeCampaignSetup } from "@/src/utils/campaign"; +import { toast } from "sonner"; +import { SetupFail } from "../notifications/setup-fail"; +import { SetupSuccess } from "../notifications/setup-success"; import styles from "./styles.module.css"; @@ -83,10 +97,72 @@ export function LiquityV2ForksForm({ }: LiquityV2ForksFormProps) { const t = useTranslations("newCampaign"); const chainId = useChainId(); + const router = useRouter(); + const pathname = usePathname(); + const { switchChainAsync, isPending: switchingChain } = useSwitchChain(); + const searchParams = useSearchParams(); + const { + loading: loadingSetup, + error: setupError, + setup, + } = useCampaignSetup({ + hash: searchParams.get("setup"), + enabled: !!searchParams.get("setup"), + }); const [payload, setPayload] = useState(initialPayload); const [errors, setErrors] = useState({}); + // This hook auto fills the form state when a campaign setup is available. + useLayoutEffect(() => { + if (!setup) return; + + const autocompletePayload = async () => { + const decodedSetup = decodeCampaignSetup(setup); + if ( + decodedSetup.kind !== CampaignKind.LiquityV2Debt && + decodedSetup.kind !== CampaignKind.LiquityV2StabilityPool + ) + return; + + const payload = decodedSetup as LiquityV2CampaignPayload; + if (!payload.brand?.chainId) return; + + const { brand } = payload; + + if (brand.chainId !== chainId) + await switchChainAsync({ chainId: brand.chainId }); + + // Remove the 'setup' parameter from the URL after parsing. + // This ensures the form behaves correctly after autocomplete completes. + const params = new URLSearchParams(searchParams.toString()); + params.delete("setup"); + router.replace(`${pathname}?${params.toString()}`, { + scroll: false, + }); + + setPayload(payload); + }; + + autocompletePayload(); + }, [ + loadingSetup, + searchParams, + chainId, + setup, + pathname, + router, + switchChainAsync, + ]); + + useEffect(() => { + if (!loadingSetup && !!setupError) + toast.custom((toastId) => ); + + if (!loadingSetup && !setupError && !!setup) + toast.custom((toastId) => ); + }, [loadingSetup, setupError, setup]); + const previewPayload = useMemo(() => { if (Object.values(errors).some((error) => !!error)) return null; return validatePayload(payload); @@ -132,21 +208,27 @@ export function LiquityV2ForksForm({ onPreviewClick(previewPayload); } + const loading = loadingSetup || switchingChain; + return (
*/}