From 1f7d27e75e365277c112589d1628ee13f5b1b0dd Mon Sep 17 00:00:00 2001 From: Mikhail Mikheev Date: Wed, 21 Apr 2021 12:45:09 +0300 Subject: [PATCH] Feature: Fullscreen Safe apps (#2183) * Change delete button to be in app list card * add remove modal * show button on hover --- src/components/AppLayout/index.tsx | 2 +- .../components/Apps/__tests__/utils.test.ts | 2 +- .../Apps/components/AddAppForm/index.tsx | 2 +- .../components/Apps/components/AppFrame.tsx | 77 ++---------------- .../components/Apps/components/AppsList.tsx | 80 +++++++++++++++++-- .../safe/components/Apps/hooks/useAppList.ts | 20 +++-- 6 files changed, 99 insertions(+), 84 deletions(-) diff --git a/src/components/AppLayout/index.tsx b/src/components/AppLayout/index.tsx index 72fb20a0..79ee0a17 100644 --- a/src/components/AppLayout/index.tsx +++ b/src/components/AppLayout/index.tsx @@ -44,7 +44,7 @@ const SidebarWrapper = styled.aside` box-shadow: 0 2px 4px 0 rgba(40, 54, 61, 0.18); ` -const ContentWrapper = styled.section` +const ContentWrapper = styled.div` width: 100%; display: flex; flex-direction: column; diff --git a/src/routes/safe/components/Apps/__tests__/utils.test.ts b/src/routes/safe/components/Apps/__tests__/utils.test.ts index b2e43f87..e4349b9e 100644 --- a/src/routes/safe/components/Apps/__tests__/utils.test.ts +++ b/src/routes/safe/components/Apps/__tests__/utils.test.ts @@ -1,5 +1,5 @@ import { isAppManifestValid } from '../utils' -import { SafeApp, SAFE_APP_FETCH_STATUS } from '../types.d' +import { SafeApp } from '../types.d' describe('SafeApp manifest', () => { it('It should return true given a manifest with mandatory values supplied', async () => { diff --git a/src/routes/safe/components/Apps/components/AddAppForm/index.tsx b/src/routes/safe/components/Apps/components/AddAppForm/index.tsx index 2d8d2790..76827a62 100644 --- a/src/routes/safe/components/Apps/components/AddAppForm/index.tsx +++ b/src/routes/safe/components/Apps/components/AddAppForm/index.tsx @@ -41,7 +41,7 @@ const AppDocsInfo = styled.div` } ` -export interface AddAppFormValues { +interface AddAppFormValues { appUrl: string agreementAccepted: boolean } diff --git a/src/routes/safe/components/Apps/components/AppFrame.tsx b/src/routes/safe/components/Apps/components/AppFrame.tsx index afc39ca9..43774350 100644 --- a/src/routes/safe/components/Apps/components/AppFrame.tsx +++ b/src/routes/safe/components/Apps/components/AppFrame.tsx @@ -1,18 +1,8 @@ import React, { useState, useRef, useCallback, useEffect } from 'react' import styled from 'styled-components' -import { - FixedIcon, - Loader, - Title, - Text, - Card, - GenericModal, - ModalFooterConfirmation, - Menu, - ButtonLink, -} from '@gnosis.pm/safe-react-components' +import { FixedIcon, Loader, Title, Card } from '@gnosis.pm/safe-react-components' import { MethodToResponse, RPCPayload } from '@gnosis.pm/safe-apps-sdk' -import { useHistory, useRouteMatch } from 'react-router-dom' +import { useHistory } from 'react-router-dom' import { useSelector } from 'react-redux' import { INTERFACE_MESSAGES, Transaction, RequestId, LowercaseNetworks } from '@gnosis.pm/safe-apps-sdk-v1' @@ -26,7 +16,6 @@ import { getNetworkName, getTxServiceUrl } from 'src/config' import { SAFELIST_ADDRESS } from 'src/routes/routes' import { isSameURL } from 'src/utils/url' import { useAnalytics, SAFE_NAVIGATION_EVENT } from 'src/utils/googleAnalytics' -import { loadFromStorage, saveToStorage } from 'src/utils/storage' import { useAppList } from '../hooks/useAppList' import { LoadingContainer } from 'src/components/LoaderContainer/index' import { TIMEOUT } from 'src/utils/constants' @@ -36,8 +25,8 @@ import { ConfirmTxModal } from '../components/ConfirmTxModal' import { useIframeMessageHandler } from '../hooks/useIframeMessageHandler' import { useLegalConsent } from '../hooks/useLegalConsent' import LegalDisclaimer from './LegalDisclaimer' -import { APPS_STORAGE_KEY, getAppInfoFromUrl } from '../utils' -import { SafeApp, StoredSafeApp } from '../types.d' +import { getAppInfoFromUrl } from '../utils' +import { SafeApp } from '../types.d' import { useAppCommunicator } from '../communicator' const OwnerDisclaimer = styled.div` @@ -51,12 +40,14 @@ const OwnerDisclaimer = styled.div` const AppWrapper = styled.div` display: flex; flex-direction: column; - height: 100%; + height: calc(100% + 59px); + margin: 0 -16px; ` const StyledCard = styled(Card)` flex-grow: 1; padding: 0; + border-radius: 0; ` const StyledIframe = styled.iframe<{ isLoading: boolean }>` @@ -67,10 +58,6 @@ const StyledIframe = styled.iframe<{ isLoading: boolean }>` display: ${({ isLoading }) => (isLoading ? 'none' : 'block')}; ` -const Breadcrumb = styled.div` - height: 51px; -` - export type TransactionParams = { safeTxGas?: number } @@ -105,16 +92,12 @@ const AppFrame = ({ appUrl }: Props): React.ReactElement => { const { consentReceived, onConsentReceipt } = useLegalConsent() const { staticAppsList } = useAppList() - const matchSafeWithAddress = useRouteMatch<{ safeAddress: string }>({ path: `${SAFELIST_ADDRESS}/:safeAddress` }) - const iframeRef = useRef(null) const [confirmTransactionModal, setConfirmTransactionModal] = useState( INITIAL_CONFIRM_TX_MODAL_STATE, ) const [appIsLoading, setAppIsLoading] = useState(true) const [safeApp, setSafeApp] = useState() - const [isRemoveModalOpen, setIsRemoveModalOpen] = useState(false) - const [isAppDeletable, setIsAppDeletable] = useState() const redirectToBalance = () => history.push(`${SAFELIST_ADDRESS}/${safeAddress}/balances`) const timer = useRef() @@ -247,25 +230,10 @@ const AppFrame = ({ appUrl }: Props): React.ReactElement => { communicator?.send('Transaction was rejected', confirmTransactionModal.requestId, true) } - const openRemoveModal = () => setIsRemoveModalOpen(true) - - const closeRemoveModal = () => setIsRemoveModalOpen(false) - - const removeApp = async () => { - const persistedAppList = (await loadFromStorage(APPS_STORAGE_KEY)) || [] - const filteredList = persistedAppList.filter((a) => a.url !== safeApp?.url) - saveToStorage(APPS_STORAGE_KEY, filteredList) - - const goToApp = `${matchSafeWithAddress?.url}/apps` - history.push(goToApp) - } - useEffect(() => { const loadApp = async () => { const app = await getAppInfoFromUrl(appUrl) - const existsStaticApp = staticAppsList.some((staticApp) => staticApp.url === app.url) - setIsAppDeletable(!existsStaticApp) setSafeApp(app) } if (staticAppsList.length) { @@ -307,21 +275,12 @@ const AppFrame = ({ appUrl }: Props): React.ReactElement => { return ( - - - {isAppDeletable && ( - - Remove app - - )} - - {appIsLoading && ( {appTimeout && ( - The safe-app is taking longer than usual to load. There might be a problem with the safe-app provider. + The safe app is taking longer than usual to load. There might be a problem with the app provider. )} @@ -339,26 +298,6 @@ const AppFrame = ({ appUrl }: Props): React.ReactElement => { /> - {isRemoveModalOpen && ( - - Remove app - - } - body={This action will remove {safeApp.name} from the interface} - footer={ - - } - onClose={closeRemoveModal} - /> - )} - SAFE_APP_FETCH_STATUS.LOADING === app.fetchStatus +const isCustomApp = (appUrl: string, staticAppsList: AppData[]) => !staticAppsList.some(({ url }) => url === appUrl) + const AppsList = (): React.ReactElement => { const matchSafeWithAddress = useRouteMatch<{ safeAddress: string }>({ path: `${SAFELIST_ADDRESS}/:safeAddress` }) const safeAddress = useSelector(safeParamAddressFromStateSelector) - const { appList } = useAppList() + const { appList, removeApp, staticAppsList } = useAppList() const [isAddAppModalOpen, setIsAddAppModalOpen] = useState(false) + const [appToRemove, setAppToRemove] = useState(null) const openAddAppModal = () => setIsAddAppModalOpen(true) const closeAddAppModal = () => setIsAddAppModalOpen(false) - const isAppLoading = (app: SafeApp) => SAFE_APP_FETCH_STATUS.LOADING === app.fetchStatus - if (!appList.length || !safeAddress) { return ( @@ -90,9 +123,23 @@ const AppsList = (): React.ReactElement => { {appList .filter((a) => a.fetchStatus !== SAFE_APP_FETCH_STATUS.ERROR) .map((a) => ( - - - + + + + + {isCustomApp(a.url, staticAppsList) && ( + { + e.stopPropagation() + + setAppToRemove(a) + }} + > + + + )} + ))} @@ -112,6 +159,25 @@ const AppsList = (): React.ReactElement => { onClose={closeAddAppModal} /> )} + + {appToRemove && ( + This action will remove {appToRemove.name} from the interface} + footer={ + setAppToRemove(null)} + handleOk={() => { + removeApp(appToRemove.url) + setAppToRemove(null) + }} + okText="Remove" + /> + } + onClose={() => setAppToRemove(null)} + /> + )} ) } diff --git a/src/routes/safe/components/Apps/hooks/useAppList.ts b/src/routes/safe/components/Apps/hooks/useAppList.ts index ede9267a..7dd6d835 100644 --- a/src/routes/safe/components/Apps/hooks/useAppList.ts +++ b/src/routes/safe/components/Apps/hooks/useAppList.ts @@ -1,5 +1,5 @@ -import { useState, useEffect } from 'react' -import { loadFromStorage } from 'src/utils/storage' +import { useState, useEffect, useCallback } from 'react' +import { loadFromStorage, saveToStorage } from 'src/utils/storage' import { APPS_STORAGE_KEY, getAppInfoFromUrl, getAppsList, getEmptySafeApp } from '../utils' import { AppData } from '../api/fetchSafeAppsList' import { SafeApp, StoredSafeApp, SAFE_APP_FETCH_STATUS } from '../types.d' @@ -7,6 +7,7 @@ import { getNetworkId } from 'src/config' type UseAppListReturnType = { appList: SafeApp[] + removeApp: (appUrl: string) => void staticAppsList: AppData[] } @@ -73,14 +74,23 @@ const useAppList = (): UseAppListReturnType => { }) } - if (staticAppsList.length) { - loadApps() - } + loadApps() }, [staticAppsList]) + const removeApp = useCallback((appUrl: string): void => { + setAppList((list) => { + const newList = list.filter(({ url }) => url !== appUrl) + const persistedAppList = newList.map(({ url, disabled }) => ({ url, disabled })) + saveToStorage(APPS_STORAGE_KEY, persistedAppList) + + return newList + }) + }, []) + return { appList, staticAppsList, + removeApp, } }