diff --git a/src/App.js b/src/App.js index 3078e61c6..b8aa996c6 100644 --- a/src/App.js +++ b/src/App.js @@ -4,7 +4,7 @@ import { Spring, animated } from 'react-spring' import { useTheme } from '@aragon/ui' import { EthereumAddressType, ClientThemeType } from './prop-types' import { useWallet } from './wallet' -import { network, web3Providers } from './environment' +import { network, web3Providers, enableMigrateBanner } from './environment' import { useClientTheme } from './client-theme' import { useRouting } from './routing' import initWrapper, { pollConnectivity } from './aragonjs-wrapper' @@ -22,6 +22,7 @@ import CustomToast from './components/CustomToast/CustomToast' import OrgView from './components/OrgView/OrgView' import { isKnownRepo } from './repo-utils' + import { APPS_STATUS_ERROR, APPS_STATUS_READY, @@ -33,6 +34,13 @@ import { DAO_STATUS_UNLOADED, } from './symbols' +const MIGRATION_BANNER_HIDE = 'MIGRATION_BANNER_HIDE&' +const MIGRATION_LAST_DATE_ELIGIBLE_TIMESTAMP = new Date( + '2021-05-14T15:43:08Z' +).getTime() + +const getMigrateBannerKey = address => `${MIGRATION_BANNER_HIDE}${address}` + const INITIAL_DAO_STATE = { apps: [], appIdentifiers: {}, @@ -42,6 +50,7 @@ const INITIAL_DAO_STATE = { permissions: {}, permissionsLoading: true, repos: [], + showMigrateBanner: false, } const SELECTOR_NETWORKS = [ @@ -141,10 +150,19 @@ class App extends React.Component { }, provider: web3Providers.default, walletAccount, - onDaoAddress: ({ address, domain }) => { + onDaoAddress: ({ address, domain, createdAt }) => { log('dao address', address) log('dao domain', domain) + log('dao createdAt', createdAt) + const hideMigrateBanner = getMigrateBannerKey(address) + const showMigrateBanner = + enableMigrateBanner && + createdAt && + !localStorage.getItem(hideMigrateBanner) && + createdAt < MIGRATION_LAST_DATE_ELIGIBLE_TIMESTAMP + this.setState({ + showMigrateBanner, daoStatus: DAO_STATUS_READY, daoAddress: { address, domain }, }) @@ -246,6 +264,11 @@ class App extends React.Component { }) } + closeMigrateBanner = address => { + this.setState({ showMigrateBanner: false }) + localStorage.setItem(getMigrateBannerKey(address), String(true)) + } + handleIdentityCancel = () => { const { identityIntent } = this.state identityIntent.reject(new Error('Identity modification cancelled')) @@ -308,6 +331,7 @@ class App extends React.Component { signatureBag, web3, wrapper, + showMigrateBanner, } = this.state const { address: intentAddress = null, label: intentLabel = '' } = @@ -388,6 +412,10 @@ class App extends React.Component { visible={routing.mode.name === 'org'} web3={web3} wrapper={wrapper} + showMigrateBanner={showMigrateBanner} + closeMigrateBanner={() => + this.closeMigrateBanner(daoAddress.address) + } /> diff --git a/src/aragonjs-wrapper.js b/src/aragonjs-wrapper.js index 5b8facde9..d4b7e3775 100644 --- a/src/aragonjs-wrapper.js +++ b/src/aragonjs-wrapper.js @@ -25,6 +25,7 @@ import { } from './web3-utils' import SandboxedWorker from './worker/SandboxedWorker' import WorkerSubscriptionPool from './worker/WorkerSubscriptionPool' +import { getOrganizationByAddress } from './services/gql' const POLL_DELAY_CONNECTIVITY = 2000 @@ -267,7 +268,18 @@ const initWrapper = async ( throw new DAONotFound(dao) } - onDaoAddress({ address: daoAddress, domain: dao }) + const daoData = { + address: daoAddress, + domain: dao, + } + + const data = await getOrganizationByAddress(daoAddress) + if (data?.createdAt) { + // transform into ml seconds + daoData.createdAt = parseInt(data.createdAt) * 1000 + } + + onDaoAddress(daoData) const wrapper = new Aragon(daoAddress, { provider, diff --git a/src/components/Banner/Banner.js b/src/components/Banner/Banner.js index 720ae380e..563a4c465 100644 --- a/src/components/Banner/Banner.js +++ b/src/components/Banner/Banner.js @@ -1,21 +1,30 @@ import React from 'react' import PropTypes from 'prop-types' -import { GU } from '@aragon/ui' +import { ButtonIcon, IconCross, GU } from '@aragon/ui' export const BANNER_HEIGHT = 38 -function Banner({ text, textColor, button, color }) { +function Banner({ text, textColor, button, color, height, compact, onClose }) { return (
color}; + height: ${height}px; + background: ${({ color }) => color}; + ${compact + ? ` + flex-flow: column nowrap; + align-items: flex-start; + justify-content: flex-start; + padding: ${0.5 * GU}px ${2 * GU}px; + ` + : ` + flex-wrap: nowrap; + align-items: center; + justify-content: center; + padding: ${0.5 * GU}px ${1 * GU}px; + `}; `} >
{button}
+ {onClose && ( + + + + )}
) } @@ -44,6 +74,14 @@ Banner.propTypes = { color: PropTypes.string, text: PropTypes.node, textColor: PropTypes.string, + height: PropTypes.number, + compact: PropTypes.bool, + onClose: PropTypes.func, +} + +Banner.defaultProps = { + height: BANNER_HEIGHT, + compact: false, } export default Banner diff --git a/src/components/Migrate/MigrateBanner.js b/src/components/Migrate/MigrateBanner.js new file mode 100644 index 000000000..132672ec6 --- /dev/null +++ b/src/components/Migrate/MigrateBanner.js @@ -0,0 +1,64 @@ +import React from 'react' +import PropTypes from 'prop-types' +import { Transition, animated } from 'react-spring' +import { Button, springs, useViewport } from '@aragon/ui' +import Banner, { BANNER_HEIGHT } from '../Banner/Banner' + +const MIGRATE_REWARD_URL = + 'https://help.aragon.org/article/99-aragon-govern-migration-reward-program' + +const BANNER_TEXT = { + large: 'Your DAO is eligible for the migration reward program.', + compact: 'Migration Reward Program.', +} + +const MigrateBanner = React.memo(({ visible, onClose }) => { + const { width } = useViewport() + const compact = width < 570 + const bannerHeight = compact ? 70 : BANNER_HEIGHT + + return ( + + + {visible => + visible && + /* eslint-disable react/prop-types */ + (({ height }) => ( + + + + Apply Now! + + + } + color="linear-gradient(107.79deg, #00ace2 1.46%, #02dfed 100%)" + textColor="#FFFFFF" + compact={compact} + height={bannerHeight} + onClose={onClose} + /> + + )) + /* eslint-enable react/prop-types */ + } + + + ) +}) + +MigrateBanner.propTypes = { + onClose: PropTypes.func.isRequired, + visible: PropTypes.bool, +} + +export default MigrateBanner diff --git a/src/components/OrgView/OrgView.js b/src/components/OrgView/OrgView.js index 1e1d83b58..b204b9cd0 100644 --- a/src/components/OrgView/OrgView.js +++ b/src/components/OrgView/OrgView.js @@ -32,6 +32,7 @@ import SignerPanel from '../SignerPanel/SignerPanel' import UpgradeBanner from '../Upgrade/UpgradeBanner' import UpgradeModal from '../Upgrade/UpgradeModal' import UpgradeOrganizationPanel from '../Upgrade/UpgradeOrganizationPanel' +import MigrateBanner from '../Migrate/MigrateBanner' // Remaining viewport width after the menu panel is factored in const AppWidthContext = React.createContext(0) @@ -52,6 +53,8 @@ function OrgView({ visible, web3, wrapper, + showMigrateBanner, + closeMigrateBanner, }) { const theme = useTheme() const routing = useRouting() @@ -186,6 +189,12 @@ function OrgView({ flex-shrink: 0; `} > + {showMigrateBanner && ( + + )} p), }, rinkeby: { + enableMigrateBanner: true, addresses: { ensRegistry: localEnsRegistryAddress || '0x98df287b6c145399aaa709692c8d308357bc085d', @@ -40,6 +45,8 @@ export const networkConfigs = { nodes: { defaultEth: 'wss://rinkeby.eth.aragon.network/ws', }, + connectGraphEndpoint: + 'https://api.thegraph.com/subgraphs/name/aragon/aragon-rinkeby', settings: { chainId: 4, name: 'Rinkeby testnet', @@ -56,6 +63,7 @@ export const networkConfigs = { ].filter(p => p), }, ropsten: { + enableMigrateBanner: true, addresses: { ensRegistry: localEnsRegistryAddress || '0x6afe2cacee211ea9179992f89dc61ff25c61e923', @@ -63,6 +71,7 @@ export const networkConfigs = { nodes: { defaultEth: 'wss://ropsten.eth.aragon.network/ws', }, + connectGraphEndpoint: null, settings: { chainId: 3, name: 'Ropsten testnet', @@ -73,12 +82,14 @@ export const networkConfigs = { providers: [{ id: 'provided' }, { id: 'frame' }], }, local: { + enableMigrateBanner: true, addresses: { ensRegistry: localEnsRegistryAddress, }, nodes: { defaultEth: 'ws://localhost:8545', }, + connectGraphEndpoint: null, settings: { // Local development environments by convention use // a chainId of value 1337, but for the sake of configuration @@ -94,6 +105,7 @@ export const networkConfigs = { // xDai is an experimental chain in the Aragon Client. It's possible // and expected that a few things will break. xdai: { + enableMigrateBanner: false, addresses: { ensRegistry: localEnsRegistryAddress || '0xaafca6b0c89521752e559650206d7c925fd0e530', @@ -101,6 +113,7 @@ export const networkConfigs = { nodes: { defaultEth: 'wss://xdai.poanetwork.dev/wss', }, + connectGraphEndpoint: null, settings: { chainId: 100, name: 'xDai', @@ -115,12 +128,14 @@ export const networkConfigs = { ].filter(p => p), }, unknown: { + enableMigrateBanner: true, addresses: { ensRegistry: localEnsRegistryAddress, }, nodes: { defaultEth: 'ws://localhost:8545', }, + connectGraphEndpoint: null, settings: { name: `Unknown network`, shortName: 'Unknown', diff --git a/src/services/gql.js b/src/services/gql.js new file mode 100644 index 000000000..2e7c87768 --- /dev/null +++ b/src/services/gql.js @@ -0,0 +1,44 @@ +import { connectGraphEndpoint } from '../environment' + +const query = `query Organizations($id: ID!) { + organizations(where: {id: $id}, orderBy: createdAt, orderDirection: desc){ + id + address + createdAt + } + }` + +const ORGANIZATION_INFO = 'ORGANIZATION_INFO&' + +export async function getOrganizationByAddress(daoAddress) { + const LOCAL_STORAGE_KEY = `${ORGANIZATION_INFO}${daoAddress}` + if (localStorage.getItem(LOCAL_STORAGE_KEY)) { + return JSON.parse(localStorage.getItem(LOCAL_STORAGE_KEY)) + } + + const data = await fetch(connectGraphEndpoint, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Accept: 'application/json', + }, + body: JSON.stringify({ + query, + variables: { id: daoAddress.toLowerCase() }, + }), + }) + + if (data.ok) { + const json = await data.json() + if ( + json && + json.data.organizations && + json.data.organizations.length === 1 + ) { + const organization = json.data.organizations[0] + localStorage.setItem(LOCAL_STORAGE_KEY, JSON.stringify(organization)) + return json.data.organizations[0] + } + } + return null +}