Skip to content
This repository has been archived by the owner on Nov 10, 2023. It is now read-only.

Commit

Permalink
Epic: Bookmarked Safe Apps (#2836)
Browse files Browse the repository at this point in the history
* useCustomApps

* move error check to the useAppList hook

* use custom field to check if the app is custom

* add comment for filter

* Feature: Pinned Safe Apps section (#2793)

* Added GA event to pin and unpin Safe Apps (#2840)
  • Loading branch information
mmv08 authored Oct 18, 2021
1 parent adcef2d commit f471491
Show file tree
Hide file tree
Showing 21 changed files with 757 additions and 420 deletions.
25 changes: 0 additions & 25 deletions config/jest/LocalStorageMock.js

This file was deleted.

4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -185,7 +185,7 @@
"abi-decoder": "^2.4.0",
"axios": "0.21.4",
"bignumber.js": "9.0.1",
"bnc-onboard": "^1.35.1",
"bnc-onboard": "~1.35.1",
"classnames": "^2.2.6",
"connected-react-router": "6.8.0",
"currency-flags": "3.2.1",
Expand Down Expand Up @@ -251,7 +251,7 @@
"@storybook/preset-create-react-app": "^3.1.5",
"@storybook/react": "6.0.0",
"@testing-library/jest-dom": "^5.11.10",
"@testing-library/react": "^12.0.0",
"@testing-library/react": "^12.1.2",
"@typechain/web3-v1": "^3.0.0",
"@types/detect-port": "^1.3.1",
"@types/express": "^4.17.13",
Expand Down
10 changes: 8 additions & 2 deletions src/components/Collapse/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -45,12 +45,18 @@ const Collapse: React.FC<CollapseProps> = ({
const [open, setOpen] = useState(defaultExpanded)

const handleClick = () => {
setOpen(!open)
setOpen((prevOpen) => !prevOpen)
}

const handleKeyDown = (event: React.KeyboardEvent<HTMLDivElement>) => {
if (event.key === 'Enter' || event.key === ' ') {
setOpen((prevOpen) => !prevOpen)
}
}

return (
<Wrapper>
<HeaderWrapper onClick={handleClick}>
<HeaderWrapper tabIndex={0} role="button" aria-pressed="false" onClick={handleClick} onKeyDown={handleKeyDown}>
<TitleWrapper>{title}</TitleWrapper>
<Header>
<IconButton disableRipple size="small">
Expand Down
15 changes: 7 additions & 8 deletions src/logic/configService/index.ts
Original file line number Diff line number Diff line change
@@ -1,22 +1,21 @@
import axios from 'axios'
import { getNetworkId } from 'src/config'
import { ETHEREUM_NETWORK } from 'src/config/networks/network'
import { CONFIG_SERVICE_URL } from 'src/utils/constants'

export type AppData = {
export type RemoteAppData = {
id: number
url: string
name?: string
disabled?: boolean
description?: string
networks: ETHEREUM_NETWORK[]
custom?: boolean
name: string
iconUrl: string
description: string
chainIds: number[]
}

enum Endpoints {
SAFE_APPS = '/safe-apps/',
}

export const fetchSafeAppsList = async (): Promise<AppData[]> => {
export const fetchSafeAppsList = async (): Promise<RemoteAppData[]> => {
const networkId = getNetworkId()
return axios.get(`${CONFIG_SERVICE_URL}${Endpoints['SAFE_APPS']}?chainId=${networkId}`).then(({ data }) => data)
}
22 changes: 7 additions & 15 deletions src/routes/safe/components/Apps/__tests__/utils.test.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,16 @@
import { isAppManifestValid } from '../utils'
import { SafeApp } from '../types'

describe('SafeApp manifest', () => {
it('It should return true given a manifest with mandatory values supplied', async () => {
const manifest = {
name: 'test',
description: 'a test',
error: false,
iconPath: 'icon.png',
providedBy: 'test',
}

const result = isAppManifestValid(manifest as SafeApp)
const result = isAppManifestValid(manifest)
expect(result).toBe(true)
})

Expand All @@ -20,7 +21,8 @@ describe('SafeApp manifest', () => {
error: false,
}

const result = isAppManifestValid(manifest as SafeApp)
// @ts-expect-error we're testing handling invalid data
const result = isAppManifestValid(manifest)
expect(result).toBe(false)
})

Expand All @@ -31,18 +33,8 @@ describe('SafeApp manifest', () => {
error: false,
}

const result = isAppManifestValid(manifest as SafeApp)
expect(result).toBe(false)
})

it('It should return false given a manifest with error', async () => {
const manifest = {
name: 'test',
description: 'a test',
error: true,
}

const result = isAppManifestValid(manifest as SafeApp)
// @ts-expect-error we're testing handling invalid data
const result = isAppManifestValid(manifest)
expect(result).toBe(false)
})
})
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@ import { ReactElement, useMemo } from 'react'
import { useFormState } from 'react-final-form'

import { Modal } from 'src/components/Modal'
import { SafeApp } from 'src/routes/safe/components/Apps/types'
import { isAppManifestValid } from 'src/routes/safe/components/Apps/utils'
import { SafeApp } from '../../types'

interface Props {
appInfo: SafeApp
Expand All @@ -19,6 +19,7 @@ export const FormButtons = ({ appInfo, onCancel }: Props): ReactElement => {
// if non visited, fields were not evaluated yet. Then, the default value is considered invalid
const fieldsVisited = visited?.agreementAccepted && visited?.appUrl

// @ts-expect-error adding this because isAppManifestValid only checks name and description which are both present in the SafeApp type
return validating || !valid || !fieldsVisited || !isAppManifestValid(appInfo)
}, [validating, valid, visited, appInfo])

Expand Down
18 changes: 6 additions & 12 deletions src/routes/safe/components/Apps/components/AddAppForm/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,15 @@ import { useSelector } from 'react-redux'
import { generatePath, useHistory } from 'react-router-dom'
import styled from 'styled-components'

import { SafeApp, StoredSafeApp } from 'src/routes/safe/components/Apps/types'
import { SafeApp } from 'src/routes/safe/components/Apps/types'
import GnoForm from 'src/components/forms/GnoForm'
import Img from 'src/components/layout/Img'
import { Modal } from 'src/components/Modal'

import AppAgreement from './AppAgreement'
import AppUrl, { AppInfoUpdater, appUrlResolver } from './AppUrl'
import { FormButtons } from './FormButtons'
import { APPS_STORAGE_KEY, getEmptySafeApp } from 'src/routes/safe/components/Apps/utils'
import { loadFromStorage, saveToStorage } from 'src/utils/storage'
import { getEmptySafeApp } from 'src/routes/safe/components/Apps/utils'
import { SAFE_ROUTES } from 'src/routes/routes'
import { Errors, logError } from 'src/logic/exceptions/CodedException'
import { safeAddressFromUrl } from 'src/logic/safe/store/selectors'
Expand Down Expand Up @@ -77,9 +76,10 @@ const DEFAULT_APP_INFO = getEmptySafeApp()
interface AddAppProps {
appList: SafeApp[]
closeModal: () => void
onAddApp: (app: SafeApp) => void
}

const AddApp = ({ appList, closeModal }: AddAppProps): ReactElement => {
const AddApp = ({ appList, closeModal, onAddApp }: AddAppProps): ReactElement => {
const safeAddress = useSelector(safeAddressFromUrl)
const appsPath = generatePath(SAFE_ROUTES.APPS, {
safeAddress,
Expand All @@ -90,16 +90,10 @@ const AddApp = ({ appList, closeModal }: AddAppProps): ReactElement => {
const [isLoading, setIsLoading] = useState(false)

const handleSubmit = useCallback(async () => {
const persistedAppList =
(await loadFromStorage<(StoredSafeApp & { disabled?: number[] })[]>(APPS_STORAGE_KEY)) || []
const newAppList = [
{ url: appInfo.url, disabled: false },
...persistedAppList.map(({ url, disabled }) => ({ url, disabled })),
]
saveToStorage(APPS_STORAGE_KEY, newAppList)
onAddApp(appInfo)
const goToApp = `${appsPath}?appUrl=${encodeURI(appInfo.url)}`
history.push(goToApp)
}, [appInfo.url, history, appsPath])
}, [history, appsPath, appInfo, onAddApp])

useEffect(() => {
if (isLoading) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,23 +1,26 @@
import AppCard from './index'

import AddAppIcon from 'src/routes/safe/components/Apps/assets/addApp.svg'
import { FETCH_STATUS } from 'src/utils/requests'
import { getEmptySafeApp } from '../../utils'
import { AppCard, AddCustomAppCard } from './index'

export default {
title: 'Apps/AppCard',
component: AppCard,
}

export const Loading = (): React.ReactElement => <AppCard isLoading />
export const Loading = (): React.ReactElement => <AppCard to="" app={getEmptySafeApp()} />

export const AddCustomApp = (): React.ReactElement => (
<AppCard iconUrl={AddAppIcon} onClick={console.log} buttonText="Add custom app" />
)
export const AddCustomApp = (): React.ReactElement => <AddCustomAppCard onClick={(): void => {}} />

export const LoadedApp = (): React.ReactElement => (
<AppCard
iconUrl="https://cryptologos.cc/logos/versions/gnosis-gno-gno-logo-circle.svg?v=007"
name="Gnosis"
description="Gnosis safe app"
onClick={console.log}
to=""
app={{
id: '228',
url: '',
name: 'Gnosis',
iconUrl: 'https://cryptologos.cc/logos/versions/gnosis-gno-gno-logo-circle.svg?v=007',
description: 'Gnosis safe app',
fetchStatus: FETCH_STATUS.SUCCESS,
}}
/>
)
Loading

0 comments on commit f471491

Please sign in to comment.