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. 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 1. select Custom RPC in the network dropdown in MetaMask
2. under New Network, enter `https://pacific.oceanprotocol.com` as the custom RPC URL 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 = { const userMock = {
isLogged: false, isLogged: false,
isLoading: false, isLoading: false,
isWeb3: false, isBurner: false,
isOceanNetwork: false, isWeb3Capable: false,
account: '', account: '',
web3: {}, web3: {},
...oceanMock, ...oceanMock,
balance: { eth: 0, ocn: 0 }, balance: { eth: 0, ocn: 0 },
network: '', network: '',
requestFromFaucet: jest.fn(), requestFromFaucet: jest.fn(),
unlockAccounts: jest.fn(), loginMetamask: jest.fn(),
loginBurnerWallet: jest.fn(),
message: '' message: ''
} }
const userMockConnected = { const userMockConnected = {
isLogged: true, isLogged: true,
isLoading: false, isLoading: false,
isWeb3: true, isBurner: false,
isOceanNetwork: true, isWeb3Capable: true,
account: '0xxxxxx', account: '0xxxxxx',
web3: {}, web3: {},
...oceanMock, ...oceanMock,
balance: { eth: 0, ocn: 0 }, balance: { eth: 0, ocn: 0 },
network: '', network: '',
requestFromFaucet: jest.fn(), requestFromFaucet: jest.fn(),
unlockAccounts: jest.fn(), loginMetamask: jest.fn(),
loginBurnerWallet: jest.fn(),
message: '' message: ''
} }

1431
client/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

View File

