1
0
mirror of https://github.com/oceanprotocol/commons.git synced 2023-03-15 18:03:00 +01:00

Merge pull request #176 from oceanprotocol/feature/wallets

Wallet selection
This commit is contained in:
Jernej Pregelj 2019-07-17 09:22:11 +02:00 committed by GitHub
commit 599e1834cc
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
86 changed files with 2867 additions and 3096 deletions

View File

@ -60,7 +60,7 @@ To make use of all the functionality, you need to connect to an Ocean network.
By default, the client will connect to Ocean components running within [Ocean's Pacific network](https://docs.oceanprotocol.com/concepts/pacific-network/) remotely.
With your MetaMask, connect to the Pacific network. To do this:
By default, the client uses a burner wallet connected to the correct network automatically. If you choose to use MetaMask, you need to connect to the Pacific network. To do this:
1. select Custom RPC in the network dropdown in MetaMask
2. under New Network, enter `https://pacific.oceanprotocol.com` as the custom RPC URL

View File

@ -0,0 +1,8 @@
const marketMock = {
totalAssets: 1000,
categories: ['category'],
network: 'Pacific',
networkMatch: true
}
export { marketMock }

View File

@ -3,30 +3,32 @@ import oceanMock from './ocean-mock'
const userMock = {
isLogged: false,
isLoading: false,
isWeb3: false,
isOceanNetwork: false,
isBurner: false,
isWeb3Capable: false,
account: '',
web3: {},
...oceanMock,
balance: { eth: 0, ocn: 0 },
network: '',
requestFromFaucet: jest.fn(),
unlockAccounts: jest.fn(),
loginMetamask: jest.fn(),
loginBurnerWallet: jest.fn(),
message: ''
}
const userMockConnected = {
isLogged: true,
isLoading: false,
isWeb3: true,
isOceanNetwork: true,
isBurner: false,
isWeb3Capable: true,
account: '0xxxxxx',
web3: {},
...oceanMock,
balance: { eth: 0, ocn: 0 },
network: '',
requestFromFaucet: jest.fn(),
unlockAccounts: jest.fn(),
loginMetamask: jest.fn(),
loginBurnerWallet: jest.fn(),
message: ''
}

1431
client/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -13,10 +13,11 @@
},
"dependencies": {
"@oceanprotocol/art": "^2.2.0",
"@oceanprotocol/squid": "0.6.2",
"@oceanprotocol/squid": "^0.6.4",
"@oceanprotocol/typographies": "^0.1.0",
"@sindresorhus/slugify": "^0.9.1",
"axios": "^0.19.0",
"bip39": "^3.0.2",
"classnames": "^2.2.6",
"ethereum-blockies": "github:MyEtherWallet/blockies",
"filesize": "^4.1.2",
@ -38,6 +39,7 @@
"react-popper": "^1.3.3",
"react-router-dom": "^5.0.1",
"react-transition-group": "^4.1.1",
"truffle-hdwallet-provider": "^1.0.13",
"web3": "1.0.0-beta.37"
},
"devDependencies": {

View File

@ -0,0 +1 @@
declare module 'truffle-hdwallet-provider'

View File

@ -1,6 +1,5 @@
import React from 'react'
import { Route, Switch } from 'react-router-dom'
import withTracker from './hoc/withTracker'
import About from './routes/About'
import Home from './routes/Home'
@ -17,17 +16,17 @@ import Channel from './components/templates/Channel'
const Routes = () => (
<Switch>
<Route component={withTracker(Home)} exact path="/" />
<Route component={withTracker(Styleguide)} path="/styleguide" />
<Route component={withTracker(About)} path="/about" />
<Route component={withTracker(Publish)} path="/publish" />
<Route component={withTracker(Search)} path="/search" />
<Route component={withTracker(Asset)} path="/asset/:did" />
<Route component={withTracker(Faucet)} path="/faucet" />
<Route component={withTracker(History)} path="/history" />
<Route component={withTracker(Channels)} exact path="/channels" />
<Route component={withTracker(Channel)} path="/channels/:channel" />
<Route component={withTracker(NotFound)} />
<Route component={Home} exact path="/" />
<Route component={Styleguide} path="/styleguide" />
<Route component={About} path="/about" />
<Route component={Publish} path="/publish" />
<Route component={Search} path="/search" />
<Route component={Asset} path="/asset/:did" />
<Route component={Faucet} path="/faucet" />
<Route component={History} path="/history" />
<Route component={Channels} exact path="/channels" />
<Route component={Channel} path="/channels/:channel" />
<Route component={NotFound} />
</Switch>
)

View File

@ -2,18 +2,82 @@
.account {
display: flex;
flex-wrap: wrap;
align-items: center;
text-align: left;
> div {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
font-family: $font-family-monospace;
font-size: $font-size-small;
> div:first-of-type {
flex: 0 0 80%;
}
}
.accountId {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
font-family: $font-family-monospace;
font-size: $font-size-small;
font-weight: 700;
}
.unlock {
font-size: $font-size-small !important; // stylelint-disable-line
margin-left: $spacer / 2;
}
.accountType {
width: 100%;
margin-left: calc(1.5rem + #{$spacer / 3});
font-size: $font-size-small;
font-weight: $font-weight-bold;
color: $brand-grey-light;
}
.toggle {
background: none;
font-family: inherit;
font-size: inherit;
font-weight: inherit;
color: inherit;
border: none;
padding: 0;
cursor: pointer;
svg {
display: inline-block;
fill: currentColor;
margin-right: $spacer / 8;
transition: .2s ease-out;
}
}
.open {
transform: rotate(90deg);
}
.seedphrase {
margin-top: $spacer / 2;
margin-left: calc(1.5rem + #{$spacer / 4});
margin-right: calc(1.5rem + #{$spacer / 4});
code {
display: block;
text-align: center;
padding: $spacer / 2 $spacer;
border-radius: $border-radius;
background: $body-background;
border: 1px solid $brand-grey-lighter;
margin-bottom: $spacer / 4;
word-break: normal;
}
}
.seedphraseHelp {
color: $brand-grey-light;
font-size: $font-size-small;
margin: 0;
}
.blockies {
width: 1.5rem;
height: 1.5rem;
@ -21,4 +85,5 @@
display: inline-block;
margin-right: $spacer / 3;
margin-left: 0;
border: 1px solid $brand-grey-lighter;
}

View File

@ -1,28 +1,61 @@
import React from 'react'
import { render } from '@testing-library/react'
import { render, fireEvent } from '@testing-library/react'
import { toDataUrl } from 'ethereum-blockies'
import Account from './Account'
import { User } from '../../context'
import { userMockConnected } from '../../../__mocks__/user-mock'
describe('Account', () => {
it('renders without crashing', () => {
const { container } = render(<Account account={'0xxxxxxxxxxxxxxx'} />)
const { container } = render(
<User.Provider
value={{ ...userMockConnected, account: '0xxxxxxxxxxxxxxx' }}
>
<Account />
</User.Provider>
)
expect(container.firstChild).toBeInTheDocument()
})
it('outputs empty state without account', () => {
const { container } = render(<Account account={''} />)
const { container, getByText } = render(
<User.Provider value={{ ...userMockConnected, account: '' }}>
<Account />
</User.Provider>
)
expect(container.firstChild).toHaveTextContent('No account selected')
fireEvent.click(getByText('Unlock Account'))
})
it('outputs blockie img', () => {
const account = '0xxxxxxxxxxxxxxx'
const blockies = toDataUrl(account)
const { container } = render(<Account account={account} />)
const { container } = render(
<User.Provider value={{ ...userMockConnected, account }}>
<Account />
</User.Provider>
)
expect(container.querySelector('.blockies')).toBeInTheDocument()
expect(container.querySelector('.blockies')).toHaveAttribute(
'src',
blockies
)
})
it('Account info can be toggled', () => {
const { container, getByText } = render(
<User.Provider
value={{
...userMockConnected,
isBurner: true,
account: '0xxxxxxxxxxxxxxx'
}}
>
<Account />
</User.Provider>
)
expect(container.firstChild).toBeInTheDocument()
fireEvent.click(getByText('Burner Wallet'))
})
})

View File

@ -1,19 +1,89 @@
import React from 'react'
import React, { PureComponent } from 'react'
import Dotdotdot from 'react-dotdotdot'
import { toDataUrl } from 'ethereum-blockies'
import styles from './Account.module.scss'
import WalletSelector from '../organisms/WalletSelector'
import content from '../../data/web3message.json'
import { ReactComponent as Caret } from '../../img/caret.svg'
import { User } from '../../context'
import Button from './Button'
const Account = ({ account }: { account: string }) => {
const blockies = account && toDataUrl(account)
export default class Account extends PureComponent<
{},
{ isAccountInfoOpen: boolean }
> {
public static contextType = User
return account && blockies ? (
<div className={styles.account}>
<img className={styles.blockies} src={blockies} alt="Blockies" />
<Dotdotdot clamp={2}>{account}</Dotdotdot>
</div>
) : (
<em>No account selected</em>
)
public state = {
isAccountInfoOpen: false
}
private toggleAccountInfo() {
this.setState({ isAccountInfoOpen: !this.state.isAccountInfoOpen })
}
public render() {
const { account, isBurner, loginMetamask, isWeb3Capable } = this.context
const { isAccountInfoOpen } = this.state
const seedphrase = localStorage.getItem('seedphrase') as string
const blockies = account && toDataUrl(account)
return (
<div className={styles.account}>
{account ? (
<>
<img
className={styles.blockies}
src={blockies}
alt="Blockies"
/>
<Dotdotdot className={styles.accountId} clamp={2}>
{account}
</Dotdotdot>
</>
) : (
<>
<span className={styles.blockies} />
<em className={styles.noAccount}>
No account selected
</em>
<Button
link
className={styles.unlock}
onClick={() => loginMetamask()}
>
Unlock Account
</Button>
</>
)}
<div className={styles.accountType}>
{isBurner ? (
<button
className={styles.toggle}
onClick={() => this.toggleAccountInfo()}
title="Show More Account Info"
>
<Caret
className={isAccountInfoOpen ? styles.open : ''}
/>{' '}
Burner Wallet
</button>
) : (
'MetaMask'
)}
{isWeb3Capable && <WalletSelector />}
</div>
{isBurner && isAccountInfoOpen && (
<div className={styles.seedphrase}>
<code>{seedphrase}</code>
<p className={styles.seedphraseHelp}>
{content.seedphrase}
</p>
</div>
)}
</div>
)
}
}
export default Account

View File

@ -32,7 +32,11 @@ const Modal = ({
overlayClassName={styles.modalOverlay}
{...props}
>
<button className={styles.close} onClick={toggleModal}>
<button
className={styles.close}
onClick={toggleModal}
data-testid="closeModal"
>
&times;
</button>

View File

@ -1,6 +1,6 @@
import React from 'react'
import cx from 'classnames'
import { User } from '../../../context'
import { User, Market } from '../../../context'
import styles from './Indicator.module.scss'
const Indicator = ({
@ -19,15 +19,19 @@ const Indicator = ({
ref={forwardedRef}
>
<User.Consumer>
{states =>
!states.isWeb3 ? (
<span className={styles.statusIndicator} />
) : !states.isLogged || !states.isOceanNetwork ? (
<span className={styles.statusIndicatorCloseEnough} />
) : states.isLogged ? (
<span className={styles.statusIndicatorActive} />
) : null
}
{user => (
<Market.Consumer>
{market =>
!user.isLogged || !market.networkMatch ? (
<span
className={styles.statusIndicatorCloseEnough}
/>
) : user.isLogged ? (
<span className={styles.statusIndicatorActive} />
) : null
}
</Market.Consumer>
)}
</User.Consumer>
</div>
)

View File

@ -40,9 +40,15 @@ $popoverWidth: 18rem;
border-bottom: 0;
}
/* stylelint-disable */
button {
font-size: $font-size-small;
svg,
&[data-action] {
display: none;
}
}
/* stylelint-enable */
}
.balance {
@ -50,6 +56,10 @@ $popoverWidth: 18rem;
margin-left: $spacer / 2;
white-space: nowrap;
strong {
color: $brand-grey-lighter;
}
&:first-child {
margin-left: 0;
}

View File

@ -2,7 +2,8 @@ import React from 'react'
import { render } from '@testing-library/react'
import Popover from './Popover'
import { userMock, userMockConnected } from '../../../../__mocks__/user-mock'
import { User } from '../../../context'
import { marketMock } from '../../../../__mocks__/market-mock'
import { User, Market } from '../../../context'
describe('Popover', () => {
it('renders without crashing', () => {
@ -25,12 +26,14 @@ describe('Popover', () => {
it('renders correct network', () => {
const { container } = render(
<User.Provider value={{ ...userMockConnected, network: 'Nile' }}>
<Popover forwardedRef={() => null} style={{}} />
<User.Provider value={{ ...userMockConnected, network: 'Pacific' }}>
<Market.Provider value={{ ...marketMock }}>
<Popover forwardedRef={() => null} style={{}} />
</Market.Provider>
</User.Provider>
)
expect(container.firstChild).toBeInTheDocument()
expect(container.firstChild).toHaveTextContent('Connected to Nile')
expect(container.firstChild).toHaveTextContent('Connected to Pacific')
})
it('renders with wrong network', () => {
@ -38,7 +41,6 @@ describe('Popover', () => {
<User.Provider
value={{
...userMockConnected,
isOceanNetwork: false,
network: '1'
}}
>

View File

@ -1,20 +1,16 @@
import React, { PureComponent } from 'react'
import Account from '../../atoms/Account'
import { User } from '../../../context'
import { User, Market } from '../../../context'
import styles from './Popover.module.scss'
export default class Popover extends PureComponent<{
forwardedRef: (ref: HTMLElement | null) => void
style: React.CSSProperties
forwardedRef?: (ref: HTMLElement | null) => void
style?: React.CSSProperties
}> {
public static contextType = User
public render() {
const {
account,
balance,
network,
isWeb3,
isOceanNetwork
} = this.context
const { account, balance, network } = this.context
return (
<div
@ -22,15 +18,10 @@ export default class Popover extends PureComponent<{
ref={this.props.forwardedRef}
style={this.props.style}
>
{!isWeb3 ? (
<div className={styles.popoverInfoline}>
No Web3 detected. Use a browser with MetaMask installed
to publish assets.
</div>
) : (
{
<>
<div className={styles.popoverInfoline}>
<Account account={account} />
<Account />
</div>
{account && balance && (
@ -52,16 +43,28 @@ export default class Popover extends PureComponent<{
</div>
)}
<div className={styles.popoverInfoline}>
{network && !isOceanNetwork
? 'Please connect to Custom RPC\n https://pacific.oceanprotocol.com'
: network && `Connected to ${network} network`}
</div>
<Market.Consumer>
{market => (
<div className={styles.popoverInfoline}>
{network && !market.networkMatch
? `Please connect to Custom RPC
${
market.network === 'Pacific'
? 'https://pacific.oceanprotocol.com'
: market.network === 'Nile'
? 'https://nile.dev-ocean.com'
: market.network === 'Duero'
? 'https://duero.dev-ocean.com'
: 'http://localhost:8545'
}`
: network &&
`Connected to ${network} network`}
</div>
)}
</Market.Consumer>
</>
)}
}
</div>
)
}
}
Popover.contextType = User

View File

@ -10,9 +10,7 @@ describe('AccountStatus', () => {
it('togglePopover fires', () => {
const { container } = render(<AccountStatus />)
const indicator = container.querySelector('.statusIndicator')
const indicator = container.querySelector('.status')
indicator && fireEvent.mouseOver(indicator)
expect(container.querySelector('.popover')).toBeInTheDocument()
indicator && fireEvent.mouseOut(indicator)

View File

@ -10,8 +10,8 @@
.element {
display: inline-block;
margin-left: $spacer / 2;
margin-right: $spacer / 2;
margin-left: $spacer / 1.5;
margin-right: $spacer / 1.5;
text-align: center;
}

View File

@ -26,7 +26,7 @@
}
td {
padding: $spacer / 4 $spacer / 2;
padding: $spacer / 4 $spacer / 2 $spacer / 4 $spacer * 1.3;
vertical-align: top;
&:last-child {

View File

@ -26,6 +26,17 @@ describe('VersionTableContracts', () => {
/submarine.duero.dev-ocean/
)
rerender(
<VersionTableContracts
contracts={{ hello: 'hello', hello2: 'hello2' }}
network="nile"
keeperVersion="6.6.6"
/>
)
expect(container.querySelector('tr:last-child a').href).toMatch(
/submarine.nile.dev-ocean/
)
rerender(
<VersionTableContracts
contracts={{ hello: 'hello', hello2: 'hello2' }}
@ -34,7 +45,7 @@ describe('VersionTableContracts', () => {
/>
)
expect(container.querySelector('tr:last-child a').href).toMatch(
/submarine.pacific.dev-ocean/
/submarine.oceanprotocol/
)
})
})

View File

@ -53,13 +53,11 @@ export const VersionTableContracts = ({
// sort alphabetically
.sort((a, b) => a.localeCompare(b))
.map(key => {
const submarineLink = `https://submarine${
network === 'duero'
? '.duero'
: network === 'pacific'
? '.pacific'
: ''
}.dev-ocean.com/address/${contracts[key]}`
const submarineLink = `https://submarine.${
network === 'pacific'
? 'oceanprotocol'
: `${network}.dev-ocean`
}.com/address/${contracts[key]}`
return (
<tr key={key}>

View File

@ -11,5 +11,13 @@
margin-top: -.1rem;
padding-right: .5rem;
cursor: pointer;
color: $brand-grey-light;
svg {
fill: $brand-grey-light;
transition: .2s ease-out;
}
}
.open {
transform: rotate(90deg);
}

View File

@ -4,6 +4,7 @@ import slugify from '@sindresorhus/slugify'
import styles from './VersionTableRow.module.scss'
import { VersionTableContracts, VersionTableCommons } from './VersionTable'
import VersionNumber from './VersionNumber'
import { ReactComponent as Caret } from '../../../img/caret.svg'
const VersionTableRow = ({ value }: { value: any }) => {
const collapseStyles = {
@ -26,11 +27,7 @@ const VersionTableRow = ({ value }: { value: any }) => {
<td>
{(value.name === 'Commons' || value.contracts) && (
<button className={styles.handle} {...getToggleProps()}>
{isOpen ? (
<span>&#9660;</span>
) : (
<span>&#9658;</span>
)}
<Caret className={isOpen ? styles.open : ''} />
</button>
)}
<a

View File

@ -1,12 +1,9 @@
@import '../../../styles/variables';
.versions {
margin-top: $spacer * 2;
}
.versionsTitle {
font-size: $font-size-large;
margin-bottom: $spacer / 2;
margin-top: $spacer * 2;
}
.versionsMinimal {

View File

@ -9,19 +9,14 @@ import { version } from '../../../../package.json'
import styles from './index.module.scss'
import { nodeUri, faucetUri } from '../../../config'
import { User } from '../../../context'
import { User, Market } from '../../../context'
import VersionTable from './VersionTable'
import VersionStatus from './VersionStatus'
// construct values which are not part of any response
export const commonsVersion =
process.env.NODE_ENV === 'production' ? version : `${version}-dev`
const commonsNetwork = new URL(nodeUri).hostname.split('.')[0]
const faucetNetwork = new URL(faucetUri).hostname.split('.')[1]
interface VersionNumbersProps {
minimal?: boolean
account: string
}
export interface VersionNumbersState extends OceanPlatformVersions {
@ -44,12 +39,20 @@ export default class VersionNumbers extends PureComponent<
> {
public static contextType = User
// construct values which are not part of any response
public commonsVersion =
process.env.NODE_ENV === 'production' ? version : `${version}-dev`
public commonsNetwork = new URL(nodeUri).hostname.split('.')[0]
public faucetNetwork = faucetUri.includes('dev-ocean')
? new URL(faucetUri).hostname.split('.')[1]
: 'Pacific'
// define a minimal default state to fill UI
public state: VersionNumbersState = {
commons: {
name: 'Commons',
network: commonsNetwork,
version: commonsVersion
network: this.commonsNetwork,
version: this.commonsVersion
},
squid: {
name: 'Squid-js',
@ -66,7 +69,7 @@ export default class VersionNumbers extends PureComponent<
faucet: {
name: 'Faucet',
version: '',
network: faucetNetwork,
network: this.faucetNetwork,
status: OceanPlatformTechStatus.Loading
},
status: {
@ -79,11 +82,20 @@ export default class VersionNumbers extends PureComponent<
// for canceling axios requests
public signal = axios.CancelToken.source()
public async componentDidMount() {
public componentDidMount() {
this.getOceanVersions()
this.getFaucetVersion()
}
public async componentDidUpdate(prevProps: any) {
// Workaround: Using account prop instead of getting it from
// context to be able to compare. Cause there is no `prevContext`.
if (prevProps.account !== this.props.account) {
this.getOceanVersions()
this.getFaucetVersion()
}
}
public componentWillUnmount() {
this.signal.cancel()
}
@ -132,14 +144,19 @@ export default class VersionNumbers extends PureComponent<
const { commons, squid, brizo, aquarius } = this.state
return (
<p className={styles.versionsMinimal}>
<a
title={`${squid.name} v${squid.version}\n${brizo.name} v${brizo.version}\n${aquarius.name} v${aquarius.version}`}
href={'/about'}
>
v{commons.version} {squid.network && `(${squid.network})`}
</a>
</p>
<Market.Consumer>
{market => (
<p className={styles.versionsMinimal}>
<a
title={`${squid.name} v${squid.version}\n${brizo.name} v${brizo.version}\n${aquarius.name} v${aquarius.version}`}
href={'/about'}
>
v{commons.version}{' '}
{market.network && `(${market.network})`}
</a>
</p>
)}
</Market.Consumer>
)
}
@ -149,13 +166,13 @@ export default class VersionNumbers extends PureComponent<
return minimal ? (
<this.MinimalOutput />
) : (
<div className={styles.versions} id="#oceanversions">
<h2 className={styles.versionsTitle}>
<>
<h2 className={styles.versionsTitle} id="#oceanversions">
Ocean Components Status
</h2>
<VersionStatus status={this.state.status} />
<VersionTable data={this.state} />
</div>
</>
)
}
}

View File

@ -57,10 +57,9 @@ export default class AssetsUser extends PureComponent<
}
public render() {
const { account, isOceanNetwork } = this.context
const { account } = this.context
return (
isOceanNetwork &&
account && (
<div className={styles.assetsUser}>
{this.props.recent && (

View File

@ -1,5 +1,5 @@
import React from 'react'
import { Market } from '../../context'
import React, { useContext } from 'react'
import { Market, User } from '../../context'
import Content from '../atoms/Content'
import { ReactComponent as AiCommons } from '../../img/aicommons.svg'
import styles from './Footer.module.scss'
@ -7,48 +7,47 @@ import styles from './Footer.module.scss'
import meta from '../../data/meta.json'
import VersionNumbers from '../molecules/VersionNumbers'
const Footer = () => (
<footer className={styles.footer}>
<aside className={styles.stats}>
export default function Footer() {
const market = useContext(Market)
const user = useContext(User)
return (
<footer className={styles.footer}>
<aside className={styles.stats}>
<Content wide>
<p>
Online since March 2019.
{market.totalAssets > 0 &&
` With a total of ${market.totalAssets} registered assets.`}
</p>
<p className={styles.aicommons}>
Proud supporter of{' '}
<a
href="https://aicommons.com/?utm_source=commons.oceanprotocol.com"
title="AI Commons"
>
<AiCommons />
</a>
</p>
<VersionNumbers account={user.account} minimal />
</Content>
</aside>
<Content wide>
<p>
Online since March 2019.
<Market.Consumer>
{state =>
state.totalAssets > 0 &&
` With a total of ${state.totalAssets} registered assets.`
}
</Market.Consumer>
</p>
<p className={styles.aicommons}>
Proud supporter of{' '}
<a
href="https://aicommons.com/?utm_source=commons.oceanprotocol.com"
title="AI Commons"
>
<AiCommons />
</a>
</p>
<VersionNumbers minimal />
<small>
&copy; {new Date().getFullYear()}{' '}
<a href={meta.social[0].url}>{meta.company}</a> &mdash; All
Rights Reserved
</small>
<nav className={styles.links}>
{meta.social.map(site => (
<a key={site.title} href={site.url}>
{site.title}
</a>
))}
</nav>
</Content>
</aside>
<Content wide>
<small>
&copy; {new Date().getFullYear()}{' '}
<a href={meta.social[0].url}>{meta.company}</a> &mdash; All
Rights Reserved
</small>
<nav className={styles.links}>
{meta.social.map(site => (
<a key={site.title} href={site.url}>
{site.title}
</a>
))}
</nav>
</Content>
</footer>
)
export default Footer
</footer>
)
}

View File

@ -1,7 +1,6 @@
import React, { PureComponent } from 'react'
import { NavLink } from 'react-router-dom'
import { ReactComponent as Logo } from '@oceanprotocol/art/logo/logo.svg'
import { User } from '../../context'
import AccountStatus from '../molecules/AccountStatus'
import styles from './Header.module.scss'
@ -40,5 +39,3 @@ export default class Header extends PureComponent {
)
}
}
Header.contextType = User

View File

@ -0,0 +1,73 @@
@import '../../styles/variables';
.openLink {
font-size: $font-size-small !important; // stylelint-disable-line
margin-left: $spacer / 2;
}
.info {
display: flex;
flex-wrap: wrap;
justify-content: space-between;
}
.button {
flex: 0 0 100%;
background: $brand-white;
border: 1px solid $brand-grey-lighter;
border-radius: $border-radius;
line-height: 1.5;
padding: $spacer / 1.5;
font-family: $font-family-base;
display: flex;
flex-wrap: wrap;
align-items: flex-start;
text-align: left;
cursor: pointer;
transition: border .2s ease-out;
margin-bottom: $spacer;
position: relative;
@media (min-width: $break-point--small) {
flex-basis: 48%;
margin-bottom: 0;
}
&:hover,
&:focus {
border-color: $brand-pink;
}
}
.buttonActive {
composes: button;
pointer-events: none;
background: $body-background;
}
.selected {
position: absolute;
right: $spacer / 3;
top: $spacer / 4;
color: $brand-grey-light;
font-weight: $font-weight-bold;
}
.buttonIcon {
font-size: $font-size-h4;
display: inline-block;
margin-right: $spacer / 4;
}
.buttonTitle {
font-size: $font-size-base;
margin-bottom: $spacer / 2;
font-weight: $font-weight-bold;
margin-top: 0;
}
.buttonDescription {
font-size: $font-size-small;
font-weight: $font-weight-bold;
color: $brand-grey-light;
}

View File

@ -0,0 +1,23 @@
import React from 'react'
import { render, fireEvent } from '@testing-library/react'
import ReactModal from 'react-modal'
import WalletSelector from './WalletSelector'
import { User, Market } from '../../context'
import { userMockConnected } from '../../../__mocks__/user-mock'
import { marketMock } from '../../../__mocks__/market-mock'
describe('WalletSelector', () => {
it('renders without crashing', () => {
ReactModal.setAppElement(document.createElement('div'))
const { container } = render(
<User.Provider value={userMockConnected}>
<Market.Provider value={marketMock}>
<WalletSelector />
</Market.Provider>
</User.Provider>
)
expect(container.firstChild).toBeInTheDocument()
fireEvent.click(container.querySelector('button'))
})
})

View File

@ -0,0 +1,108 @@
import React, { PureComponent } from 'react'
import Modal from '../atoms/Modal'
import { User } from '../../context'
import styles from './WalletSelector.module.scss'
import Button from '../atoms/Button'
import content from '../../data/wallets.json'
export default class WalletSelector extends PureComponent<
{},
{ isModalOpen: boolean }
> {
public static contextType = User
public state = {
isModalOpen: false
}
private toggleModal = () => {
this.setState({ isModalOpen: !this.state.isModalOpen })
}
private loginBurnerWallet = () => {
this.context.loginBurnerWallet()
this.toggleModal()
}
private loginMetamask = () => {
this.context.loginMetamask()
this.context.logoutBurnerWallet()
this.toggleModal()
}
private WalletButton = ({
title,
description,
icon
}: {
title: string
description: string
icon: string
}) => {
const active =
(title === 'Burner Wallet' && this.context.isBurner) ||
(title === 'MetaMask' && !this.context.isBurner)
return (
<button
className={active ? styles.buttonActive : styles.button}
onClick={
title === 'MetaMask'
? this.loginMetamask
: this.loginBurnerWallet
}
>
<div>
<h3 className={styles.buttonTitle}>
<span
className={styles.buttonIcon}
role="img"
aria-label={title}
>
{icon}
</span>
{title}
</h3>
<span className={styles.buttonDescription}>
{description}
</span>
{active && (
<span className={styles.selected}>Selected</span>
)}
</div>
</button>
)
}
public render() {
return (
<>
<Button
link
className={styles.openLink}
onClick={this.toggleModal}
data-action="wallet"
>
{content.title}
</Button>
<Modal
title={content.title}
description={content.description}
isOpen={this.state.isModalOpen}
toggleModal={this.toggleModal}
>
<div className={styles.info}>
{content.buttons.map(({ title, description, icon }) => (
<this.WalletButton
key={title}
title={title}
icon={icon}
description={description}
/>
))}
</div>
</Modal>
</>
)
}
}

View File

@ -4,19 +4,27 @@
margin-bottom: $spacer;
color: $brand-grey;
position: relative;
border-bottom: .1rem solid $brand-grey-lighter;
border-top: .1rem solid $brand-grey-lighter;
padding-top: $spacer / 2;
padding-bottom: $spacer / 2;
text-align: left;
font-size: $font-size-small;
}
.warnings {
padding-left: $spacer;
.account {
margin-bottom: $spacer / 2;
background: $brand-white;
border-radius: $border-radius;
border: 1px solid $brand-grey-lighter;
padding: $spacer / 2;
}
.text {
padding-left: $spacer * 1.5;
display: inline-block;
margin-bottom: 0;
}
.status {
margin-left: -($spacer);
margin-right: $spacer / 2;
margin-left: -($spacer / 1.2);
margin-right: $spacer / 2.5;
padding: 0;
}

View File

@ -1,64 +1,60 @@
import React from 'react'
import { render, fireEvent } from '@testing-library/react'
import { render } from '@testing-library/react'
import Web3message from './Web3message'
import { User } from '../../context'
import { User, Market } from '../../context'
import { userMock, userMockConnected } from '../../../__mocks__/user-mock'
import { marketMock } from '../../../__mocks__/market-mock'
describe('Web3message', () => {
it('renders with noWeb3 message', () => {
it('renders with burner wallet message', () => {
const { container } = render(
<User.Provider value={{ ...userMock }}>
<Web3message />
<User.Provider value={{ ...userMockConnected, isBurner: true }}>
<Market.Provider value={{ ...marketMock }}>
<Web3message extended />
</Market.Provider>
</User.Provider>
)
expect(container.firstChild).toHaveTextContent('Not a Web3 Browser')
expect(container.firstChild).toHaveTextContent('Burner Wallet')
})
it('renders with wrongNetwork message', () => {
const { container } = render(
<User.Provider value={{ ...userMock, isWeb3: true }}>
<Web3message />
<User.Provider value={{ ...userMockConnected, network: 'Pacific' }}>
<Market.Provider
value={{
...marketMock,
networkMatch: false,
network: 'Nile'
}}
>
<Web3message extended />
</Market.Provider>
</User.Provider>
)
expect(container.firstChild).toHaveTextContent(
'Not connected to Pacific network'
'Not connected to Nile network'
)
})
it('renders with noAccount message', () => {
const { container } = render(
<User.Provider
value={{ ...userMock, isWeb3: true, isOceanNetwork: true }}
>
<Web3message />
<User.Provider value={userMock}>
<Market.Provider value={marketMock}>
<Web3message extended />
</Market.Provider>
</User.Provider>
)
expect(container.firstChild).toHaveTextContent('No accounts detected')
expect(container.firstChild).toHaveTextContent('No account selected')
})
it('renders with hasAccount message', () => {
const { container } = render(
<User.Provider value={userMockConnected}>
<Web3message />
<Market.Provider value={marketMock}>
<Web3message />
</Market.Provider>
</User.Provider>
)
expect(container.firstChild).toHaveTextContent('0xxxxxx')
})
it('button click fires unlockAccounts', () => {
const { getByText } = render(
<User.Provider
value={{
...userMock,
isWeb3: true,
isOceanNetwork: true
}}
>
<Web3message />
</User.Provider>
)
fireEvent.click(getByText('Unlock Account'))
expect(userMock.unlockAccounts).toBeCalled()
})
})

View File

@ -1,53 +1,66 @@
import React, { PureComponent } from 'react'
import Account from '../atoms/Account'
import Button from '../atoms/Button'
import AccountStatus from '../molecules/AccountStatus'
import styles from './Web3message.module.scss'
import { User } from '../../context'
import { User, Market } from '../../context'
import content from '../../data/web3message.json'
export default class Web3message extends PureComponent {
private message = (
message: string,
account?: string,
unlockAccounts?: () => any
) => (
<div className={styles.message}>
{account ? (
<Account account={account} />
) : (
<div className={styles.warnings}>
<AccountStatus className={styles.status} />
<span dangerouslySetInnerHTML={{ __html: message }} />{' '}
{unlockAccounts && (
<Button onClick={() => unlockAccounts()} link>
Unlock Account
</Button>
)}
</div>
)}
</div>
)
export default class Web3message extends PureComponent<{ extended?: boolean }> {
public static contextType = Market
private messageOceanNetwork = () =>
this.context.network === 'Pacific'
? content.wrongNetworkPacific
: this.context.network === 'Nile'
? content.wrongNetworkNile
: this.context.network === 'Duero'
? content.wrongNetworkDuero
: content.wrongNetworkSpree
private Message = () => {
const { networkMatch, network } = this.context
return (
<User.Consumer>
{user => (
<em
dangerouslySetInnerHTML={{
__html:
!networkMatch && !user.isBurner
? this.messageOceanNetwork()
: !user.isLogged
? content.noAccount
: user.isBurner
? content.hasBurnerWallet
: user.isLogged
? content.hasMetaMaskWallet.replace(
'NETWORK',
network
)
: ''
}}
/>
)}
</User.Consumer>
)
}
public render() {
const {
isWeb3,
isOceanNetwork,
isLogged,
account,
unlockAccounts
} = this.context
const { networkMatch } = this.context
return !isWeb3
? this.message(content.noweb3)
: !isOceanNetwork
? this.message(content.wrongNetwork)
: !isLogged
? this.message(content.noAccount, '', unlockAccounts)
: isLogged
? this.message(content.hasAccount, account)
: null
return (
<div className={styles.message}>
<div className={styles.account}>
<Account />
</div>
{(!networkMatch || this.props.extended) && (
<div className={styles.text}>
<AccountStatus className={styles.status} />
<this.Message />
</div>
)}
</div>
)
}
}
Web3message.contextType = User

View File

@ -5,9 +5,10 @@ import { render, fireEvent } from '@testing-library/react'
import { DDO } from '@oceanprotocol/squid'
import { StateMock } from '@react-mock/state'
import ReactGA from 'react-ga'
import { User } from '../../../context'
import { User, Market } from '../../../context'
import AssetFile, { messages } from './AssetFile'
import { userMockConnected } from '../../../../__mocks__/user-mock'
import { marketMock } from '../../../../__mocks__/market-mock'
const file = {
index: 0,
@ -39,7 +40,9 @@ describe('AssetFile', () => {
it('button to be enabled when connected', async () => {
const { getByText } = render(
<User.Provider value={userMockConnected}>
<AssetFile file={file} ddo={ddo} />
<Market.Provider value={marketMock}>
<AssetFile file={file} ddo={ddo} />
</Market.Provider>
</User.Provider>
)
const button = getByText('Get file')
@ -50,12 +53,12 @@ describe('AssetFile', () => {
it('renders feedback message: initial', async () => {
const { container } = render(
<StateMock state={{ isLoading: true, step: null }}>
<StateMock state={{ isLoading: true, step: 99 }}>
<AssetFile file={file} ddo={ddo} />
</StateMock>
)
expect(container.querySelector('.spinner')).toHaveTextContent(
messages.start
messages[99]
)
})

View File

@ -3,13 +3,13 @@ import { Logger, DDO, File } from '@oceanprotocol/squid'
import filesize from 'filesize'
import Button from '../../atoms/Button'
import Spinner from '../../atoms/Spinner'
import { User } from '../../../context'
import { User, Market } from '../../../context'
import styles from './AssetFile.module.scss'
import ReactGA from 'react-ga'
import cleanupContentType from '../../../utils/cleanupContentType'
export const messages = {
start: 'Decrypting file URL...',
export const messages: any = {
99: 'Decrypting file URL...',
0: '1/3<br />Asking for agreement signature...',
1: '1/3<br />Agreement initialized.',
2: '2/3<br />Asking for two payment confirmations...',
@ -25,24 +25,26 @@ interface AssetFileProps {
interface AssetFileState {
isLoading: boolean
error: string
step: number | string | null
step: number
}
export default class AssetFile extends PureComponent<
AssetFileProps,
AssetFileState
> {
public static contextType = User
public state = {
isLoading: false,
error: '',
step: null
step: 99
}
private resetState = () =>
this.setState({
isLoading: true,
error: '',
step: null
step: 99
})
private purchaseAsset = async (ddo: DDO, index: number) => {
@ -109,7 +111,7 @@ export default class AssetFile extends PureComponent<
public render() {
const { ddo, file } = this.props
const { isLoading, error, step } = this.state
const { isLogged, isOceanNetwork } = this.context
const { isLogged } = this.context
const { index, contentType, contentLength } = file
return (
@ -134,22 +136,24 @@ export default class AssetFile extends PureComponent<
</ul>
{isLoading ? (
<Spinner
message={
step === null ? messages.start : messages[step]
}
/>
<Spinner message={messages[step]} />
) : (
<Button
primary
className={styles.buttonMain}
// weird 0 hack so TypeScript is happy
onClick={() => this.purchaseAsset(ddo, index || 0)}
disabled={!isLogged || !isOceanNetwork}
name="Download"
>
Get file
</Button>
<Market.Consumer>
{market => (
<Button
primary
className={styles.buttonMain}
// weird 0 hack so TypeScript is happy
onClick={() =>
this.purchaseAsset(ddo, index || 0)
}
disabled={!isLogged || !market.networkMatch}
name="Download"
>
Get file
</Button>
)}
</Market.Consumer>
)}
{error !== '' && <div className={styles.error}>{error}</div>}
@ -157,5 +161,3 @@ export default class AssetFile extends PureComponent<
)
}
}
AssetFile.contextType = User

View File

@ -4,8 +4,6 @@ import React from 'react'
import { render } from '@testing-library/react'
import { DDO } from '@oceanprotocol/squid'
import AssetFilesDetails from './AssetFilesDetails'
import { User } from '../../../context'
import { userMockConnected } from '../../../../__mocks__/user-mock'
describe('AssetFilesDetails', () => {
it('renders without crashing', () => {
@ -28,16 +26,4 @@ describe('AssetFilesDetails', () => {
)
expect(container.firstChild).toHaveTextContent('No files attached.')
})
it('hides Web3message when all connected', () => {
const { container } = render(
<User.Provider value={userMockConnected}>
<AssetFilesDetails
files={[{ index: 0, url: '' }]}
ddo={({} as any) as DDO}
/>
</User.Provider>
)
expect(container.querySelector('.status')).not.toBeInTheDocument()
})
})

View File

@ -19,9 +19,7 @@ export default class AssetFilesDetails extends PureComponent<{
<AssetFile key={file.index} ddo={ddo} file={file} />
))}
</div>
{(!this.context.isOceanNetwork || !this.context.isLogged) && (
<Web3message />
)}
<Web3message />
</>
) : (
<div>No files attached.</div>

View File

@ -0,0 +1,42 @@
import React from 'react'
import { render, fireEvent, wait } from '@testing-library/react'
import ReactModal from 'react-modal'
import mockAxios from 'jest-mock-axios'
import Report from './Report'
afterEach(() => {
mockAxios.reset()
})
const mockResponse = {
data: { status: 'success' }
}
describe('Report', () => {
it('renders without crashing', async () => {
ReactModal.setAppElement(document.createElement('div'))
const { getByText, getByLabelText, getByTestId } = render(
<Report did="did:xxx" title="Hello" />
)
// Renders button by default
expect(getByText('Report Data Set')).toBeInTheDocument()
// open modal
fireEvent.click(getByText('Report Data Set'))
await wait(() => expect(getByText('did:xxx')).toBeInTheDocument())
// add comment
const comment = getByLabelText('Comment')
fireEvent.change(comment, {
target: { value: 'Plants' }
})
expect(comment).toHaveTextContent('Plants')
fireEvent.click(getByTestId('report'))
mockAxios.mockResponse(mockResponse)
// expect(mockAxios.post).toHaveBeenCalled()
// close modal
fireEvent.click(getByTestId('closeModal'))
})
})

View File

@ -142,6 +142,7 @@ export default class Report extends PureComponent<
primary
onClick={(e: Event) => this.sendEmail(e)}
disabled={this.state.comment === ''}
data-testid="report"
>
Report Data Set
</Button>

View File

@ -1,22 +1,18 @@
import React from 'react'
import { render } from '@testing-library/react'
import { createMemoryHistory, createLocation } from 'history'
import Details from './index'
const history = createMemoryHistory()
const location = createLocation('/asset/did:xxx')
describe('Details', () => {
it('renders loading state by default', () => {
const { container } = render(
<Details
location={{
search: '',
pathname: '/',
state: '',
hash: ''
}}
match={{
params: {
did: ''
}
}}
history={history}
location={location}
match={{ params: '', path: '', url: '', isExact: true }}
/>
)
expect(container.firstChild).toBeInTheDocument()

View File

@ -8,6 +8,7 @@ import stylesApp from '../../../App.module.scss'
import Content from '../../atoms/Content'
import CategoryImage from '../../atoms/CategoryImage'
import styles from './index.module.scss'
import withTracker from '../../../hoc/withTracker'
interface AssetProps {
match: {
@ -23,7 +24,9 @@ interface AssetState {
error: string
}
export default class Asset extends Component<AssetProps, AssetState> {
class Asset extends Component<AssetProps, AssetState> {
public static contextType = User
public state = {
ddo: ({} as any) as DDO,
metadata: ({ base: { name: '' } } as any) as MetaData,
@ -79,4 +82,4 @@ export default class Asset extends Component<AssetProps, AssetState> {
}
}
Asset.contextType = User
export default withTracker(Asset)

View File

@ -0,0 +1,11 @@
import { BurnerWalletProvider } from './BurnerWalletProvider'
describe('BurnerWalletProvider', () => {
it('Burner wallet can be created', async () => {
const burnerwalletProvider = new BurnerWalletProvider()
await burnerwalletProvider.startLogin()
const web3 = burnerwalletProvider.getProvider()
expect(web3)
})
})

View File

@ -0,0 +1,53 @@
import Web3 from 'web3'
import { nodeUri } from '../config'
import HDWalletProvider from 'truffle-hdwallet-provider'
import { requestFromFaucet } from '../ocean'
const bip39 = require('bip39') // eslint-disable-line @typescript-eslint/no-var-requires
export class BurnerWalletProvider {
private web3: Web3
public constructor() {
// Default
this.web3 = null as any
}
public async isAvailable() {
return true
}
public async isLogged() {
if (localStorage.getItem('seedphrase') !== null) {
return true
}
return false
}
public async startLogin() {
let mnemonic
const isLogged = await this.isLogged()
if (isLogged) {
mnemonic = (await localStorage.getItem('seedphrase')) as string
} else {
mnemonic = bip39.generateMnemonic()
localStorage.setItem('seedphrase', mnemonic)
}
localStorage.setItem('logType', 'BurnerWallet')
const provider = new HDWalletProvider(mnemonic, `${nodeUri}`, 0, 1)
this.web3 = new Web3(provider)
// fill with Ether
await requestFromFaucet(provider.addresses[0])
}
public async logout() {
// localStorage.removeItem('seedphrase')
localStorage.removeItem('logType')
}
public getProvider() {
return this.web3
}
}

View File

@ -0,0 +1,22 @@
import React from 'react'
import { render } from '@testing-library/react'
import MarketProvider from './MarketProvider'
import { User, Market } from '../context'
import { userMockConnected } from '../../__mocks__/user-mock'
describe('MarketProvider', () => {
it('renders without crashing', () => {
const { getByTestId } = render(
<User.Provider value={userMockConnected}>
<MarketProvider ocean={userMockConnected.ocean as any}>
<Market.Consumer>
{market => (
<div data-testid="hello">{market.network}</div>
)}
</Market.Consumer>
</MarketProvider>
</User.Provider>
)
expect(getByTestId('hello')).toBeInTheDocument()
})
})

View File

@ -1,6 +1,6 @@
import React, { PureComponent } from 'react'
import { Logger, Ocean } from '@oceanprotocol/squid'
import { Market } from '.'
import { Market, User } from '.'
import formPublish from '../data/form-publish.json'
const categories =
@ -16,24 +16,34 @@ interface MarketProviderProps {
interface MarketProviderState {
totalAssets: number
categories: string[]
network: string
networkMatch: boolean
}
export default class MarketProvider extends PureComponent<
MarketProviderProps,
MarketProviderState
> {
public static contextType = User
public state = {
totalAssets: 0,
categories
categories,
network: 'Pacific',
networkMatch: false
}
public async componentDidMount() {}
public async componentDidMount() {
await this.checkCorrectUserNetwork()
}
public async componentDidUpdate(prevProps: any) {
// Using ocean prop instead of getting it from context to be able to compare.
// Cause there is no `prevContext`.
if (prevProps.ocean !== this.props.ocean) {
await this.getTotalAssets()
await this.getMarketNetwork()
await this.checkCorrectUserNetwork()
}
}
@ -56,6 +66,27 @@ export default class MarketProvider extends PureComponent<
}
}
private getMarketNetwork = async () => {
try {
const { ocean } = this.props
// Set desired network to whatever Brizo is running in
const brizo = await ocean.brizo.getVersionInfo()
const network =
brizo.network.charAt(0).toUpperCase() + brizo.network.slice(1)
this.setState({ network })
} catch (error) {
Logger.error('Error', error.message)
}
}
private async checkCorrectUserNetwork() {
if (this.context.network === this.state.network) {
this.setState({ networkMatch: true })
} else {
this.setState({ networkMatch: false })
}
}
public render() {
return (
<Market.Provider value={this.state}>

View File

@ -0,0 +1,11 @@
import { MetamaskProvider } from './MetamaskProvider'
describe('MetamaskProvider', () => {
it('MetamaskProvider can be created', async () => {
const metamaskProvider = new MetamaskProvider()
await metamaskProvider.startLogin()
const web3 = metamaskProvider.getProvider()
expect(web3)
})
})

View File

@ -0,0 +1,48 @@
import Web3 from 'web3'
export class MetamaskProvider {
private web3: Web3
public constructor() {
// Default
this.web3 = null as any
// Modern dapp browsers
if (window.ethereum) {
this.web3 = new Web3(window.ethereum)
}
// Legacy dapp browsers
else if (window.web3) {
this.web3 = new Web3(window.web3.currentProvider)
}
}
public async isAvailable() {
return this.web3 !== null
}
public async isLogged() {
if (this.web3 === null) return false
if ((await this.web3.eth.getAccounts()).length > 0) {
return true
}
return false
}
public async startLogin() {
try {
await window.ethereum.enable()
localStorage.setItem('logType', 'Metamask')
} catch (error) {
return false
}
}
public async logout() {
localStorage.removeItem('logType')
// reload page?
}
public getProvider() {
return this.web3
}
}

View File

@ -1,13 +1,16 @@
import React, { PureComponent } from 'react'
import Web3 from 'web3'
import { Logger, Ocean, Account } from '@oceanprotocol/squid'
import { Ocean, Account } from '@oceanprotocol/squid'
import { User } from '.'
import { provideOcean, requestFromFaucet, FaucetResponse } from '../ocean'
import { nodeUri } from '../config'
import MarketProvider from './MarketProvider'
import { MetamaskProvider } from './MetamaskProvider'
import { BurnerWalletProvider } from './BurnerWalletProvider'
const POLL_ACCOUNTS = 1000 // every 1s
const POLL_NETWORK = POLL_ACCOUNTS * 60 // every 1 min
const DEFAULT_WEB3 = new Web3(new Web3.providers.HttpProvider(nodeUri)) // default web3
// taken from
// https://github.com/DefinitelyTyped/DefinitelyTyped/blob/master/types/web3/providers.d.ts
@ -45,9 +48,9 @@ declare global {
interface UserProviderState {
isLogged: boolean
isBurner: boolean
isWeb3Capable: boolean
isLoading: boolean
isWeb3: boolean
isOceanNetwork: boolean
account: string
balance: {
eth: number
@ -57,35 +60,67 @@ interface UserProviderState {
web3: Web3
ocean: Ocean
requestFromFaucet(account: string): Promise<FaucetResponse>
unlockAccounts(): Promise<any>
loginMetamask(): Promise<any>
loginBurnerWallet(): Promise<any>
logoutBurnerWallet(): Promise<any>
message: string
}
export default class UserProvider extends PureComponent<{}, UserProviderState> {
private unlockAccounts = async () => {
try {
await window.ethereum.enable()
} catch (error) {
// User denied account access...
return null
}
private loginMetamask = async () => {
const metamaskProvider = new MetamaskProvider()
await metamaskProvider.startLogin()
const web3 = metamaskProvider.getProvider()
this.setState(
{
isLogged: true,
isBurner: false,
web3
},
() => {
this.loadOcean()
}
)
}
private loginBurnerWallet = async () => {
const burnerwalletProvider = new BurnerWalletProvider()
await burnerwalletProvider.startLogin()
const web3 = burnerwalletProvider.getProvider()
this.setState(
{
isLogged: true,
isBurner: true,
web3
},
() => {
this.loadOcean()
}
)
}
private logoutBurnerWallet = async () => {
const burnerwalletProvider = new BurnerWalletProvider()
await burnerwalletProvider.logout()
}
public state = {
isLogged: false,
isBurner: false,
isWeb3Capable: Boolean(window.web3 || window.ethereum),
isLoading: true,
isWeb3: false,
isOceanNetwork: false,
balance: {
eth: 0,
ocn: 0
},
network: '',
web3: new Web3(new Web3.providers.HttpProvider(nodeUri)),
web3: DEFAULT_WEB3,
account: '',
ocean: {} as any,
requestFromFaucet: () => requestFromFaucet(''),
unlockAccounts: () => this.unlockAccounts(),
loginMetamask: () => this.loginMetamask(),
loginBurnerWallet: () => this.loginBurnerWallet(),
logoutBurnerWallet: () => this.logoutBurnerWallet(),
message: 'Connecting to Ocean...'
}
@ -94,9 +129,6 @@ export default class UserProvider extends PureComponent<{}, UserProviderState> {
public async componentDidMount() {
await this.bootstrap()
this.initAccountsPoll()
this.initNetworkPoll()
}
private initAccountsPoll() {
@ -114,109 +146,70 @@ export default class UserProvider extends PureComponent<{}, UserProviderState> {
}
}
private getWeb3 = () => {
// Modern dapp browsers
if (window.ethereum) {
window.web3 = new Web3(window.ethereum)
return window.web3
}
// Legacy dapp browsers
else if (window.web3) {
window.web3 = new Web3(window.web3.currentProvider)
return window.web3
}
// Non-dapp browsers
else {
return null
}
private loadDefaultWeb3 = async () => {
this.setState(
{
isLogged: false,
isBurner: false,
web3: DEFAULT_WEB3
},
() => {
this.loadOcean()
}
)
}
private loadOcean = async () => {
const { ocean } = await provideOcean(this.state.web3)
this.setState({ ocean, isLoading: false }, () => {
this.initNetworkPoll()
this.initAccountsPoll()
this.fetchNetwork()
this.fetchAccounts()
})
}
private bootstrap = async () => {
try {
//
// Start with Web3 detection only
//
this.setState({ message: 'Setting up Web3...' })
let web3 = await this.getWeb3()
const logType = localStorage.getItem('logType')
const metamaskProvider = new MetamaskProvider()
web3
? this.setState({ isWeb3: true })
: this.setState({ isWeb3: false })
// Modern & legacy dapp browsers
if (web3 && this.state.isWeb3) {
//
// Detecting network with window.web3
//
let isOceanNetwork
await window.web3.eth.net.getId((err, netId) => {
if (err) return
const isPacific = netId === 0xcea11
const isNile = netId === 8995
const isDuero = netId === 2199
const isSpree = netId === 8996
isOceanNetwork = isPacific || isNile || isDuero || isSpree
const network = isPacific
? 'Pacific'
: isNile
? 'Nile'
: isDuero
? 'Duero'
: netId.toString()
if (
isOceanNetwork !== this.state.isOceanNetwork ||
network !== this.state.network
) {
this.setState({ isOceanNetwork, network })
}
})
if (!isOceanNetwork) {
web3 = this.state.web3 // eslint-disable-line
switch (logType) {
case 'Metamask':
if (
(await metamaskProvider.isAvailable()) &&
(await metamaskProvider.isLogged())
) {
const web3 = metamaskProvider.getProvider()
this.setState(
{
isLogged: true,
web3
},
() => {
this.loadOcean()
}
)
} else {
this.loadDefaultWeb3()
}
//
// Provide the Ocean
//
this.setState({ message: 'Connecting to Ocean...' })
const { ocean } = await provideOcean(web3)
this.setState({ ocean, message: 'Getting accounts...' })
// Get accounts
await this.fetchAccounts()
this.setState({ isLoading: false, message: '' })
}
// Non-dapp browsers
else {
this.setState({ message: 'Connecting to Ocean...' })
const { ocean } = await provideOcean(this.state.web3)
this.setState({ ocean, isLoading: false })
this.fetchNetwork()
}
} catch (e) {
// error in bootstrap process
// show error connecting to ocean
Logger.error('web3 error', e.message)
this.setState({ isLoading: false })
break
case 'BurnerWallet':
this.loginBurnerWallet()
break
default:
this.loginBurnerWallet()
break
}
}
private fetchAccounts = async () => {
const { ocean, isWeb3, isLogged, isOceanNetwork } = this.state
const { ocean, isLogged } = this.state
if (isWeb3) {
if (isLogged) {
let accounts
// Modern dapp browsers
if (window.ethereum && !isLogged && isOceanNetwork) {
if (window.ethereum && !isLogged) {
// simply set to empty, and have user click a button somewhere
// to initiate account unlocking
accounts = []
@ -254,19 +247,12 @@ export default class UserProvider extends PureComponent<{}, UserProviderState> {
}
private fetchNetwork = async () => {
const { ocean, isWeb3 } = this.state
if (isWeb3) {
const network = await ocean.keeper.getNetworkName()
const isPacific = network === 'Pacific'
const isNile = network === 'Nile'
const isDuero = network === 'Duero'
const isSpree = network === 'Spree'
const isOceanNetwork = isPacific || isNile || isDuero || isSpree
network !== this.state.network &&
this.setState({ isOceanNetwork, network })
const { ocean } = this.state
let network = 'Unknown'
if (ocean.keeper) {
network = await ocean.keeper.getNetworkName()
}
network !== this.state.network && this.setState({ network })
}
public render() {

View File

@ -2,9 +2,9 @@ import React from 'react'
export const User = React.createContext({
isLogged: false,
isBurner: false,
isWeb3Capable: false,
isLoading: false,
isWeb3: false,
isOceanNetwork: false,
account: '',
web3: {},
ocean: {},
@ -16,10 +16,18 @@ export const User = React.createContext({
requestFromFaucet: () => {
/* empty */
},
unlockAccounts: () => {
loginMetamask: () => {
/* empty */
},
loginBurnerWallet: () => {
/* empty */
},
message: ''
})
export const Market = React.createContext({ totalAssets: 0, categories: [''] })
export const Market = React.createContext({
totalAssets: 0,
categories: [''],
network: '',
networkMatch: false
})

View File

@ -0,0 +1,16 @@
{
"title": "Select wallet",
"description": "Select the wallet you want to use in the Commons Marketplace. By default, we create a burner wallet in your browser allowing you to use all functionality without further setup.",
"buttons": [
{
"title": "Burner Wallet",
"icon": "🔥",
"description": "Easiest way to use Commons without further setup. But the wallet will be gone when you change browsers or clear your cache."
},
{
"title": "MetaMask",
"icon": "🦊",
"description": "Most secure experience attaching everything you do in Commons to an account you control. But you need to setup MetaMask first."
}
]
}

View File

@ -1,6 +1,10 @@
{
"noweb3": "Not a Web3 Browser. For publishing and downloading an asset you need to <a href='https://docs.oceanprotocol.com/tutorials/metamask-setup/' target='_blank' rel='noopener noreferrer'>setup MetaMask</a> or use any other Web3-capable plugin or browser.",
"noAccount": "No accounts detected. For publishing and downloading an asset you need to unlock your Web3 account.",
"hasAccount": "",
"wrongNetwork": "Not connected to Pacific network.<br />Please connect in MetaMask with Custom RPC <code>https://pacific.oceanprotocol.com</code>"
"noAccount": "No wallet selected. For publishing and downloading an asset you need to use one.",
"hasBurnerWallet": "We created a temporary burner wallet for you, allowing you to use all Commons functionality without any setup on your side, and without a Web3-capable browser. This wallet will persist in your browser across sessions, but not across different browsers or devices. <strong>Never use this burner wallet to send or receive any tokens.</strong> To personalize your experience and improve your security, <a href='https://docs.oceanprotocol.com/tutorials/metamask-setup/'>migrate to MetaMask</a>.",
"hasMetaMaskWallet": "Connected with MetaMask to NETWORK. You're a Pro.",
"wrongNetworkPacific": "Not connected to Pacific network. Please connect in MetaMask with Custom RPC <code>https://pacific.oceanprotocol.com</code>",
"wrongNetworkNile": "Not connected to Nile network. Please connect in MetaMask with Custom RPC <code>https://nile.dev-ocean.com</code>",
"wrongNetworkDuero": "Not connected to Duero network. Please connect in MetaMask with Custom RPC <code>https://duero.dev-ocean.com</code>",
"wrongNetworkSpree": "Not connected to Spree network. Please connect in MetaMask with Custom RPC <code>http://localhost:8545</code>",
"seedphrase": "You can use this seed phrase to import this burner wallet account into other wallets, e.g. MetaMask."
}

View File

@ -13,16 +13,14 @@ const withTracker = <P extends RouteComponentProps>(
options: FieldsObject = {}
) => {
const trackPage = (page: string) => {
options.isWeb3 = window.web3 !== undefined
ReactGA.set({ page, ...options })
ReactGA.pageview(page)
}
const HOC = (props: P) => {
useEffect(() => trackPage(props.location.pathname), [
props.location.pathname
])
useEffect(() => {
trackPage(props.location.pathname)
}, [props.location.pathname])
return <WrappedComponent {...props} />
}

3
client/src/img/caret.svg Normal file
View File

@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="9" height="10" viewBox="0 0 9 10">
<polygon points="9 5 0 10 0 0"/>
</svg>

After

Width:  |  Height:  |  Size: 124 B

View File

@ -11,9 +11,9 @@ import {
verbose
} from './config'
export async function provideOcean(web3provider: Web3) {
export async function provideOcean(web3Provider: Web3) {
const config = {
web3provider,
web3Provider,
nodeUri,
aquariusUri,
brizoUri,
@ -21,9 +21,7 @@ export async function provideOcean(web3provider: Web3) {
secretStoreUri,
verbose
}
const ocean: Ocean = await Ocean.getInstance(config)
const ocean: any = await Ocean.getInstance(config)
return { ocean }
}

View File

@ -1,13 +1,21 @@
import React from 'react'
import { render } from '@testing-library/react'
import { MemoryRouter } from 'react-router'
import { createMemoryHistory, createLocation } from 'history'
import About from './About'
const history = createMemoryHistory()
const location = createLocation('/about')
describe('About', () => {
it('renders without crashing', () => {
const { container } = render(
<MemoryRouter>
<About />
<About
history={history}
location={location}
match={{ params: '', path: '', url: '', isExact: true }}
/>
</MemoryRouter>
)
expect(container.firstChild).toBeInTheDocument()

View File

@ -1,47 +1,55 @@
import React, { Component } from 'react'
import React, { useContext } from 'react'
import { Market, User } from '../context'
import Route from '../components/templates/Route'
import Content from '../components/atoms/Content'
import VersionNumbers from '../components/molecules/VersionNumbers'
import Web3message from '../components/organisms/Web3message'
import stylesVersionNumbers from '../components/molecules/VersionNumbers/index.module.scss'
import withTracker from '../hoc/withTracker'
class About extends Component {
public render() {
return (
<Route
title="About"
description="A marketplace to find and publish open data sets in the Ocean Pacific Network."
>
<Content>
<p>
Commons is built on top of the Ocean{' '}
<a href="https://docs.oceanprotocol.com/concepts/pacific-network/">
Pacific network
</a>{' '}
and is targeted at enthusiastic data scientists with
some crypto experience. It can be used with any
Web3-capable browser, like Firefox with MetaMask
installed.
</p>
const About = () => {
const market = useContext(Market)
const user = useContext(User)
<ul>
<li>
<a href="https://blog.oceanprotocol.com/the-commons-marketplace-c57a44288314">
Read the blog post
</a>
</li>
<li>
<a href="https://github.com/oceanprotocol/commons">
Check out oceanprotocol/commons on GitHub
</a>
</li>
</ul>
</Content>
return (
<Route
title="About"
description={`A marketplace to find and publish open data sets in the Ocean ${market.network} Network.`}
>
<Content>
<p>
Commons is built on top of the Ocean{' '}
<a href="https://docs.oceanprotocol.com/concepts/pacific-network/">
{market.network} network
</a>{' '}
and is targeted at enthusiastic data scientists with some
crypto experience. It can be used with any Web3-capable
browser, like Firefox with MetaMask installed.
</p>
<Content>
<VersionNumbers />
</Content>
</Route>
)
}
<ul>
<li>
<a href="https://blog.oceanprotocol.com/the-commons-marketplace-c57a44288314">
Read the blog post
</a>
</li>
<li>
<a href="https://github.com/oceanprotocol/commons">
Check out oceanprotocol/commons on GitHub
</a>
</li>
</ul>
</Content>
<Content>
<h2 className={stylesVersionNumbers.versionsTitle}>
Your Web3 Account Status
</h2>
<Web3message extended />
<VersionNumbers account={user.account} />
</Content>
</Route>
)
}
export default About
export default withTracker(About)

View File

@ -1,16 +1,24 @@
import React from 'react'
import { MemoryRouter } from 'react-router'
import { createMemoryHistory, createLocation } from 'history'
import { render } from '@testing-library/react'
import Channels from './Channels'
import { User } from '../context'
import { userMockConnected } from '../../__mocks__/user-mock'
const history = createMemoryHistory()
const location = createLocation('/channels')
describe('Channels', () => {
it('renders without crashing', () => {
const { container } = render(
<User.Provider value={userMockConnected}>
<MemoryRouter>
<Channels />
<Channels
history={history}
location={location}
match={{ params: '', path: '', url: '', isExact: true }}
/>
</MemoryRouter>
</User.Provider>
)

View File

@ -3,6 +3,7 @@ import Route from '../components/templates/Route'
import Content from '../components/atoms/Content'
import channels from '../data/channels.json'
import ChannelTeaser from '../components/organisms/ChannelTeaser'
import withTracker from '../hoc/withTracker'
class Channels extends Component {
public render() {
@ -21,4 +22,4 @@ class Channels extends Component {
}
}
export default Channels
export default withTracker(Channels)

View File

@ -1,15 +1,23 @@
import React from 'react'
import { render, fireEvent } from '@testing-library/react'
import { MemoryRouter } from 'react-router'
import { createMemoryHistory, createLocation } from 'history'
import Faucet from './Faucet'
import { User } from '../context'
import { userMockConnected } from '../../__mocks__/user-mock'
const history = createMemoryHistory()
const location = createLocation('/faucet')
const setup = () => {
const utils = render(
<User.Provider value={userMockConnected}>
<MemoryRouter>
<Faucet />
<Faucet
history={history}
location={location}
match={{ params: '', path: '', url: '', isExact: true }}
/>
</MemoryRouter>
</User.Provider>
)

View File

@ -3,10 +3,11 @@ import { FaucetResponse } from '../ocean'
import Route from '../components/templates/Route'
import Button from '../components/atoms/Button'
import Spinner from '../components/atoms/Spinner'
import { User } from '../context'
import { User, Market } from '../context'
import Web3message from '../components/organisms/Web3message'
import styles from './Faucet.module.scss'
import Content from '../components/atoms/Content'
import withTracker from '../hoc/withTracker'
interface FaucetState {
isLoading: boolean
@ -15,7 +16,7 @@ interface FaucetState {
trxHash?: string
}
export default class Faucet extends PureComponent<{}, FaucetState> {
class Faucet extends PureComponent<{}, FaucetState> {
public static contextType = User
public state = {
@ -99,9 +100,8 @@ export default class Faucet extends PureComponent<{}, FaucetState> {
<Button
primary
onClick={() => this.getTokens(this.context.requestFromFaucet)}
disabled={
!this.context.isLogged || !this.context.isOceanNetwork
}
disabled={!this.context.isLogged}
name="Faucet"
>
Request Ether
</Button>
@ -112,32 +112,36 @@ export default class Faucet extends PureComponent<{}, FaucetState> {
)
public render() {
const { isWeb3 } = this.context
const { isLogged } = this.context
const { isLoading, error, success } = this.state
return (
<Route
title="Faucet"
description="Shower yourself with some Ether for Ocean's Pacific network."
>
<Content>
<Web3message />
<Market.Consumer>
{market => (
<Route
title="Faucet"
description={`Shower yourself with some Ether for Ocean's ${market.network} network.`}
>
<Content>
<Web3message />
<div className={styles.action}>
{isLoading ? (
<Spinner message="Getting Ether..." />
) : error ? (
<this.Error />
) : success ? (
<this.Success />
) : (
isWeb3 && <this.Action />
)}
</div>
</Content>
</Route>
<div className={styles.action}>
{isLoading ? (
<Spinner message="Getting Ether..." />
) : error ? (
<this.Error />
) : success ? (
<this.Success />
) : (
isLogged && <this.Action />
)}
</div>
</Content>
</Route>
)}
</Market.Consumer>
)
}
}
Faucet.contextType = User
export default withTracker(Faucet)

View File

@ -1,45 +1,23 @@
import React from 'react'
import { render } from '@testing-library/react'
import { MemoryRouter } from 'react-router'
import { User } from '../context'
import { createMemoryHistory, createLocation } from 'history'
import History from './History'
const history = createMemoryHistory()
const location = createLocation('/history')
describe('History', () => {
it('renders without crashing', () => {
const { container } = render(
<MemoryRouter>
<History />
<History
history={history}
location={location}
match={{ params: '', path: '', url: '', isExact: true }}
/>
</MemoryRouter>
)
expect(container.firstChild).toBeInTheDocument()
})
it('outputs Web3 message when no Web3 detected', () => {
const context = {
isLogged: false,
isLoading: false,
isWeb3: false,
isOceanNetwork: false,
account: '',
web3: {},
ocean: {},
balance: { eth: 0, ocn: 0 },
network: '',
requestFromFaucet: () => {},
unlockAccounts: () => {},
message: ''
}
const { container } = render(
<User.Provider value={context}>
<MemoryRouter>
<History />
</MemoryRouter>
</User.Provider>
)
expect(container.querySelector('.message')).toBeInTheDocument()
expect(container.querySelector('.message')).toHaveTextContent(
'Not a Web3 Browser.'
)
})
})

View File

@ -4,15 +4,16 @@ import AssetsUser from '../components/organisms/AssetsUser'
import Web3message from '../components/organisms/Web3message'
import { User } from '../context'
import Content from '../components/atoms/Content'
import withTracker from '../hoc/withTracker'
class History extends Component {
public static contextType = User
export default class History extends Component {
public render() {
return (
<Route title="History">
<Content>
{(!this.context.isLogged ||
!this.context.isOceanNetwork) && <Web3message />}
{!this.context.isLogged && <Web3message />}
<AssetsUser list />
</Content>
</Route>
@ -20,4 +21,4 @@ export default class History extends Component {
}
}
History.contextType = User
export default withTracker(History)

View File

@ -1,22 +0,0 @@
import React from 'react'
import { Router } from 'react-router'
import { createBrowserHistory } from 'history'
import { render } from '@testing-library/react'
import Home from './Home'
import { userMock } from '../../__mocks__/user-mock'
import { User } from '../context'
const history = createBrowserHistory()
describe('Home', () => {
it('renders without crashing', () => {
const { container } = render(
<User.Provider value={{ ...userMock }}>
<Router history={history}>
<Home history={history} />
</Router>
</User.Provider>
)
expect(container.firstChild).toBeInTheDocument()
})
})

View File

@ -1,102 +0,0 @@
import React, { ChangeEvent, Component, FormEvent } from 'react'
import { History } from 'history'
import { User, Market } from '../context'
import CategoryImage from '../components/atoms/CategoryImage'
import CategoryLink from '../components/atoms/CategoryLink'
import Button from '../components/atoms/Button'
import Form from '../components/atoms/Form/Form'
import Input from '../components/atoms/Form/Input'
import Route from '../components/templates/Route'
import styles from './Home.module.scss'
import meta from '../data/meta.json'
import Content from '../components/atoms/Content'
import AssetsLatest from '../components/organisms/AssetsLatest'
import ChannelTeaser from '../components/organisms/ChannelTeaser'
interface HomeProps {
history: History
}
interface HomeState {
search?: string
}
export default class Home extends Component<HomeProps, HomeState> {
public static contextType = User
public state = {
search: ''
}
private inputChange = (event: ChangeEvent<HTMLInputElement>) => {
this.setState({
[event.target.name]: event.target.value
})
}
private searchAssets = (event: FormEvent<HTMLFormElement>) => {
event.preventDefault()
this.props.history.push(`/search?text=${this.state.search}`)
}
public render() {
const { search } = this.state
return (
<Route
title={meta.title}
description={meta.description}
className={styles.home}
>
<Content>
<Form onSubmit={this.searchAssets} minimal>
<Input
type="search"
name="search"
label="Search for data sets"
placeholder="e.g. shapes of plants"
value={search}
onChange={this.inputChange}
group={
<Button primary disabled={!search}>
Search
</Button>
}
/>
</Form>
</Content>
<Content wide>
<h2 className={styles.title}>Featured Channel</h2>
<ChannelTeaser channel="ai-for-good" />
<AssetsLatest />
</Content>
<Content wide>
<h2 className={styles.title}>Explore Categories</h2>
<div className={styles.categories}>
<Market.Consumer>
{({ categories }) =>
categories
.sort((a, b) => a.localeCompare(b)) // sort alphabetically
.map((category: string) => (
<CategoryLink
category={category}
key={category}
className={styles.category}
>
<CategoryImage
category={category}
/>
<h3>{category}</h3>
</CategoryLink>
))
}
</Market.Consumer>
</div>
</Content>
</Route>
)
}
}

View File

@ -0,0 +1,16 @@
import React from 'react'
import { render, fireEvent } from '@testing-library/react'
import Search from './Search'
describe('Search', () => {
it('renders without crashing', () => {
const { container } = render(<Search searchAssets={() => null} />)
expect(container.firstChild).toBeInTheDocument()
// type search query
fireEvent.change(container.querySelector('input'), {
target: { value: 'Plants' }
})
expect(container.querySelector('input').value).toBe('Plants')
// fireEvent.click(getByText('Search'))
})
})

View File

@ -0,0 +1,51 @@
import React, { ChangeEvent, FormEvent, PureComponent } from 'react'
import Button from '../../components/atoms/Button'
import Form from '../../components/atoms/Form/Form'
import Input from '../../components/atoms/Form/Input'
interface SearchProps {
searchAssets: any
}
interface SearchState {
search: string
}
export default class Search extends PureComponent<SearchProps, SearchState> {
public state = {
search: ''
}
private inputChange = (event: ChangeEvent<HTMLInputElement>) => {
this.setState({
search: event.target.value
})
}
public render() {
const { search } = this.state
return (
<Form
onSubmit={(e: FormEvent<HTMLFormElement>) =>
this.props.searchAssets(search, e)
}
minimal
>
<Input
type="search"
name="search"
label="Search for data sets"
placeholder="e.g. shapes of plants"
value={search}
onChange={this.inputChange}
group={
<Button primary disabled={!search}>
Search
</Button>
}
/>
</Form>
)
}
}

View File

@ -1,4 +1,4 @@
@import '../styles/variables';
@import '../../styles/variables';
.home {
display: block;

View File

@ -0,0 +1,27 @@
import React from 'react'
import { Router } from 'react-router'
import { createMemoryHistory, createLocation } from 'history'
import { render } from '@testing-library/react'
import Home from '.'
import { userMock } from '../../../__mocks__/user-mock'
import { User } from '../../context'
const history = createMemoryHistory()
const location = createLocation('/')
describe('Home', () => {
it('renders without crashing', () => {
const { container } = render(
<User.Provider value={{ ...userMock }}>
<Router history={history}>
<Home
history={history}
location={location}
match={{ params: '', path: '', url: '', isExact: true }}
/>
</Router>
</User.Provider>
)
expect(container.firstChild).toBeInTheDocument()
})
})

View File

@ -0,0 +1,74 @@
import React, { PureComponent, FormEvent } from 'react'
import { History } from 'history'
import { Market } from '../../context'
import CategoryImage from '../../components/atoms/CategoryImage'
import CategoryLink from '../../components/atoms/CategoryLink'
import Route from '../../components/templates/Route'
import styles from './index.module.scss'
import meta from '../../data/meta.json'
import Content from '../../components/atoms/Content'
import AssetsLatest from '../../components/organisms/AssetsLatest'
import ChannelTeaser from '../../components/organisms/ChannelTeaser'
import Search from './Search'
import withTracker from '../../hoc/withTracker'
interface HomeProps {
history: History
}
interface HomeState {
search?: string
}
class Home extends PureComponent<HomeProps, HomeState> {
public static contextType = Market
public searchAssets = (
search: string,
event: FormEvent<HTMLFormElement>
) => {
event.preventDefault()
this.props.history.push(`/search?text=${search}`)
}
public render() {
return (
<Route
title={meta.title}
description={meta.description}
className={styles.home}
>
<Content>
<Search searchAssets={this.searchAssets} />
</Content>
<Content wide>
<h2 className={styles.title}>Featured Channel</h2>
<ChannelTeaser channel="ai-for-good" />
<AssetsLatest />
</Content>
<Content wide>
<h2 className={styles.title}>Explore Categories</h2>
<div className={styles.categories}>
{this.context.categories
.sort((a: any, b: any) => a.localeCompare(b)) // sort alphabetically
.map((category: string) => (
<CategoryLink
category={category}
key={category}
className={styles.category}
>
<CategoryImage category={category} />
<h3>{category}</h3>
</CategoryLink>
))}
</div>
</Content>
</Route>
)
}
}
export default withTracker(Home)

View File

@ -1,13 +1,21 @@
import React from 'react'
import { render } from '@testing-library/react'
import { createMemoryHistory, createLocation } from 'history'
import NotFound from './NotFound'
import { MemoryRouter } from 'react-router'
const history = createMemoryHistory()
const location = createLocation('/whatever')
describe('NotFound', () => {
it('renders without crashing', () => {
const { container } = render(
<MemoryRouter>
<NotFound />
<NotFound
history={history}
location={location}
match={{ params: '', path: '', url: '', isExact: true }}
/>
</MemoryRouter>
)
expect(container.firstChild).toBeInTheDocument()

View File

@ -1,6 +1,7 @@
import React, { Component } from 'react'
import Route from '../components/templates/Route'
import Content from '../components/atoms/Content'
import withTracker from '../hoc/withTracker'
class NotFound extends Component {
public render() {
@ -12,4 +13,4 @@ class NotFound extends Component {
}
}
export default NotFound
export default withTracker(NotFound)

View File

@ -24,3 +24,7 @@
margin-left: auto;
}
}
.account {
margin-top: $spacer * $line-height;
}

View File

@ -3,10 +3,11 @@ import Input from '../../components/atoms/Form/Input'
import Label from '../../components/atoms/Form/Label'
import Row from '../../components/atoms/Form/Row'
import Button from '../../components/atoms/Button'
import { User } from '../../context'
import { User, Market } from '../../context'
import Files from './Files/'
import StepRegisterContent from './StepRegisterContent'
import styles from './Step.module.scss'
import Web3message from '../../components/organisms/Web3message'
interface Fields {
label: string
@ -42,6 +43,8 @@ interface StepProps {
}
export default class Step extends PureComponent<StepProps, {}> {
public static contextType = User
public previousButton() {
const { currentStep, prev } = this.props
@ -151,20 +154,26 @@ export default class Step extends PureComponent<StepProps, {}> {
{this.nextButton()}
{lastStep && (
<Button
disabled={
!this.context.isLogged ||
this.props.state.isPublishing
}
primary
>
Register asset
</Button>
<Market.Consumer>
{market => (
<Button
disabled={
!this.context.isLogged ||
!market.networkMatch ||
this.props.state.isPublishing
}
primary
>
Register asset
</Button>
)}
</Market.Consumer>
)}
</div>
<div className={styles.account}>
{!lastStep && <Web3message />}
</div>
</>
)
}
}
Step.contextType = User

View File

@ -1,16 +1,24 @@
import React from 'react'
import { MemoryRouter } from 'react-router'
import { render, fireEvent } from '@testing-library/react'
import { createMemoryHistory, createLocation } from 'history'
import Publish from '.'
import { User } from '../../context'
import { userMockConnected } from '../../../__mocks__/user-mock'
const history = createMemoryHistory()
const location = createLocation('/publish')
describe('Publish', () => {
it('renders without crashing', () => {
const { container, getByText } = render(
<User.Provider value={userMockConnected}>
<MemoryRouter>
<Publish />
<Publish
history={history}
location={location}
match={{ params: '', path: '', url: '', isExact: true }}
/>
</MemoryRouter>
</User.Provider>
)
@ -22,7 +30,16 @@ describe('Publish', () => {
const { getByText, getByLabelText, getByTestId } = render(
<User.Provider value={userMockConnected}>
<MemoryRouter>
<Publish />
<Publish
history={history}
location={{
pathname: '/publish',
search: '',
hash: '',
state: ''
}}
match={{ params: '', path: '', url: '', isExact: true }}
/>
</MemoryRouter>
</User.Provider>
)

View File

@ -3,8 +3,7 @@ import { Logger } from '@oceanprotocol/squid'
import Route from '../../components/templates/Route'
import Form from '../../components/atoms/Form/Form'
import AssetModel from '../../models/AssetModel'
import { User } from '../../context'
import Web3message from '../../components/organisms/Web3message'
import { User, Market } from '../../context'
import Step from './Step'
import Progress from './Progress'
import ReactGA from 'react-ga'
@ -12,6 +11,7 @@ import ReactGA from 'react-ga'
import { steps } from '../../data/form-publish.json'
import Content from '../../components/atoms/Content'
import { File } from './Files'
import withTracker from '../../hoc/withTracker'
type AssetType = 'dataset' | 'algorithm' | 'container' | 'workflow' | 'other'
@ -36,7 +36,7 @@ interface PublishState {
validationStatus?: any
}
export default class Publish extends Component<{}, PublishState> {
class Publish extends Component<{}, PublishState> {
public static contextType = User
public state = {
@ -319,41 +319,44 @@ export default class Publish extends Component<{}, PublishState> {
public render() {
return (
<Route
title="Publish"
description="Publish a new data set into the Ocean Protocol Network."
>
<Content>
{(!this.context.isLogged ||
!this.context.isOceanNetwork) && <Web3message />}
<Progress
steps={steps}
currentStep={this.state.currentStep}
/>
<Form onSubmit={this.registerAsset}>
{steps.map((step: any, index: number) => (
<Step
key={index}
index={index}
title={step.title}
description={step.description}
<Market.Consumer>
{market => (
<Route
title="Publish"
description={`Publish a new data set into the Ocean Protocol ${market.network} Network.`}
>
<Content>
<Progress
steps={steps}
currentStep={this.state.currentStep}
fields={step.fields}
inputChange={this.inputChange}
state={this.state}
next={this.next}
prev={this.prev}
totalSteps={steps.length}
tryAgain={this.tryAgain}
toStart={this.toStart}
content={step.content}
/>
))}
</Form>
</Content>
</Route>
<Form onSubmit={this.registerAsset}>
{steps.map((step: any, index: number) => (
<Step
key={index}
index={index}
title={step.title}
description={step.description}
currentStep={this.state.currentStep}
fields={step.fields}
inputChange={this.inputChange}
state={this.state}
next={this.next}
prev={this.prev}
totalSteps={steps.length}
tryAgain={this.tryAgain}
toStart={this.toStart}
content={step.content}
/>
))}
</Form>
</Content>
</Route>
)}
</Market.Consumer>
)
}
}
export default withTracker(Publish)

View File

@ -6,10 +6,10 @@ import { createMemoryHistory } from 'history'
import { BrowserRouter as Router } from 'react-router-dom'
import { userMockConnected } from '../../__mocks__/user-mock'
const history = createMemoryHistory()
describe('Search', () => {
it('renders without crashing', () => {
const history = createMemoryHistory()
const { container } = render(
<User.Provider value={userMockConnected}>
<Router>
@ -21,6 +21,7 @@ describe('Search', () => {
hash: ''
}}
history={history}
match={{ params: '', path: '', url: '', isExact: true }}
/>
</Router>
</User.Provider>

View File

@ -10,6 +10,7 @@ import AssetTeaser from '../components/molecules/AssetTeaser'
import Pagination from '../components/molecules/Pagination'
import styles from './Search.module.scss'
import Content from '../components/atoms/Content'
import withTracker from '../hoc/withTracker'
interface SearchProps {
location: Location
@ -27,7 +28,9 @@ interface SearchState {
searchCategories: string
}
export default class Search extends PureComponent<SearchProps, SearchState> {
class Search extends PureComponent<SearchProps, SearchState> {
public static contextType = User
public state = {
results: [],
totalResults: 0,
@ -159,4 +162,4 @@ export default class Search extends PureComponent<SearchProps, SearchState> {
}
}
Search.contextType = User
export default withTracker(Search)

View File

@ -2,12 +2,20 @@ import React from 'react'
import { render } from '@testing-library/react'
import Styleguide from './Styleguide'
import { MemoryRouter } from 'react-router'
import { createMemoryHistory, createLocation } from 'history'
const history = createMemoryHistory()
const location = createLocation('/styleguide')
describe('Styleguide', () => {
it('renders without crashing', () => {
const { container } = render(
<MemoryRouter>
<Styleguide />
<Styleguide
history={history}
location={location}
match={{ params: '', path: '', url: '', isExact: true }}
/>
</MemoryRouter>
)
expect(container.firstChild).toBeInTheDocument()

View File

@ -6,6 +6,7 @@ import Route from '../components/templates/Route'
import styles from './Styleguide.module.scss'
import form from '../data/form-styleguide.json'
import Content from '../components/atoms/Content'
import withTracker from '../hoc/withTracker'
class Styleguide extends Component {
public formFields = (entries: any[]) =>
@ -47,4 +48,4 @@ class Styleguide extends Component {
}
}
export default Styleguide
export default withTracker(Styleguide)

View File

@ -140,6 +140,11 @@ em,
font-style: italic;
}
small {
font-size: $font-size-small;
display: inline-block;
}
abbr[title],
dfn {
text-transform: none;

View File

@ -1,8 +1,6 @@
{
"baseUrl": "http://localhost:3000",
"env": {
"NODE_URI": "https://pacific.oceanprotocol.com",
"SEEDPHRASE": "taxi music thumb unique chat sand crew more leg another off lamp",
"CONSUME_ASSET": "did:op:3fa4721ad643489bad77e0e52ecefe9b73166873faaf4661a02486b735eea0c8"
}
}

View File

@ -3,18 +3,27 @@ context('Faucet', () => {
before(() => {
cy.visit('/faucet')
// Wait for end of loading
cy.get('button', { timeout: 60000 }).should('have.length', 1)
cy.get('button[name="Faucet"]', { timeout: 60000 }).should(
'have.length',
1
)
})
beforeEach(() => {
cy.get('button[name="Faucet"]')
.first()
.as('button')
})
it('Faucet button is clickable when user is connected.', () => {
cy.get('button')
cy.get('@button')
.contains('Request Ether')
.should('not.be.disabled')
})
it('Execute faucet call', () => {
// Execute call
cy.get('button')
cy.get('@button')
.contains('Request Ether')
.click()
// Verify that we got response from server

View File

@ -1,14 +1,3 @@
/// <reference types="Cypress" />
import Web3 from 'web3'
import HDWalletProvider from 'truffle-hdwallet-provider'
before(function() {
cy.on('window:before:load', win => {
const provider = new HDWalletProvider(
Cypress.env('SEEDPHRASE'),
Cypress.env('NODE_URI')
)
win.web3 = new Web3(provider)
win.ethereum = win.web3
})
})
before(function() {})

2169
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -24,17 +24,14 @@
"cypress:run": "cypress run --browser chrome",
"cypress:open": "cypress open"
},
"dependencies": {
"truffle-hdwallet-provider": "^1.0.10",
"web3": "1.0.0-beta.37"
},
"dependencies": {},
"devDependencies": {
"@release-it/bumper": "^1.0.3",
"@typescript-eslint/eslint-plugin": "^1.11.0",
"@typescript-eslint/parser": "^1.11.0",
"auto-changelog": "^1.13.0",
"concurrently": "^4.1.1",
"cypress": "^3.3.1",
"cypress": "^3.4.0",
"eslint": "^5.16.0",
"eslint-config-oceanprotocol": "^1.3.0",
"eslint-config-prettier": "^6.0.0",

View File

@ -4096,9 +4096,9 @@
"dev": true
},
"mixin-deep": {
"version": "1.3.1",
"resolved": "https://registry.npmjs.org/mixin-deep/-/mixin-deep-1.3.1.tgz",
"integrity": "sha512-8ZItLHeEgaqEvd5lYBXfm4EZSFCX29Jb9K+lAHhDKzReKBQKj3R+7NOF6tjqYi9t4oI8VUfaWITJQm86wnXGNQ==",
"version": "1.3.2",
"resolved": "https://registry.npmjs.org/mixin-deep/-/mixin-deep-1.3.2.tgz",
"integrity": "sha512-WRoDn//mXBiJ1H40rqa3vH0toePwSsGb45iInWlTySa+Uu4k3tYUSxa2v1KqAiLtvlrSzaExqS1gtk96A9zvEA==",
"dev": true,
"requires": {
"for-in": "^1.0.2",
@ -5107,9 +5107,9 @@
"dev": true
},
"set-value": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/set-value/-/set-value-2.0.0.tgz",
"integrity": "sha512-hw0yxk9GT/Hr5yJEYnHNKYXkIA8mVJgd9ditYZCe16ZczcaELYYcfvaXesNACk2O8O0nTiPQcQhGUQj8JLzeeg==",
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/set-value/-/set-value-2.0.1.tgz",
"integrity": "sha512-JxHc1weCN68wRY0fhCoXpyK55m/XPHafOmK4UWD7m2CI14GMcFypt4w/0+NV5f/ZMby2F6S2wwA7fgynh9gWSw==",
"dev": true,
"requires": {
"extend-shallow": "^2.0.1",
@ -5856,38 +5856,15 @@
}
},
"union-value": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/union-value/-/union-value-1.0.0.tgz",
"integrity": "sha1-XHHDTLW61dzr4+oM0IIHulqhrqQ=",
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/union-value/-/union-value-1.0.1.tgz",
"integrity": "sha512-tJfXmxMeWYnczCVs7XAEvIV7ieppALdyepWMkHkwciRpZraG/xwT+s2JN8+pr1+8jCRf80FFzvr+MpQeeoF4Xg==",
"dev": true,
"requires": {
"arr-union": "^3.1.0",
"get-value": "^2.0.6",
"is-extendable": "^0.1.1",
"set-value": "^0.4.3"
},
"dependencies": {
"extend-shallow": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz",
"integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=",
"dev": true,
"requires": {
"is-extendable": "^0.1.0"
}
},
"set-value": {
"version": "0.4.3",
"resolved": "https://registry.npmjs.org/set-value/-/set-value-0.4.3.tgz",
"integrity": "sha1-fbCPnT0i3H945Trzw79GZuzfzPE=",
"dev": true,
"requires": {
"extend-shallow": "^2.0.1",
"is-extendable": "^0.1.1",
"is-plain-object": "^2.0.1",
"to-object-path": "^0.3.0"
}
}
"set-value": "^2.0.1"
}
},
"unique-string": {