diff --git a/devkit/.eslintrc.js b/devkit/.eslintrc.js index 6d157cf..f1d0b31 100644 --- a/devkit/.eslintrc.js +++ b/devkit/.eslintrc.js @@ -1,5 +1,6 @@ module.exports = { rules: { + 'react/prop-types': 'off', 'require-jsdoc': 'off', 'valid-jsdoc': 'off' } diff --git a/devkit/pages/_app.js b/devkit/pages/_app.js index 05e0717..300438b 100644 --- a/devkit/pages/_app.js +++ b/devkit/pages/_app.js @@ -25,7 +25,7 @@ class Devkit extends App { return ( - My page + Sojourn Devkit {/* Wrap every page in Jss and Theme providers */} ({ - root: { - textAlign: 'center', - paddingTop: theme.spacing.unit * 20 - } -}); - -const Index = ({ classes }) => ( -
- - What is this developer kit - - - and why is it free? - -
-); +import Grid from '@material-ui/core/Grid'; +import HeartIcon from 'react-feather/dist/icons/heart'; +import EyeIcon from 'react-feather/dist/icons/eye'; +import LockIcon from 'react-feather/dist/icons/lock'; +import SunIcon from 'react-feather/dist/icons/sun'; +import GithubIcon from 'react-feather/dist/icons/github'; +import Band from '../src/components/Band'; +import ImageBand from '../src/components/ImageBand'; +import AppExpose, { + AppExposeColumn, + AppExposeItem +} from '../src/components/AppExpose'; +import CTACircle from '../src/components/CTACircle'; +import AsideText from '../src/components/AsideText'; +import InfoText from '../src/components/InfoText'; +import AsideImage from '../src/components/AsideImage'; +import IconBlob from '../src/components/IconBlob'; +import { H1, H2, H3, H4, P } from '../src/components/Typography'; + +import CodeBlocks from '../src/components/images/CodeBlocks'; +import HamsterWorks from '../src/components/images/HamsterWorks'; +import SojournLogo from '../src/components/images/SojournLogo'; + +const Index = () => ( +
+ + + + + +

What is this developer kit

+

and why are you giving it away for free?

+

+ The ConsenSys Web3 Studio in Durham, NC has a pretty sweet job. Our + team of developers, designers and writers spend our days coming up + with what we hope are novel, unexpected ways to use blockchain and + Web3-related technologies. We publish stories about these uses in the + form of what we call “product reviews from the near future.” Then we + build some of these stories into developer kits like this one. The + kits are open source. +

+

+ Success for us is you taking the kit’s ideas and code and building + your own web3 applications. +

+
+ + + + +

+ Get Started! +

+

+ Start building your Web3 app +

+

+ Get the Code +

+
+
+
+ + + + + +

What is Sojourn

+

+ SoJourn is a really simple example of what you can do with an approach + to Web3 storage that our team simplified for a React Native + application. It’s a note taking application...or rather, it is a set + of UI and back-end components that you can use to construct your own + Web3 note taking application. +

+

+ Think of SoJourn like the picture on a box of Legos. The important + thing is what you do with those pieces, and we hope you find a few of + them unique and useful in your own projects. +

+

+ What makes SoJourn a Web3 note taker is the way it stores data, + minimizing the need to trust institutions to keep it safe. +

+
+
+ + +

How it Works

+

+ SoJourn lets you write notes or edit existing notes. Then it: +

+
    +
  1. +

    + Stores and encrypts the notes locally on an iPhone using the Apple + keychain, so that all the notes are instantly available to the + user; +

    +
  2. + +
  3. +

    + “Notarizes” the data by storing a hash onto a public blockchain + (we’re using the Ethereum Rinkeby testnet to demonstrate), so that + a user can prove that the original note was saved on a particular + date and not changed since; +

    +
  4. + +
  5. +

    + Encrypts each note, divides it into parts (using Shamir’s Secret + Sharing algorithm), and distributes each of them to different + locations on IPFS. We’re using the Infura gateway as an example, + but you can add your own. +

    +
  6. +
+

+ This means the user doesn’t have to worry about losing their data if + they lose their device. But – and this is important – the user also + doesn’t have to worry about a centralized cloud service keeping the + data secure. [Insert video.] +

+
+ + + +
+ + + +

Why do Web3 Data Storage This Way?

+

+ We thought of many practical uses of this kind of Web3 Storage + pattern, the simplest of which is a diary app that lets you record + things like your account of harassment, which you want to be sure + can’t be hacked into but which you can also produce years later and + prove that you didn’t just recently make the story up. +

+

+ The whole point of our team is to give you a leg up on your own ideas, + but we did publish one story that might give you ideas: +

+

+ The SoJourn story was published on Medium on October 4, 2018 and + received a lot of interest from developers and startups generally. In + the story, a journalist is taking sensitive interview notes with a + political informant and really wants to be sure that nobody but her + will ever access them unless and until she wants to share them. We + thought the user, Zoe the journalist, made a good case for a + professional need for the functionality. +

+
+
+ + + + + + + + + + + + + + + + + + + + +

Restore from Vault

+

+ The Vault uses a Web3 approach to secure data storage, so that the + user doesn’t have to worry about losing their device and their data + with it. +

+

+ We have not added the restore function in this UI, because it relies + on decisions you will make about how to handle passing data to the new + device. Access full details of the Vault module, its use of Shamir’s + Secret Sharing and IPFS, and approaches to modifying it for your + project here. +

+
+
+ + +

Left for you

+

+ To be a complete note taking app, clearly it will want to allow the + user to do all sorts of things, like formatting text, sorting the note + list, and searching. These are basics that can be done in any number + of ways and are usually where designers and app developers make their + mark, so we have left this to you. We hope that the clarity we tried + to achieve with the structure of the code and the docs make it easy + for you to add functionality. +

+

+ Special note: The Vault enables versioning, but we implemented storage + on device more simply, so the devKit doesn’t enable the UI to provide + version control as-is. There are many well-known ways to implement + data to support versioning, so we leave that to you. +

+
+ +
+ + +

Other things you could use it for

+

+ At half-past eight the door opened, the policeman appeared, and, + requesting them to follow him, led the way to an adjoining hall. It + was evidently. +

+
+ + } + title="First One" + body="At half-past eight the door opened" + /> -Index.propTypes = { - classes: PropTypes.object.isRequired -}; + } + title="Second One" + body="At half-past eight the door opened" + /> + + } + title="Third One" + body="At half-past eight the door opened" + /> + } + title="Fourth One" + body="At half-past eight the door opened" + /> + +
+ + +

