From 115fd39321ced3fef29a04ddeed297b7fa00df9c Mon Sep 17 00:00:00 2001 From: nicosampler Date: Mon, 2 Nov 2020 15:35:47 -0300 Subject: [PATCH 01/39] speed-up load time for safe-apps --- .../safe/components/Apps/AddAppForm/index.tsx | 11 +- .../safe/components/Apps/hooks/useAppList.ts | 161 +++++++++++------- src/routes/safe/components/Apps/index.tsx | 17 +- src/routes/safe/components/Apps/types.d.ts | 10 +- src/routes/safe/components/Apps/utils.ts | 23 ++- 5 files changed, 139 insertions(+), 83 deletions(-) diff --git a/src/routes/safe/components/Apps/AddAppForm/index.tsx b/src/routes/safe/components/Apps/AddAppForm/index.tsx index 292f522f53..f3451b1c8c 100644 --- a/src/routes/safe/components/Apps/AddAppForm/index.tsx +++ b/src/routes/safe/components/Apps/AddAppForm/index.tsx @@ -9,7 +9,7 @@ import SubmitButtonStatus from './SubmitButtonStatus' import { SafeApp } from 'src/routes/safe/components/Apps/types.d' import GnoForm from 'src/components/forms/GnoForm' import Img from 'src/components/layout/Img' -import appsIconSvg from 'src/routes/safe/components/Transactions/TxsTable/TxType/assets/appsIcon.svg' +import { getEmptySafeApp } from '../utils' const StyledText = styled(Text)` margin-bottom: 19px; @@ -39,14 +39,7 @@ const INITIAL_VALUES: AddAppFormValues = { agreementAccepted: false, } -const APP_INFO: SafeApp = { - id: '', - url: '', - name: '', - iconUrl: appsIconSvg, - error: false, - description: '', -} +const APP_INFO = getEmptySafeApp() interface AddAppProps { appList: SafeApp[] diff --git a/src/routes/safe/components/Apps/hooks/useAppList.ts b/src/routes/safe/components/Apps/hooks/useAppList.ts index c2f13c1393..007f7672c4 100644 --- a/src/routes/safe/components/Apps/hooks/useAppList.ts +++ b/src/routes/safe/components/Apps/hooks/useAppList.ts @@ -1,9 +1,11 @@ import { useState, useEffect, useCallback } from 'react' import { loadFromStorage, saveToStorage } from 'src/utils/storage' -import { getAppInfoFromUrl, staticAppsList } from '../utils' -import { SafeApp, StoredSafeApp } from '../types' +import { getAppInfoFromUrl, getEmptySafeApp, staticAppsList } from '../utils' +import { SafeApp, StoredSafeApp, SAFE_APP_LOADING_STATUS } from '../types.d' import { getNetworkId } from 'src/config' +const loaderDotsSvg = require('src/routes/opening/assets/loader-dots.svg') + const APPS_STORAGE_KEY = 'APPS_STORAGE_KEY' type onAppToggleHandler = (appId: string, enabled: boolean) => Promise @@ -12,7 +14,7 @@ type onAppRemovedHandler = (appId: string) => void type UseAppListReturnType = { appList: SafeApp[] - loadingAppList: boolean + // loadingAppList: boolean onAppToggle: onAppToggleHandler onAppAdded: onAppAddedHandler onAppRemoved: onAppRemovedHandler @@ -20,67 +22,6 @@ type UseAppListReturnType = { const useAppList = (): UseAppListReturnType => { const [appList, setAppList] = useState([]) - const [loadingAppList, setLoadingAppList] = useState(true) - - // Load apps list - useEffect(() => { - const loadApps = async () => { - // recover apps from storage: - // * third-party apps added by the user - // * disabled status for both static and third-party apps - const persistedAppList = (await loadFromStorage(APPS_STORAGE_KEY)) || [] - let list: (StoredSafeApp & { isDeletable: boolean; networks?: number[] })[] = persistedAppList.map((a) => ({ - ...a, - isDeletable: true, - })) - - // merge stored apps with static apps (apps added manually can be deleted by the user) - staticAppsList.forEach((staticApp) => { - const app = list.find((persistedApp) => persistedApp.url === staticApp.url) - if (app) { - app.isDeletable = false - app.networks = staticApp.networks - } else { - list.push({ ...staticApp, isDeletable: false }) - } - }) - - // filter app by network - list = list.filter((app) => { - // if the app does not expose supported networks, include them. (backward compatible) - if (!app.networks) { - return true - } - return app.networks.includes(getNetworkId()) - }) - - let apps: SafeApp[] = [] - // using the appURL to recover app info - for (let index = 0; index < list.length; index++) { - try { - const currentApp = list[index] - - const appInfo: SafeApp = await getAppInfoFromUrl(currentApp.url) - if (appInfo.error) { - throw Error(`There was a problem trying to load app ${currentApp.url}`) - } - - appInfo.disabled = Boolean(currentApp.disabled) - appInfo.isDeletable = Boolean(currentApp.isDeletable) === undefined ? true : currentApp.isDeletable - - apps.push(appInfo) - } catch (error) { - console.error(error) - } - } - apps = apps.sort((a, b) => a.name.localeCompare(b.name)) - - setAppList(apps) - setLoadingAppList(false) - } - - loadApps() - }, []) const onAppToggle: onAppToggleHandler = useCallback( async (appId, enabled) => { @@ -130,9 +71,99 @@ const useAppList = (): UseAppListReturnType => { [appList], ) + // Load apps list + // for each URL we return a mocked safe-app with a loading status + // it was developed to speed up initial page load, otherwise the + // app renders a loading until all the safe-apps are fetched. + useEffect(() => { + const loadApps = async () => { + // recover apps from storage: + // * third-party apps added by the user + // * disabled status for both static and third-party apps + const persistedAppList = (await loadFromStorage(APPS_STORAGE_KEY)) || [] + let list: (StoredSafeApp & { isDeletable: boolean; networks?: number[] })[] = persistedAppList.map((a) => ({ + ...a, + isDeletable: true, + })) + + // merge stored apps with static apps (apps added manually can be deleted by the user) + staticAppsList.forEach((staticApp) => { + const app = list.find((persistedApp) => persistedApp.url === staticApp.url) + if (app) { + app.isDeletable = false + app.networks = staticApp.networks + } else { + list.push({ ...staticApp, isDeletable: false }) + } + }) + + // filter app by network + list = list.filter((app) => { + // if the app does not expose supported networks, include them. (backward compatible) + if (!app.networks) { + return true + } + return app.networks.includes(getNetworkId()) + }) + + const apps: SafeApp[] = [] + for (let index = 0; index < list.length; index++) { + const currentApp = list[index] + const appUrl = currentApp.url.trim() + + const appInfo = { + ...getEmptySafeApp(), + url: appUrl, + name: 'loading', + iconUrl: loaderDotsSvg, + } + + appInfo.disabled = Boolean(currentApp.disabled) + appInfo.isDeletable = Boolean(currentApp.isDeletable) === undefined ? true : currentApp.isDeletable + + apps.push(appInfo) + } + + setAppList(apps) + } + + if (!appList.length) { + loadApps() + } + }, [appList]) + + // fetch each safe-app + // replace real data if it was success remove it from the list otherwise + useEffect(() => { + if (!appList?.length) { + return + } + + appList + .filter((app) => app.loadingStatus === SAFE_APP_LOADING_STATUS.ADDED) + .forEach((resApp) => { + let cpApps = [...appList] + const index = appList.findIndex((currentApp) => currentApp.url === resApp.url) + cpApps[index] = { ...cpApps[index], loadingStatus: SAFE_APP_LOADING_STATUS.LOADING } + setAppList(cpApps) + + getAppInfoFromUrl(resApp.url).then((res: SafeApp) => { + if (res.error) { + // if there was an error trying to load the safe-app, remove it from the list + cpApps.splice(index, 1) + throw Error(`There was a problem trying to load app ${res.url}`) + } else { + // if the safe app was loaded correctly, update the safe-app info + cpApps[index] = res + } + cpApps = cpApps.sort((a, b) => a.name.toLowerCase().localeCompare(b.name.toLowerCase())) + setAppList(cpApps) + }) + }) + }, [appList]) + return { appList, - loadingAppList, onAppToggle, onAppAdded, onAppRemoved, diff --git a/src/routes/safe/components/Apps/index.tsx b/src/routes/safe/components/Apps/index.tsx index 64cd9b74a8..91ecff0638 100644 --- a/src/routes/safe/components/Apps/index.tsx +++ b/src/routes/safe/components/Apps/index.tsx @@ -13,7 +13,7 @@ import styled, { css } from 'styled-components' import ManageApps from './components/ManageApps' import AppFrame from './components/AppFrame' import { useAppList } from './hooks/useAppList' -import { SafeApp } from './types.d' +import { SafeApp, SAFE_APP_LOADING_STATUS } from './types.d' import LCL from 'src/components/ListContentLayout' import { grantedSelector } from 'src/routes/safe/container/selector' @@ -67,7 +67,7 @@ const INITIAL_CONFIRM_TX_MODAL_STATE: ConfirmTransactionModalState = { const NETWORK_NAME = getNetworkName() const Apps = (): React.ReactElement => { - const { appList, loadingAppList, onAppToggle, onAppAdded, onAppRemoved } = useAppList() + const { appList, onAppToggle, onAppAdded, onAppRemoved } = useAppList() const [appIsLoading, setAppIsLoading] = useState(true) const [selectedAppId, setSelectedAppId] = useState() @@ -97,7 +97,7 @@ const Apps = (): React.ReactElement => { ]) const selectedApp = useMemo(() => appList.find((app) => app.id === selectedAppId), [appList, selectedAppId]) - const enabledApps = useMemo(() => appList.filter((a) => !a.disabled), [appList]) + const enabledApps = useMemo(() => appList.filter((app) => !app.disabled), [appList]) const { sendMessageToIframe } = useIframeMessageHandler( selectedApp, openConfirmationModal, @@ -134,8 +134,15 @@ const Apps = (): React.ReactElement => { // Auto Select app first App useEffect(() => { const selectFirstEnabledApp = () => { + const areAppsLoading = appList.some((a) => + [SAFE_APP_LOADING_STATUS.LOADING, SAFE_APP_LOADING_STATUS.ADDED].includes(a.loadingStatus), + ) + if (areAppsLoading) { + return + } + const firstEnabledApp = appList.find((a) => !a.disabled) - if (firstEnabledApp) { + if (firstEnabledApp && firstEnabledApp.loadingStatus === SAFE_APP_LOADING_STATUS.SUCCESS) { setSelectedAppId(firstEnabledApp.id) } } @@ -171,7 +178,7 @@ const Apps = (): React.ReactElement => { }) }, [ethBalance, safeAddress, selectedApp, sendMessageToIframe]) - if (loadingAppList || !appList.length || !safeAddress) { + if (!appList.length || !safeAddress) { return ( diff --git a/src/routes/safe/components/Apps/types.d.ts b/src/routes/safe/components/Apps/types.d.ts index db8b49d112..27004312c3 100644 --- a/src/routes/safe/components/Apps/types.d.ts +++ b/src/routes/safe/components/Apps/types.d.ts @@ -1,3 +1,10 @@ +export enum SAFE_APP_LOADING_STATUS { + ADDED = 'ADDED', + LOADING = 'LOADING', + SUCCESS = 'SUCCESS', + ERROR = 'ERROR', +} + export type SafeApp = { id: string url: string @@ -5,8 +12,9 @@ export type SafeApp = { iconUrl: string disabled?: boolean isDeletable?: boolean - error: boolean description: string + error: boolean + loadingStatus: SAFE_APP_LOADING_STATUS } export type StoredSafeApp = { diff --git a/src/routes/safe/components/Apps/utils.ts b/src/routes/safe/components/Apps/utils.ts index 75c1b88103..11572a7941 100644 --- a/src/routes/safe/components/Apps/utils.ts +++ b/src/routes/safe/components/Apps/utils.ts @@ -1,7 +1,7 @@ import axios from 'axios' import memoize from 'lodash.memoize' -import { SafeApp } from './types.d' +import { SafeApp, SAFE_APP_LOADING_STATUS } from './types.d' import { getGnosisSafeAppsUrl } from 'src/config' import { getContentFromENS } from 'src/logic/wallets/getWeb3' @@ -111,7 +111,7 @@ export const staticAppsList: Array<{ url: string; disabled: boolean; networks: n }, ] -export const getAppInfoFromOrigin = (origin: string): Record | null => { +export const getAppInfoFromOrigin = (origin: string): { url: string; name: string } | null => { try { return JSON.parse(origin) } catch (error) { @@ -132,9 +132,25 @@ export const isAppManifestValid = (appInfo: SafeApp): boolean => // no `error` (or `error` undefined) !appInfo.error +export const getEmptySafeApp = (): SafeApp => { + return { + id: Math.random().toString(), + url: '', + name: 'unknown', + iconUrl: appsIconSvg, + error: false, + description: '', + loadingStatus: SAFE_APP_LOADING_STATUS.ADDED, + } +} + export const getAppInfoFromUrl = memoize( async (appUrl: string): Promise => { - let res = { id: '', url: appUrl, name: 'unknown', iconUrl: appsIconSvg, error: true, description: '' } + let res = { + ...getEmptySafeApp(), + error: true, + loadingStatus: SAFE_APP_LOADING_STATUS.ERROR, + } if (!appUrl?.length) { return res @@ -161,6 +177,7 @@ export const getAppInfoFromUrl = memoize( ...appInfo.data, id: JSON.stringify({ url: res.url, name: appInfo.data.name.substring(0, remainingSpace) }), error: false, + loadingStatus: SAFE_APP_LOADING_STATUS.SUCCESS, } if (appInfo.data.iconPath) { From f03a901b17046963bb6d87f9e47fc685db2b0be6 Mon Sep 17 00:00:00 2001 From: nicosampler Date: Tue, 3 Nov 2020 14:50:13 -0300 Subject: [PATCH 02/39] show loading status until all the apps are loaded --- src/routes/safe/components/Apps/hooks/useAppList.ts | 4 ---- src/routes/safe/components/Apps/index.tsx | 12 +++++++++++- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/src/routes/safe/components/Apps/hooks/useAppList.ts b/src/routes/safe/components/Apps/hooks/useAppList.ts index 007f7672c4..ea4f3c59f2 100644 --- a/src/routes/safe/components/Apps/hooks/useAppList.ts +++ b/src/routes/safe/components/Apps/hooks/useAppList.ts @@ -4,8 +4,6 @@ import { getAppInfoFromUrl, getEmptySafeApp, staticAppsList } from '../utils' import { SafeApp, StoredSafeApp, SAFE_APP_LOADING_STATUS } from '../types.d' import { getNetworkId } from 'src/config' -const loaderDotsSvg = require('src/routes/opening/assets/loader-dots.svg') - const APPS_STORAGE_KEY = 'APPS_STORAGE_KEY' type onAppToggleHandler = (appId: string, enabled: boolean) => Promise @@ -114,8 +112,6 @@ const useAppList = (): UseAppListReturnType => { const appInfo = { ...getEmptySafeApp(), url: appUrl, - name: 'loading', - iconUrl: loaderDotsSvg, } appInfo.disabled = Boolean(currentApp.disabled) diff --git a/src/routes/safe/components/Apps/index.tsx b/src/routes/safe/components/Apps/index.tsx index 91ecff0638..e049ecae8e 100644 --- a/src/routes/safe/components/Apps/index.tsx +++ b/src/routes/safe/components/Apps/index.tsx @@ -28,6 +28,8 @@ import ConfirmTransactionModal from './components/ConfirmTransactionModal' import { useAnalytics, SAFE_NAVIGATION_EVENT } from 'src/utils/googleAnalytics' import { getNetworkName } from 'src/config' +const loaderDotsSvg = require('src/routes/opening/assets/loader-dots.svg') + const centerCSS = css` display: flex; align-items: center; @@ -97,7 +99,15 @@ const Apps = (): React.ReactElement => { ]) const selectedApp = useMemo(() => appList.find((app) => app.id === selectedAppId), [appList, selectedAppId]) - const enabledApps = useMemo(() => appList.filter((app) => !app.disabled), [appList]) + const enabledApps = useMemo(() => { + const areAppsLoading = appList.some((app) => + [SAFE_APP_LOADING_STATUS.ADDED, SAFE_APP_LOADING_STATUS.LOADING].includes(app.loadingStatus), + ) + + return areAppsLoading + ? appList.map((app) => ({ ...app, name: 'Loading', iconUrl: loaderDotsSvg })) + : appList.filter((app) => !app.disabled) + }, [appList]) const { sendMessageToIframe } = useIframeMessageHandler( selectedApp, openConfirmationModal, From 89d30af123d0fa05bd20d2e2089244004dc8de00 Mon Sep 17 00:00:00 2001 From: nicosampler Date: Tue, 3 Nov 2020 14:51:46 -0300 Subject: [PATCH 03/39] remove commented code --- src/routes/safe/components/Apps/hooks/useAppList.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/routes/safe/components/Apps/hooks/useAppList.ts b/src/routes/safe/components/Apps/hooks/useAppList.ts index ea4f3c59f2..1e13065024 100644 --- a/src/routes/safe/components/Apps/hooks/useAppList.ts +++ b/src/routes/safe/components/Apps/hooks/useAppList.ts @@ -12,7 +12,6 @@ type onAppRemovedHandler = (appId: string) => void type UseAppListReturnType = { appList: SafeApp[] - // loadingAppList: boolean onAppToggle: onAppToggleHandler onAppAdded: onAppAddedHandler onAppRemoved: onAppRemovedHandler From 45c6b95cd0e3b7be9480238476db3d1dd052d87a Mon Sep 17 00:00:00 2001 From: nicosampler Date: Tue, 10 Nov 2020 15:59:26 -0300 Subject: [PATCH 04/39] Add App route --- src/routes/safe/components/App/index.tsx | 21 +++++++++++++++++++++ src/routes/safe/components/Apps/index.tsx | 22 +++++++++++++++------- src/routes/safe/container/index.tsx | 18 ++++++++++++++++-- 3 files changed, 52 insertions(+), 9 deletions(-) create mode 100644 src/routes/safe/components/App/index.tsx diff --git a/src/routes/safe/components/App/index.tsx b/src/routes/safe/components/App/index.tsx new file mode 100644 index 0000000000..ad169fb745 --- /dev/null +++ b/src/routes/safe/components/App/index.tsx @@ -0,0 +1,21 @@ +import React, { useEffect } from 'react' +import { useLocation } from 'react-router-dom' + +function useQuery() { + return new URLSearchParams(useLocation().search) +} + +const App = (): React.ReactElement => { + const query = useQuery() + const appUrl = query.get('appUrl') + + useEffect(() => { + if (!appUrl) { + throw Error('No safe-app provided') + } + }, [appUrl]) + + return
ID: {appUrl}
+} + +export default App diff --git a/src/routes/safe/components/Apps/index.tsx b/src/routes/safe/components/Apps/index.tsx index e049ecae8e..26792a6bae 100644 --- a/src/routes/safe/components/Apps/index.tsx +++ b/src/routes/safe/components/Apps/index.tsx @@ -27,6 +27,8 @@ import { useIframeMessageHandler } from './hooks/useIframeMessageHandler' import ConfirmTransactionModal from './components/ConfirmTransactionModal' import { useAnalytics, SAFE_NAVIGATION_EVENT } from 'src/utils/googleAnalytics' import { getNetworkName } from 'src/config' +import { useRouteMatch, useHistory } from 'react-router-dom' +import { SAFELIST_ADDRESS } from 'src/routes/routes' const loaderDotsSvg = require('src/routes/opening/assets/loader-dots.svg') @@ -69,6 +71,9 @@ const INITIAL_CONFIRM_TX_MODAL_STATE: ConfirmTransactionModalState = { const NETWORK_NAME = getNetworkName() const Apps = (): React.ReactElement => { + const history = useHistory() + const matchSafeWithAddress = useRouteMatch<{ safeAddress: string }>({ path: `${SAFELIST_ADDRESS}/:safeAddress` }) + const { appList, onAppToggle, onAppAdded, onAppRemoved } = useAppList() const [appIsLoading, setAppIsLoading] = useState(true) @@ -106,7 +111,7 @@ const Apps = (): React.ReactElement => { return areAppsLoading ? appList.map((app) => ({ ...app, name: 'Loading', iconUrl: loaderDotsSvg })) - : appList.filter((app) => !app.disabled) + : appList.filter((app) => !app.disabled).map((app) => ({ ...app, id: app.url })) }, [appList]) const { sendMessageToIframe } = useIframeMessageHandler( selectedApp, @@ -130,15 +135,18 @@ const Apps = (): React.ReactElement => { } const onSelectApp = useCallback( - (appId) => { - if (selectedAppId === appId) { + (appUrl) => { + if (selectedAppId === appUrl) { return } + const goToApp = `${matchSafeWithAddress?.url}/app?appUrl=${encodeURI(appUrl)}` + debugger + history.push(goToApp) - setAppIsLoading(true) - setSelectedAppId(appId) + //setAppIsLoading(true) + //setSelectedAppId(appId) }, - [selectedAppId], + [selectedAppId, history, matchSafeWithAddress], ) // Auto Select app first App @@ -153,7 +161,7 @@ const Apps = (): React.ReactElement => { const firstEnabledApp = appList.find((a) => !a.disabled) if (firstEnabledApp && firstEnabledApp.loadingStatus === SAFE_APP_LOADING_STATUS.SUCCESS) { - setSelectedAppId(firstEnabledApp.id) + setSelectedAppId(firstEnabledApp.url) } } diff --git a/src/routes/safe/container/index.tsx b/src/routes/safe/container/index.tsx index f0008b308e..773044d31e 100644 --- a/src/routes/safe/container/index.tsx +++ b/src/routes/safe/container/index.tsx @@ -20,6 +20,7 @@ export const SAFE_VIEW_NAME_HEADING_TEST_ID = 'safe-name-heading' export const TRANSACTIONS_TAB_NEW_BTN_TEST_ID = 'transactions-tab-new-btn' const Apps = React.lazy(() => import('../components/Apps')) +const App = React.lazy(() => import('../components/App')) const Settings = React.lazy(() => import('../components/Settings')) const Balances = React.lazy(() => import('../components/Balances')) const TxsTable = React.lazy(() => import('src/routes/safe/components/Transactions/TxsTable')) @@ -46,7 +47,7 @@ const Container = (): React.ReactElement => { return left === right }, ) - const matchSafeWithAddress = useRouteMatch<{ safeAddress: string }>({ path: `${SAFELIST_ADDRESS}/:safeAddress` }) + const matchSafeWithAddress = useRouteMatch({ path: `${SAFELIST_ADDRESS}/:safeAddress` }) const safeAppsEnabled = Boolean(featuresEnabled?.includes(FEATURES.SAFE_APPS)) if (!safeAddress) { @@ -90,7 +91,20 @@ const Container = (): React.ReactElement => { return wrapInSuspense(, null) }} /> - + { + // if (!safeAppsEnabled) { + // history.push(`${matchSafeWithAddress?.url}/balances`) + // } + return wrapInSuspense(, null) + }} + /> Date: Wed, 11 Nov 2020 12:08:09 -0300 Subject: [PATCH 05/39] partial imp (broken) --- .../appsIcon.svg => assets/icons/apps.svg} | 0 src/routes/safe/components/App/index.tsx | 21 ----- .../safe/components/Apps/assets/addApp.svg | 31 +++++++ .../Apps/components/AppCard/index.stories.tsx | 24 ++++++ .../Apps/components/AppCard/index.tsx | 80 +++++++++++++++++++ .../components/Apps/components/AppFrame.tsx | 42 +++++----- .../components/Apps/components/ManageApps.tsx | 2 +- .../Apps/hooks/useIframeMessageHandler.ts | 8 +- src/routes/safe/components/Apps/index.tsx | 59 ++++++++------ src/routes/safe/components/Apps/utils.ts | 2 +- .../safe/components/assets/AppsIcon.tsx | 18 ----- src/routes/safe/container/index.tsx | 29 +++---- 12 files changed, 208 insertions(+), 108 deletions(-) rename src/{routes/safe/components/Transactions/TxsTable/TxType/assets/appsIcon.svg => assets/icons/apps.svg} (100%) delete mode 100644 src/routes/safe/components/App/index.tsx create mode 100644 src/routes/safe/components/Apps/assets/addApp.svg create mode 100644 src/routes/safe/components/Apps/components/AppCard/index.stories.tsx create mode 100644 src/routes/safe/components/Apps/components/AppCard/index.tsx delete mode 100644 src/routes/safe/components/assets/AppsIcon.tsx diff --git a/src/routes/safe/components/Transactions/TxsTable/TxType/assets/appsIcon.svg b/src/assets/icons/apps.svg similarity index 100% rename from src/routes/safe/components/Transactions/TxsTable/TxType/assets/appsIcon.svg rename to src/assets/icons/apps.svg diff --git a/src/routes/safe/components/App/index.tsx b/src/routes/safe/components/App/index.tsx deleted file mode 100644 index ad169fb745..0000000000 --- a/src/routes/safe/components/App/index.tsx +++ /dev/null @@ -1,21 +0,0 @@ -import React, { useEffect } from 'react' -import { useLocation } from 'react-router-dom' - -function useQuery() { - return new URLSearchParams(useLocation().search) -} - -const App = (): React.ReactElement => { - const query = useQuery() - const appUrl = query.get('appUrl') - - useEffect(() => { - if (!appUrl) { - throw Error('No safe-app provided') - } - }, [appUrl]) - - return
ID: {appUrl}
-} - -export default App diff --git a/src/routes/safe/components/Apps/assets/addApp.svg b/src/routes/safe/components/Apps/assets/addApp.svg new file mode 100644 index 0000000000..76500778d3 --- /dev/null +++ b/src/routes/safe/components/Apps/assets/addApp.svg @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/routes/safe/components/Apps/components/AppCard/index.stories.tsx b/src/routes/safe/components/Apps/components/AppCard/index.stories.tsx new file mode 100644 index 0000000000..8b3f137da7 --- /dev/null +++ b/src/routes/safe/components/Apps/components/AppCard/index.stories.tsx @@ -0,0 +1,24 @@ +import React from 'react' + +import App from './index' + +import AddAppIcon from 'src/routes/safe/components/Apps/assets/addApp.svg' + +export default { + title: 'Apps/AppCard', + component: App, +} + +export const Loading = (): React.ReactElement => + +export const AddCustomApp = (): React.ReactElement => ( + +) + +export const LoadedApp = (): React.ReactElement => ( + +) diff --git a/src/routes/safe/components/Apps/components/AppCard/index.tsx b/src/routes/safe/components/Apps/components/AppCard/index.tsx new file mode 100644 index 0000000000..dbd62d9024 --- /dev/null +++ b/src/routes/safe/components/Apps/components/AppCard/index.tsx @@ -0,0 +1,80 @@ +import React, { SyntheticEvent } from 'react' +import styled from 'styled-components' +import { fade } from '@material-ui/core/styles/colorManipulator' +import { Title, Text, Button } from '@gnosis.pm/safe-react-components' + +import appsIconSvg from 'src/assets/icons/apps.svg' + +const StyledApps = styled.div` + display: flex; + align-items: center; + flex-direction: column; + justify-content: space-evenly; + box-shadow: 1px 2px 10px 0 ${({ theme }) => fade(theme.colors.shadow.color, 0.18)}; + border-radius: 8px; + padding: 24px; + margin: 10px; + background-color: ${({ theme }) => theme.colors.white}; + width: 244px; + height: 232px; + + :hover { + box-shadow: 1px 2px 16px 0 ${({ theme }) => fade(theme.colors.shadow.color, 0.35)}; + transition: box-shadow 0.3s ease-in-out; + cursor: pointer; + + h4 { + color: ${({ theme }) => theme.colors.primary}; + } + } +` + +const IconImg = styled.img` + width: 92px; +` + +type Props = { + isLoading?: boolean + className?: string + name?: string + description?: string + iconUrl?: string + buttonText?: string + onButtonClick?: () => void +} + +export const setAppImageFallback = (error: SyntheticEvent): void => { + error.currentTarget.onerror = null + error.currentTarget.src = appsIconSvg +} + +const Apps = ({ + isLoading = false, + className, + name, + description, + iconUrl, + buttonText, + onButtonClick, +}: Props): React.ReactElement => { + if (isLoading) { + return
skeleton
+ } + + return ( + + + + {name && {name}} + {description && {description} } + + {onButtonClick && ( + + )} + + ) +} + +export default Apps diff --git a/src/routes/safe/components/Apps/components/AppFrame.tsx b/src/routes/safe/components/Apps/components/AppFrame.tsx index 9d021a8125..258c740b63 100644 --- a/src/routes/safe/components/Apps/components/AppFrame.tsx +++ b/src/routes/safe/components/Apps/components/AppFrame.tsx @@ -4,7 +4,6 @@ import { FixedIcon, Loader, Title } from '@gnosis.pm/safe-react-components' import { useHistory } from 'react-router-dom' import { SAFELIST_ADDRESS } from 'src/routes/routes' import { useLegalConsent } from '../hooks/useLegalConsent' -import { SafeApp } from '../types' import LegalDisclaimer from './LegalDisclaimer' const StyledIframe = styled.iframe` @@ -37,7 +36,7 @@ const Centered = styled.div` ` type AppFrameProps = { - selectedApp: SafeApp | undefined + appUrl: string safeAddress: string network: string granted: boolean @@ -46,14 +45,14 @@ type AppFrameProps = { } const AppFrame = forwardRef(function AppFrameComponent( - { selectedApp, safeAddress, network, appIsLoading, granted, onIframeLoad }, + { appUrl, safeAddress, network, appIsLoading, granted, onIframeLoad }, iframeRef, ): React.ReactElement { const history = useHistory() const { consentReceived, onConsentReceipt } = useLegalConsent() const redirectToBalance = () => history.push(`${SAFELIST_ADDRESS}/${safeAddress}/balances`) - if (!selectedApp) { + if (!appUrl) { return
} @@ -71,21 +70,26 @@ const AppFrame = forwardRef(function AppFrameC } return ( - - {appIsLoading && ( - - - - )} - - + <> + {/* + + */} + + {appIsLoading && ( + + + + )} + + + ) }) diff --git a/src/routes/safe/components/Apps/components/ManageApps.tsx b/src/routes/safe/components/Apps/components/ManageApps.tsx index 26ccb49416..0ad60a986c 100644 --- a/src/routes/safe/components/Apps/components/ManageApps.tsx +++ b/src/routes/safe/components/Apps/components/ManageApps.tsx @@ -1,7 +1,7 @@ import { ButtonLink, ManageListModal } from '@gnosis.pm/safe-react-components' import React, { useState } from 'react' -import appsIconSvg from 'src/routes/safe/components/Transactions/TxsTable/TxType/assets/appsIcon.svg' +import appsIconSvg from 'src/assets/icons/apps.svg' import AddAppForm from '../AddAppForm' import { SafeApp } from '../types' diff --git a/src/routes/safe/components/Apps/hooks/useIframeMessageHandler.ts b/src/routes/safe/components/Apps/hooks/useIframeMessageHandler.ts index 3ca4844a8a..dfa75a501e 100644 --- a/src/routes/safe/components/Apps/hooks/useIframeMessageHandler.ts +++ b/src/routes/safe/components/Apps/hooks/useIframeMessageHandler.ts @@ -161,10 +161,10 @@ const useIframeMessageHandler = ( if (message.origin === window.origin) { return } - if (!selectedApp?.url.includes(message.origin)) { - console.error(`ThirdPartyApp: A message was received from an unknown origin ${message.origin}`) - return - } + // if (!selectedApp?.url.includes(message.origin)) { + // console.error(`ThirdPartyApp: A message was received from an unknown origin ${message.origin}`) + // return + // } handleIframeMessage(message.data.messageId, message.data.data, message.data.requestId) } diff --git a/src/routes/safe/components/Apps/index.tsx b/src/routes/safe/components/Apps/index.tsx index 26792a6bae..13d0622789 100644 --- a/src/routes/safe/components/Apps/index.tsx +++ b/src/routes/safe/components/Apps/index.tsx @@ -27,7 +27,7 @@ import { useIframeMessageHandler } from './hooks/useIframeMessageHandler' import ConfirmTransactionModal from './components/ConfirmTransactionModal' import { useAnalytics, SAFE_NAVIGATION_EVENT } from 'src/utils/googleAnalytics' import { getNetworkName } from 'src/config' -import { useRouteMatch, useHistory } from 'react-router-dom' +import { useRouteMatch, useHistory, useLocation } from 'react-router-dom' import { SAFELIST_ADDRESS } from 'src/routes/routes' const loaderDotsSvg = require('src/routes/opening/assets/loader-dots.svg') @@ -70,10 +70,17 @@ const INITIAL_CONFIRM_TX_MODAL_STATE: ConfirmTransactionModalState = { const NETWORK_NAME = getNetworkName() +const useQuery = () => { + return new URLSearchParams(useLocation().search) +} + const Apps = (): React.ReactElement => { const history = useHistory() const matchSafeWithAddress = useRouteMatch<{ safeAddress: string }>({ path: `${SAFELIST_ADDRESS}/:safeAddress` }) + const query = useQuery() + const appUrl = query.get('appUrl') + const { appList, onAppToggle, onAppAdded, onAppRemoved } = useAppList() const [appIsLoading, setAppIsLoading] = useState(true) @@ -104,14 +111,14 @@ const Apps = (): React.ReactElement => { ]) const selectedApp = useMemo(() => appList.find((app) => app.id === selectedAppId), [appList, selectedAppId]) - const enabledApps = useMemo(() => { + const apps = useMemo(() => { const areAppsLoading = appList.some((app) => [SAFE_APP_LOADING_STATUS.ADDED, SAFE_APP_LOADING_STATUS.LOADING].includes(app.loadingStatus), ) return areAppsLoading ? appList.map((app) => ({ ...app, name: 'Loading', iconUrl: loaderDotsSvg })) - : appList.filter((app) => !app.disabled).map((app) => ({ ...app, id: app.url })) + : appList.map((app) => ({ ...app, id: app.url })) }, [appList]) const { sendMessageToIframe } = useIframeMessageHandler( selectedApp, @@ -139,8 +146,7 @@ const Apps = (): React.ReactElement => { if (selectedAppId === appUrl) { return } - const goToApp = `${matchSafeWithAddress?.url}/app?appUrl=${encodeURI(appUrl)}` - debugger + const goToApp = `${matchSafeWithAddress?.url}/apps?appUrl=${encodeURI(appUrl)}` history.push(goToApp) //setAppIsLoading(true) @@ -181,7 +187,7 @@ const Apps = (): React.ReactElement => { const handleIframeLoad = useCallback(() => { const iframe = iframeRef.current - if (!iframe || !selectedApp || !isSameURL(iframe.src, selectedApp.url)) { + if (!iframe || !isSameURL(iframe.src, appUrl as string)) { return } @@ -194,7 +200,7 @@ const Apps = (): React.ReactElement => { ethBalance: ethBalance as string, }, }) - }, [ethBalance, safeAddress, selectedApp, sendMessageToIframe]) + }, [ethBalance, safeAddress, appUrl, sendMessageToIframe]) if (!appList.length || !safeAddress) { return ( @@ -204,27 +210,32 @@ const Apps = (): React.ReactElement => { ) } + if (appUrl) { + return ( + + ) + } + return ( <> - + {/* - - {enabledApps.length ? ( + */} + + {apps.map((a) => {}) ? ( - + - - - + ) : ( @@ -240,7 +251,7 @@ const Apps = (): React.ReactElement => { textSize="sm" /> - { onUserConfirm={onUserTxConfirm} params={confirmTransactionModal.params} onTxReject={onTxReject} - /> + /> */} ) } diff --git a/src/routes/safe/components/Apps/utils.ts b/src/routes/safe/components/Apps/utils.ts index 20d27da69b..6f4800f7e2 100644 --- a/src/routes/safe/components/Apps/utils.ts +++ b/src/routes/safe/components/Apps/utils.ts @@ -5,7 +5,7 @@ import { SafeApp, SAFE_APP_LOADING_STATUS } from './types.d' import { getGnosisSafeAppsUrl } from 'src/config' import { getContentFromENS } from 'src/logic/wallets/getWeb3' -import appsIconSvg from 'src/routes/safe/components/Transactions/TxsTable/TxType/assets/appsIcon.svg' +import appsIconSvg from 'src/assets/icons/apps.svg' import { ETHEREUM_NETWORK } from 'src/config/networks/network.d' const removeLastTrailingSlash = (url) => { diff --git a/src/routes/safe/components/assets/AppsIcon.tsx b/src/routes/safe/components/assets/AppsIcon.tsx deleted file mode 100644 index 9bd1639d8a..0000000000 --- a/src/routes/safe/components/assets/AppsIcon.tsx +++ /dev/null @@ -1,18 +0,0 @@ -import React from 'react' - -export const AppsIcon = () => ( - - - - - - - -) diff --git a/src/routes/safe/container/index.tsx b/src/routes/safe/container/index.tsx index 773044d31e..bb6836c879 100644 --- a/src/routes/safe/container/index.tsx +++ b/src/routes/safe/container/index.tsx @@ -20,7 +20,6 @@ export const SAFE_VIEW_NAME_HEADING_TEST_ID = 'safe-name-heading' export const TRANSACTIONS_TAB_NEW_BTN_TEST_ID = 'transactions-tab-new-btn' const Apps = React.lazy(() => import('../components/Apps')) -const App = React.lazy(() => import('../components/App')) const Settings = React.lazy(() => import('../components/Settings')) const Balances = React.lazy(() => import('../components/Balances')) const TxsTable = React.lazy(() => import('src/routes/safe/components/Transactions/TxsTable')) @@ -84,26 +83,16 @@ const Container = (): React.ReactElement => { { - if (!safeAppsEnabled) { - history.push(`${matchSafeWithAddress?.url}/balances`) + render={ + (/* { + history + } */) => { + // if (!safeAppsEnabled) { + // history.push(`${matchSafeWithAddress?.url}/balances`) + // } + return wrapInSuspense(, null) } - return wrapInSuspense(, null) - }} - /> - { - // if (!safeAppsEnabled) { - // history.push(`${matchSafeWithAddress?.url}/balances`) - // } - return wrapInSuspense(, null) - }} + } /> Date: Wed, 11 Nov 2020 12:45:22 -0300 Subject: [PATCH 06/39] Add cards for each safe-app --- .../Apps/components/AppCard/index.stories.tsx | 10 +++---- src/routes/safe/components/Apps/index.tsx | 29 +++++++++---------- 2 files changed, 19 insertions(+), 20 deletions(-) diff --git a/src/routes/safe/components/Apps/components/AppCard/index.stories.tsx b/src/routes/safe/components/Apps/components/AppCard/index.stories.tsx index 8b3f137da7..e37eb58afa 100644 --- a/src/routes/safe/components/Apps/components/AppCard/index.stories.tsx +++ b/src/routes/safe/components/Apps/components/AppCard/index.stories.tsx @@ -1,22 +1,22 @@ import React from 'react' -import App from './index' +import AppCard from './index' import AddAppIcon from 'src/routes/safe/components/Apps/assets/addApp.svg' export default { title: 'Apps/AppCard', - component: App, + component: AppCard, } -export const Loading = (): React.ReactElement => +export const Loading = (): React.ReactElement => export const AddCustomApp = (): React.ReactElement => ( - + ) export const LoadedApp = (): React.ReactElement => ( - { */} - {apps.map((a) => {}) ? ( - - - - - - - ) : ( - - No Apps Enabled - - )} + + + + {apps.map((a) => ( + + ))} + + Date: Wed, 11 Nov 2020 15:28:05 -0300 Subject: [PATCH 07/39] move logic to AppFrame --- .../Apps/components/AppCard/index.tsx | 4 +- .../components/Apps/components/AppFrame.tsx | 149 +++++++++++-- src/routes/safe/components/Apps/index.tsx | 201 +++--------------- 3 files changed, 164 insertions(+), 190 deletions(-) diff --git a/src/routes/safe/components/Apps/components/AppCard/index.tsx b/src/routes/safe/components/Apps/components/AppCard/index.tsx index dbd62d9024..e18b0366f3 100644 --- a/src/routes/safe/components/Apps/components/AppCard/index.tsx +++ b/src/routes/safe/components/Apps/components/AppCard/index.tsx @@ -41,6 +41,7 @@ type Props = { iconUrl?: string buttonText?: string onButtonClick?: () => void + onCardClick?: () => void } export const setAppImageFallback = (error: SyntheticEvent): void => { @@ -56,13 +57,14 @@ const Apps = ({ iconUrl, buttonText, onButtonClick, + onCardClick, }: Props): React.ReactElement => { if (isLoading) { return
skeleton
} return ( - + undefined : onCardClick}> {name && {name}} diff --git a/src/routes/safe/components/Apps/components/AppFrame.tsx b/src/routes/safe/components/Apps/components/AppFrame.tsx index 258c740b63..deb63216aa 100644 --- a/src/routes/safe/components/Apps/components/AppFrame.tsx +++ b/src/routes/safe/components/Apps/components/AppFrame.tsx @@ -1,10 +1,32 @@ -import React, { forwardRef } from 'react' +import React, { useState, useRef, useCallback, useEffect } from 'react' import styled from 'styled-components' import { FixedIcon, Loader, Title } from '@gnosis.pm/safe-react-components' import { useHistory } from 'react-router-dom' +import { useSelector } from 'react-redux' +import { + INTERFACE_MESSAGES, + Transaction, + RequestId, + LowercaseNetworks, + SendTransactionParams, +} from '@gnosis.pm/safe-apps-sdk' + +import { + safeEthBalanceSelector, + safeParamAddressFromStateSelector, + safeNameSelector, +} from 'src/logic/safe/store/selectors' +import { grantedSelector } from 'src/routes/safe/container/selector' +import { getNetworkName } from 'src/config' import { SAFELIST_ADDRESS } from 'src/routes/routes' +import { isSameURL } from 'src/utils/url' + +import ConfirmTransactionModal from '../components/ConfirmTransactionModal' +import { useIframeMessageHandler } from '../hooks/useIframeMessageHandler' import { useLegalConsent } from '../hooks/useLegalConsent' import LegalDisclaimer from './LegalDisclaimer' +import { getAppInfoFromUrl } from '../utils' +import { SafeApp } from '../types.d' const StyledIframe = styled.iframe` padding: 15px; @@ -34,33 +56,119 @@ const Centered = styled.div` justify-content: center; flex-direction: column; ` +type ConfirmTransactionModalState = { + isOpen: boolean + txs: Transaction[] + requestId?: RequestId + params?: SendTransactionParams +} -type AppFrameProps = { +type Props = { appUrl: string - safeAddress: string - network: string - granted: boolean - appIsLoading: boolean - onIframeLoad: () => void } -const AppFrame = forwardRef(function AppFrameComponent( - { appUrl, safeAddress, network, appIsLoading, granted, onIframeLoad }, - iframeRef, -): React.ReactElement { +const NETWORK_NAME = getNetworkName() + +const INITIAL_CONFIRM_TX_MODAL_STATE: ConfirmTransactionModalState = { + isOpen: false, + txs: [], + requestId: undefined, + params: undefined, +} + +const AppFrame = ({ appUrl }: Props): React.ReactElement => { + const granted = useSelector(grantedSelector) + const safeAddress = useSelector(safeParamAddressFromStateSelector) + const ethBalance = useSelector(safeEthBalanceSelector) + const safeName = useSelector(safeNameSelector) + const history = useHistory() const { consentReceived, onConsentReceipt } = useLegalConsent() + const iframeRef = useRef(null) + + const [confirmTransactionModal, setConfirmTransactionModal] = useState( + INITIAL_CONFIRM_TX_MODAL_STATE, + ) + const [appIsLoading, setAppIsLoading] = useState(true) + const [selectedApp, setSelectedApp] = useState() + const redirectToBalance = () => history.push(`${SAFELIST_ADDRESS}/${safeAddress}/balances`) + const openConfirmationModal = useCallback( + (txs: Transaction[], params: SendTransactionParams | undefined, requestId: RequestId) => + setConfirmTransactionModal({ + isOpen: true, + txs, + requestId, + params, + }), + [setConfirmTransactionModal], + ) + const closeConfirmationModal = useCallback(() => setConfirmTransactionModal(INITIAL_CONFIRM_TX_MODAL_STATE), [ + setConfirmTransactionModal, + ]) + + const { sendMessageToIframe } = useIframeMessageHandler( + selectedApp, + openConfirmationModal, + closeConfirmationModal, + iframeRef, + ) + + const onIframeLoad = useCallback(() => { + const iframe = iframeRef.current + if (!iframe || !isSameURL(iframe.src, appUrl as string)) { + return + } + + setAppIsLoading(false) + sendMessageToIframe({ + messageId: INTERFACE_MESSAGES.ON_SAFE_INFO, + data: { + safeAddress: safeAddress as string, + network: NETWORK_NAME.toLowerCase() as LowercaseNetworks, + ethBalance: ethBalance as string, + }, + }) + }, [ethBalance, safeAddress, appUrl, sendMessageToIframe]) + + const onUserTxConfirm = (safeTxHash: string) => { + sendMessageToIframe( + { messageId: INTERFACE_MESSAGES.TRANSACTION_CONFIRMED, data: { safeTxHash } }, + confirmTransactionModal.requestId, + ) + } + + const onTxReject = () => { + sendMessageToIframe( + { messageId: INTERFACE_MESSAGES.TRANSACTION_REJECTED, data: {} }, + confirmTransactionModal.requestId, + ) + } + + useEffect(() => { + const loadApp = async () => { + const app = await getAppInfoFromUrl(appUrl) + setSelectedApp(app) + } + + loadApp() + }, [appUrl]) + + // TODO check if URL if (!appUrl) { - return
+ throw Error('App url No provided or it is invalid.') + } + + if (!selectedApp) { + return
Loading...
} if (!consentReceived) { return } - if (network === 'UNKNOWN' || !granted) { + if (NETWORK_NAME === 'UNKNOWN' || !granted) { return ( @@ -89,8 +197,21 @@ const AppFrame = forwardRef(function AppFrameC onLoad={onIframeLoad} /> + + ) -}) +} export default AppFrame diff --git a/src/routes/safe/components/Apps/index.tsx b/src/routes/safe/components/Apps/index.tsx index a04dcecfc3..8d1eadcdef 100644 --- a/src/routes/safe/components/Apps/index.tsx +++ b/src/routes/safe/components/Apps/index.tsx @@ -1,34 +1,17 @@ -import React, { useCallback, useEffect, useRef, useState, useMemo } from 'react' -import { - INTERFACE_MESSAGES, - Transaction, - RequestId, - LowercaseNetworks, - SendTransactionParams, -} from '@gnosis.pm/safe-apps-sdk' -import { Card, IconText, Loader, Menu, Title } from '@gnosis.pm/safe-react-components' -import { useSelector } from 'react-redux' +import React, { useMemo } from 'react' +import { IconText, Loader } from '@gnosis.pm/safe-react-components' import styled, { css } from 'styled-components' +import { useSelector } from 'react-redux' -import ManageApps from './components/ManageApps' import AppFrame from './components/AppFrame' import { useAppList } from './hooks/useAppList' -import { SafeApp, SAFE_APP_LOADING_STATUS } from './types.d' +import { SAFE_APP_LOADING_STATUS } from './types.d' +import { safeParamAddressFromStateSelector } from 'src/logic/safe/store/selectors' import AppCard from 'src/routes/safe/components/Apps/components/AppCard' import AddAppIcon from 'src/routes/safe/components/Apps/assets/addApp.svg' -import LCL from 'src/components/ListContentLayout' -import { grantedSelector } from 'src/routes/safe/container/selector' -import { - safeEthBalanceSelector, - safeParamAddressFromStateSelector, - safeNameSelector, -} from 'src/logic/safe/store/selectors' -import { isSameURL } from 'src/utils/url' -import { useIframeMessageHandler } from './hooks/useIframeMessageHandler' -import ConfirmTransactionModal from './components/ConfirmTransactionModal' -import { useAnalytics, SAFE_NAVIGATION_EVENT } from 'src/utils/googleAnalytics' -import { getNetworkName } from 'src/config' +// import { useAnalytics /* SAFE_NAVIGATION_EVENT */ } from 'src/utils/googleAnalytics' + import { useRouteMatch, useHistory, useLocation } from 'react-router-dom' import { SAFELIST_ADDRESS } from 'src/routes/routes' @@ -57,22 +40,6 @@ const CenteredMT = styled.div` margin-top: 16px; ` -type ConfirmTransactionModalState = { - isOpen: boolean - txs: Transaction[] - requestId?: RequestId - params?: SendTransactionParams -} - -const INITIAL_CONFIRM_TX_MODAL_STATE: ConfirmTransactionModalState = { - isOpen: false, - txs: [], - requestId: undefined, - params: undefined, -} - -const NETWORK_NAME = getNetworkName() - const useQuery = () => { return new URLSearchParams(useLocation().search) } @@ -80,40 +47,15 @@ const useQuery = () => { const Apps = (): React.ReactElement => { const history = useHistory() const matchSafeWithAddress = useRouteMatch<{ safeAddress: string }>({ path: `${SAFELIST_ADDRESS}/:safeAddress` }) + const safeAddress = useSelector(safeParamAddressFromStateSelector) const query = useQuery() const appUrl = query.get('appUrl') - const { appList, onAppToggle, onAppAdded, onAppRemoved } = useAppList() - - const [appIsLoading, setAppIsLoading] = useState(true) - const [selectedAppId, setSelectedAppId] = useState() - const [confirmTransactionModal, setConfirmTransactionModal] = useState( - INITIAL_CONFIRM_TX_MODAL_STATE, - ) - const iframeRef = useRef(null) - - const { trackEvent } = useAnalytics() - const granted = useSelector(grantedSelector) - const safeAddress = useSelector(safeParamAddressFromStateSelector) - const safeName = useSelector(safeNameSelector) - const ethBalance = useSelector(safeEthBalanceSelector) + const { appList /* onAppAdded, onAppRemoved */ } = useAppList() - const openConfirmationModal = useCallback( - (txs: Transaction[], params: SendTransactionParams | undefined, requestId: RequestId) => - setConfirmTransactionModal({ - isOpen: true, - txs, - requestId, - params, - }), - [setConfirmTransactionModal], - ) - const closeConfirmationModal = useCallback(() => setConfirmTransactionModal(INITIAL_CONFIRM_TX_MODAL_STATE), [ - setConfirmTransactionModal, - ]) + // const { trackEvent } = useAnalytics() - const selectedApp = useMemo(() => appList.find((app) => app.id === selectedAppId), [appList, selectedAppId]) const apps = useMemo(() => { const areAppsLoading = appList.some((app) => [SAFE_APP_LOADING_STATUS.ADDED, SAFE_APP_LOADING_STATUS.LOADING].includes(app.loadingStatus), @@ -123,87 +65,13 @@ const Apps = (): React.ReactElement => { ? appList.map((app) => ({ ...app, name: 'Loading', iconUrl: loaderDotsSvg })) : appList.map((app) => ({ ...app, id: app.url })) }, [appList]) - const { sendMessageToIframe } = useIframeMessageHandler( - selectedApp, - openConfirmationModal, - closeConfirmationModal, - iframeRef, - ) - - const onUserTxConfirm = (safeTxHash: string) => { - sendMessageToIframe( - { messageId: INTERFACE_MESSAGES.TRANSACTION_CONFIRMED, data: { safeTxHash } }, - confirmTransactionModal.requestId, - ) - } - - const onTxReject = () => { - sendMessageToIframe( - { messageId: INTERFACE_MESSAGES.TRANSACTION_REJECTED, data: {} }, - confirmTransactionModal.requestId, - ) - } - - const onSelectApp = useCallback( - (appUrl) => { - if (selectedAppId === appUrl) { - return - } - const goToApp = `${matchSafeWithAddress?.url}/apps?appUrl=${encodeURI(appUrl)}` - history.push(goToApp) - - //setAppIsLoading(true) - //setSelectedAppId(appId) - }, - [selectedAppId, history, matchSafeWithAddress], - ) - - // Auto Select app first App - useEffect(() => { - const selectFirstEnabledApp = () => { - const areAppsLoading = appList.some((a) => - [SAFE_APP_LOADING_STATUS.LOADING, SAFE_APP_LOADING_STATUS.ADDED].includes(a.loadingStatus), - ) - if (areAppsLoading) { - return - } - - const firstEnabledApp = appList.find((a) => !a.disabled) - if (firstEnabledApp && firstEnabledApp.loadingStatus === SAFE_APP_LOADING_STATUS.SUCCESS) { - setSelectedAppId(firstEnabledApp.url) - } - } - - const initialSelect = appList.length && !selectedAppId - const currentAppWasDisabled = selectedApp?.disabled - if (initialSelect || currentAppWasDisabled) { - selectFirstEnabledApp() - } - }, [appList, selectedApp, selectedAppId, trackEvent]) // track GA - useEffect(() => { - if (selectedApp) { - trackEvent({ category: SAFE_NAVIGATION_EVENT, action: 'Apps', label: selectedApp.name }) - } - }, [selectedApp, trackEvent]) - - const handleIframeLoad = useCallback(() => { - const iframe = iframeRef.current - if (!iframe || !isSameURL(iframe.src, appUrl as string)) { - return - } - - setAppIsLoading(false) - sendMessageToIframe({ - messageId: INTERFACE_MESSAGES.ON_SAFE_INFO, - data: { - safeAddress: safeAddress as string, - network: NETWORK_NAME.toLowerCase() as LowercaseNetworks, - ethBalance: ethBalance as string, - }, - }) - }, [ethBalance, safeAddress, appUrl, sendMessageToIframe]) + // useEffect(() => { + // if (selectedApp) { + // trackEvent({ category: SAFE_NAVIGATION_EVENT, action: 'Apps', label: selectedApp.name }) + // } + // }, [selectedApp, trackEvent]) if (!appList.length || !safeAddress) { return ( @@ -214,30 +82,25 @@ const Apps = (): React.ReactElement => { } if (appUrl) { - return ( - - ) + return } return ( <> - {/* - - */} - {apps.map((a) => ( - + { + const goToApp = `${matchSafeWithAddress?.url}/apps?appUrl=${encodeURI(a.url)}` + history.push(goToApp) + }} + /> ))} @@ -250,18 +113,6 @@ const Apps = (): React.ReactElement => { textSize="sm" /> - {/* */} ) } From 1220f6c99859586854e41ad4c8292cee28da350d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Agust=C3=ADn=20Longoni?= Date: Wed, 11 Nov 2020 16:00:26 -0300 Subject: [PATCH 08/39] add basic skeleton styles --- .../Apps/components/AppCard/index.tsx | 32 +++++++++++++++++-- 1 file changed, 30 insertions(+), 2 deletions(-) diff --git a/src/routes/safe/components/Apps/components/AppCard/index.tsx b/src/routes/safe/components/Apps/components/AppCard/index.tsx index dbd62d9024..5837700b24 100644 --- a/src/routes/safe/components/Apps/components/AppCard/index.tsx +++ b/src/routes/safe/components/Apps/components/AppCard/index.tsx @@ -28,7 +28,26 @@ const StyledApps = styled.div` } } ` - +const Skeleton = styled.div`` +const AppIconSK = styled.div` + height: 60px; + width: 60px; + border-radius: 30px; + margin: 0 auto; + background-color: lightgrey; +` +const TitleSK = styled.div` + height: 24px; + width: 160px; + margin: 24px auto; + background-color: lightgrey; +` +const DescriptionSK = styled.div` + height: 16px; + width: 200px; + margin: 8px auto; + background-color: lightgrey; +` const IconImg = styled.img` width: 92px; ` @@ -58,7 +77,16 @@ const Apps = ({ onButtonClick, }: Props): React.ReactElement => { if (isLoading) { - return
skeleton
+ return ( + + + + + + + + + ) } return ( From 1fbb740c5563791981725f8e926c77dc91cc0f34 Mon Sep 17 00:00:00 2001 From: nicosampler Date: Wed, 11 Nov 2020 17:28:15 -0300 Subject: [PATCH 09/39] css fixes --- .../components/Apps/components/AppFrame.tsx | 20 +++++++++++++++---- src/routes/safe/components/Apps/index.tsx | 11 ++++++++-- 2 files changed, 25 insertions(+), 6 deletions(-) diff --git a/src/routes/safe/components/Apps/components/AppFrame.tsx b/src/routes/safe/components/Apps/components/AppFrame.tsx index deb63216aa..0ab3dd8612 100644 --- a/src/routes/safe/components/Apps/components/AppFrame.tsx +++ b/src/routes/safe/components/Apps/components/AppFrame.tsx @@ -1,6 +1,6 @@ import React, { useState, useRef, useCallback, useEffect } from 'react' import styled from 'styled-components' -import { FixedIcon, Loader, Title } from '@gnosis.pm/safe-react-components' +import { FixedIcon, Loader, Title, Card } from '@gnosis.pm/safe-react-components' import { useHistory } from 'react-router-dom' import { useSelector } from 'react-redux' import { @@ -56,6 +56,14 @@ const Centered = styled.div` justify-content: center; flex-direction: column; ` + +const ContentWrapper = styled(Card)` + height: calc(100% - 73px); + width: calc(100% - 50px); + margin: 20px 0; + overflow: auto; +` + type ConfirmTransactionModalState = { isOpen: boolean txs: Transaction[] @@ -161,7 +169,11 @@ const AppFrame = ({ appUrl }: Props): React.ReactElement => { } if (!selectedApp) { - return
Loading...
+ return ( + + + + ) } if (!consentReceived) { @@ -178,7 +190,7 @@ const AppFrame = ({ appUrl }: Props): React.ReactElement => { } return ( - <> + {/* */} @@ -210,7 +222,7 @@ const AppFrame = ({ appUrl }: Props): React.ReactElement => { params={confirmTransactionModal.params} onTxReject={onTxReject} /> - + ) } diff --git a/src/routes/safe/components/Apps/index.tsx b/src/routes/safe/components/Apps/index.tsx index 8d1eadcdef..a25ea50994 100644 --- a/src/routes/safe/components/Apps/index.tsx +++ b/src/routes/safe/components/Apps/index.tsx @@ -40,6 +40,13 @@ const CenteredMT = styled.div` margin-top: 16px; ` +const ContentWrapper = styled.div` + height: 100%; + display: flex; + flex-direction: column; + justify-content: space-between; +` + const useQuery = () => { return new URLSearchParams(useLocation().search) } @@ -86,7 +93,7 @@ const Apps = (): React.ReactElement => { } return ( - <> + @@ -113,7 +120,7 @@ const Apps = (): React.ReactElement => { textSize="sm" /> - + ) } From fc2d00a4764bb186185b0ff082b5cec773ffa5e1 Mon Sep 17 00:00:00 2001 From: nicosampler Date: Wed, 11 Nov 2020 17:43:47 -0300 Subject: [PATCH 10/39] fix image sizes --- .../safe/components/Apps/components/AppCard/index.tsx | 10 +++++++--- src/routes/safe/components/Apps/index.tsx | 2 +- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/src/routes/safe/components/Apps/components/AppCard/index.tsx b/src/routes/safe/components/Apps/components/AppCard/index.tsx index e18b0366f3..8fccdf055b 100644 --- a/src/routes/safe/components/Apps/components/AppCard/index.tsx +++ b/src/routes/safe/components/Apps/components/AppCard/index.tsx @@ -29,8 +29,10 @@ const StyledApps = styled.div` } ` -const IconImg = styled.img` - width: 92px; +const IconImg = styled.img<{ size: 'md' | 'lg' }>` + width: ${({ size }) => (size === 'md' ? '48px' : '102px')}; + height: ${({ size }) => (size === 'md' ? '60px' : '92px')}; + object-fit: contain; ` type Props = { @@ -39,6 +41,7 @@ type Props = { name?: string description?: string iconUrl?: string + iconSize?: 'md' | 'lg' buttonText?: string onButtonClick?: () => void onCardClick?: () => void @@ -55,6 +58,7 @@ const Apps = ({ name, description, iconUrl, + iconSize = 'md', buttonText, onButtonClick, onCardClick, @@ -65,7 +69,7 @@ const Apps = ({ return ( undefined : onCardClick}> - + {name && {name}} {description && {description} } diff --git a/src/routes/safe/components/Apps/index.tsx b/src/routes/safe/components/Apps/index.tsx index a25ea50994..b83116f198 100644 --- a/src/routes/safe/components/Apps/index.tsx +++ b/src/routes/safe/components/Apps/index.tsx @@ -95,7 +95,7 @@ const Apps = (): React.ReactElement => { return ( - + {apps.map((a) => ( Date: Thu, 12 Nov 2020 11:21:38 -0300 Subject: [PATCH 11/39] add transition animation to skeleton --- .../Apps/components/AppCard/index.tsx | 169 +++++++++++++++++- 1 file changed, 160 insertions(+), 9 deletions(-) diff --git a/src/routes/safe/components/Apps/components/AppCard/index.tsx b/src/routes/safe/components/Apps/components/AppCard/index.tsx index 73373a3abc..716fafb1e5 100644 --- a/src/routes/safe/components/Apps/components/AppCard/index.tsx +++ b/src/routes/safe/components/Apps/components/AppCard/index.tsx @@ -35,26 +35,179 @@ const IconImg = styled.img<{ size: 'md' | 'lg' }>` object-fit: contain; ` -const Skeleton = styled.div`` - const AppIconSK = styled.div` height: 60px; width: 60px; border-radius: 30px; margin: 0 auto; background-color: lightgrey; + background: linear-gradient(84deg, lightgrey, transparent); + background-size: 400% 400%; + + -webkit-animation: gradient-SK 1.5s ease infinite; + -moz-animation: gradient-SK 1.5s ease infinite; + -o-animation: gradient-SK 1.5s ease infinite; + animation: gradient-SK 1.5s ease infinite; + + @-webkit-keyframes gradient-SK { + 0% { + background-position: 0% 54%; + } + 50% { + background-position: 100% 47%; + } + 100% { + background-position: 0% 54%; + } + } + @-moz-keyframes gradient-SK { + 0% { + background-position: 0% 54%; + } + 50% { + background-position: 100% 47%; + } + 100% { + background-position: 0% 54%; + } + } + @-o-keyframes gradient-SK { + 0% { + background-position: 0% 54%; + } + 50% { + background-position: 100% 47%; + } + 100% { + background-position: 0% 54%; + } + } + @keyframes gradient-SK { + 0% { + background-position: 0% 54%; + } + 50% { + background-position: 100% 47%; + } + 100% { + background-position: 0% 54%; + } + } ` const TitleSK = styled.div` height: 24px; width: 160px; margin: 24px auto; background-color: lightgrey; + background: linear-gradient(84deg, lightgrey, transparent); + background-size: 400% 400%; + + -webkit-animation: gradient-SK 1.5s ease infinite; + -moz-animation: gradient-SK 1.5s ease infinite; + -o-animation: gradient-SK 1.5s ease infinite; + animation: gradient-SK 1.5s ease infinite; + + @-webkit-keyframes gradient-SK { + 0% { + background-position: 0% 54%; + } + 50% { + background-position: 100% 47%; + } + 100% { + background-position: 0% 54%; + } + } + @-moz-keyframes gradient-SK { + 0% { + background-position: 0% 54%; + } + 50% { + background-position: 100% 47%; + } + 100% { + background-position: 0% 54%; + } + } + @-o-keyframes gradient-SK { + 0% { + background-position: 0% 54%; + } + 50% { + background-position: 100% 47%; + } + 100% { + background-position: 0% 54%; + } + } + @keyframes gradient-SK { + 0% { + background-position: 0% 54%; + } + 50% { + background-position: 100% 47%; + } + 100% { + background-position: 0% 54%; + } + } ` const DescriptionSK = styled.div` height: 16px; width: 200px; - margin: 8px auto; background-color: lightgrey; + background: linear-gradient(84deg, lightgrey, transparent); + background-size: 400% 400%; + + -webkit-animation: gradient-SK 1.5s ease infinite; + -moz-animation: gradient-SK 1.5s ease infinite; + -o-animation: gradient-SK 1.5s ease infinite; + animation: gradient-SK 1.5s ease infinite; + + @-webkit-keyframes gradient-SK { + 0% { + background-position: 0% 54%; + } + 50% { + background-position: 100% 47%; + } + 100% { + background-position: 0% 54%; + } + } + @-moz-keyframes gradient-SK { + 0% { + background-position: 0% 54%; + } + 50% { + background-position: 100% 47%; + } + 100% { + background-position: 0% 54%; + } + } + @-o-keyframes gradient-SK { + 0% { + background-position: 0% 54%; + } + 50% { + background-position: 100% 47%; + } + 100% { + background-position: 0% 54%; + } + } + @keyframes gradient-SK { + 0% { + background-position: 0% 54%; + } + 50% { + background-position: 100% 47%; + } + 100% { + background-position: 0% 54%; + } + } ` type Props = { @@ -88,12 +241,10 @@ const Apps = ({ if (isLoading) { return ( - - - - - - + + + + ) } From 1562e108394dd995b18336ff259e898acab9cffb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Agust=C3=ADn=20Longoni?= Date: Thu, 12 Nov 2020 11:23:29 -0300 Subject: [PATCH 12/39] remove duplicated code --- .../Apps/components/AppCard/index.tsx | 90 ------------------- 1 file changed, 90 deletions(-) diff --git a/src/routes/safe/components/Apps/components/AppCard/index.tsx b/src/routes/safe/components/Apps/components/AppCard/index.tsx index 716fafb1e5..67e2f6c084 100644 --- a/src/routes/safe/components/Apps/components/AppCard/index.tsx +++ b/src/routes/safe/components/Apps/components/AppCard/index.tsx @@ -106,51 +106,6 @@ const TitleSK = styled.div` -moz-animation: gradient-SK 1.5s ease infinite; -o-animation: gradient-SK 1.5s ease infinite; animation: gradient-SK 1.5s ease infinite; - - @-webkit-keyframes gradient-SK { - 0% { - background-position: 0% 54%; - } - 50% { - background-position: 100% 47%; - } - 100% { - background-position: 0% 54%; - } - } - @-moz-keyframes gradient-SK { - 0% { - background-position: 0% 54%; - } - 50% { - background-position: 100% 47%; - } - 100% { - background-position: 0% 54%; - } - } - @-o-keyframes gradient-SK { - 0% { - background-position: 0% 54%; - } - 50% { - background-position: 100% 47%; - } - 100% { - background-position: 0% 54%; - } - } - @keyframes gradient-SK { - 0% { - background-position: 0% 54%; - } - 50% { - background-position: 100% 47%; - } - 100% { - background-position: 0% 54%; - } - } ` const DescriptionSK = styled.div` height: 16px; @@ -163,51 +118,6 @@ const DescriptionSK = styled.div` -moz-animation: gradient-SK 1.5s ease infinite; -o-animation: gradient-SK 1.5s ease infinite; animation: gradient-SK 1.5s ease infinite; - - @-webkit-keyframes gradient-SK { - 0% { - background-position: 0% 54%; - } - 50% { - background-position: 100% 47%; - } - 100% { - background-position: 0% 54%; - } - } - @-moz-keyframes gradient-SK { - 0% { - background-position: 0% 54%; - } - 50% { - background-position: 100% 47%; - } - 100% { - background-position: 0% 54%; - } - } - @-o-keyframes gradient-SK { - 0% { - background-position: 0% 54%; - } - 50% { - background-position: 100% 47%; - } - 100% { - background-position: 0% 54%; - } - } - @keyframes gradient-SK { - 0% { - background-position: 0% 54%; - } - 50% { - background-position: 100% 47%; - } - 100% { - background-position: 0% 54%; - } - } ` type Props = { From 164d7ce47f690baf5eb4422f0eefa5e82f469806 Mon Sep 17 00:00:00 2001 From: nicosampler Date: Thu, 12 Nov 2020 17:40:26 -0300 Subject: [PATCH 13/39] Modals --- .../Apps/AddAppForm/SubmitButtonStatus.tsx | 27 ---- .../AddAppForm/AppAgreement.tsx | 0 .../{ => components}/AddAppForm/AppUrl.tsx | 0 .../AddAppForm/SubmitButtonStatus.tsx | 51 ++++++ .../{ => components}/AddAppForm/index.tsx | 46 +++--- .../Apps/components/AppCard/index.tsx | 16 +- .../components/Apps/components/AppFrame.tsx | 150 +++++++++++++----- .../components/Apps/components/AppsList.tsx | 125 +++++++++++++++ .../components/Apps/components/ManageApps.tsx | 75 --------- .../safe/components/Apps/hooks/useAppList.ts | 77 +-------- src/routes/safe/components/Apps/index.tsx | 114 +------------ src/routes/safe/components/Apps/types.d.ts | 2 - src/routes/safe/components/Apps/utils.ts | 11 +- src/routes/safe/container/index.tsx | 32 ++-- 14 files changed, 350 insertions(+), 376 deletions(-) delete mode 100644 src/routes/safe/components/Apps/AddAppForm/SubmitButtonStatus.tsx rename src/routes/safe/components/Apps/{ => components}/AddAppForm/AppAgreement.tsx (100%) rename src/routes/safe/components/Apps/{ => components}/AddAppForm/AppUrl.tsx (100%) create mode 100644 src/routes/safe/components/Apps/components/AddAppForm/SubmitButtonStatus.tsx rename src/routes/safe/components/Apps/{ => components}/AddAppForm/index.tsx (56%) create mode 100644 src/routes/safe/components/Apps/components/AppsList.tsx delete mode 100644 src/routes/safe/components/Apps/components/ManageApps.tsx diff --git a/src/routes/safe/components/Apps/AddAppForm/SubmitButtonStatus.tsx b/src/routes/safe/components/Apps/AddAppForm/SubmitButtonStatus.tsx deleted file mode 100644 index b79a888fcf..0000000000 --- a/src/routes/safe/components/Apps/AddAppForm/SubmitButtonStatus.tsx +++ /dev/null @@ -1,27 +0,0 @@ -import React from 'react' -import { useFormState } from 'react-final-form' - -import { SafeApp } from 'src/routes/safe/components/Apps/types.d' -import { isAppManifestValid } from 'src/routes/safe/components/Apps/utils' - -interface SubmitButtonStatusProps { - appInfo: SafeApp - onSubmitButtonStatusChange: (disabled: boolean) => void -} - -const SubmitButtonStatus = ({ appInfo, onSubmitButtonStatusChange }: SubmitButtonStatusProps): null => { - const { valid, validating, visited } = useFormState({ - subscription: { valid: true, validating: true, visited: true }, - }) - - React.useEffect(() => { - // if non visited, fields were not evaluated yet. Then, the default value is considered invalid - const fieldsVisited = visited?.agreementAccepted && visited.appUrl - - onSubmitButtonStatusChange(validating || !valid || !fieldsVisited || !isAppManifestValid(appInfo)) - }, [validating, valid, visited, onSubmitButtonStatusChange, appInfo]) - - return null -} - -export default SubmitButtonStatus diff --git a/src/routes/safe/components/Apps/AddAppForm/AppAgreement.tsx b/src/routes/safe/components/Apps/components/AddAppForm/AppAgreement.tsx similarity index 100% rename from src/routes/safe/components/Apps/AddAppForm/AppAgreement.tsx rename to src/routes/safe/components/Apps/components/AddAppForm/AppAgreement.tsx diff --git a/src/routes/safe/components/Apps/AddAppForm/AppUrl.tsx b/src/routes/safe/components/Apps/components/AddAppForm/AppUrl.tsx similarity index 100% rename from src/routes/safe/components/Apps/AddAppForm/AppUrl.tsx rename to src/routes/safe/components/Apps/components/AddAppForm/AppUrl.tsx diff --git a/src/routes/safe/components/Apps/components/AddAppForm/SubmitButtonStatus.tsx b/src/routes/safe/components/Apps/components/AddAppForm/SubmitButtonStatus.tsx new file mode 100644 index 0000000000..b0248076c4 --- /dev/null +++ b/src/routes/safe/components/Apps/components/AddAppForm/SubmitButtonStatus.tsx @@ -0,0 +1,51 @@ +import { Button, Divider } from '@gnosis.pm/safe-react-components' +import React, { ReactElement, useMemo } from 'react' +import { useFormState } from 'react-final-form' +import styled from 'styled-components' + +import GnoButton from 'src/components/layout/Button' +import { SafeApp } from 'src/routes/safe/components/Apps/types.d' +import { isAppManifestValid } from 'src/routes/safe/components/Apps/utils' + +const StyledDivider = styled(Divider)` + margin: 16px -24px; +` + +const ButtonsContainer = styled.div` + display: flex; + justify-content: space-between; +` + +interface SubmitButtonStatusProps { + appInfo: SafeApp + onCancel: () => void +} + +const SubmitButtonStatus = ({ appInfo, onCancel }: SubmitButtonStatusProps): ReactElement => { + const { valid, validating, visited } = useFormState({ + subscription: { valid: true, validating: true, visited: true }, + }) + + const isSubmitDisabled = useMemo(() => { + // if non visited, fields were not evaluated yet. Then, the default value is considered invalid + const fieldsVisited = visited?.agreementAccepted && visited.appUrl + + return validating || !valid || !fieldsVisited || !isAppManifestValid(appInfo) + }, [validating, valid, visited, appInfo]) + + return ( + <> + + + + + Add + + + + ) +} + +export default SubmitButtonStatus diff --git a/src/routes/safe/components/Apps/AddAppForm/index.tsx b/src/routes/safe/components/Apps/components/AddAppForm/index.tsx similarity index 56% rename from src/routes/safe/components/Apps/AddAppForm/index.tsx rename to src/routes/safe/components/Apps/components/AddAppForm/index.tsx index f3451b1c8c..8483cacc35 100644 --- a/src/routes/safe/components/Apps/AddAppForm/index.tsx +++ b/src/routes/safe/components/Apps/components/AddAppForm/index.tsx @@ -1,19 +1,20 @@ -import { Text, TextField } from '@gnosis.pm/safe-react-components' -import React from 'react' +import { TextField } from '@gnosis.pm/safe-react-components' +import React, { useState, ReactElement } from 'react' import styled from 'styled-components' -import AppAgreement from './AppAgreement' -import AppUrl, { AppInfoUpdater, appUrlResolver } from './AppUrl' -import SubmitButtonStatus from './SubmitButtonStatus' - import { SafeApp } from 'src/routes/safe/components/Apps/types.d' import GnoForm from 'src/components/forms/GnoForm' import Img from 'src/components/layout/Img' -import { getEmptySafeApp } from '../utils' -const StyledText = styled(Text)` - margin-bottom: 19px; -` +import AppAgreement from './AppAgreement' +import AppUrl, { AppInfoUpdater, appUrlResolver } from './AppUrl' +import SubmitButtonStatus from './SubmitButtonStatus' +import { APPS_STORAGE_KEY, getEmptySafeApp } from '../../utils' +import { saveToStorage } from 'src/utils/storage' +import { SAFELIST_ADDRESS } from 'src/routes/routes' +import { useHistory, useRouteMatch } from 'react-router-dom' + +const FORM_ID = 'add-apps-form' const StyledTextFileAppName = styled(TextField)` && { @@ -44,26 +45,29 @@ const APP_INFO = getEmptySafeApp() interface AddAppProps { appList: SafeApp[] closeModal: () => void - formId: string - onAppAdded: (app: SafeApp) => void - setIsSubmitDisabled: (disabled: boolean) => void } -const AddApp = ({ appList, closeModal, formId, onAppAdded, setIsSubmitDisabled }: AddAppProps): React.ReactElement => { - const [appInfo, setAppInfo] = React.useState(APP_INFO) +const AddApp = ({ appList, closeModal }: AddAppProps): ReactElement => { + const [appInfo, setAppInfo] = useState(APP_INFO) + const history = useHistory() + const matchSafeWithAddress = useRouteMatch<{ safeAddress: string }>({ path: `${SAFELIST_ADDRESS}/:safeAddress` }) const handleSubmit = () => { - closeModal() - onAppAdded(appInfo) + const newAppList = [ + { url: appInfo.url, disabled: false }, + ...appList.map(({ url, disabled }) => ({ url, disabled })), + ] + saveToStorage(APPS_STORAGE_KEY, newAppList) + const goToApp = `${matchSafeWithAddress?.url}/apps?appUrl=${encodeURI(appInfo.url)}` + history.push(goToApp) } return ( - + {() => ( <> - Add custom app - + {/* Fetch app from url and return a SafeApp */} @@ -74,7 +78,7 @@ const AddApp = ({ appList, closeModal, formId, onAppAdded, setIsSubmitDisabled } - + )} diff --git a/src/routes/safe/components/Apps/components/AppCard/index.tsx b/src/routes/safe/components/Apps/components/AppCard/index.tsx index 73373a3abc..631fbdb6d0 100644 --- a/src/routes/safe/components/Apps/components/AppCard/index.tsx +++ b/src/routes/safe/components/Apps/components/AppCard/index.tsx @@ -32,7 +32,7 @@ const StyledApps = styled.div` const IconImg = styled.img<{ size: 'md' | 'lg' }>` width: ${({ size }) => (size === 'md' ? '48px' : '102px')}; height: ${({ size }) => (size === 'md' ? '60px' : '92px')}; - object-fit: contain; + object-fit: cover; ` const Skeleton = styled.div`` @@ -57,6 +57,14 @@ const DescriptionSK = styled.div` background-color: lightgrey; ` +const AppName = styled(Title)` + text-align: center; +` + +const AppDescription = styled(Text)` + height: 40px; +` + type Props = { isLoading?: boolean className?: string @@ -102,11 +110,11 @@ const Apps = ({ undefined : onCardClick}> - {name && {name}} - {description && {description} } + {name && {name}} + {description && {description} } {onButtonClick && ( - )} diff --git a/src/routes/safe/components/Apps/components/AppFrame.tsx b/src/routes/safe/components/Apps/components/AppFrame.tsx index 0ab3dd8612..c5a11d372e 100644 --- a/src/routes/safe/components/Apps/components/AppFrame.tsx +++ b/src/routes/safe/components/Apps/components/AppFrame.tsx @@ -1,7 +1,17 @@ import React, { useState, useRef, useCallback, useEffect } from 'react' import styled from 'styled-components' -import { FixedIcon, Loader, Title, Card } from '@gnosis.pm/safe-react-components' -import { useHistory } from 'react-router-dom' +import { + FixedIcon, + Loader, + Title, + Text, + Card, + GenericModal, + ModalFooterConfirmation, + Menu, + ButtonLink, +} from '@gnosis.pm/safe-react-components' +import { useHistory, useRouteMatch } from 'react-router-dom' import { useSelector } from 'react-redux' import { INTERFACE_MESSAGES, @@ -20,13 +30,16 @@ import { grantedSelector } from 'src/routes/safe/container/selector' import { getNetworkName } 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 { staticAppsList } from 'src/routes/safe/components/Apps/utils' import ConfirmTransactionModal from '../components/ConfirmTransactionModal' import { useIframeMessageHandler } from '../hooks/useIframeMessageHandler' import { useLegalConsent } from '../hooks/useLegalConsent' import LegalDisclaimer from './LegalDisclaimer' -import { getAppInfoFromUrl } from '../utils' -import { SafeApp } from '../types.d' +import { APPS_STORAGE_KEY, getAppInfoFromUrl } from '../utils' +import { SafeApp, StoredSafeApp } from '../types.d' const StyledIframe = styled.iframe` padding: 15px; @@ -58,7 +71,7 @@ const Centered = styled.div` ` const ContentWrapper = styled(Card)` - height: calc(100% - 73px); + height: calc(100% - 127px); width: calc(100% - 50px); margin: 20px 0; overflow: auto; @@ -89,16 +102,20 @@ const AppFrame = ({ appUrl }: Props): React.ReactElement => { const safeAddress = useSelector(safeParamAddressFromStateSelector) const ethBalance = useSelector(safeEthBalanceSelector) const safeName = useSelector(safeNameSelector) - + const { trackEvent } = useAnalytics() const history = useHistory() const { consentReceived, onConsentReceipt } = useLegalConsent() - const iframeRef = useRef(null) + 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 [selectedApp, setSelectedApp] = useState() + const [safeApp, setSafeApp] = useState() + const [isRemoveModalOpen, setIsRemoveModalOpen] = useState(false) + const [isAppDeletable, setIsAppDeletable] = useState() const redirectToBalance = () => history.push(`${SAFELIST_ADDRESS}/${safeAddress}/balances`) @@ -117,7 +134,7 @@ const AppFrame = ({ appUrl }: Props): React.ReactElement => { ]) const { sendMessageToIframe } = useIframeMessageHandler( - selectedApp, + safeApp, openConfirmationModal, closeConfirmationModal, iframeRef, @@ -154,21 +171,44 @@ const AppFrame = ({ appUrl }: Props): React.ReactElement => { ) } + 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) - setSelectedApp(app) + + const existsStaticApp = staticAppsList.some((staticApp) => staticApp.url === app.url) + setIsAppDeletable(!existsStaticApp) + setSafeApp(app) } loadApp() }, [appUrl]) + //track GA + useEffect(() => { + if (safeApp) { + trackEvent({ category: SAFE_NAVIGATION_EVENT, action: 'Apps', label: safeApp.name }) + } + }, [safeApp, trackEvent]) + // TODO check if URL if (!appUrl) { throw Error('App url No provided or it is invalid.') } - if (!selectedApp) { + if (!safeApp) { return ( @@ -190,39 +230,65 @@ const AppFrame = ({ appUrl }: Props): React.ReactElement => { } return ( - - {/* - - */} - - {appIsLoading && ( - - - + <> + + {isAppDeletable && ( + + Remove app + + )} + + + + {appIsLoading && ( + + + + )} + + + + {isRemoveModalOpen && ( + + Remove app + + } + body={This action will remove {safeApp.name} from the interface} + footer={ + + } + onClose={closeRemoveModal} + /> )} - - - - - + + ) } diff --git a/src/routes/safe/components/Apps/components/AppsList.tsx b/src/routes/safe/components/Apps/components/AppsList.tsx new file mode 100644 index 0000000000..f52c58cd9a --- /dev/null +++ b/src/routes/safe/components/Apps/components/AppsList.tsx @@ -0,0 +1,125 @@ +import React, { useMemo, useState } from 'react' +import styled, { css } from 'styled-components' +import { useSelector } from 'react-redux' +import { GenericModal, IconText, Loader, Menu } from '@gnosis.pm/safe-react-components' + +import { safeParamAddressFromStateSelector } from 'src/logic/safe/store/selectors' +import AppCard from 'src/routes/safe/components/Apps/components/AppCard' +import AddAppIcon from 'src/routes/safe/components/Apps/assets/addApp.svg' +import { useRouteMatch, useHistory } from 'react-router-dom' +import { SAFELIST_ADDRESS } from 'src/routes/routes' + +import { useAppList } from '../hooks/useAppList' +import { SAFE_APP_LOADING_STATUS } from '../types.d' +import AddAppForm from './AddAppForm' + +const loaderDotsSvg = require('src/routes/opening/assets/loader-dots.svg') + +const centerCSS = css` + display: flex; + align-items: center; + justify-content: center; +` + +const LoadingContainer = styled.div` + width: 100%; + height: 100%; + ${centerCSS}; +` + +const CardsWrapper = styled.div` + display: flex; + flex-wrap: wrap; + flex-direction: justify-content-center; +` + +const CenteredMT = styled.div` + ${centerCSS}; + margin-top: 16px; +` + +const ContentWrapper = styled.div` + height: 100%; + display: flex; + flex-direction: column; + justify-content: space-between; +` + +const AppsList = (): React.ReactElement => { + const history = useHistory() + const matchSafeWithAddress = useRouteMatch<{ safeAddress: string }>({ path: `${SAFELIST_ADDRESS}/:safeAddress` }) + const safeAddress = useSelector(safeParamAddressFromStateSelector) + const { appList } = useAppList() + const [isAddAppModalOpen, setIsAddAppModalOpen] = useState(false) + + const onAddAppHandler = (url: string) => () => { + const goToApp = `${matchSafeWithAddress?.url}/apps?appUrl=${encodeURI(url)}` + history.push(goToApp) + } + + const openAddAppModal = () => setIsAddAppModalOpen(true) + + const closeAddAppModal = () => setIsAddAppModalOpen(false) + + const apps = useMemo(() => { + const areAppsLoading = appList.some((app) => + [SAFE_APP_LOADING_STATUS.ADDED, SAFE_APP_LOADING_STATUS.LOADING].includes(app.loadingStatus), + ) + + return areAppsLoading + ? appList.map((app) => ({ ...app, name: 'Loading', iconUrl: loaderDotsSvg })) + : appList.map((app) => ({ ...app, id: app.url })) + }, [appList]) + + if (!appList.length || !safeAddress) { + return ( + + + + ) + } + + return ( + <> + + {/* TODO: Add navigation breadcrumb. Empty for now to give top margin */} +
+
+ + + + + {apps.map((a) => ( + + ))} + + + + + + + + {isAddAppModalOpen && ( + } + onClose={closeAddAppModal} + /> + )} + + ) +} + +export default AppsList diff --git a/src/routes/safe/components/Apps/components/ManageApps.tsx b/src/routes/safe/components/Apps/components/ManageApps.tsx deleted file mode 100644 index 0ad60a986c..0000000000 --- a/src/routes/safe/components/Apps/components/ManageApps.tsx +++ /dev/null @@ -1,75 +0,0 @@ -import { ButtonLink, ManageListModal } from '@gnosis.pm/safe-react-components' -import React, { useState } from 'react' - -import appsIconSvg from 'src/assets/icons/apps.svg' -import AddAppForm from '../AddAppForm' -import { SafeApp } from '../types' - -const FORM_ID = 'add-apps-form' - -type Props = { - appList: Array - onAppAdded: (app: SafeApp) => void - onAppToggle: (appId: string, enabled: boolean) => void - onAppRemoved: (appId: string) => void -} - -type AppListItem = SafeApp & { checked: boolean } - -const ManageApps = ({ appList, onAppAdded, onAppToggle, onAppRemoved }: Props): React.ReactElement => { - const [isOpen, setIsOpen] = useState(false) - const [isSubmitDisabled, setIsSubmitDisabled] = useState(true) - - const onSubmitForm = () => { - // This sucks, but it's the way the docs suggest - // https://github.com/final-form/react-final-form/blob/master/docs/faq.md#via-documentgetelementbyid - document.querySelectorAll(`[data-testId=${FORM_ID}]`)[0].dispatchEvent(new Event('submit', { cancelable: true })) - } - - const toggleOpen = () => setIsOpen(!isOpen) - - const closeModal = () => setIsOpen(false) - - const getItemList = (): AppListItem[] => - appList.map((a) => { - return { ...a, checked: !a.disabled } - }) - - const onItemToggle = (itemId: string, checked: boolean): void => { - onAppToggle(itemId, checked) - } - - const Form = ( - - ) - - return ( - <> - - Manage Apps - - {isOpen && ( - - )} - - ) -} - -export default ManageApps diff --git a/src/routes/safe/components/Apps/hooks/useAppList.ts b/src/routes/safe/components/Apps/hooks/useAppList.ts index 1e13065024..f3fda83517 100644 --- a/src/routes/safe/components/Apps/hooks/useAppList.ts +++ b/src/routes/safe/components/Apps/hooks/useAppList.ts @@ -1,73 +1,16 @@ -import { useState, useEffect, useCallback } from 'react' -import { loadFromStorage, saveToStorage } from 'src/utils/storage' -import { getAppInfoFromUrl, getEmptySafeApp, staticAppsList } from '../utils' +import { useState, useEffect } from 'react' +import { loadFromStorage } from 'src/utils/storage' +import { APPS_STORAGE_KEY, getAppInfoFromUrl, getEmptySafeApp, staticAppsList } from '../utils' import { SafeApp, StoredSafeApp, SAFE_APP_LOADING_STATUS } from '../types.d' import { getNetworkId } from 'src/config' -const APPS_STORAGE_KEY = 'APPS_STORAGE_KEY' - -type onAppToggleHandler = (appId: string, enabled: boolean) => Promise -type onAppAddedHandler = (app: SafeApp) => void -type onAppRemovedHandler = (appId: string) => void - type UseAppListReturnType = { appList: SafeApp[] - onAppToggle: onAppToggleHandler - onAppAdded: onAppAddedHandler - onAppRemoved: onAppRemovedHandler } const useAppList = (): UseAppListReturnType => { const [appList, setAppList] = useState([]) - const onAppToggle: onAppToggleHandler = useCallback( - async (appId, enabled) => { - // update in-memory list - const appListCopy = [...appList] - - const app = appListCopy.find((a) => a.id === appId) - if (!app) { - return - } - app.disabled = !enabled - - setAppList(appListCopy) - - // update storage list - const listToPersist: StoredSafeApp[] = appListCopy.map(({ url, disabled }) => ({ url, disabled })) - saveToStorage(APPS_STORAGE_KEY, listToPersist) - }, - [appList], - ) - - const onAppAdded: onAppAddedHandler = useCallback( - (app) => { - const newAppList = [ - { url: app.url, disabled: false }, - ...appList.map((a) => ({ - url: a.url, - disabled: a.disabled, - })), - ] - saveToStorage(APPS_STORAGE_KEY, newAppList) - - setAppList([...appList, { ...app, isDeletable: true }]) - }, - [appList], - ) - - const onAppRemoved: onAppRemovedHandler = useCallback( - (appId) => { - const appListCopy = appList.filter((a) => a.id !== appId) - - setAppList(appListCopy) - - const listToPersist: StoredSafeApp[] = appListCopy.map(({ url, disabled }) => ({ url, disabled })) - saveToStorage(APPS_STORAGE_KEY, listToPersist) - }, - [appList], - ) - // Load apps list // for each URL we return a mocked safe-app with a loading status // it was developed to speed up initial page load, otherwise the @@ -78,19 +21,15 @@ const useAppList = (): UseAppListReturnType => { // * third-party apps added by the user // * disabled status for both static and third-party apps const persistedAppList = (await loadFromStorage(APPS_STORAGE_KEY)) || [] - let list: (StoredSafeApp & { isDeletable: boolean; networks?: number[] })[] = persistedAppList.map((a) => ({ - ...a, - isDeletable: true, - })) + let list: (StoredSafeApp & { networks?: number[] })[] = [...persistedAppList] // merge stored apps with static apps (apps added manually can be deleted by the user) staticAppsList.forEach((staticApp) => { const app = list.find((persistedApp) => persistedApp.url === staticApp.url) if (app) { - app.isDeletable = false app.networks = staticApp.networks } else { - list.push({ ...staticApp, isDeletable: false }) + list.push({ ...staticApp }) } }) @@ -113,9 +52,6 @@ const useAppList = (): UseAppListReturnType => { url: appUrl, } - appInfo.disabled = Boolean(currentApp.disabled) - appInfo.isDeletable = Boolean(currentApp.isDeletable) === undefined ? true : currentApp.isDeletable - apps.push(appInfo) } @@ -159,9 +95,6 @@ const useAppList = (): UseAppListReturnType => { return { appList, - onAppToggle, - onAppAdded, - onAppRemoved, } } diff --git a/src/routes/safe/components/Apps/index.tsx b/src/routes/safe/components/Apps/index.tsx index b83116f198..06d9c7e7a5 100644 --- a/src/routes/safe/components/Apps/index.tsx +++ b/src/routes/safe/components/Apps/index.tsx @@ -1,127 +1,23 @@ -import React, { useMemo } from 'react' -import { IconText, Loader } from '@gnosis.pm/safe-react-components' -import styled, { css } from 'styled-components' -import { useSelector } from 'react-redux' +import React from 'react' import AppFrame from './components/AppFrame' -import { useAppList } from './hooks/useAppList' -import { SAFE_APP_LOADING_STATUS } from './types.d' +import AppsList from './components/AppsList' -import { safeParamAddressFromStateSelector } from 'src/logic/safe/store/selectors' -import AppCard from 'src/routes/safe/components/Apps/components/AppCard' -import AddAppIcon from 'src/routes/safe/components/Apps/assets/addApp.svg' -// import { useAnalytics /* SAFE_NAVIGATION_EVENT */ } from 'src/utils/googleAnalytics' - -import { useRouteMatch, useHistory, useLocation } from 'react-router-dom' -import { SAFELIST_ADDRESS } from 'src/routes/routes' - -const loaderDotsSvg = require('src/routes/opening/assets/loader-dots.svg') - -const centerCSS = css` - display: flex; - align-items: center; - justify-content: center; -` - -const LoadingContainer = styled.div` - width: 100%; - height: 100%; - ${centerCSS}; -` - -const CardsWrapper = styled.div` - display: flex; - flex-wrap: wrap; - flex-direction: justify-content-center; -` - -const CenteredMT = styled.div` - ${centerCSS}; - margin-top: 16px; -` - -const ContentWrapper = styled.div` - height: 100%; - display: flex; - flex-direction: column; - justify-content: space-between; -` +import { useLocation } from 'react-router-dom' const useQuery = () => { return new URLSearchParams(useLocation().search) } const Apps = (): React.ReactElement => { - const history = useHistory() - const matchSafeWithAddress = useRouteMatch<{ safeAddress: string }>({ path: `${SAFELIST_ADDRESS}/:safeAddress` }) - const safeAddress = useSelector(safeParamAddressFromStateSelector) - const query = useQuery() const appUrl = query.get('appUrl') - const { appList /* onAppAdded, onAppRemoved */ } = useAppList() - - // const { trackEvent } = useAnalytics() - - const apps = useMemo(() => { - const areAppsLoading = appList.some((app) => - [SAFE_APP_LOADING_STATUS.ADDED, SAFE_APP_LOADING_STATUS.LOADING].includes(app.loadingStatus), - ) - - return areAppsLoading - ? appList.map((app) => ({ ...app, name: 'Loading', iconUrl: loaderDotsSvg })) - : appList.map((app) => ({ ...app, id: app.url })) - }, [appList]) - - // track GA - // useEffect(() => { - // if (selectedApp) { - // trackEvent({ category: SAFE_NAVIGATION_EVENT, action: 'Apps', label: selectedApp.name }) - // } - // }, [selectedApp, trackEvent]) - - if (!appList.length || !safeAddress) { - return ( - - - - ) - } - if (appUrl) { return + } else { + return } - - return ( - - - - - {apps.map((a) => ( - { - const goToApp = `${matchSafeWithAddress?.url}/apps?appUrl=${encodeURI(a.url)}` - history.push(goToApp) - }} - /> - ))} - - - - - - - ) } export default Apps diff --git a/src/routes/safe/components/Apps/types.d.ts b/src/routes/safe/components/Apps/types.d.ts index 27004312c3..54698ac9d7 100644 --- a/src/routes/safe/components/Apps/types.d.ts +++ b/src/routes/safe/components/Apps/types.d.ts @@ -11,7 +11,6 @@ export type SafeApp = { name: string iconUrl: string disabled?: boolean - isDeletable?: boolean description: string error: boolean loadingStatus: SAFE_APP_LOADING_STATUS @@ -19,5 +18,4 @@ export type SafeApp = { export type StoredSafeApp = { url: string - disabled?: boolean } diff --git a/src/routes/safe/components/Apps/utils.ts b/src/routes/safe/components/Apps/utils.ts index 6f4800f7e2..02ae8e43f4 100644 --- a/src/routes/safe/components/Apps/utils.ts +++ b/src/routes/safe/components/Apps/utils.ts @@ -8,6 +8,8 @@ import { getContentFromENS } from 'src/logic/wallets/getWeb3' import appsIconSvg from 'src/assets/icons/apps.svg' import { ETHEREUM_NETWORK } from 'src/config/networks/network.d' +export const APPS_STORAGE_KEY = 'APPS_STORAGE_KEY' + const removeLastTrailingSlash = (url) => { if (url.substr(-1) === '/') { return url.substr(0, url.length - 1) @@ -16,7 +18,12 @@ const removeLastTrailingSlash = (url) => { } const gnosisAppsUrl = removeLastTrailingSlash(getGnosisSafeAppsUrl()) -export const staticAppsList: Array<{ url: string; disabled: boolean; networks: number[] }> = [ +export type StaticAppInfo = { + url: string + disabled: boolean + networks: number[] +} +export const staticAppsList: Array = [ // 1inch { url: `${process.env.REACT_APP_IPFS_GATEWAY}/QmUDTSghr154kCCGguyA3cbG5HRVd2tQgNR7yD69bcsjm5`, @@ -213,10 +220,10 @@ export const getIpfsLinkFromEns = memoize( ) export const uniqueApp = (appList: SafeApp[]) => (url: string): string | undefined => { + const newUrl = new URL(url) const exists = appList.some((a) => { try { const currentUrl = new URL(a.url) - const newUrl = new URL(url) return currentUrl.href === newUrl.href } catch (error) { console.error('There was a problem trying to validate the URL existence.', error.message) diff --git a/src/routes/safe/container/index.tsx b/src/routes/safe/container/index.tsx index bb6836c879..443c471d24 100644 --- a/src/routes/safe/container/index.tsx +++ b/src/routes/safe/container/index.tsx @@ -5,11 +5,11 @@ import { Redirect, Route, Switch, useRouteMatch } from 'react-router-dom' import NoSafe from 'src/components/NoSafe' import { providerNameSelector } from 'src/logic/wallets/store/selectors' -import { safeFeaturesEnabledSelector, safeParamAddressFromStateSelector } from 'src/logic/safe/store/selectors' -import { AppReduxState } from 'src/store' +import { safeParamAddressFromStateSelector } from 'src/logic/safe/store/selectors' import { wrapInSuspense } from 'src/utils/wrapInSuspense' import { SAFELIST_ADDRESS } from 'src/routes/routes' import { FEATURES } from 'src/config/networks/network.d' +import { getNetworkConfigDisabledFeatures } from 'src/config' export const BALANCES_TAB_BTN_TEST_ID = 'balances-tab-btn' export const SETTINGS_TAB_BTN_TEST_ID = 'settings-tab-btn' @@ -36,18 +36,7 @@ const Container = (): React.ReactElement => { const safeAddress = useSelector(safeParamAddressFromStateSelector) const provider = useSelector(providerNameSelector) - const featuresEnabled = useSelector( - safeFeaturesEnabledSelector, - (left, right) => { - if (Array.isArray(left) && Array.isArray(right)) { - return JSON.stringify(left) === JSON.stringify(right) - } - - return left === right - }, - ) const matchSafeWithAddress = useRouteMatch({ path: `${SAFELIST_ADDRESS}/:safeAddress` }) - const safeAppsEnabled = Boolean(featuresEnabled?.includes(FEATURES.SAFE_APPS)) if (!safeAddress) { return @@ -83,16 +72,15 @@ const Container = (): React.ReactElement => { { - // if (!safeAppsEnabled) { - // history.push(`${matchSafeWithAddress?.url}/balances`) - // } - return wrapInSuspense(, null) + render={({ history }) => { + // TODO: This will only check if Safe apps are disabled by network config + // I removed the check for safe version features because seems to be a race-condition issue + const disabledFeatures = getNetworkConfigDisabledFeatures() + if (disabledFeatures[FEATURES.SAFE_APPS]) { + history.push(`${matchSafeWithAddress?.url}/balances`) } - } + return wrapInSuspense(, null) + }} /> Date: Thu, 12 Nov 2020 18:48:28 -0300 Subject: [PATCH 14/39] refactor skeleton --- .../Apps/components/AppCard/index.tsx | 101 ++---------------- .../Apps/components/AppCard/skeleton.tsx | 86 +++++++++++++++ .../components/Apps/components/AppsList.tsx | 20 ++-- 3 files changed, 101 insertions(+), 106 deletions(-) create mode 100644 src/routes/safe/components/Apps/components/AppCard/skeleton.tsx diff --git a/src/routes/safe/components/Apps/components/AppCard/index.tsx b/src/routes/safe/components/Apps/components/AppCard/index.tsx index ccb3113b6d..e2da1d985a 100644 --- a/src/routes/safe/components/Apps/components/AppCard/index.tsx +++ b/src/routes/safe/components/Apps/components/AppCard/index.tsx @@ -4,8 +4,9 @@ import { fade } from '@material-ui/core/styles/colorManipulator' import { Title, Text, Button } from '@gnosis.pm/safe-react-components' import appsIconSvg from 'src/assets/icons/apps.svg' +import { AppIconSK, DescriptionSK, TitleSK } from './skeleton' -const StyledApps = styled.div` +const AppCard = styled.div` display: flex; align-items: center; flex-direction: column; @@ -29,95 +30,10 @@ const StyledApps = styled.div` } ` -const IconImg = styled.img<{ size: 'md' | 'lg' }>` +const IconImg = styled.img<{ size: 'md' | 'lg'; src: string | undefined }>` width: ${({ size }) => (size === 'md' ? '48px' : '102px')}; height: ${({ size }) => (size === 'md' ? '60px' : '92px')}; - object-fit: cover; -` - -const AppIconSK = styled.div` - height: 60px; - width: 60px; - border-radius: 30px; - margin: 0 auto; - background-color: lightgrey; - background: linear-gradient(84deg, lightgrey, transparent); - background-size: 400% 400%; - - -webkit-animation: gradient-SK 1.5s ease infinite; - -moz-animation: gradient-SK 1.5s ease infinite; - -o-animation: gradient-SK 1.5s ease infinite; - animation: gradient-SK 1.5s ease infinite; - - @-webkit-keyframes gradient-SK { - 0% { - background-position: 0% 54%; - } - 50% { - background-position: 100% 47%; - } - 100% { - background-position: 0% 54%; - } - } - @-moz-keyframes gradient-SK { - 0% { - background-position: 0% 54%; - } - 50% { - background-position: 100% 47%; - } - 100% { - background-position: 0% 54%; - } - } - @-o-keyframes gradient-SK { - 0% { - background-position: 0% 54%; - } - 50% { - background-position: 100% 47%; - } - 100% { - background-position: 0% 54%; - } - } - @keyframes gradient-SK { - 0% { - background-position: 0% 54%; - } - 50% { - background-position: 100% 47%; - } - 100% { - background-position: 0% 54%; - } - } -` -const TitleSK = styled.div` - height: 24px; - width: 160px; - margin: 24px auto; - background-color: lightgrey; - background: linear-gradient(84deg, lightgrey, transparent); - background-size: 400% 400%; - - -webkit-animation: gradient-SK 1.5s ease infinite; - -moz-animation: gradient-SK 1.5s ease infinite; - -o-animation: gradient-SK 1.5s ease infinite; - animation: gradient-SK 1.5s ease infinite; -` -const DescriptionSK = styled.div` - height: 16px; - width: 200px; - background-color: lightgrey; - background: linear-gradient(84deg, lightgrey, transparent); - background-size: 400% 400%; - - -webkit-animation: gradient-SK 1.5s ease infinite; - -moz-animation: gradient-SK 1.5s ease infinite; - -o-animation: gradient-SK 1.5s ease infinite; - animation: gradient-SK 1.5s ease infinite; + object-fit: contain; ` const AppName = styled(Title)` @@ -126,6 +42,7 @@ const AppName = styled(Title)` const AppDescription = styled(Text)` height: 40px; + text-align: center; ` type Props = { @@ -158,17 +75,17 @@ const Apps = ({ }: Props): React.ReactElement => { if (isLoading) { return ( - + - + ) } return ( - undefined : onCardClick}> + undefined : onCardClick}> {name && {name}} @@ -179,7 +96,7 @@ const Apps = ({ {buttonText} )} - + ) } diff --git a/src/routes/safe/components/Apps/components/AppCard/skeleton.tsx b/src/routes/safe/components/Apps/components/AppCard/skeleton.tsx new file mode 100644 index 0000000000..1cd86fce8f --- /dev/null +++ b/src/routes/safe/components/Apps/components/AppCard/skeleton.tsx @@ -0,0 +1,86 @@ +import styled from 'styled-components' + +export const AppIconSK = styled.div` + height: 60px; + width: 60px; + border-radius: 30px; + margin: 0 auto; + background-color: lightgrey; + background: linear-gradient(84deg, lightgrey, transparent); + background-size: 400% 400%; + + -webkit-animation: gradient-SK 1.5s ease infinite; + -moz-animation: gradient-SK 1.5s ease infinite; + -o-animation: gradient-SK 1.5s ease infinite; + animation: gradient-SK 1.5s ease infinite; + + @-webkit-keyframes gradient-SK { + 0% { + background-position: 0% 54%; + } + 50% { + background-position: 100% 47%; + } + 100% { + background-position: 0% 54%; + } + } + @-moz-keyframes gradient-SK { + 0% { + background-position: 0% 54%; + } + 50% { + background-position: 100% 47%; + } + 100% { + background-position: 0% 54%; + } + } + @-o-keyframes gradient-SK { + 0% { + background-position: 0% 54%; + } + 50% { + background-position: 100% 47%; + } + 100% { + background-position: 0% 54%; + } + } + @keyframes gradient-SK { + 0% { + background-position: 0% 54%; + } + 50% { + background-position: 100% 47%; + } + 100% { + background-position: 0% 54%; + } + } +` +export const TitleSK = styled.div` + height: 24px; + width: 160px; + margin: 24px auto; + background-color: lightgrey; + background: linear-gradient(84deg, lightgrey, transparent); + background-size: 400% 400%; + + -webkit-animation: gradient-SK 1.5s ease infinite; + -moz-animation: gradient-SK 1.5s ease infinite; + -o-animation: gradient-SK 1.5s ease infinite; + animation: gradient-SK 1.5s ease infinite; +` +export const DescriptionSK = styled.div` + height: 16px; + width: 200px; + background-color: lightgrey; + background: linear-gradient(84deg, lightgrey, transparent); + background-size: 400% 400%; + + -webkit-animation: gradient-SK 1.5s ease infinite; + -moz-animation: gradient-SK 1.5s ease infinite; + -o-animation: gradient-SK 1.5s ease infinite; + animation: gradient-SK 1.5s ease infinite; +` diff --git a/src/routes/safe/components/Apps/components/AppsList.tsx b/src/routes/safe/components/Apps/components/AppsList.tsx index f52c58cd9a..88b88333d4 100644 --- a/src/routes/safe/components/Apps/components/AppsList.tsx +++ b/src/routes/safe/components/Apps/components/AppsList.tsx @@ -1,4 +1,4 @@ -import React, { useMemo, useState } from 'react' +import React, { useState } from 'react' import styled, { css } from 'styled-components' import { useSelector } from 'react-redux' import { GenericModal, IconText, Loader, Menu } from '@gnosis.pm/safe-react-components' @@ -10,11 +10,9 @@ import { useRouteMatch, useHistory } from 'react-router-dom' import { SAFELIST_ADDRESS } from 'src/routes/routes' import { useAppList } from '../hooks/useAppList' -import { SAFE_APP_LOADING_STATUS } from '../types.d' +import { SAFE_APP_LOADING_STATUS, SafeApp } from '../types.d' import AddAppForm from './AddAppForm' -const loaderDotsSvg = require('src/routes/opening/assets/loader-dots.svg') - const centerCSS = css` display: flex; align-items: center; @@ -61,15 +59,8 @@ const AppsList = (): React.ReactElement => { const closeAddAppModal = () => setIsAddAppModalOpen(false) - const apps = useMemo(() => { - const areAppsLoading = appList.some((app) => - [SAFE_APP_LOADING_STATUS.ADDED, SAFE_APP_LOADING_STATUS.LOADING].includes(app.loadingStatus), - ) - - return areAppsLoading - ? appList.map((app) => ({ ...app, name: 'Loading', iconUrl: loaderDotsSvg })) - : appList.map((app) => ({ ...app, id: app.url })) - }, [appList]) + const isAppLoading = (app: SafeApp) => + [SAFE_APP_LOADING_STATUS.ADDED, SAFE_APP_LOADING_STATUS.LOADING].includes(app.loadingStatus) if (!appList.length || !safeAddress) { return ( @@ -89,8 +80,9 @@ const AppsList = (): React.ReactElement => { - {apps.map((a) => ( + {appList.map((a) => ( Date: Fri, 13 Nov 2020 16:26:26 -0300 Subject: [PATCH 15/39] refactor layout using flexfox --- src/components/AppLayout/index.tsx | 116 ++++++++++++++++------------- 1 file changed, 63 insertions(+), 53 deletions(-) diff --git a/src/components/AppLayout/index.tsx b/src/components/AppLayout/index.tsx index 4ac72c9e21..5704ec0c28 100644 --- a/src/components/AppLayout/index.tsx +++ b/src/components/AppLayout/index.tsx @@ -6,53 +6,63 @@ import Header from './Header' import Footer from './Footer' import Sidebar from './Sidebar' -const Grid = styled.div` - height: 100%; - overflow: auto; +const Container = styled.div` + height: 100vh; + width: 100vw; + display: flex; + flex-direction: column; + background-color: ${({ theme }) => theme.colors.background}; - display: grid; - grid-template-columns: 200px 1fr; - grid-template-rows: 54px 1fr; - grid-template-areas: - 'topbar topbar' - 'sidebar body'; ` -const GridTopbarWrapper = styled.nav` +const HeaderWrapper = styled.nav` + height: 54px; + width: 100%; + z-index: 1; + background-color: white; - box-shadow: 0 2px 4px 0 rgba(212, 212, 211, 0.59); - border-bottom: 2px solid ${({ theme }) => theme.colors.separator}; - z-index: 999; - grid-area: topbar; + box-shadow: 0 0 4px 0 rgba(212, 212, 211, 0.59); ` -const GridSidebarWrapper = styled.aside` - width: 200px; - padding: 62px 8px 0 8px; +const BodyWrapper = styled.div` + height: calc(100% - 54px); + width: 100%; + display: flex; + flex-direction: row; +` + +const SidebarWrapper = styled.aside` height: 100%; - background-color: ${({ theme }) => theme.colors.white}; - border-right: 2px solid ${({ theme }) => theme.colors.separator}; + width: 200px; + overflow-y: auto; display: flex; flex-direction: column; - box-sizing: border-box; - position: fixed; - grid-area: sidebar; + z-index: 1; + + padding: 8px 8px 0 8px; + background-color: ${({ theme }) => theme.colors.white}; + border-right: 2px solid ${({ theme }) => theme.colors.separator}; ` -const GridBodyWrapper = styled.section` - margin: 0 16px 0 16px; - grid-area: body; +const ContentWrapper = styled.section` + width: 100%; display: flex; flex-direction: column; - align-content: stretch; -` + overflow-x: auto; -export const BodyWrapper = styled.div` - flex: 1 100%; -` + padding: 0 16px; + + > :nth-child(1) { + flex-grow: 1; + width: 100%; + align-items: center; + justify-content: center; + } -export const FooterWrapper = styled.footer` - margin: 0 16px; + > :nth-child(2) { + width: 100%; + height: 59px; + } ` type Props = { @@ -77,29 +87,29 @@ const Layout: React.FC = ({ children, sidebarItems, }): React.ReactElement => ( - - + +
- - - - - - {children} - + + + + + + +
{children}