Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat/referrals alias #66

Open
wants to merge 39 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 35 commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
50432f6
set up referral route
TomasCImach Nov 26, 2024
b7b2649
set up navlink to referrals
TomasCImach Nov 26, 2024
36c6b3e
create Referrals page
TomasCImach Nov 26, 2024
5b441e2
implement traderSignedUpTo response from API
TomasCImach Nov 26, 2024
4f86af2
enhance errors
TomasCImach Nov 26, 2024
e1d8b47
add Success button
TomasCImach Nov 26, 2024
c5dab52
auto populate affiliate with search param
TomasCImach Nov 26, 2024
9e1e901
Merge branch 'main' into Refferal-page-ui
Aliona-D Dec 2, 2024
8aa0d3c
Add styles to the title and GradientBorderBox on the Referrals page
Aliona-D Dec 3, 2024
36b7567
Add StyledInput component
Aliona-D Dec 3, 2024
e2890dc
Implement styles to all cases on Referrals page
Aliona-D Dec 3, 2024
a447fbe
Add a condition to handle cases when user disconnects
Aliona-D Dec 3, 2024
6ac22e8
Add Loader in the case of isConnecting or checkingTraderStatus
Aliona-D Dec 3, 2024
cbc941e
Fix conflict while merging
Aliona-D Dec 3, 2024
0f4b925
Add checkAffiliateStatus function on Referrals page
Aliona-D Dec 9, 2024
1650704
Replace REFERRAL_API_BASE_URL to constants
Aliona-D Dec 9, 2024
cacfc97
Implement AliasSubmit page with checking alias availability
Aliona-D Dec 9, 2024
c965bf4
Implement handleAliasConfirm function with fetching signature and reg…
Aliona-D Dec 9, 2024
d383591
Add copy link logic with Toast component
Aliona-D Dec 9, 2024
d5596d9
Refine alias validation and link copy UX
Aliona-D Dec 9, 2024
7b42ec8
Refactor Referrals component to improve status handling and input beh…
Aliona-D Dec 9, 2024
93e9af9
Fix async affiliate check
Aliona-D Dec 10, 2024
389359b
Ensure trader signup status is cleared when address updates
Aliona-D Dec 10, 2024
47d81a3
Update Toast component styles for improved positioning
Aliona-D Dec 10, 2024
d2d8456
Enhance loading state handling with initialLoading for better UX
Aliona-D Dec 10, 2024
dc1260f
Update text for active alias
Aliona-D Dec 10, 2024
7fe1091
Update affiliate state to accept both alias and address
Aliona-D Dec 11, 2024
a2175ac
Add getAffiliateAddress function to fetch address from alias
Aliona-D Dec 11, 2024
86bd368
Enhance handleSubmit to validate affiliate as either an address or an…
Aliona-D Dec 11, 2024
c809551
Normalize affiliate by adding toLowerCase and toUpperCase handling
Aliona-D Dec 11, 2024
c882896
Add getAffiliateAlias function and implement it to enhance traderSign…
Aliona-D Dec 11, 2024
69efeb7
Add condition to display traderSignedUpTo alias upperCase
Aliona-D Dec 11, 2024
b93da2f
Normalize affiliateAddress and registeredAlias by adding toLowerCase …
Aliona-D Dec 11, 2024
e79a3a9
Update text label in success message for clarity
Aliona-D Dec 11, 2024
5414b38
Merge branch 'main' into feat/referrals-alias
Aliona-D Dec 11, 2024
61cb6f9
Fix referrer link
Aliona-D Dec 11, 2024
4ed9e80
Remove GradientBorderBox while loading
Aliona-D Dec 11, 2024
4449cff
Add checking if affiliate is valid before signing
Aliona-D Dec 11, 2024
a216c2e
Trim input values
Aliona-D Dec 11, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@ import { DEFAULT_MARKET_ID } from "./constants/applications";
import Portfolio from "./pages/Portfolio";
import { AppContainer } from "./app-styles";
import SDKProvider from "./providers/SDKProvider";
import ScrollToTop from './utils/scrollToTop'
import ScrollToTop from "./utils/scrollToTop";
import Referrals from "./pages/Referrals";
import Trackers from "./components/Trackers";

