mirror of
https://github.com/status-im/safe-react.git
synced 2025-02-07 15:23:50 +00:00
Feature: Fullscreen Safe apps (#2183)
* Change delete button to be in app list card * add remove modal * show button on hover
This commit is contained in:
parent
072d9d9980
commit
1f7d27e75e
@ -44,7 +44,7 @@ const SidebarWrapper = styled.aside`
|
|||||||
box-shadow: 0 2px 4px 0 rgba(40, 54, 61, 0.18);
|
box-shadow: 0 2px 4px 0 rgba(40, 54, 61, 0.18);
|
||||||
`
|
`
|
||||||
|
|
||||||
const ContentWrapper = styled.section`
|
const ContentWrapper = styled.div`
|
||||||
width: 100%;
|
width: 100%;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { isAppManifestValid } from '../utils'
|
import { isAppManifestValid } from '../utils'
|
||||||
import { SafeApp, SAFE_APP_FETCH_STATUS } from '../types.d'
|
import { SafeApp } from '../types.d'
|
||||||
|
|
||||||
describe('SafeApp manifest', () => {
|
describe('SafeApp manifest', () => {
|
||||||
it('It should return true given a manifest with mandatory values supplied', async () => {
|
it('It should return true given a manifest with mandatory values supplied', async () => {
|
||||||
|
@ -41,7 +41,7 @@ const AppDocsInfo = styled.div`
|
|||||||
}
|
}
|
||||||
`
|
`
|
||||||
|
|
||||||
export interface AddAppFormValues {
|
interface AddAppFormValues {
|
||||||
appUrl: string
|
appUrl: string
|
||||||
agreementAccepted: boolean
|
agreementAccepted: boolean
|
||||||
}
|
}
|
||||||
|
@ -1,18 +1,8 @@
|
|||||||
import React, { useState, useRef, useCallback, useEffect } from 'react'
|
import React, { useState, useRef, useCallback, useEffect } from 'react'
|
||||||
import styled from 'styled-components'
|
import styled from 'styled-components'
|
||||||
import {
|
import { FixedIcon, Loader, Title, Card } from '@gnosis.pm/safe-react-components'
|
||||||
FixedIcon,
|
|
||||||
Loader,
|
|
||||||
Title,
|
|
||||||
Text,
|
|
||||||
Card,
|
|
||||||
GenericModal,
|
|
||||||
ModalFooterConfirmation,
|
|
||||||
Menu,
|
|
||||||
ButtonLink,
|
|
||||||
} from '@gnosis.pm/safe-react-components'
|
|
||||||
import { MethodToResponse, RPCPayload } from '@gnosis.pm/safe-apps-sdk'
|
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 { useSelector } from 'react-redux'
|
||||||
import { INTERFACE_MESSAGES, Transaction, RequestId, LowercaseNetworks } from '@gnosis.pm/safe-apps-sdk-v1'
|
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 { SAFELIST_ADDRESS } from 'src/routes/routes'
|
||||||
import { isSameURL } from 'src/utils/url'
|
import { isSameURL } from 'src/utils/url'
|
||||||
import { useAnalytics, SAFE_NAVIGATION_EVENT } from 'src/utils/googleAnalytics'
|
import { useAnalytics, SAFE_NAVIGATION_EVENT } from 'src/utils/googleAnalytics'
|
||||||
import { loadFromStorage, saveToStorage } from 'src/utils/storage'
|
|
||||||
import { useAppList } from '../hooks/useAppList'
|
import { useAppList } from '../hooks/useAppList'
|
||||||
import { LoadingContainer } from 'src/components/LoaderContainer/index'
|
import { LoadingContainer } from 'src/components/LoaderContainer/index'
|
||||||
import { TIMEOUT } from 'src/utils/constants'
|
import { TIMEOUT } from 'src/utils/constants'
|
||||||
@ -36,8 +25,8 @@ import { ConfirmTxModal } from '../components/ConfirmTxModal'
|
|||||||
import { useIframeMessageHandler } from '../hooks/useIframeMessageHandler'
|
import { useIframeMessageHandler } from '../hooks/useIframeMessageHandler'
|
||||||
import { useLegalConsent } from '../hooks/useLegalConsent'
|
import { useLegalConsent } from '../hooks/useLegalConsent'
|
||||||
import LegalDisclaimer from './LegalDisclaimer'
|
import LegalDisclaimer from './LegalDisclaimer'
|
||||||
import { APPS_STORAGE_KEY, getAppInfoFromUrl } from '../utils'
|
import { getAppInfoFromUrl } from '../utils'
|
||||||
import { SafeApp, StoredSafeApp } from '../types.d'
|
import { SafeApp } from '../types.d'
|
||||||
import { useAppCommunicator } from '../communicator'
|
import { useAppCommunicator } from '../communicator'
|
||||||
|
|
||||||
const OwnerDisclaimer = styled.div`
|
const OwnerDisclaimer = styled.div`
|
||||||
@ -51,12 +40,14 @@ const OwnerDisclaimer = styled.div`
|
|||||||
const AppWrapper = styled.div`
|
const AppWrapper = styled.div`
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
height: 100%;
|
height: calc(100% + 59px);
|
||||||
|
margin: 0 -16px;
|
||||||
`
|
`
|
||||||
|
|
||||||
const StyledCard = styled(Card)`
|
const StyledCard = styled(Card)`
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
|
border-radius: 0;
|
||||||
`
|
`
|
||||||
|
|
||||||
const StyledIframe = styled.iframe<{ isLoading: boolean }>`
|
const StyledIframe = styled.iframe<{ isLoading: boolean }>`
|
||||||
@ -67,10 +58,6 @@ const StyledIframe = styled.iframe<{ isLoading: boolean }>`
|
|||||||
display: ${({ isLoading }) => (isLoading ? 'none' : 'block')};
|
display: ${({ isLoading }) => (isLoading ? 'none' : 'block')};
|
||||||
`
|
`
|
||||||
|
|
||||||
const Breadcrumb = styled.div`
|
|
||||||
height: 51px;
|
|
||||||
`
|
|
||||||
|
|
||||||
export type TransactionParams = {
|
export type TransactionParams = {
|
||||||
safeTxGas?: number
|
safeTxGas?: number
|
||||||
}
|
}
|
||||||
@ -105,16 +92,12 @@ const AppFrame = ({ appUrl }: Props): React.ReactElement => {
|
|||||||
const { consentReceived, onConsentReceipt } = useLegalConsent()
|
const { consentReceived, onConsentReceipt } = useLegalConsent()
|
||||||
const { staticAppsList } = useAppList()
|
const { staticAppsList } = useAppList()
|
||||||
|
|
||||||
const matchSafeWithAddress = useRouteMatch<{ safeAddress: string }>({ path: `${SAFELIST_ADDRESS}/:safeAddress` })
|
|
||||||
|
|
||||||
const iframeRef = useRef<HTMLIFrameElement>(null)
|
const iframeRef = useRef<HTMLIFrameElement>(null)
|
||||||
const [confirmTransactionModal, setConfirmTransactionModal] = useState<ConfirmTransactionModalState>(
|
const [confirmTransactionModal, setConfirmTransactionModal] = useState<ConfirmTransactionModalState>(
|
||||||
INITIAL_CONFIRM_TX_MODAL_STATE,
|
INITIAL_CONFIRM_TX_MODAL_STATE,
|
||||||
)
|
)
|
||||||
const [appIsLoading, setAppIsLoading] = useState<boolean>(true)
|
const [appIsLoading, setAppIsLoading] = useState<boolean>(true)
|
||||||
const [safeApp, setSafeApp] = useState<SafeApp | undefined>()
|
const [safeApp, setSafeApp] = useState<SafeApp | undefined>()
|
||||||
const [isRemoveModalOpen, setIsRemoveModalOpen] = useState(false)
|
|
||||||
const [isAppDeletable, setIsAppDeletable] = useState<boolean | undefined>()
|
|
||||||
|
|
||||||
const redirectToBalance = () => history.push(`${SAFELIST_ADDRESS}/${safeAddress}/balances`)
|
const redirectToBalance = () => history.push(`${SAFELIST_ADDRESS}/${safeAddress}/balances`)
|
||||||
const timer = useRef<number>()
|
const timer = useRef<number>()
|
||||||
@ -247,25 +230,10 @@ const AppFrame = ({ appUrl }: Props): React.ReactElement => {
|
|||||||
communicator?.send('Transaction was rejected', confirmTransactionModal.requestId, true)
|
communicator?.send('Transaction was rejected', confirmTransactionModal.requestId, true)
|
||||||
}
|
}
|
||||||
|
|
||||||
const openRemoveModal = () => setIsRemoveModalOpen(true)
|
|
||||||
|
|
||||||
const closeRemoveModal = () => setIsRemoveModalOpen(false)
|
|
||||||
|
|
||||||
const removeApp = async () => {
|
|
||||||
const persistedAppList = (await loadFromStorage<StoredSafeApp[]>(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(() => {
|
useEffect(() => {
|
||||||
const loadApp = async () => {
|
const loadApp = async () => {
|
||||||
const app = await getAppInfoFromUrl(appUrl)
|
const app = await getAppInfoFromUrl(appUrl)
|
||||||
|
|
||||||
const existsStaticApp = staticAppsList.some((staticApp) => staticApp.url === app.url)
|
|
||||||
setIsAppDeletable(!existsStaticApp)
|
|
||||||
setSafeApp(app)
|
setSafeApp(app)
|
||||||
}
|
}
|
||||||
if (staticAppsList.length) {
|
if (staticAppsList.length) {
|
||||||
@ -307,21 +275,12 @@ const AppFrame = ({ appUrl }: Props): React.ReactElement => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<AppWrapper>
|
<AppWrapper>
|
||||||
<Menu>
|
|
||||||
<Breadcrumb />
|
|
||||||
{isAppDeletable && (
|
|
||||||
<ButtonLink color="error" iconType="delete" onClick={openRemoveModal}>
|
|
||||||
Remove app
|
|
||||||
</ButtonLink>
|
|
||||||
)}
|
|
||||||
</Menu>
|
|
||||||
|
|
||||||
<StyledCard>
|
<StyledCard>
|
||||||
{appIsLoading && (
|
{appIsLoading && (
|
||||||
<LoadingContainer style={{ flexDirection: 'column' }}>
|
<LoadingContainer style={{ flexDirection: 'column' }}>
|
||||||
{appTimeout && (
|
{appTimeout && (
|
||||||
<Title size="xs">
|
<Title size="xs">
|
||||||
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.
|
||||||
</Title>
|
</Title>
|
||||||
)}
|
)}
|
||||||
<Loader size="md" />
|
<Loader size="md" />
|
||||||
@ -339,26 +298,6 @@ const AppFrame = ({ appUrl }: Props): React.ReactElement => {
|
|||||||
/>
|
/>
|
||||||
</StyledCard>
|
</StyledCard>
|
||||||
|
|
||||||
{isRemoveModalOpen && (
|
|
||||||
<GenericModal
|
|
||||||
title={
|
|
||||||
<Title size="sm" withoutMargin>
|
|
||||||
Remove app
|
|
||||||
</Title>
|
|
||||||
}
|
|
||||||
body={<Text size="md">This action will remove {safeApp.name} from the interface</Text>}
|
|
||||||
footer={
|
|
||||||
<ModalFooterConfirmation
|
|
||||||
cancelText="Cancel"
|
|
||||||
handleCancel={closeRemoveModal}
|
|
||||||
handleOk={removeApp}
|
|
||||||
okText="Remove"
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
onClose={closeRemoveModal}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<ConfirmTxModal
|
<ConfirmTxModal
|
||||||
isOpen={confirmTransactionModal.isOpen}
|
isOpen={confirmTransactionModal.isOpen}
|
||||||
app={safeApp as SafeApp}
|
app={safeApp as SafeApp}
|
||||||
|
@ -1,7 +1,16 @@
|
|||||||
import React, { useState } from 'react'
|
import React, { useState } from 'react'
|
||||||
import styled, { css } from 'styled-components'
|
import styled, { css } from 'styled-components'
|
||||||
import { useSelector } from 'react-redux'
|
import { useSelector } from 'react-redux'
|
||||||
import { GenericModal, IconText, Loader, Menu } from '@gnosis.pm/safe-react-components'
|
import {
|
||||||
|
GenericModal,
|
||||||
|
IconText,
|
||||||
|
Loader,
|
||||||
|
Menu,
|
||||||
|
Icon,
|
||||||
|
ModalFooterConfirmation,
|
||||||
|
Text,
|
||||||
|
} from '@gnosis.pm/safe-react-components'
|
||||||
|
import IconButton from '@material-ui/core/IconButton'
|
||||||
|
|
||||||
import { safeParamAddressFromStateSelector } from 'src/logic/safe/store/selectors'
|
import { safeParamAddressFromStateSelector } from 'src/logic/safe/store/selectors'
|
||||||
import AppCard from 'src/routes/safe/components/Apps/components/AppCard'
|
import AppCard from 'src/routes/safe/components/Apps/components/AppCard'
|
||||||
@ -12,6 +21,7 @@ import { SAFELIST_ADDRESS } from 'src/routes/routes'
|
|||||||
import { useAppList } from '../hooks/useAppList'
|
import { useAppList } from '../hooks/useAppList'
|
||||||
import { SAFE_APP_FETCH_STATUS, SafeApp } from '../types.d'
|
import { SAFE_APP_FETCH_STATUS, SafeApp } from '../types.d'
|
||||||
import AddAppForm from './AddAppForm'
|
import AddAppForm from './AddAppForm'
|
||||||
|
import { AppData } from '../api/fetchSafeAppsList'
|
||||||
|
|
||||||
const Wrapper = styled.div`
|
const Wrapper = styled.div`
|
||||||
height: 100%;
|
height: 100%;
|
||||||
@ -56,18 +66,41 @@ const Breadcrumb = styled.div`
|
|||||||
height: 51px;
|
height: 51px;
|
||||||
`
|
`
|
||||||
|
|
||||||
|
const IconBtn = styled(IconButton)`
|
||||||
|
position: absolute;
|
||||||
|
top: 10px;
|
||||||
|
right: 10px;
|
||||||
|
z-index: 10;
|
||||||
|
padding: 5px;
|
||||||
|
opacity: 0;
|
||||||
|
|
||||||
|
transition: opacity 0.2s ease-in-out;
|
||||||
|
`
|
||||||
|
|
||||||
|
const AppContainer = styled.div`
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
${IconBtn} {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
const isAppLoading = (app: SafeApp) => SAFE_APP_FETCH_STATUS.LOADING === app.fetchStatus
|
||||||
|
const isCustomApp = (appUrl: string, staticAppsList: AppData[]) => !staticAppsList.some(({ url }) => url === appUrl)
|
||||||
|
|
||||||
const AppsList = (): React.ReactElement => {
|
const AppsList = (): React.ReactElement => {
|
||||||
const matchSafeWithAddress = useRouteMatch<{ safeAddress: string }>({ path: `${SAFELIST_ADDRESS}/:safeAddress` })
|
const matchSafeWithAddress = useRouteMatch<{ safeAddress: string }>({ path: `${SAFELIST_ADDRESS}/:safeAddress` })
|
||||||
const safeAddress = useSelector(safeParamAddressFromStateSelector)
|
const safeAddress = useSelector(safeParamAddressFromStateSelector)
|
||||||
const { appList } = useAppList()
|
const { appList, removeApp, staticAppsList } = useAppList()
|
||||||
const [isAddAppModalOpen, setIsAddAppModalOpen] = useState<boolean>(false)
|
const [isAddAppModalOpen, setIsAddAppModalOpen] = useState<boolean>(false)
|
||||||
|
const [appToRemove, setAppToRemove] = useState<SafeApp | null>(null)
|
||||||
|
|
||||||
const openAddAppModal = () => setIsAddAppModalOpen(true)
|
const openAddAppModal = () => setIsAddAppModalOpen(true)
|
||||||
|
|
||||||
const closeAddAppModal = () => setIsAddAppModalOpen(false)
|
const closeAddAppModal = () => setIsAddAppModalOpen(false)
|
||||||
|
|
||||||
const isAppLoading = (app: SafeApp) => SAFE_APP_FETCH_STATUS.LOADING === app.fetchStatus
|
|
||||||
|
|
||||||
if (!appList.length || !safeAddress) {
|
if (!appList.length || !safeAddress) {
|
||||||
return (
|
return (
|
||||||
<LoadingContainer>
|
<LoadingContainer>
|
||||||
@ -90,9 +123,23 @@ const AppsList = (): React.ReactElement => {
|
|||||||
{appList
|
{appList
|
||||||
.filter((a) => a.fetchStatus !== SAFE_APP_FETCH_STATUS.ERROR)
|
.filter((a) => a.fetchStatus !== SAFE_APP_FETCH_STATUS.ERROR)
|
||||||
.map((a) => (
|
.map((a) => (
|
||||||
<StyledLink key={a.url} to={`${matchSafeWithAddress?.url}/apps?appUrl=${encodeURI(a.url)}`}>
|
<AppContainer key={a.url}>
|
||||||
<AppCard isLoading={isAppLoading(a)} iconUrl={a.iconUrl} name={a.name} description={a.description} />
|
<StyledLink key={a.url} to={`${matchSafeWithAddress?.url}/apps?appUrl=${encodeURI(a.url)}`}>
|
||||||
</StyledLink>
|
<AppCard isLoading={isAppLoading(a)} iconUrl={a.iconUrl} name={a.name} description={a.description} />
|
||||||
|
</StyledLink>
|
||||||
|
{isCustomApp(a.url, staticAppsList) && (
|
||||||
|
<IconBtn
|
||||||
|
title="Remove"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation()
|
||||||
|
|
||||||
|
setAppToRemove(a)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Icon size="sm" type="delete" color="error" />
|
||||||
|
</IconBtn>
|
||||||
|
)}
|
||||||
|
</AppContainer>
|
||||||
))}
|
))}
|
||||||
</CardsWrapper>
|
</CardsWrapper>
|
||||||
|
|
||||||
@ -112,6 +159,25 @@ const AppsList = (): React.ReactElement => {
|
|||||||
onClose={closeAddAppModal}
|
onClose={closeAddAppModal}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{appToRemove && (
|
||||||
|
<GenericModal
|
||||||
|
title="Remove app"
|
||||||
|
body={<Text size="md">This action will remove {appToRemove.name} from the interface</Text>}
|
||||||
|
footer={
|
||||||
|
<ModalFooterConfirmation
|
||||||
|
cancelText="Cancel"
|
||||||
|
handleCancel={() => setAppToRemove(null)}
|
||||||
|
handleOk={() => {
|
||||||
|
removeApp(appToRemove.url)
|
||||||
|
setAppToRemove(null)
|
||||||
|
}}
|
||||||
|
okText="Remove"
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
onClose={() => setAppToRemove(null)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</Wrapper>
|
</Wrapper>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { useState, useEffect } from 'react'
|
import { useState, useEffect, useCallback } from 'react'
|
||||||
import { loadFromStorage } from 'src/utils/storage'
|
import { loadFromStorage, saveToStorage } from 'src/utils/storage'
|
||||||
import { APPS_STORAGE_KEY, getAppInfoFromUrl, getAppsList, getEmptySafeApp } from '../utils'
|
import { APPS_STORAGE_KEY, getAppInfoFromUrl, getAppsList, getEmptySafeApp } from '../utils'
|
||||||
import { AppData } from '../api/fetchSafeAppsList'
|
import { AppData } from '../api/fetchSafeAppsList'
|
||||||
import { SafeApp, StoredSafeApp, SAFE_APP_FETCH_STATUS } from '../types.d'
|
import { SafeApp, StoredSafeApp, SAFE_APP_FETCH_STATUS } from '../types.d'
|
||||||
@ -7,6 +7,7 @@ import { getNetworkId } from 'src/config'
|
|||||||
|
|
||||||
type UseAppListReturnType = {
|
type UseAppListReturnType = {
|
||||||
appList: SafeApp[]
|
appList: SafeApp[]
|
||||||
|
removeApp: (appUrl: string) => void
|
||||||
staticAppsList: AppData[]
|
staticAppsList: AppData[]
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -73,14 +74,23 @@ const useAppList = (): UseAppListReturnType => {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
if (staticAppsList.length) {
|
loadApps()
|
||||||
loadApps()
|
|
||||||
}
|
|
||||||
}, [staticAppsList])
|
}, [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 {
|
return {
|
||||||
appList,
|
appList,
|
||||||
staticAppsList,
|
staticAppsList,
|
||||||
|
removeApp,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user