Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Epic] Governance visibility #1256

Merged
merged 14 commits into from
Dec 9, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions public/images/common/network-error.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
146 changes: 146 additions & 0 deletions src/components/dashboard/GovernanceSection/GovernanceSection.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
import { useRef } from 'react'
import { Typography, Card, Box, Alert, IconButton, Link, SvgIcon } from '@mui/material'
import { WidgetBody } from '@/components/dashboard/styled'
import ExpandMoreIcon from '@mui/icons-material/ExpandMore'
import Accordion from '@mui/material/Accordion'
import AccordionSummary from '@mui/material/AccordionSummary'
import AccordionDetails from '@mui/material/AccordionDetails'
import css from './styles.module.css'
import { useBrowserPermissions } from '@/hooks/safe-apps/permissions'
import { useRemoteSafeApps } from '@/hooks/safe-apps/useRemoteSafeApps'
import { SafeAppsTag, SAFE_APPS_SUPPORT_CHAT_URL } from '@/config/constants'
import { useDarkMode } from '@/hooks/useDarkMode'
import { OpenInNew } from '@mui/icons-material'
import NetworkError from '@/public/images/common/network-error.svg'
import useChainId from '@/hooks/useChainId'
import { getSafeTokenAddress } from '@/components/common/SafeTokenWidget'
import SafeAppIframe from '@/components/safe-apps/AppFrame/SafeAppIframe'
import type { UseAppCommunicatorHandlers } from '@/components/safe-apps/AppFrame/useAppCommunicator'
import useAppCommunicator from '@/components/safe-apps/AppFrame/useAppCommunicator'
import { useCurrentChain } from '@/hooks/useChains'
import useGetSafeInfo from '@/components/safe-apps/AppFrame/useGetSafeInfo'
import type { SafeAppData } from '@gnosis.pm/safe-react-gateway-sdk'
import useSafeInfo from '@/hooks/useSafeInfo'
import { fetchSafeAppFromManifest } from '@/services/safe-apps/manifest'
import useAsync from '@/hooks/useAsync'

// A fallback component when the Safe App fails to load
const WidgetLoadErrorFallback = () => (
<Box display="flex" flexDirection="column" alignItems="center" height="100%">
<Card className={css.loadErrorCard}>
<Box className={css.loadErrorMsgContainer}>
<Typography variant="h4" color="text.primary" fontWeight="bold">
Couldn&apos;t load governance widgets
</Typography>
<SvgIcon component={NetworkError} inheritViewBox className={css.loadErroricon} />
<Typography variant="body1" color="text.primary">
You can try to reload the page and in case the problem persists, please reach out to us via{' '}
<Link target="_blank" href={SAFE_APPS_SUPPORT_CHAT_URL} fontSize="medium">
Discord
<OpenInNew fontSize="small" color="primary" className={css.loadErroricon} />
</Link>
</Typography>
</Box>
</Card>
</Box>
)

// A mini Safe App frame with a minimal set of communication handlers
const MiniAppFrame = ({ app, title }: { app: SafeAppData; title: string }) => {
const chain = useCurrentChain()
const isDarkMode = useDarkMode()
const theme = isDarkMode ? 'dark' : 'light'
const { getAllowedFeaturesList } = useBrowserPermissions()
const iframeRef = useRef<HTMLIFrameElement>(null)

const [, error] = useAsync(() => {
if (!chain?.chainId) return
return fetchSafeAppFromManifest(app.url, chain.chainId)
}, [app.url, chain?.chainId])

// Initialize the app communicator
useAppCommunicator(iframeRef, app, chain, {
onGetSafeInfo: useGetSafeInfo(),
} as Partial<UseAppCommunicatorHandlers> as UseAppCommunicatorHandlers)

return error ? (
<WidgetLoadErrorFallback />
) : (
<SafeAppIframe
key={theme}
appUrl={`${app.url}#widget+${theme}`}
allowedFeaturesList={getAllowedFeaturesList(app.url)}
title={title}
iframeRef={iframeRef}
/>
)
}

// Entire section for the governance widgets
const GovernanceSection = () => {
const [matchingApps, errorFetchingClaimingSafeApp] = useRemoteSafeApps(SafeAppsTag.SAFE_CLAIMING_APP)
const claimingApp = matchingApps?.[0]
const fetchingSafeClaimingApp = !claimingApp && !errorFetchingClaimingSafeApp
const { safeLoading } = useSafeInfo()

return (
<Accordion className={css.accordion} defaultExpanded>
<AccordionSummary
expandIcon={
<IconButton size="small">
<ExpandMoreIcon color="border" />
</IconButton>
}
>
<div>
<Typography component="h2" variant="subtitle1" fontWeight={700}>
Governance
</Typography>
<Typography variant="body2" mb={2} color="text.secondary">
Use your SAFE tokens to vote on important proposals or participate in forum discussions.
</Typography>
</div>
</AccordionSummary>

<AccordionDetails sx={({ spacing }) => ({ padding: `0 ${spacing(3)}` })}>
{claimingApp || fetchingSafeClaimingApp ? (
<WidgetBody>
<Card className={css.widgetWrapper}>
{claimingApp && !safeLoading ? (
<MiniAppFrame app={claimingApp} title="Safe Governance" />
) : (
<Box
className={css.widgetWrapper}
display="flex"
alignItems="center"
justifyContent="center"
textAlign="center"
>
<Typography variant="h1" color="text.secondary">
Loading section...
</Typography>
</Box>
)}
</Card>
</WidgetBody>
) : (
<Alert severity="warning" elevation={3}>
There was an error fetching the Governance section. Please reload the page.
</Alert>
)}
</AccordionDetails>
</Accordion>
)
}

