From 2fc32a0046d6a43519b29a8bf857651c11124ea0 Mon Sep 17 00:00:00 2001 From: khannanov-nil Date: Tue, 15 Apr 2025 07:28:00 +0000 Subject: [PATCH] [playground] implemented smart account settings fixes fixes --- .../account-connector/ActiveComponent.ts | 6 +- .../assets/PrivateKeyIcon.tsx | 18 ++ .../assets/ResetAccountIcon.tsx | 21 ++ .../account-connector/assets/RpcUrlIcon.tsx | 26 ++ .../account-connector/assets/SettingsIcon.tsx | 17 ++ .../components/AccountContainer.tsx | 14 +- .../components/AccountRegenerationScreen.tsx | 107 +++++++++ .../components/AccountSettingsButton.tsx | 26 ++ .../account-connector/components/BackLink.tsx | 8 +- .../components/InfoAndBalancesScreen.tsx | 208 ++++++++++++++++ .../components/MainScreen.tsx | 76 ++---- .../components/PrivateKeyScreen.tsx | 113 +++++++++ .../components/RpcUrlScreen.tsx | 226 +++++++----------- .../components/SettingsScreen.tsx | 135 +++++++++++ .../src/features/account-connector/init.ts | 11 + .../src/features/account-connector/model.ts | 6 +- explorer_frontend/src/themes.tsx | 13 +- 17 files changed, 827 insertions(+), 204 deletions(-) create mode 100644 explorer_frontend/src/features/account-connector/assets/PrivateKeyIcon.tsx create mode 100644 explorer_frontend/src/features/account-connector/assets/ResetAccountIcon.tsx create mode 100644 explorer_frontend/src/features/account-connector/assets/RpcUrlIcon.tsx create mode 100644 explorer_frontend/src/features/account-connector/assets/SettingsIcon.tsx create mode 100644 explorer_frontend/src/features/account-connector/components/AccountRegenerationScreen.tsx create mode 100644 explorer_frontend/src/features/account-connector/components/AccountSettingsButton.tsx create mode 100644 explorer_frontend/src/features/account-connector/components/InfoAndBalancesScreen.tsx create mode 100644 explorer_frontend/src/features/account-connector/components/PrivateKeyScreen.tsx create mode 100644 explorer_frontend/src/features/account-connector/components/SettingsScreen.tsx diff --git a/explorer_frontend/src/features/account-connector/ActiveComponent.ts b/explorer_frontend/src/features/account-connector/ActiveComponent.ts index 5d9ad961a..e32a8cd0a 100644 --- a/explorer_frontend/src/features/account-connector/ActiveComponent.ts +++ b/explorer_frontend/src/features/account-connector/ActiveComponent.ts @@ -1,6 +1,10 @@ export enum ActiveComponent { - RpcUrl = "RpcUrl", + InfoAndBalances = "InfoAndBalances", Main = "Main", Topup = "Topup", SmartAccountSelector = "SmartAccountSelector", + SettingsScreen = "SettingsScreen", + PrivateKeyScreen = "PrivateKeyScreen", + AccountRegenerationScreen = "AccountRegenerationScreen", + RpcUrlScreen = "RpcUrlScreen", } diff --git a/explorer_frontend/src/features/account-connector/assets/PrivateKeyIcon.tsx b/explorer_frontend/src/features/account-connector/assets/PrivateKeyIcon.tsx new file mode 100644 index 000000000..cf73150b1 --- /dev/null +++ b/explorer_frontend/src/features/account-connector/assets/PrivateKeyIcon.tsx @@ -0,0 +1,18 @@ +export const PrivateKeyIcon = () => { + return ( + + Private key + + + + + ); +}; diff --git a/explorer_frontend/src/features/account-connector/assets/ResetAccountIcon.tsx b/explorer_frontend/src/features/account-connector/assets/ResetAccountIcon.tsx new file mode 100644 index 000000000..aacb111c6 --- /dev/null +++ b/explorer_frontend/src/features/account-connector/assets/ResetAccountIcon.tsx @@ -0,0 +1,21 @@ +export const ResetAccountIcon = () => { + return ( + + Reset account + + + + + + + + ); +}; diff --git a/explorer_frontend/src/features/account-connector/assets/RpcUrlIcon.tsx b/explorer_frontend/src/features/account-connector/assets/RpcUrlIcon.tsx new file mode 100644 index 000000000..be1f3eb62 --- /dev/null +++ b/explorer_frontend/src/features/account-connector/assets/RpcUrlIcon.tsx @@ -0,0 +1,26 @@ +export const RpcUrlIcon = () => { + return ( + + RPC URL + + + + + + ); +}; diff --git a/explorer_frontend/src/features/account-connector/assets/SettingsIcon.tsx b/explorer_frontend/src/features/account-connector/assets/SettingsIcon.tsx new file mode 100644 index 000000000..667933939 --- /dev/null +++ b/explorer_frontend/src/features/account-connector/assets/SettingsIcon.tsx @@ -0,0 +1,17 @@ +export const SettingsIcon = () => { + return ( + + Settings + + + + ); +}; diff --git a/explorer_frontend/src/features/account-connector/components/AccountContainer.tsx b/explorer_frontend/src/features/account-connector/components/AccountContainer.tsx index 59555be57..f6442232b 100644 --- a/explorer_frontend/src/features/account-connector/components/AccountContainer.tsx +++ b/explorer_frontend/src/features/account-connector/components/AccountContainer.tsx @@ -4,22 +4,30 @@ import { expandProperty } from "inline-style-expand-shorthand"; import { useSwipeable } from "react-swipeable"; import { ActiveComponent } from "../ActiveComponent"; import { $activeComponent, setActiveComponent } from "../model"; +import { AccountRegenerationScreen } from "./AccountRegenerationScreen.tsx"; +import { InfoAndBalancesScreen } from "./InfoAndBalancesScreen.tsx"; import { MainScreen } from "./MainScreen"; +import { PrivateKeyScreen } from "./PrivateKeyScreen.tsx"; import { RpcUrlScreen } from "./RpcUrlScreen.tsx"; +import { SettingsScreen } from "./SettingsScreen.tsx"; import { TopUpPanel } from "./TopUpPanel"; const featureMap = new Map(); -featureMap.set(ActiveComponent.RpcUrl, RpcUrlScreen); +featureMap.set(ActiveComponent.InfoAndBalances, InfoAndBalancesScreen); featureMap.set(ActiveComponent.Main, MainScreen); featureMap.set(ActiveComponent.Topup, TopUpPanel); +featureMap.set(ActiveComponent.SettingsScreen, SettingsScreen); +featureMap.set(ActiveComponent.PrivateKeyScreen, PrivateKeyScreen); +featureMap.set(ActiveComponent.AccountRegenerationScreen, AccountRegenerationScreen); +featureMap.set(ActiveComponent.RpcUrlScreen, RpcUrlScreen); const AccountContainer = () => { const activeComponent = useUnit($activeComponent); const Component = activeComponent ? featureMap.get(activeComponent) : null; const [css, theme] = useStyletron(); const handlers = useSwipeable({ - onSwipedLeft: () => setActiveComponent(ActiveComponent.RpcUrl), - onSwipedRight: () => setActiveComponent(ActiveComponent.RpcUrl), + onSwipedLeft: () => setActiveComponent(ActiveComponent.InfoAndBalances), + onSwipedRight: () => setActiveComponent(ActiveComponent.InfoAndBalances), }); return ( diff --git a/explorer_frontend/src/features/account-connector/components/AccountRegenerationScreen.tsx b/explorer_frontend/src/features/account-connector/components/AccountRegenerationScreen.tsx new file mode 100644 index 000000000..86b7a0076 --- /dev/null +++ b/explorer_frontend/src/features/account-connector/components/AccountRegenerationScreen.tsx @@ -0,0 +1,107 @@ +import { COLORS, LabelLarge, LabelSmall } from "@nilfoundation/ui-kit"; +import { useStyletron } from "baseui"; +import { useUnit } from "effector-react"; +import lottie from "lottie-web/build/player/lottie_light"; +import { useEffect, useRef, useState } from "react"; +import { ActiveComponent } from "../ActiveComponent"; +import animationData from "../assets/wallet-creation.json"; +import { + $initializingSmartAccountError, + $initializingSmartAccountState, + createSmartAccountFx, + setActiveComponent, +} from "../model"; +import { BackLink } from "./BackLink"; + +export const AccountRegenerationScreen = () => { + const [css, theme] = useStyletron(); + const [error, setError] = useState(""); + const [isDisabled, setIsDisabled] = useState(false); + + const animationContainerRef = useRef(null); + + const [ + initializingSmartAccountState, + initializingSmartAccountError, + isPendingSmartAccountCreation, + ] = useUnit([ + $initializingSmartAccountState, + $initializingSmartAccountError, + createSmartAccountFx.pending, + ]); + + // biome-ignore lint/correctness/useExhaustiveDependencies: + useEffect(() => { + if (animationContainerRef.current) { + const animationInstance = lottie.loadAnimation({ + container: animationContainerRef.current as Element, + renderer: "svg", + loop: true, + autoplay: true, + animationData, + }); + + return () => animationInstance.destroy(); + } + }, [isPendingSmartAccountCreation]); + + useEffect(() => { + if (initializingSmartAccountError) { + setIsDisabled(false); + setError(initializingSmartAccountError); + } + }, [initializingSmartAccountError]); + + return ( +
+ {/* Optional fixed header */} + {!isPendingSmartAccountCreation && ( +
+ { + setActiveComponent(ActiveComponent.SettingsScreen); + }} + /> +
+ )} + + {/* Main Content Container */} +
+ {isPendingSmartAccountCreation ? ( + <> +
+ + {initializingSmartAccountState} + + {error} + + ) : ( + <> + + A new smart account has been created! + + {error} + + )} +
+
+ ); +}; diff --git a/explorer_frontend/src/features/account-connector/components/AccountSettingsButton.tsx b/explorer_frontend/src/features/account-connector/components/AccountSettingsButton.tsx new file mode 100644 index 000000000..79da9c9fe --- /dev/null +++ b/explorer_frontend/src/features/account-connector/components/AccountSettingsButton.tsx @@ -0,0 +1,26 @@ +import { BUTTON_KIND, ButtonIcon } from "@nilfoundation/ui-kit"; +import { useStyletron } from "baseui"; +import { ActiveComponent } from "../ActiveComponent"; +import { SettingsIcon } from "../assets/SettingsIcon"; +import { setActiveComponent } from "../model"; + +export const AccountSettingsButton = () => { + const [css, theme] = useStyletron(); + + return ( + } + kind={BUTTON_KIND.secondary} + onClick={() => setActiveComponent(ActiveComponent.SettingsScreen)} + /> + ); +}; diff --git a/explorer_frontend/src/features/account-connector/components/BackLink.tsx b/explorer_frontend/src/features/account-connector/components/BackLink.tsx index 8e5a001fe..aed0c8eb4 100644 --- a/explorer_frontend/src/features/account-connector/components/BackLink.tsx +++ b/explorer_frontend/src/features/account-connector/components/BackLink.tsx @@ -6,8 +6,8 @@ import { COLORS, LabelMedium, } from "@nilfoundation/ui-kit"; +import { useStyletron } from "baseui"; import type { FC } from "react"; -import { useStyletron } from "styletron-react"; type BackLinkProps = { title: string; @@ -16,7 +16,7 @@ type BackLinkProps = { }; const BackLink: FC = ({ title, goBackCb, disabled }) => { - const [css] = useStyletron(); + const [css, theme] = useStyletron(); return (
= ({ title, goBackCb, disabled }) => { style: { paddingLeft: 0, paddingRight: 0, + backgroundColor: theme.colors.backLinkBackgroundColor, + ":hover": { + backgroundColor: theme.colors.backLinkBackgroundHoverColor, + }, }, }, }} diff --git a/explorer_frontend/src/features/account-connector/components/InfoAndBalancesScreen.tsx b/explorer_frontend/src/features/account-connector/components/InfoAndBalancesScreen.tsx new file mode 100644 index 000000000..4e65344ea --- /dev/null +++ b/explorer_frontend/src/features/account-connector/components/InfoAndBalancesScreen.tsx @@ -0,0 +1,208 @@ +import { FormControl, HeadingLarge, Input, type InputProps } from "@nilfoundation/ui-kit"; +import { BUTTON_KIND, Button, COLORS, LabelLarge, LabelSmall } from "@nilfoundation/ui-kit"; +import { useStyletron } from "baseui"; +import type { ButtonOverrides } from "baseui/button"; +import { useUnit } from "effector-react"; +import lottie from "lottie-web/build/player/lottie_light"; +import { useEffect, useRef, useState } from "react"; +import { getRuntimeConfigOrThrow } from "../../runtime-config/index.ts"; +import { ActiveComponent } from "../ActiveComponent.ts"; +import asteriskIcon from "../assets/asterisk.svg"; +import animationData from "../assets/wallet-creation.json"; +import { + $balance, + $balanceToken, + $initializingSmartAccountError, + $initializingSmartAccountState, + $rpcUrl, + $smartAccount, + createSmartAccountFx, + initilizeSmartAccount, + setActiveComponent, +} from "../model.ts"; + +const btnOverrides: ButtonOverrides = { + Root: { + style: ({ $disabled }) => ({ + backgroundColor: $disabled ? `${COLORS.gray400}!important` : "", + width: "100%", + }), + }, +}; + +export const InfoAndBalancesScreen = () => { + const [css] = useStyletron(); + const [error, setError] = useState(""); + const [isDisabled, setIsDisabled] = useState(false); + const { RPC_TELEGRAM_BOT } = getRuntimeConfigOrThrow(); + + // Get initialization states + const [ + smartAccount, + balance, + balanceToken, + initializingSmartAccountState, + initializingSmartAccountError, + isPendingSmartAccountCreation, + rpcUrl, + ] = useUnit([ + $smartAccount, + $balance, + $balanceToken, + $initializingSmartAccountState, + $initializingSmartAccountError, + createSmartAccountFx.pending, + $rpcUrl, + ]); + const [inputValue, setInputValue] = useState(rpcUrl); + const animationContainerRef = useRef(null); + + // biome-ignore lint/correctness/useExhaustiveDependencies: + useEffect(() => { + if (animationContainerRef.current) { + const animationInstance = lottie.loadAnimation({ + container: animationContainerRef.current as Element, + renderer: "svg", + loop: true, + autoplay: true, + animationData, + }); + + return () => animationInstance.destroy(); + } + }, [isPendingSmartAccountCreation]); + + const handleConnect = () => { + const trimmedInputValue = inputValue.trim(); + setIsDisabled(true); + initilizeSmartAccount(trimmedInputValue); + }; + + const handleGetRpcUrl = () => { + if (RPC_TELEGRAM_BOT) { + window.open(RPC_TELEGRAM_BOT, "_blank"); + } else { + console.error("RPC_TELEGRAM_BOT runtime variable is not set."); + } + }; + + const handleInputChange: InputProps["onChange"] = (e) => { + setError(""); + setInputValue(e.target.value); + }; + + useEffect(() => { + if (smartAccount !== null && balance !== null && balanceToken !== null) { + setActiveComponent(ActiveComponent.Main); + } + }, [smartAccount, balance, balanceToken]); + + useEffect(() => { + if (initializingSmartAccountError) { + setIsDisabled(false); + setError(initializingSmartAccountError); + } + }, [initializingSmartAccountError]); + + return ( +
+ {!isPendingSmartAccountCreation ? ( +
+ Asterisk Icon + Enter the RPC URL to connect the wallet +
+ ) : ( +
+
+ + {initializingSmartAccountState} + +
+ )} + + + + + + {error} + +
+ + +
+
+ ); +}; diff --git a/explorer_frontend/src/features/account-connector/components/MainScreen.tsx b/explorer_frontend/src/features/account-connector/components/MainScreen.tsx index be341a1e8..123148776 100644 --- a/explorer_frontend/src/features/account-connector/components/MainScreen.tsx +++ b/explorer_frontend/src/features/account-connector/components/MainScreen.tsx @@ -3,7 +3,6 @@ import { Button, COLORS, CopyButton, - Input, LabelLarge, LabelMedium, LabelSmall, @@ -20,7 +19,6 @@ import { formatEther } from "viem"; import { $rpcIsHealthy } from "../../healthcheck/model"; import { OverflowEllipsis, useMobile } from "../../shared"; import { ActiveComponent } from "../ActiveComponent"; -import CheckmarkIcon from "../assets/checkmark.svg"; import Linkicon from "../assets/link.svg"; import { $balance, @@ -33,6 +31,7 @@ import { setActiveComponent, topUpSmartAccountBalanceFx, } from "../model"; +import { AccountSettingsButton } from "./AccountSettingsButton"; import { Token } from "./Token"; import { styles } from "./styles"; @@ -114,13 +113,23 @@ const MainScreen = () => { display: "flex", flexDirection: "column", position: "sticky", - alignItems: "center", - gap: "24px", + alignItems: "start", + gap: "12px", top: 0, backgroundColor: theme.colors.backgroundSecondary, })} > - Smart Account +
+ Smart Account + +
{address !== null && (
{
)} - {rpcUrl !== null && ( -
- {/* Read-Only Input */} - - {/* Copy Button */} - -
- )}
    { + const [css, theme] = useStyletron(); + const [privateKey] = useUnit([$privateKey, $smartAccount]); + const [copied, setCopied] = useState(false); + + const handleCopy = async () => { + if (privateKey && typeof privateKey === "string" && privateKey.startsWith("0x")) { + await navigator.clipboard.writeText(privateKey); + + setCopied(true); + + // Reset text after 2 seconds + setTimeout(() => setCopied(false), 2000); + } + }; + + return ( +
    + { + setActiveComponent(ActiveComponent.SettingsScreen); + }} + /> + +
    + Private key +
    + {/* Read-Only Input */} + + {/* Copy Button */} + +
    +
    +
    + ); +}; diff --git a/explorer_frontend/src/features/account-connector/components/RpcUrlScreen.tsx b/explorer_frontend/src/features/account-connector/components/RpcUrlScreen.tsx index 7b2f6bc85..2c4172e61 100644 --- a/explorer_frontend/src/features/account-connector/components/RpcUrlScreen.tsx +++ b/explorer_frontend/src/features/account-connector/components/RpcUrlScreen.tsx @@ -1,25 +1,13 @@ -import { FormControl, HeadingLarge, Input, type InputProps } from "@nilfoundation/ui-kit"; -import { BUTTON_KIND, Button, COLORS, LabelLarge, LabelSmall } from "@nilfoundation/ui-kit"; +import { BUTTON_KIND, Button, COLORS, Input, LabelMedium } from "@nilfoundation/ui-kit"; import { useStyletron } from "baseui"; import type { ButtonOverrides } from "baseui/button"; import { useUnit } from "effector-react"; -import lottie from "lottie-web/build/player/lottie_light"; -import { useEffect, useRef, useState } from "react"; -import { getRuntimeConfigOrThrow } from "../../runtime-config"; -import { ActiveComponent } from "../ActiveComponent.ts"; -import asteriskIcon from "../assets/asterisk.svg"; -import animationData from "../assets/wallet-creation.json"; -import { - $balance, - $balanceToken, - $initializingSmartAccountError, - $initializingSmartAccountState, - $rpcUrl, - $smartAccount, - createSmartAccountFx, - initilizeSmartAccount, - setActiveComponent, -} from "../model"; +import { useState } from "react"; +import { getRuntimeConfigOrThrow } from "../../runtime-config/getRuntimeConfigOrThrow"; +import { ActiveComponent } from "../ActiveComponent"; +import CheckmarkIcon from "../assets/checkmark.svg"; +import { $rpcUrl, setActiveComponent } from "../model"; +import { BackLink } from "./BackLink"; const btnOverrides: ButtonOverrides = { Root: { @@ -31,51 +19,24 @@ const btnOverrides: ButtonOverrides = { }; export const RpcUrlScreen = () => { - const [css] = useStyletron(); - const [error, setError] = useState(""); - const [isDisabled, setIsDisabled] = useState(false); + const [css, theme] = useStyletron(); + + const [isDisabled] = useState(false); const { RPC_TELEGRAM_BOT } = getRuntimeConfigOrThrow(); - // Get initialization states - const [ - smartAccount, - balance, - balanceToken, - initializingSmartAccountState, - initializingSmartAccountError, - isPendingSmartAccountCreation, - rpcUrl, - ] = useUnit([ - $smartAccount, - $balance, - $balanceToken, - $initializingSmartAccountState, - $initializingSmartAccountError, - createSmartAccountFx.pending, - $rpcUrl, - ]); - const [inputValue, setInputValue] = useState(rpcUrl); - const animationContainerRef = useRef(null); + const [rpcUrl] = useUnit([$rpcUrl]); - // biome-ignore lint/correctness/useExhaustiveDependencies: - useEffect(() => { - if (animationContainerRef.current) { - const animationInstance = lottie.loadAnimation({ - container: animationContainerRef.current as Element, - renderer: "svg", - loop: true, - autoplay: true, - animationData, - }); + const [copied, setCopied] = useState(false); - return () => animationInstance.destroy(); - } - }, [isPendingSmartAccountCreation]); + const handleCopy = async () => { + if (rpcUrl && typeof rpcUrl === "string") { + await navigator.clipboard.writeText(rpcUrl); + + setCopied(true); - const handleConnect = () => { - const trimmedInputValue = inputValue.trim(); - setIsDisabled(true); - initilizeSmartAccount(trimmedInputValue); + // Reset text after 2 seconds + setTimeout(() => setCopied(false), 2000); + } }; const handleGetRpcUrl = () => { @@ -86,89 +47,98 @@ export const RpcUrlScreen = () => { } }; - const handleInputChange: InputProps["onChange"] = (e) => { - setError(""); - setInputValue(e.target.value); - }; - - useEffect(() => { - if (smartAccount !== null && balance !== null && balanceToken !== null) { - setActiveComponent(ActiveComponent.Main); - } - }, [smartAccount, balance, balanceToken]); - - useEffect(() => { - if (initializingSmartAccountError) { - setIsDisabled(false); - setError(initializingSmartAccountError); - } - }, [initializingSmartAccountError]); - return (
    - {!isPendingSmartAccountCreation ? ( -
    - Asterisk Icon - Enter the RPC URL to connect the wallet -
    - ) : ( + { + setActiveComponent(ActiveComponent.SettingsScreen); + }} + /> + {rpcUrl !== null && (
    + RPC URL
    - - {initializingSmartAccountState} - + className={css({ + display: "flex", + flexDirection: "row", + gap: "12px", + justifyContent: "space-around", + })} + > + {/* Read-Only Input */} + + {/* Copy Button */} + +
    )} - - - - - {error} - +
    { gap: "8px", }} > -