diff --git a/.all-contributorsrc b/.all-contributorsrc new file mode 100644 index 000000000..8af9cfd9f --- /dev/null +++ b/.all-contributorsrc @@ -0,0 +1,308 @@ +{ + "projectName": "aragon", + "projectOwner": "aragon", + "repoType": "github", + "repoHost": "https://github.com", + "files": [ + "README.md" + ], + "imageSize": 75, + "commit": true, + "contributors": [ + { + "login": "bpierre", + "name": "Pierre Bertet", + "avatar_url": "https://avatars2.githubusercontent.com/u/36158?v=4", + "profile": "https://pierre.world/", + "contributions": [ + "code" + ] + }, + { + "login": "sohkai", + "name": "Brett Sun", + "avatar_url": "https://avatars2.githubusercontent.com/u/4166642?v=4", + "profile": "http://キタ.moe", + "contributions": [ + "code" + ] + }, + { + "login": "AquiGorka", + "name": "Gorka Ludlow", + "avatar_url": "https://avatars3.githubusercontent.com/u/3072458?v=4", + "profile": "http://AquiGorka.com", + "contributions": [ + "code" + ] + }, + { + "login": "izqui", + "name": "Jorge Izquierdo", + "avatar_url": "https://avatars3.githubusercontent.com/u/447328?v=4", + "profile": "http://izqui.me", + "contributions": [ + "code" + ] + }, + { + "login": "luisivan", + "name": "Luis Iván Cuende", + "avatar_url": "https://avatars0.githubusercontent.com/u/718208?v=4", + "profile": "http://aragon.org", + "contributions": [ + "code", + "design", + "ideas" + ] + }, + { + "login": "onbjerg", + "name": "Oliver", + "avatar_url": "https://avatars0.githubusercontent.com/u/8862627?v=4", + "profile": "http://notbjerg.me", + "contributions": [ + "code" + ] + }, + { + "login": "bingen", + "name": "ßingen", + "avatar_url": "https://avatars0.githubusercontent.com/u/701095?v=4", + "profile": "https://github.com/bingen", + "contributions": [ + "code" + ] + }, + { + "login": "2color", + "name": "Daniel Norman", + "avatar_url": "https://avatars1.githubusercontent.com/u/1992255?v=4", + "profile": "http://2color.me", + "contributions": [ + "code" + ] + }, + { + "login": "john-light", + "name": "John Light", + "avatar_url": "https://avatars1.githubusercontent.com/u/9424721?v=4", + "profile": "https://www.lightco.in", + "contributions": [ + "doc", + "bug" + ] + }, + { + "login": "Smokyish", + "name": "Tatu", + "avatar_url": "https://avatars0.githubusercontent.com/u/21331903?v=4", + "profile": "https://github.com/Smokyish", + "contributions": [ + "doc" + ] + }, + { + "login": "dizzypaty", + "name": "Patricia Davila", + "avatar_url": "https://avatars0.githubusercontent.com/u/7205369?v=4", + "profile": "https://github.com/dizzypaty", + "contributions": [ + "design", + "userTesting" + ] + }, + { + "login": "jounih", + "name": "Jouni Helminen", + "avatar_url": "https://avatars0.githubusercontent.com/u/10109867?v=4", + "profile": "https://github.com/jounih", + "contributions": [ + "design", + "userTesting" + ] + }, + { + "login": "lkngtn", + "name": "Luke Duncan", + "avatar_url": "https://avatars0.githubusercontent.com/u/4986634?v=4", + "profile": "https://github.com/lkngtn", + "contributions": [ + "ideas" + ] + }, + { + "login": "0x6431346e", + "name": "Daniel Constantin", + "avatar_url": "https://avatars1.githubusercontent.com/u/26041347?v=4", + "profile": "http://danielconstantin.net/", + "contributions": [ + "code" + ] + }, + { + "login": "ewingrj", + "name": "RJ Ewing", + "avatar_url": "https://avatars3.githubusercontent.com/u/30963004?v=4", + "profile": "https://rjewing.com", + "contributions": [ + "code" + ] + }, + { + "login": "drcmda", + "name": "Paul Henschel", + "avatar_url": "https://avatars0.githubusercontent.com/u/2223602?v=4", + "profile": "https://twitter.com/0xca0a", + "contributions": [ + "code" + ] + }, + { + "login": "rperez89", + "name": "Rodrigo Perez", + "avatar_url": "https://avatars2.githubusercontent.com/u/11763623?v=4", + "profile": "https://github.com/rperez89", + "contributions": [ + "code" + ] + }, + { + "login": "gasolin", + "name": "gasolin", + "avatar_url": "https://avatars1.githubusercontent.com/u/748808?v=4", + "profile": "http://www.gasolin.idv.tw", + "contributions": [ + "code" + ] + }, + { + "login": "asoltys", + "name": "Adam Soltys", + "avatar_url": "https://avatars0.githubusercontent.com/u/7641?v=4", + "profile": "http://adamsoltys.com/", + "contributions": [ + "code" + ] + }, + { + "login": "arku", + "name": "Arun Kumar", + "avatar_url": "https://avatars2.githubusercontent.com/u/7039523?v=4", + "profile": "https://github.com/arku", + "contributions": [ + "code" + ] + }, + { + "login": "bvanderdrift", + "name": "Beer van der Drift", + "avatar_url": "https://avatars1.githubusercontent.com/u/6398452?v=4", + "profile": "https://github.com/bvanderdrift", + "contributions": [ + "code" + ] + }, + { + "login": "danielcaballero", + "name": "Daniel Caballero", + "avatar_url": "https://avatars1.githubusercontent.com/u/1639333?v=4", + "profile": "https://github.com/danielcaballero", + "contributions": [ + "code" + ] + }, + { + "login": "deamme", + "name": "Deam", + "avatar_url": "https://avatars2.githubusercontent.com/u/9392750?v=4", + "profile": "https://twitter.com/deamlabs", + "contributions": [ + "code" + ] + }, + { + "login": "uniconstructor", + "name": "Ilia Smirnov", + "avatar_url": "https://avatars3.githubusercontent.com/u/1384545?v=4", + "profile": "https://github.com/uniconstructor", + "contributions": [ + "doc", + "tool" + ] + }, + { + "login": "JulSar", + "name": "julsar", + "avatar_url": "https://avatars0.githubusercontent.com/u/28685529?v=4", + "profile": "https://github.com/JulSar", + "contributions": [ + "doc" + ] + }, + { + "login": "PascalPrecht", + "name": "Pascal Precht", + "avatar_url": "https://avatars1.githubusercontent.com/u/445106?v=4", + "profile": "https://pascalprecht.github.io", + "contributions": [ + "tool" + ] + }, + { + "login": "rudygodoy", + "name": "Rudy Godoy", + "avatar_url": "https://avatars2.githubusercontent.com/u/2400137?v=4", + "profile": "https://rudygodoy.com", + "contributions": [ + "doc" + ] + }, + { + "login": "stellarmagnet", + "name": "Yalda Mousavinia", + "avatar_url": "https://avatars3.githubusercontent.com/u/2584493?v=4", + "profile": "http://spacedecentral.net", + "contributions": [ + "code" + ] + }, + { + "login": "decodedbrain", + "name": "decodedbrain", + "avatar_url": "https://avatars3.githubusercontent.com/u/18285094?v=4", + "profile": "https://github.com/decodedbrain", + "contributions": [ + "code" + ] + }, + { + "login": "jvluso", + "name": "jvluso", + "avatar_url": "https://avatars2.githubusercontent.com/u/8061735?v=4", + "profile": "https://github.com/jvluso", + "contributions": [ + "code" + ] + }, + { + "login": "MarkGeeRomano", + "name": "mark g romano", + "avatar_url": "https://avatars1.githubusercontent.com/u/13630752?v=4", + "profile": "https://github.com/MarkGeeRomano", + "contributions": [ + "code" + ] + }, + { + "login": "mul53", + "name": "mul53", + "avatar_url": "https://avatars0.githubusercontent.com/u/19148531?v=4", + "profile": "https://github.com/mul53", + "contributions": [ + "code" + ] + } + ], + "contributorsPerLine": 7 +} diff --git a/.eslintrc b/.eslintrc index 5120e5f09..84f4a7ce7 100644 --- a/.eslintrc +++ b/.eslintrc @@ -4,17 +4,28 @@ "es6": true }, "extends": [ + "plugin:import/recommended", + "plugin:promise/recommended", "standard", "standard-react", - "plugin:prettier/recommended", - "prettier/react" + "prettier/react", + "plugin:prettier/recommended" ], "parser": "babel-eslint", - "plugins": ["prettier", "react"], + "parserOptions": { + "ecmaFeatures": { + "experimentalObjectRestSpread": true, + "jsx": true + }, + "sourceType": "module" + }, + "plugins": ["prettier", "react", "import", "promise"], "rules": { + "import/no-unresolved": ["error", { ignore: ["^react(-dom)?$", "^styled-components$"] }], + "promise/no-nesting": ["off"], "valid-jsdoc": "error", "react/prop-types": 'warn', - "linebreak-style": ["error", "unix"] + "linebreak-style": ["error", "unix"], }, "settings": { "react": { diff --git a/.github/main.workflow b/.github/main.workflow new file mode 100644 index 000000000..39f96fc9d --- /dev/null +++ b/.github/main.workflow @@ -0,0 +1,21 @@ +workflow "Lint and build" { + on = "push" + resolves = ["install", "lint", "build"] +} + +action "install" { + uses = "actions/npm@master" + args = "install" +} + +action "lint" { + needs = "install" + uses = "actions/npm@master" + args = "run lint" +} + +action "build" { + needs = "install" + uses = "actions/npm@master" + args = "run build" +} \ No newline at end of file diff --git a/.github/screenshot.png b/.github/screenshot.png new file mode 100644 index 000000000..093f1c800 Binary files /dev/null and b/.github/screenshot.png differ diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 000000000..523ca5873 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,67 @@ +# Contributing to Aragon + +:tada: Thank you for being interested in contributing to Aragon! :tada: + +Feel welcome and read the following sections in order to know how to ask questions and how to work on something. + +There are many ways to contribute, from writing tutorials or blog posts, improving the documentation, submitting bug reports and feature requests or writing code which can be incorporated into the project. + +All members of our community are expected to follow our [Code of Conduct](https://wiki.aragon.org/documentation/Code_of_Conduct/). Please make sure you are welcoming and friendly in all of our spaces. + +## Your first contribution + +Unsure where to begin contributing to Aragon? + +You can start with a [Good First Issue](https://github.com/aragon/aragon/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22). + +> Good first issues are usually for small features, additional tests, spelling / grammar fixes, formatting changes, or other clean up. + +Start small, pick a subject you care about, are familiar with, or want to learn. + +If you're not already familiar with git or Github, here are a couple of friendly tutorials: [First Contributions](https://github.com/firstcontributions/first-contributions), [Open Source Guide](https://opensource.guide/), and [How to Contribute to an Open Source Project on GitHub](https://egghead.io/series/how-to-contribute-to-an-open-source-project-on-github). + +## How to file an issue or report a bug + +If you see a problem, you can report it in our [issue tracker](https://github.com/aragon/aragon/issues). + +Please take a quick look to see if the issue doesn't already exist before filing yours. + +Do your best to include as many details as needed in order for someone else to fix the problem and resolve the issue. + +#### If you find a security vulnerability, do NOT open an issue. Email security@aragon.org instead. + +In order to determine whether you are dealing with a security issue, ask yourself these two questions: + +- Can I access or steal something that's not mine, or access something I shouldn't have access to? +- Can I disable something for other people? + +If the answer to either of those two questions are "yes", then you're probably dealing with a security issue. Note that even if you answer "no" to both questions, you may still be dealing with a security issue, so if you're unsure, please send a email. + +#### If you're interested in the smart contracts underlying Aragon, a [bug bounty program](https://wiki.aragon.org/dev/bug_bounty/) with payouts up to $50,000 is available for rewarding contributors who find security vulnerabilities. + +## Fixing issues + +1. [Find an issue](https://github.com/aragon/aragon/issues) that you are interested in. + - You may want to ask on the issue or on Aragon Chat's [#dev channel](https://aragon.chat/channel/dev) if anyone has already started working on the issue. +1. Fork and clone a local copy of the repository. +1. Make the appropriate changes for the issue you are trying to address or the feature that you want to add. +1. Push the changes to the remote repository. +1. Submit a pull request in Github, explaining any changes and further questions you may have. +1. Wait for the pull request to be reviewed. +1. Make changes to the pull request if the maintainer recommends them. +1. Celebrate your success after your pull request is merged! + +It's OK if your pull request is not perfect (no pull request is). +The reviewer will be able to help you fix any problems and improve it! + +You can also edit a page directly through your browser by clicking the "EDIT" link in the top-right corner of any page and then clicking the pencil icon in the github copy of the page. + +## Styleguide and development processes + +We use [prettier](https://prettier.io/) and [eslint](https://eslint.org/) to automatically lint and format the project. + +We generally avoid adding external dependencies if they can be ported over easily, due to numerous NPM-related security issues in the past (e.g. [`event-stream`](https://blog.npmjs.org/post/180565383195/details-about-the-event-stream-incident)). + +## Community + +If you need help, please reach out to Aragon core contributors and community members in the Aragon Chat [#dev](https://aragon.chat/channel/dev) and [#dev-help](https://aragon.chat/channel/dev-help) channels. We'd love to hear from you and know what you're working on! diff --git a/README.md b/README.md index 4067fc200..fbb16c568 100644 --- a/README.md +++ b/README.md @@ -1,16 +1,18 @@ # Aragon [![Build Status](https://travis-ci.org/aragon/aragon.svg?branch=master)](https://travis-ci.org/aragon/aragon) +[![All Contributors](https://img.shields.io/badge/all_contributors-32-orange.svg?style=flat-square)](#contributors) -#### 📝 Please report support and feedback related issues at the [Aragon Chat #feedback](https://aragon.chat/channel/feedback) channel. -#### 🔧 For technical stuff, use this project's [issues](http://github.com/aragon/aragon/issues) or join the technical conversation in our [#dev](https://aragon.chat/channel/dev) channel. -#### 🦋 For an overview of what changed in every version check the [changelog](https://github.com/aragon/aragon/blob/master/changelog.md) +#### 🌎🚀 Trusted by over [300 organizations](https://daolist.io/), securing more than $1MM in funds. -## Contributing - -Please note that all of the code is still undocumented, and no contribution guidelines are in place. + -Contributions are welcome, just beware of the dragons. 🐲 +- 📚 Read the [User Guide](https://wiki.aragon.org/tutorials/Aragon_User_Guide/) first, if you have any questions as a user. +- 💻 You may be interested in [Aragon Desktop](https://github.com/aragon/aragon-desktop/), the most decentralized Aragon experience to date. +- 🏗 If you'd like to develop an Aragon app, please visit the [Aragon Developer Portal](https://hack.aragon.org). +- 📝 Please report any issues and feedback in the [Aragon Chat #feedback](https://aragon.chat/channel/feedback) channel. +- 🔧 For technical stuff, use this project's [issues](http://github.com/aragon/aragon/issues) or join the technical conversation in our [#dev](https://aragon.chat/channel/dev) channel. +- 🚢 For an overview of what changed with each release, check the [releases](https://github.com/aragon/aragon/releases) and [changelog](https://github.com/aragon/aragon/blob/master/changelog.md). ## Quick start @@ -19,10 +21,26 @@ Contributions are welcome, just beware of the dragons. 🐲 For connecting to other chains / deployments, a few useful npm scripts are provided: - Mainnet: `npm run start:mainnet` will launch the app, configured to connect to our mainnet deployment -- Local development: `npm run start:local` will launch the app, configured to connect to our [aragen](https://github.com/aragon/aragen) local development environment. It will also use the local IPFS daemon, if it detects one exists. +- Local development: `npm run start:local` will launch the app, configured to connect to our [aragen](https://github.com/aragon/aragen) local development environment. It will also use the local IPFS daemon, if it detects one exists. If you're using the [aragonCLI](http://github.com/aragon/aragon-cli), you'll want to run this to connect to its local chain. **Note**: Windows users may need to install the [windows-build-tools](https://www.npmjs.com/package/windows-build-tools) before installing this project's dependencies. +## Contributing + +#### 👋 Get started contributing with a [good first issue](https://github.com/aragon/aragon/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22). + +Don't be shy to contribute even the smallest tweak. 🐲 There are still some dragons to be aware of, but we'll be here to help you get started! + +For more details about contributing to Aragon, please check the [contributing guide](./CONTRIBUTING.md). + +#### Issues + +If you come across an issue with Aragon, do a search in the [Issues](https://github.com/aragon/aragon/issues?utf8=%E2%9C%93&q=is%3Aissue) tab of this repo and the [Aragon Apps Issues](https://github.com/aragon/aragon-apps/issues?utf8=%E2%9C%93&q=is%3Aissue) to make sure it hasn't been reported before. Follow these steps to help us prevent duplicate issues and unnecessary notifications going to the many people watching this repo: + +- If the issue you found has been reported and is still open, and the details match your issue, give a "thumbs up" to the relevant posts in the issue thread to signal that you have the same issue. No further action is required on your part. +- If the issue you found has been reported and is still open, but the issue is missing some details, you can add a comment to the issue thread describing the additional details. +- If the issue you found has been reported but has been closed, you can comment on the closed issue thread and ask to have the issue reopened because you are still experiencing the issue. Alternatively, you can open a new issue, reference the closed issue by number or link, and state that you are still experiencing the issue. Provide any additional details in your post so we can better understand the issue and how to fix it. + ## Environment options The app can be configured in a number of ways via environment variables: @@ -36,10 +54,14 @@ The app can be configured in a number of ways via environment variables: Without any settings, the app is configured to connect to our Rinkeby deployment fetching assets from IPFS. -## Issues +## Contributors -If you come across an issue with Aragon, do a search in the [Issues](https://github.com/aragon/aragon/issues?utf8=%E2%9C%93&q=is%3Aissue) tab of this repo and the [Aragon Apps Issues](https://github.com/aragon/aragon-apps/issues?utf8=%E2%9C%93&q=is%3Aissue) to make sure it hasn't been reported before. Follow these steps to help us prevent duplicate issues and unnecessary notifications going to the many people watching this repo: +Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/docs/en/emoji-key)): -- If the issue you found has been reported and is still open, and the details match your issue, give a "thumbs up" to the relevant posts in the issue thread to signal that you have the same issue. No further action is required on your part. -- If the issue you found has been reported and is still open, but the issue is missing some details, you can add a comment to the issue thread describing the additional details. -- If the issue you found has been reported but has been closed, you can comment on the closed issue thread and ask to have the issue reopened because you are still experiencing the issue. Alternatively, you can open a new issue, reference the closed issue by number or link, and state that you are still experiencing the issue. Provide any additional details in your post so we can better understand the issue and how to fix it. + + +
Pierre Bertet
Pierre Bertet

💻
Brett Sun
Brett Sun

💻
Gorka Ludlow
Gorka Ludlow

💻
Jorge Izquierdo
Jorge Izquierdo

💻
Luis Iván Cuende
Luis Iván Cuende

💻 🎨 🤔
Oliver
Oliver

💻
ßingen
ßingen

💻
Daniel Norman
Daniel Norman

💻
John Light
John Light

📖 🐛
Tatu
Tatu

📖
Patricia Davila
Patricia Davila

🎨 📓
Jouni Helminen
Jouni Helminen

🎨 📓
Luke Duncan
Luke Duncan

🤔
Daniel Constantin
Daniel Constantin

💻
RJ Ewing
RJ Ewing

💻
Paul Henschel
Paul Henschel

💻
Rodrigo Perez
Rodrigo Perez

💻
gasolin
gasolin

💻
Adam Soltys
Adam Soltys

💻
Arun Kumar
Arun Kumar

💻
Beer van der Drift
Beer van der Drift

💻
Daniel Caballero
Daniel Caballero

💻
Deam
Deam

💻
Ilia Smirnov
Ilia Smirnov

📖 🔧
julsar
julsar

📖
Pascal Precht
Pascal Precht

🔧
Rudy Godoy
Rudy Godoy

📖
Yalda Mousavinia
Yalda Mousavinia

💻
decodedbrain
decodedbrain

💻
jvluso
jvluso

💻
mark g romano
mark g romano

💻
mul53
mul53

💻
+ + + +This project follows the [all-contributors](https://github.com/all-contributors/all-contributors) specification. Contributions of any kind welcome! diff --git a/package.json b/package.json index e54bfd0d1..536e38698 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "aragon", "description": "Aragon DApp", - "version": "0.6.0", + "version": "0.6.4", "private": true, "license": "AGPL-3.0-or-later", "repository": { @@ -25,25 +25,26 @@ ], "dependencies": { "@aragon/templates-tokens": "^1.1.1", - "@aragon/ui": "^0.28.0", - "@aragon/wrapper": "^3.0.0-beta.3", + "@aragon/ui": "^0.33.0", + "@aragon/wrapper": "^4.0.0", "@babel/polyfill": "^7.0.0", "bn.js": "4.11.6", "date-fns": "2.0.0-alpha.22", - "eth-provider": "^0.1.5", + "eth-provider": "^0.2.0", "history": "^4.7.2", "lodash.memoize": "^4.1.2", "lodash.throttle": "^4.1.1", "lodash.uniqby": "^4.7.0", "onecolor": "^3.0.5", "prop-types": "^15.6.2", - "react": "^16.7.0", + "react": "^16.8.3", "react-blockies": "^1.3.0", "react-container-dimensions": "^1.3.3", - "react-display-name": "^0.2.3", - "react-dom": "^16.7.0", + "react-dom": "^16.8.4", + "react-dropzone": "^10.0.0", "react-onclickout": "^2.0.8", - "react-spring": "^7.2.8", + "react-spring": "^7.2.10", + "react-with-gesture": "^4.0.4", "resolve-pathname": "^3.0.0", "styled-components": "^4.1.3", "underscore": "1.8.3", @@ -51,7 +52,7 @@ "web3-utils": "1.0.0-beta.33" }, "devDependencies": { - "@aragon/cli": "^5.2.0-beta.2", + "@aragon/cli": "^5.4.0-beta.1", "@aragon/os": "^4.0.0", "@babel/core": "^7.0.0", "@babel/plugin-proposal-class-properties": "^7.0.0", @@ -64,14 +65,14 @@ "eslint-config-prettier": "^3.1.0", "eslint-config-standard": "^12.0.0", "eslint-config-standard-react": "^7.0.2", - "eslint-plugin-import": "^2.8.0", + "eslint-plugin-import": "^2.16.0", "eslint-plugin-node": "^7.0.1", "eslint-plugin-prettier": "^2.7.0", "eslint-plugin-promise": "^4.0.1", "eslint-plugin-react": "^7.5.1", "eslint-plugin-standard": "^4.0.0", "husky": "^1.0.1", - "lint-staged": "^8.1.0", + "lint-staged": "^8.1.1", "parcel-bundler": "^1.10.1", "parcel-plugin-bundle-visualiser": "^1.2.0", "prettier": "^1.15.0", diff --git a/src/App.js b/src/App.js index 064ffebef..211913634 100644 --- a/src/App.js +++ b/src/App.js @@ -14,9 +14,11 @@ import { getWeb3, getUnknownBalance, identifyProvider } from './web3-utils' import { log } from './utils' import { PermissionsProvider } from './contexts/PermissionsContext' import { FavoriteDaosProvider } from './contexts/FavoriteDaosContext' -import { ScreenSizeProvider } from './contexts/ScreenSize' import { ModalProvider } from './components/ModalManager/ModalManager' import DeprecatedBanner from './components/DeprecatedBanner/DeprecatedBanner' +import { IdentityProvider } from './components/IdentityManager/IdentityManager' +import { LocalIdentityModalProvider } from './components/LocalIdentityModal/LocalIdentityModalManager' +import LocalIdentityModal from './components/LocalIdentityModal/LocalIdentityModal' import { APPS_STATUS_ERROR, APPS_STATUS_READY, @@ -28,32 +30,33 @@ import { class App extends React.Component { state = { - fatalError: null, - locator: {}, - prevLocator: null, - wrapper: null, account: '', - balance: getUnknownBalance(), - connected: false, apps: [], appsStatus: APPS_STATUS_LOADING, - permissions: {}, - permissionsLoading: true, - walletWeb3: null, + balance: getUnknownBalance(), + buildData: null, // data returned by aragon.js when a DAO is created + connected: false, daoAddress: { address: '', domain: '' }, // daoCreationStatus is one of: // - DAO_CREATION_STATUS_NONE // - DAO_CREATION_STATUS_SUCCESS // - DAO_CREATION_STATUS_ERROR daoCreationStatus: DAO_CREATION_STATUS_NONE, - buildData: null, // data returned by aragon.js when a DAO is created - transactionBag: null, - walletNetwork: '', - showDeprecatedBanner: false, + fatalError: null, + identityIntent: null, + locator: {}, + permissions: {}, + permissionsLoading: true, + prevLocator: null, selectorNetworks: [ ['main', 'Ethereum Mainnet', 'https://mainnet.aragon.org/'], ['rinkeby', 'Ethereum Testnet (Rinkeby)', 'https://rinkeby.aragon.org/'], ], + showDeprecatedBanner: false, + transactionBag: null, + walletNetwork: '', + walletWeb3: null, + wrapper: null, } history = createHistory() @@ -99,12 +102,12 @@ class App extends React.Component { return } // For providers supporting .enable() (EIP 1102 draft). - if ('enable' in provider) { + if (typeof provider.enable === 'function') { provider.enable() return } // For providers supporting EIP 1102 (final). - if ('send' in provider) { + if (typeof provider.send === 'function') { // Some providers (Metamask) don’t return a promise as defined in EIP // 1102, so we can’t rely on it to know the connected accounts. provider.send('eth_requestAccounts') @@ -240,10 +243,27 @@ class App extends React.Component { log('transaction bag', transactionBag) this.setState({ transactionBag }) }, + onIdentityIntent: async identityIntent => { + // set the state for modifying a specific address identity + let name = null + try { + const identity = await this.handleIdentityResolve( + identityIntent.address + ) + name = identity.name + } catch (e) {} + this.setState({ + identityIntent: { + label: name, + ...identityIntent, + }, + }) + }, }) .then(wrapper => { log('wrapper', wrapper) this.setState({ wrapper }) + return wrapper }) .catch(err => { log(`Wrapper init, fatal error: ${err.name}. ${err.message}.`) @@ -259,6 +279,35 @@ class App extends React.Component { }, 1000) } + handleIdentityCancel = () => { + this.setState({ identityIntent: null }) + } + + handleIdentitySave = ({ address, label }) => { + const { identityIntent } = this.state + this.state.wrapper + .modifyAddressIdentity(address, { name: label }) + .then(() => + this.setState({ identityIntent: null }, identityIntent.resolve) + ) + .catch(identityIntent.reject) + } + + handleIdentityResolve = address => { + // returns promise + if (this.state.wrapper) { + return this.state.wrapper.resolveAddressIdentity(address) + } else { + // wrapper has not been initialized + // re-request in 100 ms + return new Promise(resolve => { + setTimeout(async () => { + resolve(await this.handleIdentityResolve(address)) + }, 100) + }) + } + } + handleCompleteOnboarding = () => { const { domain } = this.state.buildData this.historyPush(`/${domain}`) @@ -266,27 +315,31 @@ class App extends React.Component { handleOpenOrganization = address => { this.historyPush(`/${address}`) } + handleOpenLocalIdentityModal = address => { + return this.state.wrapper.requestAddressIdentityModification(address) + } render() { const { - fatalError, - locator, - wrapper, - apps, - permissions, account, + apps, + appsStatus, balance, - walletNetwork, - transactionBag, - daoCreationStatus, - walletWeb3, connected, daoAddress, - appsStatus, + daoCreationStatus, + fatalError, + identityIntent, + locator, + permissions, permissionsLoading, - showDeprecatedBanner, selectorNetworks, + showDeprecatedBanner, + transactionBag, + walletNetwork, walletProviderId, + walletWeb3, + wrapper, } = this.state const { mode, dao } = locator @@ -300,56 +353,74 @@ class App extends React.Component { if (fatalError !== null) { throw fatalError } + const { address: intentAddress = null, label: intentLabel = '' } = + identityIntent || {} return ( - - - - - } - historyBack={this.historyBack} - historyPush={this.historyPush} - locator={locator} + + + + + + + + } + connected={connected} + daoAddress={daoAddress} + historyBack={this.historyBack} + historyPush={this.historyPush} + identityIntent={identityIntent} + locator={locator} + onRequestAppsReload={this.handleRequestAppsReload} + onRequestEnable={this.handleRequestEnable} + permissionsLoading={permissionsLoading} + transactionBag={transactionBag} + walletNetwork={walletNetwork} + walletWeb3={walletWeb3} + wrapper={wrapper} + /> + + + + ) + } + visible={mode === 'home' || mode === 'setup'} account={account} + balance={balance} walletNetwork={walletNetwork} - walletWeb3={walletWeb3} - daoAddress={daoAddress} - transactionBag={transactionBag} - connected={connected} - onRequestAppsReload={this.handleRequestAppsReload} + walletProviderId={walletProviderId} + onBuildDao={this.handleBuildDao} + daoCreationStatus={daoCreationStatus} + onComplete={this.handleCompleteOnboarding} + onOpenOrganization={this.handleOpenOrganization} + onRequestEnable={this.handleRequestEnable} + onResetDaoBuilder={this.handleResetDaoBuilder} + selectorNetworks={selectorNetworks} /> - - - - } - visible={mode === 'home' || mode === 'setup'} - account={account} - balance={balance} - walletNetwork={walletNetwork} - walletProviderId={walletProviderId} - onBuildDao={this.handleBuildDao} - daoCreationStatus={daoCreationStatus} - onComplete={this.handleCompleteOnboarding} - onOpenOrganization={this.handleOpenOrganization} - onResetDaoBuilder={this.handleResetDaoBuilder} - onRequestEnable={this.handleRequestEnable} - selectorNetworks={selectorNetworks} - walletWeb3={walletWeb3} - /> - - - + + + + ) } } diff --git a/src/GlobalErrorHandler.js b/src/GlobalErrorHandler.js index 96e22939d..ef2098c1a 100644 --- a/src/GlobalErrorHandler.js +++ b/src/GlobalErrorHandler.js @@ -1,10 +1,14 @@ import React from 'react' -import styled from 'styled-components' +import PropTypes from 'prop-types' +import { BaseStyles, PublicUrl } from '@aragon/ui' import GenericError from './components/Error/GenericError' import DAONotFoundError from './components/Error/DAONotFoundError' import { DAONotFound } from './errors' class GlobalErrorHandler extends React.Component { + static propTypes = { + children: PropTypes.node, + } state = { error: null, errorStack: null } componentDidCatch(error, errorInfo) { this.setState({ @@ -33,34 +37,37 @@ class GlobalErrorHandler extends React.Component { return this.props.children } return ( -
- - {error instanceof DAONotFound ? ( - - ) : ( - - )} - -
+ + +
+
+ {error instanceof DAONotFound ? ( + + ) : ( + + )} +
+
+
) } } -const Main = styled.div` - height: 100vh; - overflow: auto; -` - -const In = styled.div` - display: flex; - justify-content: center; - align-items: center; - margin-top: -30px; - padding: 50px 20px 20px; - min-height: 100%; -` - export default GlobalErrorHandler diff --git a/src/Wrapper.js b/src/Wrapper.js index 75cd88281..1613fde0f 100644 --- a/src/Wrapper.js +++ b/src/Wrapper.js @@ -1,77 +1,72 @@ import React from 'react' import PropTypes from 'prop-types' import styled from 'styled-components' +import { Viewport } from '@aragon/ui' import { Apps, Permissions, Settings } from './apps' -import ethereumLoadingAnimation from './assets/ethereum-loading.svg' -import { ScreenSizeConsumer, SMALL } from './contexts/ScreenSize' import AppIFrame from './components/App/AppIFrame' import App404 from './components/App404/App404' import Home from './components/Home/Home' +import Preferences from './components/Preferences/Preferences' import MenuPanel from './components/MenuPanel/MenuPanel' +import SwipeContainer from './components/MenuPanel/SwipeContainer' import SignerPanel from './components/SignerPanel/SignerPanel' import DeprecatedBanner from './components/DeprecatedBanner/DeprecatedBanner' -import { DaoAddressType } from './prop-types' +import NotificationBar from './components/Notifications/NotificationBar' +import { + AppType, + AppsStatusType, + AragonType, + DaoAddressType, + EthereumAddressType, +} from './prop-types' import { getAppPath } from './routing' import { staticApps } from './static-apps' +import { APPS_STATUS_LOADING } from './symbols' import { addressesEqual } from './web3-utils' -import { noop } from './utils' -import { - APPS_STATUS_ERROR, - APPS_STATUS_READY, - APPS_STATUS_LOADING, -} from './symbols' -import NotificationBar from './components/Notifications/NotificationBar' +import ethereumLoadingAnimation from './assets/ethereum-loading.svg' -class Wrapper extends React.Component { +class Wrapper extends React.PureComponent { static propTypes = { - account: PropTypes.string.isRequired, - apps: PropTypes.array.isRequired, - appsStatus: PropTypes.oneOf([ - APPS_STATUS_ERROR, - APPS_STATUS_READY, - APPS_STATUS_LOADING, - ]).isRequired, + account: EthereumAddressType, + apps: PropTypes.arrayOf(AppType).isRequired, + appsStatus: AppsStatusType.isRequired, banner: PropTypes.oneOfType([ PropTypes.bool, PropTypes.shape({ type: PropTypes.oneOf([DeprecatedBanner]), }), - ]).isRequired, - connected: PropTypes.bool.isRequired, + ]), + connected: PropTypes.bool, daoAddress: DaoAddressType.isRequired, historyBack: PropTypes.func.isRequired, historyPush: PropTypes.func.isRequired, locator: PropTypes.object.isRequired, onRequestAppsReload: PropTypes.func.isRequired, + onRequestEnable: PropTypes.func.isRequired, permissionsLoading: PropTypes.bool.isRequired, - screenSize: PropTypes.symbol.isRequired, + autoClosingPanel: PropTypes.bool.isRequired, + menuSwipeEnabled: PropTypes.bool.isRequired, transactionBag: PropTypes.object, - walletNetwork: PropTypes.string.isRequired, - walletWeb3: PropTypes.object, + walletNetwork: PropTypes.string, walletProviderId: PropTypes.string, - wrapper: PropTypes.object, - onRequestEnable: PropTypes.func, + walletWeb3: PropTypes.object, + wrapper: AragonType, } static defaultProps = { account: '', - apps: [], - banner: null, + banner: false, connected: false, - daoAddress: '', - historyBack: noop, - historyPush: noop, - locator: {}, - onRequestEnable: noop, transactionBag: null, walletNetwork: '', walletProviderId: '', walletWeb3: null, - wrapper: null, } + state = { appInstance: {}, - menuPanelOpened: this.props.screenSize !== SMALL, + menuPanelOpened: !this.props.autoClosingPanel, + preferencesOpened: false, notificationOpen: false, notifications: [ { @@ -89,18 +84,44 @@ class Wrapper extends React.Component { ], queuedNotifications: [], } + + componentDidUpdate(prevProps) { + this.updateAutoClosingPanel(prevProps) + } + + updateAutoClosingPanel(prevProps) { + const { autoClosingPanel } = this.props + if (autoClosingPanel !== prevProps.autoClosingPanel) { + this.setState({ menuPanelOpened: !autoClosingPanel }) + this.sendDisplayMenuButtonStatus() + } + } + + sendDisplayMenuButtonStatus() { + const { autoClosingPanel } = this.props + if (this.appIFrame) { + this.appIFrame.sendMessage({ + from: 'wrapper', + name: 'displayMenuButton', + value: autoClosingPanel, + }) + } + } + openApp = (instanceId, params) => { - if (this.props.screenSize === SMALL) { + if (this.props.autoClosingPanel) { this.handleMenuPanelClose() } const { historyPush, locator } = this.props historyPush(getAppPath({ dao: locator.dao, instanceId, params })) } + handleAppIFrameRef = appIFrame => { this.appIFrame = appIFrame } - handleAppIFrameLoad = event => { + + handleAppIFrameLoad = async event => { const { apps, wrapper, @@ -114,25 +135,44 @@ class Wrapper extends React.Component { return } - wrapper.connectAppIFrame(event.target, instanceId) + await wrapper.connectAppIFrame(event.target, instanceId) + this.appIFrame.sendMessage({ from: 'wrapper', name: 'ready', value: true, }) + this.sendDisplayMenuButtonStatus() } handleAppMessage = ({ data: { name, value } }) => { - if (name === 'menuPanel') { - this.setState({ menuPanelOpened: Boolean(value) }) + if ( + // “menuPanel: Boolean” is deprecated but still supported for a while if + // value is `true`. + name === 'menuPanel' || + // “requestMenu: true” should now be used. + name === 'requestMenu' + ) { + this.setState({ menuPanelOpened: value === true }) } } + handleMenuPanelOpen = () => { + this.setState({ menuPanelOpened: true }) + } handleMenuPanelClose = () => { this.setState({ menuPanelOpened: false }) } handleNotificationClicked = () => { this.setState(state => ({ notificationOpen: !state.notificationOpen })) } - + handleClosePreferences = () => { + this.setState({ preferencesOpened: false }) + } + handleOpenPreferences = () => { + if (this.props.autoClosingPanel) { + this.handleMenuPanelClose() + } + this.setState({ preferencesOpened: true }) + } // params need to be a string handleParamsRequest = params => { this.openApp(this.props.locator.instanceId, params) @@ -172,49 +212,72 @@ class Wrapper extends React.Component { account, apps, appsStatus, + autoClosingPanel, banner, connected, daoAddress, locator, onRequestAppsReload, onRequestEnable, + menuSwipeEnabled, transactionBag, walletNetwork, walletProviderId, walletWeb3, + wrapper, } = this.props - const { menuPanelOpened, notifications, notificationOpen } = this.state + const { + menuPanelOpened, + notifications, + notificationOpen, + preferencesOpened, + } = this.state return (
+ {banner} - - app.hasWebApp)} - appsStatus={appsStatus} - activeInstanceId={locator.instanceId} - connected={connected} - notifications={notifications.length} - daoAddress={daoAddress} - menuPanelOpened={menuPanelOpened} - onOpenApp={this.openApp} - onCloseMenuPanel={this.handleMenuPanelClose} - onRequestAppsReload={onRequestAppsReload} - onNotificationClicked={this.handleNotificationClicked} - notificationOpen={notificationOpen} - /> - - - - {this.renderApp(locator.instanceId, locator.params)} - - + + {progress => ( + + app.hasWebApp)} + appsStatus={appsStatus} + activeInstanceId={locator.instanceId} + connected={connected} + notifications={notifications.length} + daoAddress={daoAddress} + openProgress={progress} + autoClosing={autoClosingPanel} + onOpenApp={this.openApp} + onCloseMenuPanel={this.handleMenuPanelClose} + onOpenPreferences={this.handleOpenPreferences} + onRequestAppsReload={onRequestAppsReload} + onNotificationClicked={this.handleNotificationClicked} + notificationOpen={notificationOpen} + /> + + + {this.renderApp(locator.instanceId, locator.params)} + + + )} + ) } @@ -315,11 +378,13 @@ class Wrapper extends React.Component { ) } @@ -347,6 +412,7 @@ const Main = styled.div` display: flex; flex-direction: column; height: 100vh; + min-width: 320px; ` const BannerWrapper = styled.div` @@ -355,13 +421,6 @@ const BannerWrapper = styled.div` flex-shrink: 0; ` -const Container = styled.div` - position: relative; - display: flex; - flex-grow: 1; - min-height: 0; -` - const AppScreen = styled.div` position: relative; z-index: 1; @@ -390,7 +449,13 @@ const LoadingApps = () => ( ) export default props => ( - - {({ screenSize }) => } - + + {({ below }) => ( + + )} + ) diff --git a/src/apps/Apps/Apps.js b/src/apps/Apps/Apps.js index efcb553c7..0b2dd9272 100644 --- a/src/apps/Apps/Apps.js +++ b/src/apps/Apps/Apps.js @@ -1,26 +1,28 @@ import React from 'react' +import PropTypes from 'prop-types' import styled from 'styled-components' import { - Card, - Button, Badge, - Text, + Button, + Card, SafeLink, - theme, + Text, + breakpoint, colors, + theme, unselectable, - font, - breakpoint, - BreakPoint, } from '@aragon/ui' import AppLayout from '../../components/AppLayout/AppLayout' -import MenuButton from '../../components/MenuPanel/MenuButton' +import AppIcon from '../../components/AppIcon/AppIcon' -import defaultIcon from './icons/default.svg' import payrollIcon from './icons/payroll.svg' import espressoIcon from './icons/espresso.svg' class Apps extends React.Component { + static propTypes = { + onMessage: PropTypes.func.isRequired, + } + handleMenuPanelOpen = () => { this.props.onMessage({ data: { from: 'app', name: 'menuPanel', value: true }, @@ -30,23 +32,20 @@ class Apps extends React.Component { render() { return ( - - - - Apps - - } - endContent={ - - Create a new app - - } + title="Apps" + onMenuOpen={this.handleMenuPanelOpen} + mainButton={{ + button: ( + + Create a new app + + ), + }} + smallViewPadding={20} >

@@ -66,7 +65,7 @@ class Apps extends React.Component { {knownApps.map((app, i) => (

- + {app.name} @@ -89,31 +88,12 @@ class Apps extends React.Component { } } -const AppBarTitle = styled.span` - display: flex; - align-items: center; - margin-left: -30px; -` - -const AppBarLabel = styled.span` - margin-left: 8px; - ${font({ size: 'xxlarge' })}; - - ${breakpoint( - 'medium', - ` - margin-left: 24px; - ` - )}; -` - const DevPortalAnchor = styled(Button.Anchor)` + margin-right: 20px; display: block; ` const Content = styled.div` - padding: 30px; - > h1 { margin: 30px 0; font-weight: 600; @@ -153,10 +133,6 @@ const Icon = styled.div` } ` -const Img = styled.img` - display: block; -` - const Name = styled.p` display: flex; width: 100%; @@ -200,7 +176,7 @@ const statuses = { const knownApps = [ { - icon: defaultIcon, + icon: null, name: 'That Planning Suite', status: 'alpha', description: `Suite for open and fluid organizations. @@ -225,7 +201,7 @@ const knownApps = [ link: 'https://github.com/espresso-org', }, { - icon: defaultIcon, + icon: null, name: 'Liquid democracy', status: 'pre-alpha', description: `Delegate your voting power to others, diff --git a/src/apps/Apps/icons/default.svg b/src/apps/Apps/icons/default.svg deleted file mode 100644 index f10348ce1..000000000 --- a/src/apps/Apps/icons/default.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/src/apps/Permissions/AppCard.js b/src/apps/Permissions/AppCard.js index b54d4dc49..0a39f996b 100644 --- a/src/apps/Permissions/AppCard.js +++ b/src/apps/Permissions/AppCard.js @@ -2,13 +2,13 @@ import React from 'react' import PropTypes from 'prop-types' import styled from 'styled-components' import { Text, Card, Badge, theme, unselectable } from '@aragon/ui' +import { AppType } from '../../prop-types' import { shortenAddress } from '../../web3-utils' -import AppIcon from './AppIcon' -import App from '../../types/App' +import AppIcon from '../../components/AppIcon/AppIcon' class AppCard extends React.PureComponent { static propTypes = { - app: App.isRequired, + app: AppType.isRequired, onOpen: PropTypes.func.isRequired, } @@ -17,12 +17,26 @@ class AppCard extends React.PureComponent { } render() { const { app } = this.props - const { name, identifier, proxyAddress } = app - const instanceLabel = identifier || shortenAddress(proxyAddress) + const { + name, + identifier, + isAragonOsInternalApp, + hasWebApp, + proxyAddress, + } = app + const instanceTitle = `Address: ${proxyAddress}` + const instanceLabel = isAragonOsInternalApp + ? 'System App' + : !hasWebApp + ? 'Background App' + : identifier || shortenAddress(proxyAddress) + return (
- +
+ +
{name || 'Unknown'} {instanceLabel} @@ -48,10 +62,6 @@ const Main = styled(Card).attrs({ width: '100%', height: '180px' })` cursor: pointer; ` -const AppIconCard = styled(AppIcon)` - margin-bottom: 5px; -` - const Name = styled.p` display: flex; width: 100%; diff --git a/src/apps/Permissions/AppIcon.js b/src/apps/Permissions/AppIcon.js deleted file mode 100644 index c03cb6fa3..000000000 --- a/src/apps/Permissions/AppIcon.js +++ /dev/null @@ -1,42 +0,0 @@ -import React from 'react' -import PropTypes from 'prop-types' -import styled from 'styled-components' -import { IconBlank } from '@aragon/ui' -import { appIconUrl } from '../../utils' -import RemoteIcon from '../../components/RemoteIcon' -import IconKernel from '../../icons/IconKernel' - -class AppIcon extends React.Component { - static propTypes = { - app: PropTypes.object, - size: PropTypes.number.isRequired, - } - - static defaultProps = { - size: 22, - app: null, - } - render() { - const { app, size, ...props } = this.props - return ( -
- {(() => { - if (app && app.isAragonOsInternalApp) { - return - } - if (app && app.baseUrl) { - return - } - return - })()} -
- ) - } -} - -const Main = styled.span` - display: flex; - align-items: center; -` - -export default AppIcon diff --git a/src/apps/Permissions/AppInstanceLabel.js b/src/apps/Permissions/AppInstanceLabel.js index b30c8b1af..fb57c626d 100644 --- a/src/apps/Permissions/AppInstanceLabel.js +++ b/src/apps/Permissions/AppInstanceLabel.js @@ -1,14 +1,15 @@ import React from 'react' import PropTypes from 'prop-types' import styled from 'styled-components' -import { Badge } from '@aragon/ui' +import { Badge, Viewport, breakpoint } from '@aragon/ui' +import { AppType, EthereumAddressType } from '../../prop-types' import { shortenAddress } from '../../web3-utils' -import AppIcon from './AppIcon' +import AppIcon from '../../components/AppIcon/AppIcon' class AppInstanceLabel extends React.PureComponent { static propTypes = { - app: PropTypes.object.isRequired, - proxyAddress: PropTypes.string.isRequired, + app: AppType.isRequired, + proxyAddress: EthereumAddressType.isRequired, showIcon: PropTypes.bool, } @@ -16,29 +17,68 @@ class AppInstanceLabel extends React.PureComponent { const { app, proxyAddress, showIcon = true } = this.props return (
- {showIcon && } + + {({ above }) => + above('medium') && + showIcon && ( +
+ +
+ ) + } +
{app ? app.name : 'Unknown'} - + {(app && app.identifier) || shortenAddress(proxyAddress)} - +
) } } const Main = styled.div` - display: flex; - align-items: center; + margin: auto; + + ${breakpoint( + 'medium', + ` + display: flex; + align-items: center; + text-align: left; + margin: unset; + ` + )} ` -const AppIconInRow = styled(AppIcon)` - height: 0; - margin-right: 10px; - margin-top: -1px; +const StyledBadge = styled(Badge.App)` + display: inline-block; + + ${breakpoint( + 'medium', + ` + display: inline; + ` + )} ` const AppName = styled.span` - margin-right: 10px; + display: block; + + ${breakpoint( + 'medium', + ` + display: inline; + margin-right: 10px; + ` + )} ` export default AppInstanceLabel diff --git a/src/apps/Permissions/AppPermissions.js b/src/apps/Permissions/AppPermissions.js index 79b22c3a0..3296ef987 100644 --- a/src/apps/Permissions/AppPermissions.js +++ b/src/apps/Permissions/AppPermissions.js @@ -1,29 +1,23 @@ import React from 'react' import PropTypes from 'prop-types' -import { - Button, - Table, - TableCell, - TableHeader, - TableRow, - Text, -} from '@aragon/ui' +import { Button, Table, TableRow, Text, Viewport } from '@aragon/ui' +import LocalIdentityBadge from '../../components/LocalIdentityBadge/LocalIdentityBadge' +import { TableHeader, TableCell, FirstTableCell, LastTableCell } from './Table' import { PermissionsConsumer } from '../../contexts/PermissionsContext' +import { AppType, EthereumAddressType } from '../../prop-types' import Section from './Section' import EmptyBlock from './EmptyBlock' import AppInstanceLabel from './AppInstanceLabel' -import IdentityBadge from '../../components/IdentityBadge' import EntityPermissions from './EntityPermissions' import AppRoles from './AppRoles' class AppPermissions extends React.PureComponent { static propTypes = { - address: PropTypes.string.isRequired, - app: PropTypes.object, // may not be available if still loading + address: EthereumAddressType.isRequired, + app: AppType, // may not be available if still loading loading: PropTypes.bool.isRequired, onManageRole: PropTypes.func.isRequired, } - render() { const { app, loading, address, onManageRole } = this.props return ( @@ -45,25 +39,33 @@ class AppPermissions extends React.PureComponent { : 'No permissions set.'} ) : ( - - - - - - } - > - {appPermissions.map(({ role, entity }, i) => ( - - ))} -
+ + {({ below }) => ( + + + + + + } + > + {appPermissions.map(({ role, entity }, i) => ( + + ))} +
+ )} +
)} } + return ( - ) @@ -115,11 +118,11 @@ class Row extends React.Component { const { role } = this.props return ( - + {role ? role.name : 'Unknown'} - + {this.renderEntity()} - + - + ) } diff --git a/src/apps/Permissions/AppRoles.js b/src/apps/Permissions/AppRoles.js index a1ea450d9..8c7efc917 100644 --- a/src/apps/Permissions/AppRoles.js +++ b/src/apps/Permissions/AppRoles.js @@ -1,21 +1,25 @@ import React from 'react' -import { - Button, - Table, - TableCell, - TableHeader, - TableRow, - Text, -} from '@aragon/ui' -import IdentityBadge from '../../components/IdentityBadge' +import PropTypes from 'prop-types' +import { Button, Table, TableRow, Text, Viewport } from '@aragon/ui' +import { AppType, EthereumAddressType } from '../../prop-types' +import { TableHeader, TableCell, FirstTableCell, LastTableCell } from './Table' +import LocalIdentityBadge from '../../components/LocalIdentityBadge/LocalIdentityBadge' +import { PermissionsConsumer } from '../../contexts/PermissionsContext' import Section from './Section' import EmptyBlock from './EmptyBlock' import AppInstanceLabel from './AppInstanceLabel' -import { PermissionsConsumer } from '../../contexts/PermissionsContext' import { isBurnEntity } from '../../permissions' import { isEmptyAddress } from '../../web3-utils' class AppRoles extends React.PureComponent { + static propTypes = { + app: AppType, + emptyLabel: PropTypes.string, + loading: PropTypes.bool.isRequired, + loadingLabel: PropTypes.string, + onManageRole: PropTypes.func.isRequired, + } + handleManageRole = roleBytes => { this.props.onManageRole(this.props.app.proxyAddress, roleBytes) } @@ -40,24 +44,32 @@ class AppRoles extends React.PureComponent { {loading || roles.length === 0 ? ( {loading ? loadingLabel : emptyLabel} ) : ( - - - - - - } - > - {roles.map(({ role, manager }, i) => ( - - ))} -
+ + {({ below }) => ( + + + + + + } + > + {roles.map(({ role, manager }, i) => ( + + ))} +
+ )} +
)} ) @@ -68,6 +80,14 @@ class AppRoles extends React.PureComponent { } class RoleRow extends React.Component { + static propTypes = { + onManage: PropTypes.func.isRequired, + role: PropTypes.shape({ bytes: PropTypes.string }).isRequired, + manager: PropTypes.shape({ + type: PropTypes.string, + address: EthereumAddressType, + }).isRequired, + } handleManageClick = () => { this.props.onManage(this.props.role.bytes) } @@ -78,10 +98,11 @@ class RoleRow extends React.Component { ) } - if (manager.type === 'burn') { - return - } - return + return ( + + ) } render() { const { role, manager } = this.props @@ -91,13 +112,13 @@ class RoleRow extends React.Component { return ( - + {name} - + {emptyManager ? 'No manager set' : this.renderManager()} - + - + ) } diff --git a/src/apps/Permissions/AssignPermissionPanel.js b/src/apps/Permissions/AssignPermissionPanel.js index d54990b08..759618102 100644 --- a/src/apps/Permissions/AssignPermissionPanel.js +++ b/src/apps/Permissions/AssignPermissionPanel.js @@ -2,6 +2,7 @@ import React from 'react' import PropTypes from 'prop-types' import { SidePanel, DropDown, Info, Field, Button } from '@aragon/ui' import { PermissionsConsumer } from '../../contexts/PermissionsContext' +import { AppType } from '../../prop-types' import { isAddress, isEmptyAddress } from '../../web3-utils' import AppInstanceLabel from './AppInstanceLabel' import EntitySelector from './EntitySelector' @@ -16,7 +17,7 @@ const DEFAULT_STATE = { // The permission panel, wrapped in a PermissionsContext (see end of file) class AssignPermissionPanel extends React.PureComponent { static propTypes = { - apps: PropTypes.array.isRequired, + apps: PropTypes.arrayOf(AppType).isRequired, grantPermission: PropTypes.func.isRequired, getAppRoles: PropTypes.func.isRequired, onClose: PropTypes.func.isRequired, @@ -152,11 +153,12 @@ class AssignPermissionPanel extends React.PureComponent { {selectedApp && ( diff --git a/src/apps/Permissions/EmptyBlock.js b/src/apps/Permissions/EmptyBlock.js index 7a63e5671..6d215ed9c 100644 --- a/src/apps/Permissions/EmptyBlock.js +++ b/src/apps/Permissions/EmptyBlock.js @@ -1,5 +1,5 @@ import styled from 'styled-components' -import { Card } from '@aragon/ui' +import { Card, breakpoint } from '@aragon/ui' const EmptyBlock = styled(Card)` display: flex; @@ -7,6 +7,18 @@ const EmptyBlock = styled(Card)` justify-content: center; width: 100%; height: 180px; + border-left-width: 0; + border-right-width: 0; + border-radius: 0; + + ${breakpoint( + 'medium', + ` + border-left-width: 1px; + border-right-width: 1px; + border-radius: 3px; + ` + )} ` export default EmptyBlock diff --git a/src/apps/Permissions/EntityPermissions.js b/src/apps/Permissions/EntityPermissions.js index 2fd89c2e2..72c31276d 100644 --- a/src/apps/Permissions/EntityPermissions.js +++ b/src/apps/Permissions/EntityPermissions.js @@ -1,21 +1,16 @@ import React from 'react' import PropTypes from 'prop-types' -import { - Button, - Table, - TableCell, - TableHeader, - TableRow, - Text, -} from '@aragon/ui' +import { Button, Table, TableRow, Text, Viewport } from '@aragon/ui' +import { TableHeader, TableCell, FirstTableCell, LastTableCell } from './Table' import Section from './Section' import EmptyBlock from './EmptyBlock' import AppInstanceLabel from './AppInstanceLabel' import { PermissionsConsumer } from '../../contexts/PermissionsContext' +import { EthereumAddressType } from '../../prop-types' class EntityPermissions extends React.PureComponent { static propTypes = { - address: PropTypes.string.isRequired, + address: EthereumAddressType.isRequired, loadPermissionsLabel: PropTypes.string, loading: PropTypes.bool.isRequired, noPermissionsLabel: PropTypes.string, @@ -24,7 +19,6 @@ class EntityPermissions extends React.PureComponent { static defaultProps = { loadPermissionsLabel: 'Loading entity permissions…', noPermissionsLabel: 'No permissions set.', - title: 'Permissions', } render() { const { @@ -46,30 +40,38 @@ class EntityPermissions extends React.PureComponent { {loading ? loadPermissionsLabel : noPermissionsLabel} ) : ( - - - - - - } - > - {roles.map( - ({ role, roleBytes, roleFrom, proxyAddress }, i) => ( - - ) + + {({ below }) => ( +
+ + + + + } + > + {roles.map( + ({ role, roleBytes, roleFrom, proxyAddress }, i) => ( + + ) + )} +
)} - + )} ) @@ -88,13 +90,13 @@ class Row extends React.Component { const { action, app, proxyAddress } = this.props return ( - + {action} - + - + - + ) } @@ -112,9 +114,9 @@ class Row extends React.Component { Row.propTypes = { action: PropTypes.string.isRequired, app: PropTypes.object.isRequired, - entityAddress: PropTypes.string.isRequired, + entityAddress: EthereumAddressType.isRequired, onRevoke: PropTypes.func.isRequired, - proxyAddress: PropTypes.string.isRequired, + proxyAddress: EthereumAddressType.isRequired, roleBytes: PropTypes.string.isRequired, } diff --git a/src/apps/Permissions/EntitySelector.js b/src/apps/Permissions/EntitySelector.js index ca9432b2a..d11d21481 100644 --- a/src/apps/Permissions/EntitySelector.js +++ b/src/apps/Permissions/EntitySelector.js @@ -2,24 +2,19 @@ import React from 'react' import PropTypes from 'prop-types' import { DropDown, Field, TextInput } from '@aragon/ui' import { getAnyEntity } from '../../permissions' +import { AppType } from '../../prop-types' import { getEmptyAddress } from '../../web3-utils' import AppInstanceLabel from './AppInstanceLabel' class EntitySelector extends React.Component { static propTypes = { activeIndex: PropTypes.number.isRequired, - apps: PropTypes.array.isRequired, + apps: PropTypes.arrayOf(AppType).isRequired, + includeAnyEntity: PropTypes.bool, label: PropTypes.string.isRequired, labelCustomAddress: PropTypes.string.isRequired, onChange: PropTypes.func.isRequired, } - - static defaultProps = { - apps: [], - activeIndex: 0, - label: 'Entity', - labelCustomAddress: 'Entity address', - } state = { customAddress: '', } @@ -58,7 +53,7 @@ class EntitySelector extends React.Component { return this.state.customAddress } - if (index === items.length - 2) { + if (this.props.includeAnyEntity && index === items.length - 2) { return getAnyEntity() } @@ -66,12 +61,19 @@ class EntitySelector extends React.Component { return (app && app.proxyAddress) || getEmptyAddress() } getItems() { - return [ + const { includeAnyEntity } = this.props + + const items = [ 'Select an entity', ...this.getAppsItems(), - 'Any account', 'Custom address…', ] + if (includeAnyEntity) { + // Add immediately before last item + items.splice(-1, 0, 'Any account') + } + + return items } render() { const { customAddress } = this.state diff --git a/src/apps/Permissions/Home/BrowseByApp.js b/src/apps/Permissions/Home/BrowseByApp.js index 2964a719d..5d525d5ab 100644 --- a/src/apps/Permissions/Home/BrowseByApp.js +++ b/src/apps/Permissions/Home/BrowseByApp.js @@ -1,14 +1,15 @@ import React from 'react' import PropTypes from 'prop-types' import styled from 'styled-components' +import { breakpoint } from '@aragon/ui' +import { AppType } from '../../../prop-types' import Section from '../Section' import AppCard from '../AppCard' import EmptyBlock from '../EmptyBlock' -import App from '../../../types/App' class BrowseByApp extends React.Component { static propTypes = { - apps: PropTypes.arrayOf(App).isRequired, + apps: PropTypes.arrayOf(AppType).isRequired, loading: PropTypes.bool.isRequired, onOpenApp: PropTypes.func.isRequired, } @@ -39,10 +40,19 @@ class BrowseByApp extends React.Component { const Apps = styled.div` display: grid; - grid-auto-flow: row; - grid-gap: 25px; - justify-items: start; - grid-template-columns: repeat(auto-fill, 160px); + grid-gap: 10px; + grid-template-columns: minmax(150px, 1fr) minmax(150px, 1fr); + margin: 0 20px; + + ${breakpoint( + 'medium', + ` + margin: unset; + grid-gap: 25px; + justify-items: start; + grid-template-columns: repeat(auto-fill, 160px); + ` + )} ` export default BrowseByApp diff --git a/src/apps/Permissions/Home/BrowseByEntity.js b/src/apps/Permissions/Home/BrowseByEntity.js index 36e883c97..82b37ca66 100644 --- a/src/apps/Permissions/Home/BrowseByEntity.js +++ b/src/apps/Permissions/Home/BrowseByEntity.js @@ -1,6 +1,6 @@ import React from 'react' import PropTypes from 'prop-types' -import { Table, TableHeader, TableRow } from '@aragon/ui' +import { Table, TableHeader, TableRow, Viewport } from '@aragon/ui' import Section from '../Section' import EmptyBlock from '../EmptyBlock' import EntityRow from './EntityRow' @@ -28,25 +28,35 @@ class BrowseByEntity extends React.Component { } return ( - - - - - - - } - > - {roles.map(({ entity, entityAddress, roles }) => ( - - ))} -
+ + {({ above, below }) => ( + + + + + + + ) + } + > + {roles.map(({ entity, entityAddress, roles }) => ( + + ))} +
+ )} +
) }} diff --git a/src/apps/Permissions/Home/EntityRow.js b/src/apps/Permissions/Home/EntityRow.js index 477409660..29fc8df2a 100644 --- a/src/apps/Permissions/Home/EntityRow.js +++ b/src/apps/Permissions/Home/EntityRow.js @@ -1,19 +1,34 @@ import React from 'react' import PropTypes from 'prop-types' +import styled from 'styled-components' import uniqBy from 'lodash.uniqby' -import { TableRow, TableCell, Button, Text, theme } from '@aragon/ui' -import IdentityBadge from '../../../components/IdentityBadge' +import { + TableCell, + TableRow, + Text, + Viewport, + breakpoint, + theme, +} from '@aragon/ui' +import LocalIdentityBadge from '../../../components/LocalIdentityBadge/LocalIdentityBadge' import AppInstanceLabel from '../AppInstanceLabel' +import ViewDetailsButton from './ViewDetailsButton' +import { FirstTableCell, LastTableCell } from '../Table' class EntityRow extends React.PureComponent { static propTypes = { + smallView: PropTypes.bool, entity: PropTypes.object.isRequired, onOpen: PropTypes.func.isRequired, roles: PropTypes.array.isRequired, } + static defaultProps = { + smallView: false, + } - handleClick = () => { - this.props.onOpen(this.props.entity.address) + open() { + const { onOpen, entity } = this.props + onOpen(entity.address) } renderType(type) { switch (type) { @@ -27,12 +42,12 @@ class EntityRow extends React.PureComponent { } renderEntity(entity) { if (entity.type === 'any') { - return + return } if (entity.type === 'app' && entity.app.name) { return } - return + return } roleTitle({ role, roleBytes, appEntity, proxyAddress }) { if (!appEntity || !appEntity.app) { @@ -66,27 +81,70 @@ class EntityRow extends React.PureComponent { )) } + handleDetailsClick = () => { + this.open() + } + handleRowClick = () => { + if (this.props.smallView) { + this.open() + } + } render() { - const { entity, roles } = this.props + const { entity, roles, smallView } = this.props if (!entity) { return null } return ( - - {this.renderEntity(entity)} - {this.renderType(entity.type)} - -
{this.renderRoles(roles)}
-
- - - -
+ + div { + display: inline-block; + } + `} + > + {this.renderEntity(entity)} + + {!smallView && ( + + {this.renderType(entity.type)} + +
{this.renderRoles(roles)}
+
+
+ )} + div { + max-width: unset; + } + `} + > + + +
) } } -export default EntityRow +const StyledTableRow = styled(TableRow)` + cursor: pointer; + + ${breakpoint( + 'medium', + ` + cursor: initial; + ` + )} +` + +export default props => ( + + {({ below }) => } + +) diff --git a/src/apps/Permissions/Home/Home.js b/src/apps/Permissions/Home/Home.js index 0d1783376..15af8a969 100644 --- a/src/apps/Permissions/Home/Home.js +++ b/src/apps/Permissions/Home/Home.js @@ -1,11 +1,12 @@ import React from 'react' import PropTypes from 'prop-types' +import { AppType } from '../../../prop-types' import BrowseByApp from './BrowseByApp' import BrowseByEntity from './BrowseByEntity' class Home extends React.Component { static propTypes = { - apps: PropTypes.array.isRequired, + apps: PropTypes.arrayOf(AppType).isRequired, appsLoading: PropTypes.bool.isRequired, permissionsLoading: PropTypes.bool.isRequired, onOpenApp: PropTypes.func.isRequired, diff --git a/src/apps/Permissions/Home/ViewDetailsButton.js b/src/apps/Permissions/Home/ViewDetailsButton.js new file mode 100644 index 000000000..255e2b714 --- /dev/null +++ b/src/apps/Permissions/Home/ViewDetailsButton.js @@ -0,0 +1,20 @@ +import React from 'react' +import { Button, ButtonIcon, IconArrowRight, Viewport } from '@aragon/ui' + +const ViewDetailsButton = props => ( + + {({ below }) => + below('medium') ? ( + + + + ) : ( + + ) + } + +) + +export default ViewDetailsButton diff --git a/src/apps/Permissions/ManageRolePanel.js b/src/apps/Permissions/ManageRolePanel.js index 69ca8493f..97b2ab362 100644 --- a/src/apps/Permissions/ManageRolePanel.js +++ b/src/apps/Permissions/ManageRolePanel.js @@ -1,10 +1,18 @@ import React from 'react' import PropTypes from 'prop-types' import styled from 'styled-components' -import { SidePanel, DropDown, Info, Field, Button } from '@aragon/ui' -import IdentityBadge from '../../components/IdentityBadge' +import { + SidePanel, + DropDown, + Info, + Field, + Button, + breakpoint, +} from '@aragon/ui' +import LocalIdentityBadge from '../../components/LocalIdentityBadge/LocalIdentityBadge' import { PermissionsConsumer } from '../../contexts/PermissionsContext' import { isBurnEntity } from '../../permissions' +import { AppType } from '../../prop-types' import { isAddress, isEmptyAddress } from '../../web3-utils' import AppInstanceLabel from './AppInstanceLabel' import EntitySelector from './EntitySelector' @@ -77,8 +85,8 @@ const DEFAULT_STATE = { // The role manager panel, wrapped in a PermissionsContext (see end of file) class ManageRolePanel extends React.PureComponent { static propTypes = { - app: PropTypes.object, - apps: PropTypes.array.isRequired, + app: AppType, + apps: PropTypes.arrayOf(AppType).isRequired, createPermission: PropTypes.func.isRequired, getRoleManager: PropTypes.func.isRequired, onClose: PropTypes.func.isRequired, @@ -172,11 +180,11 @@ class ManageRolePanel extends React.PureComponent { handleSubmit = () => { const { newRoleManagerValue, assignEntityAddress } = this.state const { + app, onClose, + createPermission, removePermissionManager, setPermissionManager, - createPermission, - app, role, } = this.props @@ -238,10 +246,11 @@ class ManageRolePanel extends React.PureComponent { ) } - if (manager.type === 'burn') { - return - } - return + return ( + + ) } render() { @@ -302,27 +311,28 @@ class ManageRolePanel extends React.PureComponent { )} {action === CREATE_PERMISSION && ( )} @@ -348,8 +358,15 @@ class ManageRolePanel extends React.PureComponent { } const FlexRow = styled.div` - display: flex; + display: inline-flex; align-items: center; + + ${breakpoint( + 'medium', + ` + display: flex; + ` + )} ` export default props => ( diff --git a/src/apps/Permissions/NavigationItem.js b/src/apps/Permissions/NavigationItem.js index ef625da33..b30fc7d89 100644 --- a/src/apps/Permissions/NavigationItem.js +++ b/src/apps/Permissions/NavigationItem.js @@ -1,26 +1,31 @@ import React from 'react' import PropTypes from 'prop-types' import styled from 'styled-components' -import { Badge } from '@aragon/ui' -import IdentityBadge from '../../components/IdentityBadge' +import { Badge, Viewport } from '@aragon/ui' +import LocalIdentityBadge from '../../components/LocalIdentityBadge/LocalIdentityBadge' +import { EthereumAddressType } from '../../prop-types' const NavigationItem = ({ title, badge, address, entity }) => { const isEntity = !badge && address return ( -
- {title} - {isEntity && ( - + + {({ above }) => ( +
+ {title} + {above('medium') && isEntity && ( + + )} + {badge && {badge.label}} +
)} - {badge && {badge.label}} -
+ ) } NavigationItem.propTypes = { - address: PropTypes.string, + address: EthereumAddressType, badge: PropTypes.object, entity: PropTypes.object, title: PropTypes.string.isRequired, diff --git a/src/apps/Permissions/Permissions.js b/src/apps/Permissions/Permissions.js index 27248008c..38fa1838d 100644 --- a/src/apps/Permissions/Permissions.js +++ b/src/apps/Permissions/Permissions.js @@ -1,15 +1,8 @@ import React from 'react' import PropTypes from 'prop-types' import styled from 'styled-components' -import { - AppBar, - AppView, - NavigationBar, - Button, - font, - breakpoint, - BreakPoint, -} from '@aragon/ui' +import { AppType } from '../../prop-types' +import { IconPlus } from '@aragon/ui' import { addressesEqual, shortenAddress, isAddress } from '../../web3-utils' import Screen from './Screen' import Home from './Home/Home' @@ -18,13 +11,14 @@ import EntityPermissions from './EntityPermissions' import NavigationItem from './NavigationItem' import AssignPermissionPanel from './AssignPermissionPanel' import ManageRolePanel from './ManageRolePanel' -import MenuButton from '../../components/MenuPanel/MenuButton' import { PermissionsConsumer } from '../../contexts/PermissionsContext' +import AppLayout from '../../components/AppLayout/AppLayout' class Permissions extends React.Component { static propTypes = { - apps: PropTypes.array.isRequired, + apps: PropTypes.arrayOf(AppType).isRequired, appsLoading: PropTypes.bool.isRequired, + onMessage: PropTypes.func.isRequired, onParamsRequest: PropTypes.func.isRequired, params: PropTypes.string, permissionsLoading: PropTypes.bool.isRequired, @@ -186,7 +180,6 @@ class Permissions extends React.Component { render() { const { apps, appsLoading, permissionsLoading, params } = this.props const { showAssignPermissionPanel, animateScreens } = this.state - const location = this.getLocation(params) return ( @@ -206,40 +199,18 @@ class Permissions extends React.Component { return ( - - Add permission - - } - > - - {navigationItems.length === 1 ? ( - - - Permissions - - ) : ( - - )} - - - - - - } + , + label: 'Add permission', + onClick: this.createPermission, + disabled: appsLoading || permissionsLoading, + }} > { @@ -247,16 +218,7 @@ class Permissions extends React.Component { }} /> -
+ {location.screen === 'home' && ( )} -
-
+ +
@@ -314,21 +276,14 @@ class Permissions extends React.Component { } } -const AppBarTitle = styled.span` - display: flex; - align-items: center; -` - -const AppBarLabel = styled.span` - margin: 0 10px 0 8px; - ${font({ size: 'xxlarge' })}; - - ${breakpoint( - 'medium', - ` - margin-left: 24px; - ` - )}; +const Wrap = styled.div` + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + overflowx: hidden; + min-width: 320px; ` // This element is only used to reset the view scroll using scrollIntoView() diff --git a/src/apps/Permissions/Screen.js b/src/apps/Permissions/Screen.js index 0b9c58578..c27d5f45f 100644 --- a/src/apps/Permissions/Screen.js +++ b/src/apps/Permissions/Screen.js @@ -2,6 +2,7 @@ import React from 'react' import PropTypes from 'prop-types' import styled from 'styled-components' import { Transition, animated } from 'react-spring' +import { breakpoint } from '@aragon/ui' import springs from '../../springs' const SCREEN_SHIFT = 0.05 @@ -55,7 +56,13 @@ const StyledMain = styled(animated.div)` top: 0; left: 0; right: 0; - padding: 30px; + + ${breakpoint( + 'medium', + ` + padding: 30px; + ` + )} ` export default Screen diff --git a/src/apps/Permissions/Section.js b/src/apps/Permissions/Section.js index d1062c042..3adeb560e 100644 --- a/src/apps/Permissions/Section.js +++ b/src/apps/Permissions/Section.js @@ -1,6 +1,7 @@ import React from 'react' import PropTypes from 'prop-types' import styled from 'styled-components' +import { breakpoint } from '@aragon/ui' const Section = ({ title, children }) => (
@@ -15,12 +16,27 @@ Section.propTypes = { } const Main = styled.section` + margin-bottom: 30px; + > h1 { - margin-bottom: 30px; - font-weight: 600; - } - & + & { - margin-top: 50px; + margin: 20px; } + + ${breakpoint( + 'medium', + ` + margin-bottom: 0; + + > h1 { + margin: 0; + margin-bottom: 30px; + font-weight: 600; + } + + & + & { + margin-top: 50px; + } + ` + )} ` export default Section diff --git a/src/apps/Permissions/Table.js b/src/apps/Permissions/Table.js new file mode 100644 index 000000000..490528107 --- /dev/null +++ b/src/apps/Permissions/Table.js @@ -0,0 +1,59 @@ +import { TableHeader, TableCell, breakpoint } from '@aragon/ui' +import styled from 'styled-components' + +const StyledTableHeader = styled(TableHeader)` + padding-left: 0; + text-align: center; + + ${breakpoint( + 'medium', + ` + padding-left: 20px; + text-align: unset; + ` + )} +` + +const StyledTableCell = styled(TableCell)` + padding: 20px 10px; + width: 33.333333%; + + > div { + display: block; + max-width: 33.333333vw; + text-align: center; + } + + ${breakpoint( + 'medium', + ` + width: auto; + padding: 20px; + + > div { + display: inline-flex; + max-width: unset; + text-align: unset; + } + ` + )} +` + +const FirstTableCell = styled(StyledTableCell)` + > div { + text-align: left; + } +` + +const LastTableCell = styled(StyledTableCell)` + > div { + text-align: right; + } +` + +export { + StyledTableHeader as TableHeader, + StyledTableCell as TableCell, + FirstTableCell, + LastTableCell, +} diff --git a/src/apps/Permissions/assets/kernel-icon.svg b/src/apps/Permissions/assets/kernel-icon.svg deleted file mode 100644 index b00d3c7b5..000000000 --- a/src/apps/Permissions/assets/kernel-icon.svg +++ /dev/null @@ -1 +0,0 @@ -Artboard \ No newline at end of file diff --git a/src/apps/Settings/DaoSettings.js b/src/apps/Settings/DaoSettings.js index 4acebd5c1..cd4d60581 100644 --- a/src/apps/Settings/DaoSettings.js +++ b/src/apps/Settings/DaoSettings.js @@ -1,11 +1,11 @@ import React from 'react' import PropTypes from 'prop-types' import styled from 'styled-components' -import { Button, Field, Text, IdentityBadge, BreakPoint } from '@aragon/ui' +import { Button, Text, Viewport, theme } from '@aragon/ui' +import LocalIdentityBadge from '../../components/LocalIdentityBadge/LocalIdentityBadge' import { appIds, network } from '../../environment' import { sanitizeNetworkType } from '../../network-config' -import { noop } from '../../utils' -import { DaoAddressType } from '../../prop-types' +import { AppType, DaoAddressType, EthereumAddressType } from '../../prop-types' import { toChecksumAddress } from '../../web3-utils' import airdrop, { testTokensEnabled } from '../../testnet/airdrop' import Option from './Option' @@ -15,21 +15,19 @@ const AppsList = styled.ul` list-style: none; ` -class DaoSettings extends React.Component { +class DaoSettings extends React.PureComponent { static propTypes = { - account: PropTypes.string.isRequired, - apps: PropTypes.array.isRequired, + account: EthereumAddressType, + apps: PropTypes.arrayOf(AppType).isRequired, + appsLoading: PropTypes.bool.isRequired, daoAddress: DaoAddressType.isRequired, onOpenApp: PropTypes.func.isRequired, - shorten: PropTypes.bool.isRequired, + shortAddresses: PropTypes.bool, walletNetwork: PropTypes.string.isRequired, - walletWeb3: PropTypes.object, + walletWeb3: PropTypes.object.isRequired, } static defaultProps = { - account: '', - apps: [], - onOpenApp: noop, - shorten: false, + shortAddresses: false, } handleDepositTestTokens = () => { const { account, apps, walletWeb3 } = this.props @@ -46,38 +44,49 @@ class DaoSettings extends React.Component { } } render() { - const { account, apps, daoAddress, shorten, walletNetwork } = this.props + const { + account, + apps, + appsLoading, + daoAddress, + shortAddresses, + walletNetwork, + } = this.props const enableTransactions = !!account && walletNetwork === network.type const financeApp = apps.find(({ name }) => name === 'Finance') const checksummedDaoAddr = daoAddress.address && toChecksumAddress(daoAddress.address) - const webApps = apps.filter(app => app.hasWebApp) + const apmApps = apps.filter(app => !app.isAragonOsInternalApp) return (
{testTokensEnabled(network.type) && ( )} - {webApps.length > 0 && ( + {appsLoading && ( +
) +LoadingRing.propTypes = { + spin: PropTypes.bool, +} const Main = styled.span` position: relative; diff --git a/src/components/LocalIdentityBadge/LocalIdentityBadge.js b/src/components/LocalIdentityBadge/LocalIdentityBadge.js new file mode 100644 index 000000000..7bd3ddecd --- /dev/null +++ b/src/components/LocalIdentityBadge/LocalIdentityBadge.js @@ -0,0 +1,108 @@ +import React from 'react' +import PropTypes from 'prop-types' +import styled from 'styled-components' +import { Badge, IdentityBadge, font } from '@aragon/ui' +import { LocalIdentityModalContext } from '../LocalIdentityModal/LocalIdentityModalManager' +import { isAddress } from '../../web3-utils' +import { + IdentityContext, + identityEventTypes, +} from '../IdentityManager/IdentityManager' + +const LocalIdentityBadge = ({ entity, ...props }) => { + const address = isAddress(entity) ? entity : null + if (address === null) { + return + } + + const { resolve, identityEvents$ } = React.useContext(IdentityContext) + const { showLocalIdentityModal } = React.useContext(LocalIdentityModalContext) + const [label, setLabel] = React.useState() + const handleResolve = async () => { + try { + const { name = null } = await resolve(address) + setLabel(name) + } catch (e) { + // address does not ressolve to identity + } + } + const handleClick = () => { + showLocalIdentityModal(address) + .then(handleResolve) + .catch(e => { + /* user cancelled modify intent */ + }) + } + const handleEvent = updatedAddress => { + if (updatedAddress.toLowerCase() === address.toLowerCase()) { + handleResolve() + } + } + const clearLabel = () => { + setLabel(null) + } + React.useEffect(() => { + handleResolve() + const subscription = identityEvents$.subscribe(event => { + switch (event.type) { + case identityEventTypes.MODIFY: + return handleEvent(event.address) + case identityEventTypes.CLEAR: + return clearLabel() + case identityEventTypes.IMPORT: + return handleResolve() + } + }) + return () => { + subscription.unsubscribe() + } + }, []) + + return ( + +
{label}
+ Custom label + + ) : ( + 'Address' + ) + } + /> + ) +} + +LocalIdentityBadge.propTypes = { + entity: PropTypes.string, +} + +const Wrap = styled.div` + display: grid; + align-items: center; + grid-template-columns: auto 1fr; + padding-right: 24px; +` + +const Address = styled.span` + display: inline-block; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +` + +const StyledBadge = styled(Badge)` + margin-left: 16px; + text-transform: uppercase; + ${font({ size: 'xxsmall' })}; +` + +export default LocalIdentityBadge diff --git a/src/components/LocalIdentityModal/LocalIdentityModal.js b/src/components/LocalIdentityModal/LocalIdentityModal.js new file mode 100644 index 000000000..38d7f3dc8 --- /dev/null +++ b/src/components/LocalIdentityModal/LocalIdentityModal.js @@ -0,0 +1,164 @@ +import React from 'react' +import PropTypes from 'prop-types' +import styled from 'styled-components' +import { + Button, + IdentityBadge, + TextInput, + breakpoint, + font, + theme, +} from '@aragon/ui' +import { ModalContext } from '../ModalManager/ModalManager' +import EscapeOutside from '../EscapeOutside/EscapeOutside' + +const LocalIdentityModal = ({ opened, ...props }) => { + const { showModal, hideModal } = React.useContext(ModalContext) + React.useEffect(() => { + opened ? showModal(Modal, props) : hideModal() + }, [opened]) + + return null +} + +LocalIdentityModal.propTypes = { + opened: PropTypes.bool.isRequired, +} + +const Modal = ({ address, label, onCancel, onSave }) => { + const [action, setAction] = React.useState() + const labelInput = React.useRef(null) + const handleCancel = () => { + onCancel() + } + const [error, setError] = React.useState(null) + const handleSave = () => { + try { + const label = labelInput.current.value.trim() + if (label) { + onSave({ address, label }) + } + } catch (e) { + setError(e) + } + } + const handlekeyDown = e => { + if (e.keyCode === 13) { + handleSave() + } + } + React.useEffect(() => { + setAction(label && label.trim() ? 'Edit' : 'Add') + labelInput.current.focus() + labelInput.current.select() + window.addEventListener('keydown', handlekeyDown) + return () => window.removeEventListener('keydown', handlekeyDown) + }, []) + + return ( + + + {action} custom label + + This label would be displayed instead of the following address and + only be stored on this device. + + + + + + + Save + + + + + ) +} + +Modal.propTypes = { + address: PropTypes.string, + label: PropTypes.string, + onCancel: PropTypes.func, + onSave: PropTypes.func, +} + +const Error = styled.div` + color: #f56a6a; + text-transform: initial; +` + +const Wrap = styled.div` + background: #fff; + padding: 16px; + max-width: calc(100vw - 32px); + + ${breakpoint( + 'medium', + ` + padding: 16px 32px; + max-width: 50vw; + /* wide identity badge + paddings */ + min-width: calc(400px + 32px * 2); + ` + )} +` + +const Title = styled.h3` + ${font({ size: 'xlarge' })}; +` + +const Description = styled.p` + margin: 20px 0; + & span { + font-weight: bold; + } +` + +const Label = styled.label` + display: block; + margin: 20px 0; + text-transform: uppercase; + color: ${theme.textSecondary}; + ${font({ size: 'xsmall' })}; + + & > div { + margin: 5px 0; + } +` + +const Controls = styled.div` + display: grid; + grid-gap: 10px; + grid-template-columns: 1fr 1fr; + + ${breakpoint( + 'medium', + ` + display: flex; + justify-content: flex-end; + ` + )} +` + +const StyledSaveButton = styled(Button)` + ${breakpoint( + 'medium', + ` + margin-left: 16px; + ` + )} +` + +export default LocalIdentityModal diff --git a/src/components/LocalIdentityModal/LocalIdentityModalManager.js b/src/components/LocalIdentityModal/LocalIdentityModalManager.js new file mode 100644 index 000000000..06e9a2ec4 --- /dev/null +++ b/src/components/LocalIdentityModal/LocalIdentityModalManager.js @@ -0,0 +1,27 @@ +import React from 'react' +import PropTypes from 'prop-types' + +const LocalIdentityModalContext = React.createContext({}) + +const LocalIdentityModalProvider = ({ onShowLocalIdentityModal, children }) => { + return ( + + {children} + + ) +} + +LocalIdentityModalProvider.propTypes = { + onShowLocalIdentityModal: PropTypes.func.isRequired, + children: PropTypes.node.isRequired, +} + +const LocalIdentityModalConsumer = LocalIdentityModalContext.Consumer + +export { + LocalIdentityModalProvider, + LocalIdentityModalConsumer, + LocalIdentityModalContext, +} diff --git a/src/components/MenuPanel/MenuButton.js b/src/components/MenuPanel/MenuButton.js index a897f97be..26b4ac989 100644 --- a/src/components/MenuPanel/MenuButton.js +++ b/src/components/MenuPanel/MenuButton.js @@ -1,31 +1,17 @@ import React from 'react' -import styled from 'styled-components' -import { theme } from '@aragon/ui' -import IconMenu from '../../icons/IconMenu' - -const StyledButton = styled.button` - border: none; - background: none; - margin-left: 24px; - height: 32px; - width: 32px; - display: flex; - align-items: center; - justify-content: center; - padding: 0; - cursor: pointer; - outline: none; - - &:focus { - border: 2px solid ${theme.accent}; - } - &:active { - border: none; - } -` +import { ButtonIcon, IconMenu } from '@aragon/ui' export default props => ( - + - + ) diff --git a/src/components/MenuPanel/MenuPanel.js b/src/components/MenuPanel/MenuPanel.js index 34e6fadee..080d26a94 100644 --- a/src/components/MenuPanel/MenuPanel.js +++ b/src/components/MenuPanel/MenuPanel.js @@ -1,34 +1,44 @@ import React from 'react' import PropTypes from 'prop-types' import styled from 'styled-components' -import { Spring, animated } from 'react-spring' +import { Transition, Spring, animated } from 'react-spring' +import throttle from 'lodash.throttle' import { - Text, - theme, - unselectable, + ButtonBase, + Button, + IconSettings, + Viewport, breakpoint, - BreakPoint, springs, + theme, + unselectable, } from '@aragon/ui' import memoize from 'lodash.memoize' -import { appIconUrl } from '../../utils' -import { DaoAddressType } from '../../prop-types' +import { AppType, AppsStatusType, DaoAddressType } from '../../prop-types' import { staticApps } from '../../static-apps' +import MenuPanelFooter from './MenuPanelFooter' import MenuPanelAppGroup from './MenuPanelAppGroup' import MenuPanelAppsLoader from './MenuPanelAppsLoader' -import RemoteIcon from '../RemoteIcon' import NotificationAlert from '../Notifications/NotificationAlert' import OrganizationSwitcher from './OrganizationSwitcher/OrganizationSwitcher' -import { - APPS_STATUS_ERROR, - APPS_STATUS_READY, - APPS_STATUS_LOADING, -} from '../../symbols' +import AppIcon from '../AppIcon/AppIcon' +import IconArrow from '../../icons/IconArrow' const APP_APPS_CENTER = staticApps.get('apps').app const APP_HOME = staticApps.get('home').app const APP_PERMISSIONS = staticApps.get('permissions').app const APP_SETTINGS = staticApps.get('settings').app +const SHADOW_WIDTH = 15 + +const systemAppsOpenedState = { + key: 'SYSTEM_APPS_OPENED_STATE', + isOpen: function() { + return localStorage.getItem(this.key) === '1' + }, + set: function(opened) { + localStorage.setItem(this.key, opened ? '1' : '0') + }, +} const prepareAppGroups = apps => apps.reduce((groups, app) => { @@ -45,7 +55,7 @@ const prepareAppGroups = apps => { appId: app.appId, name: app.name, - icon: , + icon: , instances: [instance], }, ]) @@ -53,45 +63,85 @@ const prepareAppGroups = apps => class MenuPanel extends React.PureComponent { static propTypes = { - apps: PropTypes.array.isRequired, - appsStatus: PropTypes.oneOf([ - APPS_STATUS_ERROR, - APPS_STATUS_READY, - APPS_STATUS_LOADING, - ]).isRequired, activeInstanceId: PropTypes.string, - onOpenApp: PropTypes.func.isRequired, - onNotificationClicked: PropTypes.func.isRequired, - onRequestAppsReload: PropTypes.func.isRequired, - daoAddress: DaoAddressType.isRequired, + apps: PropTypes.arrayOf(AppType).isRequired, + appsStatus: AppsStatusType.isRequired, connected: PropTypes.bool.isRequired, + daoAddress: DaoAddressType.isRequired, notifications: PropTypes.number, + onNotificationClicked: PropTypes.func.isRequired, + onOpenApp: PropTypes.func.isRequired, + onOpenPreferences: PropTypes.func.isRequired, + onRequestAppsReload: PropTypes.func.isRequired, + viewportHeight: PropTypes.number, } + _animateTimer = -1 + _contentRef = React.createRef() + _innerContentRef = React.createRef() + state = { notifications: [], + systemAppsOpened: systemAppsOpenedState.isOpen(), + animate: false, + scrollVisible: false, } + componentDidMount() { + this._animateTimer = setTimeout(() => this.setState({ animate: true }), 0) + } + componentWillUnmount() { + clearTimeout(this._animateTimer) + } + componentDidUpdate(prevProps) { + if (prevProps.viewportHeight !== this.props.viewportHeight) { + this.updateScrollVisible() + } + } + + // ResizeObserver is still not supported everywhere, so… this method checks + // if the height of the content is higher than the height of the container, + // which means that there is a scrollbar displayed. + // It is called in two cases: when the viewport’s height changes, and when + // the system menu open / close transition is running. + updateScrollVisible = throttle(() => { + const content = this._contentRef.current + const innerContent = this._innerContentRef.current + this.setState({ + scrollVisible: + content && + innerContent && + innerContent.clientHeight > content.clientHeight, + }) + }, 100) + getAppGroups = memoize(apps => prepareAppGroups(apps)) + handleToggleSystemApps = () => { + this.setState( + ({ systemAppsOpened }) => ({ + systemAppsOpened: !systemAppsOpened, + }), + () => systemAppsOpenedState.set(this.state.systemAppsOpened) + ) + } + render() { const { apps, connected, daoAddress, onNotificationClicked, + onOpenPreferences, notifications, notificationOpen, } = this.props + const { animate, scrollVisible, systemAppsOpened } = this.state const appGroups = this.getAppGroups(apps) - const menuApps = [ - APP_HOME, - appGroups, - APP_PERMISSIONS, - APP_APPS_CENTER, - APP_SETTINGS, - ] + const menuApps = [APP_HOME, appGroups] + + const systemApps = [APP_PERMISSIONS, APP_APPS_CENTER, APP_SETTINGS] return (
@@ -109,9 +159,10 @@ class MenuPanel extends React.PureComponent { notificationOpen={notificationOpen} /> - -
+ +

Apps

+
{menuApps.map(app => // If it's an array, it's the group being loaded from the ACL @@ -120,20 +171,75 @@ class MenuPanel extends React.PureComponent { : this.renderAppGroup(app, false) )}
+ +

+ System + + + +

+
+ + {show => + show && + (props => ( + + {systemApps.map(app => this.renderAppGroup(app, true))} + + )) + } +
- - - - {connected ? 'Connected to the network' : 'Not connected'} - - + {scrollVisible && ( +
+ )} + + + + Preferences + +
) } - renderAppGroup(app) { + renderAppGroup(app, isSystem) { const { activeInstanceId, onOpenApp } = this.props const { appId, name, icon, instances = [] } = app @@ -147,6 +253,7 @@ class MenuPanel extends React.PureComponent { { - return ( - - - {({ progress }) => ( - `translate3d(${-100 * (1 - v)}%, 0, 0)` - ), - opacity: progress.interpolate(v => (v > 0 ? 1 : 0)), - }} - > - - - )} - - - - ) +class AnimatedMenuPanel extends React.Component { + state = { + animate: false, + } + _animateTimer = -1 + componentDidMount() { + this.setState({ animate: this.props.autoClosing }) + } + componentDidUpdate(prevProps) { + this.updateAnimate(prevProps) + } + componentWillUnmount() { + clearTimeout(this._animateTimer) + } + updateAnimate(prevProps) { + if (prevProps.autoClosing === this.props.autoClosing) { + return + } + + // If we autoclosing has changed, it means we are switching from + // autoclosing to fixed or the opposite, and we should stop animating the + // panel for a short period of time. + this.setState({ animate: false }) + this._animateTimer = setTimeout(() => { + this.setState({ animate: true }) + }, 0) + } + + render() { + const { animate } = this.state + const { openProgress, onCloseMenuPanel, ...props } = this.props + return ( + + + {({ progress }) => ( + + ` + translate3d( + calc( + ${-100 * (1 - v)}% - + ${SHADOW_WIDTH * (1 - v)}px + ), + 0, 0 + ) + ` + ), + opacity: progress.interpolate(v => Number(v > 0)), + }} + > + + {({ height }) => ( + + )} + + + )} + + + {({ below }) => + below('medium') && ( + + ) + } + + + ) + } } -const Overlay = styled.div` - position: absolute; - z-index: 2; - width: 100vw; - height: 100vh; - display: ${({ opened }) => (opened ? 'block' : 'none')}; +AnimatedMenuPanel.propTypes = { + autoClosing: PropTypes.bool, + openProgress: PropTypes.number.isRequired, + onCloseMenuPanel: PropTypes.func.isRequired, +} + +const SystemAppsToggle = styled(ButtonBase)` + padding: 0; + margin: 0; + margin-top: 20px; + background: none; + border: none; + cursor: pointer; + width: 100%; + text-align: left; + outline: none; +` + +const PreferencesWrap = styled.div` + text-align: left; ${breakpoint( 'medium', ` - display: none; + text-align: center; ` - )}; + )} +` + +const StyledPreferencesButton = styled(Button)` + display: inline-flex; + margin: 0 16px 16px 16px; + align-items: center; + + ${breakpoint( + 'medium', + ` + margin: 0 0 16px 0; + ` + )} +` + +const Overlay = styled.div` + position: absolute; + z-index: 2; + /* by leaving a 1px edge Android users can swipe to open + * from the edge of their screen when an iframe app is being + * used */ + width: ${({ opened }) => (opened ? '100vw' : '1px')}; + height: 100vh; ` const Wrap = styled(animated.div)` @@ -240,12 +433,15 @@ const Wrap = styled(animated.div)` z-index: 3; width: 90vw; height: 100vh; + min-width: 300px; + flex: none; ${breakpoint( 'medium', ` position: relative; width: 220px; + min-width: 0; ` )}; ` @@ -254,9 +450,8 @@ const Main = styled.div` width: 100%; height: 100%; display: flex; + flex: none; flex-direction: column; - flex-grow: 0; - flex-shrink: 0; ${unselectable}; ` @@ -269,7 +464,7 @@ const In = styled.div` flex-shrink: 1; background: #fff; border-right: 1px solid #e8e8e8; - box-shadow: 1px 0 15px rgba(0, 0, 0, 0.1); + box-shadow: 1px 0 ${SHADOW_WIDTH}px rgba(0, 0, 0, 0.1); ` const Header = styled.div` @@ -319,28 +514,4 @@ const Content = styled.nav` } ` -const ConnectionWrapper = styled.div` - margin: 15px 20px; -` - -const ConnectionBullet = styled.span` - width: 8px; - height: 8px; - margin-top: -2px; - margin-right: 8px; - border-radius: 50%; - display: inline-block; - background: ${({ connected }) => - connected ? theme.positive : theme.negative}; -` - -export default props => ( - - - - - - - - -) +export default AnimatedMenuPanel diff --git a/src/components/MenuPanel/MenuPanelAppGroup.js b/src/components/MenuPanel/MenuPanelAppGroup.js index ee8880373..51c3609fc 100644 --- a/src/components/MenuPanel/MenuPanelAppGroup.js +++ b/src/components/MenuPanel/MenuPanelAppGroup.js @@ -2,7 +2,7 @@ import React from 'react' import PropTypes from 'prop-types' import styled from 'styled-components' import { Spring, animated } from 'react-spring' -import { theme, IconBlank } from '@aragon/ui' +import { theme } from '@aragon/ui' import color from 'onecolor' import MenuPanelInstance from './MenuPanelInstance' import springs from '../../springs' @@ -13,6 +13,7 @@ class MenuPanelAppGroup extends React.PureComponent { activeInstanceId: PropTypes.string, expand: PropTypes.bool.isRequired, icon: PropTypes.object.isRequired, + system: PropTypes.bool, instances: PropTypes.array.isRequired, name: PropTypes.string.isRequired, onActivate: PropTypes.func.isRequired, @@ -34,6 +35,7 @@ class MenuPanelAppGroup extends React.PureComponent { const { name, icon, + system, instances, activeInstanceId, active, @@ -47,7 +49,7 @@ class MenuPanelAppGroup extends React.PureComponent { native > {({ openProgress }) => ( -
+
- {icon || } + {icon} {name} @@ -136,6 +138,12 @@ const Main = styled.div` margin-right: 15px; color: ${({ active }) => active ? theme.textPrimary : theme.textSecondary}; + filter: ${({ system }) => + system ? `brightness(${({ active }) => (active ? 0 : 100)}%)` : 'none'}; + + & > img { + border-radius: 5px; + } } .instances { overflow: hidden; diff --git a/src/components/MenuPanel/MenuPanelAppsLoader.js b/src/components/MenuPanel/MenuPanelAppsLoader.js index 91722274f..b476d8268 100644 --- a/src/components/MenuPanel/MenuPanelAppsLoader.js +++ b/src/components/MenuPanel/MenuPanelAppsLoader.js @@ -4,8 +4,9 @@ import styled from 'styled-components' import { Spring, animated } from 'react-spring' import { IconError, Button, theme } from '@aragon/ui' import color from 'onecolor' -import { noop } from '../../utils' +import { AppsStatusType } from '../../prop-types' import springs from '../../springs' +import { noop } from '../../utils' import LoadingRing from '../LoadingRing' import { APPS_STATUS_ERROR, @@ -15,11 +16,7 @@ import { class MenuPanelAppsLoader extends React.Component { static propTypes = { - appsStatus: PropTypes.oneOf([ - APPS_STATUS_ERROR, - APPS_STATUS_READY, - APPS_STATUS_LOADING, - ]).isRequired, + appsStatus: AppsStatusType.isRequired, children: PropTypes.func.isRequired, expandedInstancesCount: PropTypes.number.isRequired, appsCount: PropTypes.number.isRequired, diff --git a/src/components/MenuPanel/MenuPanelFooter.js b/src/components/MenuPanel/MenuPanelFooter.js new file mode 100644 index 000000000..f7dd59ffd --- /dev/null +++ b/src/components/MenuPanel/MenuPanelFooter.js @@ -0,0 +1,30 @@ +import React from 'react' +import PropTypes from 'prop-types' +import styled from 'styled-components' +import { Text, theme } from '@aragon/ui' + +const MenuPanelFooter = ({ connected }) => ( +
+ + + {connected ? 'Connected to the network' : 'Not connected'} + +
+) + +MenuPanelFooter.propTypes = { + connected: PropTypes.bool, +} + +const ConnectionBullet = styled.span` + width: 8px; + height: 8px; + margin-top: -2px; + margin-right: 8px; + border-radius: 50%; + display: inline-block; + background: ${({ connected }) => + connected ? theme.positive : theme.negative}; +` + +export default MenuPanelFooter diff --git a/src/components/MenuPanel/OrganizationSwitcher/OrganizationItem.js b/src/components/MenuPanel/OrganizationSwitcher/OrganizationItem.js index f58fef9bd..923202641 100644 --- a/src/components/MenuPanel/OrganizationSwitcher/OrganizationItem.js +++ b/src/components/MenuPanel/OrganizationSwitcher/OrganizationItem.js @@ -1,8 +1,10 @@ import React from 'react' import PropTypes from 'prop-types' import styled from 'styled-components' +import { EthIdenticon } from '@aragon/ui' import { DaoItemType } from '../../../prop-types' -import Identicon from '../../Identicon' +import { getKnownOrganization } from '../../../known-organizations' +import { network } from '../../../environment' class OrganizationItem extends React.Component { static propTypes = { @@ -15,12 +17,26 @@ class OrganizationItem extends React.Component { const styleProps = {} if (style) styleProps.style = style if (className) styleProps.className = className + const knownOrg = getKnownOrganization(network.type, dao.address) return ( - - - - {dao.name || dao.address} + + {knownOrg ? ( + + ) : ( + + )} + + {knownOrg ? knownOrg.name : dao.name || dao.address} ) } @@ -32,13 +48,17 @@ const Organization = styled.div` padding: 10px 20px; ` -const OrgIdenticon = styled.span` +const OrgIcon = styled.div` + overflow: hidden; + width: 24px; + height: 24px; flex-shrink: 0; flex-grow: 0; display: inline-flex; - border-radius: 50%; - overflow: hidden; + justify-content: center; + align-items: center; margin-right: 15px; + border-radius: ${p => (p.rounded ? '50%' : '0')}; ` const OrgName = styled.span` diff --git a/src/components/MenuPanel/OrganizationSwitcher/OrganizationSwitcher.js b/src/components/MenuPanel/OrganizationSwitcher/OrganizationSwitcher.js index b9b9eeb62..8009933fa 100644 --- a/src/components/MenuPanel/OrganizationSwitcher/OrganizationSwitcher.js +++ b/src/components/MenuPanel/OrganizationSwitcher/OrganizationSwitcher.js @@ -97,7 +97,7 @@ const LoaderLabel = styled.span` font-size: 15px; ` -export default props => +const OrganizationSwitcherWithFavorites = props => props.currentDao.address ? ( {({ favoriteDaos, updateFavoriteDaos }) => ( @@ -114,3 +114,9 @@ export default props => Loading… ) + +OrganizationSwitcherWithFavorites.propTypes = { + currentDao: DaoItemType.isRequired, +} + +export default OrganizationSwitcherWithFavorites diff --git a/src/components/MenuPanel/SwipeContainer.js b/src/components/MenuPanel/SwipeContainer.js new file mode 100644 index 000000000..862f21814 --- /dev/null +++ b/src/components/MenuPanel/SwipeContainer.js @@ -0,0 +1,117 @@ +import React from 'react' +import PropTypes from 'prop-types' +import styled from 'styled-components' +import { Gesture } from 'react-with-gesture' +import { Viewport } from '@aragon/ui' + +const THRESHOLD_VERTICAL_TOLERANCE = 10 +const THRESHOLD_DIRECTION = 0.2 +const THRESHOLD_PROGRESS = 0.5 + +class SwipeContainer extends React.Component { + static propTypes = { + children: PropTypes.func.isRequired, + enabled: PropTypes.bool.isRequired, + menuPanelOpened: PropTypes.bool.isRequired, + onMenuPanelClose: PropTypes.func.isRequired, + onMenuPanelOpen: PropTypes.func.isRequired, + width: PropTypes.number.isRequired, + } + + _previousProgress = 0 + + render() { + const { + children, + enabled, + menuPanelOpened, + onMenuPanelClose, + onMenuPanelOpen, + width, + } = this.props + + const oneThirdWindowWidth = width / 3 + + return ( + + {({ + delta: [xDelta, yDelta], + direction: [xDir, yDir], + down, + event, + initial: [xInitial], + xy: [x], + }) => { + if (!enabled) { + return {children(Number(menuPanelOpened))} + } + + if ( + !down && + this._previousProgress > 0 && + this._previousProgress < 1 + ) { + this._previousProgress = Number( + this._previousProgress > THRESHOLD_PROGRESS + ) + setTimeout( + this._previousProgress + ? () => { + // reset for menu buttons to work + this._previousProgress = 0 + onMenuPanelOpen() + } + : onMenuPanelClose, + 0 + ) + return {children(this._previousProgress)} + } + + let progress = this._previousProgress || Number(menuPanelOpened) + + if ( + (progress > 0 && progress < 1) || + (down && + xDelta !== 0 && + yDelta > -THRESHOLD_VERTICAL_TOLERANCE && + yDelta < THRESHOLD_VERTICAL_TOLERANCE && + xDir !== 0 && + yDir > -THRESHOLD_DIRECTION && + yDir < THRESHOLD_DIRECTION) + ) { + event.preventDefault() + if ( + xDelta > 0 && + !menuPanelOpened && + xInitial < oneThirdWindowWidth + ) { + // opening + progress = this._previousProgress = x / (width * 0.9) + } else if (menuPanelOpened) { + // closing + progress = this._previousProgress = 1 + xDelta / width + } + progress = this._previousProgress = Math.max( + 0.000001, + Math.min(0.999999, progress) + ) + } + return {children(progress)} + }} + + ) + } +} + +const Container = styled.div` + position: relative; + display: flex; + flex-grow: 1; + min-height: 0; +` + +export default props => ( + + {({ width }) => } + +) diff --git a/src/components/ModalManager/Modal.js b/src/components/ModalManager/Modal.js index c2ef035cf..5a0d45254 100644 --- a/src/components/ModalManager/Modal.js +++ b/src/components/ModalManager/Modal.js @@ -1,10 +1,11 @@ import React from 'react' +import PropTypes from 'prop-types' import styled from 'styled-components' import EscapeOutside from '../EscapeOutside/EscapeOutside' import { Button, breakpoint, font } from '@aragon/ui' import { noop } from '../../utils' -const Modal = ({ title, body, moreText, onHide, More, blocking }) => ( +const Modal = ({ title, body, onHide, More, blocking }) => (
{title}
@@ -19,6 +20,14 @@ const Modal = ({ title, body, moreText, onHide, More, blocking }) => (
) +Modal.propTypes = { + title: PropTypes.node.isRequired, + body: PropTypes.node.isRequired, + onHide: PropTypes.func.isRequired, + More: PropTypes.node, + blocking: PropTypes.bool, +} + const Main = styled.div` display: grid; grid-template-rows: auto 1fr auto; diff --git a/src/components/ModalManager/ModalManager.js b/src/components/ModalManager/ModalManager.js index 411d920e3..b12f5cebf 100644 --- a/src/components/ModalManager/ModalManager.js +++ b/src/components/ModalManager/ModalManager.js @@ -1,4 +1,5 @@ import React from 'react' +import PropTypes from 'prop-types' import styled from 'styled-components' import { Transition, animated } from 'react-spring' import springs from '../../springs' @@ -37,6 +38,10 @@ class ModalProvider extends React.Component { } } +ModalProvider.propTypes = { + children: PropTypes.node, +} + const ModalConsumer = ModalContext.Consumer const ModalView = () => ( @@ -52,6 +57,7 @@ const ModalView = () => ( > {ModalComponent => ModalComponent && + /* eslint-disable react/prop-types */ (({ opacity, enterProgress, blocking }) => ( v * 0.5) }} /> @@ -70,6 +76,7 @@ const ModalView = () => ( )) + /* eslint-enable react/prop-types */ } )} @@ -104,4 +111,4 @@ const AnimatedWrap = styled(animated.div)` min-height: 0; ` -export { ModalProvider, ModalConsumer, ModalView } +export { ModalContext, ModalProvider, ModalConsumer, ModalView } diff --git a/src/components/Notifications/NotificationAlert.js b/src/components/Notifications/NotificationAlert.js index 37c454ced..7756946c2 100644 --- a/src/components/Notifications/NotificationAlert.js +++ b/src/components/Notifications/NotificationAlert.js @@ -1,11 +1,15 @@ import React from 'react' +import PropTypes from 'prop-types' import styled from 'styled-components' import { Spring, animated } from 'react-spring' import { theme, springs, IconNotifications } from '@aragon/ui' export default class NotificationAlert extends React.PureComponent { - state = { opened: false, previousNotifications: 0 } - + static propTypes = { + notifications: PropTypes.number.isRequired, + notificationOpen: PropTypes.bool.isRequired, + onClick: PropTypes.func.isRequired, + } static getDerivedStateFromProps( { notificationOpen, notifications }, { opened, previousNotifications } @@ -19,6 +23,8 @@ export default class NotificationAlert extends React.PureComponent { } } + state = { opened: false, previousNotifications: 0 } + handleClick = () => { this.setState({ opened: true }) this.props.onClick() @@ -27,7 +33,7 @@ export default class NotificationAlert extends React.PureComponent { render() { const show = !this.state.opened && this.props.notifications > 0 return ( -
+
{ let payload = typeof item.content === 'string' ?

{item.content}

: item.content switch (item.type) { diff --git a/src/components/Notifications/NotificationHub.js b/src/components/Notifications/NotificationHub.js index b0fec45bf..ebe62fc46 100644 --- a/src/components/Notifications/NotificationHub.js +++ b/src/components/Notifications/NotificationHub.js @@ -1,4 +1,5 @@ import React from 'react' +import PropTypes from 'prop-types' import { Spring, Transition, animated } from 'react-spring' import styled from 'styled-components' import { theme, IconClose } from '@aragon/ui' @@ -7,6 +8,12 @@ import TimeTag from './TimeTag' const spring = { tension: 1900, friction: 200, precision: 0.0001, clamp: true } class NotificationHub extends React.Component { + static propTypes = { + items: PropTypes.arrayOf(PropTypes.object).isRequired, + keys: PropTypes.func.isRequired, + children: PropTypes.func.isRequired, + } + state = { ready: {} } render() { const { items, keys, children, onNotificationClosed } = this.props @@ -46,6 +53,11 @@ class NotificationHub extends React.Component { } class Notification extends React.Component { + static propTypes = { + title: PropTypes.string, + children: PropTypes.node, + time: PropTypes.string, + } render() { const { children, title } = this.props return ( @@ -63,6 +75,10 @@ class Notification extends React.Component { } Notification.Transaction = class extends React.Component { + static propTypes = { + children: PropTypes.node, + ready: PropTypes.bool, + } state = { showPayload: true } isDone = props => props.p === 1 && this.setState({ showPayload: false }) render() { diff --git a/src/components/Preferences/EmptyLocalIdentities.js b/src/components/Preferences/EmptyLocalIdentities.js new file mode 100644 index 000000000..2609294c7 --- /dev/null +++ b/src/components/Preferences/EmptyLocalIdentities.js @@ -0,0 +1,72 @@ +import React from 'react' +import PropTypes from 'prop-types' +import styled from 'styled-components' +import { EthIdenticon, IdentityBadge, breakpoint } from '@aragon/ui' +import { getEmptyAddress } from '../../web3-utils' +import Import from './Import' + +const EmptyLocalIdentities = ({ onImport }) => ( + + Start adding labels + + You can add labels by clicking on the{' '} + + + + + anywhere in the app, or importing a .json file with labels by clicking + "Import" below. + + + + + +) + +EmptyLocalIdentities.propTypes = { + onImport: PropTypes.func.isRequired, +} + +const Wrap = styled.div` + padding: 0 16px; + + ${breakpoint( + 'medium', + ` + padding: 0; + ` + )} +` + +const WrapImport = styled.div` + margin: 20px 0; +` + +// div cannot appear as descendant of p +const Paragraph = styled.div` + margin: 16px 0px; +` + +const Title = styled.h2` + font-weight: bold; + margin: 8px 0; +` + +export default EmptyLocalIdentities diff --git a/src/components/Preferences/Import.js b/src/components/Preferences/Import.js new file mode 100644 index 000000000..f7d332472 --- /dev/null +++ b/src/components/Preferences/Import.js @@ -0,0 +1,83 @@ +import React from 'react' +import PropTypes from 'prop-types' +import { useDropzone } from 'react-dropzone' +import { Button } from '@aragon/ui' +import { isString } from '../../utils' +import { isAddress } from '../../web3-utils' + +// What is the answer to the ultimate question of Life, the Universe, and Everything? +const MAX_LENGTH = 42 + +const verifyLocalIdentityObject = obj => { + return ( + Array.isArray(obj) && + obj.every( + ({ address, name, createdAt }) => + !!address.trim() && + !!name.trim() && + !!createdAt && + isAddress(address) && + isString(name) && + name.length <= MAX_LENGTH + ) + ) +} + +const fileImport = cb => files => { + if (!files || !files.length) { + return + } + + const reader = new FileReader() + reader.onload = event => { + try { + const list = JSON.parse(event.target.result) + if (verifyLocalIdentityObject(list)) { + cb(list) + } else { + throw new Error('There was an error reading from the file') + } + } catch (e) { + console.warn(e) + } + } + reader.readAsText(files[0]) +} + +const Import = ({ onImport }) => { + const handleImport = e => fileImport(onImport)(e.currentTarget.files) + const { getRootProps } = useDropzone({ + onDrop: fileImport(onImport), + }) + + return ( + + ) +} + +Import.propTypes = { onImport: PropTypes.func.isRequired } + +export default Import diff --git a/src/components/Preferences/LocalIdentities.js b/src/components/Preferences/LocalIdentities.js new file mode 100644 index 000000000..100765860 --- /dev/null +++ b/src/components/Preferences/LocalIdentities.js @@ -0,0 +1,254 @@ +import React from 'react' +import PropTypes from 'prop-types' +import styled from 'styled-components' +import { format } from 'date-fns' +import { + Badge, + Button, + IconCross, + IdentityBadge, + Info, + breakpoint, + font, + theme, +} from '@aragon/ui' +import { LocalIdentityModalContext } from '../LocalIdentityModal/LocalIdentityModalManager' +import { + IdentityContext, + identityEventTypes, +} from '../IdentityManager/IdentityManager' +import EmptyLocalIdentities from './EmptyLocalIdentities' +import Import from './Import' + +const iOS = /iPad|iPhone|iPod/.test(navigator.userAgent) && !window.MSStream + +const LocalIdentities = ({ + onClearAll, + onImport, + onModify, + onModifyEvent, + localIdentities, +}) => { + // transform localIdentities from object into array + const identities = Object.keys(localIdentities).map(address => { + return Object.assign({}, localIdentities[address], { address }) + }) + + if (!identities.length) { + return + } + const { identityEvents$ } = React.useContext(IdentityContext) + const updateLabel = (fn, address) => async () => { + try { + await fn(address) + // preferences get all + onModifyEvent() + // for iframe apps + identityEvents$.next({ type: identityEventTypes.MODIFY, address }) + } catch (e) { + /* nothing was updated */ + } + } + const href = window.URL.createObjectURL( + new Blob([JSON.stringify(identities)], { type: 'text/json' }) + ) + // standard: https://en.wikipedia.org/wiki/ISO_8601 + const today = format(Date.now(), 'yyyy-MM-dd') + const { showLocalIdentityModal } = React.useContext(LocalIdentityModalContext) + + return ( + + +
Custom label
+
Address
+
+ + {identities.map(({ address, name }) => ( + + +
+ + } + /> +
+
+ ))} +
+ + + {!iOS && ( + + Export + + )} + + + +
+ ) +} + +LocalIdentities.propTypes = { + localIdentities: PropTypes.object, + onClearAll: PropTypes.func.isRequired, + onImport: PropTypes.func.isRequired, + onModify: PropTypes.func.isRequired, + onModifyEvent: PropTypes.func, +} + +LocalIdentities.defaultProps = { + localIdentities: {}, + onModifyEvent: () => null, +} + +const PopoverActionTitle = ({ address, name }) => { + return ( + + {name} + + Custom label + + + ) +} + +PopoverActionTitle.propTypes = { + address: PropTypes.string.isRequired, + name: PropTypes.string.isRequired, +} + +const Label = styled.div` + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +` + +const WrapTitle = styled.div` + display: grid; + align-items: center; + grid-template-columns: auto 1fr; + padding-right: 24px; +` + +const TitleLabel = styled.span` + display: inline-block; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +` + +const Warning = () => ( + +
+ Any labels you add or import will only be shown on this device, and not + stored anywhere else. If you want to share the labels with other devices + or users, you will need to export them and share the .json file +
+
+) + +const StyledExport = styled(Button.Anchor)` + margin: 0 24px 24px; +` + +const Controls = styled.div` + display: flex; + align-items: start; + flex-wrap: wrap; + margin-top: 20px; + padding: 0 16px; + + ${breakpoint( + 'medium', + ` + padding: 0; + ` + )} +` + +const StyledInfoAction = styled(Info.Action)` + margin: 16px 16px 0 16px; + + ${breakpoint( + 'medium', + ` + margin: 0; + margin-top: 16px; + ` + )} +` + +const Headers = styled.div` + margin: 10px auto; + text-transform: uppercase; + color: ${theme.textSecondary}; + ${font({ size: 'xsmall' })}; + display: grid; + grid-template-columns: 1fr 1fr; + align-items: center; + + & > div { + padding-left: 16px; + } +` + +const Item = styled.li` + padding: 16px 0; + display: grid; + grid-template-columns: 1fr 1fr; + align-items: center; + border-bottom: 1px solid ${theme.contentBorder}; + + & > div { + padding-left: 16px; + } +` + +const List = styled.ul` + padding: 0; + list-style: none; + overflow: hidden; + + li:first-child { + border-top: 1px solid ${theme.contentBorder}; + } + + ${breakpoint( + 'medium', + ` + max-height: 50vh; + overflow: auto; + border-radius: 4px; + border: 1px solid ${theme.contentBorder}; + + li:first-child { + border-top: none; + } + li:last-child { + border-bottom: none; + } + ` + )} +` + +export default LocalIdentities diff --git a/src/components/Preferences/Preferences.js b/src/components/Preferences/Preferences.js new file mode 100644 index 000000000..31e7fbd01 --- /dev/null +++ b/src/components/Preferences/Preferences.js @@ -0,0 +1,215 @@ +import React from 'react' +import PropTypes from 'prop-types' +import styled from 'styled-components' +import { Transition, animated } from 'react-spring' +import { + AppBar, + AppView, + BREAKPOINTS, + ButtonIcon, + IconClose, + TabBar, + Viewport, + breakpoint, + font, + springs, +} from '@aragon/ui' +import LocalIdentitiesComponent from './LocalIdentities' +import { AragonType } from '../../prop-types' +import { + IdentityContext, + identityEventTypes, +} from '../IdentityManager/IdentityManager' + +const TABS = ['Manage labels'] +const ESCAPE_KEY_CODE = 27 + +const Preferences = ({ onClose, smallView, wrapper }) => { + const { identityEvents$ } = React.useContext(IdentityContext) + const [selectedTab, setSelectedTab] = React.useState(0) + const [localIdentities, setLocalIdentities] = React.useState({}) + const handleGetAll = async () => { + if (!wrapper) { + return + } + setLocalIdentities(await wrapper.getLocalIdentities()) + } + const handleClearAll = async () => { + if (!wrapper) { + return + } + await wrapper.clearLocalIdentities() + setLocalIdentities({}) + identityEvents$.next({ type: identityEventTypes.CLEAR }) + } + const handleModify = (address, data) => { + if (!wrapper) { + return + } + wrapper.modifyAddressIdentity(address, data) + } + const handleImport = async list => { + if (!wrapper) { + return + } + setLocalIdentities({}) + for (const { name, address } of list) { + await wrapper.modifyAddressIdentity(address, { name }) + } + setLocalIdentities(await wrapper.getLocalIdentities()) + identityEvents$.next({ type: identityEventTypes.IMPORT }) + } + const handlekeyDown = e => { + if (e.keyCode === ESCAPE_KEY_CODE) { + onClose() + } + } + React.useEffect(() => { + handleGetAll() + window.addEventListener('keydown', handlekeyDown) + return () => window.removeEventListener('keydown', handlekeyDown) + }, []) + + return ( + + Preferences + + + + + } + > +
+ + + {selectedTab === 0 && ( + + )} + +
+
+ ) +} + +Preferences.propTypes = { + onClose: PropTypes.func.isRequired, + smallView: PropTypes.bool.isRequired, + wrapper: AragonType, +} + +const AnimatedPreferences = ({ opened, ...props }) => { + return ( + + {show => + show && + /* eslint-disable react/prop-types */ + (({ opacity, enterProgress, blocking }) => ( + ` + translate3d(0, ${(1 - v) * 10}px, 0) + scale3d(${1 - (1 - v) * 0.03}, ${1 - (1 - v) * 0.03}, 1) + ` + ), + }} + > + + + )) + /* eslint-enable react/prop-types */ + } + + ) +} + +AnimatedPreferences.propTypes = { + opened: PropTypes.bool, + smallView: PropTypes.bool.isRequired, +} + +const AnimatedWrap = styled(animated.div)` + position: fixed; + background: #fff; + top: 0; + bottom: 0; + left: 0; + right: 0; + z-index: ${({ smallView }) => (smallView ? 2 : 4)}; + min-width: 320px; +` + +const Title = styled.h1` + ${font({ size: 'xxlarge' })}; + + ${breakpoint( + 'medium', + ` + /* half screen width minus half max container witdh */ + margin-left: calc(100vw / 2 - ${BREAKPOINTS.medium / 2}px); + ` + )} +` + +const Section = styled.section` + padding: 16px 0; + + ${breakpoint( + 'medium', + ` + width: ${BREAKPOINTS.medium}px; + margin: 0 auto; + ` + )} +` + +const Content = styled.main` + padding-top: 16px; +` + +const StyledAppBar = styled(AppBar)` + padding-left: 16px; + display: flex; + align-items: center; + justify-content: space-between; + + ${breakpoint( + 'medium', + ` + padding-left: 0; + ` + )} +` + +const StyledButton = styled(ButtonIcon)` + width: auto; + height: 100%; + padding: 0 16px; +` + +export default props => ( + + {({ below }) => ( + + )} + +) diff --git a/src/components/RemoteIcon.js b/src/components/RemoteIcon.js deleted file mode 100644 index 379b5eb4c..000000000 --- a/src/components/RemoteIcon.js +++ /dev/null @@ -1,46 +0,0 @@ -import React from 'react' -import PropTypes from 'prop-types' -import { IconBlank } from '@aragon/ui' -import RemoteImage from './RemoteImage' - -// Tries to load an image for an icon, while displaying a blank icon. -// Use `children` or `render` to change the icon component to use. -class RemoteIcon extends React.Component { - static propTypes = { - alt: PropTypes.string.isRequired, - children: PropTypes.func.isRequired, - render: PropTypes.func, - size: PropTypes.number.isRequired, - src: PropTypes.string.isRequired, - } - - static defaultProps = { - src: '', - alt: '', - size: 22, - - render: null, // render is an alias of children and takes priority - children: ({ src, alt, size }) => ( - {alt} - ), - } - - render() { - const { src, alt, size, render, children } = this.props - const renderIcon = render || children - - return ( - - {({ exists }) => - exists ? ( - renderIcon({ src, alt, size }) - ) : ( - - ) - } - - ) - } -} - -export default RemoteIcon diff --git a/src/components/SignerPanel/ActionPathsContent.js b/src/components/SignerPanel/ActionPathsContent.js index 927eea8fe..c9a64d966 100644 --- a/src/components/SignerPanel/ActionPathsContent.js +++ b/src/components/SignerPanel/ActionPathsContent.js @@ -1,14 +1,25 @@ import React from 'react' +import PropTypes from 'prop-types' import styled from 'styled-components' import { Info, RadioList, SafeLink } from '@aragon/ui' import SignerButton from './SignerButton' import AddressLink from './AddressLink' -import IdentityBadge from '../IdentityBadge' +import LocalIdentityBadge from '../LocalIdentityBadge/LocalIdentityBadge' import providerString from '../../provider-strings' const RADIO_ITEM_TITLE_LENGTH = 30 class ActionPathsContent extends React.Component { + static propTypes = { + direct: PropTypes.bool.isRequired, + intent: PropTypes.object.isRequired, + locator: PropTypes.object.isRequired, + onSign: PropTypes.func.isRequired, + paths: PropTypes.array.isRequired, + pretransaction: PropTypes.object, + signingEnabled: PropTypes.bool.isRequired, + walletProviderId: PropTypes.string.isRequired, + } state = { selected: 0, } @@ -36,14 +47,21 @@ class ActionPathsContent extends React.Component {
{annotatedDescription ? annotatedDescription.map(({ type, value }, index) => { - if (type === 'address') { + if (type === 'address' || type === 'any-account') { return ( - + css={` + display: inline-flex; + vertical-align: middle; + margin-right: 4px; + `} + > + + ) } else if (type === 'app') { return ( @@ -123,11 +141,11 @@ class ActionPathsContent extends React.Component { } render() { const { - signingEnabled, intent, direct, paths, pretransaction, + signingEnabled, walletProviderId, } = this.props const { selected } = this.state diff --git a/src/components/SignerPanel/AddressLink.js b/src/components/SignerPanel/AddressLink.js index acf282e25..b895ac8a6 100644 --- a/src/components/SignerPanel/AddressLink.js +++ b/src/components/SignerPanel/AddressLink.js @@ -1,5 +1,7 @@ import React from 'react' +import PropTypes from 'prop-types' import { SafeLink } from '@aragon/ui' +import { EthereumAddressType } from '../../prop-types' import EtherscanLink from '../Etherscan/EtherscanLink' const AddressLink = ({ children, to }) => @@ -18,5 +20,9 @@ const AddressLink = ({ children, to }) => ) : ( 'an address or app' ) +AddressLink.propTypes = { + children: PropTypes.node, + to: EthereumAddressType, +} export default AddressLink diff --git a/src/components/SignerPanel/ConfirmTransaction.js b/src/components/SignerPanel/ConfirmTransaction.js index 522d1d7c4..008aa3300 100644 --- a/src/components/SignerPanel/ConfirmTransaction.js +++ b/src/components/SignerPanel/ConfirmTransaction.js @@ -1,8 +1,8 @@ import React from 'react' import PropTypes from 'prop-types' import styled from 'styled-components' -import { Info, theme } from '@aragon/ui' -import { noop } from '../../utils' +import { Info, SafeLink, theme } from '@aragon/ui' +import { isElectron, noop } from '../../utils' import providerString from '../../provider-strings' import ActionPathsContent from './ActionPathsContent' import SignerButton from './SignerButton' @@ -13,24 +13,22 @@ class ConfirmTransaction extends React.Component { direct: PropTypes.bool.isRequired, hasAccount: PropTypes.bool.isRequired, hasWeb3: PropTypes.bool.isRequired, - intent: PropTypes.object.isRequired, + intent: PropTypes.object, + locator: PropTypes.object.isRequired, networkType: PropTypes.string.isRequired, onClose: PropTypes.func.isRequired, + onRequestEnable: PropTypes.func, onSign: PropTypes.func.isRequired, paths: PropTypes.array.isRequired, - pretransaction: PropTypes.string, + pretransaction: PropTypes.object, signError: PropTypes.string, signingEnabled: PropTypes.bool.isRequired, walletNetworkType: PropTypes.string.isRequired, - onRequestEnable: PropTypes.func, + walletProviderId: PropTypes.string.isRequired, } static defaultProps = { intent: {}, - paths: [], - pretransaction: null, - onClose: noop, - onSign: noop, onRequestEnable: noop, } render() { @@ -53,15 +51,38 @@ class ConfirmTransaction extends React.Component { } = this.props if (!hasWeb3) { + if (isElectron()) { + return ( + + Please install and enable{' '} + + Frame + + . + + } + /> + ) + } return ( + Please install and enable{' '} + + Metamask + + . + + } /> ) } @@ -131,11 +152,16 @@ const ImpossibleContent = ({ }) => ( - The action {description && `“${description}”`} failed to execute on{' '} - {name}.{' '} + The action {description && `“${description}”`} failed to execute + {name && ( + + on {name}} + + )} + .{' '} {error ? 'An error occurred when we tried to find a path or send a transaction for this action.' - : 'You might not have the necessary permissions.'} + : 'You may not have the required permissions.'} Close @@ -158,9 +184,12 @@ const Web3ProviderError = ({ {neededText} in order to perform{' '} {description ? `"${description}"` : 'this action'} - {' on '} - {name}. - {actionText} + {name && ( + + on {name} + + )} + .{actionText} Close diff --git a/src/components/SignerPanel/SignerPanel.js b/src/components/SignerPanel/SignerPanel.js index 5c3f39a11..ef16d3632 100644 --- a/src/components/SignerPanel/SignerPanel.js +++ b/src/components/SignerPanel/SignerPanel.js @@ -3,8 +3,8 @@ import PropTypes from 'prop-types' import styled from 'styled-components' import { SidePanel } from '@aragon/ui' import { Transition, animated } from 'react-spring' -import { addressesEqual } from '../../web3-utils' -import { noop } from '../../utils' +import { AppType, EthereumAddressType } from '../../prop-types' +import { addressesEqual, getInjectedProvider } from '../../web3-utils' import ConfirmTransaction from './ConfirmTransaction' import SigningStatus from './SigningStatus' import { network } from '../../environment' @@ -29,20 +29,16 @@ const INITIAL_STATE = { class SignerPanel extends React.Component { static propTypes = { - apps: PropTypes.array, - account: PropTypes.string, - walletNetwork: PropTypes.string, - walletWeb3: PropTypes.object, - walletProviderId: PropTypes.string, + apps: PropTypes.arrayOf(AppType).isRequired, + account: EthereumAddressType, + locator: PropTypes.object.isRequired, + onClose: PropTypes.func.isRequired, + onRequestEnable: PropTypes.func.isRequired, + onTransactionSuccess: PropTypes.func.isRequired, transactionBag: PropTypes.object, - onRequestEnable: PropTypes.func, - } - - static defaultProps = { - apps: [], - account: '', - walletProviderId: '', - onRequestEnable: noop, + walletNetwork: PropTypes.string.isRequired, + walletWeb3: PropTypes.object.isRequired, + walletProviderId: PropTypes.string.isRequired, } state = { ...INITIAL_STATE } @@ -166,7 +162,6 @@ class SignerPanel extends React.Component { onRequestEnable, walletNetwork, walletProviderId, - walletWeb3, } = this.props const { @@ -208,7 +203,7 @@ class SignerPanel extends React.Component { msg.replace(/^Returned error: /, '').replace(/^Error: /, '') class SigningStatus extends React.Component { + static propTypes = { + status: SignerStatusType.isRequired, + signError: PropTypes.instanceOf(Error), + walletProviderId: PropTypes.string, + onClose: PropTypes.func.isRequired, + } getLabel() { const { status } = this.props if (status === STATUS_SIGNING) return 'Waiting for signature…' @@ -99,6 +111,10 @@ const StatusImage = ({ status }) => ( ) +StatusImage.propTypes = { + status: SignerStatusType.isRequired, +} + const StatusImageMain = styled.div` position: relative; width: 150px; diff --git a/src/components/SignerPanel/signer-statuses.js b/src/components/SignerPanel/signer-statuses.js index eb819ddf0..9078f3bb1 100644 --- a/src/components/SignerPanel/signer-statuses.js +++ b/src/components/SignerPanel/signer-statuses.js @@ -1,3 +1,5 @@ +import PropTypes from 'prop-types' + // The user need to confirm the transaction export const STATUS_CONFIRMING = Symbol('STATUS_CONFIRMING') @@ -9,3 +11,11 @@ export const STATUS_SIGNED = Symbol('STATUS_SIGNED') // An error happened while signing the transaction export const STATUS_ERROR = Symbol('STATUS_ERROR') + +// Corresponding proptype +export const SignerStatusType = PropTypes.oneOf([ + STATUS_CONFIRMING, + STATUS_SIGNING, + STATUS_SIGNED, + STATUS_ERROR, +]) diff --git a/src/contexts/PermissionsContext.js b/src/contexts/PermissionsContext.js index 534496d10..755706a5f 100644 --- a/src/contexts/PermissionsContext.js +++ b/src/contexts/PermissionsContext.js @@ -8,6 +8,7 @@ import { entityRoles, permissionsByEntity, } from '../permissions' +import { AppType } from '../prop-types' import { log, noop } from '../utils' import { getEmptyAddress } from '../web3-utils' @@ -15,7 +16,7 @@ const { Provider, Consumer } = React.createContext() class PermissionsProvider extends React.Component { static propTypes = { - apps: PropTypes.array.isRequired, + apps: PropTypes.arrayOf(AppType).isRequired, children: PropTypes.node.isRequired, permissions: PropTypes.object.isRequired, wrapper: PropTypes.object, diff --git a/src/contexts/ScreenSize.js b/src/contexts/ScreenSize.js deleted file mode 100644 index d02e94f9d..000000000 --- a/src/contexts/ScreenSize.js +++ /dev/null @@ -1,30 +0,0 @@ -import React from 'react' -import PropTypes from 'prop-types' -import { BreakPoint } from '@aragon/ui' - -const SMALL = Symbol('small') -const MEDIUM = Symbol('medium') -const LARGE = Symbol('large') - -const { Provider, Consumer } = React.createContext() - -const ScreenSizeProvider = ({ children }) => ( - - - {children} - - - {children} - - - {children} - - -) - -ScreenSizeProvider.propTypes = { - children: PropTypes.oneOfType([PropTypes.func, PropTypes.array]).isRequired, -} - -export { SMALL, MEDIUM, LARGE } -export { ScreenSizeProvider, Consumer as ScreenSizeConsumer } diff --git a/src/environment.js b/src/environment.js index ff6458b74..18de1d0e8 100644 --- a/src/environment.js +++ b/src/environment.js @@ -8,7 +8,7 @@ import { } from './local-settings' import { getNetworkConfig } from './network-config' import { noop } from './utils' -import { toWei } from './web3-utils' +import { toWei, getInjectedProvider } from './web3-utils' const appsOrder = ['TokenManager', 'Voting', 'Finance', 'Vault'] const networkType = getEthNetworkType() @@ -30,6 +30,14 @@ export const sortAppsPair = (app1, app2) => { const index1 = name1 ? appsOrder.indexOf(name1) : -1 const index2 = name2 ? appsOrder.indexOf(name2) : -1 + // Keep kernel first + if (app1.name === 'Kernel') { + return -1 + } + if (app2.name === 'Kernel') { + return 1 + } + // Internal apps first if (app1.isAragonOsInternalApp !== app2.isAragonOsInternalApp) { return app1.isAragonOsInternalApp ? -1 : 1 @@ -126,7 +134,8 @@ export const defaultEthNode = export const web3Providers = { default: new Web3.providers.WebsocketProvider(defaultEthNode), - wallet: provider(), + // Only use eth-provider to connect to frame if no injected provider is detected + wallet: getInjectedProvider() || provider(['frame']), } export const defaultGasPriceFn = diff --git a/src/errors.js b/src/errors.js index 23e267695..06b7d9e64 100644 --- a/src/errors.js +++ b/src/errors.js @@ -9,6 +9,12 @@ const extendError = (name, { defaultMessage }) => export const InvalidAddress = extendError('InvalidAddress', { defaultMessage: 'The address is invalid', }) +export const InvalidNetworkType = extendError('InvalidNetworkType', { + defaultMessage: 'The network type is invalid', +}) +export const InvalidURI = extendError('InvalidURI', { + defaultMessage: 'The URI is invalid', +}) export const NoConnection = extendError('NoConnection', { defaultMessage: 'There is no connection', }) diff --git a/src/icons/IconArrow.js b/src/icons/IconArrow.js new file mode 100644 index 000000000..3b0af9462 --- /dev/null +++ b/src/icons/IconArrow.js @@ -0,0 +1,9 @@ +import React from 'react' + +const Arrow = props => ( + + + +) + +export default Arrow diff --git a/src/icons/IconKernel.js b/src/icons/IconKernel.js deleted file mode 100644 index 1ab0586b9..000000000 --- a/src/icons/IconKernel.js +++ /dev/null @@ -1,33 +0,0 @@ -import React from 'react' - -const IconKernel = ({ size = 28, ...props }) => ( - - - - - - - - - - - - - - - - -) - -export default IconKernel diff --git a/src/index.js b/src/index.js index 6377beb21..6ace38468 100644 --- a/src/index.js +++ b/src/index.js @@ -2,7 +2,7 @@ import '@babel/polyfill' import React from 'react' import ReactDOM from 'react-dom' -import { PublicUrl, BaseStyles } from '@aragon/ui' +import { Main } from '@aragon/ui' import GlobalErrorHandler from './GlobalErrorHandler' import App from './App' @@ -19,15 +19,27 @@ if ( ) { window.localStorage.clear() window.localStorage.setItem(PACKAGE_VERSION_KEY, PACKAGE_VERSION) + + // Attempt to clean up indexedDB storage as well + if ( + window.indexedDB && + window.indexedDB.databases && + window.indexedDB.deleteDatabase + ) { + // eslint-disable-next-line promise/catch-or-return + window.indexedDB + .databases() + .then(databases => + databases.forEach(({ name }) => window.indexedDB.deleteDatabase(name)) + ) + } } ReactDOM.render( - - - - + +
- - , +
+
, document.getElementById('root') ) diff --git a/src/known-organizations/images/aragon-governance.svg b/src/known-organizations/images/aragon-governance.svg new file mode 100644 index 000000000..4f6ad5c53 --- /dev/null +++ b/src/known-organizations/images/aragon-governance.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/known-organizations/images/aragon-one.svg b/src/known-organizations/images/aragon-one.svg new file mode 100644 index 000000000..8fcdb78d8 --- /dev/null +++ b/src/known-organizations/images/aragon-one.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/known-organizations/images/melon-council.svg b/src/known-organizations/images/melon-council.svg new file mode 100644 index 000000000..7f5eb64d6 --- /dev/null +++ b/src/known-organizations/images/melon-council.svg @@ -0,0 +1,15 @@ + + + + melon-sticker + Created with Sketch Beta. + + + + + + + + + + \ No newline at end of file diff --git a/src/known-organizations/images/melonport.svg b/src/known-organizations/images/melonport.svg new file mode 100644 index 000000000..dbd191130 --- /dev/null +++ b/src/known-organizations/images/melonport.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/known-organizations/index.js b/src/known-organizations/index.js new file mode 100644 index 000000000..0c3ba3282 --- /dev/null +++ b/src/known-organizations/index.js @@ -0,0 +1,30 @@ +import aragonGovernanceImage from './images/aragon-governance.svg' +import aragonOneImage from './images/aragon-one.svg' +import melonCouncilImage from './images/melon-council.svg' + +export const KnownOrganizations = { + main: new Map([ + // meloncouncil.eth + [ + '0xfe1f2de598f42ce67bb9aad5ad473f0272d09b74', + { name: 'Melon Council', image: melonCouncilImage }, + ], + + // governance.aragonproject.eth + [ + '0x2de83b50af29678774d5abc4a7cb2a588762f28c', + { name: 'Aragon Governance', image: aragonGovernanceImage }, + ], + + // a1.aragonid.eth + [ + '0x635193983512c621e6a3e15ee1dbf36f0c0db8e0', + { name: 'Aragon One', image: aragonOneImage }, + ], + ]), +} + +export const getKnownOrganization = (networkType, address) => { + if (!KnownOrganizations[networkType]) return null + return KnownOrganizations[networkType].get(address) || null +} diff --git a/src/network-config.js b/src/network-config.js index 7395a3b23..9130e39df 100644 --- a/src/network-config.js +++ b/src/network-config.js @@ -1,5 +1,4 @@ import { getEnsRegistryAddress } from './local-settings' -import { makeEtherscanBaseUrl } from './utils' const localEnsRegistryAddress = getEnsRegistryAddress() @@ -14,7 +13,6 @@ export const networkConfigs = { }, settings: { chainId: 1, - etherscanBaseUrl: makeEtherscanBaseUrl('main'), name: 'Mainnet', type: 'main', // as returned by web3.eth.net.getNetworkType() }, @@ -29,7 +27,6 @@ export const networkConfigs = { }, settings: { chainId: 4, - etherscanBaseUrl: makeEtherscanBaseUrl('rinkeby'), name: 'Rinkeby testnet', type: 'rinkeby', // as returned by web3.eth.net.getNetworkType() }, diff --git a/src/onboarding/Domain.js b/src/onboarding/Domain.js index 44555a730..e36cd7e08 100644 --- a/src/onboarding/Domain.js +++ b/src/onboarding/Domain.js @@ -1,3 +1,4 @@ +/* eslint react/prop-types: 0 */ import React from 'react' import styled from 'styled-components' import { theme, Text, TextInput, IconCheck, IconCross } from '@aragon/ui' diff --git a/src/onboarding/Launch.js b/src/onboarding/Launch.js index 16eb77c0c..e3e1aa6b2 100644 --- a/src/onboarding/Launch.js +++ b/src/onboarding/Launch.js @@ -1,3 +1,4 @@ +/* eslint react/prop-types: 0 */ import React from 'react' import styled from 'styled-components' import { theme, Text, Button } from '@aragon/ui' diff --git a/src/onboarding/Onboarding.js b/src/onboarding/Onboarding.js index 69729b118..c6f510189 100644 --- a/src/onboarding/Onboarding.js +++ b/src/onboarding/Onboarding.js @@ -2,6 +2,7 @@ import React from 'react' import PropTypes from 'prop-types' import styled from 'styled-components' import { Spring, animated } from 'react-spring' +import { breakpoint } from '@aragon/ui' import { noop } from '../utils' import { getUnknownBalance } from '../web3-utils' import { isNameAvailable } from '../aragonjs-wrapper' @@ -46,14 +47,14 @@ const initialState = { class Onboarding extends React.PureComponent { static propTypes = { - account: PropTypes.string.isRequired, + account: PropTypes.string, balance: PropTypes.object, banner: PropTypes.oneOfType([ PropTypes.bool, PropTypes.shape({ type: PropTypes.oneOf([DeprecatedBanner]), }), - ]).isRequired, + ]), daoCreationStatus: PropTypes.oneOf([ DAO_CREATION_STATUS_NONE, DAO_CREATION_STATUS_SUCCESS, @@ -63,23 +64,18 @@ class Onboarding extends React.PureComponent { onComplete: PropTypes.func.isRequired, onOpenOrganization: PropTypes.func.isRequired, onResetDaoBuilder: PropTypes.func.isRequired, + onRequestEnable: PropTypes.func.isRequired, selectorNetworks: PropTypes.array.isRequired, visible: PropTypes.bool.isRequired, - walletNetwork: PropTypes.string.isRequired, - walletWeb3: PropTypes.object, + walletNetwork: PropTypes.string, + walletProviderId: PropTypes.string, } static defaultProps = { account: '', balance: getUnknownBalance(), - banner: null, - daoCreationStatus: DAO_CREATION_STATUS_NONE, + banner: false, onComplete: noop, - onBuildDao: noop, - onOpenOrganization: noop, - onResetDaoBuilder: noop, - onRequestEnable: noop, - visible: true, walletNetwork: '', walletProviderId: '', } @@ -472,7 +468,6 @@ class Onboarding extends React.PureComponent { onRequestEnable, walletNetwork, walletProviderId, - walletWeb3, } = this.props // No need to move the screens farther than one step @@ -510,7 +505,6 @@ class Onboarding extends React.PureComponent { onRequestEnable={onRequestEnable} walletNetwork={walletNetwork} walletProviderId={walletProviderId} - walletWeb3={walletWeb3} {...sharedProps} /> ) @@ -578,7 +572,6 @@ const Main = styled(animated.div)` left: 0; right: 0; bottom: 0; - overflow: auto; height: 100%; display: flex; flex-direction: column; @@ -588,6 +581,13 @@ const Main = styled(animated.div)` rgba(0, 0, 0, 0.08) 100% ), linear-gradient(-226deg, #00f1e1 0%, #00b4e4 100%); + + ${breakpoint( + 'medium', + ` + overflow: auto; + ` + )} ` const BannerWrapper = styled.div` @@ -599,18 +599,32 @@ const View = styled.div` display: flex; align-items: center; justify-content: center; - min-width: 800px; flex-grow: 1; - padding: 50px; + + ${breakpoint( + 'medium', + ` + min-width: 800px; + padding: 50px; + ` + )} ` const Window = styled.div` position: relative; - width: 1080px; - height: 660px; + width: 100vw; + height: 100vh; background: #fff; - border-radius: 3px; - box-shadow: 0 10px 28px 0 rgba(11, 103, 157, 0.7); + + ${breakpoint( + 'medium', + ` + width: 1080px; + height: 660px; + border-radius: 3px; + box-shadow: 0 10px 28px 0 rgba(11, 103, 157, 0.7); + ` + )} ` const Screen = styled.div` @@ -619,8 +633,15 @@ const Screen = styled.div` left: 0; right: 0; bottom: 0; - overflow: hidden; + overflow: auto; pointer-events: ${({ active }) => (active ? 'auto' : 'none')}; + + ${breakpoint( + 'medium', + ` + overflow: hidden; + ` + )} ` export default Onboarding diff --git a/src/onboarding/PrevNext.js b/src/onboarding/PrevNext.js index ebc8ad752..5e1841973 100644 --- a/src/onboarding/PrevNext.js +++ b/src/onboarding/PrevNext.js @@ -1,3 +1,4 @@ +/* eslint react/prop-types: 0 */ import React from 'react' import styled from 'styled-components' import { Spring, animated } from 'react-spring' diff --git a/src/onboarding/Sign.js b/src/onboarding/Sign.js index 63569cef0..f4717b4cc 100644 --- a/src/onboarding/Sign.js +++ b/src/onboarding/Sign.js @@ -1,3 +1,4 @@ +/* eslint react/prop-types: 0 */ import React from 'react' import styled from 'styled-components' import { theme, Text, Button } from '@aragon/ui' diff --git a/src/onboarding/Start.js b/src/onboarding/Start.js index 03fc77d68..865ae67f5 100644 --- a/src/onboarding/Start.js +++ b/src/onboarding/Start.js @@ -1,23 +1,32 @@ +/* eslint react/prop-types: 0 */ import React from 'react' import styled from 'styled-components' import BN from 'bn.js' import { - theme, - Text, - SafeLink, Button, - TextInput, - IconCheck, - IconCross, DropDown, IconAttention, + IconCheck, + IconCross, + SafeLink, + Text, + TextInput, + Viewport, + breakpoint, + theme, } from '@aragon/ui' import { animated } from 'react-spring' import { network, getDemoDao, web3Providers } from '../environment' import { sanitizeNetworkType } from '../network-config' -import { noop } from '../utils' import providerString from '../provider-strings' -import { fromWei, toWei, getUnknownBalance, formatBalance } from '../web3-utils' +import { isElectron, noop } from '../utils' +import { + fromWei, + toWei, + getUnknownBalance, + formatBalance, + isConnected, +} from '../web3-utils' import LoadingRing from '../components/LoadingRing' import logo from './assets/logo-welcome.svg' @@ -73,23 +82,34 @@ class Start extends React.Component { return (
- - - + + {({ below }) => ( + + {below('medium') && ( + + If you want to create an organization, please use + your desktop browser. + + )} + + + )} +
) } @@ -137,6 +157,7 @@ class StartContent extends React.PureComponent { domainCheckStatus, onDomainChange, onOpenOrganization, + smallMode, } = this.props const canCreate = @@ -150,15 +171,21 @@ class StartContent extends React.PureComponent { return ( - <Text size="great" weight="bold" color={theme.textDimmed}> - Welcome to Aragon + <Text + size={smallMode ? 'xxlarge' : 'great'} + weight="bold" + color={theme.textDimmed} + > + {smallMode ? 'Find an existing organization' : 'Welcome to Aragon'} </Text>

- Start by choosing the network for your organization + {smallMode + ? 'Choose network' + : 'Start by choosing the network for your organization'}

@@ -167,10 +194,11 @@ class StartContent extends React.PureComponent { label)} onChange={this.handleNetworkChange} + wide={smallMode} />
- {network.type === 'main' && ( + {!smallMode && network.type === 'main' && ( @@ -191,39 +219,46 @@ class StartContent extends React.PureComponent { - -

- - Then create a new organization - -

- - {this.renderWarning()} -
+ {!smallMode && ( + +

+ + Then create a new organization + +

+ + {this.renderWarning()} +
+ )}

- Or open an existing organization + {smallMode + ? 'Enter an organization’s name' + : 'Or open an existing organization'}

- - + {domainCheckStatus === DomainCheckAccepted && ( - + + {smallMode ? 'Next' : 'Open organization'} + )} {domainCheckStatus === DomainCheckRejected && ( - + No organization with that name exists. - + )} - +
{demoDao && ( - -

- - Not ready to create an organization? Try browsing this{' '} - - demo organization - {' '} - instead. - -

-
+

+ + Not ready to create an organization? Try browsing this{' '} + + demo organization + {' '} + instead. + +

)} ) @@ -294,11 +328,24 @@ class StartContent extends React.PureComponent { if (!hasWallet) { return ( - Please install an Ethereum provider (e.g.{' '} - - MetaMask - - ) . + {isElectron() ? ( + + Please install{' '} + + Frame + {' '} + as your Ethereum provider + + ) : ( + + Please install an Ethereum provider (e.g.{' '} + + MetaMask + + ) + + )} + . ) } @@ -351,13 +398,73 @@ class StartContent extends React.PureComponent { } } +const DomainStatus = styled(Text)` + display: block; + margin-left: 5px; + + ${breakpoint( + 'medium', + ` + margin: -10px 0 0 5px; + ` + )} +` + +const SubmitWrap = styled.span` + height: 40px; + display: flex; + + ${breakpoint( + 'medium', + ` + display: inline; + ` + )} +` + +const StyledSubmitButton = styled(Button)` + margin-left: auto; + + ${breakpoint( + 'medium', + ` + margin-left: unset; + ` + )} +` + +const Warning = styled.div` + background: rgba(255, 195, 70, 0.09); + border-radius: 3px; + padding: 13px; + margin: 0 auto; + margin-bottom: 45px; + font-size: 15px; + + & span { + font-weight: bold; + } +` + const Main = styled(animated.div)` display: flex; align-items: center; justify-content: flex-start; width: 100%; height: 100%; - padding: 100px; + padding: 24px; + background: url(${logo}) no-repeat top center; + background-size: calc(100vw - 32px); + background-position: 50% 16.666666vh; + + ${breakpoint( + 'medium', + ` + padding: 100px; + background: none; + ` + )} + @media (min-width: 1180px) { justify-content: flex-start; background: url(${logo}) no-repeat calc(100% - 70px) 60%; @@ -365,29 +472,62 @@ const Main = styled(animated.div)` ` const Content = styled(animated.div)` + height: 100%; + width: 100%; display: flex; flex-direction: column; - justify-content: center; align-items: flex-start; + + ${breakpoint( + 'medium', + ` + justify-content: center; + ` + )} ` const TwoActions = styled.div` - display: flex; - align-items: flex-start; - > *:first-child { - width: 400px; - } + width: 100%; + + ${breakpoint( + 'medium', + ` + display: flex; + align-items: flex-start; + > *:first-child { + width: 400px; + } + ` + )} ` const NetworkChooser = styled.div` - margin-bottom: 60px; + width: 100%; + margin-bottom: 45px; > p:first-child { - margin-bottom: 40px; + margin-bottom: 20px; } + + ${breakpoint( + 'medium', + ` + margin-bottom: 60px; + > p:first-child { + margin-bottom: 40px; + } + ` + )} ` const NetworkChooserContainer = styled.div` - display: flex; + display: block; + + ${breakpoint( + 'medium', + ` + display: flex; + ` + )} ` const StrongSafeLink = styled(SafeLink)` @@ -430,21 +570,60 @@ const ActionInfo = styled.span` const Title = styled.h1` font-size: 37px; - margin-bottom: 40px; + margin-bottom: 45px; + + ${breakpoint( + 'medium', + ` + margin-bottom: 40px; + ` + )} ` const OpenOrganization = styled.div` - display: flex; - flex-direction: column; + width: 100%; + + ${breakpoint( + 'medium', + ` + display: flex; + flex-direction: column; + ` + )} +` + +const StyledTextInput = styled(TextInput)` + width: 100%; + margin-bottom: 10px; + + ${breakpoint( + 'medium', + ` + text-align: right; + margin-bottom: 0; + ` + )} ` const Field = styled.div` - display: flex; - align-items: center; margin-bottom: 20px; + label { - margin: 0 10px; + display: inline-block; + margin: 0 4px 0 8px; } + + ${breakpoint( + 'medium', + ` + display: flex; + align-items: center; + + label { + margin: 0 10px; + } + ` + )} ` const Status = styled.span` diff --git a/src/onboarding/StepsBar.js b/src/onboarding/StepsBar.js index 7253a9b9b..43fdd94e2 100644 --- a/src/onboarding/StepsBar.js +++ b/src/onboarding/StepsBar.js @@ -1,3 +1,4 @@ +/* eslint react/prop-types: 0 */ import React from 'react' import styled from 'styled-components' import { Spring } from 'react-spring' diff --git a/src/onboarding/Template.js b/src/onboarding/Template.js index 2ec14a02f..90f2cf937 100644 --- a/src/onboarding/Template.js +++ b/src/onboarding/Template.js @@ -1,3 +1,4 @@ +/* eslint react/prop-types: 0 */ import React from 'react' import styled from 'styled-components' import { theme, Text } from '@aragon/ui' diff --git a/src/onboarding/TemplateCard.js b/src/onboarding/TemplateCard.js index ba4a42053..e4eee8a2c 100644 --- a/src/onboarding/TemplateCard.js +++ b/src/onboarding/TemplateCard.js @@ -1,9 +1,18 @@ import React from 'react' +import PropTypes from 'prop-types' import styled from 'styled-components' import { theme, font, IconCheck } from '@aragon/ui' import { noop } from '../utils' +import { TemplateType } from './templates' class TemplateCard extends React.Component { + static propTypes = { + template: TemplateType, + active: PropTypes.bool, + onSelect: PropTypes.func, + label: PropTypes.string, + icon: PropTypes.string.isRequired, + } static defaultProps = { template: null, active: false, diff --git a/src/onboarding/templates.js b/src/onboarding/templates.js index 91caf8d50..85569e2d3 100644 --- a/src/onboarding/templates.js +++ b/src/onboarding/templates.js @@ -1,3 +1,5 @@ +import PropTypes from 'prop-types' + import democracy from './templates/democracy' import multisig from './templates/multisig' @@ -8,5 +10,7 @@ const Templates = new Map() Templates.set(Democracy, democracy) Templates.set(Multisig, multisig) +const TemplateType = PropTypes.oneOf([Multisig, Democracy]) + export default Templates -export { Multisig, Democracy } +export { TemplateType, Multisig, Democracy } diff --git a/src/onboarding/templates/democracy/ConfigureTokenName.js b/src/onboarding/templates/democracy/ConfigureTokenName.js index 055b3665c..53c968522 100644 --- a/src/onboarding/templates/democracy/ConfigureTokenName.js +++ b/src/onboarding/templates/democracy/ConfigureTokenName.js @@ -1,3 +1,4 @@ +/* eslint react/prop-types: 0 */ import React from 'react' import styled from 'styled-components' import { Field, TextInput, Text, theme } from '@aragon/ui' diff --git a/src/onboarding/templates/democracy/ConfigureVotingDefaults.js b/src/onboarding/templates/democracy/ConfigureVotingDefaults.js index 5946148f4..43afb4d47 100644 --- a/src/onboarding/templates/democracy/ConfigureVotingDefaults.js +++ b/src/onboarding/templates/democracy/ConfigureVotingDefaults.js @@ -1,3 +1,4 @@ +/* eslint react/prop-types: 0 */ import React from 'react' import styled from 'styled-components' import { Field, TextInput, Text, theme } from '@aragon/ui' diff --git a/src/onboarding/templates/multisig/ConfigureMultisigAddresses.js b/src/onboarding/templates/multisig/ConfigureMultisigAddresses.js index ef4cfe99f..27401ad77 100644 --- a/src/onboarding/templates/multisig/ConfigureMultisigAddresses.js +++ b/src/onboarding/templates/multisig/ConfigureMultisigAddresses.js @@ -1,3 +1,4 @@ +/* eslint react/prop-types: 0 */ import React from 'react' import styled, { css } from 'styled-components' import { Button, TextInput, Text, DropDown, theme } from '@aragon/ui' diff --git a/src/onboarding/templates/multisig/ConfigureMultisigToken.js b/src/onboarding/templates/multisig/ConfigureMultisigToken.js index 3c6209955..873be198f 100644 --- a/src/onboarding/templates/multisig/ConfigureMultisigToken.js +++ b/src/onboarding/templates/multisig/ConfigureMultisigToken.js @@ -1,3 +1,4 @@ +/* eslint react/prop-types: 0 */ import React from 'react' import styled from 'styled-components' import { Field, TextInput, Text, theme } from '@aragon/ui' diff --git a/src/prop-types.js b/src/prop-types.js index e7902b260..788a603f9 100644 --- a/src/prop-types.js +++ b/src/prop-types.js @@ -1,17 +1,106 @@ import PropTypes from 'prop-types' +import Aragon from '@aragon/wrapper' +import { + APPS_STATUS_ERROR, + APPS_STATUS_READY, + APPS_STATUS_LOADING, +} from './symbols' +import { isAddress } from './web3-utils' + +const validatorCreator = nonRequiredFunction => { + const validator = nonRequiredFunction + + validator.isRequired = (props, propName, componentName) => { + const value = props[propName] + + if (value === null || value === undefined || value === '') { + return new Error( + `Property ${propName} is required on ${componentName}, but ${value} was given.` + ) + } + + return nonRequiredFunction(props, propName, componentName) + } + + return validator +} + +const ethereumAddressValidator = (props, propName, componentName) => { + const value = props[propName] + + if (value === null || value === undefined || value === '') { + return null + } + + if (!isAddress(value)) { + const valueType = typeof value + let nonAddress = null + + if (valueType !== 'object') { + nonAddress = value.toString() + } + + return new Error( + `Invalid prop ${propName} supplied to ${componentName}. The provided value is not a valid ethereum address.${nonAddress && + ` You provided "${nonAddress}"`}` + ) + } +} + +export const EthereumAddressType = validatorCreator(ethereumAddressValidator) + +export const AppType = PropTypes.shape({ + abi: PropTypes.array.isRequired, + appId: PropTypes.string.isRequired, + baseUrl: PropTypes.string.isRequired, + codeAddress: EthereumAddressType, + functions: PropTypes.array.isRequired, + hasWebApp: PropTypes.bool.isRequired, + name: PropTypes.string.isRequired, + proxyAddress: EthereumAddressType, + src: PropTypes.string.isRequired, + tags: PropTypes.arrayOf(PropTypes.string).isRequired, + + appName: PropTypes.string, + apmRegistry: PropTypes.string, + content: PropTypes.shape({ + location: PropTypes.string.isRequired, + provider: PropTypes.string.isRequired, + }), + description: PropTypes.string, + icons: PropTypes.arrayOf( + PropTypes.shape({ + src: PropTypes.string.isRequired, + }) + ), + isForwarder: PropTypes.bool, + kernelAddress: EthereumAddressType, + isAragonOsInternalApp: PropTypes.bool, + roles: PropTypes.array, + status: PropTypes.string, + version: PropTypes.string, +}) + +export const AppsStatusType = PropTypes.oneOf([ + APPS_STATUS_ERROR, + APPS_STATUS_READY, + APPS_STATUS_LOADING, +]) + +export const AragonType = PropTypes.instanceOf(Aragon) export const FavoriteDaoType = PropTypes.shape({ name: PropTypes.string, - address: PropTypes.string, + address: EthereumAddressType, favorited: PropTypes.bool, }) export const DaoItemType = PropTypes.shape({ name: PropTypes.string, - address: PropTypes.string, + address: EthereumAddressType, }) export const DaoAddressType = PropTypes.shape({ - address: PropTypes.string, + address: EthereumAddressType, domain: PropTypes.string, }) diff --git a/src/provider-strings.js b/src/provider-strings.js index 7010cd2af..3281a1f2f 100644 --- a/src/provider-strings.js +++ b/src/provider-strings.js @@ -5,6 +5,9 @@ // be detected. const PROVIDERS_STRINGS = { + frame: { + 'your Ethereum provider': 'Frame', + }, metamask: { 'your Ethereum provider': 'Metamask', }, diff --git a/src/static-apps.js b/src/static-apps.js index b22bf7810..890512e65 100644 --- a/src/static-apps.js +++ b/src/static-apps.js @@ -1,5 +1,12 @@ import React from 'react' -import { IconHome, IconSettings, IconPermissions, IconApps } from '@aragon/ui' +import { IconSettings, IconPermissions, IconApps } from '@aragon/ui' +import AppIcon from './components/AppIcon/AppIcon' + +const homeApp = { + appId: 'home', + name: 'Home', + instances: [{ instanceId: 'home' }], +} export const staticApps = new Map( Object.entries({ @@ -14,10 +21,8 @@ export const staticApps = new Map( }, home: { app: { - appId: 'home', - name: 'Home', - icon: , - instances: [{ instanceId: 'home' }], + ...homeApp, + icon: , }, route: '/', }, diff --git a/src/types/App.js b/src/types/App.js deleted file mode 100644 index 248e28541..000000000 --- a/src/types/App.js +++ /dev/null @@ -1,19 +0,0 @@ -import { shape, array, string, bool } from 'prop-types' - -const App = shape({ - abi: array.isRequired, - appId: string.isRequired, - baseUrl: string.isRequired, - codeAddress: string.isRequired, - functions: array.isRequired, - hasWebApp: bool.isRequired, - isAragonOsInternalApp: bool, - isForwarder: bool, - kernelAddress: string, - name: string.isRequired, - proxyAddress: string.isRequired, - roles: array.isRequired, - src: string.isRequired, -}) - -export default App diff --git a/src/utils.js b/src/utils.js index 7b9d88230..7f8fd7a7b 100644 --- a/src/utils.js +++ b/src/utils.js @@ -20,16 +20,13 @@ export function appIconUrl(app) { : null } -export function makeEtherscanBaseUrl(network) { - // Don't make etherscan urls if the network isn't one that etherscan supports - if ( - network === 'main' || - network === 'kovan' || - network === 'rinkeby' || - network === 'ropsten' - ) { - return `https://${network === 'main' ? '' : `${network}.`}etherscan.io` - } +export function isElectron() { + // See https://github.com/electron/electron/issues/2288 + return ( + typeof navigator === 'object' && + typeof navigator.userAgent === 'string' && + navigator.userAgent.indexOf('Electron') >= 0 + ) } export function noop() {} @@ -48,3 +45,7 @@ export function log(...params) { console.log(...params) } } + +export function isString(str) { + return typeof str === 'string' || str instanceof String +} diff --git a/src/web3-utils.js b/src/web3-utils.js index 61c8246de..ed0ce285b 100644 --- a/src/web3-utils.js +++ b/src/web3-utils.js @@ -5,6 +5,8 @@ */ import Web3 from 'web3' import BN from 'bn.js' +import { InvalidNetworkType, InvalidURI, NoConnection } from './errors' +import { isElectron } from './utils' const EMPTY_ADDRESS = '0x0000000000000000000000000000000000000000' @@ -20,6 +22,46 @@ export function addressesEqual(first, second) { return first === second } +const websocketRegex = /^wss?:\/\/.+/ + +/** + * Check if the ETH node at the given URI is compatible for the current environment + * @param {string} uri URI of the ETH node. + * @param {string} expectedNetworkType The expected network type of the ETH node. + * @returns {Promise} Resolves if the ETH node is compatible, otherwise throws: + * - InvalidURI: URI given is not compatible (e.g. must be WebSockets) + * - InvalidNetworkType: ETH node connected to wrong network + * - NoConnection: Couldn't connect to URI + */ +export async function checkValidEthNode(uri, expectedNetworkType) { + // Must be websocket connection + if (!websocketRegex.test(uri)) { + throw new InvalidURI('The URI must use the WebSocket protocol') + } + + try { + const web3 = new Web3(uri) + const connectedNetworkType = await web3.eth.net.getNetworkType() + if (web3.currentProvider.disconnect) { + web3.currentProvider.disconnect() + } else { + // Older versions of web3's providers didn't expose a generic interface for disconnecting + web3.currentProvider.connection.close() + } + + if (connectedNetworkType !== expectedNetworkType) { + throw new InvalidNetworkType() + } + } catch (err) { + if (err instanceof InvalidNetworkType) { + throw err + } + throw new NoConnection() + } + + return true +} + /** * Format the balance to a fixed number of decimals * @@ -48,32 +90,35 @@ export function formatBalance( return `${whole}.${fraction}` } -/** - * Shorten an Ethereum address. `charsLength` allows to change the number of - * characters on both sides of the ellipsis. - * - * Examples: - * shortenAddress('0x19731977931271') // 0x1973…1271 - * shortenAddress('0x19731977931271', 2) // 0x19…71 - * shortenAddress('0x197319') // 0x197319 (already short enough) - * - * @param {string} address The address to shorten - * @param {number} [charsLength=4] The number of characters to change on both sides of the ellipsis - * @returns {string} The shortened address +export function getEmptyAddress() { + return EMPTY_ADDRESS +} + +/* + * Return the injected provider, if any. */ -export function shortenAddress(address, charsLength = 4) { - const prefixLength = 2 // "0x" - if (!address) { - return '' +export function getInjectedProvider() { + if (window.ethereum) { + return window.ethereum } - if (address.length < charsLength * 2 + prefixLength) { - return address + if (window.web3 && window.web3.currentProvider) { + return window.web3.currentProvider } - return ( - address.slice(0, charsLength + prefixLength) + - '…' + - address.slice(-charsLength) - ) + return null +} + +// Get the first account of a web3 instance +export async function getMainAccount(web3) { + try { + const accounts = await web3.eth.getAccounts() + return (accounts && accounts[0]) || null + } catch (err) { + return null + } +} + +export function getUnknownBalance() { + return new BN('-1') } // Cache web3 instances used in the app @@ -93,14 +138,25 @@ export function getWeb3(provider) { return web3 } -// Get the first account of a web3 instance -export async function getMainAccount(web3) { - try { - const accounts = await web3.eth.getAccounts() - return (accounts && accounts[0]) || null - } catch (err) { - return null +// Returns an identifier for the provider, if it can be detected +export function identifyProvider(provider) { + if (provider && isElectron()) { + return 'frame' } + if (provider && provider.isMetaMask) { + return 'metamask' + } + return 'unknown' +} + +export function isConnected(provider) { + // EIP-1193 compliant providers may not include `isConnected()`, but most should support it for + // the foreseeable future to be backwards compatible with older Web3.js implementations. + // The `status` property is also not required by EIP-1193, but is often set on providers for + // backwards compatibility as well. + return typeof provider.isConnected === 'function' + ? provider.isConnected() + : provider.status === 'connected' } // Check if the address represents an empty address @@ -108,24 +164,36 @@ export function isEmptyAddress(address) { return addressesEqual(address, EMPTY_ADDRESS) } -export function getEmptyAddress() { - return EMPTY_ADDRESS -} - -export function getUnknownBalance() { - return new BN('-1') +export function isValidEnsName(name) { + return /^([\w-]+\.)+eth$/.test(name) } -// Returns an identifier for the provider, if it can be detected -export function identifyProvider(provider) { - if (provider && provider.isMetaMask) { - return 'metamask' +/** + * Shorten an Ethereum address. `charsLength` allows to change the number of + * characters on both sides of the ellipsis. + * + * Examples: + * shortenAddress('0x19731977931271') // 0x1973…1271 + * shortenAddress('0x19731977931271', 2) // 0x19…71 + * shortenAddress('0x197319') // 0x197319 (already short enough) + * + * @param {string} address The address to shorten + * @param {number} [charsLength=4] The number of characters to change on both sides of the ellipsis + * @returns {string} The shortened address + */ +export function shortenAddress(address, charsLength = 4) { + const prefixLength = 2 // "0x" + if (!address) { + return '' } - return 'unknown' -} - -export function isValidEnsName(name) { - return /^([\w-]+\.)+eth$/.test(name) + if (address.length < charsLength * 2 + prefixLength) { + return address + } + return ( + address.slice(0, charsLength + prefixLength) + + '…' + + address.slice(-charsLength) + ) } // Re-export some utilities from web3-utils