// Prevent `GovernanceSection` hooks from needlessly being called
const GovernanceSectionWrapper = () => {
const chainId = useChainId()
if (!getSafeTokenAddress(chainId)) {
return null
}

return <GovernanceSection />
}

export default GovernanceSectionWrapper
66 changes: 66 additions & 0 deletions src/components/dashboard/GovernanceSection/styles.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
.accordion {
box-shadow: none;
border: none;
background: transparent;
}

.accordion :global .MuiAccordionSummary-root,
.accordion :global .MuiAccordionDetails-root {
padding: 0;
}

.accordion:hover :global .MuiAccordionSummary-root,
.accordion :global .MuiAccordionSummary-root:hover,
.accordion :global .Mui-expanded.MuiAccordionSummary-root {
background: inherit;
}

.accordion :global .MuiAccordionSummary-root {
pointer-events: none;
}

.accordion :global .MuiAccordionSummary-expandIconWrapper {
pointer-events: auto;
}

.widgetWrapper {
border: none;
height: 300px;
}

/* iframe sm breakpoint + paddings */
@media (max-width: 662px) {
.widgetWrapper {
height: 624px;
}
}

.loadErrorCard {
display: flex;
justify-content: center;
align-items: center;
padding: var(--space-2);
text-align: center;
flex-grow: 1;
}

.loadErrorCard:last-of-type {
min-width: 300px;
}

.loadErrorMsgContainer {
height: 100%;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: var(--space-2);
max-width: 80%;
}

.loadErroricon {
font-size: 54px;
position: relative;
left: 3px;
top: 3px;
}
5 changes: 5 additions & 0 deletions src/components/dashboard/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import PendingTxsList from '@/components/dashboard/PendingTxs/PendingTxsList'
import Overview from '@/components/dashboard/Overview/Overview'
import { FeaturedApps } from '@/components/dashboard/FeaturedApps/FeaturedApps'
import SafeAppsDashboardSection from '@/components/dashboard/SafeAppsDashboardSection/SafeAppsDashboardSection'
import GovernanceSection from '@/components/dashboard/GovernanceSection/GovernanceSection'
import CreationDialog from '@/components/dashboard/CreationDialog'
import { useRouter } from 'next/router'