Key Modules

+
+ + + +

The Basic Notes App - UI

+

+ The notes app is structured as you would expect for a redux based + React app. For example, the screen to edit a note is in the Note + module directory `src/modules/notes/EditNotesScreen.js`. The same + directory contains reducers and selectors providing the business logic + for managing apps. +

+

+ For quick reference, the source code follows these patterns (under + src/). +

+

[Figure out to format src]

+
+
+ + + +

The Vault

+

The repo for The Vault is.

+

+ The code for the service lives in the following files in the repo. +

+

+ [Modsquad] What if you circumvented the encryption and sent the file + directly to the Vault. You would then have an easy of sharing the file + simply by giving recipients the IPFS locations. +

+

+ [Modsquad] Ways to handle the location array (or when it is ok to + centralize). We did it this way...but there are many other ways. Here + are a few. +

+

[Figure out to format src]

+
+ +
+ + + +

Alternative Approaches

+

+ We built the SoJourn devKit with an eye toward providing a clean, open + source Shamir’s Secret Sharing library that works for React Native + apps. Mission accomplished. +

+

+ Until recently, there were no well-known ways to do Web3 storage in a + way that we considered acceptable for very secret data. It’s one thing + to worry about a system admin at AWS, but I’ll take that over some guy + somewhere with a hard disk on a public network storing my file, even + if it is encrypted. +

+

+ However, there are new ways emerging of splitting up files, rendering + them useless without also having many other slices, and storing them + on decentralized infrastructure emerging. One service is Storj’s + upcoming version 3. [Storj...add content here.] +

+

