diff --git a/apps/dapp/pages/discord.tsx b/apps/dapp/pages/discord.tsx deleted file mode 100644 index 04a0da355..000000000 --- a/apps/dapp/pages/discord.tsx +++ /dev/null @@ -1,15 +0,0 @@ -// GNU AFFERO GENERAL PUBLIC LICENSE Version 3. Copyright (C) 2022 DAO DAO Contributors. -// See the "LICENSE" file in the root directory of this package for more copyright information. - -import { GetStaticProps } from 'next' - -import { serverSideTranslations } from '@dao-dao/i18n/serverSideTranslations' -import { DiscordRedirect } from '@dao-dao/stateful' - -export default DiscordRedirect - -export const getStaticProps: GetStaticProps = async ({ locale }) => ({ - props: { - ...(await serverSideTranslations(locale, ['translation'])), - }, -}) diff --git a/apps/sda/pages/discord.tsx b/apps/sda/pages/discord.tsx deleted file mode 100644 index 04a0da355..000000000 --- a/apps/sda/pages/discord.tsx +++ /dev/null @@ -1,15 +0,0 @@ -// GNU AFFERO GENERAL PUBLIC LICENSE Version 3. Copyright (C) 2022 DAO DAO Contributors. -// See the "LICENSE" file in the root directory of this package for more copyright information. - -import { GetStaticProps } from 'next' - -import { serverSideTranslations } from '@dao-dao/i18n/serverSideTranslations' -import { DiscordRedirect } from '@dao-dao/stateful' - -export default DiscordRedirect - -export const getStaticProps: GetStaticProps = async ({ locale }) => ({ - props: { - ...(await serverSideTranslations(locale, ['translation'])), - }, -}) diff --git a/packages/i18n/locales/bad/translation.json b/packages/i18n/locales/bad/translation.json index 4b809390f..b6e350365 100644 --- a/packages/i18n/locales/bad/translation.json +++ b/packages/i18n/locales/bad/translation.json @@ -196,7 +196,6 @@ "selectWidget": "bad bad", "setAsProfilePhoto": "bad bad bad bad", "setDisplayName": "bad bad bad", - "setUpDiscordNotifier": "bad bad bad bad", "settings": "bad", "showInstantiateMessage": "bad bad bad", "showQrCode": "bad bad bad", diff --git a/packages/i18n/locales/en/translation.json b/packages/i18n/locales/en/translation.json index fe524c2f0..c037d87e1 100644 --- a/packages/i18n/locales/en/translation.json +++ b/packages/i18n/locales/en/translation.json @@ -155,6 +155,8 @@ "go": "Go", "goBack": "Go back", "goToDaoPage": "Go to DAO page", + "goToDiscordBotRegistration": "Go to Discord Bot Registration", + "goToDiscordDeveloperPortal": "Go to the Discord Developer Portal", "goToOverruleProposal": "Go to overrule proposal", "goToProposal": "Go to proposal", "gotIt": "Got it", @@ -212,6 +214,7 @@ "refresh": "Refresh", "refundAndCancel": "Refund and cancel", "register": "Register", + "registerDiscordBotWithDaoDao": "Register Discord bot with DAO DAO", "registerSlashes": "Register slashes", "registerToVote": "Register to vote", "registered": "Registered", @@ -236,7 +239,7 @@ "selectWidget": "Select widget", "setAsProfilePhoto": "Set as profile photo", "setDisplayName": "set display name", - "setUpDiscordNotifier": "Set up Discord Notifier", + "setUpNewNotifier": "Set up new notifier", "setUpRebalancer": "Set up rebalancer", "settings": "Settings", "showInstantiateMessage": "Show Instantiate Message", @@ -602,6 +605,7 @@ "baseToken": "Base token", "becomeSubDaoAdminInputLabel": "New parent DAO", "blocksToPauseFor": "Blocks to pause for", + "botTokenTooltip": "Find this in the \"Bot\" tab under the \"TOKEN\" heading. Click \"Reset Token\" to generate a new token and view it.", "buttonLabel": "Button label", "calls": "Calls", "callsDescription": "The maximum amount of times the designated account is authorized to call a smart contract.", @@ -731,6 +735,8 @@ "noLimit": "No limit", "noOne": "No one", "nobody": "Nobody", + "oAuth2ClientIdTooltip": "Find this in the \"OAuth2\" tab under the \"Client information\" heading.", + "oAuth2ClientSecretTooltip": "Find this in the \"OAuth2\" tab under the \"Client information\" heading, next to \"CLIENT ID\". Click \"Reset Secret\" to generate a new secret and view it.", "oneOneCollection": "1/1 Collection", "oneOrMoreAccounts": "One or more accounts", "onlyMembersExecuteDescription": "If enabled, only members may execute passed proposals.", @@ -801,6 +807,7 @@ "spendingAllowanceDescription": "The amount of funds allowed to be spent by the authorized account.", "standardCollection": "Standard Collection", "startDate": "Start date", + "stepNumber": "Step {{number}}", "steps": "Steps", "subDaosToRecognize": "SubDAOs to recognize", "subDaosToRemove": "SubDAOs to remove", @@ -954,6 +961,7 @@ "activeThresholdDescription": "The amount of voting power that needs to be staked in order for the DAO to become active and thus allow proposals to be created.", "addCw20ToTreasuryActionDescription": "Display the DAO's balance of a CW20 token in the treasury view.", "addCw721ToTreasuryActionDescription": "Display the NFTs owned by the DAO from a CW721 NFT collection in the treasury view.", + "addDiscordNotifierRedirectInstructions": "In the \"OAuth2\" tab of your Discord application's settings, press the \"Add Redirect\" button under the \"Redirects\" heading. Paste in the URL below, and then click out of the text field. Press the \"Save Changes\" button that appears.", "addToProfileExplanation": "Add the current chain to your profile shown below.", "addWalletToProfile": "Add this wallet to your profile.", "addWalletToProfileToEdit": "You cannot edit your profile until you add the chain your wallet is currently connected to. Click to add it.", @@ -1085,9 +1093,12 @@ "direct": "direct", "disableVetoerDaoDescription": "Stop displaying proposals from a DAO on the home page when they are vetoable.", "disabled": "Disabled", - "discordNotifierExplanation": "The Discord Notifier broadcasts a message to registered Discord channels when a proposal is created. Clicking the button below begins the registration process; it will take you to Discord's website and prompt you to choose a server and channel. You will then be redirected back here and prompted to authorize with your wallet. Once this is done, proposal notifications are enabled.", + "discordNotifierBeginSetupExplanation": "Click the button below to begin the process of setting up a new notifier.", + "discordNotifierFinishSetupExplanation": "Approve the registration confirmation with your wallet to complete the setup process.", "discordNotifierRegistrations_one": "You have setup {{count}} notifier.", "discordNotifierRegistrations_other": "You have setup {{count}} notifiers.", + "discordNotifierSetupExplanation": "Follow the steps below to complete the setup process. Once you complete step 4, you will be redirected back to this page to sign the final confirmation with your wallet.", + "discordNotifierSubtitle": "Broadcast proposal creation notifications to one or more Discord channels.", "dontKnowNotSure": "Don't know/Not sure", "draftSavedAtTime": "Draft saved at {{time}}.", "draftSaving": "Draft saving...", @@ -1269,7 +1280,7 @@ "opensAtDate": "Opens at {{date}}", "optionInert": "This option will not perform any actions if it wins.", "orSelectWallet": "or select a wallet...", - "otherNotifiersNotShown": "Notifiers setup by others are not shown.", + "otherNotifiersNotShown": "Notifiers set up by others are not shown.", "overruleSubDaoProposalDescription": "Overrule a proposal in a SubDAO.", "overwritingSave": "This save will overwrite the existing one with the same name.", "paid": "Paid", @@ -1617,7 +1628,7 @@ "daoCreatedPleaseWait": "DAO created. You will be redirected once the DAO page is ready...", "deposited": "Deposited {{amount}} ${{tokenSymbol}}.", "depositedTokenIntoDao": "Deposited {{amount}} ${{tokenSymbol}} into the {{daoName}} treasury.", - "discordNotifierEnabled": "Discord notifier setup successfully. Your notifier list will be updated shortly.", + "discordNotifierEnabled": "Discord notifier set up successfully. Your notifier list will be updated shortly.", "discordNotifierRemoved": "Discord notifier removed. Your notifier list will be updated shortly.", "multisigImported": "Import successful. Review your new DAO's configuration, and make sure to set an image, name, and description.", "nftCollectionContractInstantiated": "NFT collection created successfully.", @@ -1659,8 +1670,10 @@ "actions_other": "Actions", "active": "Active", "activeThreshold": "Active threshold", + "addBotToServer": "Add bot to server", "addCw20ToTreasury": "Display Token Balance in Treasury", "addCw721ToTreasury": "Display NFT Collection in Treasury", + "addRedirect": "Add redirect", "addToProfile": "Add to Profile", "advancedConfiguration": "Advanced configuration", "all": "All", @@ -1725,11 +1738,13 @@ "continuous": "Continuous", "contributions": "Contributions", "contributor": "Contributor", + "copyAppDetails": "Copy app details", "createADAO": "Create a DAO", "createAProposal": "Create a proposal", "createASubDao": "Create a SubDAO", "createCrossChainAccount": "Create Cross-Chain Account", "createDao": "Create DAO", + "createDiscordApplication": "Create Discord application", "createIca": "Create ICA", "createNftCollection": "Create NFT Collection", "createPost": "Create Post", @@ -1798,6 +1813,7 @@ "fees": "Fees", "fiatOnOffRamp": "Fiat on/off ramp", "filter": "Filter", + "finishSetup": "Finish setup", "finished": "Finished", "fixChildAdmin": "Fix {{child}} admin", "following": "Following", diff --git a/packages/i18n/locales/es/translation.json b/packages/i18n/locales/es/translation.json index 18c183e8c..c5c57ea5b 100644 --- a/packages/i18n/locales/es/translation.json +++ b/packages/i18n/locales/es/translation.json @@ -241,7 +241,6 @@ "selectWidget": "Seleccionar widget", "setAsProfilePhoto": "Establecer como foto de perfil", "setDisplayName": "Establecer nombre para mostrar", - "setUpDiscordNotifier": "Configurar notificador de Discord", "setUpRebalancer": "Configurar reequilibrador", "settings": "Configuración", "shareFeedback": "Compartir retroalimentación", diff --git a/packages/state/recoil/atoms/dao.ts b/packages/state/recoil/atoms/dao.ts index 90555a926..4d93d8b82 100644 --- a/packages/state/recoil/atoms/dao.ts +++ b/packages/state/recoil/atoms/dao.ts @@ -1,4 +1,4 @@ -import { atom } from 'recoil' +import { atom, atomFamily } from 'recoil' import { localStorageEffectJSON } from '../effects' @@ -21,11 +21,18 @@ export const temporaryFollowingDaosAtom = atom<{ default: { following: [], unfollowing: [] }, }) -export const discordNotifierSetupAtom = atom<{ - state: string - coreAddress: string -} | null>({ +export const discordNotifierSetupAtom = atomFamily< + | { + state: string + clientId: string + clientSecret: string + botToken: string + redirectUri: string + } + | undefined, + string +>({ key: 'discordNotifierSetup', - default: null, + default: undefined, effects: [localStorageEffectJSON], }) diff --git a/packages/state/recoil/effects.ts b/packages/state/recoil/effects.ts index 81898315f..3c6823eae 100644 --- a/packages/state/recoil/effects.ts +++ b/packages/state/recoil/effects.ts @@ -12,12 +12,12 @@ export const localStorageEffect = } const savedValue = localStorage.getItem(key) - if (savedValue !== null) { + if (savedValue && savedValue !== 'undefined') { setSelf(parse(savedValue)) } onSet((newValue: T, _: any, isReset: boolean) => { - if (isReset) { + if (isReset || newValue === undefined) { localStorage.removeItem(key) } else { localStorage.setItem(key, serialize(newValue)) diff --git a/packages/stateful/components/DiscordRedirect.tsx b/packages/stateful/components/DiscordRedirect.tsx deleted file mode 100644 index c69ea4ba9..000000000 --- a/packages/stateful/components/DiscordRedirect.tsx +++ /dev/null @@ -1,56 +0,0 @@ -import { useCallback, useEffect, useRef } from 'react' -import toast from 'react-hot-toast' -import { useTranslation } from 'react-i18next' -import { useRecoilState, useRecoilValue } from 'recoil' - -import { discordNotifierSetupAtom, mountedInBrowserAtom } from '@dao-dao/state' -import { PageLoader, useDaoNavHelpers } from '@dao-dao/stateless' -import { DaoTabId } from '@dao-dao/types' - -export const DiscordRedirect = () => { - const { t } = useTranslation() - const { goToDao, router } = useDaoNavHelpers() - const [discordNotifierSetup, setDiscordNotificationSetup] = useRecoilState( - discordNotifierSetupAtom - ) - const mountedInBrowser = useRecoilValue(mountedInBrowserAtom) - - const redirect = useCallback(async () => { - const { state, code } = router.query - if ( - discordNotifierSetup && - typeof state === 'string' && - typeof code === 'string' - ) { - // If state doesn't match, clear setup state, error, and redirect to DAO. - if (state !== discordNotifierSetup.state) { - setDiscordNotificationSetup(null) - toast.error(t('error.discordAuthFailed')) - goToDao(discordNotifierSetup.coreAddress) - return - } - - // If state matches, redirect to DAO page with code parameter. - goToDao(discordNotifierSetup.coreAddress, DaoTabId.Proposals, { - discordNotifier: code, - }) - } else { - // If necessary data is not loaded, just redirect home. We are probably - // not in a setup flow. - router.push('/') - } - }, [discordNotifierSetup, goToDao, router, setDiscordNotificationSetup, t]) - - const redirected = useRef(false) - useEffect(() => { - if (!mountedInBrowser || !router.isReady || redirected.current) { - return - } - - // Only redirect once. - redirected.current = true - redirect() - }, [mountedInBrowser, redirect, router.isReady]) - - return -} diff --git a/packages/stateful/components/dao/DiscordNotifierConfigureModal.tsx b/packages/stateful/components/dao/DiscordNotifierConfigureModal.tsx index fb8ebf427..a4c4b36ea 100644 --- a/packages/stateful/components/dao/DiscordNotifierConfigureModal.tsx +++ b/packages/stateful/components/dao/DiscordNotifierConfigureModal.tsx @@ -1,12 +1,8 @@ -import { - NotificationsActiveRounded, - NotificationsNoneRounded, -} from '@mui/icons-material' import { useRouter } from 'next/router' import { useCallback, useEffect, useRef, useState } from 'react' import toast from 'react-hot-toast' import { useTranslation } from 'react-i18next' -import { useSetRecoilState } from 'recoil' +import { useRecoilState, useSetRecoilState } from 'recoil' import { discordNotifierRegistrationsSelector, @@ -14,19 +10,22 @@ import { refreshDiscordNotifierRegistrationsAtom, } from '@dao-dao/state/recoil' import { + DiscordNoCircleIcon, + DiscordNotifierRegistrationForm, IconButton, DiscordNotifierConfigureModal as StatelessDiscordNotifierConfigureModal, Tooltip, useCachedLoadable, useChain, useDaoInfoContext, + useDaoNavHelpers, } from '@dao-dao/stateless' import { DaoTabId } from '@dao-dao/types' import { DISCORD_NOTIFIER_API_BASE, - DISCORD_NOTIFIER_CLIENT_ID, - DISCORD_NOTIFIER_REDIRECT_URI, DISCORD_NOTIFIER_SIGNATURE_TYPE, + SITE_URL, + objectMatchesStructure, processError, } from '@dao-dao/utils' @@ -39,6 +38,7 @@ export const DiscordNotifierConfigureModal = () => { const router = useRouter() const { chain_id: chainId } = useChain() const { coreAddress } = useDaoInfoContext() + const { getDaoPath } = useDaoNavHelpers() const { isWalletConnected, hexPublicKey } = useWallet({ loadAccount: true, }) @@ -51,8 +51,8 @@ export const DiscordNotifierConfigureModal = () => { DISCORD_NOTIFIER_SIGNATURE_TYPE ) - const setDiscordNotificationSetup = useSetRecoilState( - discordNotifierSetupAtom + const [discordNotifierSetup, setDiscordNotifierSetup] = useRecoilState( + discordNotifierSetupAtom(coreAddress) ) const setRefreshRegistrations = useSetRecoilState( @@ -63,14 +63,26 @@ export const DiscordNotifierConfigureModal = () => { }) ) + const registrationsLoadable = useCachedLoadable( + !hexPublicKey.loading + ? discordNotifierRegistrationsSelector({ + chainId, + coreAddress, + walletPublicKey: hexPublicKey.data, + }) + : undefined + ) + const registrations = + registrationsLoadable.state === 'hasValue' + ? registrationsLoadable.contents + : [] + // Refresh in a loop for 60 seconds. - const [refreshRegistrationsLoop, setRefreshRegistrationsLoop] = - useState(false) + const [refreshing, setRefreshing] = useState(false) const refreshRegistrationLoopNum = useRef(0) useEffect(() => { - if (refreshRegistrationsLoop && refreshRegistrationLoopNum.current === 0) { + if (refreshing && refreshRegistrationLoopNum.current === 0) { refreshRegistrationLoopNum.current = 20 - setRefreshRegistrationsLoop(false) const interval = setInterval(() => { setRefreshRegistrations((prev) => prev + 1) @@ -78,112 +90,157 @@ export const DiscordNotifierConfigureModal = () => { refreshRegistrationLoopNum.current -= 1 if (refreshRegistrationLoopNum.current === 0) { clearInterval(interval) + setRefreshing(false) } }, 3000) } - }, [refreshRegistrationsLoop, setRefreshRegistrations]) + }, [refreshing, setRefreshRegistrations]) - const registrationsLoadable = useCachedLoadable( - !hexPublicKey.loading - ? discordNotifierRegistrationsSelector({ - chainId, - coreAddress, - walletPublicKey: hexPublicKey.data, - }) - : undefined - ) - const registrations = - registrationsLoadable.state === 'hasValue' - ? registrationsLoadable.contents - : [] + // Stop refreshing if registration length updates. + const numRegistrations = registrations.length + useEffect(() => { + setRefreshing(false) + }, [numRegistrations]) + + const redirectUri = + SITE_URL + + getDaoPath(coreAddress, DaoTabId.Proposals, { + discord: 1, + }) - const setup = () => { + const setup = ({ + clientId, + clientSecret, + botToken, + }: DiscordNotifierRegistrationForm) => { // Nonce to prevent CSRF attacks. const state = Date.now().toString(36) + Math.random().toString(36).substring(2) // Store setup state in atom, which saves to localStorage to be loaded on - // Discord redirect URI page. - setDiscordNotificationSetup({ + // Discord redirect URI. + setDiscordNotifierSetup({ state, - coreAddress, + clientId, + clientSecret, + botToken, + redirectUri, }) // Permissions = 1024 is read messages/view channels. - window.location.href = `https://discord.com/api/oauth2/authorize?client_id=${DISCORD_NOTIFIER_CLIENT_ID}&redirect_uri=${encodeURIComponent( - DISCORD_NOTIFIER_REDIRECT_URI + window.location.href = `https://discord.com/api/oauth2/authorize?client_id=${clientId}&redirect_uri=${encodeURIComponent( + redirectUri )}&response_type=code&permissions=1024&scope=webhook.incoming%20bot&state=${encodeURIComponent( state )}` } - const [loading, setLoading] = useState(false) + const [finishingRegistration, setFinishingRegistration] = useState(false) + const [loadingRegistration, setLoadingRegistration] = useState(false) + const register = useCallback(async () => { - const { discordNotifier } = router.query - if (discordNotifier) { - setLoading(true) - try { - await postRequest(`/${coreAddress}/register`, { - code: discordNotifier, - redirectUri: DISCORD_NOTIFIER_REDIRECT_URI, - }) + // These should already have been checked. Type-check. + if (!discordNotifierSetup || !router.query.code) { + setDiscordNotifierSetup(undefined) + toast.error(t('error.discordAuthFailed')) + return + } - toast.success(t('success.discordNotifierEnabled')) - setRefreshRegistrationsLoop(true) - } catch (err) { - console.error(err) - toast.error(processError(err)) - } finally { - setLoading(false) - // Remove query params and redirect to proposals. - router.replace( - router.asPath.split('?')[0] + '/' + DaoTabId.Proposals, - undefined, - { - shallow: true, - } - ) - } + setLoadingRegistration(true) + try { + await postRequest(`/${coreAddress}/register`, { + code: router.query.code, + clientId: discordNotifierSetup.clientId, + clientSecret: discordNotifierSetup.clientSecret, + botToken: discordNotifierSetup.botToken, + redirectUri: discordNotifierSetup.redirectUri, + }) + + toast.success(t('success.discordNotifierEnabled')) + + setFinishingRegistration(false) + setRefreshing(true) + setDiscordNotifierSetup(undefined) + + // Clear URL query params from redirect. + router.replace(getDaoPath(coreAddress, DaoTabId.Proposals), undefined, { + shallow: true, + }) + } catch (err) { + console.error(err) + toast.error(processError(err)) + } finally { + setLoadingRegistration(false) } - }, [coreAddress, postRequest, router, t]) + }, [ + discordNotifierSetup, + router, + setDiscordNotifierSetup, + t, + postRequest, + coreAddress, + getDaoPath, + ]) - const redirected = useRef(false) + const registered = useRef(false) useEffect(() => { if ( + registered.current || !router.isReady || - redirected.current || - !router.query.discordNotifier + !discordNotifierSetup || + !objectMatchesStructure(router.query, { + discord: {}, + code: {}, + state: {}, + }) ) { return } // Open this modal since we're completing registration. + setFinishingRegistration(true) setVisible(true) + // If state doesn't match, clear setup state and error. + if (router.query.state !== discordNotifierSetup.state) { + setDiscordNotifierSetup(undefined) + toast.error(t('error.discordAuthFailed')) + return + } + + setLoadingRegistration(true) + // Don't attempt to auto-register until wallet ready. Still show the modal // above since the wallet may be connecting. if (postRequestReady) { - // Only register once. - redirected.current = true + registered.current = true register() } - }, [router, register, postRequestReady]) + }, [ + router, + postRequestReady, + discordNotifierSetup, + setDiscordNotifierSetup, + t, + finishingRegistration, + register, + ]) const unregister = useCallback( async (id: string) => { - setLoading(true) + setLoadingRegistration(true) try { await postRequest(`/${coreAddress}/unregister`, { id, }) toast.success(t('success.discordNotifierRemoved')) - setRefreshRegistrationsLoop(true) + setRefreshing(true) } catch (err) { console.error(err) toast.error(processError(err)) } finally { - setLoading(false) + setLoadingRegistration(false) } }, [coreAddress, postRequest, t] @@ -193,13 +250,9 @@ export const DiscordNotifierConfigureModal = () => { <> 0 - ? NotificationsActiveRounded - : NotificationsNoneRounded - } + Icon={DiscordNoCircleIcon} className="animate-fade-in" - iconClassName="!w-5 !h-5" + iconClassName="!w-[18px] !h-[18px]" onClick={() => setVisible(true)} variant="secondary" /> @@ -208,9 +261,17 @@ export const DiscordNotifierConfigureModal = () => { } connected={isWalletConnected} - loading={loading} - onClose={() => setVisible(false)} + doRegister={register} + finishingRegistration={finishingRegistration} + formDefaults={discordNotifierSetup} + loadingRegistration={loadingRegistration} + onClose={() => { + setVisible(false) + setFinishingRegistration(false) + }} onDelete={unregister} + redirectUri={redirectUri} + refreshing={refreshing} registrations={registrations} setup={setup} visible={visible} diff --git a/packages/stateful/components/index.ts b/packages/stateful/components/index.ts index e55bbeeb0..71b4fb322 100644 --- a/packages/stateful/components/index.ts +++ b/packages/stateful/components/index.ts @@ -14,7 +14,6 @@ export * from './ChainStatus' export * from './ConnectWallet' export * from './DaoCreatedModal' export * from './DappLayout' -export * from './DiscordRedirect' export * from './EntityDisplay' export * from './IconButtonLink' export * from './LinkWrapper' diff --git a/packages/stateless/components/icons/DiscordNoCircleIcon.tsx b/packages/stateless/components/icons/DiscordNoCircleIcon.tsx new file mode 100644 index 000000000..b545c7f73 --- /dev/null +++ b/packages/stateless/components/icons/DiscordNoCircleIcon.tsx @@ -0,0 +1,14 @@ +import { SVGProps } from 'react' + +export const DiscordNoCircleIcon = (props: SVGProps) => ( + + + +) diff --git a/packages/stateless/components/icons/index.ts b/packages/stateless/components/icons/index.ts index dd43a3df3..954118cf4 100644 --- a/packages/stateless/components/icons/index.ts +++ b/packages/stateless/components/icons/index.ts @@ -1,4 +1,5 @@ export * from './CrownIcon' export * from './DiscordIcon' +export * from './DiscordNoCircleIcon' export * from './GithubIcon' export * from './TwitterIcon' diff --git a/packages/stateless/components/modals/DiscordNotifierConfigureModal.stories.tsx b/packages/stateless/components/modals/DiscordNotifierConfigureModal.stories.tsx index e5ee3ebbd..ef456bc52 100644 --- a/packages/stateless/components/modals/DiscordNotifierConfigureModal.stories.tsx +++ b/packages/stateless/components/modals/DiscordNotifierConfigureModal.stories.tsx @@ -20,8 +20,10 @@ Default.args = { visible: true, onClose: () => alert('close'), setup: () => alert('setup'), + doRegister: () => alert('register'), registrations: [], - loading: false, + loadingRegistration: false, + finishingRegistration: false, connected: true, ConnectWallet: () =>
ConnectWallet
, } diff --git a/packages/stateless/components/modals/DiscordNotifierConfigureModal.tsx b/packages/stateless/components/modals/DiscordNotifierConfigureModal.tsx index bbadcbc66..ba2cbbe1b 100644 --- a/packages/stateless/components/modals/DiscordNotifierConfigureModal.tsx +++ b/packages/stateless/components/modals/DiscordNotifierConfigureModal.tsx @@ -1,112 +1,283 @@ import { ArrowOutwardRounded, DeleteRounded } from '@mui/icons-material' -import { ComponentType } from 'react' +import clsx from 'clsx' +import { ComponentType, useState } from 'react' +import { useForm } from 'react-hook-form' import { useTranslation } from 'react-i18next' import { DiscordNotifierRegistration, ModalProps } from '@dao-dao/types' import { useDaoInfoContext } from '../../contexts' -import { Button } from '../buttons' +import { Button, ButtonLink } from '../buttons' +import { CopyToClipboard } from '../CopyToClipboard' import { IconButton, IconButtonLink } from '../icon_buttons' -import { DiscordIcon } from '../icons' +import { InputErrorMessage, InputLabel, TextInput } from '../inputs' +import { Loader } from '../logo' import { Modal } from './Modal' +export type DiscordNotifierRegistrationForm = { + clientId: string + clientSecret: string + botToken: string +} + export type DiscordNotifierConfigureModalProps = Pick< ModalProps, 'visible' | 'onClose' > & { - setup: () => void + setup: (data: DiscordNotifierRegistrationForm) => void + doRegister: () => void registrations: DiscordNotifierRegistration[] - loading: boolean + formDefaults?: DiscordNotifierRegistrationForm + loadingRegistration: boolean + finishingRegistration: boolean onDelete: (id: string) => Promise connected: boolean + redirectUri: string + refreshing: boolean ConnectWallet: ComponentType } export const DiscordNotifierConfigureModal = ({ setup, + doRegister, registrations, - loading, + formDefaults, + loadingRegistration, + finishingRegistration, onDelete, connected, + redirectUri, + refreshing, ConnectWallet, + onClose, ...props }: DiscordNotifierConfigureModalProps) => { const { t } = useTranslation() const { name: daoName } = useDaoInfoContext() + const [registering, setRegistering] = useState(false) + + const { + watch, + register, + handleSubmit, + formState: { errors }, + } = useForm({ + defaultValues: formDefaults, + }) + + const canAddBot = !!( + watch('clientId') && + watch('clientSecret') && + watch('botToken') + ) + return ( { + setRegistering(false) + onClose?.() }} > -

{t('info.discordNotifierExplanation')}

- {connected ? ( - - ) : ( - - )} - - {registrations.length > 0 && ( - <> -
-

{t('title.yourNotifiers')}

-

- {t('info.otherNotifiersNotShown')} + registering ? ( +

+

+ {t('info.discordNotifierSetupExplanation')}

-
-
- {registrations.map(({ id, guild, channel }) => ( -
+ + - {!!guild.iconHash && ( -
- )} - -

- {/* eslint-disable-next-line i18next/no-literal-string */} - {guild.name} • #{channel.name} -

- -
- + +
+
+ + +

{t('info.addDiscordNotifierRedirectInstructions')}

+ + +
+ +
+ +
+
+ + + +
+ +
+ - onDelete(id)} - variant="ghost" + + +
+ +
+ + +
- ))} -
- +
+ +
+ + +
+ + ) : finishingRegistration ? ( + <> +

+ {t('info.discordNotifierFinishSetupExplanation')} +

+ +
+ + +
+ + ) : ( + <> +

+ {t('info.discordNotifierBeginSetupExplanation')} +

+ + + + ) + ) : ( + )} + + {!registering && + !finishingRegistration && + (registrations.length > 0 || refreshing) && ( + <> +
+

{t('title.yourNotifiers')}

+

+ {t('info.otherNotifiersNotShown')} +

+
+ +
+ {registrations.map(({ id, guild, channel }) => ( +
+ {!!guild.iconHash && ( +
+ )} + +

+ {/* eslint-disable-next-line i18next/no-literal-string */} + {guild.name} • #{channel.name} +

+ +
+ + onDelete(id)} + variant="ghost" + /> +
+
+ ))} +
+ + {refreshing && } + + )} ) }