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:
commit
599e1834cc
@ -60,7 +60,7 @@ To make use of all the functionality, you need to connect to an Ocean network.
|
||||
|
||||
By default, the client will connect to Ocean components running within [Ocean's Pacific network](https://docs.oceanprotocol.com/concepts/pacific-network/) remotely.
|
||||
|
||||
With your MetaMask, connect to the Pacific network. To do this:
|
||||
By default, the client uses a burner wallet connected to the correct network automatically. If you choose to use MetaMask, you need to connect to the Pacific network. To do this:
|
||||
|
||||
1. select Custom RPC in the network dropdown in MetaMask
|
||||
2. under New Network, enter `https://pacific.oceanprotocol.com` as the custom RPC URL
|
||||
|
8
client/__mocks__/market-mock.ts
Normal file
8
client/__mocks__/market-mock.ts
Normal file
@ -0,0 +1,8 @@
|
||||
const marketMock = {
|
||||
totalAssets: 1000,
|
||||
categories: ['category'],
|
||||
network: 'Pacific',
|
||||
networkMatch: true
|
||||
}
|
||||
|
||||
export { marketMock }
|
@ -3,30 +3,32 @@ import oceanMock from './ocean-mock'
|
||||
const userMock = {
|
||||
isLogged: false,
|
||||
isLoading: false,
|
||||
isWeb3: false,
|
||||
isOceanNetwork: false,
|
||||
isBurner: false,
|
||||
isWeb3Capable: false,
|
||||
account: '',
|
||||
web3: {},
|
||||
...oceanMock,
|
||||
balance: { eth: 0, ocn: 0 },
|
||||
network: '',
|
||||
requestFromFaucet: jest.fn(),
|
||||
unlockAccounts: jest.fn(),
|
||||
loginMetamask: jest.fn(),
|
||||
loginBurnerWallet: jest.fn(),
|
||||
message: ''
|
||||
}
|
||||
|
||||
const userMockConnected = {
|
||||
isLogged: true,
|
||||
isLoading: false,
|
||||
isWeb3: true,
|
||||
isOceanNetwork: true,
|
||||
isBurner: false,
|
||||
isWeb3Capable: true,
|
||||
account: '0xxxxxx',
|
||||
web3: {},
|
||||
...oceanMock,
|
||||
balance: { eth: 0, ocn: 0 },
|
||||
network: '',
|
||||
requestFromFaucet: jest.fn(),
|
||||
unlockAccounts: jest.fn(),
|
||||
loginMetamask: jest.fn(),
|
||||
loginBurnerWallet: jest.fn(),
|
||||
message: ''
|
||||
}
|
||||
|
||||
|
1431
client/package-lock.json
generated
1431
client/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -13,10 +13,11 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@oceanprotocol/art": "^2.2.0",
|
||||
"@oceanprotocol/squid": "0.6.2",
|
||||
"@oceanprotocol/squid": "^0.6.4",
|
||||
"@oceanprotocol/typographies": "^0.1.0",
|
||||
"@sindresorhus/slugify": "^0.9.1",
|
||||
"axios": "^0.19.0",
|
||||
"bip39": "^3.0.2",
|
||||
"classnames": "^2.2.6",
|
||||
"ethereum-blockies": "github:MyEtherWallet/blockies",
|
||||
"filesize": "^4.1.2",
|
||||
@ -38,6 +39,7 @@
|
||||
"react-popper": "^1.3.3",
|
||||
"react-router-dom": "^5.0.1",
|
||||
"react-transition-group": "^4.1.1",
|
||||
"truffle-hdwallet-provider": "^1.0.13",
|
||||
"web3": "1.0.0-beta.37"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
1
client/src/@types/truffle-hdwallet-provider'/index.d.ts
vendored
Normal file
1
client/src/@types/truffle-hdwallet-provider'/index.d.ts
vendored
Normal file
@ -0,0 +1 @@
|
||||
declare module 'truffle-hdwallet-provider'
|
@ -1,6 +1,5 @@
|
||||
import React from 'react'
|
||||
import { Route, Switch } from 'react-router-dom'
|
||||
import withTracker from './hoc/withTracker'
|
||||
|
||||
import About from './routes/About'
|
||||
import Home from './routes/Home'
|
||||
@ -17,17 +16,17 @@ import Channel from './components/templates/Channel'
|
||||
|
||||
const Routes = () => (
|
||||
<Switch>
|
||||
<Route component={withTracker(Home)} exact path="/" />
|
||||
<Route component={withTracker(Styleguide)} path="/styleguide" />
|
||||
<Route component={withTracker(About)} path="/about" />
|
||||
<Route component={withTracker(Publish)} path="/publish" />
|
||||
<Route component={withTracker(Search)} path="/search" />
|
||||
<Route component={withTracker(Asset)} path="/asset/:did" />
|
||||
<Route component={withTracker(Faucet)} path="/faucet" />
|
||||
<Route component={withTracker(History)} path="/history" />
|
||||
<Route component={withTracker(Channels)} exact path="/channels" />
|
||||
<Route component={withTracker(Channel)} path="/channels/:channel" />
|
||||
<Route component={withTracker(NotFound)} />
|
||||
<Route component={Home} exact path="/" />
|
||||
<Route component={Styleguide} path="/styleguide" />
|
||||
<Route component={About} path="/about" />
|
||||
<Route component={Publish} path="/publish" />
|
||||
<Route component={Search} path="/search" />
|
||||
<Route component={Asset} path="/asset/:did" />
|
||||
<Route component={Faucet} path="/faucet" />
|
||||
<Route component={History} path="/history" />
|
||||
<Route component={Channels} exact path="/channels" />
|
||||
<Route component={Channel} path="/channels/:channel" />
|
||||
<Route component={NotFound} />
|
||||
</Switch>
|
||||
)
|
||||
|
||||
|
@ -2,18 +2,82 @@
|
||||
|
||||
.account {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
text-align: left;
|
||||
|
||||
> div {
|
||||
> div:first-of-type {
|
||||
flex: 0 0 80%;
|
||||
}
|
||||
}
|
||||
|
||||
.accountId {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
font-family: $font-family-monospace;
|
||||
font-size: $font-size-small;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.unlock {
|
||||
font-size: $font-size-small !important; // stylelint-disable-line
|
||||
margin-left: $spacer / 2;
|
||||
}
|
||||
|
||||
.accountType {
|
||||
width: 100%;
|
||||
margin-left: calc(1.5rem + #{$spacer / 3});
|
||||
font-size: $font-size-small;
|
||||
font-weight: $font-weight-bold;
|
||||
color: $brand-grey-light;
|
||||
}
|
||||
|
||||
.toggle {
|
||||
background: none;
|
||||
font-family: inherit;
|
||||
font-size: inherit;
|
||||
font-weight: inherit;
|
||||
color: inherit;
|
||||
border: none;
|
||||
padding: 0;
|
||||
cursor: pointer;
|
||||
|
||||
svg {
|
||||
display: inline-block;
|
||||
fill: currentColor;
|
||||
margin-right: $spacer / 8;
|
||||
transition: .2s ease-out;
|
||||
}
|
||||
}
|
||||
|
||||
.open {
|
||||
transform: rotate(90deg);
|
||||
}
|
||||
|
||||
.seedphrase {
|
||||
margin-top: $spacer / 2;
|
||||
margin-left: calc(1.5rem + #{$spacer / 4});
|
||||
margin-right: calc(1.5rem + #{$spacer / 4});
|
||||
|
||||
code {
|
||||
display: block;
|
||||
text-align: center;
|
||||
padding: $spacer / 2 $spacer;
|
||||
border-radius: $border-radius;
|
||||
background: $body-background;
|
||||
border: 1px solid $brand-grey-lighter;
|
||||
margin-bottom: $spacer / 4;
|
||||
word-break: normal;
|
||||
}
|
||||
}
|
||||
|
||||
.seedphraseHelp {
|
||||
color: $brand-grey-light;
|
||||
font-size: $font-size-small;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.blockies {
|
||||
width: 1.5rem;
|
||||
height: 1.5rem;
|
||||
@ -21,4 +85,5 @@
|
||||
display: inline-block;
|
||||
margin-right: $spacer / 3;
|
||||
margin-left: 0;
|
||||
border: 1px solid $brand-grey-lighter;
|
||||
}
|
||||
|
@ -1,28 +1,61 @@
|
||||
import React from 'react'
|
||||
import { render } from '@testing-library/react'
|
||||
import { render, fireEvent } from '@testing-library/react'
|
||||
import { toDataUrl } from 'ethereum-blockies'
|
||||
import Account from './Account'
|
||||
import { User } from '../../context'
|
||||
import { userMockConnected } from '../../../__mocks__/user-mock'
|
||||
|
||||
describe('Account', () => {
|
||||
it('renders without crashing', () => {
|
||||
const { container } = render(<Account account={'0xxxxxxxxxxxxxxx'} />)
|
||||
const { container } = render(
|
||||
<User.Provider
|
||||
value={{ ...userMockConnected, account: '0xxxxxxxxxxxxxxx' }}
|
||||
>
|
||||
<Account />
|
||||
</User.Provider>
|
||||
)
|
||||
expect(container.firstChild).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('outputs empty state without account', () => {
|
||||
const { container } = render(<Account account={''} />)
|
||||
const { container, getByText } = render(
|
||||
<User.Provider value={{ ...userMockConnected, account: '' }}>
|
||||
<Account />
|
||||
</User.Provider>
|
||||
)
|
||||
expect(container.firstChild).toHaveTextContent('No account selected')
|
||||
fireEvent.click(getByText('Unlock Account'))
|
||||
})
|
||||
|
||||
it('outputs blockie img', () => {
|
||||
const account = '0xxxxxxxxxxxxxxx'
|
||||
const blockies = toDataUrl(account)
|
||||
|
||||
const { container } = render(<Account account={account} />)
|
||||
const { container } = render(
|
||||
<User.Provider value={{ ...userMockConnected, account }}>
|
||||
<Account />
|
||||
</User.Provider>
|
||||
)
|
||||
expect(container.querySelector('.blockies')).toBeInTheDocument()
|
||||
expect(container.querySelector('.blockies')).toHaveAttribute(
|
||||
'src',
|
||||
blockies
|
||||
)
|
||||
})
|
||||
|
||||
it('Account info can be toggled', () => {
|
||||
const { container, getByText } = render(
|
||||
<User.Provider
|
||||
value={{
|
||||
...userMockConnected,
|
||||
isBurner: true,
|
||||
account: '0xxxxxxxxxxxxxxx'
|
||||
}}
|
||||
>
|
||||
<Account />
|
||||
</User.Provider>
|
||||
)
|
||||
expect(container.firstChild).toBeInTheDocument()
|
||||
fireEvent.click(getByText('Burner Wallet'))
|
||||
})
|
||||
})
|
||||
|
@ -1,19 +1,89 @@
|
||||
import React from 'react'
|
||||
import React, { PureComponent } from 'react'
|
||||
import Dotdotdot from 'react-dotdotdot'
|
||||
import { toDataUrl } from 'ethereum-blockies'
|
||||
import styles from './Account.module.scss'
|
||||
import WalletSelector from '../organisms/WalletSelector'
|
||||
import content from '../../data/web3message.json'
|
||||
import { ReactComponent as Caret } from '../../img/caret.svg'
|
||||
import { User } from '../../context'
|
||||
import Button from './Button'
|
||||
|
||||
const Account = ({ account }: { account: string }) => {
|
||||
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)
|
||||
|
||||
return account && blockies ? (
|
||||
return (
|
||||
<div className={styles.account}>
|
||||
<img className={styles.blockies} src={blockies} alt="Blockies" />
|
||||
<Dotdotdot clamp={2}>{account}</Dotdotdot>
|
||||
</div>
|
||||
{account ? (
|
||||
<>
|
||||
<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>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -32,7 +32,11 @@ const Modal = ({
|
||||
overlayClassName={styles.modalOverlay}
|
||||
{...props}
|
||||
>
|
||||
<button className={styles.close} onClick={toggleModal}>
|
||||
<button
|
||||
className={styles.close}
|
||||
onClick={toggleModal}
|
||||
data-testid="closeModal"
|
||||
>
|
||||
×
|
||||
</button>
|
||||
|
||||
|
@ -1,6 +1,6 @@
|
||||
import React from 'react'
|
||||
import cx from 'classnames'
|
||||
import { User } from '../../../context'
|
||||
import { User, Market } from '../../../context'
|
||||
import styles from './Indicator.module.scss'
|
||||
|
||||
const Indicator = ({
|
||||
@ -19,15 +19,19 @@ const Indicator = ({
|
||||
ref={forwardedRef}
|
||||
>
|
||||
<User.Consumer>
|
||||
{states =>
|
||||
!states.isWeb3 ? (
|
||||
<span className={styles.statusIndicator} />
|
||||
) : !states.isLogged || !states.isOceanNetwork ? (
|
||||
<span className={styles.statusIndicatorCloseEnough} />
|
||||
) : states.isLogged ? (
|
||||
{user => (
|
||||
<Market.Consumer>
|
||||
{market =>
|
||||
!user.isLogged || !market.networkMatch ? (
|
||||
<span
|
||||
className={styles.statusIndicatorCloseEnough}
|
||||
/>
|
||||
) : user.isLogged ? (
|
||||
<span className={styles.statusIndicatorActive} />
|
||||
) : null
|
||||
}
|
||||
</Market.Consumer>
|
||||
)}
|
||||
</User.Consumer>
|
||||
</div>
|
||||
)
|
||||
|
@ -40,9 +40,15 @@ $popoverWidth: 18rem;
|
||||
border-bottom: 0;
|
||||
}
|
||||
|
||||
/* stylelint-disable */
|
||||
button {
|
||||
font-size: $font-size-small;
|
||||
svg,
|
||||
&[data-action] {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
/* stylelint-enable */
|
||||
}
|
||||
|
||||
.balance {
|
||||
@ -50,6 +56,10 @@ $popoverWidth: 18rem;
|
||||
margin-left: $spacer / 2;
|
||||
white-space: nowrap;
|
||||
|
||||
strong {
|
||||
color: $brand-grey-lighter;
|
||||
}
|
||||
|
||||
&:first-child {
|
||||
margin-left: 0;
|
||||
}
|
||||
|
@ -2,7 +2,8 @@ import React from 'react'
|
||||
import { render } from '@testing-library/react'
|
||||
import Popover from './Popover'
|
||||
import { userMock, userMockConnected } from '../../../../__mocks__/user-mock'
|
||||
import { User } from '../../../context'
|
||||
import { marketMock } from '../../../../__mocks__/market-mock'
|
||||
import { User, Market } from '../../../context'
|
||||
|
||||
describe('Popover', () => {
|
||||
it('renders without crashing', () => {
|
||||
@ -25,12 +26,14 @@ describe('Popover', () => {
|
||||
|
||||
it('renders correct network', () => {
|
||||
const { container } = render(
|
||||
<User.Provider value={{ ...userMockConnected, network: 'Nile' }}>
|
||||
<User.Provider value={{ ...userMockConnected, network: 'Pacific' }}>
|
||||
<Market.Provider value={{ ...marketMock }}>
|
||||
<Popover forwardedRef={() => null} style={{}} />
|
||||
</Market.Provider>
|
||||
</User.Provider>
|
||||
)
|
||||
expect(container.firstChild).toBeInTheDocument()
|
||||
expect(container.firstChild).toHaveTextContent('Connected to Nile')
|
||||
expect(container.firstChild).toHaveTextContent('Connected to Pacific')
|
||||
})
|
||||
|
||||
it('renders with wrong network', () => {
|
||||
@ -38,7 +41,6 @@ describe('Popover', () => {
|
||||
<User.Provider
|
||||
value={{
|
||||
...userMockConnected,
|
||||
isOceanNetwork: false,
|
||||
network: '1'
|
||||
}}
|
||||
>
|
||||
|
@ -1,20 +1,16 @@
|
||||
import React, { PureComponent } from 'react'
|
||||
import Account from '../../atoms/Account'
|
||||
import { User } from '../../../context'
|
||||
import { User, Market } from '../../../context'
|
||||
import styles from './Popover.module.scss'
|
||||
|
||||
export default class Popover extends PureComponent<{
|
||||
forwardedRef: (ref: HTMLElement | null) => void
|
||||
style: React.CSSProperties
|
||||
forwardedRef?: (ref: HTMLElement | null) => void
|
||||
style?: React.CSSProperties
|
||||
}> {
|
||||
public static contextType = User
|
||||
|
||||
public render() {
|
||||
const {
|
||||
account,
|
||||
balance,
|
||||
network,
|
||||
isWeb3,
|
||||
isOceanNetwork
|
||||
} = this.context
|
||||
const { account, balance, network } = this.context
|
||||
|
||||
return (
|
||||
<div
|
||||
@ -22,15 +18,10 @@ export default class Popover extends PureComponent<{
|
||||
ref={this.props.forwardedRef}
|
||||
style={this.props.style}
|
||||
>
|
||||
{!isWeb3 ? (
|
||||
<div className={styles.popoverInfoline}>
|
||||
No Web3 detected. Use a browser with MetaMask installed
|
||||
to publish assets.
|
||||
</div>
|
||||
) : (
|
||||
{
|
||||
<>
|
||||
<div className={styles.popoverInfoline}>
|
||||
<Account account={account} />
|
||||
<Account />
|
||||
</div>
|
||||
|
||||
{account && balance && (
|
||||
@ -52,16 +43,28 @@ export default class Popover extends PureComponent<{
|
||||
</div>
|
||||
)}
|
||||
|
||||
<Market.Consumer>
|
||||
{market => (
|
||||
<div className={styles.popoverInfoline}>
|
||||
{network && !isOceanNetwork
|
||||
? 'Please connect to Custom RPC\n https://pacific.oceanprotocol.com'
|
||||
: network && `Connected to ${network} network`}
|
||||
{network && !market.networkMatch
|
||||
? `Please connect to Custom RPC
|
||||
${
|
||||
market.network === 'Pacific'
|
||||
? 'https://pacific.oceanprotocol.com'
|
||||
: market.network === 'Nile'
|
||||
? 'https://nile.dev-ocean.com'
|
||||
: market.network === 'Duero'
|
||||
? 'https://duero.dev-ocean.com'
|
||||
: 'http://localhost:8545'
|
||||
}`
|
||||
: network &&
|
||||
`Connected to ${network} network`}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</Market.Consumer>
|
||||
</>
|
||||
}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Popover.contextType = User
|
||||
|
@ -10,9 +10,7 @@ describe('AccountStatus', () => {
|
||||
|
||||
it('togglePopover fires', () => {
|
||||
const { container } = render(<AccountStatus />)
|
||||
|
||||
const indicator = container.querySelector('.statusIndicator')
|
||||
|
||||
const indicator = container.querySelector('.status')
|
||||
indicator && fireEvent.mouseOver(indicator)
|
||||
expect(container.querySelector('.popover')).toBeInTheDocument()
|
||||
indicator && fireEvent.mouseOut(indicator)
|
||||
|
@ -10,8 +10,8 @@
|
||||
|
||||
.element {
|
||||
display: inline-block;
|
||||
margin-left: $spacer / 2;
|
||||
margin-right: $spacer / 2;
|
||||
margin-left: $spacer / 1.5;
|
||||
margin-right: $spacer / 1.5;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
|
@ -26,7 +26,7 @@
|
||||
}
|
||||
|
||||
td {
|
||||
padding: $spacer / 4 $spacer / 2;
|
||||
padding: $spacer / 4 $spacer / 2 $spacer / 4 $spacer * 1.3;
|
||||
vertical-align: top;
|
||||
|
||||
&:last-child {
|
||||
|
@ -26,6 +26,17 @@ describe('VersionTableContracts', () => {
|
||||
/submarine.duero.dev-ocean/
|
||||
)
|
||||
|
||||
rerender(
|
||||
<VersionTableContracts
|
||||
contracts={{ hello: 'hello', hello2: 'hello2' }}
|
||||
network="nile"
|
||||
keeperVersion="6.6.6"
|
||||
/>
|
||||
)
|
||||
expect(container.querySelector('tr:last-child a').href).toMatch(
|
||||
/submarine.nile.dev-ocean/
|
||||
)
|
||||
|
||||
rerender(
|
||||
<VersionTableContracts
|
||||
contracts={{ hello: 'hello', hello2: 'hello2' }}
|
||||
@ -34,7 +45,7 @@ describe('VersionTableContracts', () => {
|
||||
/>
|
||||
)
|
||||
expect(container.querySelector('tr:last-child a').href).toMatch(
|
||||
/submarine.pacific.dev-ocean/
|
||||
/submarine.oceanprotocol/
|
||||
)
|
||||
})
|
||||
})
|
||||
|
@ -53,13 +53,11 @@ export const VersionTableContracts = ({
|
||||
// sort alphabetically
|
||||
.sort((a, b) => a.localeCompare(b))
|
||||
.map(key => {
|
||||
const submarineLink = `https://submarine${
|
||||
network === 'duero'
|
||||
? '.duero'
|
||||
: network === 'pacific'
|
||||
? '.pacific'
|
||||
: ''
|
||||
}.dev-ocean.com/address/${contracts[key]}`
|
||||
const submarineLink = `https://submarine.${
|
||||
network === 'pacific'
|
||||
? 'oceanprotocol'
|
||||
: `${network}.dev-ocean`
|
||||
}.com/address/${contracts[key]}`
|
||||
|
||||
return (
|
||||
<tr key={key}>
|
||||
|
@ -11,5 +11,13 @@
|
||||
margin-top: -.1rem;
|
||||
padding-right: .5rem;
|
||||
cursor: pointer;
|
||||
color: $brand-grey-light;
|
||||
|
||||
svg {
|
||||
fill: $brand-grey-light;
|
||||
transition: .2s ease-out;
|
||||
}
|
||||
}
|
||||
|
||||
.open {
|
||||
transform: rotate(90deg);
|
||||
}
|
||||
|
@ -4,6 +4,7 @@ import slugify from '@sindresorhus/slugify'
|
||||
import styles from './VersionTableRow.module.scss'
|
||||
import { VersionTableContracts, VersionTableCommons } from './VersionTable'
|
||||
import VersionNumber from './VersionNumber'
|
||||
import { ReactComponent as Caret } from '../../../img/caret.svg'
|
||||
|
||||
const VersionTableRow = ({ value }: { value: any }) => {
|
||||
const collapseStyles = {
|
||||
@ -26,11 +27,7 @@ const VersionTableRow = ({ value }: { value: any }) => {
|
||||
<td>
|
||||
{(value.name === 'Commons' || value.contracts) && (
|
||||
<button className={styles.handle} {...getToggleProps()}>
|
||||
{isOpen ? (
|
||||
<span>▼</span>
|
||||
) : (
|
||||
<span>►</span>
|
||||
)}
|
||||
<Caret className={isOpen ? styles.open : ''} />
|
||||
</button>
|
||||
)}
|
||||
<a
|
||||
|
@ -1,12 +1,9 @@
|
||||
@import '../../../styles/variables';
|
||||
|
||||
.versions {
|
||||
margin-top: $spacer * 2;
|
||||
}
|
||||
|
||||
.versionsTitle {
|
||||
font-size: $font-size-large;
|
||||
margin-bottom: $spacer / 2;
|
||||
margin-top: $spacer * 2;
|
||||
}
|
||||
|
||||
.versionsMinimal {
|
||||
|
@ -9,19 +9,14 @@ import { version } from '../../../../package.json'
|
||||
import styles from './index.module.scss'
|
||||
|
||||
import { nodeUri, faucetUri } from '../../../config'
|
||||
import { User } from '../../../context'
|
||||
import { User, Market } from '../../../context'
|
||||
|
||||
import VersionTable from './VersionTable'
|
||||
import VersionStatus from './VersionStatus'
|
||||
|
||||
// construct values which are not part of any response
|
||||
export const commonsVersion =
|
||||
process.env.NODE_ENV === 'production' ? version : `${version}-dev`
|
||||
const commonsNetwork = new URL(nodeUri).hostname.split('.')[0]
|
||||
const faucetNetwork = new URL(faucetUri).hostname.split('.')[1]
|
||||
|
||||
interface VersionNumbersProps {
|
||||
minimal?: boolean
|
||||
account: string
|
||||
}
|
||||
|
||||
export interface VersionNumbersState extends OceanPlatformVersions {
|
||||
@ -44,12 +39,20 @@ export default class VersionNumbers extends PureComponent<
|
||||
> {
|
||||
public static contextType = User
|
||||
|
||||
// construct values which are not part of any response
|
||||
public commonsVersion =
|
||||
process.env.NODE_ENV === 'production' ? version : `${version}-dev`
|
||||
public commonsNetwork = new URL(nodeUri).hostname.split('.')[0]
|
||||
public faucetNetwork = faucetUri.includes('dev-ocean')
|
||||
? new URL(faucetUri).hostname.split('.')[1]
|
||||
: 'Pacific'
|
||||
|
||||
// define a minimal default state to fill UI
|
||||
public state: VersionNumbersState = {
|
||||
commons: {
|
||||
name: 'Commons',
|
||||
network: commonsNetwork,
|
||||
version: commonsVersion
|
||||
network: this.commonsNetwork,
|
||||
version: this.commonsVersion
|
||||
},
|
||||
squid: {
|
||||
name: 'Squid-js',
|
||||
@ -66,7 +69,7 @@ export default class VersionNumbers extends PureComponent<
|
||||
faucet: {
|
||||
name: 'Faucet',
|
||||
version: '',
|
||||
network: faucetNetwork,
|
||||
network: this.faucetNetwork,
|
||||
status: OceanPlatformTechStatus.Loading
|
||||
},
|
||||
status: {
|
||||
@ -79,11 +82,20 @@ export default class VersionNumbers extends PureComponent<
|
||||
// for canceling axios requests
|
||||
public signal = axios.CancelToken.source()
|
||||
|
||||
public async componentDidMount() {
|
||||
public componentDidMount() {
|
||||
this.getOceanVersions()
|
||||
this.getFaucetVersion()
|
||||
}
|
||||
|
||||
public async componentDidUpdate(prevProps: any) {
|
||||
// Workaround: Using account prop instead of getting it from
|
||||
// context to be able to compare. Cause there is no `prevContext`.
|
||||
if (prevProps.account !== this.props.account) {
|
||||
this.getOceanVersions()
|
||||
this.getFaucetVersion()
|
||||
}
|
||||
}
|
||||
|
||||
public componentWillUnmount() {
|
||||
this.signal.cancel()
|
||||
}
|
||||
@ -132,14 +144,19 @@ export default class VersionNumbers extends PureComponent<
|
||||
const { commons, squid, brizo, aquarius } = this.state
|
||||
|
||||
return (
|
||||
<Market.Consumer>
|
||||
{market => (
|
||||
<p className={styles.versionsMinimal}>
|
||||
<a
|
||||
title={`${squid.name} v${squid.version}\n${brizo.name} v${brizo.version}\n${aquarius.name} v${aquarius.version}`}
|
||||
href={'/about'}
|
||||
>
|
||||
v{commons.version} {squid.network && `(${squid.network})`}
|
||||
v{commons.version}{' '}
|
||||
{market.network && `(${market.network})`}
|
||||
</a>
|
||||
</p>
|
||||
)}
|
||||
</Market.Consumer>
|
||||
)
|
||||
}
|
||||
|
||||
@ -149,13 +166,13 @@ export default class VersionNumbers extends PureComponent<
|
||||
return minimal ? (
|
||||
<this.MinimalOutput />
|
||||
) : (
|
||||
<div className={styles.versions} id="#oceanversions">
|
||||
<h2 className={styles.versionsTitle}>
|
||||
<>
|
||||
<h2 className={styles.versionsTitle} id="#oceanversions">
|
||||
Ocean Components Status
|
||||
</h2>
|
||||
<VersionStatus status={this.state.status} />
|
||||
<VersionTable data={this.state} />
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -57,10 +57,9 @@ export default class AssetsUser extends PureComponent<
|
||||
}
|
||||
|
||||
public render() {
|
||||
const { account, isOceanNetwork } = this.context
|
||||
const { account } = this.context
|
||||
|
||||
return (
|
||||
isOceanNetwork &&
|
||||
account && (
|
||||
<div className={styles.assetsUser}>
|
||||
{this.props.recent && (
|
||||
|
@ -1,5 +1,5 @@
|
||||
import React from 'react'
|
||||
import { Market } from '../../context'
|
||||
import React, { useContext } from 'react'
|
||||
import { Market, User } from '../../context'
|
||||
import Content from '../atoms/Content'
|
||||
import { ReactComponent as AiCommons } from '../../img/aicommons.svg'
|
||||
import styles from './Footer.module.scss'
|
||||
@ -7,18 +7,18 @@ import styles from './Footer.module.scss'
|
||||
import meta from '../../data/meta.json'
|
||||
import VersionNumbers from '../molecules/VersionNumbers'
|
||||
|
||||
const Footer = () => (
|
||||
export default function Footer() {
|
||||
const market = useContext(Market)
|
||||
const user = useContext(User)
|
||||
|
||||
return (
|
||||
<footer className={styles.footer}>
|
||||
<aside className={styles.stats}>
|
||||
<Content wide>
|
||||
<p>
|
||||
Online since March 2019.
|
||||
<Market.Consumer>
|
||||
{state =>
|
||||
state.totalAssets > 0 &&
|
||||
` With a total of ${state.totalAssets} registered assets.`
|
||||
}
|
||||
</Market.Consumer>
|
||||
{market.totalAssets > 0 &&
|
||||
` With a total of ${market.totalAssets} registered assets.`}
|
||||
</p>
|
||||
<p className={styles.aicommons}>
|
||||
Proud supporter of{' '}
|
||||
@ -29,7 +29,7 @@ const Footer = () => (
|
||||
<AiCommons />
|
||||
</a>
|
||||
</p>
|
||||
<VersionNumbers minimal />
|
||||
<VersionNumbers account={user.account} minimal />
|
||||
</Content>
|
||||
</aside>
|
||||
|
||||
@ -49,6 +49,5 @@ const Footer = () => (
|
||||
</nav>
|
||||
</Content>
|
||||
</footer>
|
||||
)
|
||||
|
||||
export default Footer
|
||||
)
|
||||
}
|
||||
|
@ -1,7 +1,6 @@
|
||||
import React, { PureComponent } from 'react'
|
||||
import { NavLink } from 'react-router-dom'
|
||||
import { ReactComponent as Logo } from '@oceanprotocol/art/logo/logo.svg'
|
||||
import { User } from '../../context'
|
||||
import AccountStatus from '../molecules/AccountStatus'
|
||||
import styles from './Header.module.scss'
|
||||
|
||||
@ -40,5 +39,3 @@ export default class Header extends PureComponent {
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Header.contextType = User
|
||||
|
73
client/src/components/organisms/WalletSelector.module.scss
Normal file
73
client/src/components/organisms/WalletSelector.module.scss
Normal 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;
|
||||
}
|
23
client/src/components/organisms/WalletSelector.test.tsx
Normal file
23
client/src/components/organisms/WalletSelector.test.tsx
Normal 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'))
|
||||
})
|
||||
})
|
108
client/src/components/organisms/WalletSelector.tsx
Normal file
108
client/src/components/organisms/WalletSelector.tsx
Normal 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>
|
||||
</>
|
||||
)
|
||||
}
|
||||
}
|
@ -4,19 +4,27 @@
|
||||
margin-bottom: $spacer;
|
||||
color: $brand-grey;
|
||||
position: relative;
|
||||
border-bottom: .1rem solid $brand-grey-lighter;
|
||||
border-top: .1rem solid $brand-grey-lighter;
|
||||
padding-top: $spacer / 2;
|
||||
padding-bottom: $spacer / 2;
|
||||
text-align: left;
|
||||
font-size: $font-size-small;
|
||||
}
|
||||
|
||||
.warnings {
|
||||
padding-left: $spacer;
|
||||
.account {
|
||||
margin-bottom: $spacer / 2;
|
||||
background: $brand-white;
|
||||
border-radius: $border-radius;
|
||||
border: 1px solid $brand-grey-lighter;
|
||||
padding: $spacer / 2;
|
||||
}
|
||||
|
||||
.text {
|
||||
padding-left: $spacer * 1.5;
|
||||
display: inline-block;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.status {
|
||||
margin-left: -($spacer);
|
||||
margin-right: $spacer / 2;
|
||||
margin-left: -($spacer / 1.2);
|
||||
margin-right: $spacer / 2.5;
|
||||
padding: 0;
|
||||
}
|
||||
|
@ -1,64 +1,60 @@
|
||||
import React from 'react'
|
||||
import { render, fireEvent } from '@testing-library/react'
|
||||
import { render } from '@testing-library/react'
|
||||
import Web3message from './Web3message'
|
||||
import { User } from '../../context'
|
||||
import { User, Market } from '../../context'
|
||||
import { userMock, userMockConnected } from '../../../__mocks__/user-mock'
|
||||
import { marketMock } from '../../../__mocks__/market-mock'
|
||||
|
||||
describe('Web3message', () => {
|
||||
it('renders with noWeb3 message', () => {
|
||||
it('renders with burner wallet message', () => {
|
||||
const { container } = render(
|
||||
<User.Provider value={{ ...userMock }}>
|
||||
<Web3message />
|
||||
<User.Provider value={{ ...userMockConnected, isBurner: true }}>
|
||||
<Market.Provider value={{ ...marketMock }}>
|
||||
<Web3message extended />
|
||||
</Market.Provider>
|
||||
</User.Provider>
|
||||
)
|
||||
expect(container.firstChild).toHaveTextContent('Not a Web3 Browser')
|
||||
expect(container.firstChild).toHaveTextContent('Burner Wallet')
|
||||
})
|
||||
|
||||
it('renders with wrongNetwork message', () => {
|
||||
const { container } = render(
|
||||
<User.Provider value={{ ...userMock, isWeb3: true }}>
|
||||
<Web3message />
|
||||
<User.Provider value={{ ...userMockConnected, network: 'Pacific' }}>
|
||||
<Market.Provider
|
||||
value={{
|
||||
...marketMock,
|
||||
networkMatch: false,
|
||||
network: 'Nile'
|
||||
}}
|
||||
>
|
||||
<Web3message extended />
|
||||
</Market.Provider>
|
||||
</User.Provider>
|
||||
)
|
||||
expect(container.firstChild).toHaveTextContent(
|
||||
'Not connected to Pacific network'
|
||||
'Not connected to Nile network'
|
||||
)
|
||||
})
|
||||
|
||||
it('renders with noAccount message', () => {
|
||||
const { container } = render(
|
||||
<User.Provider
|
||||
value={{ ...userMock, isWeb3: true, isOceanNetwork: true }}
|
||||
>
|
||||
<Web3message />
|
||||
<User.Provider value={userMock}>
|
||||
<Market.Provider value={marketMock}>
|
||||
<Web3message extended />
|
||||
</Market.Provider>
|
||||
</User.Provider>
|
||||
)
|
||||
expect(container.firstChild).toHaveTextContent('No accounts detected')
|
||||
expect(container.firstChild).toHaveTextContent('No account selected')
|
||||
})
|
||||
|
||||
it('renders with hasAccount message', () => {
|
||||
const { container } = render(
|
||||
<User.Provider value={userMockConnected}>
|
||||
<Market.Provider value={marketMock}>
|
||||
<Web3message />
|
||||
</Market.Provider>
|
||||
</User.Provider>
|
||||
)
|
||||
expect(container.firstChild).toHaveTextContent('0xxxxxx')
|
||||
})
|
||||
|
||||
it('button click fires unlockAccounts', () => {
|
||||
const { getByText } = render(
|
||||
<User.Provider
|
||||
value={{
|
||||
...userMock,
|
||||
isWeb3: true,
|
||||
isOceanNetwork: true
|
||||
}}
|
||||
>
|
||||
<Web3message />
|
||||
</User.Provider>
|
||||
)
|
||||
|
||||
fireEvent.click(getByText('Unlock Account'))
|
||||
expect(userMock.unlockAccounts).toBeCalled()
|
||||
})
|
||||
})
|
||||
|
@ -1,53 +1,66 @@
|
||||
import React, { PureComponent } from 'react'
|
||||
import Account from '../atoms/Account'
|
||||
import Button from '../atoms/Button'
|
||||
import AccountStatus from '../molecules/AccountStatus'
|
||||
import styles from './Web3message.module.scss'
|
||||
import { User } from '../../context'
|
||||
import { User, Market } from '../../context'
|
||||
import content from '../../data/web3message.json'
|
||||
|
||||
export default class Web3message extends PureComponent {
|
||||
private message = (
|
||||
message: string,
|
||||
account?: string,
|
||||
unlockAccounts?: () => any
|
||||
) => (
|
||||
<div className={styles.message}>
|
||||
{account ? (
|
||||
<Account account={account} />
|
||||
) : (
|
||||
<div className={styles.warnings}>
|
||||
<AccountStatus className={styles.status} />
|
||||
<span dangerouslySetInnerHTML={{ __html: message }} />{' '}
|
||||
{unlockAccounts && (
|
||||
<Button onClick={() => unlockAccounts()} link>
|
||||
Unlock Account
|
||||
</Button>
|
||||
export default class Web3message extends PureComponent<{ extended?: boolean }> {
|
||||
public static contextType = Market
|
||||
|
||||
private messageOceanNetwork = () =>
|
||||
this.context.network === 'Pacific'
|
||||
? content.wrongNetworkPacific
|
||||
: this.context.network === 'Nile'
|
||||
? content.wrongNetworkNile
|
||||
: this.context.network === 'Duero'
|
||||
? content.wrongNetworkDuero
|
||||
: content.wrongNetworkSpree
|
||||
|
||||
private Message = () => {
|
||||
const { networkMatch, network } = this.context
|
||||
|
||||
return (
|
||||
<User.Consumer>
|
||||
{user => (
|
||||
<em
|
||||
dangerouslySetInnerHTML={{
|
||||
__html:
|
||||
!networkMatch && !user.isBurner
|
||||
? this.messageOceanNetwork()
|
||||
: !user.isLogged
|
||||
? content.noAccount
|
||||
: user.isBurner
|
||||
? content.hasBurnerWallet
|
||||
: user.isLogged
|
||||
? content.hasMetaMaskWallet.replace(
|
||||
'NETWORK',
|
||||
network
|
||||
)
|
||||
: ''
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</User.Consumer>
|
||||
)
|
||||
}
|
||||
|
||||
public render() {
|
||||
const { 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>
|
||||
)
|
||||
|
||||
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
|
||||
|
@ -5,9 +5,10 @@ import { render, fireEvent } from '@testing-library/react'
|
||||
import { DDO } from '@oceanprotocol/squid'
|
||||
import { StateMock } from '@react-mock/state'
|
||||
import ReactGA from 'react-ga'
|
||||
import { User } from '../../../context'
|
||||
import { User, Market } from '../../../context'
|
||||
import AssetFile, { messages } from './AssetFile'
|
||||
import { userMockConnected } from '../../../../__mocks__/user-mock'
|
||||
import { marketMock } from '../../../../__mocks__/market-mock'
|
||||
|
||||
const file = {
|
||||
index: 0,
|
||||
@ -39,7 +40,9 @@ describe('AssetFile', () => {
|
||||
it('button to be enabled when connected', async () => {
|
||||
const { getByText } = render(
|
||||
<User.Provider value={userMockConnected}>
|
||||
<Market.Provider value={marketMock}>
|
||||
<AssetFile file={file} ddo={ddo} />
|
||||
</Market.Provider>
|
||||
</User.Provider>
|
||||
)
|
||||
const button = getByText('Get file')
|
||||
@ -50,12 +53,12 @@ describe('AssetFile', () => {
|
||||
|
||||
it('renders feedback message: initial', async () => {
|
||||
const { container } = render(
|
||||
<StateMock state={{ isLoading: true, step: null }}>
|
||||
<StateMock state={{ isLoading: true, step: 99 }}>
|
||||
<AssetFile file={file} ddo={ddo} />
|
||||
</StateMock>
|
||||
)
|
||||
expect(container.querySelector('.spinner')).toHaveTextContent(
|
||||
messages.start
|
||||
messages[99]
|
||||
)
|
||||
})
|
||||
|
||||
|
@ -3,13 +3,13 @@ import { Logger, DDO, File } from '@oceanprotocol/squid'
|
||||
import filesize from 'filesize'
|
||||
import Button from '../../atoms/Button'
|
||||
import Spinner from '../../atoms/Spinner'
|
||||
import { User } from '../../../context'
|
||||
import { User, Market } from '../../../context'
|
||||
import styles from './AssetFile.module.scss'
|
||||
import ReactGA from 'react-ga'
|
||||
import cleanupContentType from '../../../utils/cleanupContentType'
|
||||
|
||||
export const messages = {
|
||||
start: 'Decrypting file URL...',
|
||||
export const messages: any = {
|
||||
99: 'Decrypting file URL...',
|
||||
0: '1/3<br />Asking for agreement signature...',
|
||||
1: '1/3<br />Agreement initialized.',
|
||||
2: '2/3<br />Asking for two payment confirmations...',
|
||||
@ -25,24 +25,26 @@ interface AssetFileProps {
|
||||
interface AssetFileState {
|
||||
isLoading: boolean
|
||||
error: string
|
||||
step: number | string | null
|
||||
step: number
|
||||
}
|
||||
|
||||
export default class AssetFile extends PureComponent<
|
||||
AssetFileProps,
|
||||
AssetFileState
|
||||
> {
|
||||
public static contextType = User
|
||||
|
||||
public state = {
|
||||
isLoading: false,
|
||||
error: '',
|
||||
step: null
|
||||
step: 99
|
||||
}
|
||||
|
||||
private resetState = () =>
|
||||
this.setState({
|
||||
isLoading: true,
|
||||
error: '',
|
||||
step: null
|
||||
step: 99
|
||||
})
|
||||
|
||||
private purchaseAsset = async (ddo: DDO, index: number) => {
|
||||
@ -109,7 +111,7 @@ export default class AssetFile extends PureComponent<
|
||||
public render() {
|
||||
const { ddo, file } = this.props
|
||||
const { isLoading, error, step } = this.state
|
||||
const { isLogged, isOceanNetwork } = this.context
|
||||
const { isLogged } = this.context
|
||||
const { index, contentType, contentLength } = file
|
||||
|
||||
return (
|
||||
@ -134,28 +136,28 @@ export default class AssetFile extends PureComponent<
|
||||
</ul>
|
||||
|
||||
{isLoading ? (
|
||||
<Spinner
|
||||
message={
|
||||
step === null ? messages.start : messages[step]
|
||||
}
|
||||
/>
|
||||
<Spinner message={messages[step]} />
|
||||
) : (
|
||||
<Market.Consumer>
|
||||
{market => (
|
||||
<Button
|
||||
primary
|
||||
className={styles.buttonMain}
|
||||
// weird 0 hack so TypeScript is happy
|
||||
onClick={() => this.purchaseAsset(ddo, index || 0)}
|
||||
disabled={!isLogged || !isOceanNetwork}
|
||||
onClick={() =>
|
||||
this.purchaseAsset(ddo, index || 0)
|
||||
}
|
||||
disabled={!isLogged || !market.networkMatch}
|
||||
name="Download"
|
||||
>
|
||||
Get file
|
||||
</Button>
|
||||
)}
|
||||
</Market.Consumer>
|
||||
)}
|
||||
|
||||
{error !== '' && <div className={styles.error}>{error}</div>}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
AssetFile.contextType = User
|
||||
|
@ -4,8 +4,6 @@ import React from 'react'
|
||||
import { render } from '@testing-library/react'
|
||||
import { DDO } from '@oceanprotocol/squid'
|
||||
import AssetFilesDetails from './AssetFilesDetails'
|
||||
import { User } from '../../../context'
|
||||
import { userMockConnected } from '../../../../__mocks__/user-mock'
|
||||
|
||||
describe('AssetFilesDetails', () => {
|
||||
it('renders without crashing', () => {
|
||||
@ -28,16 +26,4 @@ describe('AssetFilesDetails', () => {
|
||||
)
|
||||
expect(container.firstChild).toHaveTextContent('No files attached.')
|
||||
})
|
||||
|
||||
it('hides Web3message when all connected', () => {
|
||||
const { container } = render(
|
||||
<User.Provider value={userMockConnected}>
|
||||
<AssetFilesDetails
|
||||
files={[{ index: 0, url: '' }]}
|
||||
ddo={({} as any) as DDO}
|
||||
/>
|
||||
</User.Provider>
|
||||
)
|
||||
expect(container.querySelector('.status')).not.toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
@ -19,9 +19,7 @@ export default class AssetFilesDetails extends PureComponent<{
|
||||
<AssetFile key={file.index} ddo={ddo} file={file} />
|
||||
))}
|
||||
</div>
|
||||
{(!this.context.isOceanNetwork || !this.context.isLogged) && (
|
||||
<Web3message />
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<div>No files attached.</div>
|
||||
|
42
client/src/components/templates/Asset/Report.test.tsx
Normal file
42
client/src/components/templates/Asset/Report.test.tsx
Normal 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'))
|
||||
})
|
||||
})
|
@ -142,6 +142,7 @@ export default class Report extends PureComponent<
|
||||
primary
|
||||
onClick={(e: Event) => this.sendEmail(e)}
|
||||
disabled={this.state.comment === ''}
|
||||
data-testid="report"
|
||||
>
|
||||
Report Data Set
|
||||
</Button>
|
||||
|
@ -1,22 +1,18 @@
|
||||
import React from 'react'
|
||||
import { render } from '@testing-library/react'
|
||||
import { createMemoryHistory, createLocation } from 'history'
|
||||
import Details from './index'
|
||||
|
||||
const history = createMemoryHistory()
|
||||
const location = createLocation('/asset/did:xxx')
|
||||
|
||||
describe('Details', () => {
|
||||
it('renders loading state by default', () => {
|
||||
const { container } = render(
|
||||
<Details
|
||||
location={{
|
||||
search: '',
|
||||
pathname: '/',
|
||||
state: '',
|
||||
hash: ''
|
||||
}}
|
||||
match={{
|
||||
params: {
|
||||
did: ''
|
||||
}
|
||||
}}
|
||||
history={history}
|
||||
location={location}
|
||||
match={{ params: '', path: '', url: '', isExact: true }}
|
||||
/>
|
||||
)
|
||||
expect(container.firstChild).toBeInTheDocument()
|
||||
|
@ -8,6 +8,7 @@ import stylesApp from '../../../App.module.scss'
|
||||
import Content from '../../atoms/Content'
|
||||
import CategoryImage from '../../atoms/CategoryImage'
|
||||
import styles from './index.module.scss'
|
||||
import withTracker from '../../../hoc/withTracker'
|
||||
|
||||
interface AssetProps {
|
||||
match: {
|
||||
@ -23,7 +24,9 @@ interface AssetState {
|
||||
error: string
|
||||
}
|
||||
|
||||
export default class Asset extends Component<AssetProps, AssetState> {
|
||||
class Asset extends Component<AssetProps, AssetState> {
|
||||
public static contextType = User
|
||||
|
||||
public state = {
|
||||
ddo: ({} as any) as DDO,
|
||||
metadata: ({ base: { name: '' } } as any) as MetaData,
|
||||
@ -79,4 +82,4 @@ export default class Asset extends Component<AssetProps, AssetState> {
|
||||
}
|
||||
}
|
||||
|
||||
Asset.contextType = User
|
||||
export default withTracker(Asset)
|
||||
|
11
client/src/context/BurnerWalletProvider.test.ts
Normal file
11
client/src/context/BurnerWalletProvider.test.ts
Normal 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)
|
||||
})
|
||||
})
|
53
client/src/context/BurnerWalletProvider.ts
Normal file
53
client/src/context/BurnerWalletProvider.ts
Normal 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
|
||||
}
|
||||
}
|
22
client/src/context/MarketProvider.test.tsx
Normal file
22
client/src/context/MarketProvider.test.tsx
Normal 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()
|
||||
})
|
||||
})
|
@ -1,6 +1,6 @@
|
||||
import React, { PureComponent } from 'react'
|
||||
import { Logger, Ocean } from '@oceanprotocol/squid'
|
||||
import { Market } from '.'
|
||||
import { Market, User } from '.'
|
||||
import formPublish from '../data/form-publish.json'
|
||||
|
||||
const categories =
|
||||
@ -16,24 +16,34 @@ interface MarketProviderProps {
|
||||
interface MarketProviderState {
|
||||
totalAssets: number
|
||||
categories: string[]
|
||||
network: string
|
||||
networkMatch: boolean
|
||||
}
|
||||
|
||||
export default class MarketProvider extends PureComponent<
|
||||
MarketProviderProps,
|
||||
MarketProviderState
|
||||
> {
|
||||
public static contextType = User
|
||||
|
||||
public state = {
|
||||
totalAssets: 0,
|
||||
categories
|
||||
categories,
|
||||
network: 'Pacific',
|
||||
networkMatch: false
|
||||
}
|
||||
|
||||
public async componentDidMount() {}
|
||||
public async componentDidMount() {
|
||||
await this.checkCorrectUserNetwork()
|
||||
}
|
||||
|
||||
public async componentDidUpdate(prevProps: any) {
|
||||
// Using ocean prop instead of getting it from context to be able to compare.
|
||||
// Cause there is no `prevContext`.
|
||||
if (prevProps.ocean !== this.props.ocean) {
|
||||
await this.getTotalAssets()
|
||||
await this.getMarketNetwork()
|
||||
await this.checkCorrectUserNetwork()
|
||||
}
|
||||
}
|
||||
|
||||
@ -56,6 +66,27 @@ export default class MarketProvider extends PureComponent<
|
||||
}
|
||||
}
|
||||
|
||||
private getMarketNetwork = async () => {
|
||||
try {
|
||||
const { ocean } = this.props
|
||||
// Set desired network to whatever Brizo is running in
|
||||
const brizo = await ocean.brizo.getVersionInfo()
|
||||
const network =
|
||||
brizo.network.charAt(0).toUpperCase() + brizo.network.slice(1)
|
||||
this.setState({ network })
|
||||
} catch (error) {
|
||||
Logger.error('Error', error.message)
|
||||
}
|
||||
}
|
||||
|
||||
private async checkCorrectUserNetwork() {
|
||||
if (this.context.network === this.state.network) {
|
||||
this.setState({ networkMatch: true })
|
||||
} else {
|
||||
this.setState({ networkMatch: false })
|
||||
}
|
||||
}
|
||||
|
||||
public render() {
|
||||
return (
|
||||
<Market.Provider value={this.state}>
|
||||
|
11
client/src/context/MetamaskProvider.test.tsx
Normal file
11
client/src/context/MetamaskProvider.test.tsx
Normal 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)
|
||||
})
|
||||
})
|
48
client/src/context/MetamaskProvider.ts
Normal file
48
client/src/context/MetamaskProvider.ts
Normal 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
|
||||
}
|
||||
}
|
@ -1,13 +1,16 @@
|
||||
import React, { PureComponent } from 'react'
|
||||
import Web3 from 'web3'
|
||||
import { Logger, Ocean, Account } from '@oceanprotocol/squid'
|
||||
import { Ocean, Account } from '@oceanprotocol/squid'
|
||||
import { User } from '.'
|
||||
import { provideOcean, requestFromFaucet, FaucetResponse } from '../ocean'
|
||||
import { nodeUri } from '../config'
|
||||
import MarketProvider from './MarketProvider'
|
||||
import { MetamaskProvider } from './MetamaskProvider'
|
||||
import { BurnerWalletProvider } from './BurnerWalletProvider'
|
||||
|
||||
const POLL_ACCOUNTS = 1000 // every 1s
|
||||
const POLL_NETWORK = POLL_ACCOUNTS * 60 // every 1 min
|
||||
const DEFAULT_WEB3 = new Web3(new Web3.providers.HttpProvider(nodeUri)) // default web3
|
||||
|
||||
// taken from
|
||||
// https://github.com/DefinitelyTyped/DefinitelyTyped/blob/master/types/web3/providers.d.ts
|
||||
@ -45,9 +48,9 @@ declare global {
|
||||
|
||||
interface UserProviderState {
|
||||
isLogged: boolean
|
||||
isBurner: boolean
|
||||
isWeb3Capable: boolean
|
||||
isLoading: boolean
|
||||
isWeb3: boolean
|
||||
isOceanNetwork: boolean
|
||||
account: string
|
||||
balance: {
|
||||
eth: number
|
||||
@ -57,35 +60,67 @@ interface UserProviderState {
|
||||
web3: Web3
|
||||
ocean: Ocean
|
||||
requestFromFaucet(account: string): Promise<FaucetResponse>
|
||||
unlockAccounts(): Promise<any>
|
||||
loginMetamask(): Promise<any>
|
||||
loginBurnerWallet(): Promise<any>
|
||||
logoutBurnerWallet(): Promise<any>
|
||||
message: string
|
||||
}
|
||||
|
||||
export default class UserProvider extends PureComponent<{}, UserProviderState> {
|
||||
private unlockAccounts = async () => {
|
||||
try {
|
||||
await window.ethereum.enable()
|
||||
} catch (error) {
|
||||
// User denied account access...
|
||||
return null
|
||||
private loginMetamask = async () => {
|
||||
const metamaskProvider = new MetamaskProvider()
|
||||
await metamaskProvider.startLogin()
|
||||
const web3 = metamaskProvider.getProvider()
|
||||
this.setState(
|
||||
{
|
||||
isLogged: true,
|
||||
isBurner: false,
|
||||
web3
|
||||
},
|
||||
() => {
|
||||
this.loadOcean()
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
private loginBurnerWallet = async () => {
|
||||
const burnerwalletProvider = new BurnerWalletProvider()
|
||||
await burnerwalletProvider.startLogin()
|
||||
const web3 = burnerwalletProvider.getProvider()
|
||||
this.setState(
|
||||
{
|
||||
isLogged: true,
|
||||
isBurner: true,
|
||||
web3
|
||||
},
|
||||
() => {
|
||||
this.loadOcean()
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
private logoutBurnerWallet = async () => {
|
||||
const burnerwalletProvider = new BurnerWalletProvider()
|
||||
await burnerwalletProvider.logout()
|
||||
}
|
||||
|
||||
public state = {
|
||||
isLogged: false,
|
||||
isBurner: false,
|
||||
isWeb3Capable: Boolean(window.web3 || window.ethereum),
|
||||
isLoading: true,
|
||||
isWeb3: false,
|
||||
isOceanNetwork: false,
|
||||
balance: {
|
||||
eth: 0,
|
||||
ocn: 0
|
||||
},
|
||||
network: '',
|
||||
web3: new Web3(new Web3.providers.HttpProvider(nodeUri)),
|
||||
web3: DEFAULT_WEB3,
|
||||
account: '',
|
||||
ocean: {} as any,
|
||||
requestFromFaucet: () => requestFromFaucet(''),
|
||||
unlockAccounts: () => this.unlockAccounts(),
|
||||
loginMetamask: () => this.loginMetamask(),
|
||||
loginBurnerWallet: () => this.loginBurnerWallet(),
|
||||
logoutBurnerWallet: () => this.logoutBurnerWallet(),
|
||||
message: 'Connecting to Ocean...'
|
||||
}
|
||||
|
||||
@ -94,9 +129,6 @@ export default class UserProvider extends PureComponent<{}, UserProviderState> {
|
||||
|
||||
public async componentDidMount() {
|
||||
await this.bootstrap()
|
||||
|
||||
this.initAccountsPoll()
|
||||
this.initNetworkPoll()
|
||||
}
|
||||
|
||||
private initAccountsPoll() {
|
||||
@ -114,109 +146,70 @@ export default class UserProvider extends PureComponent<{}, UserProviderState> {
|
||||
}
|
||||
}
|
||||
|
||||
private getWeb3 = () => {
|
||||
// Modern dapp browsers
|
||||
if (window.ethereum) {
|
||||
window.web3 = new Web3(window.ethereum)
|
||||
return window.web3
|
||||
private loadDefaultWeb3 = async () => {
|
||||
this.setState(
|
||||
{
|
||||
isLogged: false,
|
||||
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 () => {
|
||||
try {
|
||||
//
|
||||
// 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()
|
||||
const logType = localStorage.getItem('logType')
|
||||
const metamaskProvider = new MetamaskProvider()
|
||||
|
||||
switch (logType) {
|
||||
case 'Metamask':
|
||||
if (
|
||||
isOceanNetwork !== this.state.isOceanNetwork ||
|
||||
network !== this.state.network
|
||||
(await metamaskProvider.isAvailable()) &&
|
||||
(await metamaskProvider.isLogged())
|
||||
) {
|
||||
this.setState({ isOceanNetwork, network })
|
||||
const web3 = metamaskProvider.getProvider()
|
||||
this.setState(
|
||||
{
|
||||
isLogged: true,
|
||||
web3
|
||||
},
|
||||
() => {
|
||||
this.loadOcean()
|
||||
}
|
||||
})
|
||||
|
||||
if (!isOceanNetwork) {
|
||||
web3 = this.state.web3 // eslint-disable-line
|
||||
)
|
||||
} else {
|
||||
this.loadDefaultWeb3()
|
||||
}
|
||||
|
||||
//
|
||||
// Provide the Ocean
|
||||
//
|
||||
this.setState({ message: 'Connecting to Ocean...' })
|
||||
|
||||
const { ocean } = await provideOcean(web3)
|
||||
this.setState({ ocean, message: 'Getting accounts...' })
|
||||
|
||||
// Get accounts
|
||||
await this.fetchAccounts()
|
||||
|
||||
this.setState({ isLoading: false, message: '' })
|
||||
}
|
||||
// Non-dapp browsers
|
||||
else {
|
||||
this.setState({ message: 'Connecting to Ocean...' })
|
||||
const { ocean } = await provideOcean(this.state.web3)
|
||||
this.setState({ ocean, isLoading: false })
|
||||
|
||||
this.fetchNetwork()
|
||||
}
|
||||
} catch (e) {
|
||||
// error in bootstrap process
|
||||
// show error connecting to ocean
|
||||
Logger.error('web3 error', e.message)
|
||||
this.setState({ isLoading: false })
|
||||
break
|
||||
case 'BurnerWallet':
|
||||
this.loginBurnerWallet()
|
||||
break
|
||||
default:
|
||||
this.loginBurnerWallet()
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
private fetchAccounts = async () => {
|
||||
const { ocean, isWeb3, isLogged, isOceanNetwork } = this.state
|
||||
const { ocean, isLogged } = this.state
|
||||
|
||||
if (isWeb3) {
|
||||
if (isLogged) {
|
||||
let accounts
|
||||
|
||||
// Modern dapp browsers
|
||||
if (window.ethereum && !isLogged && isOceanNetwork) {
|
||||
if (window.ethereum && !isLogged) {
|
||||
// simply set to empty, and have user click a button somewhere
|
||||
// to initiate account unlocking
|
||||
accounts = []
|
||||
@ -254,19 +247,12 @@ export default class UserProvider extends PureComponent<{}, UserProviderState> {
|
||||
}
|
||||
|
||||
private fetchNetwork = async () => {
|
||||
const { ocean, isWeb3 } = this.state
|
||||
|
||||
if (isWeb3) {
|
||||
const network = await ocean.keeper.getNetworkName()
|
||||
const isPacific = network === 'Pacific'
|
||||
const isNile = network === 'Nile'
|
||||
const isDuero = network === 'Duero'
|
||||
const isSpree = network === 'Spree'
|
||||
const isOceanNetwork = isPacific || isNile || isDuero || isSpree
|
||||
|
||||
network !== this.state.network &&
|
||||
this.setState({ isOceanNetwork, network })
|
||||
const { ocean } = this.state
|
||||
let network = 'Unknown'
|
||||
if (ocean.keeper) {
|
||||
network = await ocean.keeper.getNetworkName()
|
||||
}
|
||||
network !== this.state.network && this.setState({ network })
|
||||
}
|
||||
|
||||
public render() {
|
||||
|
@ -2,9 +2,9 @@ import React from 'react'
|
||||
|
||||
export const User = React.createContext({
|
||||
isLogged: false,
|
||||
isBurner: false,
|
||||
isWeb3Capable: false,
|
||||
isLoading: false,
|
||||
isWeb3: false,
|
||||
isOceanNetwork: false,
|
||||
account: '',
|
||||
web3: {},
|
||||
ocean: {},
|
||||
@ -16,10 +16,18 @@ export const User = React.createContext({
|
||||
requestFromFaucet: () => {
|
||||
/* empty */
|
||||
},
|
||||
unlockAccounts: () => {
|
||||
loginMetamask: () => {
|
||||
/* empty */
|
||||
},
|
||||
loginBurnerWallet: () => {
|
||||
/* empty */
|
||||
},
|
||||
message: ''
|
||||
})
|
||||
|
||||
export const Market = React.createContext({ totalAssets: 0, categories: [''] })
|
||||
export const Market = React.createContext({
|
||||
totalAssets: 0,
|
||||
categories: [''],
|
||||
network: '',
|
||||
networkMatch: false
|
||||
})
|
||||
|
16
client/src/data/wallets.json
Normal file
16
client/src/data/wallets.json
Normal 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."
|
||||
}
|
||||
]
|
||||
}
|
@ -1,6 +1,10 @@
|
||||
{
|
||||
"noweb3": "Not a Web3 Browser. For publishing and downloading an asset you need to <a href='https://docs.oceanprotocol.com/tutorials/metamask-setup/' target='_blank' rel='noopener noreferrer'>setup MetaMask</a> or use any other Web3-capable plugin or browser.",
|
||||
"noAccount": "No accounts detected. For publishing and downloading an asset you need to unlock your Web3 account.",
|
||||
"hasAccount": "",
|
||||
"wrongNetwork": "Not connected to Pacific network.<br />Please connect in MetaMask with Custom RPC <code>https://pacific.oceanprotocol.com</code>"
|
||||
"noAccount": "No wallet selected. For publishing and downloading an asset you need to use one.",
|
||||
"hasBurnerWallet": "We created a temporary burner wallet for you, allowing you to use all Commons functionality without any setup on your side, and without a Web3-capable browser. This wallet will persist in your browser across sessions, but not across different browsers or devices. <strong>Never use this burner wallet to send or receive any tokens.</strong> To personalize your experience and improve your security, <a href='https://docs.oceanprotocol.com/tutorials/metamask-setup/'>migrate to MetaMask</a>.",
|
||||
"hasMetaMaskWallet": "Connected with MetaMask to NETWORK. You're a Pro.",
|
||||
"wrongNetworkPacific": "Not connected to Pacific network. Please connect in MetaMask with Custom RPC <code>https://pacific.oceanprotocol.com</code>",
|
||||
"wrongNetworkNile": "Not connected to Nile network. Please connect in MetaMask with Custom RPC <code>https://nile.dev-ocean.com</code>",
|
||||
"wrongNetworkDuero": "Not connected to Duero network. Please connect in MetaMask with Custom RPC <code>https://duero.dev-ocean.com</code>",
|
||||
"wrongNetworkSpree": "Not connected to Spree network. Please connect in MetaMask with Custom RPC <code>http://localhost:8545</code>",
|
||||
"seedphrase": "You can use this seed phrase to import this burner wallet account into other wallets, e.g. MetaMask."
|
||||
}
|
||||
|
@ -13,16 +13,14 @@ const withTracker = <P extends RouteComponentProps>(
|
||||
options: FieldsObject = {}
|
||||
) => {
|
||||
const trackPage = (page: string) => {
|
||||
options.isWeb3 = window.web3 !== undefined
|
||||
|
||||
ReactGA.set({ page, ...options })
|
||||
ReactGA.pageview(page)
|
||||
}
|
||||
|
||||
const HOC = (props: P) => {
|
||||
useEffect(() => trackPage(props.location.pathname), [
|
||||
props.location.pathname
|
||||
])
|
||||
useEffect(() => {
|
||||
trackPage(props.location.pathname)
|
||||
}, [props.location.pathname])
|
||||
|
||||
return <WrappedComponent {...props} />
|
||||
}
|
||||
|
3
client/src/img/caret.svg
Normal file
3
client/src/img/caret.svg
Normal 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 |
@ -11,9 +11,9 @@ import {
|
||||
verbose
|
||||
} from './config'
|
||||
|
||||
export async function provideOcean(web3provider: Web3) {
|
||||
export async function provideOcean(web3Provider: Web3) {
|
||||
const config = {
|
||||
web3provider,
|
||||
web3Provider,
|
||||
nodeUri,
|
||||
aquariusUri,
|
||||
brizoUri,
|
||||
@ -21,9 +21,7 @@ export async function provideOcean(web3provider: Web3) {
|
||||
secretStoreUri,
|
||||
verbose
|
||||
}
|
||||
|
||||
const ocean: Ocean = await Ocean.getInstance(config)
|
||||
|
||||
const ocean: any = await Ocean.getInstance(config)
|
||||
return { ocean }
|
||||
}
|
||||
|
||||
|
@ -1,13 +1,21 @@
|
||||
import React from 'react'
|
||||
import { render } from '@testing-library/react'
|
||||
import { MemoryRouter } from 'react-router'
|
||||
import { createMemoryHistory, createLocation } from 'history'
|
||||
import About from './About'
|
||||
|
||||
const history = createMemoryHistory()
|
||||
const location = createLocation('/about')
|
||||
|
||||
describe('About', () => {
|
||||
it('renders without crashing', () => {
|
||||
const { container } = render(
|
||||
<MemoryRouter>
|
||||
<About />
|
||||
<About
|
||||
history={history}
|
||||
location={location}
|
||||
match={{ params: '', path: '', url: '', isExact: true }}
|
||||
/>
|
||||
</MemoryRouter>
|
||||
)
|
||||
expect(container.firstChild).toBeInTheDocument()
|
||||
|
@ -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 Content from '../components/atoms/Content'
|
||||
import VersionNumbers from '../components/molecules/VersionNumbers'
|
||||
import Web3message from '../components/organisms/Web3message'
|
||||
import stylesVersionNumbers from '../components/molecules/VersionNumbers/index.module.scss'
|
||||
import withTracker from '../hoc/withTracker'
|
||||
|
||||
const About = () => {
|
||||
const market = useContext(Market)
|
||||
const user = useContext(User)
|
||||
|
||||
class About extends Component {
|
||||
public render() {
|
||||
return (
|
||||
<Route
|
||||
title="About"
|
||||
description="A marketplace to find and publish open data sets in the Ocean Pacific Network."
|
||||
description={`A marketplace to find and publish open data sets in the Ocean ${market.network} Network.`}
|
||||
>
|
||||
<Content>
|
||||
<p>
|
||||
Commons is built on top of the Ocean{' '}
|
||||
<a href="https://docs.oceanprotocol.com/concepts/pacific-network/">
|
||||
Pacific network
|
||||
{market.network} network
|
||||
</a>{' '}
|
||||
and is targeted at enthusiastic data scientists with
|
||||
some crypto experience. It can be used with any
|
||||
Web3-capable browser, like Firefox with MetaMask
|
||||
installed.
|
||||
and is targeted at enthusiastic data scientists with some
|
||||
crypto experience. It can be used with any Web3-capable
|
||||
browser, like Firefox with MetaMask installed.
|
||||
</p>
|
||||
|
||||
<ul>
|
||||
@ -37,11 +42,14 @@ class About extends Component {
|
||||
</Content>
|
||||
|
||||
<Content>
|
||||
<VersionNumbers />
|
||||
<h2 className={stylesVersionNumbers.versionsTitle}>
|
||||
Your Web3 Account Status
|
||||
</h2>
|
||||
<Web3message extended />
|
||||
<VersionNumbers account={user.account} />
|
||||
</Content>
|
||||
</Route>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default About
|
||||
export default withTracker(About)
|
||||
|
@ -1,16 +1,24 @@
|
||||
import React from 'react'
|
||||
import { MemoryRouter } from 'react-router'
|
||||
import { createMemoryHistory, createLocation } from 'history'
|
||||
import { render } from '@testing-library/react'
|
||||
import Channels from './Channels'
|
||||
import { User } from '../context'
|
||||
import { userMockConnected } from '../../__mocks__/user-mock'
|
||||
|
||||
const history = createMemoryHistory()
|
||||
const location = createLocation('/channels')
|
||||
|
||||
describe('Channels', () => {
|
||||
it('renders without crashing', () => {
|
||||
const { container } = render(
|
||||
<User.Provider value={userMockConnected}>
|
||||
<MemoryRouter>
|
||||
<Channels />
|
||||
<Channels
|
||||
history={history}
|
||||
location={location}
|
||||
match={{ params: '', path: '', url: '', isExact: true }}
|
||||
/>
|
||||
</MemoryRouter>
|
||||
</User.Provider>
|
||||
)
|
||||
|
@ -3,6 +3,7 @@ import Route from '../components/templates/Route'
|
||||
import Content from '../components/atoms/Content'
|
||||
import channels from '../data/channels.json'
|
||||
import ChannelTeaser from '../components/organisms/ChannelTeaser'
|
||||
import withTracker from '../hoc/withTracker'
|
||||
|
||||
class Channels extends Component {
|
||||
public render() {
|
||||
@ -21,4 +22,4 @@ class Channels extends Component {
|
||||
}
|
||||
}
|
||||
|
||||
export default Channels
|
||||
export default withTracker(Channels)
|
||||
|
@ -1,15 +1,23 @@
|
||||
import React from 'react'
|
||||
import { render, fireEvent } from '@testing-library/react'
|
||||
import { MemoryRouter } from 'react-router'
|
||||
import { createMemoryHistory, createLocation } from 'history'
|
||||
import Faucet from './Faucet'
|
||||
import { User } from '../context'
|
||||
import { userMockConnected } from '../../__mocks__/user-mock'
|
||||
|
||||
const history = createMemoryHistory()
|
||||
const location = createLocation('/faucet')
|
||||
|
||||
const setup = () => {
|
||||
const utils = render(
|
||||
<User.Provider value={userMockConnected}>
|
||||
<MemoryRouter>
|
||||
<Faucet />
|
||||
<Faucet
|
||||
history={history}
|
||||
location={location}
|
||||
match={{ params: '', path: '', url: '', isExact: true }}
|
||||
/>
|
||||
</MemoryRouter>
|
||||
</User.Provider>
|
||||
)
|
||||
|
@ -3,10 +3,11 @@ import { FaucetResponse } from '../ocean'
|
||||
import Route from '../components/templates/Route'
|
||||
import Button from '../components/atoms/Button'
|
||||
import Spinner from '../components/atoms/Spinner'
|
||||
import { User } from '../context'
|
||||
import { User, Market } from '../context'
|
||||
import Web3message from '../components/organisms/Web3message'
|
||||
import styles from './Faucet.module.scss'
|
||||
import Content from '../components/atoms/Content'
|
||||
import withTracker from '../hoc/withTracker'
|
||||
|
||||
interface FaucetState {
|
||||
isLoading: boolean
|
||||
@ -15,7 +16,7 @@ interface FaucetState {
|
||||
trxHash?: string
|
||||
}
|
||||
|
||||
export default class Faucet extends PureComponent<{}, FaucetState> {
|
||||
class Faucet extends PureComponent<{}, FaucetState> {
|
||||
public static contextType = User
|
||||
|
||||
public state = {
|
||||
@ -99,9 +100,8 @@ export default class Faucet extends PureComponent<{}, FaucetState> {
|
||||
<Button
|
||||
primary
|
||||
onClick={() => this.getTokens(this.context.requestFromFaucet)}
|
||||
disabled={
|
||||
!this.context.isLogged || !this.context.isOceanNetwork
|
||||
}
|
||||
disabled={!this.context.isLogged}
|
||||
name="Faucet"
|
||||
>
|
||||
Request Ether
|
||||
</Button>
|
||||
@ -112,13 +112,15 @@ export default class Faucet extends PureComponent<{}, FaucetState> {
|
||||
)
|
||||
|
||||
public render() {
|
||||
const { isWeb3 } = this.context
|
||||
const { isLogged } = this.context
|
||||
const { isLoading, error, success } = this.state
|
||||
|
||||
return (
|
||||
<Market.Consumer>
|
||||
{market => (
|
||||
<Route
|
||||
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>
|
||||
<Web3message />
|
||||
@ -131,13 +133,15 @@ export default class Faucet extends PureComponent<{}, FaucetState> {
|
||||
) : success ? (
|
||||
<this.Success />
|
||||
) : (
|
||||
isWeb3 && <this.Action />
|
||||
isLogged && <this.Action />
|
||||
)}
|
||||
</div>
|
||||
</Content>
|
||||
</Route>
|
||||
)}
|
||||
</Market.Consumer>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Faucet.contextType = User
|
||||
export default withTracker(Faucet)
|
||||
|
@ -1,45 +1,23 @@
|
||||
import React from 'react'
|
||||
import { render } from '@testing-library/react'
|
||||
import { MemoryRouter } from 'react-router'
|
||||
import { User } from '../context'
|
||||
import { createMemoryHistory, createLocation } from 'history'
|
||||
import History from './History'
|
||||
|
||||
const history = createMemoryHistory()
|
||||
const location = createLocation('/history')
|
||||
|
||||
describe('History', () => {
|
||||
it('renders without crashing', () => {
|
||||
const { container } = render(
|
||||
<MemoryRouter>
|
||||
<History />
|
||||
<History
|
||||
history={history}
|
||||
location={location}
|
||||
match={{ params: '', path: '', url: '', isExact: true }}
|
||||
/>
|
||||
</MemoryRouter>
|
||||
)
|
||||
expect(container.firstChild).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('outputs Web3 message when no Web3 detected', () => {
|
||||
const context = {
|
||||
isLogged: false,
|
||||
isLoading: false,
|
||||
isWeb3: false,
|
||||
isOceanNetwork: false,
|
||||
account: '',
|
||||
web3: {},
|
||||
ocean: {},
|
||||
balance: { eth: 0, ocn: 0 },
|
||||
network: '',
|
||||
requestFromFaucet: () => {},
|
||||
unlockAccounts: () => {},
|
||||
message: ''
|
||||
}
|
||||
|
||||
const { container } = render(
|
||||
<User.Provider value={context}>
|
||||
<MemoryRouter>
|
||||
<History />
|
||||
</MemoryRouter>
|
||||
</User.Provider>
|
||||
)
|
||||
expect(container.querySelector('.message')).toBeInTheDocument()
|
||||
expect(container.querySelector('.message')).toHaveTextContent(
|
||||
'Not a Web3 Browser.'
|
||||
)
|
||||
})
|
||||
})
|
||||
|
@ -4,15 +4,16 @@ import AssetsUser from '../components/organisms/AssetsUser'
|
||||
import Web3message from '../components/organisms/Web3message'
|
||||
import { User } from '../context'
|
||||
import Content from '../components/atoms/Content'
|
||||
import withTracker from '../hoc/withTracker'
|
||||
|
||||
class History extends Component {
|
||||
public static contextType = User
|
||||
|
||||
export default class History extends Component {
|
||||
public render() {
|
||||
return (
|
||||
<Route title="History">
|
||||
<Content>
|
||||
{(!this.context.isLogged ||
|
||||
!this.context.isOceanNetwork) && <Web3message />}
|
||||
|
||||
{!this.context.isLogged && <Web3message />}
|
||||
<AssetsUser list />
|
||||
</Content>
|
||||
</Route>
|
||||
@ -20,4 +21,4 @@ export default class History extends Component {
|
||||
}
|
||||
}
|
||||
|
||||
History.contextType = User
|
||||
export default withTracker(History)
|
||||
|
@ -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()
|
||||
})
|
||||
})
|
@ -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>
|
||||
)
|
||||
}
|
||||
}
|
16
client/src/routes/Home/Search.test.tsx
Normal file
16
client/src/routes/Home/Search.test.tsx
Normal 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'))
|
||||
})
|
||||
})
|
51
client/src/routes/Home/Search.tsx
Normal file
51
client/src/routes/Home/Search.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
}
|
@ -1,4 +1,4 @@
|
||||
@import '../styles/variables';
|
||||
@import '../../styles/variables';
|
||||
|
||||
.home {
|
||||
display: block;
|
27
client/src/routes/Home/index.test.tsx
Normal file
27
client/src/routes/Home/index.test.tsx
Normal 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()
|
||||
})
|
||||
})
|
74
client/src/routes/Home/index.tsx
Normal file
74
client/src/routes/Home/index.tsx
Normal 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)
|
@ -1,13 +1,21 @@
|
||||
import React from 'react'
|
||||
import { render } from '@testing-library/react'
|
||||
import { createMemoryHistory, createLocation } from 'history'
|
||||
import NotFound from './NotFound'
|
||||
import { MemoryRouter } from 'react-router'
|
||||
|
||||
const history = createMemoryHistory()
|
||||
const location = createLocation('/whatever')
|
||||
|
||||
describe('NotFound', () => {
|
||||
it('renders without crashing', () => {
|
||||
const { container } = render(
|
||||
<MemoryRouter>
|
||||
<NotFound />
|
||||
<NotFound
|
||||
history={history}
|
||||
location={location}
|
||||
match={{ params: '', path: '', url: '', isExact: true }}
|
||||
/>
|
||||
</MemoryRouter>
|
||||
)
|
||||
expect(container.firstChild).toBeInTheDocument()
|
||||
|
@ -1,6 +1,7 @@
|
||||
import React, { Component } from 'react'
|
||||
import Route from '../components/templates/Route'
|
||||
import Content from '../components/atoms/Content'
|
||||
import withTracker from '../hoc/withTracker'
|
||||
|
||||
class NotFound extends Component {
|
||||
public render() {
|
||||
@ -12,4 +13,4 @@ class NotFound extends Component {
|
||||
}
|
||||
}
|
||||
|
||||
export default NotFound
|
||||
export default withTracker(NotFound)
|
||||
|
@ -24,3 +24,7 @@
|
||||
margin-left: auto;
|
||||
}
|
||||
}
|
||||
|
||||
.account {
|
||||
margin-top: $spacer * $line-height;
|
||||
}
|
||||
|
@ -3,10 +3,11 @@ import Input from '../../components/atoms/Form/Input'
|
||||
import Label from '../../components/atoms/Form/Label'
|
||||
import Row from '../../components/atoms/Form/Row'
|
||||
import Button from '../../components/atoms/Button'
|
||||
import { User } from '../../context'
|
||||
import { User, Market } from '../../context'
|
||||
import Files from './Files/'
|
||||
import StepRegisterContent from './StepRegisterContent'
|
||||
import styles from './Step.module.scss'
|
||||
import Web3message from '../../components/organisms/Web3message'
|
||||
|
||||
interface Fields {
|
||||
label: string
|
||||
@ -42,6 +43,8 @@ interface StepProps {
|
||||
}
|
||||
|
||||
export default class Step extends PureComponent<StepProps, {}> {
|
||||
public static contextType = User
|
||||
|
||||
public previousButton() {
|
||||
const { currentStep, prev } = this.props
|
||||
|
||||
@ -151,9 +154,12 @@ export default class Step extends PureComponent<StepProps, {}> {
|
||||
{this.nextButton()}
|
||||
|
||||
{lastStep && (
|
||||
<Market.Consumer>
|
||||
{market => (
|
||||
<Button
|
||||
disabled={
|
||||
!this.context.isLogged ||
|
||||
!market.networkMatch ||
|
||||
this.props.state.isPublishing
|
||||
}
|
||||
primary
|
||||
@ -161,10 +167,13 @@ export default class Step extends PureComponent<StepProps, {}> {
|
||||
Register asset
|
||||
</Button>
|
||||
)}
|
||||
</Market.Consumer>
|
||||
)}
|
||||
</div>
|
||||
<div className={styles.account}>
|
||||
{!lastStep && <Web3message />}
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Step.contextType = User
|
||||
|
@ -1,16 +1,24 @@
|
||||
import React from 'react'
|
||||
import { MemoryRouter } from 'react-router'
|
||||
import { render, fireEvent } from '@testing-library/react'
|
||||
import { createMemoryHistory, createLocation } from 'history'
|
||||
import Publish from '.'
|
||||
import { User } from '../../context'
|
||||
import { userMockConnected } from '../../../__mocks__/user-mock'
|
||||
|
||||
const history = createMemoryHistory()
|
||||
const location = createLocation('/publish')
|
||||
|
||||
describe('Publish', () => {
|
||||
it('renders without crashing', () => {
|
||||
const { container, getByText } = render(
|
||||
<User.Provider value={userMockConnected}>
|
||||
<MemoryRouter>
|
||||
<Publish />
|
||||
<Publish
|
||||
history={history}
|
||||
location={location}
|
||||
match={{ params: '', path: '', url: '', isExact: true }}
|
||||
/>
|
||||
</MemoryRouter>
|
||||
</User.Provider>
|
||||
)
|
||||
@ -22,7 +30,16 @@ describe('Publish', () => {
|
||||
const { getByText, getByLabelText, getByTestId } = render(
|
||||
<User.Provider value={userMockConnected}>
|
||||
<MemoryRouter>
|
||||
<Publish />
|
||||
<Publish
|
||||
history={history}
|
||||
location={{
|
||||
pathname: '/publish',
|
||||
search: '',
|
||||
hash: '',
|
||||
state: ''
|
||||
}}
|
||||
match={{ params: '', path: '', url: '', isExact: true }}
|
||||
/>
|
||||
</MemoryRouter>
|
||||
</User.Provider>
|
||||
)
|
||||
|
@ -3,8 +3,7 @@ import { Logger } from '@oceanprotocol/squid'
|
||||
import Route from '../../components/templates/Route'
|
||||
import Form from '../../components/atoms/Form/Form'
|
||||
import AssetModel from '../../models/AssetModel'
|
||||
import { User } from '../../context'
|
||||
import Web3message from '../../components/organisms/Web3message'
|
||||
import { User, Market } from '../../context'
|
||||
import Step from './Step'
|
||||
import Progress from './Progress'
|
||||
import ReactGA from 'react-ga'
|
||||
@ -12,6 +11,7 @@ import ReactGA from 'react-ga'
|
||||
import { steps } from '../../data/form-publish.json'
|
||||
import Content from '../../components/atoms/Content'
|
||||
import { File } from './Files'
|
||||
import withTracker from '../../hoc/withTracker'
|
||||
|
||||
type AssetType = 'dataset' | 'algorithm' | 'container' | 'workflow' | 'other'
|
||||
|
||||
@ -36,7 +36,7 @@ interface PublishState {
|
||||
validationStatus?: any
|
||||
}
|
||||
|
||||
export default class Publish extends Component<{}, PublishState> {
|
||||
class Publish extends Component<{}, PublishState> {
|
||||
public static contextType = User
|
||||
|
||||
public state = {
|
||||
@ -319,14 +319,13 @@ export default class Publish extends Component<{}, PublishState> {
|
||||
|
||||
public render() {
|
||||
return (
|
||||
<Market.Consumer>
|
||||
{market => (
|
||||
<Route
|
||||
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>
|
||||
{(!this.context.isLogged ||
|
||||
!this.context.isOceanNetwork) && <Web3message />}
|
||||
|
||||
<Progress
|
||||
steps={steps}
|
||||
currentStep={this.state.currentStep}
|
||||
@ -354,6 +353,10 @@ export default class Publish extends Component<{}, PublishState> {
|
||||
</Form>
|
||||
</Content>
|
||||
</Route>
|
||||
)}
|
||||
</Market.Consumer>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default withTracker(Publish)
|
||||
|
@ -6,10 +6,10 @@ import { createMemoryHistory } from 'history'
|
||||
import { BrowserRouter as Router } from 'react-router-dom'
|
||||
import { userMockConnected } from '../../__mocks__/user-mock'
|
||||
|
||||
const history = createMemoryHistory()
|
||||
|
||||
describe('Search', () => {
|
||||
it('renders without crashing', () => {
|
||||
const history = createMemoryHistory()
|
||||
|
||||
const { container } = render(
|
||||
<User.Provider value={userMockConnected}>
|
||||
<Router>
|
||||
@ -21,6 +21,7 @@ describe('Search', () => {
|
||||
hash: ''
|
||||
}}
|
||||
history={history}
|
||||
match={{ params: '', path: '', url: '', isExact: true }}
|
||||
/>
|
||||
</Router>
|
||||
</User.Provider>
|
||||
|
@ -10,6 +10,7 @@ import AssetTeaser from '../components/molecules/AssetTeaser'
|
||||
import Pagination from '../components/molecules/Pagination'
|
||||
import styles from './Search.module.scss'
|
||||
import Content from '../components/atoms/Content'
|
||||
import withTracker from '../hoc/withTracker'
|
||||
|
||||
interface SearchProps {
|
||||
location: Location
|
||||
@ -27,7 +28,9 @@ interface SearchState {
|
||||
searchCategories: string
|
||||
}
|
||||
|
||||
export default class Search extends PureComponent<SearchProps, SearchState> {
|
||||
class Search extends PureComponent<SearchProps, SearchState> {
|
||||
public static contextType = User
|
||||
|
||||
public state = {
|
||||
results: [],
|
||||
totalResults: 0,
|
||||
@ -159,4 +162,4 @@ export default class Search extends PureComponent<SearchProps, SearchState> {
|
||||
}
|
||||
}
|
||||
|
||||
Search.contextType = User
|
||||
export default withTracker(Search)
|
||||
|
@ -2,12 +2,20 @@ import React from 'react'
|
||||
import { render } from '@testing-library/react'
|
||||
import Styleguide from './Styleguide'
|
||||
import { MemoryRouter } from 'react-router'
|
||||
import { createMemoryHistory, createLocation } from 'history'
|
||||
|
||||
const history = createMemoryHistory()
|
||||
const location = createLocation('/styleguide')
|
||||
|
||||
describe('Styleguide', () => {
|
||||
it('renders without crashing', () => {
|
||||
const { container } = render(
|
||||
<MemoryRouter>
|
||||
<Styleguide />
|
||||
<Styleguide
|
||||
history={history}
|
||||
location={location}
|
||||
match={{ params: '', path: '', url: '', isExact: true }}
|
||||
/>
|
||||
</MemoryRouter>
|
||||
)
|
||||
expect(container.firstChild).toBeInTheDocument()
|
||||
|
@ -6,6 +6,7 @@ import Route from '../components/templates/Route'
|
||||
import styles from './Styleguide.module.scss'
|
||||
import form from '../data/form-styleguide.json'
|
||||
import Content from '../components/atoms/Content'
|
||||
import withTracker from '../hoc/withTracker'
|
||||
|
||||
class Styleguide extends Component {
|
||||
public formFields = (entries: any[]) =>
|
||||
@ -47,4 +48,4 @@ class Styleguide extends Component {
|
||||
}
|
||||
}
|
||||
|
||||
export default Styleguide
|
||||
export default withTracker(Styleguide)
|
||||
|
@ -140,6 +140,11 @@ em,
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
small {
|
||||
font-size: $font-size-small;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
abbr[title],
|
||||
dfn {
|
||||
text-transform: none;
|
||||
|
@ -1,8 +1,6 @@
|
||||
{
|
||||
"baseUrl": "http://localhost:3000",
|
||||
"env": {
|
||||
"NODE_URI": "https://pacific.oceanprotocol.com",
|
||||
"SEEDPHRASE": "taxi music thumb unique chat sand crew more leg another off lamp",
|
||||
"CONSUME_ASSET": "did:op:3fa4721ad643489bad77e0e52ecefe9b73166873faaf4661a02486b735eea0c8"
|
||||
}
|
||||
}
|
||||
|
@ -3,18 +3,27 @@ context('Faucet', () => {
|
||||
before(() => {
|
||||
cy.visit('/faucet')
|
||||
// Wait for end of loading
|
||||
cy.get('button', { timeout: 60000 }).should('have.length', 1)
|
||||
cy.get('button[name="Faucet"]', { timeout: 60000 }).should(
|
||||
'have.length',
|
||||
1
|
||||
)
|
||||
})
|
||||
|
||||
beforeEach(() => {
|
||||
cy.get('button[name="Faucet"]')
|
||||
.first()
|
||||
.as('button')
|
||||
})
|
||||
|
||||
it('Faucet button is clickable when user is connected.', () => {
|
||||
cy.get('button')
|
||||
cy.get('@button')
|
||||
.contains('Request Ether')
|
||||
.should('not.be.disabled')
|
||||
})
|
||||
|
||||
it('Execute faucet call', () => {
|
||||
// Execute call
|
||||
cy.get('button')
|
||||
cy.get('@button')
|
||||
.contains('Request Ether')
|
||||
.click()
|
||||
// Verify that we got response from server
|
||||
|
@ -1,14 +1,3 @@
|
||||
/// <reference types="Cypress" />
|
||||
import Web3 from 'web3'
|
||||
import HDWalletProvider from 'truffle-hdwallet-provider'
|
||||
|
||||
before(function() {
|
||||
cy.on('window:before:load', win => {
|
||||
const provider = new HDWalletProvider(
|
||||
Cypress.env('SEEDPHRASE'),
|
||||
Cypress.env('NODE_URI')
|
||||
)
|
||||
win.web3 = new Web3(provider)
|
||||
win.ethereum = win.web3
|
||||
})
|
||||
})
|
||||
before(function() {})
|
||||
|
2169
package-lock.json
generated
2169
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -24,17 +24,14 @@
|
||||
"cypress:run": "cypress run --browser chrome",
|
||||
"cypress:open": "cypress open"
|
||||
},
|
||||
"dependencies": {
|
||||
"truffle-hdwallet-provider": "^1.0.10",
|
||||
"web3": "1.0.0-beta.37"
|
||||
},
|
||||
"dependencies": {},
|
||||
"devDependencies": {
|
||||
"@release-it/bumper": "^1.0.3",
|
||||
"@typescript-eslint/eslint-plugin": "^1.11.0",
|
||||
"@typescript-eslint/parser": "^1.11.0",
|
||||
"auto-changelog": "^1.13.0",
|
||||
"concurrently": "^4.1.1",
|
||||
"cypress": "^3.3.1",
|
||||
"cypress": "^3.4.0",
|
||||
"eslint": "^5.16.0",
|
||||
"eslint-config-oceanprotocol": "^1.3.0",
|
||||
"eslint-config-prettier": "^6.0.0",
|
||||
|
43
server/package-lock.json
generated
43
server/package-lock.json
generated
@ -4096,9 +4096,9 @@
|
||||
"dev": true
|
||||
},
|
||||
"mixin-deep": {
|
||||
"version": "1.3.1",
|
||||
"resolved": "https://registry.npmjs.org/mixin-deep/-/mixin-deep-1.3.1.tgz",
|
||||
"integrity": "sha512-8ZItLHeEgaqEvd5lYBXfm4EZSFCX29Jb9K+lAHhDKzReKBQKj3R+7NOF6tjqYi9t4oI8VUfaWITJQm86wnXGNQ==",
|
||||
"version": "1.3.2",
|
||||
"resolved": "https://registry.npmjs.org/mixin-deep/-/mixin-deep-1.3.2.tgz",
|
||||
"integrity": "sha512-WRoDn//mXBiJ1H40rqa3vH0toePwSsGb45iInWlTySa+Uu4k3tYUSxa2v1KqAiLtvlrSzaExqS1gtk96A9zvEA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"for-in": "^1.0.2",
|
||||
@ -5107,9 +5107,9 @@
|
||||
"dev": true
|
||||
},
|
||||
"set-value": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/set-value/-/set-value-2.0.0.tgz",
|
||||
"integrity": "sha512-hw0yxk9GT/Hr5yJEYnHNKYXkIA8mVJgd9ditYZCe16ZczcaELYYcfvaXesNACk2O8O0nTiPQcQhGUQj8JLzeeg==",
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/set-value/-/set-value-2.0.1.tgz",
|
||||
"integrity": "sha512-JxHc1weCN68wRY0fhCoXpyK55m/XPHafOmK4UWD7m2CI14GMcFypt4w/0+NV5f/ZMby2F6S2wwA7fgynh9gWSw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"extend-shallow": "^2.0.1",
|
||||
@ -5856,38 +5856,15 @@
|
||||
}
|
||||
},
|
||||
"union-value": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/union-value/-/union-value-1.0.0.tgz",
|
||||
"integrity": "sha1-XHHDTLW61dzr4+oM0IIHulqhrqQ=",
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/union-value/-/union-value-1.0.1.tgz",
|
||||
"integrity": "sha512-tJfXmxMeWYnczCVs7XAEvIV7ieppALdyepWMkHkwciRpZraG/xwT+s2JN8+pr1+8jCRf80FFzvr+MpQeeoF4Xg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"arr-union": "^3.1.0",
|
||||
"get-value": "^2.0.6",
|
||||
"is-extendable": "^0.1.1",
|
||||
"set-value": "^0.4.3"
|
||||
},
|
||||
"dependencies": {
|
||||
"extend-shallow": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz",
|
||||
"integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"is-extendable": "^0.1.0"
|
||||
}
|
||||
},
|
||||
"set-value": {
|
||||
"version": "0.4.3",
|
||||
"resolved": "https://registry.npmjs.org/set-value/-/set-value-0.4.3.tgz",
|
||||
"integrity": "sha1-fbCPnT0i3H945Trzw79GZuzfzPE=",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"extend-shallow": "^2.0.1",
|
||||
"is-extendable": "^0.1.1",
|
||||
"is-plain-object": "^2.0.1",
|
||||
"to-object-path": "^0.3.0"
|
||||
}
|
||||
}
|
||||
"set-value": "^2.0.1"
|
||||
}
|
||||
},
|
||||
"unique-string": {
|
||||
|
Loading…
Reference in New Issue
Block a user