diff --git a/apps/namadillo/src/App/AppRoutes.tsx b/apps/namadillo/src/App/AppRoutes.tsx index 6293be1d5..3b3779659 100644 --- a/apps/namadillo/src/App/AppRoutes.tsx +++ b/apps/namadillo/src/App/AppRoutes.tsx @@ -1,48 +1,68 @@ import { Router } from "@remix-run/router"; import { Route, + Routes, createBrowserRouter, createRoutesFromElements, + useLocation, } from "react-router-dom"; import { AccountOverview } from "./AccountOverview"; import { App } from "./App"; import { AnimatedTransition } from "./Common/AnimatedTransition"; import { Governance } from "./Governance"; +import { SettingsPanel } from "./Settings/SettingsPanel"; import { Staking } from "./Staking"; import GovernanceRoutes from "./Governance/routes"; +import SettingsRoutes from "./Settings/routes"; import StakingRoutes from "./Staking/routes"; -export const getRouter = (): Router => { - return createBrowserRouter( - createRoutesFromElements( - }> - - - - } - /> - - - - } - /> +export const MainRoutes = (): JSX.Element => { + const location = useLocation(); + const state = location.state as { backgroundLocation?: Location }; + + return ( + <> + + }> + + + + } + /> + + + + } + /> + + + + } + /> + + + - - - } + path={`${SettingsRoutes.index()}/*`} + element={} /> - - ) + + + ); +}; + +export const getRouter = (): Router => { + return createBrowserRouter( + createRoutesFromElements(} />) ); }; diff --git a/apps/namadillo/src/App/Common/TopNavigation.tsx b/apps/namadillo/src/App/Common/TopNavigation.tsx index abc4481f7..efd3d8a65 100644 --- a/apps/namadillo/src/App/Common/TopNavigation.tsx +++ b/apps/namadillo/src/App/Common/TopNavigation.tsx @@ -2,10 +2,11 @@ import { ToggleButton } from "@namada/components"; import { Chain } from "@namada/types"; import { ActiveAccount } from "App/Common/ActiveAccount"; import { ConnectExtensionButton } from "App/Common/ConnectExtensionButton"; +import SettingsRoutes from "App/Settings/routes"; import { ConnectStatus, useExtensionConnect } from "hooks/useExtensionConnect"; import { useAtom } from "jotai"; import { IoSettingsOutline } from "react-icons/io5"; -import { Link } from "react-router-dom"; +import { useLocation, useNavigate } from "react-router-dom"; import { hideBalancesAtom } from "slices/settings"; type Props = { @@ -15,6 +16,8 @@ type Props = { export const TopNavigation = ({ chain }: Props): JSX.Element => { const { connectionStatus } = useExtensionConnect(chain); const [hideBalances, setHideBalances] = useAtom(hideBalancesAtom); + const location = useLocation(); + const navigate = useNavigate(); return ( <> @@ -32,9 +35,16 @@ export const TopNavigation = ({ chain }: Props): JSX.Element => { onChange={() => setHideBalances(!hideBalances)} containerProps={{ className: "hidden text-white md:flex" }} /> - + )} diff --git a/apps/namadillo/src/App/Settings/Advanced.tsx b/apps/namadillo/src/App/Settings/Advanced.tsx new file mode 100644 index 000000000..83795cd66 --- /dev/null +++ b/apps/namadillo/src/App/Settings/Advanced.tsx @@ -0,0 +1,39 @@ +import { ActionButton, Input, Stack } from "@namada/components"; +import SettingsRoute from "App/Settings/routes"; +import { useAtom } from "jotai"; +import { useState } from "react"; +import { useLocation, useNavigate } from "react-router-dom"; +import { rpcUrlAtom } from "slices/settings"; + +export const Advanced = (): JSX.Element => { + const navigate = useNavigate(); + const location = useLocation(); + const [currentRpc, setCurrentRpc] = useAtom(rpcUrlAtom); + const [rpc, setRpc] = useState(currentRpc); + + const onSubmit = (e: React.FormEvent): void => { + e.preventDefault(); + setCurrentRpc(rpc); + navigate(SettingsRoute.index(), { replace: true, state: location.state }); + }; + + return ( +
+ + setRpc(e.currentTarget.value)} + required + /> + + + Confirm + +
+ ); +}; diff --git a/apps/namadillo/src/App/Settings/CurrencySelector.tsx b/apps/namadillo/src/App/Settings/CurrencySelector.tsx new file mode 100644 index 000000000..cbc173b8f --- /dev/null +++ b/apps/namadillo/src/App/Settings/CurrencySelector.tsx @@ -0,0 +1,21 @@ +import { Stack } from "@namada/components"; +import { FiatCurrencyList } from "@namada/utils"; +import { useAtom } from "jotai"; +import { selectedCurrencyAtom } from "slices/settings"; +import { CurrencySelectorEntry } from "./CurrencySelectorEntry"; + +export const CurrencySelector = (): JSX.Element => { + const [selectedCurrency, setSelectedCurrency] = useAtom(selectedCurrencyAtom); + return ( + + {FiatCurrencyList.map((currency) => ( + setSelectedCurrency(currency.id)} + /> + ))} + + ); +}; diff --git a/apps/namadillo/src/App/Settings/CurrencySelectorEntry.tsx b/apps/namadillo/src/App/Settings/CurrencySelectorEntry.tsx new file mode 100644 index 000000000..1fb463a44 --- /dev/null +++ b/apps/namadillo/src/App/Settings/CurrencySelectorEntry.tsx @@ -0,0 +1,35 @@ +import { CurrencyInfoListItem } from "@namada/utils"; +import clsx from "clsx"; +import { twMerge } from "tailwind-merge"; + +type CurrencySelectorEntryType = { + currency: CurrencyInfoListItem; + selected: boolean; + onClick: () => void; +}; + +export const CurrencySelectorEntry = ({ + currency, + selected, + onClick, +}: CurrencySelectorEntryType): JSX.Element => { + return ( +
  • + +
  • + ); +}; diff --git a/apps/namadillo/src/App/Settings/SettingsMain.tsx b/apps/namadillo/src/App/Settings/SettingsMain.tsx new file mode 100644 index 000000000..56719a8a3 --- /dev/null +++ b/apps/namadillo/src/App/Settings/SettingsMain.tsx @@ -0,0 +1,56 @@ +import { Alert, Stack, ToggleButton } from "@namada/components"; +import { useAtom } from "jotai"; +import { IoWarning } from "react-icons/io5"; +import { signArbitraryEnabledAtom } from "slices/settings"; +import { SettingsPanelMenuItem } from "./SettingsPanelMenuItem"; +import SettingsRoutes from "./routes"; + +export const SettingsMain = (): JSX.Element => { + const [signArbitraryEnabled, setSignArbitraryEnabled] = useAtom( + signArbitraryEnabledAtom + ); + + return ( +
    +
      + + +
    + + +

    Sign Arbitrary Message

    +

    + Enabling this setting puts you at risk of phishing attacks. Please + check what you are signing very carefully when using this feature. We + recommend keeping this setting turned off unless absolutely necessary +

    + {signArbitraryEnabled && ( + +
    + + + + You are at risk of phishing attacks. Please review carefully what + you sign +
    +
    + )} + setSignArbitraryEnabled(!signArbitraryEnabled)} + /> +
    +
    + ); +}; diff --git a/apps/namadillo/src/App/Settings/SettingsPanel.tsx b/apps/namadillo/src/App/Settings/SettingsPanel.tsx new file mode 100644 index 000000000..747c5c309 --- /dev/null +++ b/apps/namadillo/src/App/Settings/SettingsPanel.tsx @@ -0,0 +1,69 @@ +import { Modal } from "@namada/components"; +import clsx from "clsx"; +import { FaChevronLeft } from "react-icons/fa6"; +import { IoClose } from "react-icons/io5"; +import { Route, Routes, useLocation, useNavigate } from "react-router-dom"; +import { Advanced } from "./Advanced"; +import { CurrencySelector } from "./CurrencySelector"; +import { SettingsMain } from "./SettingsMain"; +import SettingsRoutes from "./routes"; + +export const SettingsPanel = (): JSX.Element => { + const location = useLocation(); + const navigate = useNavigate(); + + const onClose = (): void => { + if (location.state?.backgroundLocation) { + navigate((location.state.backgroundLocation as Location).pathname, { + replace: true, + }); + } else { + navigate("/", { replace: true }); + } + }; + + const onClickBack = (): void => { + navigate(SettingsRoutes.index(), { state: location.state }); + }; + + return ( + +
    +
    +

    Settings

    + + {location.pathname !== SettingsRoutes.index() && ( + + )} +
    + + } /> + } + /> + } /> + +
    +
    + ); +}; diff --git a/apps/namadillo/src/App/Settings/SettingsPanelMenuItem.tsx b/apps/namadillo/src/App/Settings/SettingsPanelMenuItem.tsx new file mode 100644 index 000000000..909d057ac --- /dev/null +++ b/apps/namadillo/src/App/Settings/SettingsPanelMenuItem.tsx @@ -0,0 +1,31 @@ +import clsx from "clsx"; +import { FaChevronRight } from "react-icons/fa6"; +import { Link, useLocation } from "react-router-dom"; + +type SettingsPanelMenuItemProps = { + text: string; + url: string; +}; + +export const SettingsPanelMenuItem = ({ + url, + text, +}: SettingsPanelMenuItemProps): JSX.Element => { + const location = useLocation(); + return ( +
  • + + {text} + + +
  • + ); +}; diff --git a/apps/namadillo/src/App/Settings/routes.ts b/apps/namadillo/src/App/Settings/routes.ts new file mode 100644 index 000000000..003d2448b --- /dev/null +++ b/apps/namadillo/src/App/Settings/routes.ts @@ -0,0 +1,11 @@ +import { RouteOutput, createRouteOutput } from "utils/routes"; + +export const index = (): string => "/settings"; + +export const routeOutput = createRouteOutput(index); + +export const currencySelection = (): RouteOutput => routeOutput("/currency"); + +export const advanced = (): RouteOutput => routeOutput("/advanced"); + +export default { index, currencySelection, advanced }; diff --git a/apps/namadillo/src/slices/settings.ts b/apps/namadillo/src/slices/settings.ts index a12f8e646..4bf514627 100644 --- a/apps/namadillo/src/slices/settings.ts +++ b/apps/namadillo/src/slices/settings.ts @@ -8,6 +8,7 @@ type SettingsStorage = { hideBalances: boolean; rpcUrl: string; chainId: string; + signArbitraryEnabled: boolean; }; export const namadaExtensionConnectedAtom = atom(false); @@ -19,6 +20,7 @@ export const namadilloSettingsAtom = atomWithStorage( hideBalances: false, rpcUrl: process.env.NAMADA_INTERFACE_NAMADA_URL || "", chainId: process.env.NAMADA_INTERFACE_NAMADA_CHAIN_ID || "", + signArbitraryEnabled: false, } ); @@ -49,6 +51,11 @@ export const chainIdAtom = atom( changeSettings("chainId") ); +export const signArbitraryEnabledAtom = atom( + (get) => get(namadilloSettingsAtom).signArbitraryEnabled, + changeSettings("signArbitraryEnabled") +); + export const connectedChainsAtom = atom([]); export const addConnectedChainAtom = atom(null, (get, set, chain: ChainKey) => { const connectedChains = get(connectedChainsAtom); diff --git a/packages/components/src/ToggleButton.tsx b/packages/components/src/ToggleButton.tsx index 416fd1406..0fd61fd5f 100644 --- a/packages/components/src/ToggleButton.tsx +++ b/packages/components/src/ToggleButton.tsx @@ -1,13 +1,6 @@ import clsx from "clsx"; import { twMerge } from "tailwind-merge"; -import { tv } from "tailwind-variants"; - -type Props = { - checked: boolean; - onChange: () => void; - label: string; - containerProps?: React.ComponentPropsWithoutRef<"label">; -}; +import { VariantProps, tv } from "tailwind-variants"; const toggleButtonClassList = tv({ slots: { @@ -17,32 +10,63 @@ const toggleButtonClassList = tv({ ), checkbox: clsx("invisible absolute pointer-events-none"), toggleContainer: clsx( - "relative rounded-3xl bg-rblack p-1 h-5 w-10 cursor-pointer", + "relative rounded-3xl p-1 h-5 w-10 cursor-pointer", "transition-all duration-100 ease-out-quad", "[&~span]:top-px" ), toggleIndicator: clsx( - "absolute left-0.5 top-0.5 h-[calc(100%-4px)] bg-yellow", + "absolute left-0.5 top-0.5 h-[calc(100%-4px)]", "aspect-square rounded-full transition-all duration-200 ease-out-quad" ), }, variants: { + color: { + yellow: { + toggleContainer: "bg-rblack", + toggleIndicator: "bg-yellow", + }, + white: { + toggleContainer: "bg-neutral-700", + toggleIndicator: "bg-white", + }, + }, + activeColor: { + yellow: {}, + }, checked: { true: { toggleIndicator: "translate-x-5", }, }, }, + compoundVariants: [ + { + checked: true, + activeColor: "yellow", + class: { + toggleIndicator: "bg-rblack", + toggleContainer: "bg-yellow", + }, + }, + ], }); +type ToggleButtonProps = { + onChange: () => void; + label: string; + containerProps?: React.ComponentPropsWithoutRef<"label">; +} & VariantProps; + export const ToggleButton = ({ checked, onChange, label, + color = "yellow", + activeColor, containerProps = {}, -}: Props): JSX.Element => { +}: ToggleButtonProps): JSX.Element => { const { container, checkbox, toggleContainer, toggleIndicator } = - toggleButtonClassList({ checked }); + toggleButtonClassList({ checked, color, activeColor }); const { className: containerClassName, ...containerPropsRest } = containerProps;