1
0
mirror of https://github.com/kremalicious/blog.git synced 2024-11-15 01:25:28 +01:00

Merge pull request #79 from kremalicious/feature/web3-loader

nicer web3 loading experience
This commit is contained in:
Matthias Kretschmann 2018-10-30 18:16:22 +01:00 committed by GitHub
commit d70fecda3f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 335 additions and 245 deletions

View File

@ -80,7 +80,7 @@
"@babel/node": "^7.0.0", "@babel/node": "^7.0.0",
"@babel/preset-env": "^7.1.0", "@babel/preset-env": "^7.1.0",
"babel-eslint": "^10.0.1", "babel-eslint": "^10.0.1",
"eslint": "^5.7.0", "eslint": "^5.8.0",
"eslint-config-prettier": "^3.1.0", "eslint-config-prettier": "^3.1.0",
"eslint-loader": "^2.1.1", "eslint-loader": "^2.1.1",
"eslint-plugin-graphql": "^2.1.1", "eslint-plugin-graphql": "^2.1.1",
@ -94,7 +94,7 @@
"prettier": "^1.14.3", "prettier": "^1.14.3",
"prettier-eslint-cli": "^4.7.1", "prettier-eslint-cli": "^4.7.1",
"prettier-stylelint": "^0.4.2", "prettier-stylelint": "^0.4.2",
"stylelint": "^9.6.0", "stylelint": "^9.7.0",
"stylelint-config-css-modules": "^1.3.0", "stylelint-config-css-modules": "^1.3.0",
"stylelint-config-standard": "^18.2.0", "stylelint-config-standard": "^18.2.0",
"stylelint-scss": "^3.3.2" "stylelint-scss": "^3.3.2"

View File

@ -4,7 +4,7 @@
///////////////////////////////////// /////////////////////////////////////
.entryMeta { .entryMeta {
font-size: $font-size-base; font-size: $font-size-small;
margin-top: $spacer * 2; margin-top: $spacer * 2;
color: $brand-grey-light; color: $brand-grey-light;
} }

View File

@ -1,66 +1,59 @@
import React, { Fragment, PureComponent } from 'react' import React, { PureComponent } from 'react'
import PropTypes from 'prop-types' import PropTypes from 'prop-types'
import styles from './Alerts.module.scss' import styles from './Alerts.module.scss'
const Message = ({ message, ...props }) => ( export const alertMessages = (networkName, transactionHash) => ({
<div dangerouslySetInnerHTML={{ __html: message }} {...props} /> noAccount:
) 'Web3 detected, but no account. Are you logged into your MetaMask account?',
noCorrectNetwork: `Please connect to <strong>Main</strong> network. You are on <strong>${networkName}</strong> right now.`,
noWeb3:
'No Web3 detected. Install <a href="https://metamask.io">MetaMask</a>, <a href="https://brave.com">Brave</a>, or <a href="https://github.com/ethereum/mist">Mist</a>.',
transaction: `<a href="https://etherscan.io/tx/${transactionHash}" target="_blank">See your transaction on etherscan.io.</a>`,
waitingForUser: 'Waiting for your confirmation',
waitingConfirmation: 'Waiting for network confirmation, hang on',
success: 'Confirmed. You are awesome, thanks!'
})
export default class Alerts extends PureComponent { export default class Alerts extends PureComponent {
static propTypes = { static propTypes = {
hasCorrectNetwork: PropTypes.bool.isRequired, message: PropTypes.object,
hasAccount: PropTypes.bool.isRequired, transactionHash: PropTypes.string
networkName: PropTypes.string,
error: PropTypes.object,
transactionHash: PropTypes.string,
confirmationNumber: PropTypes.number,
receipt: PropTypes.object,
web3Connected: PropTypes.bool.isRequired
} }
alertMessages = (networkName, transactionHash) => ({ constructMessage = () => {
noAccount: const { transactionHash, message } = this.props
'Web3 detected, but no account. Are you logged into your MetaMask account?',
noCorrectNetwork: `Please connect to <strong>Main</strong> network. You are on <strong>${networkName}</strong> right now.`, let messageOutput
noWeb3:
'No Web3 detected. Install <a href="https://metamask.io">MetaMask</a>, <a href="https://brave.com">Brave</a>, or <a href="https://github.com/ethereum/mist">Mist</a>.', if (transactionHash) {
transaction: `<a href="https://etherscan.io/tx/${transactionHash}" target="_blank">See your transaction on etherscan.io.</a>` messageOutput =
}) message.text +
'<br />' +
alertMessages(null, transactionHash).transaction
} else {
messageOutput = message.text
}
return messageOutput
}
classes() {
const { status } = this.props.message
if (status === 'success') {
return styles.success
} else if (status === 'error') {
return styles.error
}
return styles.alert
}
render() { render() {
const {
hasCorrectNetwork,
hasAccount,
networkName,
error,
transactionHash,
web3Connected
} = this.props
return ( return (
<div className={styles.alert}> <div
{!web3Connected ? ( className={this.classes()}
<Message message={this.alertMessages().noWeb3} /> dangerouslySetInnerHTML={{ __html: this.constructMessage() }}
) : ( />
<Fragment>
{!hasAccount && (
<Message message={this.alertMessages().noAccount} />
)}
{!hasCorrectNetwork && (
<Message
message={this.alertMessages(networkName).noCorrectNetwork}
/>
)}
{error && <Message message={error.message} />}
{transactionHash && (
<Message
message={this.alertMessages(null, transactionHash).transaction}
/>
)}
</Fragment>
)}
</div>
) )
} }
} }

