diff --git a/README.md b/README.md index 90f89a7f934..5372b410df5 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,6 @@ Build the documentation site locally using the following steps. ### Prerequisites - [Node.js](https://nodejs.org/) version 18+ -- [Yarn](https://yarnpkg.com/) version 3 - [Git](https://git-scm.com/) ### Steps diff --git a/docusaurus.config.js b/docusaurus.config.js index bf5cd242713..a63e234c787 100644 --- a/docusaurus.config.js +++ b/docusaurus.config.js @@ -199,6 +199,10 @@ const config = { label: "Infura dashboard", to: "developer-tools/dashboard", }, + { + label: "Faucet", + to: "developer-tools/faucet", + }, ], }, { diff --git a/src/components/Accordion/accordion.module.scss b/src/components/Accordion/accordion.module.scss new file mode 100644 index 00000000000..60e06d25f46 --- /dev/null +++ b/src/components/Accordion/accordion.module.scss @@ -0,0 +1,55 @@ +:root[data-theme="dark"] { + --accordion-background: #24272a; + --accordion-border: rgba(132, 140, 150, 0.16); +} + +:root[data-theme="light"] { + --accordion-background: #ffffff; + --accordion-border: rgba(187, 192, 197, 0.4); +} + +.accordion { + background: var(--accordion-background); + border: 1px solid var(--accordion-border); + border-radius: 8px; + margin-bottom: 24px; + + .header { + display: flex; + justify-content: space-between; + align-items: center; + cursor: pointer; + + .closeButton { + cursor: pointer; + margin-left: 24px; + margin-right: 24px; + display: block; + height: 16px; + line-height: 1; + + .image { + min-width: 16px; + width: 16px; + min-height: 16px; + height: 16px; + transition: all 0.5s; + transform: rotate(45deg); + + &.opened { + transform: rotate(0); + } + } + } + } + + .content { + visibility: hidden; + display: none; + + &.opened { + visibility: visible; + display: block; + } + } +} diff --git a/src/components/Accordion/close.svg b/src/components/Accordion/close.svg new file mode 100644 index 00000000000..afe93c72b6f --- /dev/null +++ b/src/components/Accordion/close.svg @@ -0,0 +1,4 @@ + + + diff --git a/src/components/Accordion/index.tsx b/src/components/Accordion/index.tsx new file mode 100644 index 00000000000..8b55e24d218 --- /dev/null +++ b/src/components/Accordion/index.tsx @@ -0,0 +1,52 @@ +import React, { useState } from "react"; +import clsx from "clsx"; +import styles from "./accordion.module.scss"; +import CloseImg from "./close.svg"; +import { trackClickForSegment } from "@site/src/lib/segmentAnalytics"; + +interface IAccordion { + children: [React.ReactElement, React.ReactElement]; + opened?: boolean; +} + +export default function Accordion({ + children: [title, body], + opened = false, +}: IAccordion) { + const [isOpened, setIsOpened] = useState(opened); + + const handleToggle = () => { + trackClickForSegment({ + eventName: `${isOpened ? "Expanded" : "Collapsed"} - ${title}`, + clickType: "Accordion", + userExperience: "B", + responseStatus: null, + responseMsg: null, + timestamp: Date.now(), + }); + setIsOpened((value) => !value); + }; + + return ( +
+
+ {title} + + + +
+
+ {body} +
+
+ ); +} diff --git a/src/components/Alert/index.tsx b/src/components/Alert/index.tsx index e7e90cfc8bf..9ea7acb3f8d 100644 --- a/src/components/Alert/index.tsx +++ b/src/components/Alert/index.tsx @@ -7,6 +7,7 @@ import SuccessImg from "./success.svg"; import ErrorImg from "./error.svg"; import Text from "@site/src/components/Text"; import styles from "./alert.module.scss"; +import { trackClickForSegment } from "@site/src/lib/segmentAnalytics"; export const options = { position: positions.TOP_CENTER, @@ -18,25 +19,44 @@ export const options = { }, }; -export const AlertTemplate = ({ style, options, message, close }) => ( -
- {options.type === types.INFO && } - {options.type === types.SUCCESS && } - {options.type === types.ERROR && } - {message} - - - -
-); +export const AlertTemplate = ({ style, options, message, close }) => { + const handleCloseAlert = () => { + trackClickForSegment({ + eventName: "Close", + clickType: "Alert", + userExperience: "B", + responseStatus: null, + responseMsg: null, + timestamp: Date.now(), + }); + close(); + }; + + return ( +
+ {options.type === types.INFO && } + {options.type === types.SUCCESS && } + {options.type === types.ERROR && } + {message} + + + +
+ ); +}; export const AlertTitle = ({ children, diff --git a/src/components/AuthLogin/AuthModal.tsx b/src/components/AuthLogin/AuthModal.tsx index 689865c67f8..2cbd4021e39 100644 --- a/src/components/AuthLogin/AuthModal.tsx +++ b/src/components/AuthLogin/AuthModal.tsx @@ -7,15 +7,14 @@ import global from "../ParserOpenRPC/global.module.css"; import Icon from "../Icon/Icon"; import { authenticateAndAuthorize, + AUTH_WALLET_PAIRING, + AUTH_WALLET_SESSION_NAME, + AUTH_WALLET_PROJECTS, saveTokenString, getUserIdFromJwtToken, + AUTH_WALLET_USER_PLAN, } from "../../lib/siwsrp/auth"; -import { - DASHBOARD_URL, - REQUEST_PARAMS, - AUTH_WALLET_SESSION_NAME, - AUTH_WALLET_PROJECTS, -} from "@site/src/lib/constants"; +import { DASHBOARD_URL, REQUEST_PARAMS } from "@site/src/lib/constants"; import { MetamaskProviderContext } from "@site/src/theme/Root"; Modal.setAppElement("#__docusaurus"); @@ -23,8 +22,10 @@ type AuthModalProps = { open: boolean; setOpen: (arg: boolean) => void; setUser: (arg: string) => void; + setToken: (arg: string) => void; step: AUTH_LOGIN_STEP; setStep: (arg: AUTH_LOGIN_STEP) => void; + setUksTier: (arg: string) => void; }; export enum AUTH_LOGIN_STEP { @@ -134,7 +135,15 @@ const ConnectionErrorModal = ({ ); }; -const AuthModal = ({ open, setOpen, step, setStep }: AuthModalProps) => { +const AuthModal = ({ + open, + setOpen, + step, + setStep, + setUser, + setToken, + setUksTier, +}: AuthModalProps) => { const { siteConfig } = useDocusaurusContext(); const { DASHBOARD_PREVIEW_URL, VERCEL_ENV } = siteConfig?.customFields || {}; const { @@ -168,30 +177,29 @@ const AuthModal = ({ open, setOpen, step, setStep }: AuthModalProps) => { // Call Profile SDK API to retrieve Hydra Access Token & Wallet userProfile // Hydra Access Token will be used to fetch Infura API const { accessToken, userProfile } = await authenticateAndAuthorize( - VERCEL_ENV as string + VERCEL_ENV as string, ); const loginResponse = await ( await fetch( `${DASHBOARD_URL(DASHBOARD_PREVIEW_URL, VERCEL_ENV)}/api/wallet/login`, { - ...REQUEST_PARAMS(), - headers: { - ...REQUEST_PARAMS().headers, + ...REQUEST_PARAMS("POST", { hydra_token: accessToken, token: "true", - }, + }), body: JSON.stringify({ profileId: userProfile.profileId, redirect_to: window.location.href, }), - } + }, ) ).json(); if (!loginResponse) throw new Error("Something went wrong"); const { data, session, token } = loginResponse; + localStorage.setItem(AUTH_WALLET_PAIRING, JSON.stringify({ data })); if (data.step) { // Handling no wallet pairing or multiple pairing @@ -201,7 +209,7 @@ const AuthModal = ({ open, setOpen, step, setStep }: AuthModalProps) => { mmAuthSession: localStorage.getItem(AUTH_WALLET_SESSION_NAME), walletPairing: data.pairing, token: true, - }) + }), ).toString("base64"); const walletLinkUrl = `${DASHBOARD_URL(DASHBOARD_PREVIEW_URL, VERCEL_ENV)}/login?mm_auth=${mm_auth}&redirect_to=${session.redirect_to}`; @@ -229,25 +237,43 @@ const AuthModal = ({ open, setOpen, step, setStep }: AuthModalProps) => { } saveTokenString(token); + if (setToken) { + setToken(token); + } setStep(AUTH_LOGIN_STEP.CONNECTION_SUCCESS); const userId = getUserIdFromJwtToken(); + if (setUser) { + setUser(userId); + } // You can use Infura Access Token to fetch any Infura API endpoint const projectsResponse = await fetch( `${DASHBOARD_URL(DASHBOARD_PREVIEW_URL, VERCEL_ENV)}/api/v1/users/${userId}/projects`, { - ...REQUEST_PARAMS("GET"), - headers: { - ...REQUEST_PARAMS("GET").headers, - Authorization: `Bearer ${token}`, - }, - } + ...REQUEST_PARAMS("GET", { Authorization: `Bearer ${token}` }), + }, ); const { result: { projects }, } = await projectsResponse.json(); sessionStorage.setItem(AUTH_WALLET_PROJECTS, JSON.stringify(projects)); setProjects(projects); + + const uksUserRawResp = await fetch( + `${DASHBOARD_URL(DASHBOARD_PREVIEW_URL, VERCEL_ENV)}/api/v1/users/${userId}`, + { + ...REQUEST_PARAMS("GET", { Authorization: `Bearer ${token}` }), + }, + ); + const { + result: { + servicePlan: { tier }, + }, + } = await uksUserRawResp.json(); + sessionStorage.setItem(AUTH_WALLET_USER_PLAN, JSON.stringify(tier)); + if (setUser) { + setUksTier(tier); + } setOpen(false); } catch (e: any) { if (pathname.startsWith("/wallet/reference")) { diff --git a/src/components/Badge/badge.module.scss b/src/components/Badge/badge.module.scss new file mode 100644 index 00000000000..363bdcb8367 --- /dev/null +++ b/src/components/Badge/badge.module.scss @@ -0,0 +1,46 @@ +:root { + --badge-default-color: #6a737d; + --badge-default-bg-color: #f2f4f6; + --badge-default-border-color: transparent; + --badge-success-color: #1c8234; + --badge-success-bg-color: rgba(28, 130, 52, 0.1); + --badge-error-color: #d73847; + --badge-error-bg-color: rgba(215, 56, 71, 0.1); +} + +:root[data-theme="dark"] { + --badge-default-color: #bbc0c5; + --badge-default-bg-color: #24272a; + --badge-default-border-color: #bbc0c5; + --badge-success-color: #28a745; + --badge-success-bg-color: rgba(40, 167, 69, 0.15); + --badge-error-color: #e06470; + --badge-error-bg-color: rgba(224, 100, 112, 0.15); +} + +.badge { + display: inline-flex; + align-items: center; + font-size: 12px; + line-height: 20px; + font-weight: 500; + letter-spacing: 0.25px; + border-radius: 999px; + border: 1px solid var(--badge-default-border-color); + padding: 2px 8px; + color: var(--badge-default-color); + background-color: var(--badge-default-bg-color); + text-transform: capitalize; + + &.success { + color: var(--badge-success-color); + background-color: var(--badge-success-bg-color); + border-color: transparent; + } + + &.error { + color: var(--badge-error-color); + background-color: var(--badge-error-bg-color); + border-color: transparent; + } +} diff --git a/src/components/Badge/index.tsx b/src/components/Badge/index.tsx new file mode 100644 index 00000000000..a0c14c7cec4 --- /dev/null +++ b/src/components/Badge/index.tsx @@ -0,0 +1,14 @@ +import React from "react"; +import styles from "./badge.module.scss"; +import clsx from "clsx"; + +type variant = "error" | "success" | "default"; + +interface IBadge { + variant?: variant; + label: string; +} + +export default function Badge({ variant = "default", label }: IBadge) { + return {label}; +} diff --git a/src/components/Button/button.module.scss b/src/components/Button/button.module.scss index 46674966a0f..de71208ae00 100644 --- a/src/components/Button/button.module.scss +++ b/src/components/Button/button.module.scss @@ -1,6 +1,7 @@ :root[data-theme="dark"] { --button-primary-background-color: #1098fc; --button-secondary-background-color: transparent; + --button-background-color: #1098fc; --button-color: #141618; --button-hover-background-color: #036ab5; --button-hover-shadow: 0px 2px 8px 0px rgba(16, 152, 252, 0.4); @@ -11,6 +12,7 @@ :root[data-theme="light"] { --button-primary-background-color: #0376c9; --button-secondary-background-color: transparent; + --button-background-color: #0376c9; --button-color: #ffffff; --button-hover-background-color: #036ab5; --button-hover-shadow: 0px 2px 8px 0px rgba(3, 118, 201, 0.2); @@ -35,6 +37,7 @@ a.button { .button { color: var(--button-color); + background-color: var(--button-background-color); border: none; padding: 0 16px; height: 48px; diff --git a/src/components/Button/index.tsx b/src/components/Button/index.tsx index 3178f1ddc22..8fea17e551a 100644 --- a/src/components/Button/index.tsx +++ b/src/components/Button/index.tsx @@ -5,6 +5,7 @@ import clsx from "clsx"; import styles from "./button.module.scss"; interface IButton { + testId?: string; onClick?: VoidFunction; children: string | React.ReactElement; disabled?: boolean; @@ -20,6 +21,7 @@ interface IButton { } export const Button = ({ + testId, className, onClick = () => {}, children, @@ -50,6 +52,7 @@ export const Button = ({ return !href ? ( + ) : !Object.keys(projects).length ? ( + <> + {walletLinked === undefined && ( + + )} + {walletLinked === WALLET_LINK_TYPE.NO && ( + + )} + {walletLinked === WALLET_LINK_TYPE.MULTIPLE && ( + + )} + + ) : ( + + )} + + + + ); +} diff --git a/src/components/Faucet/Maintenance.tsx b/src/components/Faucet/Maintenance.tsx new file mode 100644 index 00000000000..fb03154efb9 --- /dev/null +++ b/src/components/Faucet/Maintenance.tsx @@ -0,0 +1,47 @@ +import MaintenanceIco from "./maintenance.svg"; +import Text from "@site/src/components/Text"; +import Button from "@site/src/components/Button"; + +import styles from "./maintenance.module.scss"; +import React from "react"; +import { trackClickForSegment } from "@site/src/lib/segmentAnalytics"; + +const Maintenance = ({ network }: { network: "linea" | "sepolia" }) => { + const SEPOLIA_URL = "https://faucetlink.to/sepolia"; + const LINEA_URL = + "https://docs.linea.build/build-on-linea/use-linea-testnet/fund"; + + const handleOnClick = () => { + trackClickForSegment({ + eventName: "Explore Alternative Faucets", + clickType: `Maintenance ${network}`, + userExperience: "B", + responseStatus: null, + responseMsg: null, + timestamp: Date.now(), + }); + }; + + return ( +
+
+ + The faucet is at full capacity due to high demand. + + Try + checking back later. Thank you for your patience. Need ETH urgently? + + +
+
+ ); +}; + +export default Maintenance; diff --git a/src/components/Faucet/TransactionTable.tsx b/src/components/Faucet/TransactionTable.tsx new file mode 100644 index 00000000000..d3dd26b297a --- /dev/null +++ b/src/components/Faucet/TransactionTable.tsx @@ -0,0 +1,129 @@ +import React, { useMemo } from "react"; +import Link from "@docusaurus/Link"; +import Badge from "@site/src/components/Badge"; +import Table from "@site/src/components/Table"; +import Text from "@site/src/components/Text"; +import { LINEA_URL, SEPOLIA_URL } from "@site/src/pages/developer-tools/faucet"; +import { trackClickForSegment } from "@site/src/lib/segmentAnalytics"; + +const hideCenterLetters = (word) => { + if (word.length < 10) return word; + return `${word.substring(0, 5)}...${word.substring(word.length - 4)}`; +}; + +const transformWordEnding = (value, end) => { + const upValue = Math.floor(value); + return `${upValue} ${end}${upValue === 1 ? "" : "s"} ago`; +}; + +const getDiffTime = (time) => { + if (!time) return "unknown"; + const currentTime = Date.now(); + const startTime = new Date(time).getTime(); + const deltaTimeInSec = (currentTime - startTime) / 1000; + const deltaTimeInMin = deltaTimeInSec / 60; + const deltaTimeInHours = deltaTimeInMin / 60; + const deltaTimeInDays = deltaTimeInHours / 24; + + if (deltaTimeInMin < 1) return transformWordEnding(deltaTimeInSec, "second"); + if (deltaTimeInHours < 1) + return transformWordEnding(deltaTimeInMin, "minute"); + if (deltaTimeInDays < 1) return transformWordEnding(deltaTimeInHours, "hour"); + return transformWordEnding(deltaTimeInDays, "day"); +}; + +const renderStatus = (status) => { + switch (status) { + case "success": + return "success"; + case "failed": + return "error"; + default: + return "default"; + } +}; + +const renderValue = (status) => { + switch (status) { + case "success": + return "successful"; + case "failed": + return "failed"; + default: + return "pending"; + } +}; + +interface ITransactionTable { + className?: string; + classNameHeading?: string; + data: { + id: string; + createdAt: string; + txnHash: string; + value: string; + status: "pending" | "success" | "failure"; + hash: string; + }[]; + network: "sepolia" | "linea"; +} + +export default function TransactionTable({ + className, + classNameHeading, + data = [], + network, +}: ITransactionTable) { + if (data?.length === 0) return null; + + const handleClickViewTransaction = () => { + trackClickForSegment({ + eventName: "View on Etherscan", + clickType: `Transactions Table`, + userExperience: "B", + responseStatus: null, + responseMsg: null, + timestamp: Date.now(), + }); + }; + + const dataRows = useMemo(() => { + return data.map((item) => ({ + cells: [ + hideCenterLetters(item.txnHash), + getDiffTime(item.createdAt), + `${item.value} ETH`, + , + + View on Etherscan + , + ], + })); + }, [data]); + + return ( +
+
+ Transaction History + + Here is a list of your requests from the MetaMask faucet. View your transaction on Etherscan for more information. + +
+ + + ); +} diff --git a/src/components/Faucet/eth.svg b/src/components/Faucet/eth.svg new file mode 100644 index 00000000000..a3b04dfb471 --- /dev/null +++ b/src/components/Faucet/eth.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/components/Faucet/faq.module.scss b/src/components/Faucet/faq.module.scss new file mode 100644 index 00000000000..872b2fda519 --- /dev/null +++ b/src/components/Faucet/faq.module.scss @@ -0,0 +1,8 @@ +.accordionHeader { + margin: 0; + padding: 24px; +} +.accordionContainer { + margin: 0; + padding: 0 24px 24px; +} diff --git a/src/components/Faucet/hero.module.scss b/src/components/Faucet/hero.module.scss new file mode 100644 index 00000000000..67b7fc84baa --- /dev/null +++ b/src/components/Faucet/hero.module.scss @@ -0,0 +1,80 @@ +:root[data-theme="dark"] { + --hero-text-color: #fff; + + .hero { + &.sepolia { + --hero-image: url("/img/faucet-sepolia-hero-dark.png"); + } + + &.linea { + --hero-image: url("/img/faucet-linea-hero-dark.png"); + } + } +} + +:root[data-theme="light"] { + --hero-text-color: #141618; + + .hero { + &.sepolia { + --hero-image: url("/img/faucet-sepolia-hero-light.png"); + } + + &.linea { + --hero-image: url("/img/faucet-linea-hero-light.png"); + } + } +} + +.hero { + color: var(--hero-text-color); + background-image: var(--hero-image); + background-repeat: no-repeat; + background-position: 100% 0; + background-size: auto 100%;; + + @media screen and (max-width: 996px) { + background-image: none; + } + + .actions { + display: flex; + + @media screen and (max-width: 996px) { + flex-direction: column; + } + + > *:not(:last-child) { + margin-right: 8px; + } + + .inputCont { + max-width: 600px; + width: 100%; + + @media screen and (max-width: 996px) { + max-width: 100%; + } + + .caption { + font-size: 12px; + font-style: normal; + font-weight: 400; + line-height: 20px; + letter-spacing: 0.25px; + } + } + + .alignedButtons { + margin-top: 22px; + } + + .button { + height: 48px; + + @media screen and (max-width: 996px) { + width: 100%; + } + } + } +} diff --git a/src/components/Faucet/index.tsx b/src/components/Faucet/index.tsx new file mode 100644 index 00000000000..b6b86826153 --- /dev/null +++ b/src/components/Faucet/index.tsx @@ -0,0 +1,10 @@ +export { default as Faq } from "./Faq"; +export { default as TransactionTable } from "./TransactionTable"; +export { default as Hero } from "./Hero"; +export { default as Maintenance } from "./Maintenance"; +export { + AlertCommonIssue, + AlertPastActivity, + AlertCooldown, + AlertSuccess, +} from "./Alerts"; diff --git a/src/components/Faucet/maintenance.module.scss b/src/components/Faucet/maintenance.module.scss new file mode 100644 index 00000000000..73a9b4eb1ec --- /dev/null +++ b/src/components/Faucet/maintenance.module.scss @@ -0,0 +1,43 @@ +:root[data-theme="dark"] { + --maintenance-modal-shadow: rgba(0, 0, 0, 0.4); + --maintenance-modal-background: #24272a; +} + +:root[data-theme="light"] { + --maintenance-modal-shadow: rgba(44, 59, 88, 0.1); + --maintenance-modal-background: #fff; +} + +.maintenance { + background: rgba(0, 0, 0, 0.4); + position: absolute; + top: -55px; + bottom: 0; + left: 0; + right: 0; + padding: 25px 0; + z-index: 5; + + .modal { + position: sticky; + top: 80px; + width: 100%; + max-width: 480px; + margin: 0 auto; + padding: 16px; + text-align: center; + border-radius: 8px; + background: var(--maintenance-modal-background); + box-shadow: + 0px 0px 16px 0px var(--maintenance-modal-shadow), + 0px 32px 32px 0px var(--maintenance-modal-shadow), + 0px 16px 16px 0px var(--maintenance-modal-shadow), + 0px 8px 8px 0px var(--maintenance-modal-shadow), + 0px 4px 4px 0px var(--maintenance-modal-shadow), + 0px 2px 2px 0px var(--maintenance-modal-shadow); + + .button { + padding: 0 36px; + } + } +} diff --git a/src/components/Faucet/maintenance.svg b/src/components/Faucet/maintenance.svg new file mode 100644 index 00000000000..1e56af8ef42 --- /dev/null +++ b/src/components/Faucet/maintenance.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/src/components/Icon/Icon.jsx b/src/components/Icon/Icon.jsx index 1c2ee20acc7..57db65f49de 100644 --- a/src/components/Icon/Icon.jsx +++ b/src/components/Icon/Icon.jsx @@ -209,8 +209,8 @@ const Icon = ({ name, classes }) => { @@ -224,10 +224,10 @@ const Icon = ({ name, classes }) => { ); case "close": return ( - diff --git a/src/components/Input/index.tsx b/src/components/Input/index.tsx new file mode 100644 index 00000000000..e488bc2afe1 --- /dev/null +++ b/src/components/Input/index.tsx @@ -0,0 +1,39 @@ +import React from "react"; +import clsx from "clsx"; + +import styles from "./input.module.scss"; + +interface IInput { + onChange?: (string) => void; + disabled?: boolean; + label?: string; + className?: string; + error?: string; + placeholder?: string; + value?: string; +} + +export default function Input({ + className, + onChange, + label, + disabled = false, + error, + placeholder, + value, +}: IInput) { + return ( +

{label}

+ onChange(e?.target?.value)} + /> + {error &&

{error}

} + + ); +} diff --git a/src/components/Input/input.module.scss b/src/components/Input/input.module.scss new file mode 100644 index 00000000000..97d9af99651 --- /dev/null +++ b/src/components/Input/input.module.scss @@ -0,0 +1,66 @@ +:root[data-theme="dark"] { + --input-border: #848c96; + --input-background: #24272a; + --input-color: #fff; + --input-error: #e06470; +} + +:root[data-theme="light"] { + --input-border: #bbc0c5; + --input-background: #fff; + --input-color: #141618; + --input-error: #d73847; +} + +.container { + width: 100%; + + .label { + text-overflow: ellipsis; + font-size: 14px; + font-style: normal; + font-weight: 500; + line-height: 22px; + margin-bottom: 0; + } + + .input { + display: flex; + height: 48px; + padding: 12px 16px; + align-items: center; + gap: 10px; + align-self: stretch; + border-radius: 8px; + border: 1px solid var(--input-border); + background: var(--input-background); + overflow: hidden; + color: var(--input-color); + font-size: 14px; + font-style: normal; + font-weight: 400; + line-height: 22px; + outline: none; + width: 100%; + margin-bottom: 4px; + + &:disabled { + opacity: 0.5; + pointer-events: none; + } + + &.error { + border: 1px solid var(--input-error); + } + } + + .errorMsg { + color: var(--input-error); + font-size: 12px; + font-style: normal; + font-weight: 400; + line-height: 20px; + letter-spacing: 0.25px; + margin-bottom: 0; + } +} diff --git a/src/components/Input/loading.svg b/src/components/Input/loading.svg new file mode 100644 index 00000000000..355a53ad5d7 --- /dev/null +++ b/src/components/Input/loading.svg @@ -0,0 +1,4 @@ + + + diff --git a/src/components/NavbarWallet/index.tsx b/src/components/NavbarWallet/index.tsx index 558204aa4a9..3c0b184cef4 100644 --- a/src/components/NavbarWallet/index.tsx +++ b/src/components/NavbarWallet/index.tsx @@ -8,6 +8,7 @@ import { MetamaskProviderContext } from "@site/src/theme/Root"; import BrowserOnly from "@docusaurus/BrowserOnly"; import styles from "./navbarWallet.module.scss"; import { Tooltip } from "@site/src/components/Tooltip"; +import { trackClickForSegment } from "@site/src/lib/segmentAnalytics"; interface INavbarWalletComponent { includeUrl: string[]; @@ -32,17 +33,33 @@ const NavbarWalletComponent: FC = ({ } = useContext(MetamaskProviderContext); const [dropdownOpen, setDropdownOpen] = useState(false); const [copyMessage, setCopyMessage] = useState(COPY_TEXT); - const isExtensionActive = sdk.isExtensionActive; + const isExtensionActive = sdk.isExtensionActive(); const dialogRef = useRef(null); const buttonRef = useRef(null); const toggleDropdown = () => { setDropdownOpen((value) => !value); + trackClickForSegment({ + eventName: "Account dropdown", + clickType: "Navbar", + userExperience: "B", + responseStatus: null, + responseMsg: null, + timestamp: Date.now(), + }); }; const handleCopy = () => { navigator.clipboard.writeText(metaMaskAccount); setCopyMessage(COPIED_TEXT); + trackClickForSegment({ + eventName: "Copy wallet address", + clickType: "Navbar", + userExperience: "B", + responseStatus: null, + responseMsg: null, + timestamp: Date.now(), + }); }; const handleClickOutside = (event: MouseEvent) => { @@ -68,17 +85,52 @@ const NavbarWalletComponent: FC = ({ }; }, [dropdownOpen]); + const handleDisconnect = () => { + trackClickForSegment({ + eventName: "Disconnect account", + clickType: "Navbar", + userExperience: "B", + responseStatus: null, + responseMsg: null, + timestamp: Date.now(), + }); + metaMaskDisconnect(); + setDropdownOpen(false); + }; + + const handleConnectWallet = () => { + trackClickForSegment({ + eventName: !isExtensionActive ? "Install MetaMask" : "Connect Wallet", + clickType: "Navbar", + userExperience: "B", + responseStatus: null, + responseMsg: null, + timestamp: Date.now(), + }); + metaMaskWalletIdConnectHandler(); + }; + return !metaMaskAccount ? ( ) : (
- diff --git a/src/components/NavbarWallet/navbarWallet.module.scss b/src/components/NavbarWallet/navbarWallet.module.scss index fe0152a0e46..340b26698f5 100644 --- a/src/components/NavbarWallet/navbarWallet.module.scss +++ b/src/components/NavbarWallet/navbarWallet.module.scss @@ -95,8 +95,13 @@ .disconnect { width: 100%; - .icon { - margin-right: 8px; + .content { + display: flex; + align-items: center; + + .icon { + margin-right: 8px; + } } } } diff --git a/src/components/ParserOpenRPC/ProjectsBox/styles.module.css b/src/components/ParserOpenRPC/ProjectsBox/styles.module.css new file mode 100644 index 00000000000..76d42e0f4c9 --- /dev/null +++ b/src/components/ParserOpenRPC/ProjectsBox/styles.module.css @@ -0,0 +1,25 @@ +.selectWrapper { + padding: 0 0 24px 0; +} + +.selectTitle { +font-size: 16px; +font-weight: 500; +line-height: 24px; +text-align: left; +padding: 0 0 8px 0; +} + +.selectProjects { + background: #141618 !important; + border: 1px solid #848C96 !important; + border-radius: 8px !important; + padding: 12px 16px 12px 16px !important; + font-size: 16px !important; +} +.selectDropdown { + background: #141618 !important; + border: 1px solid #848C96 !important; + border-radius: 8px !important; + font-size: 16px !important; +} diff --git a/src/components/Table/index.tsx b/src/components/Table/index.tsx new file mode 100644 index 00000000000..b6ad083a9ed --- /dev/null +++ b/src/components/Table/index.tsx @@ -0,0 +1,46 @@ +import React from "react"; +import styles from "./table.module.scss"; +import clsx from "clsx"; + +type TableCell = string | React.ReactElement; + +interface TableRow { + cells: TableCell[]; +} + +interface ITable { + classes?: string; + thCells: TableCell[]; + trRows?: TableRow[]; +} + +export default function Table({ classes, thCells = [], trRows = [] }: ITable) { + return ( +
+
+
+ + + {thCells.map((cell, i) => ( + + ))} + + + + {trRows.map((row, i) => ( + + {row.cells.map((cell, y) => ( + + ))} + + ))} + +
+ {cell} +
+ {cell} +
+
+ + ); +} diff --git a/src/components/Table/table.module.scss b/src/components/Table/table.module.scss new file mode 100644 index 00000000000..23d88a66491 --- /dev/null +++ b/src/components/Table/table.module.scss @@ -0,0 +1,70 @@ +:root { + --table-border-color: rgba(132, 140, 150, 0.16); + --table-bg-color: #fff; + --table-bg-thead-color: #f2f4f6; +} + +:root[data-theme="dark"] { + --table-border-color: rgba(132, 140, 150, 0.16); + --table-bg-color: #24272a; + --table-bg-thead-color: #24272a; +} + +.tableWrapper { + max-width: 1014px; + width: 100%; + margin: 0 auto; + overflow-x: auto; + margin-bottom: 24px; +} + +.tableInner { + border: 1px solid var(--table-border-color); + border-radius: 8px; + overflow: hidden; + min-width: 768px; +} + +.table { + display: table; + border-collapse: collapse; + width: 100%; + font-size: 16px; + line-height: 24px; + font-weight: 400; + margin: 0; + background-color: var(--table-bg-color); + + .thead { + background-color: var(--table-bg-thead-color); + } + + .trow { + background-color: transparent; + border-top: 1px solid var(--table-border-color); + } +} + +.throw { + background-color: transparent; + border: 0; + + .thcell { + font-size: 16px; + line-height: 24px; + font-weight: 500; + padding: 16px; + text-align: left; + background-color: transparent !important; + border: 0; + } +} + +.tdcell { + font-size: 16px; + line-height: 24px; + font-weight: 400; + padding: 16px; + background-color: transparent; + border: 0; +} diff --git a/src/components/Text/index.tsx b/src/components/Text/index.tsx index 8d0564a3abf..bea166e6930 100644 --- a/src/components/Text/index.tsx +++ b/src/components/Text/index.tsx @@ -8,7 +8,11 @@ interface IText { className?: string; } -export default function Text({ as = "p", children, className }: IText) { +export default function Text({ + as = "p", + children, + className, +}: IText): React.JSX.Element { switch (as) { case "h1": return

{children}

; diff --git a/src/lib/constants.js b/src/lib/constants.js index f10a18bdc57..e06c01b3df7 100644 --- a/src/lib/constants.js +++ b/src/lib/constants.js @@ -458,16 +458,22 @@ export const GET_OPTIONS = { export const REF_SERVICES_PATH = "/services/reference/"; export const REF_WALLET_PATH = "/wallet/reference/"; +export const REF_FAUCET_PATH = "/developer-tools/faucet/"; -export const REF_ALLOW_LOGIN_PATH = [REF_SERVICES_PATH, REF_WALLET_PATH]; +export const REF_ALLOW_LOGIN_PATH = [ + REF_SERVICES_PATH, + REF_WALLET_PATH, + REF_FAUCET_PATH, +]; -export const REQUEST_PARAMS = (method = "POST") => ({ +export const REQUEST_PARAMS = (method = "POST", headers = {}) => ({ method, headers: { Accept: "application/json", "Content-Type": "application/json", "Cache-Control": "no-cache", Pragma: "no-cache", + ...headers }, }); diff --git a/src/lib/siwsrp/auth.ts b/src/lib/siwsrp/auth.ts index b294741441e..3cd1a4b999f 100644 --- a/src/lib/siwsrp/auth.ts +++ b/src/lib/siwsrp/auth.ts @@ -1,10 +1,5 @@ import { SDK } from "@metamask/profile-sync-controller"; import jwt from "jsonwebtoken"; -import { - AUTH_WALLET_PROJECTS, - AUTH_WALLET_SESSION_NAME, - AUTH_WALLET_TOKEN, -} from "@site/src/lib/constants"; type HydraEnv = { authApiUrl: string; @@ -14,7 +9,12 @@ type HydraEnv = { platform: SDK.Platform; }; -const { AuthType, Env, getEnvUrls, JwtBearerAuth, Platform } = SDK; +const { AuthType, Env, getEnvUrls, JwtBearerAuth, Platform } = SDK +export const AUTH_WALLET_PAIRING = 'auth.wallet.pairing' +export const AUTH_WALLET_SESSION_NAME = 'auth.wallet.session' +export const AUTH_WALLET_TOKEN = 'auth.wallet.token' +export const AUTH_WALLET_PROJECTS = 'auth.wallet.projects' +export const AUTH_WALLET_USER_PLAN = 'auth.wallet.uksTier' const getHydraEnv = (env: string): HydraEnv => { const platform = Platform.INFURA; @@ -76,6 +76,14 @@ export const saveTokenString = (token: string) => { sessionStorage.setItem(AUTH_WALLET_TOKEN, token); }; +export const getTokenString = (): string => { + return sessionStorage.getItem(AUTH_WALLET_TOKEN) +} + +export const getUksTier = (): string => { + return sessionStorage.getItem(AUTH_WALLET_USER_PLAN) +} + export const getUserIdFromJwtToken = () => { const token = sessionStorage.getItem(AUTH_WALLET_TOKEN); const decoded = jwt.decode(token as string, { complete: true }) as jwt.Jwt; @@ -85,6 +93,7 @@ export const getUserIdFromJwtToken = () => { export const clearStorage = () => { sessionStorage.clear(); + localStorage.removeItem(AUTH_WALLET_PAIRING); localStorage.removeItem(AUTH_WALLET_SESSION_NAME); localStorage.removeItem(AUTH_WALLET_TOKEN); localStorage.removeItem(AUTH_WALLET_PROJECTS); diff --git a/src/pages/developer-tools/faucet.module.scss b/src/pages/developer-tools/faucet.module.scss new file mode 100644 index 00000000000..2029b2afe86 --- /dev/null +++ b/src/pages/developer-tools/faucet.module.scss @@ -0,0 +1,120 @@ +:root[data-theme="dark"] { + --faucet-bg-color-primary: #252526; + --faucet-box-shadow: none; + --faucet-faq-bg: rgb(20, 22, 24); + --faucet-hero-border: rgba(132, 140, 150, 0.16); + --faucet-hero-background: #24272a; +} + +:root[data-theme="light"] { + --faucet-bg-color-primary: #ffffff; + --faucet-box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.06); + --faucet-faq-bg: rgb(242, 244, 246); + --faucet-hero-border: rgba(187, 192, 197, 0.4); + --faucet-hero-background: #f2f4f6; +} + +:global(.navbar) { + box-shadow: none; +} + +.authCont { + padding: var(--ifm-navbar-padding-vertical) + var(--ifm-navbar-padding-horizontal); + height: 80px; + display: flex; + justify-content: space-between; + align-items: center; + background: var(--faucet-bg-color-primary); + box-shadow: var(--ifm-navbar-shadow); + + @media screen and (max-width: 996px) { + box-shadow: none; + height: 110px; + padding-bottom: 70px; + } + + .title { + font-weight: 700; + font-size: 14px; + } +} + +.tabs { + :global(.tabs-container) { + display: flex; + flex-direction: column; + align-items: center; + margin-bottom: 0; + + > *:last-child { + width: 100%; + position: relative; + } + } + + .header { + font-weight: 400; + white-space: nowrap; + position: relative; + top: -60px; + display: inline-flex; + + > li { + padding: 6px 8px; + border-bottom-width: 2px; + + &:hover { + background: none; + } + } + } + + .content { + width: 100%; + padding: 0; + + .sectionHeading { + text-align: center; + margin: 0 auto; + width: 100%; + max-width: 600px; + margin-bottom: 48px; + } + + .topContent { + margin-bottom: 60px; + padding: 0 var(--ifm-navbar-padding-horizontal); + + .hero { + max-width: 1420px; + width: 100%; + margin: 0 auto; + border-radius: 8px; + border: 1px solid var(--faucet-hero-border); + background-color: var(--faucet-hero-background); + padding: 32px; + margin-bottom: 60px; + } + } + + .bottomContent { + background: var(--faucet-faq-bg); + padding: 60px var(--ifm-navbar-padding-horizontal); + + .faq { + margin: 0 auto; + width: 100%; + max-width: 1014px; + + > *:last-child { + margin-bottom: 0; + } + + @media screen and (max-width: 996px) { + width: 100%; + } + } + } + } +} diff --git a/src/pages/developer-tools/faucet.tsx b/src/pages/developer-tools/faucet.tsx new file mode 100644 index 00000000000..582018f82b0 --- /dev/null +++ b/src/pages/developer-tools/faucet.tsx @@ -0,0 +1,246 @@ +import React, { useContext, useEffect, useState } from "react"; +import Layout from "@theme/Layout"; +import Tabs from "@theme/Tabs"; +import TabItem from "@theme/TabItem"; +import ldClient from "launchdarkly"; +import useDocusaurusContext from "@docusaurus/useDocusaurusContext"; +import { + Faq, + AlertCommonIssue, + AlertCooldown, + AlertSuccess, + TransactionTable, + Hero, + Maintenance, + AlertPastActivity, +} from "@site/src/components/Faucet"; +import { useAlert } from "react-alert"; +import {MetamaskProviderContext} from "@site/src/theme/Root"; + +import styles from "./faucet.module.scss"; +import { DASHBOARD_URL, REQUEST_PARAMS } from "@site/src/lib/constants"; +import { AlertBalanceTooLow } from "@site/src/components/Faucet/Alerts"; +import { + trackInputChangeForSegment, + trackPageViewForSegment, +} from "@site/src/lib/segmentAnalytics"; + +const lineaMaintenanceFlag = "linea-maintenance-mode"; +const sepoliaMaintenanceFlag = "sepolia-maintenance-mode"; +const faucetBypassDomainFlag = "faucet-bypass-domains"; +const DEFAULT_TRANSACTIONS_LIST = { linea: [], sepolia: [] }; + +export const SEPOLIA_URL = "https://sepolia.etherscan.io/tx/"; +export const LINEA_URL = "https://sepolia.lineascan.build/tx/"; + +export default function Faucet() { + const { siteConfig } = useDocusaurusContext(); + const { userId, token, uksTier, metaMaskAccount } = useContext(MetamaskProviderContext); + const alert = useAlert(); + const [transactions, setTransactions] = useState(DEFAULT_TRANSACTIONS_LIST); + const [isLoading, setIsLoading] = useState(false); + const [walletAddress, setWalletAddress] = useState(""); + const [ldReady, setLdReady] = useState(false); + const [isLineaMaintenance, setIsLineaMaintenance] = useState(false); + const [isSepoliaMaintenance, setIsSepoliaMaintenance] = useState(false); + const [faucetBypassDomain, setFaucetBypassDomain] = useState(false); + const { DASHBOARD_PREVIEW_URL, VERCEL_ENV } = siteConfig?.customFields || {}; + + const isLimitedUserPlan = uksTier === "core" && !faucetBypassDomain; + + const setTransactionsForNetwork = (network: "linea" | "sepolia", data) => { + setTransactions((transactions) => ({ ...transactions, [network]: data })); + }; + + const mutateTransactionsForNetwork = (network: "linea" | "sepolia", data) => { + setTransactions((transactions) => ({ + ...transactions, + [network]: [data, ...transactions[network]], + })); + }; + + const getTransactions = async () => { + const sepolia = await fetch( + `${DASHBOARD_URL(DASHBOARD_PREVIEW_URL, VERCEL_ENV)}/api/faucets/sepolia/transactions`, + { + ...REQUEST_PARAMS("GET", { Authorization: `Bearer ${token}` }), + }, + ); + const { data: sepoliaData } = await sepolia.json(); + setTransactionsForNetwork("sepolia", sepoliaData); + + const linea = await fetch( + `${DASHBOARD_URL(DASHBOARD_PREVIEW_URL, VERCEL_ENV)}/api/faucets/linea/transactions`, + { + ...REQUEST_PARAMS("GET", { Authorization: `Bearer ${token}` }), + }, + ); + const { data: lineaData } = await linea.json(); + setTransactionsForNetwork("linea", lineaData); + }; + + const handleRequest = (network: "linea" | "sepolia") => async () => { + setIsLoading(true); + const address = walletAddress.trim(); + try { + const faucetRawResponse = await fetch( + `${DASHBOARD_URL(DASHBOARD_PREVIEW_URL, VERCEL_ENV)}/api/faucets/${network}?address=${address}`, + { + ...REQUEST_PARAMS("POST", { Authorization: `Bearer ${token}` }), + body: JSON.stringify({ dstAddress: address }), + }, + ); + + const faucetResponse = await faucetRawResponse.json(); + const error = faucetResponse.error; + + if (error) { + if (error.code === "balance_too_low") { + alert.error(); + } else if (error.code === "transaction_count_too_low") { + alert.error(); + } else if (error.name === "COOLDOWN PERIOD") { + alert.info(); + } else { + alert.error(); + } + } else { + mutateTransactionsForNetwork(network, faucetResponse); + alert.success( + , + ); + } + } catch (e) { + alert.error(); + } + setWalletAddress(""); + setIsLoading(false); + }; + + const handleOnInputChange = (value) => { + setWalletAddress(value); + trackInputChangeForSegment({ + eventName: "Wallet address", + userExperience: "B", + timestamp: Date.now(), + }); + }; + + useEffect(() => { + ldClient.waitUntilReady().then(() => { + setIsLineaMaintenance(ldClient.variation(lineaMaintenanceFlag, false)); + setIsSepoliaMaintenance( + ldClient.variation(sepoliaMaintenanceFlag, false), + ); + setFaucetBypassDomain(ldClient.variation(faucetBypassDomainFlag, false)); + setLdReady(true); + }); + const handleChangeLinea = (current) => { + setIsLineaMaintenance(current); + }; + const handleChangeSepolia = (current) => { + setIsSepoliaMaintenance(current); + }; + const handleFaucetBypassDomain = (current) => { + setFaucetBypassDomain(current); + }; + ldClient.on(`change:${lineaMaintenanceFlag}`, handleChangeLinea); + ldClient.on(`change:${sepoliaMaintenanceFlag}`, handleChangeSepolia); + ldClient.on(`change:${faucetBypassDomainFlag}`, handleFaucetBypassDomain); + + trackPageViewForSegment({ + name: "Faucet Page", + path: "developer-tools/faucet", + userExperience: "B", + }); + + return () => { + ldClient.off(`change:${lineaMaintenanceFlag}`, handleChangeLinea); + ldClient.off(`change:${sepoliaMaintenanceFlag}`, handleChangeSepolia); + ldClient.off( + `change:${faucetBypassDomainFlag}`, + handleFaucetBypassDomain, + ); + }; + }, []); + + useEffect(() => { + if (userId && token) { + getTransactions(); + } else { + setTransactions(DEFAULT_TRANSACTIONS_LIST); + } + }, [userId, token]); + + useEffect(() => { + if (metaMaskAccount && !walletAddress) { + setWalletAddress(metaMaskAccount); + } + }, [metaMaskAccount]); + + const tabItemContent = (network: "linea" | "sepolia") => { + return ( + <> +
+ + {transactions && ( + + )} +
+
+ +
+ + ); + }; + + return ( + +
+ MetaMask Faucet +
+
+ + + {isSepoliaMaintenance && } + {ldReady ? tabItemContent("sepolia") : null} + + + {isLineaMaintenance && } + {ldReady ? tabItemContent("linea") : null} + + +
+
+ ); +} diff --git a/src/theme/Root.tsx b/src/theme/Root.tsx index d09eacc118e..5a1ba5affbd 100644 --- a/src/theme/Root.tsx +++ b/src/theme/Root.tsx @@ -21,6 +21,8 @@ import { clearStorage, getUserIdFromJwtToken, saveTokenString, + getTokenString, + getUksTier, } from "@site/src/lib/siwsrp/auth"; import AuthModal, { AUTH_LOGIN_STEP, @@ -42,6 +44,7 @@ interface Project { } interface IMetamaskProviderContext { + token: string; projects: { [key: string]: Project }; setProjects: (arg: { [key: string]: Project }) => void; metaMaskDisconnect: () => Promise; @@ -51,8 +54,8 @@ interface IMetamaskProviderContext { setMetaMaskAccount: (arg: string[] | string) => void; metaMaskProvider: SDKProvider; setMetaMaskProvider: (arg: SDKProvider) => void; + uksTier: string; sdk: MetaMaskSDK; - disconnect: () => Promise; setWalletLinked: (arg: WALLET_LINK_TYPE) => void; walletLinked: WALLET_LINK_TYPE | undefined; setWalletLinkUrl: (arg: string) => void; @@ -62,6 +65,7 @@ interface IMetamaskProviderContext { } export const MetamaskProviderContext = createContext({ + token: undefined, projects: {}, setProjects: () => {}, metaMaskDisconnect: () => new Promise(() => {}), @@ -69,10 +73,10 @@ export const MetamaskProviderContext = createContext({ userId: undefined, metaMaskAccount: undefined, setMetaMaskAccount: () => {}, + uksTier: undefined, metaMaskProvider: undefined, setMetaMaskProvider: () => {}, sdk: undefined, - disconnect: () => new Promise(() => {}), setWalletLinked: () => {}, walletLinked: undefined, setWalletLinkUrl: () => {}, @@ -97,9 +101,11 @@ const sdk = new MetaMaskSDK({ export const LoginProvider = ({ children }) => { const [projects, setProjects] = useState({}); const [userId, setUserId] = useState(""); + const [token, setToken] = useState(undefined); const [openAuthModal, setOpenAuthModal] = useState(false); const [metaMaskProvider, setMetaMaskProvider] = useState(undefined); const [metaMaskAccount, setMetaMaskAccount] = useState(undefined); + const [uksTier, setUksTier] = useState(undefined); const [isInitialized, setIsInitialized] = useState(false); const [step, setStep] = useState(AUTH_LOGIN_STEP.CONNECTING); const [walletLinked, setWalletLinked] = useState< @@ -117,9 +123,11 @@ export const LoginProvider = ({ children }) => { const getStaleDate = async () => { try { setProjects( - JSON.parse(sessionStorage.getItem(AUTH_WALLET_PROJECTS) || "{}") + JSON.parse(sessionStorage.getItem(AUTH_WALLET_PROJECTS) || "{}"), ); setUserId(getUserIdFromJwtToken()); + setToken(getTokenString()); + setUksTier(getUksTier()); const accounts = await sdk.connect(); setMetaMaskAccount(accounts); if (accounts && accounts.length > 0) { @@ -156,6 +164,7 @@ export const LoginProvider = ({ children }) => { if (token) { saveTokenString(token); + setToken(token); const userIdFromjwtToken = getUserIdFromJwtToken(); setUserId(userIdFromjwtToken); @@ -164,12 +173,8 @@ export const LoginProvider = ({ children }) => { const projectsResponse = await fetch( `${DASHBOARD_URL(DASHBOARD_PREVIEW_URL, VERCEL_ENV)}/api/v1/users/${userIdFromjwtToken}/projects`, { - ...REQUEST_PARAMS("GET"), - headers: { - ...REQUEST_PARAMS("GET").headers, - Authorization: `Bearer ${token}`, - }, - } + ...REQUEST_PARAMS("GET", { Authorization: `Bearer ${token}` }), + }, ); const res = await projectsResponse.json(); if (res.error) throw new Error(res.error.message); @@ -179,7 +184,7 @@ export const LoginProvider = ({ children }) => { } = res; sessionStorage.setItem( AUTH_WALLET_PROJECTS, - JSON.stringify(projects) + JSON.stringify(projects), ); setProjects(projects); window.location.replace(`${url.origin}${url.pathname}`); @@ -205,7 +210,9 @@ export const LoginProvider = ({ children }) => { await sdk?.terminate(); setOpenAuthModal(false); setUserId(undefined); + setToken(undefined); setMetaMaskAccount(undefined); + setUksTier(undefined); setProjects({}); setWalletLinked(undefined); setUserAPIKey(""); @@ -213,7 +220,15 @@ export const LoginProvider = ({ children }) => { } catch (err) { console.warn("failed to disconnect..", err); } - }, [sdk, setOpenAuthModal, setUserId, setMetaMaskAccount, setProjects]); + }, [ + sdk, + setOpenAuthModal, + setUserId, + setToken, + setMetaMaskAccount, + setUksTier, + setProjects, + ]); return ( @@ -221,6 +236,7 @@ export const LoginProvider = ({ children }) => { { metaMaskWalletIdConnectHandler, userId, metaMaskProvider, + uksTier, setMetaMaskProvider, sdk, walletLinked, @@ -246,6 +263,8 @@ export const LoginProvider = ({ children }) => { open={openAuthModal} setOpen={setOpenAuthModal} setUser={setUserId} + setToken={setToken} + setUksTier={setUksTier} setStep={setStep} step={step} /> diff --git a/static/img/faucet-linea-hero-dark.png b/static/img/faucet-linea-hero-dark.png new file mode 100644 index 00000000000..f5a249364ea Binary files /dev/null and b/static/img/faucet-linea-hero-dark.png differ diff --git a/static/img/faucet-linea-hero-light.png b/static/img/faucet-linea-hero-light.png new file mode 100644 index 00000000000..130fff14d4e Binary files /dev/null and b/static/img/faucet-linea-hero-light.png differ diff --git a/static/img/faucet-sepolia-hero-dark.png b/static/img/faucet-sepolia-hero-dark.png new file mode 100644 index 00000000000..3327c79d086 Binary files /dev/null and b/static/img/faucet-sepolia-hero-dark.png differ diff --git a/static/img/faucet-sepolia-hero-light.png b/static/img/faucet-sepolia-hero-light.png new file mode 100644 index 00000000000..9afea48723b Binary files /dev/null and b/static/img/faucet-sepolia-hero-light.png differ