import { ArrowOutward, Close, Link, MenuOpen } from '@mui/icons-material' import clsx from 'clsx' import { ComponentType, Dispatch, ReactNode, RefCallback, SetStateAction, useEffect, useState, } from 'react' import { createPortal } from 'react-dom' import { useTranslation } from 'react-i18next' import { AddressInputProps } from '@dao-dao/types' import { APPS, processError, toAccessibleImageUrl } from '@dao-dao/utils' import { useChain } from '../../contexts' import { useQuerySyncedState } from '../../hooks' import { Button, ButtonLink } from '../buttons' import { ErrorPage } from '../error' import { IconButton } from '../icon_buttons' import { InputErrorMessage, InputLabel, SegmentedControls, TextInput, } from '../inputs' import { PageLoader } from '../logo' import { MarkdownRenderer } from '../MarkdownRenderer' import { Modal } from '../modals' import { StatusCard } from '../StatusCard' import { Tooltip } from '../tooltip' export type AppsRendererExecutionType = 'default' | 'authzExec' | 'daoAdminExec' export type AppsRendererProps = { /** * The reference to set the iframe for the apps passthrough functionality. */ iframeRef: RefCallback /** * Whether the apps renderer is in full screen mode. */ fullScreen: boolean /** * Set the full screen mode. */ setFullScreen: Dispatch> /** * The type of execution. */ executionType: AppsRendererExecutionType /** * Set the execution type. */ setExecutionType: Dispatch> /** * The other (non-default execution type) address. */ otherAddress: string /** * Set the other (non-default execution type) address. */ setOtherAddress: Dispatch> /** * Stateful AddressInput component. */ AddressInput: ComponentType /** * The chain picker node. */ chainPicker: ReactNode /** * Whether or not to show a loading state which prevents opening apps. */ loading?: boolean /** * Whether or not the entity is updating. */ updating?: boolean /** * Error to display. */ error?: string } // Only allow URLs starting with `http(s)://`, to prevent XSS via `javascript:` // URLs. const ALLOWED_URL_REGEX = /^https?:\/\/.+[^\.]$/ const isUrlValid = (url: string): true | string => { try { if (!!url && !!new URL(url).href && ALLOWED_URL_REGEX.test(url)) { return true } else { return 'Invalid URL.' } } catch (err) { return processError(err, { forceCapture: false, }) } } export const AppsRenderer = ({ iframeRef, fullScreen, setFullScreen, executionType, setExecutionType, otherAddress, setOtherAddress, AddressInput, chainPicker, loading, updating, error: _error, }: AppsRendererProps) => { const { t } = useTranslation() const [iframe, setIframe] = useState(null) // Show app opener when app is already open. const [appOpenerVisible, setAppOpenerVisible] = useState(false) const [url, setUrl, wasInitializedFromQuery] = useQuerySyncedState({ param: 'url', defaultValue: '', }) const [error, setError] = useState() const openApp = (url: string) => { const validity = isUrlValid(url) if (validity === true) { setError(undefined) setUrl(url) // Change existing iframe if it exists. Otherwise it will be created // when the full screen modal opens and automatically use the URL set. if (iframe) { iframe.src = url } setFullScreen(true) setAppOpenerVisible(false) } else { setError(validity) } } // If URL is set on mount, open automatically. useEffect(() => { if (wasInitializedFromQuery && isUrlValid(url) === true) { openApp(url) } // eslint-disable-next-line react-hooks/exhaustive-deps }, [wasInitializedFromQuery]) // Add event handler to inform iframe that it's wrapped in DAO DAO if it asks. useEffect(() => { if (!iframe?.contentWindow) { return } const listener = ({ data }: MessageEvent) => { if (data === 'isDaoDao') { iframe.contentWindow?.postMessage('amDaoDao') } } iframe.contentWindow.addEventListener('message', listener) return () => { iframe.contentWindow?.removeEventListener('message', listener) } }, [iframe]) const currentError = _error || error return fullScreen ? ( <> {createPortal(

{url}

setAppOpenerVisible(true)} variant="ghost" /> setFullScreen((f) => !f)} variant="ghost" />
{loading ? ( ) : currentError ? ( ) : ( )}
, document.body )} setAppOpenerVisible(false)} visible={appOpenerVisible} > ) : ( ) } type AppOpenerProps = Omit< AppsRendererProps, 'fullScreen' | 'setFullScreen' | 'iframeRef' > & { url: string openApp: (url: string) => void error: string | undefined setError: Dispatch> loading?: boolean } const AppOpener = ({ executionType, setExecutionType, otherAddress, setOtherAddress, AddressInput, chainPicker, url, openApp, error, setError, loading, }: AppOpenerProps) => { const { t } = useTranslation() const { chainId } = useChain() const appsForChain = APPS.filter( ({ chainIdFilter }) => (!chainIdFilter?.include || chainIdFilter.include.includes(chainId)) && (!chainIdFilter?.exclude || !chainIdFilter.exclude.includes(chainId)) ) const [inputUrl, setInputUrl] = useState(url) // Update the input field to match the URL if it changes in the parent // component. This should handle the URL being updated from the query params. useEffect(() => { if (url !== inputUrl) { setInputUrl(url) } // Only change the input URL when the URL changes (i.e. ignore input // change). // // eslint-disable-next-line react-hooks/exhaustive-deps }, [setInputUrl, url]) // If no app URL matching, choose the last one (custom) with empty URL. const selectedAppIndex = appsForChain.findIndex( ({ url: appUrl }) => appUrl === inputUrl || !appUrl ) const customSelected = !!inputUrl && selectedAppIndex === appsForChain.length - 1 return (
{appsForChain.map( ({ platform, name, imageUrl, url: appUrl }, index) => { const isCustom = !appUrl const selected = index === selectedAppIndex return ( ) } )}
{customSelected && ( {t('button.openIntegrationGuide')} )} { setInputUrl(event.target.value) setError(undefined) }} onKeyDown={(e) => { if (e.key === 'Enter') { openApp(inputUrl) } }} placeholder={t('form.url')} type="url" value={inputUrl} />
} /> onSelect={(value) => setExecutionType(value)} selected={executionType} tabs={[ { label: t('title.dao'), value: 'default' }, { label: t('title.authzExec'), value: 'authzExec' }, { label: t('title.daoAdminExec'), value: 'daoAdminExec' }, ]} /> {executionType !== 'default' && (
{chainPicker} setOtherAddress(value)} type={executionType === 'daoAdminExec' ? 'contract' : undefined} value={otherAddress} />
)}
) }