View File

@ -2,12 +2,44 @@
@import 'mixins'; @import 'mixins';
.alert { .alert {
margin-top: $spacer / 2;
font-size: $font-size-small; font-size: $font-size-small;
display: inline-block;
&:empty {
display: none;
}
&::after {
overflow: hidden;
display: inline-block;
vertical-align: bottom;
animation: ellipsis steps(4, end) 1s infinite;
content: '\2026'; // ascii code for the ellipsis character
width: 0;
position: absolute;
}
}
.error {
composes: alert;
color: darken($alert-error, 60%); color: darken($alert-error, 60%);
&::after {
display: none;
}
} }
.success { .success {
composes: alert; composes: alert;
color: darken($alert-success, 60%); color: darken($alert-success, 60%);
&::after {
display: none;
}
}
@keyframes ellipsis {
to {
width: .75rem;
}
} }

View File

@ -7,21 +7,17 @@ import styles from './InputGroup.module.scss'
export default class InputGroup extends PureComponent { export default class InputGroup extends PureComponent {
static propTypes = { static propTypes = {
hasCorrectNetwork: PropTypes.bool.isRequired,
hasAccount: PropTypes.bool.isRequired,
amount: PropTypes.string.isRequired, amount: PropTypes.string.isRequired,
onAmountChange: PropTypes.func.isRequired, onAmountChange: PropTypes.func.isRequired,
handleButton: PropTypes.func.isRequired, sendTransaction: PropTypes.func.isRequired,
selectedAccount: PropTypes.string selectedAccount: PropTypes.string
} }
render() { render() {
const { const {
hasCorrectNetwork,
hasAccount,
amount, amount,
onAmountChange, onAmountChange,
handleButton, sendTransaction,
selectedAccount selectedAccount
} = this.props } = this.props
@ -30,7 +26,6 @@ export default class InputGroup extends PureComponent {
<div className={styles.input}> <div className={styles.input}>
<Input <Input
type="number" type="number"
disabled={!hasCorrectNetwork || !hasAccount}
value={amount} value={amount}
onChange={onAmountChange} onChange={onAmountChange}
min="0" min="0"
@ -40,20 +35,13 @@ export default class InputGroup extends PureComponent {
<span>ETH</span> <span>ETH</span>
</div> </div>
</div> </div>
<button <button className="btn btn-primary" onClick={sendTransaction}>
className="btn btn-primary"
onClick={handleButton}
disabled={!hasCorrectNetwork || !hasAccount}
>
Make it rain Make it rain
</button> </button>
{hasCorrectNetwork && <div className={styles.infoline}>
hasAccount && ( <Conversion amount={amount} />
<div className={styles.infoline}> {selectedAccount && <Account account={selectedAccount} />}
<Conversion amount={amount} /> </div>
{selectedAccount && <Account account={selectedAccount} />}
</div>
)}
</div> </div>
) )
} }

View File

@ -5,6 +5,7 @@
max-width: 18rem; max-width: 18rem;
margin: auto; margin: auto;
position: relative; position: relative;
animation: fadeIn .8s ease-out backwards;
@media (min-width: $screen-sm) { @media (min-width: $screen-sm) {
display: flex; display: flex;
@ -79,4 +80,19 @@
align-items: center; align-items: center;
justify-content: center; justify-content: center;
margin-top: $spacer / 4; margin-top: $spacer / 4;
animation: fadeIn .5s .8s ease-out backwards;
}
.message {
composes: message from './index.module.scss';
}
@keyframes fadeIn {
from {
opacity: .01;
}
to {
opacity: 1;
}
} }

View File

@ -1,27 +1,25 @@
import React, { PureComponent } from 'react' import React, { PureComponent } from 'react'
import PropTypes from 'prop-types' import PropTypes from 'prop-types'
import Web3 from 'web3'
import InputGroup from './InputGroup' import InputGroup from './InputGroup'
import Alerts from './Alerts' import Alerts, { alertMessages } from './Alerts'
import styles from './index.module.scss' import styles from './index.module.scss'
import { getNetworkName, Logger } from './utils' import { getWeb3, getAccounts, getNetwork } from './utils'
const ONE_SECOND = 1000 const ONE_SECOND = 1000
const ONE_MINUTE = ONE_SECOND * 60 const ONE_MINUTE = ONE_SECOND * 60
const correctNetwork = 1
export default class Web3Donation extends PureComponent { export default class Web3Donation extends PureComponent {
state = { state = {
web3Connected: false, netId: null,
networkId: null,
networkName: null, networkName: null,
accounts: [], accounts: [],
selectedAccount: null, selectedAccount: null,
amount: '0.01', amount: '0.01',
transactionHash: null, transactionHash: null,
receipt: null, receipt: null,
loading: false, message: null,
error: null, inTransaction: false
message: 'Hang on...'
} }
static propTypes = { static propTypes = {
@ -40,39 +38,26 @@ export default class Web3Donation extends PureComponent {
this.resetAllTheThings() this.resetAllTheThings()
} }
async initWeb3() { initWeb3 = async () => {
// Modern dapp browsers... this.setState({ message: { text: 'Checking' } })
if (window.ethereum) {
this.web3 = new Web3(window.ethereum)
try { try {
// Request account access this.web3 = await getWeb3()
await window.ethereum.enable()
this.setState({ web3Connected: true })
this.initAllTheTings() this.web3
} catch (error) { ? this.initAllTheTings()
// User denied account access... : this.setState({
Logger.error(error) message: { status: 'error', text: alertMessages().noWeb3 }
this.setState({ error }) })
} } catch (error) {
} this.setState({ message: { status: 'error', text: error } })
// Legacy dapp browsers...
else if (window.web3) {
this.web3 = new Web3(window.web3.currentProvider)
this.setState({ web3Connected: true })
this.initAllTheTings()
}
// Non-dapp browsers...
else {
this.setState({ web3Connected: false })
} }
} }
initAllTheTings() { async initAllTheTings() {
this.fetchAccounts() this.fetchAccounts()
this.fetchNetwork() this.fetchNetwork()
this.initAccountsPoll() this.initAccountsPoll()
this.initNetworkPoll() this.initNetworkPoll()
} }
@ -80,7 +65,6 @@ export default class Web3Donation extends PureComponent {
resetAllTheThings() { resetAllTheThings() {
clearInterval(this.interval) clearInterval(this.interval)
clearInterval(this.networkInterval) clearInterval(this.networkInterval)
this.setState({ web3Connected: false })
} }
initAccountsPoll() { initAccountsPoll() {
@ -95,46 +79,46 @@ export default class Web3Donation extends PureComponent {
} }
} }
fetchNetwork = () => { fetchNetwork = async () => {
const { web3 } = this const { web3 } = this
const { netId, networkName } = await getNetwork(web3)
web3 && if (netId === correctNetwork) {
web3.eth && this.setState({ netId, networkName })
web3.eth.net.getId((err, netId) => { } else {
if (err) this.setState({ error: err }) this.setState({
message: {
if (netId !== this.state.networkId) { status: 'error',
this.setState({ text: alertMessages(networkName).noCorrectNetwork
error: null,
networkId: netId
})
getNetworkName(netId).then(networkName => {
this.setState({ networkName })
})
} }
}) })
}
} }
fetchAccounts = () => { fetchAccounts = async () => {
const { web3 } = this const { web3 } = this
const accounts = await getAccounts(web3)
web3 && if (accounts[0]) {
web3.eth && this.setState({
web3.eth.getAccounts((err, accounts) => { accounts,
if (err) this.setState({ error: err }) selectedAccount: accounts[0].toLowerCase()
this.setState({
error: null,
accounts,
selectedAccount: accounts[0].toLowerCase()
})
}) })
} else {
this.setState({
message: { status: 'error', text: alertMessages().noAccount }
})
}
} }
sendTransaction() { sendTransaction = () => {
const { web3 } = this const { web3 } = this
this.setState({
inTransaction: true,
message: { text: alertMessages().waitingForUser }
})
web3.eth web3.eth
.sendTransaction({ .sendTransaction({
from: this.state.selectedAccount, from: this.state.selectedAccount,
@ -144,46 +128,32 @@ export default class Web3Donation extends PureComponent {
.once('transactionHash', transactionHash => { .once('transactionHash', transactionHash => {
this.setState({ this.setState({
transactionHash, transactionHash,
message: 'Waiting for network confirmation, hang on...' message: { text: alertMessages().waitingConfirmation }
}) })
}) })
.on('error', error => this.setState({ error, loading: false })) .on('error', error =>
this.setState({ message: { status: 'error', text: error } })
)
.then(() => { .then(() => {
this.setState({ message: 'Confirmed. You are awesome, thanks!' }) this.setState({
message: { status: 'success', text: alertMessages().success }
})
}) })
} }
handleButton = () => {
this.setState({
loading: true,
message: 'Waiting for your confirmation...'
})
this.sendTransaction()
}
onAmountChange = ({ target }) => { onAmountChange = ({ target }) => {
this.setState({ amount: target.value }) this.setState({ amount: target.value })
} }
render() { render() {
const { const {
networkId,
accounts,
selectedAccount, selectedAccount,
web3Connected,
loading,
amount, amount,
networkName,
error,
transactionHash, transactionHash,
confirmationNumber, message,
message inTransaction
} = this.state } = this.state
const hasCorrectNetwork = networkId === 1
const hasAccount = accounts.length !== 0
return ( return (
<div className={styles.web3}> <div className={styles.web3}>
<header> <header>
@ -191,32 +161,22 @@ export default class Web3Donation extends PureComponent {
<p>Send Ether with MetaMask, Brave, or Mist.</p> <p>Send Ether with MetaMask, Brave, or Mist.</p>
</header> </header>
{web3Connected && ( <div className={styles.web3Row}>
<div className={styles.web3Row}> {selectedAccount &&
{loading ? ( this.state.netId === correctNetwork &&
message !inTransaction ? (
) : ( <InputGroup
<InputGroup selectedAccount={selectedAccount}
hasCorrectNetwork={hasCorrectNetwork} amount={amount}
hasAccount={hasAccount} onAmountChange={this.onAmountChange}
selectedAccount={selectedAccount} sendTransaction={this.sendTransaction}
amount={amount} />
onAmountChange={this.onAmountChange} ) : (
handleButton={this.handleButton} message && (
/> <Alerts message={message} transactionHash={transactionHash} />
)} )
</div> )}
)} </div>
<Alerts
hasCorrectNetwork={hasCorrectNetwork}
hasAccount={hasAccount}
networkName={networkName}
error={error}
transactionHash={transactionHash}
web3Connected={web3Connected}
confirmationNumber={confirmationNumber}
/>
</div> </div>
) )
} }

View File

@ -18,8 +18,44 @@
} }
.web3Row { .web3Row {
min-height: 58px; min-height: 77px;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
&:empty {
display: none;
}
}
.message {
font-size: $font-size-small;
position: relative;
&::after {
overflow: hidden;
display: inline-block;
vertical-align: bottom;
animation: ellipsis steps(4, end) 1s infinite;
content: '\2026'; // ascii code for the ellipsis character
width: 0;
position: absolute;
left: 100%;
bottom: 0;
}
}
.success {
composes: message;
color: green;
&::after {
display: none;
}
}
@keyframes ellipsis {
to {
width: .75rem;
}
} }

View File

@ -1,4 +1,49 @@
export const getNetworkName = async netId => { import Web3 from 'web3'
export const getWeb3 = async () => {
let web3
// Modern dapp browsers...
if (window.ethereum) {
web3 = new Web3(window.ethereum)
try {
// Request account access
await window.ethereum.enable()
return web3
} catch (error) {
// User denied account access...
Logger.error(error)
return error
}
}
// Legacy dapp browsers...
else if (window.web3) {
web3 = new Web3(window.web3.currentProvider)
return web3
}
// Non-dapp browsers...
else {
return
}
}
export const getAccounts = async web3 => {
const ethAccounts = await web3.eth.getAccounts()
return ethAccounts
}
export const getNetwork = async web3 => {
const netId = await web3.eth.net.getId()
const networkName = getNetworkName(netId)
return { netId, networkName }
}
export const getNetworkName = netId => {
let networkName let networkName
switch (netId) { switch (netId) {
@ -29,9 +74,7 @@ export const getFiat = async amount => {
try { try {
const response = await fetch(url) const response = await fetch(url)
if (!response.ok) { if (!response.ok) Logger.error(response.statusText)
throw Error(response.statusText)
}
const data = await response.json() const data = await response.json()
const { price_usd, price_eur } = data[0] const { price_usd, price_eur } = data[0]
const dollar = (amount * price_usd).toFixed(2) const dollar = (amount * price_usd).toFixed(2)

View File

@ -33,9 +33,8 @@ export default class CoinHiveClient extends PureComponent {
return new Promise(resolve => { return new Promise(resolve => {
loadScript(config.script, error => { loadScript(config.script, error => {
if (error) { if (error) return
return
}
resolve( resolve(
window.CoinHive.Anonymous(config.siteKey, { window.CoinHive.Anonymous(config.siteKey, {
throttle: config.throttle, throttle: config.throttle,

View File

@ -8,7 +8,7 @@
left: 0; left: 0;
right: 0; right: 0;
bottom: 0; bottom: 0;
z-index: 10; z-index: 9;
background: rgba($body-background-color, .9); background: rgba($body-background-color, .9);
// backdrop-filter: blur(5px); // backdrop-filter: blur(5px);
animation: fadein .3s; animation: fadein .3s;

View File

@ -1,11 +1,17 @@
import React, { Fragment } from 'react' import React from 'react'
import PropTypes from 'prop-types' import PropTypes from 'prop-types'
import { QRCode } from 'react-qr-svg' import { QRCode } from 'react-qr-svg'
import Clipboard from 'react-clipboard.js' import Clipboard from 'react-clipboard.js'
import { ReactComponent as IconClipboard } from '../../images/clipboard.svg' import { ReactComponent as IconClipboard } from '../../images/clipboard.svg'
import styles from './Qr.module.scss'
const onCopySuccess = e => {
e.trigger.classList.add(styles.copied)
}
const Qr = ({ address, title }) => ( const Qr = ({ address, title }) => (
<Fragment> <>
{title && <h4>{title}</h4>} {title && <h4>{title}</h4>}
<QRCode <QRCode
bgColor="transparent" bgColor="transparent"
@ -13,14 +19,21 @@ const Qr = ({ address, title }) => (
level="Q" level="Q"
style={{ width: 120 }} style={{ width: 120 }}
value={address} value={address}
className={styles.qr}
/> />
<pre>
<pre className={styles.code}>
<code>{address}</code> <code>{address}</code>
<Clipboard data-clipboard-text={address} button-title="Copy to clipboard"> <Clipboard
data-clipboard-text={address}
button-title="Copy to clipboard"
onSuccess={e => onCopySuccess(e)}
className={styles.button}
>
<IconClipboard /> <IconClipboard />
</Clipboard> </Clipboard>
</pre> </pre>
</Fragment> </>
) )
Qr.propTypes = { Qr.propTypes = {

View File

@ -0,0 +1,54 @@
@import 'variables';
.qr {
margin-bottom: $spacer / 2;
}
.code {
margin: 0;
position: relative;
padding: 0;
padding-right: 2rem;
code {
padding: $spacer / 2;
font-size: .65rem;
text-align: center;
}
}
.button {
margin: 0;
position: absolute;
right: 0;
top: 0;
bottom: 0;
border: 0;
box-shadow: none;
border-top-left-radius: 0;
border-bottom-left-radius: 0;
background: rgba($brand-grey, .3);
padding: $spacer / 3;
svg {
width: 1rem;
height: 1rem;
fill: $brand-grey-light;
transition: .15s ease-out;
}
&:hover {
svg {
fill: $brand-grey-dimmed;
}
}
}
.copied {
background: green;
// stylelint-disable-next-line no-descending-specificity
svg {
fill: $brand-grey-dimmed;
}
}

View File

@ -37,7 +37,7 @@ class ModalThanks extends PureComponent {
<Web3Donation address={author.ether} /> <Web3Donation address={author.ether} />
<header> <header>
<h4>Other wallets</h4> <h4>Any other wallets</h4>
<p>Send Bitcoin or Ether from any wallet.</p> <p>Send Bitcoin or Ether from any wallet.</p>
</header> </header>

View File

@ -39,48 +39,4 @@
width: 48%; width: 48%;
margin-top: 0; margin-top: 0;
} }
> svg {
margin-bottom: $spacer / 2;
}
pre {
margin: 0;
position: relative;
padding: 0;
padding-right: 2rem;
code {
padding: $spacer / 2;
font-size: .65rem;
text-align: center;
}
}
button {
margin: 0;
position: absolute;
right: 0;
top: 0;
bottom: 0;
border: 0;
box-shadow: none;
border-top-left-radius: 0;
border-bottom-left-radius: 0;
background: rgba($brand-grey, .3);
padding: $spacer / 3;
svg {
width: 1rem;
height: 1rem;
fill: $brand-grey;
transition: .15s ease-out;
}
&:hover {
svg {
fill: $brand-grey-dimmed;
}
}
}
} }