Expand All @@ -26,6 +27,10 @@ const Dashboard = (): ReactElement => {
<FeaturedApps />
</Grid>

<Grid item xs={12}>
<GovernanceSection />
</Grid>

<Grid item xs={12}>
<SafeAppsDashboardSection />
</Grid>
Expand Down
31 changes: 31 additions & 0 deletions src/components/safe-apps/AppFrame/SafeAppIframe.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import type { MutableRefObject, ReactElement } from 'react'
import css from './styles.module.css'

type SafeAppIFrameProps = {
appUrl: string
allowedFeaturesList: string
title?: string
iframeRef?: MutableRefObject<HTMLIFrameElement | null>
onLoad?: () => void
}

// see sandbox mdn docs for more details https://developer.mozilla.org/en-US/docs/Web/HTML/Element/iframe#attr-sandbox
const IFRAME_SANDBOX_ALLOWED_FEATURES =
'allow-scripts allow-same-origin allow-popups allow-popups-to-escape-sandbox allow-forms allow-downloads allow-orientation-lock'

const SafeAppIframe = ({ appUrl, allowedFeaturesList, iframeRef, onLoad, title }: SafeAppIFrameProps): ReactElement => {
return (
<iframe
className={css.iframe}
id={`iframe-${appUrl}`}
ref={iframeRef}
src={appUrl}
title={title}
onLoad={onLoad}
sandbox={IFRAME_SANDBOX_ALLOWED_FEATURES}
allow={allowedFeaturesList}
/>
)
}

export default SafeAppIframe
42 changes: 16 additions & 26 deletions src/components/safe-apps/AppFrame/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,11 @@ import { useSafeAppFromBackend } from '@/hooks/safe-apps/useSafeAppFromBackend'
import useChainId from '@/hooks/useChainId'
import useAddressBook from '@/hooks/useAddressBook'
import { useSafePermissions } from '@/hooks/safe-apps/permissions'
import useIsGranted from '@/hooks/useIsGranted'
import { useCurrentChain } from '@/hooks/useChains'
import { isSameUrl } from '@/utils/url'
import { isMultisigDetailedExecutionInfo } from '@/utils/transaction-guards'
import useTransactionQueueBarState from '@/components/safe-apps/AppFrame/useTransactionQueueBarState'
import { gtmTrackPageview } from '@/services/analytics/gtm'
import { getLegacyChainName } from '../utils'
import useThirdPartyCookies from './useThirdPartyCookies'
import useAnalyticsFromSafeApp from './useFromAppAnalytics'
import useAppIsLoading from './useAppIsLoading'
Expand All @@ -38,6 +36,8 @@ import PermissionsPrompt from '../PermissionsPrompt'
import { PermissionStatus } from '../types'

import css from './styles.module.css'
import SafeAppIframe from './SafeAppIframe'
import useGetSafeInfo from './useGetSafeInfo'

const UNKNOWN_APP_NAME = 'Unknown App'

Expand All @@ -46,18 +46,13 @@ type AppFrameProps = {
allowedFeaturesList: string
}

// see sandbox mdn docs for more details https://developer.mozilla.org/en-US/docs/Web/HTML/Element/iframe#attr-sandbox
const IFRAME_SANDBOX_ALLOWED_FEATURES =
'allow-scripts allow-same-origin allow-popups allow-popups-to-escape-sandbox allow-forms allow-downloads allow-orientation-lock'

const AppFrame = ({ appUrl, allowedFeaturesList }: AppFrameProps): ReactElement => {
const chainId = useChainId()
const [txModalState, openTxModal, closeTxModal] = useTxModal()
const [signMessageModalState, openSignMessageModal, closeSignMessageModal] = useSignMessageModal()
const { safe, safeLoaded, safeAddress } = useSafeInfo()
const addressBook = useAddressBook()
const chain = useCurrentChain()
const granted = useIsGranted()
const router = useRouter()
const {
expanded: queueBarExpanded,
Expand All @@ -75,6 +70,7 @@ const AppFrame = ({ appUrl, allowedFeaturesList }: AppFrameProps): ReactElement
const { getPermissions, hasPermission, permissionsRequest, setPermissionsRequest, confirmPermissionRequest } =
useSafePermissions()
const appName = useMemo(() => (remoteApp ? remoteApp.name : appUrl), [appUrl, remoteApp])

const communicator = useAppCommunicator(iframeRef, remoteApp || safeAppFromManifest, chain, {
onConfirmTransactions: openTxModal,
onSignMessage: openSignMessageModal,
Expand All @@ -91,14 +87,7 @@ const AppFrame = ({ appUrl, allowedFeaturesList }: AppFrameProps): ReactElement
onGetEnvironmentInfo: () => ({
origin: document.location.origin,
}),
onGetSafeInfo: () => ({
safeAddress,
chainId: parseInt(chainId, 10),
owners: safe.owners.map((owner) => owner.value),
threshold: safe.threshold,
isReadOnly: !granted,
network: getLegacyChainName(chain?.chainName || '', chainId).toUpperCase(),
}),
onGetSafeInfo: useGetSafeInfo(),
onGetSafeBalances: (currency) =>
getBalances(chainId, safeAddress, currency, {
exclude_spam: true,
Expand Down Expand Up @@ -216,24 +205,25 @@ const AppFrame = ({ appUrl, allowedFeaturesList }: AppFrameProps): ReactElement
</div>
)}

<iframe
className={css.iframe}
id={`iframe-${appUrl}`}
ref={iframeRef}
src={appUrl}
title={safeAppFromManifest?.name}
onLoad={onIframeLoad}
sandbox={IFRAME_SANDBOX_ALLOWED_FEATURES}
allow={allowedFeaturesList}
<div
style={{
height: '100%',
display: appIsLoading ? 'none' : 'block',
paddingBottom: queueBarVisible ? TRANSACTION_BAR_HEIGHT : 0,
}}
/>
>
<SafeAppIframe
appUrl={appUrl}
allowedFeaturesList={allowedFeaturesList}
iframeRef={iframeRef}
onLoad={onIframeLoad}
title={safeAppFromManifest?.name}
/>
</div>

<TransactionQueueBar
expanded={queueBarExpanded}
visible={!queueBarDismissed}
visible={queueBarVisible && !queueBarDismissed}
setExpanded={setExpanded}
onDismiss={dismissQueueBar}
transactions={transactions}
Expand Down
2 changes: 1 addition & 1 deletion src/components/safe-apps/AppFrame/useAppCommunicator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ type JsonRpcResponse = {
error?: string
}

type UseAppCommunicatorHandlers = {
export type UseAppCommunicatorHandlers = {
onConfirmTransactions: (txs: BaseTransaction[], requestId: RequestId, params?: SendTransactionRequestParams) => void
onSignMessage: (
message: string | EIP712TypedData,
Expand Down
Loading