diff --git a/components/account/AccountButton.tsx b/components/account/AccountButton.tsx index 00a9546c2..14d0a6cbd 100644 --- a/components/account/AccountButton.tsx +++ b/components/account/AccountButton.tsx @@ -31,6 +31,7 @@ import { DesktopOnboardingModal, MobileOnboardingModal, } from "./OnboardingModal"; +import SettingsModal from "components/settings/SettingsModal"; import CopyIcon from "../ui/CopyIcon"; const BalanceRow = ({ @@ -99,6 +100,7 @@ const AccountButton: FC<{ const [hovering, setHovering] = useState(false); const [showOnboarding, setShowOnboarding] = useState(false); const [showGetZtgModal, setShowGetZtgModal] = useState(false); + const [showSettingsModal, setShowSettingsModal] = useState(false); const { data: activeBalance } = useZtgBalance(activeAccount?.address); const { data: polkadotBalance } = useBalance(activeAccount?.address, { @@ -381,16 +383,17 @@ const AccountButton: FC<{ {({ active }) => ( - -
- - -
- +
setShowSettingsModal(true)} + > + + +
)}
@@ -417,6 +420,12 @@ const AccountButton: FC<{ )} + { + setShowSettingsModal(false); + }} + /> {isMobileDevice ? ( setShowOnboarding(false)}> diff --git a/components/settings/AccountSettingsForm.tsx b/components/settings/AccountSettingsForm.tsx new file mode 100644 index 000000000..ace30bf1d --- /dev/null +++ b/components/settings/AccountSettingsForm.tsx @@ -0,0 +1,173 @@ +import React from "react"; +import { useForm } from "react-hook-form"; +import { isRpcSdk } from "@zeitgeistpm/sdk-next"; +import FormTransactionButton from "components/ui/FormTransactionButton"; +import { identityRootKey } from "lib/hooks/queries/useIdentity"; +import { useExtrinsic } from "lib/hooks/useExtrinsic"; +import { useSdkv2 } from "lib/hooks/useSdkv2"; +import { queryClient } from "lib/query-client"; +import { useNotifications } from "lib/state/notifications"; +import { useWallet } from "lib/state/wallet"; +import { UserIdentity } from "lib/types/user-identity"; +import { useChainConstants } from "lib/hooks/queries/useChainConstants"; + +export type AcccountSettingsFormProps = { + identity: UserIdentity; +}; + +const AcccountSettingsForm: React.FC = ({ + identity, +}) => { + const { + register, + reset, + formState: { isValid, errors, isDirty }, + watch, + } = useForm<{ + displayName: string; + discord: string; + twitter: string; + }>({ + defaultValues: { + displayName: identity.displayName ?? "", + discord: identity.discord ?? "", + twitter: identity.twitter ?? "", + }, + mode: "all", + reValidateMode: "onChange", + }); + + const wallet = useWallet(); + const address = wallet.activeAccount?.address; + const [sdk, id] = useSdkv2(); + + const notificationStore = useNotifications(); + + const discordHandle = watch("discord"); + const twitterHandle = watch("twitter"); + const displayName = watch("displayName"); + + const { data: constants } = useChainConstants(); + + const indetityCost = + constants?.identity?.basicDeposit ?? + 0 + (constants?.identity?.fieldDeposit ?? 0); + + const isCleared = + !identity?.displayName && !identity?.discord && !identity?.twitter; + + const { send: updateIdentity, isLoading: isUpdating } = useExtrinsic( + () => { + if (isRpcSdk(sdk)) { + return sdk.api.tx.identity.setIdentity({ + additional: [[{ Raw: "discord" }, { Raw: discordHandle }]], + display: { Raw: displayName }, + twitter: { Raw: twitterHandle }, + }); + } + }, + { + onSuccess: () => { + queryClient.invalidateQueries([id, identityRootKey, address]); + notificationStore.pushNotification("Successfully set Identity", { + type: "Success", + }); + reset({ displayName, discord: discordHandle, twitter: twitterHandle }); + }, + }, + ); + + const { send: clearIdentity, isLoading: isClearing } = useExtrinsic( + () => { + if (isRpcSdk(sdk)) { + return sdk.api.tx.identity.clearIdentity(); + } + }, + { + onSuccess: () => { + queryClient.invalidateQueries([id, identityRootKey, address]); + notificationStore.pushNotification("Successfully cleared Identity", { + type: "Success", + }); + reset({ + displayName: "", + discord: "", + twitter: "", + }); + }, + }, + ); + return ( +
{ + e.preventDefault(); + if (!isValid) return; + updateIdentity(); + }} + > + + + + + + + + + + + +
+ Setting an identity requires a deposit of up to {indetityCost}{" "} + {constants?.tokenSymbol}. This deposit can be retrieved by clearing your + identity. +
+ + + Set Identity + + +
+ ); +}; + +export default AcccountSettingsForm; diff --git a/components/settings/OtherSettingsForm.tsx b/components/settings/OtherSettingsForm.tsx new file mode 100644 index 000000000..ec318b111 --- /dev/null +++ b/components/settings/OtherSettingsForm.tsx @@ -0,0 +1,120 @@ +import React, { useEffect } from "react"; +import { Controller, useForm } from "react-hook-form"; +import AddressInput, { AddressOption } from "components/ui/AddressInput"; +import FormTransactionButton from "components/ui/FormTransactionButton"; +import { isRpcSdk } from "@zeitgeistpm/sdk-next"; +import { useSdkv2 } from "lib/hooks/useSdkv2"; +import { useWallet } from "lib/state/wallet"; +import { isValidPolkadotAddress } from "lib/util"; + +export type OtherSettingsFormProps = {}; + +const OtherSettingsForm: React.FC = ({}) => { + const [sdk] = useSdkv2(); + const wallet = useWallet(); + + const proxyConfig = wallet.getProxyFor(wallet.activeAccount?.address); + + const { + register, + control, + trigger, + handleSubmit, + reset, + formState: { isValid, errors, isDirty }, + watch, + } = useForm<{ + proxyAddress: AddressOption | null; + enableProxy: boolean; + }>({ + defaultValues: { + proxyAddress: proxyConfig + ? { label: proxyConfig.address, value: proxyConfig.address } + : null, + enableProxy: proxyConfig?.enabled ?? false, + }, + mode: "all", + reValidateMode: "onChange", + }); + + const proxyEnabled = watch("enableProxy"); + + useEffect(() => { + trigger("proxyAddress"); + }, [proxyEnabled]); + + return ( +
{ + if (!wallet.activeAccount?.address) { + return; + } + wallet.setProxyFor(wallet.activeAccount.address, { + address: data.proxyAddress?.value ?? "", + enabled: data.enableProxy, + }); + reset(data); + })} + > + +
+ + +
+ { + if (!enableProxy) { + return true; + } + if (enableProxy && !v?.value) { + return "Enter an address to proxy"; + } + if (v && !isValidPolkadotAddress(v.value)) { + return "Invalid address"; + } + if (v && isRpcSdk(sdk) && v.value) { + const proxies = await sdk.api.query.proxy.proxies(v.value); + const proxyMatch = proxies?.[0]?.find((p) => { + return p.delegate.toString() === wallet.activeAccount?.address; + }); + if (!Boolean(proxyMatch)) { + return "You are not a proxy for this account."; + } + } + }, + deps: ["enableProxy"], + }} + render={({ field: { value, onChange } }) => { + return ( + + ); + }} + control={control} + /> + + Save + + + ); +}; + +export default OtherSettingsForm; diff --git a/components/settings/SettingsModal.tsx b/components/settings/SettingsModal.tsx new file mode 100644 index 000000000..1acf202f5 --- /dev/null +++ b/components/settings/SettingsModal.tsx @@ -0,0 +1,84 @@ +import React, { Fragment, useState } from "react"; +import { Dialog, Tab } from "@headlessui/react"; +import Modal from "components/ui/Modal"; +import AcccountSettingsForm from "./AccountSettingsForm"; +import OtherSettingsForm from "./OtherSettingsForm"; +import { useIdentity } from "lib/hooks/queries/useIdentity"; +import { useWallet } from "lib/state/wallet"; +import { AddressOption } from "components/ui/AddressInput"; + +export type SettingsModalProps = { + open: boolean; + onClose: () => void; +}; + +enum TabSelection { + Account, + Other, +} + +const SettingsModal: React.FC = ({ open, onClose }) => { + const [tabSelection, setTabSelection] = React.useState(TabSelection.Account); + + const wallet = useWallet(); + const address = wallet.activeAccount?.address; + + const { data: identity } = useIdentity(address); + + return ( + + +

Settings

+ setTabSelection(index)} + defaultIndex={tabSelection} + > + +
+
+ + {({ selected }) => ( + + Account + + )} + +
+
+ + {({ selected }) => ( + + Other Settings + + )} + +
+
+
+
+ { + { + [TabSelection.Account]: identity ? ( + + ) : ( + <> + ), + [TabSelection.Other]: , + }[tabSelection] + } +
+
+ ); +}; + +export default SettingsModal; diff --git a/components/ui/AddressInput.tsx b/components/ui/AddressInput.tsx index 979ad1f17..0f365c4af 100644 --- a/components/ui/AddressInput.tsx +++ b/components/ui/AddressInput.tsx @@ -137,32 +137,39 @@ const Input = (props: InputProps) => { return ; }; -export type AddressSelectProps = { +export type AddressInputProps = { onChange: (option: AddressOption | null) => void; value?: AddressOption | null; error?: string; + options?: AddressOption[]; + disabled?: boolean; }; -const AddressInput: React.FC = ({ +const AddressInput: React.FC = ({ onChange, error, value = null, + options, + disabled = false, }) => { const wallet = useWallet(); - const options = useMemo(() => { + const opts = useMemo(() => { + if (options) { + return options; + } return wallet.accounts .filter((acc) => acc.address !== wallet.activeAccount?.address) .map((account) => ({ label: shortenAddress(account.address, 13, 13), value: account.address, })); - }, [wallet.accounts]); + }, [options, wallet.accounts]); return (
@@ -170,7 +177,7 @@ const AddressInput: React.FC = ({ className="h-full" isSearchable={true} isClearable={true} - options={options} + options={opts} unstyled={true} placeholder="Enter account address" isMulti={false} @@ -189,6 +196,9 @@ const AddressInput: React.FC = ({ }} onChange={onChange} /> + {disabled && ( +
+ )} {error && (
{error}
)} diff --git a/components/ui/FormTransactionButton.tsx b/components/ui/FormTransactionButton.tsx index 0e2ed20d3..2cfeb3b5c 100644 --- a/components/ui/FormTransactionButton.tsx +++ b/components/ui/FormTransactionButton.tsx @@ -6,6 +6,7 @@ interface TransactionButtonProps { className?: string; dataTest?: string; disableFeeCheck?: boolean; + type?: "button" | "submit" | "reset"; } const FormTransactionButton: FC> = ({ @@ -13,11 +14,12 @@ const FormTransactionButton: FC> = ({ className = "", dataTest = "", disableFeeCheck = false, + type = "submit", children, }) => { return ( ; - disabled?: boolean; - }> -> = ({ onClick = () => {}, disabled = false, children }) => { - return ( - - ); -}; - -const IdentitySettings = () => { - const wallet = useWallet(); - const notificationStore = useNotifications(); - - const [displayName, setDisplayName] = useState(""); - const [discordHandle, setDiscordHandle] = useState(""); - const [twitterHandle, setTwitterHandle] = useState(""); - const queryClient = useQueryClient(); - - const address = wallet.activeAccount?.address; - const [sdk, id] = useSdkv2(); - - const { data: identity } = useIdentity(address); - - const { data: constants } = useChainConstants(); - - const { send: updateIdentity, isLoading: isUpdating } = useExtrinsic( - () => { - if (isRpcSdk(sdk)) { - return sdk.api.tx.identity.setIdentity({ - additional: [[{ Raw: "discord" }, { Raw: discordHandle }]], - display: { Raw: displayName }, - twitter: { Raw: twitterHandle }, - }); - } - }, - { - onSuccess: () => { - queryClient.invalidateQueries([id, identityRootKey, address]); - notificationStore.pushNotification("Successfully set Identity", { - type: "Success", - }); - }, - }, - ); - const { send: clearIdentity, isLoading: isClearing } = useExtrinsic( - () => { - if (isRpcSdk(sdk)) { - return sdk.api.tx.identity.clearIdentity(); - } - }, - { - onSuccess: () => { - queryClient.invalidateQueries([id, identityRootKey, address]); - notificationStore.pushNotification("Successfully cleared Identity", { - type: "Success", - }); - }, - }, - ); - - const transactionPending = isClearing || isUpdating; - - useEffect(() => { - if (!identity) { - setDisplayName(""); - setDiscordHandle(""); - setTwitterHandle(""); - } else { - setDisplayName(identity.displayName ?? ""); - setDiscordHandle(identity.discord ?? ""); - setTwitterHandle(identity.twitter ?? ""); - } - }, [identity]); - - const handleDisplayNameChange = (value: string) => { - if (getBytesCount(value) <= 32) { - setDisplayName(value); - } - }; - - const handleDiscordChange = (value: string) => { - if (getBytesCount(value) <= 32) { - setDiscordHandle(value); - } - }; - const handleTwitterChange = (value: string) => { - if (getBytesCount(value) <= 32) { - setTwitterHandle(value); - } - }; - - const getBytesCount = (string: string) => { - return new TextEncoder().encode(string).length; - }; - - const submitDisabled = - (identity?.discord === discordHandle && - identity.displayName === displayName && - identity.twitter === twitterHandle) || - transactionPending || - !wallet.connected; - - const indetityCost = - constants?.identity?.basicDeposit ?? - 0 + (constants?.identity?.fieldDeposit ?? 0); - - return ( - <> -
- Display Name -
- handleDisplayNameChange(e.target.value)} - value={displayName} - disabled={!wallet.connected} - /> -
-
-
- Discord -
- handleDiscordChange(e.target.value)} - value={discordHandle} - disabled={!wallet.connected} - /> -
-
-
- Twitter -
- handleTwitterChange(e.target.value)} - value={twitterHandle} - disabled={!wallet.connected} - /> -
-
-
- -
- Setting an identity requires a deposit of up to {indetityCost}{" "} - {constants?.tokenSymbol}. This deposit can be retrieved by clearing - your identity. -
-
-
- - Set Identity - - -
- - ); -}; - -const ProxySettings = () => { - const [sdk] = useSdkv2(); - const wallet = useWallet(); - - const proxy = wallet.getProxyFor(wallet.activeAccount?.address); - - const { - register, - handleSubmit, - reset, - formState: { errors, isDirty, isValid }, - watch, - } = useForm({ - mode: "all", - defaultValues: { - address: proxy?.address ?? "", - enabled: proxy?.enabled ?? false, - }, - }); - - const onSubmit = (data: ProxyConfig) => { - if (!wallet.activeAccount?.address) return; - wallet.setProxyFor(wallet.activeAccount?.address, data); - reset(data); - }; - - const address = watch("address"); - const enabled = watch("enabled"); - - return ( - <> -
- Proxy Account -
- -
-
-
-

Enable Proxy Execution

- -
-
- -
-
- { - if (enabled && tryCatch(() => encodeAddress(value)).isNone()) - return "Not a valid address"; - - if (isRpcSdk(sdk)) { - const proxies = await sdk.api.query.proxy.proxies(value); - const proxyMatch = proxies?.[0]?.find((p) => { - return ( - p.delegate.toString() === wallet.activeAccount?.address - ); - }); - if (!Boolean(proxyMatch)) { - return "You are not a proxy for this account."; - } - } - }, - deps: ["enabled"], - })} - /> -
-
- {errors.address && errors.address.message} -
-
- - -
- - ); -}; - -const Settings: NextPage = () => { - return ( - <> -

- Account Settings -

-
- -
-
- -
- - ); -}; - -export default Settings;