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
+}