+ In fact, you might want to consider modifying the Vault to not only + store shares on IPFS but also on Storj and other gateways, for an + additional assurance that no single gateway provider trafficked in + enough shares to reconstruct the file. All depends on how paranoid you + want to be. (Storj might also be a good choice for storing the + encrypted location file that SoJourn needs to restore data to a new + device +

+
+
+ + + + +

The Notary

+

+ The Notary service handles the hashing of user notes onto a + blockchain, preferably a mainnet like Ethereum’s, so that there is a + solid chance of long-term continuity and strong tamper-resistance. +

+

+ If we were doing this all over again, we’d probably make the Notary + library stand apart, or use something like Chainpoint but here’s what + we have: +

+

+ A Smart Contract is responsible for storing the note’s hash and + timestamp. (We’ve provided the address of the living contract that we + wrote onto the Ethereum Rinkeby testnet for demonstration). +

+

+ At the user’s request, we use Web3.js in the note app that calls the + Notary function on the smart contract. +

+
+
+ + + +

Authentication & Wallet

+

+ Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do + eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad + minim veniam, quis nostrud exercitation ullamco laboris nisi ut + aliquip +

+
+ +
+
+); -export default withStyles(styles)(Index); +export default Index; diff --git a/devkit/src/components/AppExpose.js b/devkit/src/components/AppExpose.js new file mode 100644 index 0000000..ebeaa50 --- /dev/null +++ b/devkit/src/components/AppExpose.js @@ -0,0 +1,41 @@ +import Grid from '@material-ui/core/Grid/Grid'; +import Typography from '@material-ui/core/Typography/Typography'; +import React from 'react'; + +export const AppExposeColumn = ({ children }) => ( + + {children} + +); + +export const AppExposeItem = ({ title, body }) => ( + + + {title.toUpperCase()} + + + {body} + + +); + +const AppExpose = ({ title, subtitle, children }) => ( + <> + + + + {title} + + + {subtitle} + + + + + + {children} + + +); + +export default AppExpose; diff --git a/devkit/src/components/AsideImage.js b/devkit/src/components/AsideImage.js new file mode 100644 index 0000000..152154b --- /dev/null +++ b/devkit/src/components/AsideImage.js @@ -0,0 +1,10 @@ +import React from 'react'; +import Grid from '@material-ui/core/Grid'; + +const AsideImage = ({ children, size = 4 }) => ( + + {children} + +); + +export default AsideImage; diff --git a/devkit/src/components/AsideText.js b/devkit/src/components/AsideText.js new file mode 100644 index 0000000..d818083 --- /dev/null +++ b/devkit/src/components/AsideText.js @@ -0,0 +1,32 @@ +import React from 'react'; +import Grid from '@material-ui/core/Grid'; +import { withStyles } from '@material-ui/core/styles'; +import { ms } from '../theme'; + +const styles = theme => { + const fontSizeEm = ms(11); + + return { + root: { + position: 'relative' + }, + order: { + top: '-.4em', + position: 'absolute', + fontSize: `${fontSizeEm}em`, + fontWeight: 600, + fontFamily: theme.typography.fontFamily, + color: theme.palette.grey[200], + zIndex: -1 + } + }; +}; + +const AsideText = ({ children, size = 6, order, classes }) => ( + + {order && {order}} + {children} + +); + +export default withStyles(styles)(AsideText); diff --git a/devkit/src/components/Band.js b/devkit/src/components/Band.js new file mode 100644 index 0000000..503f1f8 --- /dev/null +++ b/devkit/src/components/Band.js @@ -0,0 +1,42 @@ +import React from 'react'; +import classNames from 'classnames'; +import Grid from '@material-ui/core/Grid'; +import { withStyles } from '@material-ui/core/styles'; + +const style = theme => { + const verticalPad = theme.spacing.unit * 10; + const horizPad = theme.spacing.unit * 4; + const pxToRem = theme.typography.pxToRem; + + return { + dark: { + backgroundColor: theme.palette.grey[100] + }, + content: { + maxWidth: theme.breakpoints.values.lg, + width: '100%', + marginLeft: 'auto', + marginRight: 'auto', + padding: `${pxToRem(verticalPad)} ${pxToRem(horizPad)}` + }, + stacked: { + paddingBottom: 0 + } + }; +}; + +const Band = ({ children, classes, dark, stacked, ...otherProps }) => ( +
+ + {children} + +
+); + +export default withStyles(style)(Band); diff --git a/devkit/src/components/CTACircle.js b/devkit/src/components/CTACircle.js new file mode 100644 index 0000000..c752494 --- /dev/null +++ b/devkit/src/components/CTACircle.js @@ -0,0 +1,27 @@ +import React from 'react'; +import { withStyles } from '@material-ui/core/styles'; + +const styles = theme => ({ + root: { + textDecoration: 'none' + }, + circle: { + display: 'flex', + flexDirection: 'column', + color: theme.palette.common.white, + backgroundColor: theme.palette.primary.main, + alignItems: 'center', + justifyContent: 'center', + height: 350, + width: 350, + borderRadius: '50%' + } +}); + +const CTACircle = ({ href, classes, children }) => ( + +
{children}
+
+); + +export default withStyles(styles)(CTACircle); diff --git a/devkit/src/components/Footer.js b/devkit/src/components/Footer.js new file mode 100644 index 0000000..e69de29 diff --git a/devkit/src/components/IconBlob.js b/devkit/src/components/IconBlob.js new file mode 100644 index 0000000..76edf41 --- /dev/null +++ b/devkit/src/components/IconBlob.js @@ -0,0 +1,19 @@ +import Grid from '@material-ui/core/Grid/Grid'; +import Typography from '@material-ui/core/Typography/Typography'; +import { withTheme } from '@material-ui/core/styles'; +import React from 'react'; + +const IconBlob = ({ title, body, icon, theme }) => ( + + {React.cloneElement(icon, { + color: theme.palette.primary.main, + size: theme.spacing.unit * 6 + })} + + {title.toUpperCase()} + + {body} + +); + +export default withTheme()(IconBlob); diff --git a/devkit/src/components/ImageBand.js b/devkit/src/components/ImageBand.js new file mode 100644 index 0000000..835318b --- /dev/null +++ b/devkit/src/components/ImageBand.js @@ -0,0 +1,18 @@ +import React from 'react'; +import { withStyles } from '@material-ui/core/styles'; + +const style = () => { + return { + image: { + width: '100%' + } + }; +}; + +const Band = ({ src, alt, classes }) => ( +
+ {alt} +
+); + +export default withStyles(style)(Band); diff --git a/devkit/src/components/ImageBlock.js b/devkit/src/components/ImageBlock.js new file mode 100644 index 0000000..f7ed32b --- /dev/null +++ b/devkit/src/components/ImageBlock.js @@ -0,0 +1,10 @@ +import React from 'react'; +import Grid from '@material-ui/core/Grid'; + +const ImageBlock = ({ children, size = 5 }) => ( + + {children} + +); + +export default ImageBlock; diff --git a/devkit/src/components/InfoText.js b/devkit/src/components/InfoText.js new file mode 100644 index 0000000..64d1e3a --- /dev/null +++ b/devkit/src/components/InfoText.js @@ -0,0 +1,21 @@ +import React from 'react'; +import Grid from '@material-ui/core/Grid'; +import { withStyles } from '@material-ui/core/styles'; + +const styles = theme => { + return { + root: { + [theme.breakpoints.up('md')]: { + marginLeft: `${(100 / 12) * 2}%` + } + } + }; +}; + +const InfoText = ({ children, classes }) => ( + + {children} + +); + +export default withStyles(styles)(InfoText); diff --git a/devkit/src/components/TextBlock.js b/devkit/src/components/TextBlock.js new file mode 100644 index 0000000..b540458 --- /dev/null +++ b/devkit/src/components/TextBlock.js @@ -0,0 +1,10 @@ +import React from 'react'; +import Grid from '@material-ui/core/Grid'; + +const TextBlock = ({ children, size = 7 }) => ( + + {children} + +); + +export default TextBlock; diff --git a/devkit/src/components/Typography.js b/devkit/src/components/Typography.js new file mode 100644 index 0000000..632665a --- /dev/null +++ b/devkit/src/components/Typography.js @@ -0,0 +1,15 @@ +import React from 'react'; +import Typography from '@material-ui/core/Typography'; + +const variantFactory = variant => ({ children, ...props }) => ( + + {children} + +); + +export const H1 = variantFactory('h1'); +export const H2 = variantFactory('h2'); +export const H3 = variantFactory('h3'); +export const H4 = variantFactory('h4'); +export const H6 = variantFactory('h6'); +export const P = variantFactory('body1'); diff --git a/devkit/src/components/images/CodeBlocks.js b/devkit/src/components/images/CodeBlocks.js new file mode 100644 index 0000000..63f491d --- /dev/null +++ b/devkit/src/components/images/CodeBlocks.js @@ -0,0 +1,1002 @@ +import React from 'react'; + +export default function CodeBlocks({ size = '100%' }) { + return ( + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ); +} diff --git a/devkit/src/components/images/HamsterWorks.js b/devkit/src/components/images/HamsterWorks.js new file mode 100644 index 0000000..0c811cc --- /dev/null +++ b/devkit/src/components/images/HamsterWorks.js @@ -0,0 +1,731 @@ +import React from 'react'; + +export default function HamsterWorks({ size = '100%' }) { + return ( + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ); +} diff --git a/devkit/src/components/images/SojournLogo.js b/devkit/src/components/images/SojournLogo.js new file mode 100644 index 0000000..ef002d2 --- /dev/null +++ b/devkit/src/components/images/SojournLogo.js @@ -0,0 +1,112 @@ +import React from 'react'; + +export default function SojournLogo({ size = '4rem' }) { + return ( + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ); +} diff --git a/devkit/src/getPageContext.js b/devkit/src/getPageContext.js index 72e4bb9..e539c7c 100644 --- a/devkit/src/getPageContext.js +++ b/devkit/src/getPageContext.js @@ -1,30 +1,6 @@ import { SheetsRegistry } from 'jss'; -import { - createMuiTheme, - createGenerateClassName -} from '@material-ui/core/styles'; -import purple from '@material-ui/core/colors/purple'; -import green from '@material-ui/core/colors/green'; - -// A theme with custom primary and secondary color. -// It's optional. -const theme = createMuiTheme({ - palette: { - primary: { - light: purple[300], - main: purple[500], - dark: purple[700] - }, - secondary: { - light: green[300], - main: green[500], - dark: green[700] - } - }, - typography: { - useNextVariants: true - } -}); +import { createGenerateClassName } from '@material-ui/core/styles'; +import theme from './theme'; function createPageContext() { return { diff --git a/devkit/src/theme.js b/devkit/src/theme.js new file mode 100644 index 0000000..028e716 --- /dev/null +++ b/devkit/src/theme.js @@ -0,0 +1,81 @@ +import { createMuiTheme } from '@material-ui/core'; +import red from '@material-ui/core/colors/red'; +import green from '@material-ui/core/colors/green'; +import ModularScale from 'modular-scale'; + +export const ms = ModularScale({ + ratio: ModularScale.ratios.majorThird, + base: 1 +}); + +export default createMuiTheme({ + palette: { + primary: { + light: red[400], + main: red[600], + dark: red[800] + }, + secondary: { + light: green[300], + main: green[500], + dark: green[700] + }, + background: '#fff' + }, + typography: { + htmlFontSize: 15, + useNextVariants: true, + fontFamily: [ + '-apple-system', + 'BlinkMacSystemFont', + '"Segoe UI"', + 'Roboto', + '"Helvetica Neue"', + 'Arial', + 'sans-serif', + '"Apple Color Emoji"', + '"Segoe UI Emoji"', + '"Segoe UI Symbol"' + ].join(','), + h1: { + fontSize: `${ms(7)}em`, + fontWeight: 500 + }, + h2: { + fontSize: `${ms(6)}em` + }, + h3: { + fontSize: `${ms(5)}em` + }, + h4: { + fontSize: `${ms(4)}em` + }, + h5: { + fontSize: `${ms(3)}em` + }, + h6: { + fontSize: `${ms(2)}em` + }, + subtitle1: { + fontSize: `${ms(1)}em` + }, + subtitle2: { + fontSize: `${ms(-1)}em` + }, + body1: { + fontSize: `${ms(1)}em` + }, + body2: { + fontSize: `${ms(-1)}em` + }, + button: { + fontSize: `${ms(1)}em` + }, + caption: { + fontSize: `${ms(-1)}em` + }, + overline: { + fontSize: `${ms(-1)}em` + } + } +}); diff --git a/devkit/static/shamir.png b/devkit/static/shamir.png new file mode 100644 index 0000000..d190dca Binary files /dev/null and b/devkit/static/shamir.png differ diff --git a/next.config.js b/next.config.js new file mode 100644 index 0000000..895f370 --- /dev/null +++ b/next.config.js @@ -0,0 +1,6 @@ +const packageInfo = require('./package.json'); + +module.exports = { + assetPrefix: + process.env.NODE_ENV === 'production' ? `/${packageInfo.name}` : '' +}; diff --git a/package.json b/package.json index 2f064c1..2549a6b 100644 --- a/package.json +++ b/package.json @@ -1,5 +1,5 @@ { - "name": "sojourn", + "name": "imagineering-sojourn", "version": "0.0.1", "private": true, "scripts": { @@ -18,7 +18,7 @@ "e2e": "yarn concurrently -k -s first \"yarn start\" \"yarn detox:build && yarn detox:test\"", "ci": "./scripts/travis-test.sh", "devkit": "next devkit", - "devkit:build": "next build devkit && next export devkit" + "devkit:build": "NODE_ENV=production next build devkit && next export devkit" }, "dependencies": { "Base64": "^1.0.1", @@ -53,6 +53,7 @@ "babel-eslint": "^10.0.1", "babel-jest": "^23.6.0", "chai": "^4.2.0", + "classnames": "^2.2.6", "concurrently": "^4.0.1", "detox": "^9.0.4", "dotenv": "^6.1.0", @@ -68,10 +69,12 @@ "husky": "^1.1.1", "jest": "^23.6.0", "metro-react-native-babel-preset": "^0.48.0", + "modular-scale": "^5.1.0", "next": "^7.0.2", "prettier": "^1.14.3", "pretty-quick": "^1.7.0", "prop-types": "^15.6.2", + "react-feather": "^1.1.4", "react-jss": "^8.6.1", "react-native-mock-render": "^0.1.2", "react-test-renderer": "16.5.0", @@ -101,7 +104,7 @@ }, "rnpm": { "assets": [ - "./src/assets/fonts" + "./rc/assets/fonts" ] } } diff --git a/yarn.lock b/yarn.lock index d8b6a8f..e19833f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2682,7 +2682,7 @@ class-utils@^0.3.5: isobject "^3.0.0" static-extend "^0.1.1" -classnames@^2.2.5: +classnames@^2.2.5, classnames@^2.2.6: version "2.2.6" resolved "https://registry.yarnpkg.com/classnames/-/classnames-2.2.6.tgz#43935bffdd291f326dad0a205309b38d00f650ce" @@ -7113,6 +7113,10 @@ mock-fs@^4.1.0: version "4.7.0" resolved "https://registry.yarnpkg.com/mock-fs/-/mock-fs-4.7.0.tgz#9f17e219cacb8094f4010e0a8c38589e2b33c299" +modular-scale@^5.1.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/modular-scale/-/modular-scale-5.1.0.tgz#697858b2600462fa4fc5c800e6ad5a161a19f381" + moment@^2.10.6, moment@^2.22.1, moment@^2.22.2: version "2.22.2" resolved "https://registry.yarnpkg.com/moment/-/moment-2.22.2.tgz#3c257f9839fc0e93ff53149632239eb90783ff66" @@ -8319,6 +8323,10 @@ react-event-listener@^0.6.2: prop-types "^15.6.0" warning "^4.0.1" +react-feather@^1.1.4: + version "1.1.4" + resolved "https://registry.yarnpkg.com/react-feather/-/react-feather-1.1.4.tgz#d0143da95f9d52843cf13a553091573a7c617897" + react-is@^16.3.2, react-is@^16.5.0, react-is@^16.6.0: version "16.6.0" resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.6.0.tgz#456645144581a6e99f6816ae2bd24ee94bdd0c01"