diff --git a/src/components/trade/header/user-menu.tsx b/src/components/trade/header/user-menu.tsx index be2b7e3d..dd7e8b57 100644 --- a/src/components/trade/header/user-menu.tsx +++ b/src/components/trade/header/user-menu.tsx @@ -20,6 +20,7 @@ import { DropdownMenuTrigger, } from "@/components/ui/dropdown-menu"; import { useCopyToClipboard } from "@/hooks/ui/use-copy-to-clipboard"; +import { useAutoRegisterReferral } from "@/hooks/use-referral"; import { shortenAddress } from "@/lib/format"; function CopyAddressMenuItem({ address }: { address: string }) { @@ -53,6 +54,8 @@ export function UserMenu() { const { data: ensName } = useEnsName({ address }); const [mounted, setMounted] = useState(false); + useAutoRegisterReferral(); + useEffect(() => { setMounted(true); }, []); diff --git a/src/components/trade/tradebox/points-modal.tsx b/src/components/trade/tradebox/points-modal.tsx index 9be2c766..4b71cdec 100644 --- a/src/components/trade/tradebox/points-modal.tsx +++ b/src/components/trade/tradebox/points-modal.tsx @@ -10,13 +10,14 @@ import { WarningCircleIcon, } from "@phosphor-icons/react"; import { usePrivy } from "@privy-io/react-auth"; -import { useEffect, useId, useState } from "react"; +import { useCallback, useEffect, useId, useState } from "react"; import { useConnection } from "wagmi"; import { Button } from "@/components/ui/button"; import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog"; import { InfoRow } from "@/components/ui/info-row"; import { Input } from "@/components/ui/input"; import { useCopyToClipboard } from "@/hooks/ui/use-copy-to-clipboard"; +import { getStoredReferral } from "@/hooks/use-referral"; import { usePointsModalActions, usePointsModalOpen } from "@/stores/use-global-modal-store"; const API_URL = import.meta.env.VITE_HYPERMILES_API_URL; @@ -97,12 +98,12 @@ export function PointsModal() { const [view, setView] = useState("loading"); const [userPoints, setUserPoints] = useState(null); - const [referralCode, setReferralCode] = useState(""); + const [referralCode, setReferralCode] = useState(() => getStoredReferral() ?? ""); const [error, setError] = useState(null); const [submitting, setSubmitting] = useState(false); const referralInputId = useId(); - function fetchPoints() { + const fetchPoints = useCallback(() => { if (!address) return; setView("loading"); setError(null); @@ -122,17 +123,17 @@ export function PointsModal() { setView("signup"); } }) - .catch(() => { + .catch((error) => { + console.error("Failed to fetch points:", error); setError("Unable to connect to points service"); setView("error"); }); - } + }, [address]); - // biome-ignore lint/correctness/useExhaustiveDependencies: fetchPoints is intentionally excluded to avoid re-creating on every render useEffect(() => { if (!open || !address) return; fetchPoints(); - }, [open, address]); + }, [open, address, fetchPoints]); function handleClose() { close(); @@ -179,6 +180,7 @@ export function PointsModal() { }); setView("summary"); } catch (err) { + console.error("Signup failed:", err); setError(err instanceof Error ? err.message : "Signup failed"); } finally { setSubmitting(false); @@ -217,8 +219,8 @@ export function PointsModal() { Your Referral Code} - value={} + label={Your Referral Link} + value={} /> diff --git a/src/hooks/use-referral.ts b/src/hooks/use-referral.ts new file mode 100644 index 00000000..4930bcde --- /dev/null +++ b/src/hooks/use-referral.ts @@ -0,0 +1,78 @@ +import { usePrivy } from "@privy-io/react-auth"; +import { useEffect, useRef } from "react"; +import { useConnection } from "wagmi"; + +const API_URL = import.meta.env.VITE_HYPERMILES_API_URL; +const STORAGE_KEY = "hyperterminal:referral"; + +function getStoredReferral(): string | null { + if (typeof window === "undefined") return null; + return localStorage.getItem(STORAGE_KEY); +} + +function clearStoredReferral() { + localStorage.removeItem(STORAGE_KEY); +} + +export function useReferralCapture() { + useEffect(() => { + if (typeof window === "undefined") return; + + const params = new URLSearchParams(window.location.search); + const referral = params.get("referral"); + if (!referral) return; + + localStorage.setItem(STORAGE_KEY, referral); + + params.delete("referral"); + const newSearch = params.toString(); + const newUrl = window.location.pathname + (newSearch ? `?${newSearch}` : "") + window.location.hash; + window.history.replaceState({}, "", newUrl); + }, []); +} + +export function useAutoRegisterReferral() { + const { address, isConnected } = useConnection(); + const { user } = usePrivy(); + const registeredRef = useRef(false); + + useEffect(() => { + async function autoRegister() { + if (!isConnected || !address || registeredRef.current) return; + + registeredRef.current = true; + + try { + const res = await fetch(`${API_URL}/user_points?user_address=${address}`); + if (res.ok) { + clearStoredReferral(); + return; + } + + if (res.status === 404) { + const referral = getStoredReferral(); + const body: Record = { walletAddress: address }; + if (referral) { + body.referralCode = referral; + } + if (user?.id) { + body.privyUserId = user.id; + } + await fetch(`${API_URL}/users`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(body), + }); + clearStoredReferral(); + } + } catch (error) { + console.error("Auto-registration failed:", error); + registeredRef.current = false; + } + } + + autoRegister(); + }, [isConnected, address, user?.id]); +} + +export { getStoredReferral }; diff --git a/src/locales/ar/messages.po b/src/locales/ar/messages.po index 4d3e8deb..d2f874bd 100644 --- a/src/locales/ar/messages.po +++ b/src/locales/ar/messages.po @@ -2256,5 +2256,9 @@ msgstr "" #~ msgstr "" #: src/components/trade/tradebox/points-modal.tsx -msgid "Your Referral Code" +#~ msgid "Your Referral Code" +#~ msgstr "" + +#: src/components/trade/tradebox/points-modal.tsx +msgid "Your Referral Link" msgstr "" diff --git a/src/locales/en/messages.po b/src/locales/en/messages.po index edb9e674..2cd22d77 100644 --- a/src/locales/en/messages.po +++ b/src/locales/en/messages.po @@ -2269,5 +2269,9 @@ msgstr "Yes" #~ msgstr "You can revoke permissions at any time by registering a new approval with 0% fee rate. Builder codes are processed entirely onchain as part of the fee logic." #: src/components/trade/tradebox/points-modal.tsx -msgid "Your Referral Code" -msgstr "Your Referral Code" +#~ msgid "Your Referral Code" +#~ msgstr "Your Referral Code" + +#: src/components/trade/tradebox/points-modal.tsx +msgid "Your Referral Link" +msgstr "Your Referral Link" diff --git a/src/locales/es/messages.po b/src/locales/es/messages.po index 86b5e9ee..f6d40671 100644 --- a/src/locales/es/messages.po +++ b/src/locales/es/messages.po @@ -2256,5 +2256,9 @@ msgstr "" #~ msgstr "" #: src/components/trade/tradebox/points-modal.tsx -msgid "Your Referral Code" +#~ msgid "Your Referral Code" +#~ msgstr "" + +#: src/components/trade/tradebox/points-modal.tsx +msgid "Your Referral Link" msgstr "" diff --git a/src/locales/fr/messages.po b/src/locales/fr/messages.po index e555c780..2a6a6b02 100644 --- a/src/locales/fr/messages.po +++ b/src/locales/fr/messages.po @@ -2256,5 +2256,9 @@ msgstr "" #~ msgstr "" #: src/components/trade/tradebox/points-modal.tsx -msgid "Your Referral Code" +#~ msgid "Your Referral Code" +#~ msgstr "" + +#: src/components/trade/tradebox/points-modal.tsx +msgid "Your Referral Link" msgstr "" diff --git a/src/locales/hi/messages.po b/src/locales/hi/messages.po index eb0a8bbf..723dbb9d 100644 --- a/src/locales/hi/messages.po +++ b/src/locales/hi/messages.po @@ -2256,5 +2256,9 @@ msgstr "" #~ msgstr "" #: src/components/trade/tradebox/points-modal.tsx -msgid "Your Referral Code" +#~ msgid "Your Referral Code" +#~ msgstr "" + +#: src/components/trade/tradebox/points-modal.tsx +msgid "Your Referral Link" msgstr "" diff --git a/src/locales/zh/messages.po b/src/locales/zh/messages.po index 37c2a0a7..ff6ed273 100644 --- a/src/locales/zh/messages.po +++ b/src/locales/zh/messages.po @@ -2256,5 +2256,9 @@ msgstr "" #~ msgstr "" #: src/components/trade/tradebox/points-modal.tsx -msgid "Your Referral Code" +#~ msgid "Your Referral Code" +#~ msgstr "" + +#: src/components/trade/tradebox/points-modal.tsx +msgid "Your Referral Link" msgstr "" diff --git a/src/routes/__root.tsx b/src/routes/__root.tsx index a9ae94c8..494079f1 100644 --- a/src/routes/__root.tsx +++ b/src/routes/__root.tsx @@ -3,6 +3,7 @@ import { ClientOnly, createRootRouteWithContext, HeadContent, Outlet, Scripts } import { useEffect } from "react"; import { NotFoundPage } from "@/components/pages/not-found-page"; import { Toaster } from "@/components/ui/sonner"; +import { useReferralCapture } from "@/hooks/use-referral"; import { MarketsInfoProvider } from "@/lib/hyperliquid/hooks/MarketsInfoProvider"; import { buildPageHead, mergeHead } from "@/lib/seo"; import { ExchangeScopeProvider } from "@/providers/exchange-scope"; @@ -25,6 +26,8 @@ export const Route = createRootRouteWithContext()({ }); function RootComponent() { + useReferralCapture(); + useEffect(() => { if ("serviceWorker" in navigator) { navigator.serviceWorker.register("/sw.js", { scope: "/" });