@ -2,18 +2,82 @@
.account { .account {
display: flex; display: flex;
flex-wrap: wrap;
align-items: center; align-items: center;
text-align: left; text-align: left;
> div { > div:first-of-type {
flex: 0 0 80%;
}
}
.accountId {
white-space: nowrap; white-space: nowrap;
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
font-family: $font-family-monospace; font-family: $font-family-monospace;
font-size: $font-size-small; 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 { .blockies {
width: 1.5rem; width: 1.5rem;
height: 1.5rem; height: 1.5rem;
@ -21,4 +85,5 @@
display: inline-block; display: inline-block;
margin-right: $spacer / 3; margin-right: $spacer / 3;
margin-left: 0; margin-left: 0;
border: 1px solid $brand-grey-lighter;
} }

View File

@ -1,28 +1,61 @@
import React from 'react' import React from 'react'
import { render } from '@testing-library/react' import { render, fireEvent } from '@testing-library/react'
import { toDataUrl } from 'ethereum-blockies' import { toDataUrl } from 'ethereum-blockies'
import Account from './Account' import Account from './Account'
import { User } from '../../context'
import { userMockConnected } from '../../../__mocks__/user-mock'
describe('Account', () => { describe('Account', () => {
it('renders without crashing', () => { 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() expect(container.firstChild).toBeInTheDocument()
}) })
it('outputs empty state without account', () => { 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') expect(container.firstChild).toHaveTextContent('No account selected')
fireEvent.click(getByText('Unlock Account'))
}) })
it('outputs blockie img', () => { it('outputs blockie img', () => {
const account = '0xxxxxxxxxxxxxxx' const account = '0xxxxxxxxxxxxxxx'
const blockies = toDataUrl(account) 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')).toBeInTheDocument()
expect(container.querySelector('.blockies')).toHaveAttribute( expect(container.querySelector('.blockies')).toHaveAttribute(
'src', 'src',
blockies 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 Dotdotdot from 'react-dotdotdot'
import { toDataUrl } from 'ethereum-blockies' import { toDataUrl } from 'ethereum-blockies'
import styles from './Account.module.scss' 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 }) => { export default class Account extends PureComponent<
{},
{ isAccountInfoOpen: boolean }
> {
public static contextType = User
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) const blockies = account && toDataUrl(account)
return account && blockies ? ( return (
<div className={styles.account}> <div className={styles.account}>
<img className={styles.blockies} src={blockies} alt="Blockies" /> {account ? (
<Dotdotdot clamp={2}>{account}</Dotdotdot> <>
</div> <img
className={styles.blockies}
src={blockies}
alt="Blockies"
/>
<Dotdotdot className={styles.accountId} clamp={2}>
{account}
</Dotdotdot>
</>
) : ( ) : (
<em>No account selected</em> <>
) <span className={styles.blockies} />
} <em className={styles.noAccount}>
No account selected
</em>
<Button
link
className={styles.unlock}
onClick={() => loginMetamask()}
>
Unlock Account
</Button>
</>
)}
export default Account <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>
)
}
}

View File

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

View File

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

View File

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

View File

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

View File

@ -1,20 +1,16 @@
import React, { PureComponent } from 'react' import React, { PureComponent } from 'react'
import Account from '../../atoms/Account' import Account from '../../atoms/Account'
import { User } from '../../../context' import { User, Market } from '../../../context'
import styles from './Popover.module.scss' import styles from './Popover.module.scss'
export default class Popover extends PureComponent<{ export default class Popover extends PureComponent<{
forwardedRef: (ref: HTMLElement | null) => void forwardedRef?: (ref: HTMLElement | null) => void
style: React.CSSProperties style?: React.CSSProperties
}> { }> {
public static contextType = User
public render() { public render() {
const { const { account, balance, network } = this.context
account,
balance,
network,
isWeb3,
isOceanNetwork
} = this.context
return ( return (
<div <div
@ -22,15 +18,10 @@ export default class Popover extends PureComponent<{
ref={this.props.forwardedRef} ref={this.props.forwardedRef}
style={this.props.style} 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}> <div className={styles.popoverInfoline}>
<Account account={account} /> <Account />
</div> </div>
{account && balance && ( {account && balance && (
@ -52,16 +43,28 @@ export default class Popover extends PureComponent<{
</div> </div>
)} )}
<Market.Consumer>
{market => (
<div className={styles.popoverInfoline}> <div className={styles.popoverInfoline}>
{network && !isOceanNetwork {network && !market.networkMatch
? 'Please connect to Custom RPC\n https://pacific.oceanprotocol.com' ? `Please connect to Custom RPC
: network && `Connected to ${network} network`} ${
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> </div>
</>
)} )}
</Market.Consumer>
</>
}
</div> </div>
) )
} }
} }
Popover.contextType = User

View File

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

View File

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

View File

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

View File

@ -26,6 +26,17 @@ describe('VersionTableContracts', () => {
/submarine.duero.dev-ocean/ /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( rerender(
<VersionTableContracts <VersionTableContracts
contracts={{ hello: 'hello', hello2: 'hello2' }} contracts={{ hello: 'hello', hello2: 'hello2' }}
@ -34,7 +45,7 @@ describe('VersionTableContracts', () => {
/> />
) )
expect(container.querySelector('tr:last-child a').href).toMatch( 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 alphabetically
.sort((a, b) => a.localeCompare(b)) .sort((a, b) => a.localeCompare(b))
.map(key => { .map(key => {
const submarineLink = `https://submarine${ const submarineLink = `https://submarine.${
network === 'duero' network === 'pacific'
? '.duero' ? 'oceanprotocol'
: network === 'pacific' : `${network}.dev-ocean`
? '.pacific' }.com/address/${contracts[key]}`
: ''
}.dev-ocean.com/address/${contracts[key]}`
return ( return (
<tr key={key}> <tr key={key}>

View File

@ -11,5 +11,13 @@
margin-top: -.1rem; margin-top: -.1rem;
padding-right: .5rem; padding-right: .5rem;
cursor: pointer; 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 styles from './VersionTableRow.module.scss'
import { VersionTableContracts, VersionTableCommons } from './VersionTable' import { VersionTableContracts, VersionTableCommons } from './VersionTable'
import VersionNumber from './VersionNumber' import VersionNumber from './VersionNumber'
import { ReactComponent as Caret } from '../../../img/caret.svg'
const VersionTableRow = ({ value }: { value: any }) => { const VersionTableRow = ({ value }: { value: any }) => {
const collapseStyles = { const collapseStyles = {
@ -26,11 +27,7 @@ const VersionTableRow = ({ value }: { value: any }) => {
<td> <td>
{(value.name === 'Commons' || value.contracts) && ( {(value.name === 'Commons' || value.contracts) && (
<button className={styles.handle} {...getToggleProps()}> <button className={styles.handle} {...getToggleProps()}>
{isOpen ? ( <Caret className={isOpen ? styles.open : ''} />
<span>&#9660;</span>
) : (
<span>&#9658;</span>
)}
</button> </button>
)} )}
<a <a

View File

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

View File

@ -9,19 +9,14 @@ import { version } from '../../../../package.json'
import styles from './index.module.scss' import styles from './index.module.scss'
import { nodeUri, faucetUri } from '../../../config' import { nodeUri, faucetUri } from '../../../config'
import { User } from '../../../context' import { User, Market } from '../../../context'
import VersionTable from './VersionTable' import VersionTable from './VersionTable'
import VersionStatus from './VersionStatus' 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 { interface VersionNumbersProps {
minimal?: boolean minimal?: boolean
account: string
} }
export interface VersionNumbersState extends OceanPlatformVersions { export interface VersionNumbersState extends OceanPlatformVersions {
@ -44,12 +39,20 @@ export default class VersionNumbers extends PureComponent<
> { > {
public static contextType = User 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 // define a minimal default state to fill UI
public state: VersionNumbersState = { public state: VersionNumbersState = {
commons: { commons: {
name: 'Commons', name: 'Commons',
network: commonsNetwork, network: this.commonsNetwork,
version: commonsVersion version: this.commonsVersion
}, },
squid: { squid: {
name: 'Squid-js', name: 'Squid-js',
@ -66,7 +69,7 @@ export default class VersionNumbers extends PureComponent<
faucet: { faucet: {
name: 'Faucet', name: 'Faucet',
version: '', version: '',
network: faucetNetwork, network: this.faucetNetwork,
status: OceanPlatformTechStatus.Loading status: OceanPlatformTechStatus.Loading
}, },
status: { status: {
@ -79,11 +82,20 @@ export default class VersionNumbers extends PureComponent<
// for canceling axios requests // for canceling axios requests
public signal = axios.CancelToken.source() public signal = axios.CancelToken.source()
public async componentDidMount() { public componentDidMount() {
this.getOceanVersions() this.getOceanVersions()
this.getFaucetVersion() 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() { public componentWillUnmount() {
this.signal.cancel() this.signal.cancel()
} }
@ -132,14 +144,19 @@ export default class VersionNumbers extends PureComponent<
const { commons, squid, brizo, aquarius } = this.state const { commons, squid, brizo, aquarius } = this.state
return ( return (
<Market.Consumer>
{market => (
<p className={styles.versionsMinimal}> <p className={styles.versionsMinimal}>
<a <a
title={`${squid.name} v${squid.version}\n${brizo.name} v${brizo.version}\n${aquarius.name} v${aquarius.version}`} title={`${squid.name} v${squid.version}\n${brizo.name} v${brizo.version}\n${aquarius.name} v${aquarius.version}`}
href={'/about'} href={'/about'}
> >
v{commons.version} {squid.network && `(${squid.network})`} v{commons.version}{' '}
{market.network && `(${market.network})`}
</a> </a>
</p> </p>
)}
</Market.Consumer>
) )
} }
@ -149,13 +166,13 @@ export default class VersionNumbers extends PureComponent<
return minimal ? ( return minimal ? (
<this.MinimalOutput /> <this.MinimalOutput />
) : ( ) : (
<div className={styles.versions} id="#oceanversions"> <>
<h2 className={styles.versionsTitle}> <h2 className={styles.versionsTitle} id="#oceanversions">
Ocean Components Status Ocean Components Status
</h2> </h2>
<VersionStatus status={this.state.status} /> <VersionStatus status={this.state.status} />
<VersionTable data={this.state} /> <VersionTable data={this.state} />
</div> </>
) )
} }
} }

View File

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

View File

@ -1,5 +1,5 @@
import React from 'react' import React, { useContext } from 'react'
import { Market } from '../../context' import { Market, User } from '../../context'
import Content from '../atoms/Content' import Content from '../atoms/Content'
import { ReactComponent as AiCommons } from '../../img/aicommons.svg' import { ReactComponent as AiCommons } from '../../img/aicommons.svg'
import styles from './Footer.module.scss' import styles from './Footer.module.scss'
@ -7,18 +7,18 @@ import styles from './Footer.module.scss'
import meta from '../../data/meta.json' import meta from '../../data/meta.json'
import VersionNumbers from '../molecules/VersionNumbers' import VersionNumbers from '../molecules/VersionNumbers'
const Footer = () => ( export default function Footer() {
const market = useContext(Market)
const user = useContext(User)
return (
<footer className={styles.footer}> <footer className={styles.footer}>
<aside className={styles.stats}> <aside className={styles.stats}>
<Content wide> <Content wide>
<p> <p>
Online since March 2019. Online since March 2019.
<Market.Consumer> {market.totalAssets > 0 &&
{state => ` With a total of ${market.totalAssets} registered assets.`}
state.totalAssets > 0 &&
` With a total of ${state.totalAssets} registered assets.`
}
</Market.Consumer>
</p> </p>
<p className={styles.aicommons}> <p className={styles.aicommons}>
Proud supporter of{' '} Proud supporter of{' '}
@ -29,7 +29,7 @@ const Footer = () => (
<AiCommons /> <AiCommons />
</a> </a>
</p> </p>
<VersionNumbers minimal /> <VersionNumbers account={user.account} minimal />
</Content> </Content>
</aside> </aside>
@ -49,6 +49,5 @@ const Footer = () => (
</nav> </nav>
</Content> </Content>
</footer> </footer>
) )
}
export default Footer

View File

@ -1,7 +1,6 @@
import React, { PureComponent } from 'react' import React, { PureComponent } from 'react'
import { NavLink } from 'react-router-dom' import { NavLink } from 'react-router-dom'
import { ReactComponent as Logo } from '@oceanprotocol/art/logo/logo.svg' import { ReactComponent as Logo } from '@oceanprotocol/art/logo/logo.svg'
import { User } from '../../context'
import AccountStatus from '../molecules/AccountStatus' import AccountStatus from '../molecules/AccountStatus'
import styles from './Header.module.scss' 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; margin-bottom: $spacer;
color: $brand-grey; color: $brand-grey;
position: relative; position: relative;
border-bottom: .1rem solid $brand-grey-lighter;
border-top: .1rem solid $brand-grey-lighter;
padding-top: $spacer / 2; padding-top: $spacer / 2;
padding-bottom: $spacer / 2;
text-align: left; text-align: left;
font-size: $font-size-small;
} }
.warnings { .account {
padding-left: $spacer; 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 { .status {
margin-left: -($spacer); margin-left: -($spacer / 1.2);
margin-right: $spacer / 2; margin-right: $spacer / 2.5;
padding: 0; padding: 0;
} }

View File

@ -1,64 +1,60 @@
import React from 'react' import React from 'react'
import { render, fireEvent } from '@testing-library/react' import { render } from '@testing-library/react'
import Web3message from './Web3message' import Web3message from './Web3message'
import { User } from '../../context' import { User, Market } from '../../context'
import { userMock, userMockConnected } from '../../../__mocks__/user-mock' import { userMock, userMockConnected } from '../../../__mocks__/user-mock'
import { marketMock } from '../../../__mocks__/market-mock'
describe('Web3message', () => { describe('Web3message', () => {
it('renders with noWeb3 message', () => { it('renders with burner wallet message', () => {
const { container } = render( const { container } = render(
<User.Provider value={{ ...userMock }}> <User.Provider value={{ ...userMockConnected, isBurner: true }}>
<Web3message /> <Market.Provider value={{ ...marketMock }}>
<Web3message extended />
</Market.Provider>
</User.Provider> </User.Provider>
) )
expect(container.firstChild).toHaveTextContent('Not a Web3 Browser') expect(container.firstChild).toHaveTextContent('Burner Wallet')
}) })
it('renders with wrongNetwork message', () => { it('renders with wrongNetwork message', () => {
const { container } = render( const { container } = render(
<User.Provider value={{ ...userMock, isWeb3: true }}> <User.Provider value={{ ...userMockConnected, network: 'Pacific' }}>
<Web3message /> <Market.Provider
value={{
...marketMock,
networkMatch: false,
network: 'Nile'
}}
>
<Web3message extended />
</Market.Provider>
</User.Provider> </User.Provider>
) )
expect(container.firstChild).toHaveTextContent( expect(container.firstChild).toHaveTextContent(
'Not connected to Pacific network' 'Not connected to Nile network'
) )
}) })
it('renders with noAccount message', () => { it('renders with noAccount message', () => {
const { container } = render( const { container } = render(
<User.Provider <User.Provider value={userMock}>
value={{ ...userMock, isWeb3: true, isOceanNetwork: true }} <Market.Provider value={marketMock}>
> <Web3message extended />
<Web3message /> </Market.Provider>
</User.Provider> </User.Provider>
) )
expect(container.firstChild).toHaveTextContent('No accounts detected') expect(container.firstChild).toHaveTextContent('No account selected')
}) })
it('renders with hasAccount message', () => { it('renders with hasAccount message', () => {
const { container } = render( const { container } = render(
<User.Provider value={userMockConnected}> <User.Provider value={userMockConnected}>
<Market.Provider value={marketMock}>
<Web3message /> <Web3message />
</Market.Provider>
</User.Provider> </User.Provider>
) )
expect(container.firstChild).toHaveTextContent('0xxxxxx') 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 React, { PureComponent } from 'react'
import Account from '../atoms/Account' import Account from '../atoms/Account'
import Button from '../atoms/Button'
import AccountStatus from '../molecules/AccountStatus' import AccountStatus from '../molecules/AccountStatus'
import styles from './Web3message.module.scss' import styles from './Web3message.module.scss'
import { User } from '../../context' import { User, Market } from '../../context'
import content from '../../data/web3message.json' import content from '../../data/web3message.json'
export default class Web3message extends PureComponent { export default class Web3message extends PureComponent<{ extended?: boolean }> {
private message = ( public static contextType = Market
message: string,
account?: string, private messageOceanNetwork = () =>
unlockAccounts?: () => any this.context.network === 'Pacific'
) => ( ? content.wrongNetworkPacific
<div className={styles.message}> : this.context.network === 'Nile'
{account ? ( ? content.wrongNetworkNile
<Account account={account} /> : this.context.network === 'Duero'
) : ( ? content.wrongNetworkDuero
<div className={styles.warnings}> : content.wrongNetworkSpree
<AccountStatus className={styles.status} />
<span dangerouslySetInnerHTML={{ __html: message }} />{' '} private Message = () => {
{unlockAccounts && ( const { networkMatch, network } = this.context
<Button onClick={() => unlockAccounts()} link>
Unlock Account return (
</Button> <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 { networkMatch } = this.context
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>
)} )}
</div> </div>
) )
public render() {
const {
isWeb3,
isOceanNetwork,
isLogged,
account,
unlockAccounts
} = 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
} }
} }
Web3message.contextType = User

View File

@ -5,9 +5,10 @@ import { render, fireEvent } from '@testing-library/react'
import { DDO } from '@oceanprotocol/squid' import { DDO } from '@oceanprotocol/squid'
import { StateMock } from '@react-mock/state' import { StateMock } from '@react-mock/state'
import ReactGA from 'react-ga' import ReactGA from 'react-ga'
import { User } from '../../../context' import { User, Market } from '../../../context'
import AssetFile, { messages } from './AssetFile' import AssetFile, { messages } from './AssetFile'
import { userMockConnected } from '../../../../__mocks__/user-mock' import { userMockConnected } from '../../../../__mocks__/user-mock'
import { marketMock } from '../../../../__mocks__/market-mock'
const file = { const file = {
index: 0, index: 0,
@ -39,7 +40,9 @@ describe('AssetFile', () => {
it('button to be enabled when connected', async () => { it('button to be enabled when connected', async () => {
const { getByText } = render( const { getByText } = render(
<User.Provider value={userMockConnected}> <User.Provider value={userMockConnected}>
<Market.Provider value={marketMock}>
<AssetFile file={file} ddo={ddo} /> <AssetFile file={file} ddo={ddo} />
</Market.Provider>
</User.Provider> </User.Provider>
) )
const button = getByText('Get file') const button = getByText('Get file')
@ -50,12 +53,12 @@ describe('AssetFile', () => {
it('renders feedback message: initial', async () => { it('renders feedback message: initial', async () => {
const { container } = render( const { container } = render(
<StateMock state={{ isLoading: true, step: null }}> <StateMock state={{ isLoading: true, step: 99 }}>
<AssetFile file={file} ddo={ddo} /> <AssetFile file={file} ddo={ddo} />
</StateMock> </StateMock>
) )
expect(container.querySelector('.spinner')).toHaveTextContent( 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 filesize from 'filesize'
import Button from '../../atoms/Button' import Button from '../../atoms/Button'
import Spinner from '../../atoms/Spinner' import Spinner from '../../atoms/Spinner'
import { User } from '../../../context' import { User, Market } from '../../../context'
import styles from './AssetFile.module.scss' import styles from './AssetFile.module.scss'
import ReactGA from 'react-ga' import ReactGA from 'react-ga'
import cleanupContentType from '../../../utils/cleanupContentType' import cleanupContentType from '../../../utils/cleanupContentType'
export const messages = { export const messages: any = {
start: 'Decrypting file URL...', 99: 'Decrypting file URL...',
0: '1/3<br />Asking for agreement signature...', 0: '1/3<br />Asking for agreement signature...',
1: '1/3<br />Agreement initialized.', 1: '1/3<br />Agreement initialized.',
2: '2/3<br />Asking for two payment confirmations...', 2: '2/3<br />Asking for two payment confirmations...',
@ -25,24 +25,26 @@ interface AssetFileProps {
interface AssetFileState { interface AssetFileState {
isLoading: boolean isLoading: boolean
error: string error: string
step: number | string | null step: number
} }
export default class AssetFile extends PureComponent< export default class AssetFile extends PureComponent<
AssetFileProps, AssetFileProps,
AssetFileState AssetFileState
> { > {
public static contextType = User
public state = { public state = {
isLoading: false, isLoading: false,
error: '', error: '',
step: null step: 99
} }
private resetState = () => private resetState = () =>
this.setState({ this.setState({
isLoading: true, isLoading: true,
error: '', error: '',
step: null step: 99
}) })
private purchaseAsset = async (ddo: DDO, index: number) => { private purchaseAsset = async (ddo: DDO, index: number) => {
@ -109,7 +111,7 @@ export default class AssetFile extends PureComponent<
public render() { public render() {
const { ddo, file } = this.props const { ddo, file } = this.props
const { isLoading, error, step } = this.state const { isLoading, error, step } = this.state
const { isLogged, isOceanNetwork } = this.context const { isLogged } = this.context
const { index, contentType, contentLength } = file const { index, contentType, contentLength } = file
return ( return (
@ -134,28 +136,28 @@ export default class AssetFile extends PureComponent<
</ul> </ul>
{isLoading ? ( {isLoading ? (
<Spinner <Spinner message={messages[step]} />
message={
step === null ? messages.start : messages[step]
}
/>
) : ( ) : (
<Market.Consumer>
{market => (
<Button <Button
primary primary
className={styles.buttonMain} className={styles.buttonMain}
// weird 0 hack so TypeScript is happy // weird 0 hack so TypeScript is happy
onClick={() => this.purchaseAsset(ddo, index || 0)} onClick={() =>
disabled={!isLogged || !isOceanNetwork} this.purchaseAsset(ddo, index || 0)
}
disabled={!isLogged || !market.networkMatch}
name="Download" name="Download"
> >
Get file Get file
</Button> </Button>
)} )}
</Market.Consumer>
)}
{error !== '' && <div className={styles.error}>{error}</div>} {error !== '' && <div className={styles.error}>{error}</div>}
</div> </div>
) )
} }
} }
AssetFile.contextType = User

View File

@ -4,8 +4,6 @@ import React from 'react'
import { render } from '@testing-library/react' import { render } from '@testing-library/react'
import { DDO } from '@oceanprotocol/squid' import { DDO } from '@oceanprotocol/squid'
import AssetFilesDetails from './AssetFilesDetails' import AssetFilesDetails from './AssetFilesDetails'
import { User } from '../../../context'
import { userMockConnected } from '../../../../__mocks__/user-mock'
describe('AssetFilesDetails', () => { describe('AssetFilesDetails', () => {
it('renders without crashing', () => { it('renders without crashing', () => {
@ -28,16 +26,4 @@ describe('AssetFilesDetails', () => {
) )
expect(container.firstChild).toHaveTextContent('No files attached.') 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} /> <AssetFile key={file.index} ddo={ddo} file={file} />
))} ))}
</div> </div>
{(!this.context.isOceanNetwork || !this.context.isLogged) && (
<Web3message /> <Web3message />
)}
</> </>
) : ( ) : (
<div>No files attached.</div> <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 primary
onClick={(e: Event) => this.sendEmail(e)} onClick={(e: Event) => this.sendEmail(e)}
disabled={this.state.comment === ''} disabled={this.state.comment === ''}
data-testid="report"
> >
Report Data Set Report Data Set
</Button> </Button>

View File

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

View File

@ -8,6 +8,7 @@ import stylesApp from '../../../App.module.scss'
import Content from '../../atoms/Content' import Content from '../../atoms/Content'
import CategoryImage from '../../atoms/CategoryImage' import CategoryImage from '../../atoms/CategoryImage'
import styles from './index.module.scss' import styles from './index.module.scss'
import withTracker from '../../../hoc/withTracker'
interface AssetProps { interface AssetProps {
match: { match: {
@ -23,7 +24,9 @@ interface AssetState {
error: string error: string
} }
export default class Asset extends Component<AssetProps, AssetState> { class Asset extends Component<AssetProps, AssetState> {
public static contextType = User
public state = { public state = {
ddo: ({} as any) as DDO, ddo: ({} as any) as DDO,
metadata: ({ base: { name: '' } } as any) as MetaData, 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 React, { PureComponent } from 'react'
import { Logger, Ocean } from '@oceanprotocol/squid' import { Logger, Ocean } from '@oceanprotocol/squid'
import { Market } from '.' import { Market, User } from '.'
import formPublish from '../data/form-publish.json' import formPublish from '../data/form-publish.json'
const categories = const categories =
@ -16,24 +16,34 @@ interface MarketProviderProps {
interface MarketProviderState { interface MarketProviderState {
totalAssets: number totalAssets: number
categories: string[] categories: string[]
network: string
networkMatch: boolean
} }
export default class MarketProvider extends PureComponent< export default class MarketProvider extends PureComponent<
MarketProviderProps, MarketProviderProps,
MarketProviderState MarketProviderState
> { > {
public static contextType = User
public state = { public state = {
totalAssets: 0, totalAssets: 0,
categories categories,
network: 'Pacific',
networkMatch: false
} }
public async componentDidMount() {} public async componentDidMount() {
await this.checkCorrectUserNetwork()
}
public async componentDidUpdate(prevProps: any) { public async componentDidUpdate(prevProps: any) {
// Using ocean prop instead of getting it from context to be able to compare. // Using ocean prop instead of getting it from context to be able to compare.
// Cause there is no `prevContext`. // Cause there is no `prevContext`.
if (prevProps.ocean !== this.props.ocean) { if (prevProps.ocean !== this.props.ocean) {
await this.getTotalAssets() 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() { public render() {
return ( return (
<Market.Provider value={this.state}> <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 React, { PureComponent } from 'react'
import Web3 from 'web3' import Web3 from 'web3'
import { Logger, Ocean, Account } from '@oceanprotocol/squid' import { Ocean, Account } from '@oceanprotocol/squid'
import { User } from '.' import { User } from '.'
import { provideOcean, requestFromFaucet, FaucetResponse } from '../ocean' import { provideOcean, requestFromFaucet, FaucetResponse } from '../ocean'
import { nodeUri } from '../config' import { nodeUri } from '../config'
import MarketProvider from './MarketProvider' import MarketProvider from './MarketProvider'
import { MetamaskProvider } from './MetamaskProvider'
import { BurnerWalletProvider } from './BurnerWalletProvider'
const POLL_ACCOUNTS = 1000 // every 1s const POLL_ACCOUNTS = 1000 // every 1s
const POLL_NETWORK = POLL_ACCOUNTS * 60 // every 1 min const POLL_NETWORK = POLL_ACCOUNTS * 60 // every 1 min
const DEFAULT_WEB3 = new Web3(new Web3.providers.HttpProvider(nodeUri)) // default web3
// taken from // taken from
// https://github.com/DefinitelyTyped/DefinitelyTyped/blob/master/types/web3/providers.d.ts // https://github.com/DefinitelyTyped/DefinitelyTyped/blob/master/types/web3/providers.d.ts
@ -45,9 +48,9 @@ declare global {
interface UserProviderState { interface UserProviderState {
isLogged: boolean isLogged: boolean
isBurner: boolean
isWeb3Capable: boolean
isLoading: boolean isLoading: boolean
isWeb3: boolean
isOceanNetwork: boolean
account: string account: string
balance: { balance: {
eth: number eth: number
@ -57,35 +60,67 @@ interface UserProviderState {
web3: Web3 web3: Web3
ocean: Ocean ocean: Ocean
requestFromFaucet(account: string): Promise<FaucetResponse> requestFromFaucet(account: string): Promise<FaucetResponse>
unlockAccounts(): Promise<any> loginMetamask(): Promise<any>
loginBurnerWallet(): Promise<any>
logoutBurnerWallet(): Promise<any>
message: string message: string
} }
export default class UserProvider extends PureComponent<{}, UserProviderState> { export default class UserProvider extends PureComponent<{}, UserProviderState> {
private unlockAccounts = async () => { private loginMetamask = async () => {
try { const metamaskProvider = new MetamaskProvider()
await window.ethereum.enable() await metamaskProvider.startLogin()
} catch (error) { const web3 = metamaskProvider.getProvider()
// User denied account access... this.setState(
return null {
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 = { public state = {
isLogged: false, isLogged: false,
isBurner: false,
isWeb3Capable: Boolean(window.web3 || window.ethereum),
isLoading: true, isLoading: true,
isWeb3: false,
isOceanNetwork: false,
balance: { balance: {
eth: 0, eth: 0,
ocn: 0 ocn: 0
}, },
network: '', network: '',
web3: new Web3(new Web3.providers.HttpProvider(nodeUri)), web3: DEFAULT_WEB3,
account: '', account: '',
ocean: {} as any, ocean: {} as any,
requestFromFaucet: () => requestFromFaucet(''), requestFromFaucet: () => requestFromFaucet(''),
unlockAccounts: () => this.unlockAccounts(), loginMetamask: () => this.loginMetamask(),
loginBurnerWallet: () => this.loginBurnerWallet(),
logoutBurnerWallet: () => this.logoutBurnerWallet(),
message: 'Connecting to Ocean...' message: 'Connecting to Ocean...'
} }
@ -94,9 +129,6 @@ export default class UserProvider extends PureComponent<{}, UserProviderState> {
public async componentDidMount() { public async componentDidMount() {
await this.bootstrap() await this.bootstrap()
this.initAccountsPoll()
this.initNetworkPoll()
} }
private initAccountsPoll() { private initAccountsPoll() {
@ -114,109 +146,70 @@ export default class UserProvider extends PureComponent<{}, UserProviderState> {
} }
} }
private getWeb3 = () => { private loadDefaultWeb3 = async () => {
// Modern dapp browsers this.setState(
if (window.ethereum) { {
window.web3 = new Web3(window.ethereum) isLogged: false,
return window.web3 isBurner: false,
web3: DEFAULT_WEB3
},
() => {
this.loadOcean()
} }
// Legacy dapp browsers )
else if (window.web3) {
window.web3 = new Web3(window.web3.currentProvider)
return window.web3
}
// Non-dapp browsers
else {
return null
} }
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 () => { private bootstrap = async () => {
try { const logType = localStorage.getItem('logType')
// const metamaskProvider = new MetamaskProvider()
// Start with Web3 detection only
//
this.setState({ message: 'Setting up Web3...' })
let web3 = await this.getWeb3()
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()
switch (logType) {
case 'Metamask':
if ( if (
isOceanNetwork !== this.state.isOceanNetwork || (await metamaskProvider.isAvailable()) &&
network !== this.state.network (await metamaskProvider.isLogged())
) { ) {
this.setState({ isOceanNetwork, network }) const web3 = metamaskProvider.getProvider()
this.setState(
{
isLogged: true,
web3
},
() => {
this.loadOcean()
} }
}) )
} else {
if (!isOceanNetwork) { this.loadDefaultWeb3()
web3 = this.state.web3 // eslint-disable-line
} }
break
// case 'BurnerWallet':
// Provide the Ocean this.loginBurnerWallet()
// break
this.setState({ message: 'Connecting to Ocean...' }) default:
this.loginBurnerWallet()
const { ocean } = await provideOcean(web3) break
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 })
} }
} }
private fetchAccounts = async () => { private fetchAccounts = async () => {
const { ocean, isWeb3, isLogged, isOceanNetwork } = this.state const { ocean, isLogged } = this.state
if (isWeb3) { if (isLogged) {
let accounts let accounts
// Modern dapp browsers // Modern dapp browsers
if (window.ethereum && !isLogged && isOceanNetwork) { if (window.ethereum && !isLogged) {
// simply set to empty, and have user click a button somewhere // simply set to empty, and have user click a button somewhere
// to initiate account unlocking // to initiate account unlocking
accounts = [] accounts = []
@ -254,19 +247,12 @@ export default class UserProvider extends PureComponent<{}, UserProviderState> {
} }
private fetchNetwork = async () => { private fetchNetwork = async () => {
const { ocean, isWeb3 } = this.state const { ocean } = this.state
let network = 'Unknown'
if (isWeb3) { if (ocean.keeper) {
const network = await ocean.keeper.getNetworkName() 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 })
} }
network !== this.state.network && this.setState({ network })
} }
public render() { public render() {

View File

@ -2,9 +2,9 @@ import React from 'react'
export const User = React.createContext({ export const User = React.createContext({
isLogged: false, isLogged: false,
isBurner: false,
isWeb3Capable: false,
isLoading: false, isLoading: false,
isWeb3: false,
isOceanNetwork: false,
account: '', account: '',
web3: {}, web3: {},
ocean: {}, ocean: {},
@ -16,10 +16,18 @@ export const User = React.createContext({
requestFromFaucet: () => { requestFromFaucet: () => {
/* empty */ /* empty */
}, },
unlockAccounts: () => { loginMetamask: () => {
/* empty */
},
loginBurnerWallet: () => {
/* empty */ /* empty */
}, },
message: '' 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 wallet selected. For publishing and downloading an asset you need to use one.",
"noAccount": "No accounts detected. For publishing and downloading an asset you need to unlock your Web3 account.", "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>.",
"hasAccount": "", "hasMetaMaskWallet": "Connected with MetaMask to NETWORK. You're a Pro.",
"wrongNetwork": "Not connected to Pacific network.<br />Please connect in MetaMask with Custom RPC <code>https://pacific.oceanprotocol.com</code>" "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 = {} options: FieldsObject = {}
) => { ) => {
const trackPage = (page: string) => { const trackPage = (page: string) => {
options.isWeb3 = window.web3 !== undefined
ReactGA.set({ page, ...options }) ReactGA.set({ page, ...options })
ReactGA.pageview(page) ReactGA.pageview(page)
} }
const HOC = (props: P) => { const HOC = (props: P) => {
useEffect(() => trackPage(props.location.pathname), [ useEffect(() => {
props.location.pathname trackPage(props.location.pathname)
]) }, [props.location.pathname])
return <WrappedComponent {...props} /> 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 verbose
} from './config' } from './config'
export async function provideOcean(web3provider: Web3) { export async function provideOcean(web3Provider: Web3) {
const config = { const config = {
web3provider, web3Provider,
nodeUri, nodeUri,
aquariusUri, aquariusUri,
brizoUri, brizoUri,
@ -21,9 +21,7 @@ export async function provideOcean(web3provider: Web3) {
secretStoreUri, secretStoreUri,
verbose verbose
} }
const ocean: any = await Ocean.getInstance(config)
const ocean: Ocean = await Ocean.getInstance(config)
return { ocean } return { ocean }
} }

View File

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

View File

@ -1,25 +1,30 @@
import React, { Component } from 'react' import React, { useContext } from 'react'
import { Market, User } from '../context'
import Route from '../components/templates/Route' import Route from '../components/templates/Route'
import Content from '../components/atoms/Content' import Content from '../components/atoms/Content'
import VersionNumbers from '../components/molecules/VersionNumbers' 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'
const About = () => {
const market = useContext(Market)
const user = useContext(User)
class About extends Component {
public render() {
return ( return (
<Route <Route
title="About" title="About"
description="A marketplace to find and publish open data sets in the Ocean Pacific Network." description={`A marketplace to find and publish open data sets in the Ocean ${market.network} Network.`}
> >
<Content> <Content>
<p> <p>
Commons is built on top of the Ocean{' '} Commons is built on top of the Ocean{' '}
<a href="https://docs.oceanprotocol.com/concepts/pacific-network/"> <a href="https://docs.oceanprotocol.com/concepts/pacific-network/">
Pacific network {market.network} network
</a>{' '} </a>{' '}
and is targeted at enthusiastic data scientists with and is targeted at enthusiastic data scientists with some
some crypto experience. It can be used with any crypto experience. It can be used with any Web3-capable
Web3-capable browser, like Firefox with MetaMask browser, like Firefox with MetaMask installed.
installed.
</p> </p>
<ul> <ul>
@ -37,11 +42,14 @@ class About extends Component {
</Content> </Content>
<Content> <Content>
<VersionNumbers /> <h2 className={stylesVersionNumbers.versionsTitle}>
Your Web3 Account Status
</h2>
<Web3message extended />
<VersionNumbers account={user.account} />
</Content> </Content>
</Route> </Route>
) )
}
} }
export default About export default withTracker(About)

View File

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

View File

@ -3,6 +3,7 @@ import Route from '../components/templates/Route'
import Content from '../components/atoms/Content' import Content from '../components/atoms/Content'
import channels from '../data/channels.json' import channels from '../data/channels.json'
import ChannelTeaser from '../components/organisms/ChannelTeaser' import ChannelTeaser from '../components/organisms/ChannelTeaser'
import withTracker from '../hoc/withTracker'
class Channels extends Component { class Channels extends Component {
public render() { 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 React from 'react'
import { render, fireEvent } from '@testing-library/react' import { render, fireEvent } from '@testing-library/react'
import { MemoryRouter } from 'react-router' import { MemoryRouter } from 'react-router'
import { createMemoryHistory, createLocation } from 'history'
import Faucet from './Faucet' import Faucet from './Faucet'
import { User } from '../context' import { User } from '../context'
import { userMockConnected } from '../../__mocks__/user-mock' import { userMockConnected } from '../../__mocks__/user-mock'
const history = createMemoryHistory()
const location = createLocation('/faucet')
const setup = () => { const setup = () => {
const utils = render( const utils = render(
<User.Provider value={userMockConnected}> <User.Provider value={userMockConnected}>
<MemoryRouter> <MemoryRouter>
<Faucet /> <Faucet
history={history}
location={location}
match={{ params: '', path: '', url: '', isExact: true }}
/>
</MemoryRouter> </MemoryRouter>
</User.Provider> </User.Provider>
) )

View File

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

View File

@ -1,45 +1,23 @@
import React from 'react' import React from 'react'
import { render } from '@testing-library/react' import { render } from '@testing-library/react'
import { MemoryRouter } from 'react-router' import { MemoryRouter } from 'react-router'
import { User } from '../context' import { createMemoryHistory, createLocation } from 'history'
import History from './History' import History from './History'
const history = createMemoryHistory()
const location = createLocation('/history')
describe('History', () => { describe('History', () => {
it('renders without crashing', () => { it('renders without crashing', () => {
const { container } = render( const { container } = render(
<MemoryRouter> <MemoryRouter>
<History /> <History
history={history}
location={location}
match={{ params: '', path: '', url: '', isExact: true }}
/>
</MemoryRouter> </MemoryRouter>
) )
expect(container.firstChild).toBeInTheDocument() 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 Web3message from '../components/organisms/Web3message'
import { User } from '../context' import { User } from '../context'
import Content from '../components/atoms/Content' 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() { public render() {
return ( return (
<Route title="History"> <Route title="History">
<Content> <Content>
{(!this.context.isLogged || {!this.context.isLogged && <Web3message />}
!this.context.isOceanNetwork) && <Web3message />}
<AssetsUser list /> <AssetsUser list />
</Content> </Content>
</Route> </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 { .home {
display: block; 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 React from 'react'
import { render } from '@testing-library/react' import { render } from '@testing-library/react'
import { createMemoryHistory, createLocation } from 'history'
import NotFound from './NotFound' import NotFound from './NotFound'
import { MemoryRouter } from 'react-router' import { MemoryRouter } from 'react-router'
const history = createMemoryHistory()
const location = createLocation('/whatever')
describe('NotFound', () => { describe('NotFound', () => {
it('renders without crashing', () => { it('renders without crashing', () => {
const { container } = render( const { container } = render(
<MemoryRouter> <MemoryRouter>
<NotFound /> <NotFound
history={history}
location={location}
match={{ params: '', path: '', url: '', isExact: true }}
/>
</MemoryRouter> </MemoryRouter>
) )
expect(container.firstChild).toBeInTheDocument() expect(container.firstChild).toBeInTheDocument()

View File

@ -1,6 +1,7 @@
import React, { Component } from 'react' import React, { Component } from 'react'
import Route from '../components/templates/Route' import Route from '../components/templates/Route'
import Content from '../components/atoms/Content' import Content from '../components/atoms/Content'
import withTracker from '../hoc/withTracker'
class NotFound extends Component { class NotFound extends Component {
public render() { 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; 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 Label from '../../components/atoms/Form/Label'
import Row from '../../components/atoms/Form/Row' import Row from '../../components/atoms/Form/Row'
import Button from '../../components/atoms/Button' import Button from '../../components/atoms/Button'
import { User } from '../../context' import { User, Market } from '../../context'
import Files from './Files/' import Files from './Files/'
import StepRegisterContent from './StepRegisterContent' import StepRegisterContent from './StepRegisterContent'
import styles from './Step.module.scss' import styles from './Step.module.scss'
import Web3message from '../../components/organisms/Web3message'
interface Fields { interface Fields {
label: string label: string
@ -42,6 +43,8 @@ interface StepProps {
} }
export default class Step extends PureComponent<StepProps, {}> { export default class Step extends PureComponent<StepProps, {}> {
public static contextType = User
public previousButton() { public previousButton() {
const { currentStep, prev } = this.props const { currentStep, prev } = this.props
@ -151,9 +154,12 @@ export default class Step extends PureComponent<StepProps, {}> {
{this.nextButton()} {this.nextButton()}
{lastStep && ( {lastStep && (
<Market.Consumer>
{market => (
<Button <Button
disabled={ disabled={
!this.context.isLogged || !this.context.isLogged ||
!market.networkMatch ||
this.props.state.isPublishing this.props.state.isPublishing
} }
primary primary
@ -161,10 +167,13 @@ export default class Step extends PureComponent<StepProps, {}> {
Register asset Register asset
</Button> </Button>
)} )}
</Market.Consumer>
)}
</div>
<div className={styles.account}>
{!lastStep && <Web3message />}
</div> </div>
</> </>
) )
} }
} }
Step.contextType = User

View File

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

View File

@ -3,8 +3,7 @@ import { Logger } from '@oceanprotocol/squid'
import Route from '../../components/templates/Route' import Route from '../../components/templates/Route'
import Form from '../../components/atoms/Form/Form' import Form from '../../components/atoms/Form/Form'
import AssetModel from '../../models/AssetModel' import AssetModel from '../../models/AssetModel'
import { User } from '../../context' import { User, Market } from '../../context'
import Web3message from '../../components/organisms/Web3message'
import Step from './Step' import Step from './Step'
import Progress from './Progress' import Progress from './Progress'
import ReactGA from 'react-ga' import ReactGA from 'react-ga'
@ -12,6 +11,7 @@ import ReactGA from 'react-ga'
import { steps } from '../../data/form-publish.json' import { steps } from '../../data/form-publish.json'
import Content from '../../components/atoms/Content' import Content from '../../components/atoms/Content'
import { File } from './Files' import { File } from './Files'
import withTracker from '../../hoc/withTracker'
type AssetType = 'dataset' | 'algorithm' | 'container' | 'workflow' | 'other' type AssetType = 'dataset' | 'algorithm' | 'container' | 'workflow' | 'other'
@ -36,7 +36,7 @@ interface PublishState {
validationStatus?: any validationStatus?: any
} }
export default class Publish extends Component<{}, PublishState> { class Publish extends Component<{}, PublishState> {
public static contextType = User public static contextType = User
public state = { public state = {
@ -319,14 +319,13 @@ export default class Publish extends Component<{}, PublishState> {
public render() { public render() {
return ( return (
<Market.Consumer>
{market => (
<Route <Route
title="Publish" title="Publish"
description="Publish a new data set into the Ocean Protocol Network." description={`Publish a new data set into the Ocean Protocol ${market.network} Network.`}
> >
<Content> <Content>
{(!this.context.isLogged ||
!this.context.isOceanNetwork) && <Web3message />}
<Progress <Progress
steps={steps} steps={steps}
currentStep={this.state.currentStep} currentStep={this.state.currentStep}
@ -354,6 +353,10 @@ export default class Publish extends Component<{}, PublishState> {
</Form> </Form>
</Content> </Content>
</Route> </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 { BrowserRouter as Router } from 'react-router-dom'
import { userMockConnected } from '../../__mocks__/user-mock' import { userMockConnected } from '../../__mocks__/user-mock'
const history = createMemoryHistory()
describe('Search', () => { describe('Search', () => {
it('renders without crashing', () => { it('renders without crashing', () => {
const history = createMemoryHistory()
const { container } = render( const { container } = render(
<User.Provider value={userMockConnected}> <User.Provider value={userMockConnected}>
<Router> <Router>
@ -21,6 +21,7 @@ describe('Search', () => {
hash: '' hash: ''
}} }}
history={history} history={history}
match={{ params: '', path: '', url: '', isExact: true }}
/> />
</Router> </Router>
</User.Provider> </User.Provider>

View File

@ -10,6 +10,7 @@ import AssetTeaser from '../components/molecules/AssetTeaser'
import Pagination from '../components/molecules/Pagination' import Pagination from '../components/molecules/Pagination'
import styles from './Search.module.scss' import styles from './Search.module.scss'
import Content from '../components/atoms/Content' import Content from '../components/atoms/Content'
import withTracker from '../hoc/withTracker'
interface SearchProps { interface SearchProps {
location: Location location: Location
@ -27,7 +28,9 @@ interface SearchState {
searchCategories: string searchCategories: string
} }
export default class Search extends PureComponent<SearchProps, SearchState> { class Search extends PureComponent<SearchProps, SearchState> {
public static contextType = User
public state = { public state = {
results: [], results: [],
totalResults: 0, 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 { render } from '@testing-library/react'
import Styleguide from './Styleguide' import Styleguide from './Styleguide'
import { MemoryRouter } from 'react-router' import { MemoryRouter } from 'react-router'
import { createMemoryHistory, createLocation } from 'history'
const history = createMemoryHistory()
const location = createLocation('/styleguide')
describe('Styleguide', () => { describe('Styleguide', () => {
it('renders without crashing', () => { it('renders without crashing', () => {
const { container } = render( const { container } = render(
<MemoryRouter> <MemoryRouter>
<Styleguide /> <Styleguide
history={history}
location={location}
match={{ params: '', path: '', url: '', isExact: true }}
/>
</MemoryRouter> </MemoryRouter>
) )
expect(container.firstChild).toBeInTheDocument() expect(container.firstChild).toBeInTheDocument()

View File

@ -6,6 +6,7 @@ import Route from '../components/templates/Route'
import styles from './Styleguide.module.scss' import styles from './Styleguide.module.scss'
import form from '../data/form-styleguide.json' import form from '../data/form-styleguide.json'
import Content from '../components/atoms/Content' import Content from '../components/atoms/Content'
import withTracker from '../hoc/withTracker'
class Styleguide extends Component { class Styleguide extends Component {
public formFields = (entries: any[]) => 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; font-style: italic;
} }
small {
font-size: $font-size-small;
display: inline-block;
}
abbr[title], abbr[title],
dfn { dfn {
text-transform: none; text-transform: none;

View File

@ -1,8 +1,6 @@
{ {
"baseUrl": "http://localhost:3000", "baseUrl": "http://localhost:3000",
"env": { "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" "CONSUME_ASSET": "did:op:3fa4721ad643489bad77e0e52ecefe9b73166873faaf4661a02486b735eea0c8"
} }
} }

View File

@ -3,18 +3,27 @@ context('Faucet', () => {
before(() => { before(() => {
cy.visit('/faucet') cy.visit('/faucet')
// Wait for end of loading // 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.', () => { it('Faucet button is clickable when user is connected.', () => {
cy.get('button') cy.get('@button')
.contains('Request Ether') .contains('Request Ether')
.should('not.be.disabled') .should('not.be.disabled')
}) })
it('Execute faucet call', () => { it('Execute faucet call', () => {
// Execute call // Execute call
cy.get('button') cy.get('@button')
.contains('Request Ether') .contains('Request Ether')
.click() .click()
// Verify that we got response from server // Verify that we got response from server

View File

@ -1,14 +1,3 @@
/// <reference types="Cypress" /> /// <reference types="Cypress" />
import Web3 from 'web3'
import HDWalletProvider from 'truffle-hdwallet-provider'
before(function() { 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
})
})

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

View File

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