diff --git a/apps/web/src/components/Basenames/RegistrationForm/index.tsx b/apps/web/src/components/Basenames/RegistrationForm/index.tsx
index 5cb624a1d03..f54e1641a17 100644
--- a/apps/web/src/components/Basenames/RegistrationForm/index.tsx
+++ b/apps/web/src/components/Basenames/RegistrationForm/index.tsx
@@ -31,6 +31,10 @@ import { formatEtherPrice } from 'apps/web/src/utils/formatEtherPrice';
import { formatUsdPrice } from 'apps/web/src/utils/formatUsdPrice';
import { ConnectButton, useConnectModal } from '@rainbow-me/rainbowkit';
import { Button, ButtonSizes, ButtonVariants } from 'apps/web/src/components/Button/Button';
+import {
+ useSwitchToBasenameChain,
+ useYearSelectionCallbacks,
+} from 'apps/web/src/components/Basenames/basenameFormUtils';
export default function RegistrationForm() {
const { chain: connectedChain, address } = useAccount();
@@ -41,10 +45,7 @@ export default function RegistrationForm() {
const { basenameChain } = useBasenameChain();
const { switchChain } = useSwitchChain();
- const switchToIntendedNetwork = useCallback(
- () => switchChain({ chainId: basenameChain.id }),
- [basenameChain.id, switchChain],
- );
+ const switchToIntendedNetwork = useSwitchToBasenameChain(switchChain, basenameChain.id);
const {
selectedName,
@@ -72,17 +73,10 @@ export default function RegistrationForm() {
setLearnMoreAboutDiscountsModalOpen((open) => !open);
}, [logEventWithContext, setLearnMoreAboutDiscountsModalOpen]);
- const increment = useCallback(() => {
- logEventWithContext('registration_form_increment_year', ActionType.click);
-
- setYears((n) => n + 1);
- }, [logEventWithContext, setYears]);
-
- const decrement = useCallback(() => {
- logEventWithContext('registration_form_decement_year', ActionType.click);
-
- setYears((n) => (n > 1 ? n - 1 : n));
- }, [logEventWithContext, setYears]);
+ const { increment, decrement } = useYearSelectionCallbacks(setYears, {
+ onIncrement: () => logEventWithContext('registration_form_increment_year', ActionType.click),
+ onDecrement: () => logEventWithContext('registration_form_decrement_year', ActionType.click),
+ });
const ethUsdPrice = useEthPriceFromUniswap();
const { data: initialPrice } = useNameRegistrationPrice(selectedName, years);
diff --git a/apps/web/src/components/Basenames/RenewalForm/index.tsx b/apps/web/src/components/Basenames/RenewalForm/index.tsx
index 1679c715748..d260e002370 100644
--- a/apps/web/src/components/Basenames/RenewalForm/index.tsx
+++ b/apps/web/src/components/Basenames/RenewalForm/index.tsx
@@ -14,6 +14,10 @@ import { RenewalButton } from './RenewalButton';
import { formatUsdPrice } from 'apps/web/src/utils/formatUsdPrice';
import { formatEtherPrice } from 'apps/web/src/utils/formatEtherPrice';
import YearSelector from 'apps/web/src/components/Basenames/YearSelector';
+import {
+ useSwitchToBasenameChain,
+ useYearSelectionCallbacks,
+} from 'apps/web/src/components/Basenames/basenameFormUtils';
export default function RenewalForm() {
const { chain: connectedChain, address } = useAccount();
@@ -24,23 +28,14 @@ export default function RenewalForm() {
const { years, setYears, renewBasename, price, isPending, expirationDate } = useRenewal();
- const switchToIntendedNetwork = useCallback(
- () => switchChain({ chainId: basenameChain.id }),
- [basenameChain.id, switchChain],
- );
+ const switchToIntendedNetwork = useSwitchToBasenameChain(switchChain, basenameChain.id);
const isOnSupportedNetwork = useMemo(
() => connectedChain && supportedChainIds.includes(connectedChain.id),
[connectedChain],
);
- const increment = useCallback(() => {
- setYears((n) => n + 1);
- }, [setYears]);
-
- const decrement = useCallback(() => {
- setYears((n) => (n > 1 ? n - 1 : n));
- }, [setYears]);
+ const { increment, decrement } = useYearSelectionCallbacks(setYears);
const ethUsdPrice = useEthPriceFromUniswap();
diff --git a/apps/web/src/components/Basenames/basenameFormUtils.ts b/apps/web/src/components/Basenames/basenameFormUtils.ts
new file mode 100644
index 00000000000..8dbe5df0a1c
--- /dev/null
+++ b/apps/web/src/components/Basenames/basenameFormUtils.ts
@@ -0,0 +1,36 @@
+import { useCallback } from 'react';
+
+/**
+ * Creates a callback to switch to a specific chain
+ */
+export function useSwitchToBasenameChain(
+ switchChain: (params: { chainId: number }) => void,
+ chainId: number,
+) {
+ return useCallback(() => switchChain({ chainId }), [chainId, switchChain]);
+}
+
+/**
+ * Creates callbacks for incrementing and decrementing year selection
+ */
+export function useYearSelectionCallbacks(
+ setYears: (fn: (n: number) => number) => void,
+ options?: {
+ onIncrement?: () => void;
+ onDecrement?: () => void;
+ },
+) {
+ const { onIncrement, onDecrement } = options ?? {};
+
+ const increment = useCallback(() => {
+ onIncrement?.();
+ setYears((n) => n + 1);
+ }, [setYears, onIncrement]);
+
+ const decrement = useCallback(() => {
+ onDecrement?.();
+ setYears((n) => (n > 1 ? n - 1 : n));
+ }, [setYears, onDecrement]);
+
+ return { increment, decrement };
+}
diff --git a/apps/web/src/components/Builders/AgentKit/Hero.tsx b/apps/web/src/components/Builders/AgentKit/Hero.tsx
index b44ea0075a2..3d32c045fcd 100644
--- a/apps/web/src/components/Builders/AgentKit/Hero.tsx
+++ b/apps/web/src/components/Builders/AgentKit/Hero.tsx
@@ -1,5 +1,3 @@
-'use client';
-
import { ButtonVariants } from 'apps/web/src/components/base-org/Button/types';
import Title from 'apps/web/src/components/base-org/typography/Title';
import { TitleLevel } from 'apps/web/src/components/base-org/typography/Title/types';
@@ -9,16 +7,14 @@ import { Demo } from 'apps/web/src/components/Builders/AgentKit/Demo';
import { AGENTKIT_DOCS_LINK } from 'apps/web/src/components/Builders/AgentKit/links';
import { Icon } from 'apps/web/src/components/Icon/Icon';
import Image, { StaticImageData } from 'next/image';
-import { useCallback, useState } from 'react';
+import { useCopyToClipboard } from 'apps/web/src/hooks/useCopyToClipboard';
+import { useCallback } from 'react';
export function Hero() {
- const [hasCopied, setHasCopied] = useState(false);
-
+ const { hasCopied, copyToClipboard } = useCopyToClipboard();
const handleCopy = useCallback(() => {
- void navigator.clipboard.writeText('npx create-agentkit-app');
- setHasCopied(true);
- setTimeout(() => setHasCopied(false), 2000); // Reset after 2 seconds
- }, []);
+ copyToClipboard('npx create-agentkit-app');
+ }, [copyToClipboard]);
return (
diff --git a/apps/web/src/components/Builders/MiniKit/Hero.tsx b/apps/web/src/components/Builders/MiniKit/Hero.tsx
index 6c5c3aece47..cbdf2fc54d0 100644
--- a/apps/web/src/components/Builders/MiniKit/Hero.tsx
+++ b/apps/web/src/components/Builders/MiniKit/Hero.tsx
@@ -1,7 +1,4 @@
-'use client';
-
import Image, { StaticImageData } from 'next/image';
-import { useCallback, useState } from 'react';
import { ButtonVariants } from 'apps/web/src/components/base-org/Button/types';
import Title from 'apps/web/src/components/base-org/typography/Title';
import { TitleLevel } from 'apps/web/src/components/base-org/typography/Title/types';
@@ -9,18 +6,17 @@ import { ButtonWithLinkAndEventLogging } from 'apps/web/src/components/Button/Bu
import { HeaderAnimation } from 'apps/web/src/components/Builders/MiniKit/HeaderAnimation';
import minikit from 'apps/web/src/components/Builders/MiniKit/minikit.svg';
import { Icon } from 'apps/web/src/components/Icon/Icon';
+import { useCopyToClipboard } from 'apps/web/src/hooks/useCopyToClipboard';
+import { useCallback } from 'react';
export const GET_STARTED_URL = 'https://docs.base.org/builderkits/minikit/overview';
const MINIKIT_COMMAND = 'npx create-onchain --mini';
export function Hero() {
- const [hasCopied, setHasCopied] = useState(false);
-
+ const { hasCopied, copyToClipboard } = useCopyToClipboard();
const handleCopy = useCallback(() => {
- void navigator.clipboard.writeText(MINIKIT_COMMAND);
- setHasCopied(true);
- setTimeout(() => setHasCopied(false), 2000); // Reset after 2 seconds
- }, []);
+ copyToClipboard(MINIKIT_COMMAND);
+ }, [copyToClipboard]);
return (
diff --git a/apps/web/src/components/Builders/Onchainkit/CtaBanner.tsx b/apps/web/src/components/Builders/Onchainkit/CtaBanner.tsx
index 3d88f10da29..b841a497aed 100644
--- a/apps/web/src/components/Builders/Onchainkit/CtaBanner.tsx
+++ b/apps/web/src/components/Builders/Onchainkit/CtaBanner.tsx
@@ -1,22 +1,18 @@
-'use client';
-
import { ButtonVariants } from 'apps/web/src/components/base-org/Button/types';
import { CtaBanner as DefaultCtaBanner } from 'apps/web/src/components/Builders/Shared/CtaBanner';
import { ButtonWithLinkAndEventLogging } from 'apps/web/src/components/Button/ButtonWithLinkAndEventLogging';
-import { useCallback, useState } from 'react';
import { Icon } from 'apps/web/src/components/Icon/Icon';
+import { useCopyToClipboard } from 'apps/web/src/hooks/useCopyToClipboard';
+import { useCallback } from 'react';
const ONCHAINKIT_DOCS_LINK = 'https://docs.base.org/builderkits/onchainkit/getting-started';
const ONCHAINKIT_AI_DOCS_LINK = 'https://docs.base.org/builderkits/onchainkit/llms.txt';
export function CtaBanner() {
- const [hasCopied, setHasCopied] = useState(false);
-
+ const { hasCopied, copyToClipboard } = useCopyToClipboard();
const handleCopy = useCallback(() => {
- void navigator.clipboard.writeText('npm create onchain');
- setHasCopied(true);
- setTimeout(() => setHasCopied(false), 2000); // Reset after 2 seconds
- }, []);
+ copyToClipboard('npm create onchain');
+ }, [copyToClipboard]);
return (
{
- void navigator.clipboard.writeText('npm create onchain');
- setHasCopied(true);
- setTimeout(() => setHasCopied(false), 2000); // Reset after 2 seconds
- }, []);
+ copyToClipboard('npm create onchain');
+ }, [copyToClipboard]);
return (
diff --git a/apps/web/src/components/Builders/Shared/BottomCta/index.tsx b/apps/web/src/components/Builders/Shared/BottomCta/index.tsx
index 410c65364f6..87369f13d9e 100644
--- a/apps/web/src/components/Builders/Shared/BottomCta/index.tsx
+++ b/apps/web/src/components/Builders/Shared/BottomCta/index.tsx
@@ -1,19 +1,15 @@
-'use client';
-
import { ButtonVariants } from 'apps/web/src/components/base-org/Button/types';
import { ButtonWithLinkAndEventLogging } from 'apps/web/src/components/Button/ButtonWithLinkAndEventLogging';
-import { useCallback, useState } from 'react';
import { Icon } from 'apps/web/src/components/Icon/Icon';
import { CtaBanner } from 'apps/web/src/components/Builders/Shared/BottomCta/CtaBanner';
+import { useCopyToClipboard } from 'apps/web/src/hooks/useCopyToClipboard';
+import { useCallback } from 'react';
export function BottomCta() {
- const [hasCopied, setHasCopied] = useState(false);
-
+ const { hasCopied, copyToClipboard } = useCopyToClipboard();
const handleCopy = useCallback(() => {
- void navigator.clipboard.writeText('npm create onchain');
- setHasCopied(true);
- setTimeout(() => setHasCopied(false), 2000); // Reset after 2 seconds
- }, []);
+ copyToClipboard('npm create onchain');
+ }, [copyToClipboard]);
return (
(null);
+
+ useEffect(() => {
+ return () => {
+ if (timeoutRef.current) {
+ clearTimeout(timeoutRef.current);
+ }
+ };
+ }, []);
+
+ const copyToClipboard = useCallback(
+ async (text: string) => {
+ if (timeoutRef.current) {
+ clearTimeout(timeoutRef.current);
+ }
+
+ try {
+ await navigator.clipboard.writeText(text);
+ setHasCopied(true);
+
+ timeoutRef.current = setTimeout(() => {
+ setHasCopied(false);
+ timeoutRef.current = null;
+ }, timeout);
+ } catch (error) {
+ // Silently fail - clipboard access may be restricted
+ console.warn('Failed to copy to clipboard:', error);
+ }
+ },
+ [timeout],
+ );
+
+ return { hasCopied, copyToClipboard };
+}
diff --git a/apps/web/src/utils/formatWei.ts b/apps/web/src/utils/formatWei.ts
deleted file mode 100644
index 5ae0c2ce818..00000000000
--- a/apps/web/src/utils/formatWei.ts
+++ /dev/null
@@ -1,11 +0,0 @@
-import { formatEther, parseEther } from 'viem';
-
-export function formatWei(wei?: bigint): bigint | '...' {
- if (wei === undefined) {
- return '...';
- }
-
- const priceInEth = formatEther(wei);
- return parseEther(priceInEth.toString());
-}
-
diff --git a/apps/web/src/utils/logEvent.ts b/apps/web/src/utils/logEvent.ts
deleted file mode 100644
index cf75328cf73..00000000000
--- a/apps/web/src/utils/logEvent.ts
+++ /dev/null
@@ -1,18 +0,0 @@
-declare const window: Window &
- typeof globalThis & {
- ClientAnalytics: {
- logEvent: (name: string, event: unknown) => void;
- };
- };
-
-export default function logEvent(name: string, event: unknown) {
- if (window.ClientAnalytics) {
- window.ClientAnalytics?.logEvent(name, event);
- }
-}
-
-export function identify(event: unknown) {
- if (window.ClientAnalytics) {
- window.ClientAnalytics?.logEvent('identify', event);
- }
-}
diff --git a/apps/web/src/utils/weiToEth.ts b/apps/web/src/utils/weiToEth.ts
deleted file mode 100644
index 7efceffc7f8..00000000000
--- a/apps/web/src/utils/weiToEth.ts
+++ /dev/null
@@ -1,13 +0,0 @@
-import { formatEther } from 'viem';
-
-export function weiToEth(wei?: bigint): number | '...' {
- if (wei === undefined) {
- return '...';
- }
- const eth = parseFloat(formatEther(wei));
- if (eth < 0.001) {
- return parseFloat(eth.toFixed(4));
- } else {
- return parseFloat(eth.toFixed(3));
- }
-}