const App = () => {
Expand Down Expand Up @@ -42,6 +43,7 @@ const App = () => {
/>
<Route path="/trade/:marketId" element={<Trade />} />
<Route path="/portfolio" element={<Portfolio />} />
<Route path="/referrals" element={<Referrals />} />
</Routes>
</Flex>
</AppContainer>
Expand Down
30 changes: 30 additions & 0 deletions src/assets/icons/svg-icons.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,33 @@ export const InfoIcon: FC = () => (
/>
</svg>
);

export const CopyGradientIcon: FC = () => (
<svg
width="15"
height="15"
viewBox="0 0 15 15"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M1 9.50006C1 10.3285 1.67157 11.0001 2.5 11.0001H4L4 10.0001H2.5C2.22386 10.0001 2 9.7762 2 9.50006L2 2.50006C2 2.22392 2.22386 2.00006 2.5 2.00006L9.5 2.00006C9.77614 2.00006 10 2.22392 10 2.50006V4.00002H5.5C4.67158 4.00002 4 4.67159 4 5.50002V12.5C4 13.3284 4.67158 14 5.5 14H12.5C13.3284 14 14 13.3284 14 12.5V5.50002C14 4.67159 13.3284 4.00002 12.5 4.00002H11V2.50006C11 1.67163 10.3284 1.00006 9.5 1.00006H2.5C1.67157 1.00006 1 1.67163 1 2.50006V9.50006ZM5 5.50002C5 5.22388 5.22386 5.00002 5.5 5.00002H12.5C12.7761 5.00002 13 5.22388 13 5.50002V12.5C13 12.7762 12.7761 13 12.5 13H5.5C5.22386 13 5 12.7762 5 12.5V5.50002Z"
fill="url(#paint0_linear_212_3620)"
/>
<defs>
<linearGradient
id="paint0_linear_212_3620"
x1="2.04688"
y1="8.00005"
x2="13.95"
y2="8.00005"
gradientUnits="userSpaceOnUse"
>
<stop stopColor="#FFC955" />
<stop offset="1" stopColor="#FF7CD5" />
</linearGradient>
</defs>
</svg>
);
15 changes: 11 additions & 4 deletions src/components/NavBar/NavLinksSection.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,10 @@ import {
TradeIcon,
TradeActiveIcon,
} from "../../assets/icons/navBar-icons/trade";
// import {
// RocketIcon,
// RocketActiveIcon,
// } from "../../assets/icons/navBar-icons/rocket";
import {
RocketIcon,
RocketActiveIcon,
} from "../../assets/icons/navBar-icons/rocket";
// import {
// PowercardIcon,
// PowercardActiveIcon,
Expand Down Expand Up @@ -67,6 +67,13 @@ const NavLinksSection: React.FC<NavLinksSectionProps> = ({
activeIcon: <BackpackActiveIcon />,
showOnMobile: true,
},
{
to: "/referrals",
label: "Referrals",
icon: <RocketIcon />, // TODO change icon
activeIcon: <RocketActiveIcon />, // TODO change icon
showOnMobile: true,
},
// {
// to: "/powercards",
// label: "PowerCards",
Expand Down
2 changes: 2 additions & 0 deletions src/constants/applications.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ export enum MARKET_CHART_URL {
DEFAULT = "https://api.overlay.market/charts/v1/charts",
}

export const REFERRAL_API_BASE_URL = "https://api.overlay.market/referral";

export const DEFAULT_MARKET_ID = encodeURIComponent("BTC Dominance");

export const TRADE_POLLING_INTERVAL = 30000;
Expand Down
303 changes: 303 additions & 0 deletions src/pages/Referrals/AliasSubmit.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,303 @@
import { useEffect, useState } from "react";
import {
GradientLoaderButton,
GradientSolidButton,
} from "../../components/Button";
import { Flex, Text } from "@radix-ui/themes";
import {
ContentContainer,
GradientText,
StyledInput,
} from "./referrals-styles";
import theme from "../../theme";
import useDebounce from "../../hooks/useDebounce";
import { REFERRAL_API_BASE_URL } from "../../constants/applications";
import { useAccount, useSignTypedData } from "wagmi";
import { isAddress } from "viem";
import { CopyGradientIcon } from "../../assets/icons/svg-icons";
import { CopyLink, Toast } from "./alias-submit-styles";

type AliasSubmitProps = {
alias: string | null;
};

const AliasSubmit: React.FC<AliasSubmitProps> = ({ alias }) => {
const { address: affiliateAddress } = useAccount();
const { signTypedDataAsync } = useSignTypedData();

const [aliasValue, setAliasValue] = useState("");
const [errorMessage, setErrorMessage] = useState<string | null>(null);
const [successMessage, setSuccessMessage] = useState<string | null>(null);
const [fetchingSignature, setFetchingSignature] = useState(false);
const [registeringAlias, setRegisteringAlias] = useState(false);
const [succeededToSubmit, setSucceededToSubmit] = useState(false);
const [toastVisible, setToastVisible] = useState(false);
const [registeredAlias, setRegisteredAlias] = useState(alias);
const debouncedAliasValue = useDebounce(aliasValue, 500);

useEffect(() => {
setErrorMessage(null);
setSuccessMessage(null);
}, [debouncedAliasValue]);

useEffect(() => {
const regex = /^[a-zA-Z0-9]{3,8}$/;
if (debouncedAliasValue !== "") {
if (!regex.test(debouncedAliasValue)) {
setErrorMessage("Input must be 3-8 alphanumeric characters");
} else {
setErrorMessage(null);
checkAliasAvailability(debouncedAliasValue.toLowerCase());
}
}
}, [debouncedAliasValue]);

const checkAliasAvailability = async (alias: string) => {
try {
const response = await fetch(
REFERRAL_API_BASE_URL + `/affiliates/aliases/${alias}`
);
if (response.status === 404) {
setSuccessMessage("Alias is available! Please sign to confirm");
}
if (response.status === 200 && alias !== "") {
setErrorMessage("Alias is already taken");
}
} catch (error) {
console.error("Error checking alias availability", error);
}
};

// EIP 712 signature data
const domain = {
name: "Overlay Referrals",
version: "1.0",
};
const types = {
SetAlias: [
{ name: "affiliate", type: "address" },
{ name: "alias", type: "string" },
],
};
const primaryType = "SetAlias";

const fetchSignature = async (affiliate: string, alias: string) => {
setFetchingSignature(true);
let signature;
try {
signature = await signTypedDataAsync({
domain,
types,
primaryType,
message: { affiliate, alias },
});
} catch (error) {
const errorWithDetails = error as { details?: string };
if (errorWithDetails?.details) {
setErrorMessage(`${errorWithDetails.details}`);
} else {
setErrorMessage("unable to get error, see console");
}
console.error("Error fetching signature:", error);
} finally {
setFetchingSignature(false);
}
return signature;
};

const registerAlias = async (
signature: string,
affiliate: string,
alias: string
) => {
setRegisteringAlias(true);
try {
const response = await fetch(
REFERRAL_API_BASE_URL + `/affiliates/aliases`,
{
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
address: affiliate,
alias,
signature,
}),
}
);

if (!response.ok) {
const result = await response.json();
setErrorMessage(
`Failed to register alias: ${
result?.message ?? "Unable to get error message"
}`
);
throw new Error(`Failed to register alias: ${JSON.stringify(result)}`);
}

const result = await response.json();

if (result.alias && result.createdAt) {
setSucceededToSubmit(true);
setRegisteredAlias(result.alias);
}
} catch (error) {
console.error("Error registering alias:", error);
} finally {
setRegisteringAlias(false);
}
};

const handleAliasConfirm = async () => {
if (!affiliateAddress || !debouncedAliasValue) return;
if (!isAddress(affiliateAddress)) {
setErrorMessage("Invalid affiliate address");
return;
}
const signature = await fetchSignature(
affiliateAddress.toLowerCase(),
debouncedAliasValue.toLowerCase()
);
signature &&
(await registerAlias(
signature,
affiliateAddress.toLowerCase(),
debouncedAliasValue.toLowerCase()
));
setSuccessMessage(null);
};

const showToast = (duration = 3000) => {
setToastVisible(true);

setTimeout(() => {
setToastVisible(false);
}, duration);
};

const handleCopyLink = () => {
navigator.clipboard.writeText(
`${window.location.href}?referrer=${registeredAlias?.toLowerCase()}`
);
showToast();
};

return (
<>
{!succeededToSubmit && (
<>
{alias && (
<ContentContainer>
<Flex direction={"column"} align={"center"} gap="8px">
<Text weight={"medium"}>Your affiliate alias is active</Text>
<GradientText weight={"medium"} size={"4"}>
{alias.toUpperCase()}
</GradientText>
<Flex gap={"8px"}>
<Text weight={"medium"}>Copy referral link</Text>
<CopyLink onClick={handleCopyLink}>
<CopyGradientIcon />
</CopyLink>
<Toast visible={toastVisible.toString()}>
Link copied to clipboard
</Toast>
</Flex>
</Flex>
</ContentContainer>
)}
{!alias && (
<ContentContainer height={debouncedAliasValue && "276px"}>
<Text
size={{ initial: "2", sm: "4" }}
weight={"bold"}
align={"center"}
>
Affiliate Alias
</Text>

<Flex direction={"column"} gap="8px">
<Text
size={"1"}
style={{ paddingLeft: "16px", color: `${theme.color.grey3}` }}
>
Choose a unique name to identify your affiliate profile
</Text>
<StyledInput
type="text"
disabled={fetchingSignature || registeringAlias}
value={aliasValue.toUpperCase()}
onChange={(e) => setAliasValue(e.target.value)}
placeholder="Enter 3-8 alphanumeric chars"
/>

{errorMessage && (
<Text
size="1"
weight={"medium"}
style={{ color: theme.color.red1 }}
>
{errorMessage}
</Text>
)}
{successMessage && (
<Text
size="1"
weight={"medium"}
style={{ color: theme.color.green1 }}
>
{successMessage}
</Text>
)}
</Flex>

{successMessage &&
(fetchingSignature || registeringAlias ? (
<GradientLoaderButton
title={"Processing ..."}
height={"49px"}
/>
) : (
<GradientSolidButton
title={"Sign and Confirm Alias"}
height={"49px"}
handleClick={handleAliasConfirm}
/>
))}
</ContentContainer>
)}
</>
)}

{succeededToSubmit && (
<ContentContainer align={"center"}>
<Text size="7">🎉</Text>
<Text size="6" weight={"bold"}>
Success!
</Text>
<Flex direction={"column"} align={"center"} gap="8px">
<Text weight={"medium"} size="3" align={"center"}>
Alias{" "}
<GradientText weight={"medium"}>
{registeredAlias?.toUpperCase()}
</GradientText>{" "}
has been successfully registered!
</Text>
</Flex>
<Flex gap={"8px"}>
<Text weight={"medium"}>Copy referral link</Text>
<CopyLink onClick={handleCopyLink}>
<CopyGradientIcon />
</CopyLink>
<Toast visible={toastVisible.toString()}>
Link copied to clipboard
</Toast>
</Flex>
</ContentContainer>
)}
</>
);
};

export default AliasSubmit;
Loading