diff --git a/apps/web/app/(base-org)/builder-anniversary-nft/page.tsx b/apps/web/app/(base-org)/builder-anniversary-nft/page.tsx index 80efceb319..355dfc619e 100644 --- a/apps/web/app/(base-org)/builder-anniversary-nft/page.tsx +++ b/apps/web/app/(base-org)/builder-anniversary-nft/page.tsx @@ -1,3 +1,4 @@ +import CryptoProviders from 'apps/web/app/CryptoProviders'; import { BuilderNftHero } from 'apps/web/src/components/BuilderNft/BuilderNftHero'; import type { Metadata } from 'next'; @@ -13,7 +14,9 @@ export const metadata: Metadata = { export default async function About() { return (
- + + +
); } diff --git a/apps/web/app/(basenames)/layout.tsx b/apps/web/app/(basenames)/layout.tsx index 493d746931..87db756f35 100644 --- a/apps/web/app/(basenames)/layout.tsx +++ b/apps/web/app/(basenames)/layout.tsx @@ -1,3 +1,4 @@ +import CryptoProviders from 'apps/web/app/CryptoProviders'; import ErrorsProvider from 'apps/web/contexts/Errors'; import UsernameNav from 'apps/web/src/components/Layout/UsernameNav'; @@ -27,10 +28,12 @@ export default async function BasenameLayout({ }) { return ( -
- - {children} -
+ +
+ + {children} +
+
); } diff --git a/apps/web/app/AppProviders.tsx b/apps/web/app/AppProviders.tsx index 3f48d3ecb4..b720ff7c13 100644 --- a/apps/web/app/AppProviders.tsx +++ b/apps/web/app/AppProviders.tsx @@ -1,6 +1,4 @@ 'use client'; -import '@rainbow-me/rainbowkit/styles.css'; -import '@coinbase/onchainkit/styles.css'; import { Provider as CookieManagerProvider, @@ -8,28 +6,14 @@ import { TrackingCategory, TrackingPreference, } from '@coinbase/cookie-manager'; -import { OnchainKitProvider } from '@coinbase/onchainkit'; import { Provider as TooltipProvider } from '@radix-ui/react-tooltip'; -import { connectorsForWallets, RainbowKitProvider } from '@rainbow-me/rainbowkit'; -import { - coinbaseWallet, - metaMaskWallet, - phantomWallet, - rainbowWallet, - uniswapWallet, - walletConnectWallet, -} from '@rainbow-me/rainbowkit/wallets'; -import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import ExperimentsProvider from 'base-ui/contexts/Experiments'; import useSprig from 'base-ui/hooks/useSprig'; import { useCallback, useRef } from 'react'; -import { createConfig, http, WagmiProvider } from 'wagmi'; -import { base, baseSepolia, mainnet } from 'wagmi/chains'; import { cookieManagerConfig } from '../src/utils/cookieManagerConfig'; import ClientAnalyticsScript from 'apps/web/src/components/ClientAnalyticsScript/ClientAnalyticsScript'; import dynamic from 'next/dynamic'; import ErrorsProvider from 'apps/web/contexts/Errors'; -import { isDevelopment } from 'apps/web/src/constants'; import { logger } from 'apps/web/src/utils/logger'; const DynamicCookieBannerWrapper = dynamic( @@ -39,43 +23,6 @@ const DynamicCookieBannerWrapper = dynamic( }, ); -coinbaseWallet.preference = 'all'; - -const connectors = connectorsForWallets( - [ - { - groupName: 'Recommended', - wallets: [ - coinbaseWallet, - metaMaskWallet, - uniswapWallet, - rainbowWallet, - phantomWallet, - walletConnectWallet, - ], - }, - ], - { - projectId: process.env.NEXT_PUBLIC_WALLET_CONNECT_PROJECT_ID ?? 'dummy-id', - walletConnectParameters: {}, - appName: 'Base.org', - appDescription: '', - appUrl: 'https://www.base.org/', - appIcon: '', - }, -); - -const config = createConfig({ - connectors, - chains: [base, baseSepolia, mainnet], - transports: { - [base.id]: http(), - [baseSepolia.id]: http(), - [mainnet.id]: http(), - }, - ssr: true, -}); -const queryClient = new QueryClient(); const sprigEnvironmentId = process.env.NEXT_PUBLIC_SPRIG_ENVIRONMENT_ID; type AppProvidersProps = { @@ -136,25 +83,14 @@ export default function AppProviders({ children }: AppProvidersProps) { config={cookieManagerConfig} > - - - - - - - <> - {children} - - - - - - - - + + + <> + {children} + + + + ); diff --git a/apps/web/app/CryptoProviders.dynamic.tsx b/apps/web/app/CryptoProviders.dynamic.tsx new file mode 100644 index 0000000000..f0049a96fe --- /dev/null +++ b/apps/web/app/CryptoProviders.dynamic.tsx @@ -0,0 +1,22 @@ +'use client'; + +import { useEffect, useState } from 'react'; +import { useErrors } from 'apps/web/contexts/Errors'; + +export function DynamicCryptoProviders({ children }: { children: React.ReactNode }) { + const [CryptoProvidersDynamic, setCryptoProvidersDynamic] = + useState>(); + const { logError } = useErrors(); + + useEffect(() => { + import('apps/web/app/CryptoProviders') + .then((mod) => { + setCryptoProvidersDynamic(() => mod.default); + }) + .catch((error) => logError(error, 'Failed to load CryptoProviders')); + }, [logError]); + + if (!CryptoProvidersDynamic) return null; + + return {children}; +} diff --git a/apps/web/app/CryptoProviders.tsx b/apps/web/app/CryptoProviders.tsx new file mode 100644 index 0000000000..f25e43e6ec --- /dev/null +++ b/apps/web/app/CryptoProviders.tsx @@ -0,0 +1,78 @@ +'use client'; + +import { AppConfig, OnchainKitProvider } from '@coinbase/onchainkit'; +import { connectorsForWallets, RainbowKitProvider } from '@rainbow-me/rainbowkit'; +import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; +import { isDevelopment } from 'apps/web/src/constants'; +import { createConfig, http, WagmiProvider } from 'wagmi'; +import { base, baseSepolia, mainnet } from 'wagmi/chains'; +import { + coinbaseWallet, + metaMaskWallet, + phantomWallet, + rainbowWallet, + uniswapWallet, + walletConnectWallet, +} from '@rainbow-me/rainbowkit/wallets'; + +const connectors = connectorsForWallets( + [ + { + groupName: 'Recommended', + wallets: [ + coinbaseWallet, + metaMaskWallet, + uniswapWallet, + rainbowWallet, + phantomWallet, + walletConnectWallet, + ], + }, + ], + { + projectId: process.env.NEXT_PUBLIC_WALLET_CONNECT_PROJECT_ID ?? 'dummy-id', + walletConnectParameters: {}, + appName: 'Base.org', + appDescription: '', + appUrl: 'https://www.base.org/', + appIcon: '', + }, +); + +const config = createConfig({ + connectors, + chains: [base, baseSepolia, mainnet], + transports: { + [base.id]: http(), + [baseSepolia.id]: http(), + [mainnet.id]: http(), + }, + ssr: true, +}); +const queryClient = new QueryClient(); + +type CryptoProvidersProps = { + children: React.ReactNode; +}; + +const onchainKitConfig: AppConfig = { + appearance: { + mode: 'light', + }, +}; + +export default function CryptoProviders({ children }: CryptoProvidersProps) { + return ( + + + + {children} + + + + ); +} diff --git a/apps/web/app/global.css b/apps/web/app/global.css index 4a2dc90d14..ddfedc28f4 100644 --- a/apps/web/app/global.css +++ b/apps/web/app/global.css @@ -2,6 +2,9 @@ @tailwind components; @tailwind utilities; +@import '@rainbow-me/rainbowkit/styles.css'; +@import '@coinbase/onchainkit/styles.css'; + /* For Webkit-based browsers (Chrome, Safari and Opera) */ :not(.scrollbar)::-webkit-scrollbar { display: none; diff --git a/apps/web/src/addresses/usernames.ts b/apps/web/src/addresses/usernames.ts index 43e6e2f9ad..110fd65b0b 100644 --- a/apps/web/src/addresses/usernames.ts +++ b/apps/web/src/addresses/usernames.ts @@ -101,3 +101,8 @@ export const BASE_WORLD_DISCOUNT_VALIDATORS: AddressMap = { [baseSepolia.id]: '0xFa69f6167F40247fe3EFF2d8375B25C5d7834c48', [base.id]: '0xfEb00a4EfF372a307fDc556Cf4359f7D679E4d11', }; + +export const DEVCON_DISCOUNT_VALIDATORS: AddressMap = { + [baseSepolia.id]: '0x5c81c392C22Cba477a70D809DE6d6Cd362A1c3DE', + [base.id]: '0xFca2EB54EaB56085e25a32BfF30fe8C452216c5F', +}; diff --git a/apps/web/src/components/Basenames/RegistrationFlow.tsx b/apps/web/src/components/Basenames/RegistrationFlow.tsx index 6954b9fd03..fccbd299c6 100644 --- a/apps/web/src/components/Basenames/RegistrationFlow.tsx +++ b/apps/web/src/components/Basenames/RegistrationFlow.tsx @@ -105,7 +105,8 @@ export function RegistrationFlow() { const onBackArrowClick = useCallback(() => { setRegistrationStep(RegistrationSteps.Search); - }, [setRegistrationStep]); + setSelectedName(''); + }, [setRegistrationStep, setSelectedName]); useEffect(() => { const claimQuery = searchParams?.get(claimQueryKey); diff --git a/apps/web/src/components/Basenames/RegistrationLearnMoreModal/images/devcon.png b/apps/web/src/components/Basenames/RegistrationLearnMoreModal/images/devcon.png new file mode 100644 index 0000000000..3b140e499d Binary files /dev/null and b/apps/web/src/components/Basenames/RegistrationLearnMoreModal/images/devcon.png differ diff --git a/apps/web/src/components/Basenames/RegistrationLearnMoreModal/index.tsx b/apps/web/src/components/Basenames/RegistrationLearnMoreModal/index.tsx index 5d8169c427..62a8ba495d 100644 --- a/apps/web/src/components/Basenames/RegistrationLearnMoreModal/index.tsx +++ b/apps/web/src/components/Basenames/RegistrationLearnMoreModal/index.tsx @@ -10,6 +10,7 @@ import summerPassLvl3 from './images/summer-pass-lvl-3.svg'; import cbidVerification from './images/cbid-verification.svg'; import BNSOwnership from './images/bns.jpg'; import BaseNFT from './images/base-nft.svg'; +import DevconPNG from './images/devcon.png'; import TalentProtocolIcon from './images/TalentProtocol.svg'; import coinbaseOneVerification from './images/coinbase-one-verification.svg'; import coinbaseVerification from './images/coinbase-verification.svg'; @@ -95,6 +96,13 @@ const DISCOUNT_ITEMS: DiscountItem[] = [ label: 'Base around the world NFT', tooltipContent: 'Available for anyone holding one of the Base around the world NFTs', }, + { + discount: Discount.DEVCON, + icon: DevconPNG, + alt: 'icon of Devcon', + label: 'Devcon attendance NFT', + tooltipContent: 'Available for anyone holding one of the Base Devcon NFTs', + }, ]; export default function RegistrationLearnMoreModal({ diff --git a/apps/web/src/components/ConnectWalletButton/ConnectWalletButton.tsx b/apps/web/src/components/ConnectWalletButton/ConnectWalletButton.tsx index 6e936441ab..ff7db6a95c 100644 --- a/apps/web/src/components/ConnectWalletButton/ConnectWalletButton.tsx +++ b/apps/web/src/components/ConnectWalletButton/ConnectWalletButton.tsx @@ -23,10 +23,11 @@ import logEvent, { import sanitizeEventString from 'base-ui/utils/sanitizeEventString'; import classNames from 'classnames'; import { useCallback, useEffect, useState } from 'react'; -import { useCopyToClipboard } from 'usehooks-ts'; +import { useCopyToClipboard, useMediaQuery } from 'usehooks-ts'; import { useAccount, useSwitchChain } from 'wagmi'; import ChainDropdown from 'apps/web/src/components/ChainDropdown'; import { useSearchParams } from 'next/navigation'; +import { DynamicCryptoProviders } from 'apps/web/app/CryptoProviders.dynamic'; export enum ConnectWalletButtonVariants { BaseOrg, @@ -37,6 +38,16 @@ type ConnectWalletButtonProps = { connectWalletButtonVariant: ConnectWalletButtonVariants; }; +export function DynamicWrappedConnectWalletButton({ + connectWalletButtonVariant = ConnectWalletButtonVariants.BaseOrg, +}: ConnectWalletButtonProps) { + return ( + + + + ) +} + export function ConnectWalletButton({ connectWalletButtonVariant = ConnectWalletButtonVariants.BaseOrg, }: ConnectWalletButtonProps) { @@ -93,11 +104,13 @@ export function ConnectWalletButton({ ); }, [openConnectModal]); - const userAddressClasses = classNames('text-lg font-display hidden lg:inline-block', { + const userAddressClasses = classNames('text-lg font-display', { 'text-white': connectWalletButtonVariant === ConnectWalletButtonVariants.BaseOrg, 'text-black': connectWalletButtonVariant === ConnectWalletButtonVariants.Basename, }); + const isDesktop = useMediaQuery('(min-width: 768px)'); + if (isConnecting || isReconnecting || !isMounted) { return ; } @@ -141,12 +154,12 @@ export function ConnectWalletButton({ >
- + {isDesktop && } {showChainSwitcher && }
- + +
{showDevelopmentWarning && (

diff --git a/apps/web/src/components/base-org/shared/TopNavigation/GasPriceDropdown.tsx b/apps/web/src/components/base-org/shared/TopNavigation/GasPriceDropdown.tsx index 27543de352..042c810a4c 100644 --- a/apps/web/src/components/base-org/shared/TopNavigation/GasPriceDropdown.tsx +++ b/apps/web/src/components/base-org/shared/TopNavigation/GasPriceDropdown.tsx @@ -2,6 +2,7 @@ import Card from 'apps/web/src/components/base-org/Card'; import { Icon } from 'apps/web/src/components/Icon/Icon'; import { base, mainnet } from 'viem/chains'; import { useGasPrice } from 'wagmi'; +import { DynamicCryptoProviders } from 'apps/web/app/CryptoProviders.dynamic'; const convertWeiToMwei = (weiValue: bigint): number => { // 1 mwei = 10^6 wei @@ -9,7 +10,15 @@ const convertWeiToMwei = (weiValue: bigint): number => { return Number(mweiValue.toFixed(2)); // Round to 2 decimal places }; -export default function GasPriceDropdown() { +export function DynamicWrappedGasPriceDropdown() { + return ( + + + + ); +} + +export function GasPriceDropdown() { const { data: baseGasPriceInWei } = useGasPrice({ chainId: base.id, query: { @@ -26,7 +35,7 @@ export default function GasPriceDropdown() { return (

-
+
diff --git a/apps/web/src/components/base-org/shared/TopNavigation/index.tsx b/apps/web/src/components/base-org/shared/TopNavigation/index.tsx index b7160b2cb8..795c703805 100644 --- a/apps/web/src/components/base-org/shared/TopNavigation/index.tsx +++ b/apps/web/src/components/base-org/shared/TopNavigation/index.tsx @@ -1,17 +1,18 @@ 'use client'; -import AnalyticsProvider from 'apps/web/contexts/Analytics'; -import Link from 'next/link'; -import logo from './assets/logo.svg'; +import { Suspense } from 'react'; import Image, { StaticImageData } from 'next/image'; +import Link from 'next/link'; +import { usePathname } from 'next/navigation'; +import AnalyticsProvider from 'apps/web/contexts/Analytics'; +import logo from 'apps/web/src/components/base-org/shared/TopNavigation/assets/logo.svg'; +import MenuDesktop from 'apps/web/src/components/base-org/shared/TopNavigation/MenuDesktop'; +import MenuMobile from 'apps/web/src/components/base-org/shared/TopNavigation/MenuMobile'; +import { DynamicWrappedGasPriceDropdown } from 'apps/web/src/components/base-org/shared/TopNavigation/GasPriceDropdown'; import { - ConnectWalletButton, ConnectWalletButtonVariants, + DynamicWrappedConnectWalletButton, } from 'apps/web/src/components/ConnectWalletButton/ConnectWalletButton'; -import MenuDesktop from 'apps/web/src/components/base-org/shared/TopNavigation/MenuDesktop'; -import MenuMobile from 'apps/web/src/components/base-org/shared/TopNavigation/MenuMobile'; -import GasPriceDropdown from 'apps/web/src/components/base-org/shared/TopNavigation/GasPriceDropdown'; -import { Suspense } from 'react'; export type SubItem = { name: string; @@ -91,7 +92,11 @@ const links: TopNavigationLink[] = [ }, ]; +const cryptoExcludedPaths = ['/jobs', '/about', '/ecosystem', '/getstarted']; + export default function TopNavigation() { + const pathname = usePathname(); + const showGasDropdownAndConnectWallet = !cryptoExcludedPaths.includes(pathname ?? ''); return (
@@ -114,11 +119,13 @@ export default function TopNavigation() { {/* Connect Wallet button */}
- - - + {showGasDropdownAndConnectWallet && ( + + + + )}
diff --git a/apps/web/src/hooks/useAggregatedDiscountValidators.ts b/apps/web/src/hooks/useAggregatedDiscountValidators.ts index 0e80c6023e..d34263d162 100644 --- a/apps/web/src/hooks/useAggregatedDiscountValidators.ts +++ b/apps/web/src/hooks/useAggregatedDiscountValidators.ts @@ -8,6 +8,7 @@ import { useCheckCBIDAttestations, useCheckCoinbaseAttestations, useCheckEAAttestations, + useDevconAttestations, useDiscountCodeAttestations, useSummerPassAttestations, useTalentProtocolAttestations, @@ -58,6 +59,7 @@ export function useAggregatedDiscountValidators(code?: string) { const { data: TalentProtocolData, loading: loadingTalentProtocolAttestations } = useTalentProtocolAttestations(); const { data: BaseWorldData, loading: loadingBaseWorld } = useBaseWorldAttestations(); + const { data: DevconData, loading: loadingDevcon } = useDevconAttestations(); const loadingDiscounts = loadingCoinbaseAttestations || @@ -71,7 +73,8 @@ export function useAggregatedDiscountValidators(code?: string) { loadingBNS || loadingDiscountCode || loadingTalentProtocolAttestations || - loadingBaseWorld; + loadingBaseWorld || + loadingDevcon; const discountsToAttestationData = useMemo(() => { const discountMapping: MappedDiscountData = {}; @@ -153,6 +156,13 @@ export function useAggregatedDiscountValidators(code?: string) { discountKey: validator.key, }; } + + if (DevconData && validator.discountValidator === DevconData.discountValidatorAddress) { + discountMapping[Discount.DEVCON] = { + ...DevconData, + discountKey: validator.key, + }; + } }); return discountMapping; @@ -169,6 +179,7 @@ export function useAggregatedDiscountValidators(code?: string) { DiscountCodeData, TalentProtocolData, BaseWorldData, + DevconData, ]); return { diff --git a/apps/web/src/hooks/useAttestations.ts b/apps/web/src/hooks/useAttestations.ts index aab973f6e1..a849032785 100644 --- a/apps/web/src/hooks/useAttestations.ts +++ b/apps/web/src/hooks/useAttestations.ts @@ -12,6 +12,7 @@ import { BASE_DOT_ETH_ERC721_DISCOUNT_VALIDATOR, BASE_WORLD_DISCOUNT_VALIDATORS, BUILDATHON_ERC721_DISCOUNT_VALIDATOR, + DEVCON_DISCOUNT_VALIDATORS, TALENT_PROTOCOL_DISCOUNT_VALIDATORS, USERNAME_1155_DISCOUNT_VALIDATORS, } from 'apps/web/src/addresses/usernames'; @@ -638,3 +639,39 @@ export function useBaseWorldAttestations() { return { data: null, loading: isLoading, error }; } + +const devconTokenIds = [BigInt(100), BigInt(101)]; + +export function useDevconAttestations() { + const { address } = useAccount(); + const { basenameChain } = useBasenameChain(); + + const discountValidatorAddress = DEVCON_DISCOUNT_VALIDATORS[basenameChain.id]; + + const readContractArgs = useMemo(() => { + if (!address) { + return {}; + } + return { + address: discountValidatorAddress, + abi: ERC1155DiscountValidatorV2, + functionName: 'isValidDiscountRegistration', + args: [address, encodeAbiParameters([{ type: 'uint256[]' }], [devconTokenIds])], + }; + }, [address, discountValidatorAddress]); + + const { data: isValid, isLoading, error } = useReadContract({ ...readContractArgs, query: {} }); + if (isValid && address) { + return { + data: { + discountValidatorAddress, + discount: Discount.DEVCON, + validationData: encodeAbiParameters([{ type: 'uint256[]' }], [devconTokenIds]), + }, + loading: false, + error: null, + }; + } + + return { data: null, loading: isLoading, error }; +} diff --git a/apps/web/src/utils/usernames.ts b/apps/web/src/utils/usernames.ts index dc8a783835..43041fbe88 100644 --- a/apps/web/src/utils/usernames.ts +++ b/apps/web/src/utils/usernames.ts @@ -399,6 +399,7 @@ export enum Discount { DISCOUNT_CODE = 'DISCOUNT_CODE', TALENT_PROTOCOL = 'TALENT_PROTOCOL', BASE_WORLD = 'BASE_WORLD', + DEVCON = 'DEVCON', } export function isValidDiscount(key: string): key is keyof typeof Discount {