From 568d4c0a1b96dda66c85d9b5f764775f7592f530 Mon Sep 17 00:00:00 2001 From: Franco Victorio Date: Tue, 27 Mar 2018 16:37:46 -0300 Subject: [PATCH 01/48] WIP --- package-lock.json | 10 + package.json | 2 + src/components/stepThree/index.js | 446 +++++++++++++++++++++++------- src/stores/index.js | 16 ++ 4 files changed, 381 insertions(+), 93 deletions(-) diff --git a/package-lock.json b/package-lock.json index fdf7739a9..1ef811e24 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4832,6 +4832,11 @@ "resolved": "https://registry.npmjs.org/final-form/-/final-form-4.3.1.tgz", "integrity": "sha512-e2DUWimulmCExhaqJaOi/vAVYZTVAlMncwGtpFhZX/uygFuUzRLIHHa7uo9Et0X5ZqTCWWDdTa94U/hosnYiqQ==" }, + "final-form-arrays": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/final-form-arrays/-/final-form-arrays-1.0.4.tgz", + "integrity": "sha512-xFzOQ21zQV4oezdX35WKfDPVW0OzgX9GpBk+obAsb+BnR1CVQp8qOoVyNt7u/ddUUzzgq1F6uNnHF6i1azHeBQ==" + }, "finalhandler": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.0.tgz", @@ -12800,6 +12805,11 @@ "resolved": "https://registry.npmjs.org/react-final-form/-/react-final-form-3.1.4.tgz", "integrity": "sha512-L6bYwKPixKkpoSEd8V/WslNjB7g57ed3g1HD0hsP/4P1rUCCIn9CuHmGjAA2D6WNYhhdg1qIdwNsacB0PGknWA==" }, + "react-final-form-arrays": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/react-final-form-arrays/-/react-final-form-arrays-1.0.4.tgz", + "integrity": "sha512-ZnCze9b5RCXAI2vWeguPSss9Q0Rh3x4UO2rZ3MubyMWWa7MPg64VdD4G+PzYD/9A+TizBR+40xS7bTg2ATQmXQ==" + }, "react-html-attributes": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/react-html-attributes/-/react-html-attributes-1.4.1.tgz", diff --git a/package.json b/package.json index d2cc10393..b6342c486 100755 --- a/package.json +++ b/package.json @@ -64,6 +64,7 @@ "extract-text-webpack-plugin": "2.1.2", "file-loader": "0.11.2", "final-form": "^4.3.1", + "final-form-arrays": "^1.0.4", "font-awesome": "^4.7.0", "fs-extra": "3.0.1", "html-webpack-plugin": "2.29.0", @@ -93,6 +94,7 @@ "react-dropzone": "^4.2.8", "react-error-overlay": "^1.0.9", "react-final-form": "^3.1.4", + "react-final-form-arrays": "^1.0.4", "react-router-dom": "^4.1.2", "react-tooltip": "^3.4.0", "solc": "^0.4.14", diff --git a/src/components/stepThree/index.js b/src/components/stepThree/index.js index 846a126b7..8dcb1da8f 100644 --- a/src/components/stepThree/index.js +++ b/src/components/stepThree/index.js @@ -1,17 +1,24 @@ import React from "react"; import "../../assets/stylesheets/application.css"; +import { Field, Form, FormSpy } from 'react-final-form' +import arrayMutators from 'final-form-arrays' +import { FieldArray } from 'react-final-form-arrays' import { Link } from "react-router-dom"; import { setExistingContractParams, getNetworkVersion, getNetWorkNameById } from "../../utils/blockchainHelpers"; import { gweiToWei, weiToGwei } from "../../utils/utils"; import { StepNavigation } from "../Common/StepNavigation"; import { RadioInputField } from "../Common/RadioInputField"; +import { InputField2 } from "../Common/InputField2"; import { CrowdsaleBlock } from "./CrowdsaleBlock"; +import { WhitelistInputBlock } from '../Common/WhitelistInputBlock' +import { defaultCompanyStartDate, defaultCompanyEndDate } from './utils' import { NAVIGATION_STEPS, VALIDATION_MESSAGES, VALIDATION_TYPES, TEXT_FIELDS, CHAINS, + DESCRIPTION, defaultTier, defaultTierValidations } from '../../utils/constants' @@ -24,7 +31,17 @@ import { AddressInput } from '../Common/AddressInput' const { CROWDSALE_SETUP } = NAVIGATION_STEPS; const { VALID, INVALID } = VALIDATION_TYPES; -const { MINCAP, WALLET_ADDRESS, ENABLE_WHITELISTING } = TEXT_FIELDS; +const { + ALLOWMODIFYING, + CROWDSALE_SETUP_NAME, + MINCAP, + WALLET_ADDRESS, + ENABLE_WHITELISTING, + START_TIME, + END_TIME, + RATE, + SUPPLY +} = TEXT_FIELDS; @inject( "contractStore", @@ -70,24 +87,27 @@ export class stepThree extends React.Component { } } - componentDidMount () { - const { gasPriceStore, tierStore } = this.props + componentWillMount () { + // const { gasPriceStore, tierStore } = this.props + if (this.props.tierStore.tiers.length === 0) { + this.addCrowdsale() + this.initialTiers = JSON.parse(JSON.stringify(this.props.tierStore.tiers)) + this.initialTiers[0].startTime = defaultCompanyStartDate() + this.initialTiers[0].endTime = defaultCompanyEndDate() + } - gasPriceStore.updateValues() - .then(() => this.setGasPrice(gasPriceStore.slow)) - .catch(() => noGasPriceAvailable()) - .then(() => { - if (this.props.tierStore.tiers.length === 0) { - this.addCrowdsale() - } - this.setState({ loading: false }) - this.updateWalletAddress({ - address: tierStore.tiers[0].walletAddress, - pristine: true, - valid: VALID, - }) - window.scrollTo(0, 0) - }) + // gasPriceStore.updateValues() + // .then(() => this.setGasPrice(gasPriceStore.slow)) + // .catch(() => noGasPriceAvailable()) + // .then(() => { + // this.setState({ loading: false }) + // this.updateWalletAddress({ + // address: tierStore.tiers[0].walletAddress, + // pristine: true, + // valid: VALID, + // }) + // window.scrollTo(0, 0) + // }) } showErrorMessages = () => { @@ -126,10 +146,7 @@ export class stepThree extends React.Component { this.props.history.push('/4') } - beforeNavigate = e => { - e.preventDefault() - e.stopPropagation() - + beforeNavigate = () => { const { tierStore, gasPriceStore } = this.props const gasPriceIsValid = gasPriceStore.custom.id !== this.state.gasPriceSelected || this.state.validation.gasPrice.valid === VALID const isMinCapLessThanMaxSupply = tierStore.globalMinCap <= tierStore.maxSupply @@ -179,10 +196,7 @@ export class stepThree extends React.Component { }) .catch(error => { console.error(error) - this.showErrorMessages(e) }) - } else { - this.showErrorMessages(e) } } @@ -329,79 +343,325 @@ export class stepThree extends React.Component { } render() { - const { tierStore } = this.props; - - const globalSettingsBlock = ( -
-
-

Global settings

-
-
- - {this.renderGasPriceInput()} -
-
- - this.updateWhitelistEnabled(e)} - description="Enables whitelisting. If disabled, anyone can participate in the crowdsale." - /> -
-
- ) + const { generalStore, tierStore } = this.props return (
-
-
-
-

Crowdsale setup

-

The most important and exciting part of the crowdsale process. Here you can - define parameters of your crowdsale campaign.

-
- {globalSettingsBlock} -
- -
- { tierStore.tiers.map((tier, index) => ) } -
- -
-
this.addCrowdsale()} className="button button_fill_secondary">Add Tier
- this.beforeNavigate(e)} className="button button_fill" to="/4">Continue -
- - +
{ + return ( + +
+
+
+
+

Crowdsale setup

+

The most important and exciting part of the crowdsale process. Here you can + define parameters of your crowdsale campaign.

+
+
+

Global settings

+
+
+ value ? undefined : 'Required field'} + description="Where the money goes after investors transactions. Immediately after each transaction. We + recommend to setup a multisig wallet with hardware based signers." + /> + + (value && value > 0) ? undefined : 'Required field'} + description="Slow is cheap, fast is expensive" + /> +
+
+ (value >= 0) ? undefined : 'Required field'} + description="Minimum amount of tokens to buy. Not the minimal amount for every transaction: if minCap is 1 and a user already has 1 token from a previous transaction, they can buy any amount they want." + /> + ( +
+ +
+ + +
+

Enables whitelisting. If disabled, anyone can participate in the crowdsale.

+
+ )} + /> +
+
+
+ + + {({ fields }) => ( +
+ {fields.map((name, index) => ( +
+
+
+ + ( +
+ +
+ + +
+

{DESCRIPTION.ALLOW_MODIFYING}

+
+ )} + /> +
+ +
+ + +
+ +
+ + +
+
+ {/* + +
+ + +
+ + { + tierStore.tiers[index].whitelistEnabled === 'yes' ? ( +
+
+

Whitelist

+
+ +
+ ) : null + } + */} +
+ ))} + // FVTODO use button-container in submit too +
+
{ + this.addCrowdsale() + const lastTier = this.props.tierStore.tiers[this.props.tierStore.tiers.length - 1] + fields.push(JSON.parse(JSON.stringify(lastTier))) + }}> + Add Tier +
+
+
+ )} +
+ + + + { + tierStore.updateWalletAddress(values.walletAddress, VALID) + generalStore.setGasPrice(gweiToWei(values.gasPrice || 0)) + tierStore.setGlobalMinCap(values.minCap || 0) + tierStore.setTierProperty(values.whitelistEnabled, "whitelistEnabled", 0) + + values.tiers.forEach((tier, index) => { + tierStore.setTierProperty(tier.tier, 'tier', index) + tierStore.setTierProperty(tier.updatable, 'updatable', index) + tierStore.setTierProperty(tier.startTime, 'startTime', index) + tierStore.setTierProperty(tier.endTime, 'endTime', index) + tierStore.updateRate(tier.rate, VALID, index) + tierStore.setTierProperty(tier.supply, 'supply', index) + tierStore.validateTiers('supply', index) + }) + }} + /> + + ) + }} + />
) } + + // render() { + // const { tierStore } = this.props; + + // const globalSettingsBlock = ( + //
+ //
+ //

Global settings

+ //
+ //
+ // + // {this.renderGasPriceInput()} + //
+ //
+ // + // this.updateWhitelistEnabled(e)} + // description="Enables whitelisting. If disabled, anyone can participate in the crowdsale." + // /> + //
+ //
+ // ) + + // return ( + //
+ // + //
+ //
+ //
+ //

Crowdsale setup

+ //

The most important and exciting part of the crowdsale process. Here you can + // define parameters of your crowdsale campaign.

+ //
+ // {globalSettingsBlock} + //
+ + //
+ // { tierStore.tiers.map((tier, index) => ) } + //
+ + //
+ //
this.addCrowdsale()} className="button button_fill_secondary">Add Tier
+ // this.beforeNavigate(e)} className="button button_fill" to="/4">Continue + //
+ + // + //
+ // ) + // } } diff --git a/src/stores/index.js b/src/stores/index.js index cdf177208..78241edfd 100644 --- a/src/stores/index.js +++ b/src/stores/index.js @@ -47,3 +47,19 @@ export { gasPriceStore, deploymentStore }; + +window.stores = { // DONTCOMMIT + generalStore, + crowdsalePageStore, + contractStore, + pricingStrategyStore, + reservedTokenStore, + stepTwoValidationStore, + tierStore, + tokenStore, + web3Store, + investStore, + crowdsaleStore, + gasPriceStore, + deploymentStore +}; From 488f8e082de857eebce55eca489371225ce727d8 Mon Sep 17 00:00:00 2001 From: Franco Victorio Date: Wed, 28 Mar 2018 09:32:49 -0300 Subject: [PATCH 02/48] Migrate step three form to final-form --- package-lock.json | 10 + package.json | 2 + src/assets/stylesheets/application.css | 2 +- .../stylesheets/application/controls.scss | 4 +- src/components/stepThree/index.js | 453 ++++++++++++++---- src/utils/validations.js | 19 + 6 files changed, 395 insertions(+), 95 deletions(-) diff --git a/package-lock.json b/package-lock.json index fdf7739a9..1ef811e24 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4832,6 +4832,11 @@ "resolved": "https://registry.npmjs.org/final-form/-/final-form-4.3.1.tgz", "integrity": "sha512-e2DUWimulmCExhaqJaOi/vAVYZTVAlMncwGtpFhZX/uygFuUzRLIHHa7uo9Et0X5ZqTCWWDdTa94U/hosnYiqQ==" }, + "final-form-arrays": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/final-form-arrays/-/final-form-arrays-1.0.4.tgz", + "integrity": "sha512-xFzOQ21zQV4oezdX35WKfDPVW0OzgX9GpBk+obAsb+BnR1CVQp8qOoVyNt7u/ddUUzzgq1F6uNnHF6i1azHeBQ==" + }, "finalhandler": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.0.tgz", @@ -12800,6 +12805,11 @@ "resolved": "https://registry.npmjs.org/react-final-form/-/react-final-form-3.1.4.tgz", "integrity": "sha512-L6bYwKPixKkpoSEd8V/WslNjB7g57ed3g1HD0hsP/4P1rUCCIn9CuHmGjAA2D6WNYhhdg1qIdwNsacB0PGknWA==" }, + "react-final-form-arrays": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/react-final-form-arrays/-/react-final-form-arrays-1.0.4.tgz", + "integrity": "sha512-ZnCze9b5RCXAI2vWeguPSss9Q0Rh3x4UO2rZ3MubyMWWa7MPg64VdD4G+PzYD/9A+TizBR+40xS7bTg2ATQmXQ==" + }, "react-html-attributes": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/react-html-attributes/-/react-html-attributes-1.4.1.tgz", diff --git a/package.json b/package.json index d2cc10393..b6342c486 100755 --- a/package.json +++ b/package.json @@ -64,6 +64,7 @@ "extract-text-webpack-plugin": "2.1.2", "file-loader": "0.11.2", "final-form": "^4.3.1", + "final-form-arrays": "^1.0.4", "font-awesome": "^4.7.0", "fs-extra": "3.0.1", "html-webpack-plugin": "2.29.0", @@ -93,6 +94,7 @@ "react-dropzone": "^4.2.8", "react-error-overlay": "^1.0.9", "react-final-form": "^3.1.4", + "react-final-form-arrays": "^1.0.4", "react-router-dom": "^4.1.2", "react-tooltip": "^3.4.0", "solc": "^0.4.14", diff --git a/src/assets/stylesheets/application.css b/src/assets/stylesheets/application.css index 7164eee89..685002daa 100644 --- a/src/assets/stylesheets/application.css +++ b/src/assets/stylesheets/application.css @@ -1 +1 @@ -.header,.footer,.crowdsale,.process{left:0;right:0}.header .logo,.footer .logo{display:block;background-image:url(../images/logos.png)}@media(min--moz-device-pixel-ratio:1.3),(-webkit-min-device-pixel-ratio:1.3),(min-device-pixel-ratio:1.3),(min-resolution:1.3dppx){.header .logo,.footer .logo{background-image:url("../images/logos@2x.png");background-size:182px 59px}}@font-face{font-family:'Open Sans';font-style:normal;font-weight:400;src:local("Open Sans"),local("OpenSans"),url(https://fonts.gstatic.com/s/opensans/v13/cJZKeOuBrn4kERxqtaUH3ZBw1xU1rKptJj_0jans920.woff2) format("woff2");unicode-range:U+0000-00FF,U+0131,U+0152-0153,U+02C6,U+02DA,U+02DC,U+2000-206F,U+2074,U+20AC,U+2212,U+2215,U+E0FF,U+EFFD,U+F000}@font-face{font-family:'Open Sans';font-style:normal;font-weight:700;src:local("Open Sans Bold"),local("OpenSans-Bold"),url(https://fonts.gstatic.com/s/opensans/v13/k3k702ZOKiLJc3WVjuplzBampu5_7CjHW5spxoeN3Vs.woff2) format("woff2");unicode-range:U+0000-00FF,U+0131,U+0152-0153,U+02C6,U+02DA,U+02DC,U+2000-206F,U+2074,U+20AC,U+2212,U+2215,U+E0FF,U+EFFD,U+F000}html,body{color:#333;line-height:1;font-size:14px;font-family:'Open Sans',sans-serif;-webkit-font-smoothing:antialiased}html,body{margin:0;padding:0}p,h1,h2,h3,h4{margin:0;padding:0;font-family:'Open Sans',sans-serif}html{height:100%}body{position:relative;width:100%;min-width:1000px;min-height:100%;box-sizing:border-box;padding:80px 0 60px;background-color:#fbfbfc}.container{width:960px;margin:0 auto;box-sizing:border-box}.hidden{overflow:hidden}.notdisplayed{display:none}.left{width:46%;float:left}.right{width:46%;float:right}.item-remove{background-image:url(../images/delete.png);background-repeat:no-repeat;display:block;width:12px;height:12px;cursor:pointer;float:right}@media(min--moz-device-pixel-ratio:1.3),(-webkit-min-device-pixel-ratio:1.3),(min-device-pixel-ratio:1.3),(min-resolution:1.3dppx){.item-remove{background-image:url("../images/delete@2x.png");background-size:12px 12px}}.copy{background-image:url(../images/copy.png);background-repeat:no-repeat;display:block;width:12px;height:12px;cursor:pointer;float:right}@media(min--moz-device-pixel-ratio:1.3),(-webkit-min-device-pixel-ratio:1.3),(min-device-pixel-ratio:1.3),(min-resolution:1.3dppx){.copy{background-image:url("../images/copy@2x.png");background-size:12px 12px}}.copy-field-container{display:table-cell;padding-left:10px;vertical-align:top;padding-top:30.5px}.copy-area-container{display:table-cell;padding-left:10px}.display-container{display:table-cell;width:100%}.input-block-container{display:table;width:100%}.section-title{display:block;margin-bottom:30px;text-transform:uppercase;font-size:20px;font-weight:bold}@media(max-height:600px){body{padding-top:0}}.header{position:absolute;top:0;height:80px;background-image:url(../images/bg.png);background-size:cover;background-position:center center}.header .logo{width:182px;height:35px;margin-top:22.5px;background-position:0 -25px}@media(max-height:600px){.header{position:relative;top:0;height:auto;padding:1px 0 21px}}.footer{position:absolute;bottom:0;height:60px;background-image:url(../images/bg.png);background-size:cover;background-position:center center;color:#fff}.footer .container{position:relative}.footer .logo,.footer .socials{-webkit-transform:translateY(-50%);transform:translateY(-50%);position:absolute;z-index:1;top:50%}.footer .logo{left:0;width:126px;height:24px;background-position:0 0}.footer .rights{color:#fff;line-height:60px;text-align:center;font-size:12px}.footer .socials{right:0}.socials{font-size:0}.socials .social{transition:.3s background-color;position:relative;display:inline-block;vertical-align:top;width:30px;height:30px;border-radius:50%;background-color:rgba(255,255,255,0.2)}.socials .social:not(:first-child){margin-left:10px}.socials .social:hover{background-color:rgba(255,255,255,0.4)}.socials .social:before{-webkit-transform:translate(-50%,-50%);transform:translate(-50%,-50%);content:'';position:absolute;left:50%;top:50%;background-image:url(../images/socials.png)}@media(min--moz-device-pixel-ratio:1.3),(-webkit-min-device-pixel-ratio:1.3),(min-device-pixel-ratio:1.3),(min-resolution:1.3dppx){.socials .social:before{background-image:url("../images/socials@2x.png");background-size:16px 69px}}.socials .social_github:before{width:16px;height:16px;background-position:0 0}.socials .social_oracles:before{width:16px;height:14px;background-position:0 -16px}.socials .social_reddit:before{width:15px;height:13px;background-position:0 -30px}.socials .social_telegram:before{width:16px;height:14px;background-position:0 -43px}.socials .social_twitter:before{width:15px;height:12px;background-position:0 -57px}.step-icons{background-image:url(../images/step-icons.png)}@media(min--moz-device-pixel-ratio:1.3),(-webkit-min-device-pixel-ratio:1.3),(min-device-pixel-ratio:1.3),(min-resolution:1.3dppx){.step-icons{background-image:url("../images/step-icons@2x.png");background-size:100px 480px}}.step-icons_crowdsale-contract{width:80px;height:100px;background-position:0 0}.step-icons_crowdsale-page{width:100px;height:80px;background-position:0 -100px}.step-icons_crowdsale-setup{width:88px;height:100px;background-position:0 -180px}.step-icons_publish{width:100px;height:100px;background-position:0 -280px}.step-icons_token-setup{width:100px;height:100px;background-position:0 -380px}.button{cursor:pointer;display:inline-block;transition:.3s background-color,0.3s color;border-radius:3px;box-sizing:border-box;padding:0 15px;line-height:36px;font-size:13px;text-decoration:none;text-transform:uppercase;font-weight:bold}.button-container{text-align:center}.button_fill{background-color:#08b3f2;color:#fff}.button_fill:hover{background-color:#34c3f8;cursor:pointer}.button_disabled{background-color:#e6e6e6;color:#9c9c9c}.button_disabled:hover{cursor:default !important}.button_fill_secondary{background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAMAAADXqc3KAAAAFVBMVEX///////////////////////////9nSIHRAAAABnRSTlMASUrk5udXTd49AAAAOUlEQVR42tXQsQEAIAgDQcAn+4+snRZxAK79KokrIcNBwgYdc0Migwxk8Qsd1TJWDf/KQWobqt+9G4coA99W7as5AAAAAElFTkSuQmCC) !important;background-color:#64299d;color:#fff;margin-right:10px}.button_fill_secondary:hover{background-color:#752fb6}.button_fill_plus{background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAMAAADXqc3KAAAAFVBMVEX///////////////////////////9nSIHRAAAABnRSTlMASUrk5udXTd49AAAAOUlEQVR42tXQsQEAIAgDQcAn+4+snRZxAK79KokrIcNBwgYdc0Migwxk8Qsd1TJWDf/KQWobqt+9G4coA99W7as5AAAAAElFTkSuQmCC) !important;width:36px;height:36px;padding-left:0 !important;background-position:center center !important}.button_outline{box-shadow:inset 0 0 0 2px #08b3f2;color:#08b3f2}.button_outline:hover{background-color:#08b3f2;color:#fff}.plus-button-container{display:table-cell;padding-left:20px;padding-top:28.5px;vertical-align:top}.label{display:block;margin-bottom:15px;text-transform:uppercase;font-weight:bold}.input{transition:.3s border-color;width:100%;height:36px;margin-bottom:15px;outline:0;border:1px solid #eee;box-sizing:border-box;border-radius:3px;padding:0 10px;color:#333;font-size:14px;font-family:'Open Sans',sans-serif}.input:focus{border-color:#08b3f2}.input:disabled{background:#f2f2f2}.invest{margin:30px auto;padding-left:30px;border-radius:8px;border:1px solid #eee;background-color:#fff}.invest.container{min-width:960px;max-width:1000px;width:auto}.invest .timer{position:absolute;z-index:2;left:25px;top:25px;width:180px;height:180px;border-radius:50%;background-color:#fff;line-height:230px;text-align:center}.invest .timer-inner{line-height:normal;display:inline-block}.invest .timer-i{display:inline-block;vertical-align:middle;margin:0 5px;font-size:0;text-align:center}.invest .timer-count{display:block;margin-bottom:5px;color:#642f9c;font-size:24px;font-weight:bold}.invest .timer-interval{display:block;text-transform:uppercase;font-size:10px;color:#8197a2}.invest .timer-container{position:absolute;z-index:1;left:0;top:30px;border-radius:50%;background-color:#eee}.invest .hashes{position:relative;min-height:240px;padding-left:260px}.invest .hashes-i{margin-bottom:30px}.invest .hashes-title{margin-bottom:10px;color:#642f9c;font-weight:bold}.invest .hashes-description{color:#8197a2;font-size:12px}.invest .balance{border-bottom:1px solid #eee;padding:30px}.invest .balance-title{margin-bottom:5px;color:#642f9c;font-size:20px;font-weight:bold}.invest .balance-description{margin-bottom:30px;color:#8197a2}.invest .description{color:#8197a2;line-height:20px;font-size:12px}.invest-through{-webkit-appearance:none;-moz-appearance:none;appearance:none;float:left;height:36px;outline:0;padding:0 28px 0 10px;box-sizing:border-box;border:1px solid #eee;border-radius:3px;background-image:url(../images/select.png);background-repeat:no-repeat;background-position:center right 10px;background-color:#fff;color:#8197a2;font-size:12px}@media(min--moz-device-pixel-ratio:1.3),(-webkit-min-device-pixel-ratio:1.3),(min-device-pixel-ratio:1.3),(min-resolution:1.3dppx){.invest-through{background-image:url("../images/select@2x.png");background-size:8px 5px}}.invest-through-container{overflow:hidden;margin-bottom:30px}.invest-form{padding:30px}.invest-form ::-webkit-input-placeholder{color:#8197a2}.invest-form :-ms-input-placeholder{color:#8197a2}.invest-form ::placeholder{color:#8197a2}.invest-form-label{color:#8197a2}.invest-form-input{display:block;width:100%;height:44px;outline:0;padding-right:30px;border:0;border-bottom:1px solid #eee;box-sizing:border-box;color:#8197a2;font-size:14px;font-weight:bold}.invest-form-input-container{position:relative;margin:15px 0 50px}.invest-form-input-container .invest-form-label{-webkit-transform:translateY(-50%);transform:translateY(-50%);position:absolute;right:0;top:50%}.invest-form-input-container .error{color:red;font-weight:bold;font-size:12px;width:100%;height:10px;position:absolute;margin-top:5px}.invest-form .button{float:right}.invest-title{margin-bottom:10px;color:#333;text-transform:uppercase;font-weight:bold}.invest-description{margin-bottom:30px;color:#8197a2;line-height:18px;font-size:12px}.invest-table{display:table}.invest-table-cell{display:table-cell;max-width:293px}.invest-table-cell_left{position:relative;max-width:670px;padding-right:30px;border-right:1px solid #eee}@media(max-height:600px){.invest .qr-selected .balance-description{margin-bottom:0}.invest .qr-selected .description{display:none}.invest .qr-selected .invest-form{padding-bottom:0}.invest .qr-selected .invest-form-label{display:none}.invest .qr-selected .invest-form-input-container{display:none}.invest .qr-selected .payment-process{padding-top:5px}}@-webkit-keyframes paymentLoading{from{-webkit-transform:rotate(0);transform:rotate(0)}to{-webkit-transform:rotate(360deg);transform:rotate(360deg)}}@keyframes paymentLoading{from{-webkit-transform:rotate(0);transform:rotate(0)}to{-webkit-transform:rotate(360deg);transform:rotate(360deg)}}.payment-process{padding:30px}.payment-process-qr{display:block;width:140px;margin:0 auto 20px}.payment-process-description{margin-bottom:10px;color:#8197a2;line-height:18px;font-size:13px}.payment-process-hash{margin-bottom:15px;color:#642f9c;word-break:break-all;line-height:20px;font-size:14px;font-weight:bold}.payment-process-copy,.payment-process-see{display:inline-block;margin-bottom:20px;padding-left:20px;background-position:left center;background-repeat:no-repeat;color:#642f9c;text-decoration:none;line-height:14px;font-size:13px}.payment-process-copy:hover,.payment-process-see:hover{text-decoration:underline}.payment-process-copy{background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABwAAAAcCAMAAABF0y+mAAAAFVBMVEVlKZ5lKZ5lKZ5lKZ5lKZ5lKZ5lKZ4g+6HIAAAABnRSTlMASUrk5udXTd49AAAAS0lEQVR42sXSQQoAIAhEUTP1/kduNSIlIUT1t281KKEmNiXNERbV0ZLqSKGLmO4DpvscLamOFPqC6AR1g52f3vH862Q1dWSdrTNsAP8SCeaGkeh2AAAAAElFTkSuQmCC);background-size:14px 14px}.payment-process-see{background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABwAAAAUCAMAAACpgK3LAAAAvVBMVEVlKZ5lKZ5lKZ5lKZ5lKZ5lKZ5lKZ5lKZ5lKZ5lKZ5lKZ5lKZ5lKZ5lKZ5lKZ5lKZ5lKZ5lKZ5lKZ5lKZ5lKZ5lKZ5lKZ5lKZ5lKZ5lKZ5lKZ5lKZ5lKZ5lKZ5lKZ5lKZ5lKZ5lKZ5lKZ5lKZ5lKZ5lKZ5lKZ5lKZ5lKZ5lKZ5lKZ5lKZ5lKZ5lKZ5lKZ5lKZ5lKZ5lKZ5lKZ5lKZ5lKZ5lKZ5lKZ5lKZ5lKZ5lKZ5lKZ5lKZ5lKZ5lKZ5lKZ6cDY85AAAAPnRSTlMAAgYHCAkQESElJygpLzAxQkNITlBRUlZXW1xdXmFjZGVnaWqTo6WorcTFxtXa29ze3+Hi7/Dx8vP19vn8/uFjRaQAAADzSURBVHjafVLXEoIwEDxQERF7b6goGsFeUMTw/5/lnbEQRt0HZjcbdq4E3lDrzioIVk5DhSQUax89cRgqsqcvohhcPe6VT5GEU/njdUM68Wwzky3ZHvGw8/J6HOWy+pLVJUreE6J2QzFNAeSY77McQGqKB7caeYUAmYXEOFLg0UBq4f2gAKBt0WsDgoliGPEWujsN5qgnQPCFeXkIG9kMZvi1f5naDmMHydjBI1YUxCk4LwrK038cCypKrRjsfGYGtcJFK4T+tyH05fG5YzOdNscu8bD7Z/AVaWVe3PP0xLJHh/eyRwokoTac9fW6cZqfZ3IHucE/W04kek0AAAAASUVORK5CYII=);background-size:14px 10px}.payment-process-loader{position:relative;margin-bottom:20px;padding-left:20px;color:#8197a2;font-size:13px}.payment-process-loader:before{-webkit-animation-duration:1s;animation-duration:1s;-webkit-animation-fill-mode:forwards;animation-fill-mode:forwards;-webkit-animation-iteration-count:infinite;animation-iteration-count:infinite;-webkit-animation-name:paymentLoading;animation-name:paymentLoading;-webkit-animation-timing-function:linear;animation-timing-function:linear;content:"";position:absolute;left:0;top:50%;width:14px;height:14px;margin-top:-7px;background-image:url(../images/payment-loader.png)}@media(min--moz-device-pixel-ratio:1.3),(-webkit-min-device-pixel-ratio:1.3),(min-device-pixel-ratio:1.3),(min-resolution:1.3dppx){.payment-process-loader:before{background-image:url("../images/payment-loader@2x.png");background-size:100% 100%}}.payment-process-notation{border-radius:5px;border:1px solid #6d2eae;padding:10px;background-color:rgba(109,46,174,0.1);color:#6d2eae}.payment-process-notation-title{margin-bottom:5px;padding-left:28px;background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACQAAAAgCAMAAABNTyq8AAABX1BMVEVlKZ5lKZ5lKZ5lKZ5lKZ5lKZ5lKZ5lKZ5lKZ5lKZ5lKZ5lKZ5lKZ5lKZ5lKZ5lKZ5lKZ5lKZ5lKZ5lKZ5lKZ5lKZ5lKZ5lKZ5lKZ5lKZ5lKZ5lKZ5lKZ5lKZ5lKZ5lKZ5lKZ5lKZ5lKZ5lKZ5lKZ5lKZ5lKZ5lKZ5lKZ5lKZ5lKZ5lKZ5lKZ5lKZ5lKZ5lKZ5lKZ5lKZ5lKZ5lKZ5lKZ5lKZ5lKZ5lKZ5lKZ5lKZ5lKZ5lKZ5lKZ5lKZ5lKZ5lKZ5lKZ5lKZ5lKZ5lKZ5lKZ5lKZ5lKZ5lKZ5lKZ5lKZ5lKZ5lKZ5lKZ5lKZ5lKZ5lKZ5lKZ5lKZ5lKZ5lKZ5lKZ5lKZ5lKZ5lKZ5lKZ5lKZ5lKZ5lKZ5lKZ5lKZ5lKZ5lKZ5lKZ5lKZ5lKZ5lKZ5lKZ5lKZ5lKZ5lKZ5lKZ5lKZ5lKZ5lKZ5lKZ5lKZ5lKZ5lKZ5lKZ5lKZ5lKZ5lKZ5lKZ52EUWdAAAAdHRSTlMAAQIEBQcJCgsMDQ4SExUYGx4gISIoKjAxMzY4Oz1AQUtOT1VbXF5haW1ub3Byc3R1eHl7fH2CiImKi4yVlpeZm6Cjpaeoqq+0tre5usLExcbJysvQ0dLT1dbX2Nna29zh4uPn7O/w8fLz9PX29/n6+/z9/n/AovIAAAE8SURBVHgBhcGJmwoBHMfhj1Ub1rLY3PctdyTkJgclHSTkFiKpvv//s83RNL+npt4Xa/FSpfW3nIoxw2pdrvIKkTY15avFiZJTIEOEZFeBP9uYrqCQx0x1TMYhpkh81lA+AUtPNfQ2xqRrcpwCOCvHBSbs6MhxBeC6HD+3AtYTue4CPJLrPliH5XkOUJJrsA8j3pSnAfBVnuoCYZflawOxnnynCVn5rZFl2K6R78uMPVRgNxxQ4DaBgwMFjsJxBXq78K2va+wMpDVWXIcnpZCbcEchJ3Bt+aGQZ/BSId+WcNyT8SAvIwewt6+ZuklYqMqonTvfkPECTsqoxSHxQcYRXsnIANyQUaAtIwtwS0aLnoxfe2B/R0aHj7L+F0t9Wa/Jaq40m79ojk8bYOd7zfRuFWDj1Tf/FKFdubgIazWRPDi5QqQ+AAAAAElFTkSuQmCC);background-size:18px 16px;background-repeat:no-repeat;background-position:left center;line-height:16px;text-transform:uppercase;font-size:14px;font-weight:bold}.payment-process-notation-description{line-height:18px;font-size:12px}.payment-process-success{width:140px;height:146px;margin:0 auto 20px;background-image:url(../images/payment-success.png)}@media(min--moz-device-pixel-ratio:1.3),(-webkit-min-device-pixel-ratio:1.3),(min-device-pixel-ratio:1.3),(min-resolution:1.3dppx){.payment-process-success{background-image:url("../images/payment-success@2x.png");background-size:100% 100%}}.crowdsale,.process{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;-ms-flex-direction:column;flex-direction:column;-webkit-box-pack:center;-ms-flex-pack:center;justify-content:center;position:absolute;height:50%;box-sizing:border-box}.home{min-height:600px}.crowdsale-modal{position:absolute;display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-pack:center;-ms-flex-pack:center;justify-content:center;-webkit-box-align:center;-ms-flex-align:center;align-items:center}.crowdsale-modal .modal{background:white;width:45%;min-width:830px;padding:30px 20px 0;border-radius:10px;position:relative;height:80%;min-height:480px;max-height:615px}.crowdsale-modal .modal .title{margin-bottom:20px;text-transform:uppercase;font-size:24px;font-weight:bold}.crowdsale-modal .modal .description{margin-bottom:25px;color:#8197a2;line-height:24px;font-size:13px}.crowdsale-modal .modal .close-button{position:absolute;top:-40px;right:-40px;z-index:400000;padding:15px}.crowdsale-modal .modal .close-button i.icon:before{content:url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABkAAAAZCAYAAADE6YVjAAAABmJLR0QA/wD/AP+gvaeTAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAB3RJTUUH4gEDETQvtZKRngAAAaJJREFUSMetlcFOwkAURV9NXLjURUNwBbToTlJ04ULDJ7jSjd+hbvkEl3yC/2L8CRODBklMQAwJHhdO8VHeQFu5SdN0+jq3b+6dOyIiAgRAD2i6ZymD9DvgBLjXLwLgmV98AlFZEjdfAkzdfE/pYI9FTIBWSYJzltEVoOk6yBKFBQn2DYIPoJoWRG5ijbFr3auR0qADzDLf94HKvNBdLYNoBMSWGRTBsdHBEKiZPweErgMNrxmAthI5xQDYy+OO0TozeER+Aeq5vA7Eno5CJfK3IXIll/3VWjcM142BW4PgT+QiO9ddRwZRFu9ekQsQWmbQBLuyCQA3HpKrTRFUjY2mNQr/m6ZnhsgYrmutSoZVBIlHg0uPvRuFjgkXFVOfyM4Mlr3jvPukY3TQB2qZOsveIyDJE9ezPDvZadAwlm7ZDEoDXxZV1mgX+7JuwQwekQdAfdUaq2RIjI4mQKQP/alxHuyVSIaJ4brDLRG5FpFtVf8qIu0gCIZFSIIgeBORUxH5UsM7InKR/sVj4bj2L1+kXPeQLejOD/3yEZTeD4C7dPwHuOyCT59BxPUAAAAASUVORK5CYII=")}.crowdsale-modal .modal .close-button:hover{cursor:pointer}.home .crowdsale-modal .modal{max-height:360px}.crowdsale{top:0;padding-top:80px;text-align:center}.crowdsale .container{padding:0 40px;box-sizing:border-box}.crowdsale .title{margin-bottom:20px;text-transform:uppercase;font-size:24px;font-weight:bold}.crowdsale .description{margin-bottom:25px;color:#8197a2;line-height:24px;font-size:13px}.crowdsale .buttons{font-size:0}.crowdsale .button{margin:0 10px}@-webkit-keyframes show-process{from{opacity:0;-webkit-transform:scale(0);transform:scale(0)}to{opacity:1;transition:scale(1)}}@keyframes show-process{from{opacity:0;-webkit-transform:scale(0);transform:scale(0)}to{opacity:1;transition:scale(1)}}.process{bottom:0;padding-bottom:60px;background-color:#f5f5f7;font-size:0}.process .step-icons{-webkit-transform:translateX(-50%);transform:translateX(-50%);position:absolute;left:50%;top:0}.process .step-icons_crowdsale-page{top:10px}.process .process-item{opacity:0;-webkit-animation-name:show-process;animation-name:show-process;-webkit-animation-duration:1s;animation-duration:1s;-webkit-animation-fill-mode:forwards;animation-fill-mode:forwards;position:relative;display:inline-block;vertical-align:top;width:20%;padding:130px 10px 0;box-sizing:border-box;text-align:center}.process .process-item:nth-child(1){-webkit-animation-delay:200ms;animation-delay:200ms}.process .process-item:nth-child(1):after{content:"1"}.process .process-item:nth-child(2){-webkit-animation-delay:400ms;animation-delay:400ms}.process .process-item:nth-child(2):after{content:"2"}.process .process-item:nth-child(3){-webkit-animation-delay:600ms;animation-delay:600ms}.process .process-item:nth-child(3):after{content:"3"}.process .process-item:nth-child(4){-webkit-animation-delay:800ms;animation-delay:800ms}.process .process-item:nth-child(4):after{content:"4"}.process .process-item:nth-child(5){-webkit-animation-delay:1000ms;animation-delay:1000ms}.process .process-item:nth-child(5):after{content:"5"}.process .process-item:after{position:absolute;width:32px;height:32px;right:28px;top:-16px;border-radius:50%;border:4px solid #fff;background-color:#08b3f2;color:#fff;line-height:32px;text-align:center;font-size:14px;font-weight:bold}.process .title{margin-bottom:10px;text-transform:uppercase;font-size:14px;font-weight:bold}.process .description{color:#8197a2;line-height:18px;font-size:12px}.steps pre{display:block;overflow:auto;height:200px;padding:15px;box-sizing:border-box;border:1px solid #eee;border-radius:3px;font-size:14px;word-break:break-all;word-wrap:break-word;white-space:pre-wrap}.steps .button-container{margin:40px 0}.steps .value{margin-bottom:10px}.steps .left,.steps .right,.steps .item{margin-bottom:25px}.steps .publish-title{position:relative;z-index:2;display:inline-block;padding-right:5px;padding-left:30px;text-transform:uppercase;font-size:11px;font-weight:bold;background-color:#fff}.steps .publish-title-container{position:relative;margin-bottom:25px;line-height:20px}.steps .publish-title-container:after{content:"";position:absolute;z-index:1;right:0;left:0;top:50%;height:1px;margin-top:-1px;border-bottom:1px dashed #eee}.steps .publish-title:before{content:attr(data-step);position:absolute;z-index:2;left:0;top:50%;width:20px;height:20px;border-radius:50%;margin-top:-10px;background-color:#08b3f2;color:#fff;line-height:20px;text-align:center}.steps .button{padding-left:30px;background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAUCAMAAACgaw2xAAAAMFBMVEX///////////////////////////////////////////////////////////////9Or7hAAAAAD3RSTlMAPT5BSUpRUt3f5ufp6vUxUI2NAAAASUlEQVR42rWROw7AMAxC3abN3+b+t80WZYAxb/STsAR2kr0+xnAgEhMVwryhTLpjvg5CFOugTCXc/sGj1PM7d12irl0Plb3taRdkbQmzeFkz0wAAAABJRU5ErkJggg==);background-size:12px 10px;background-repeat:no-repeat;background-position:left 10px center;cursor:pointer}.steps .button.no_image{padding-left:15px;background-image:none;background-position:0 0;background-size:auto}.steps-content{padding:30px 30px 0;border-radius:8px;border:1px solid #eee;background-color:#fff}.steps-content .about-step{position:relative;min-height:100px;padding-left:120px;padding-bottom:30px}.steps:not(.steps_publish) .steps-content .about-step{margin-bottom:30px;border-bottom:1px solid #eee}.steps-content .about-step .step-icons{position:absolute;left:0;top:0}.steps-content .about-step .title{display:block;margin-bottom:10px;text-transform:uppercase;font-size:20px;font-weight:bold}.steps-content .about-step .description{line-height:24px;font-size:13px}.steps-content .description{color:#8197a2;line-height:20px;font-size:12px}.steps-navigation{margin-bottom:30px;padding:30px 0;border-bottom:1px solid #eee;background-color:#fff}.steps-navigation .container{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-pack:justify;-ms-flex-pack:justify;justify-content:space-between}.steps-navigation .step-navigation{position:relative;padding-left:30px;color:#8197a2;text-transform:uppercase;line-height:20px;font-size:11px;font-weight:bold}.steps-navigation .step-navigation:nth-child(1):before{content:"1"}.steps-navigation .step-navigation:nth-child(2):before{content:"2"}.steps-navigation .step-navigation:nth-child(3):before{content:"3"}.steps-navigation .step-navigation:nth-child(4):before{content:"4"}.steps-navigation .step-navigation:nth-child(5):before{content:"5"}.steps-navigation .step-navigation:before{-webkit-transform:translateY(-50%);transform:translateY(-50%);position:absolute;width:20px;height:20px;left:0;top:50%;border-radius:50%;background-color:rgba(129,151,162,0.4);color:#fff;line-height:20px;text-align:center;font-size:11px;font-weight:bold}.steps-navigation .step-navigation_active{color:#333}.steps-navigation .step-navigation_active:before{background-color:#08b3f2}.steps .radios input{display:none}.steps .radios input:checked+.title:before{background-color:#642f9c}.steps .radio{cursor:pointer;position:relative;display:block;margin-bottom:60px;padding-left:45px}.steps .radio:not(:last-child):after{cursor:default;content:'';position:absolute;left:0;right:0;bottom:-29px;height:1px;background-color:#eee}.steps .radio:last-child{margin-bottom:30px}.steps .radio .title{position:relative;display:block;margin-bottom:5px;line-height:20px;text-transform:uppercase;font-weight:bold}.steps .radio .title:before{content:'';position:absolute;left:-45px;top:50%;width:10px;height:10px;margin-top:-5px;border:5px solid #fff;box-shadow:0 0 0 1px #cdd5da;border-radius:50%}.steps .radio .title_soon:after{content:'soon';display:inline-block;vertical-align:top;margin-left:5px;padding:0 5px;border-radius:2px;background-color:#642f9c;color:#fff;line-height:20px;text-transform:uppercase;font-size:10px;font-weight:bold}.steps .radio .description{color:#8197a2;line-height:20px;font-size:12px}.steps .radios-inline{height:36px;vertical-align:middle;margin-bottom:15px}.steps .radios-inline input{display:none}.steps .radios-inline input:checked+.title:before{background-color:#642f9c}.steps .radio-inline{cursor:pointer;position:relative;display:table-cell;padding-left:30px;padding-right:20px;line-height:36px}.steps .radio-inline:last-child{margin-bottom:30px}.steps .radio-inline .title{position:relative;display:block}.steps .radio-inline .title:before{content:'';position:absolute;left:-29px;top:50%;width:10px;height:10px;margin-top:-8.5px;border:5px solid #fff;box-shadow:0 0 0 1px #cdd5da;border-radius:50%}.reserved-tokens-title{display:block;margin-bottom:30px;text-transform:uppercase;font-size:20px;font-weight:bold}.reserved-tokens-container{margin-bottom:40px}.reserved-tokens-radio-container{height:36px;vertical-align:middle;display:table;width:100%;margin-bottom:15px}.reserved-tokens-radio-container-item{display:inline;line-height:36px;vertical-align:middle;margin-right:10px}.reserved-tokens-input-property{width:33%}.reserved-tokens-input-property-left{display:table-cell;vertical-align:top;padding-right:5px}.reserved-tokens-input-property-middle{display:table-cell;vertical-align:top;padding-left:5px;padding-right:5px}.reserved-tokens-input-property-right{display:table-cell;vertical-align:top;padding-left:5px}.reserved-tokens-input-container{margin-bottom:20px;display:table}.reserved-tokens-item{display:table-cell;height:60px;line-height:60px;width:33%}.reserved-tokens-item-left{padding-right:5px}.reserved-tokens-item-middle{padding-left:5px;padding-right:5px}.reserved-tokens-item-right{padding-left:5px}.reserved-tokens-item-empty{display:table-cell;line-height:60px;width:56px}.reserved-tokens-item-container{border-top:1px solid #eee;display:table;width:100%}.reserved-tokens-item-container-last{border-bottom:1px solid #eee}.reserved-tokens-item-container-inner{display:table;width:100%}.total-funds .right{text-align:right}.total-funds-title{margin-bottom:15px;color:#642f9c;font-size:24px;font-weight:bold}.total-funds-description{color:#8197a2;font-size:14px}.total-funds-chart{position:relative;z-index:1;height:20px;margin-bottom:30px;border-radius:10px;background-color:#f5f5f5}.total-funds-chart-active{position:absolute;left:0;top:0;bottom:0;border-radius:10px;background-image:linear-gradient(to right,#7738b9,#853ecf)}.total-funds-chart-container{position:relative}.total-funds-chart-division{position:absolute;z-index:0;top:-5px;bottom:-5px;width:1px;background-color:#ddd}.total-funds-chart-division:nth-child(1){left:10%}.total-funds-chart-division:nth-child(2){left:20%}.total-funds-chart-division:nth-child(3){left:30%}.total-funds-chart-division:nth-child(4){left:40%}.total-funds-chart-division:nth-child(5){left:50%}.total-funds-chart-division:nth-child(6){left:60%}.total-funds-chart-division:nth-child(7){left:70%}.total-funds-chart-division:nth-child(8){left:80%}.total-funds-chart-division:nth-child(9){left:90%}.total-funds-statistics .left,.total-funds-statistics .right{width:50%;box-sizing:border-box}.total-funds-statistics .title,.total-funds-statistics .hash{margin-bottom:10px;color:#642f9c;font-weight:bold}.total-funds-statistics .title{font-size:16px}.total-funds-statistics .hash{font-size:14px}.white-list-container{margin-bottom:40px}.white-list-input-property-left{display:table-cell;vertical-align:top;padding-right:5px;width:42%}.white-list-input-property-middle{display:table-cell;vertical-align:top;padding-left:5px;padding-right:5px;width:29%}.white-list-input-property-right{display:table-cell;vertical-align:top;padding-left:5px;width:29%}.white-list-input-container{margin-bottom:20px;display:table}.white-list-item{display:table-cell;height:60px;line-height:60px}.white-list-item-left{padding-right:5px;width:42%}.white-list-item-middle{padding-left:5px;padding-right:5px;width:29%}.white-list-item-right{padding-left:5px;width:29%}.white-list-item-empty{display:table-cell;width:56px}.white-list-item-container{font-weight:bold;border-top:1px solid #eee;display:table;width:100%}.white-list-item-container-last{border-bottom:1px solid #eee}.white-list-item-container-inner{display:table;width:100%}.white-list-item-container.to-be-removed{font-weight:normal;color:orange;text-decoration:line-through}.white-list-item-container.no-style{font-weight:normal;color:#555}.white-list-item-container.duplicated{padding-bottom:15px;border-top:1px dashed orange;margin-top:-15px}.white-list-item-container.duplicated span{line-height:30px;height:12px}@-webkit-keyframes fadeOut{0%{opacity:.2}20%{opacity:1;-webkit-transform:scale(1);transform:scale(1)}100%{opacity:.2;-webkit-transform:scale(0.3);transform:scale(0.3)}}@keyframes fadeOut{0%{opacity:.2}20%{opacity:1;-webkit-transform:scale(1);transform:scale(1)}100%{opacity:.2;-webkit-transform:scale(0.3);transform:scale(0.3)}}.loading{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-pack:justify;-ms-flex-pack:justify;justify-content:space-between;position:absolute;left:50%;top:50%;width:206px;margin:-30px 0 0 -111.5px;padding-top:50px}.loading:before{content:'';position:absolute;left:0;top:0;width:206px;height:35px;background-image:url("../images/loading.png");background-position:0 0}@media(min--moz-device-pixel-ratio:1.3),(-webkit-min-device-pixel-ratio:1.3),(min-device-pixel-ratio:1.3),(min-resolution:1.3dppx){.loading:before{background-image:url("../images/loading@2x.png");background-size:100% 100%}}.loading-container{position:fixed;z-index:1000000;left:0;right:0;top:0;bottom:0;background-color:rgba(35,29,115,0.8)}.loading-text-container{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-pack:justify;-ms-flex-pack:justify;justify-content:space-between;position:absolute;left:50%;top:50%;width:206px;margin:-30px 0 0 -111.5px;padding-top:45px}.loading-text{color:white;font-weight:bold;text-align:justify;word-spacing:2px;width:100%;font-size:14.2px}.loading-progress{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-pack:justify;-ms-flex-pack:justify;justify-content:space-between;position:absolute;left:50%;top:50%;width:206px;margin:-30px 0 0 -111.5px;padding-top:70px}.loading-i{-webkit-animation-duration:2s;animation-duration:2s;-webkit-animation-fill-mode:forwards;animation-fill-mode:forwards;-webkit-animation-iteration-count:infinite;animation-iteration-count:infinite;-webkit-animation-name:fadeOut;animation-name:fadeOut;-webkit-animation-timing-function:linear;animation-timing-function:linear;opacity:.2;width:9px;height:9px;border-radius:50%;background-color:#fff}.loading-i:nth-child(2){-webkit-animation-delay:.1s;animation-delay:.1s}.loading-i:nth-child(3){-webkit-animation-delay:.2s;animation-delay:.2s}.loading-i:nth-child(4){-webkit-animation-delay:.3s;animation-delay:.3s}.loading-i:nth-child(5){-webkit-animation-delay:.4s;animation-delay:.4s}.loading-i:nth-child(6){-webkit-animation-delay:.5s;animation-delay:.5s}.flex-table{height:88%;display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;-ms-flex-direction:column;flex-direction:column;min-height:58vh}.flex-table .container-fluid{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;-ms-flex-direction:column;flex-direction:column;width:100%;height:90%;min-height:380px;max-height:560px}.flex-table .table-row{display:-webkit-box;display:-ms-flexbox;display:flex;display:-webkit-flex;-webkit-box-orient:horizontal;-webkit-box-direction:normal;-ms-flex-direction:row;flex-direction:row;-webkit-box-flex:0;-ms-flex-positive:0;flex-grow:0;-webkit-flex-grow:0;-ms-flex-wrap:wrap;flex-wrap:wrap;-webkit-flex-wrap:wrap;width:100%;padding-left:15px;padding-right:15px}.flex-table .sm-text,.flex-table .text{-webkit-box-flex:2;-ms-flex-positive:2;flex-grow:2;padding-right:0}.flex-table .sm-text{-webkit-box-flex:1;-ms-flex-positive:1;flex-grow:1;-webkit-flex-grow:1}.flex-table .text{width:260px}.flex-table .sm-text{width:100px}.flex-table .num{width:80px}.flex-table .table-row{border-collapse:collapse;padding-bottom:15px;padding-top:15px;-webkit-box-align:center;-ms-flex-align:center;align-items:center}.flex-table .table-row.flex-table-header{padding-top:8px;padding-bottom:8px;width:99%}.flex-table .table-row.selected{background-color:#08b3f2;color:#fff}.flex-table .table-row.clickable:hover{cursor:pointer}.flex-table .table-row.datagrid{padding-bottom:5px;padding-top:5px}.flex-table .table-row.datagrid:nth-child(even){background-color:#e8e8e8}.flex-table .scrollable-content{border:1px solid lightgray;min-height:185px;max-height:100%;height:90%;overflow-x:hidden;overflow-y:auto}.flex-table .scrollable-content::-webkit-scrollbar{width:5px}.flex-table .scrollable-content::-webkit-scrollbar-track{border-left:1px solid lightgray;padding:3px}.flex-table .scrollable-content::-webkit-scrollbar-thumb{background:lightgray}.flex-table .steps{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-pack:center;-ms-flex-pack:center;justify-content:center;-webkit-box-align:center;-ms-flex-align:center;align-items:center}.home .flex-table{height:100%;min-height:56vh}.home .flex-table .scrollable-content{min-height:160px;height:55%;margin-bottom:2%}.manage{min-height:600px}.manage .warning-logo{color:#642f9c !important;border-color:#642f9c !important;position:absolute;left:0;top:0;display:-webkit-box;display:-ms-flexbox;display:flex}.manage .white-list-item-container .remove{display:-webkit-inline-box;display:-ms-inline-flexbox;display:inline-flex;margin-top:8px}.manage .white-list-item-container .swal2-icon{width:14px;height:14px;line-height:14px;border:2px solid transparent}.manage .white-list-item-container .warning-logo{color:orange !important;border-color:orange !important;position:relative;float:right;font-size:14px;font-weight:bold}.manage .steps-content{margin-top:30px}.manage .description{margin-bottom:20px}.manage .crowdsale-page-link{color:#08b3f2;text-decoration:none;font-size:13px}.manage .divisor{margin-bottom:30px;border-bottom:1px solid #eee}.manage .no-arrow{background-image:none;padding:0 15px} \ No newline at end of file +.header,.footer,.crowdsale,.process{left:0;right:0}.header .logo,.footer .logo{display:block;background-image:url(../images/logos.png)}@media(min--moz-device-pixel-ratio:1.3),(-webkit-min-device-pixel-ratio:1.3),(min-device-pixel-ratio:1.3),(min-resolution:1.3dppx){.header .logo,.footer .logo{background-image:url("../images/logos@2x.png");background-size:182px 59px}}@font-face{font-family:'Open Sans';font-style:normal;font-weight:400;src:local("Open Sans"),local("OpenSans"),url(https://fonts.gstatic.com/s/opensans/v13/cJZKeOuBrn4kERxqtaUH3ZBw1xU1rKptJj_0jans920.woff2) format("woff2");unicode-range:U+0000-00FF,U+0131,U+0152-0153,U+02C6,U+02DA,U+02DC,U+2000-206F,U+2074,U+20AC,U+2212,U+2215,U+E0FF,U+EFFD,U+F000}@font-face{font-family:'Open Sans';font-style:normal;font-weight:700;src:local("Open Sans Bold"),local("OpenSans-Bold"),url(https://fonts.gstatic.com/s/opensans/v13/k3k702ZOKiLJc3WVjuplzBampu5_7CjHW5spxoeN3Vs.woff2) format("woff2");unicode-range:U+0000-00FF,U+0131,U+0152-0153,U+02C6,U+02DA,U+02DC,U+2000-206F,U+2074,U+20AC,U+2212,U+2215,U+E0FF,U+EFFD,U+F000}html,body{color:#333;line-height:1;font-size:14px;font-family:'Open Sans',sans-serif;-webkit-font-smoothing:antialiased}html,body{margin:0;padding:0}p,h1,h2,h3,h4{margin:0;padding:0;font-family:'Open Sans',sans-serif}html{height:100%}body{position:relative;width:100%;min-width:1000px;min-height:100%;box-sizing:border-box;padding:80px 0 60px;background-color:#fbfbfc}.container{width:960px;margin:0 auto;box-sizing:border-box}.hidden{overflow:hidden}.notdisplayed{display:none}.left{width:46%;float:left}.right{width:46%;float:right}.item-remove{background-image:url(../images/delete.png);background-repeat:no-repeat;display:block;width:12px;height:12px;cursor:pointer;float:right}@media(min--moz-device-pixel-ratio:1.3),(-webkit-min-device-pixel-ratio:1.3),(min-device-pixel-ratio:1.3),(min-resolution:1.3dppx){.item-remove{background-image:url("../images/delete@2x.png");background-size:12px 12px}}.copy{background-image:url(../images/copy.png);background-repeat:no-repeat;display:block;width:12px;height:12px;cursor:pointer;float:right}@media(min--moz-device-pixel-ratio:1.3),(-webkit-min-device-pixel-ratio:1.3),(min-device-pixel-ratio:1.3),(min-resolution:1.3dppx){.copy{background-image:url("../images/copy@2x.png");background-size:12px 12px}}.copy-field-container{display:table-cell;padding-left:10px;vertical-align:top;padding-top:30.5px}.copy-area-container{display:table-cell;padding-left:10px}.display-container{display:table-cell;width:100%}.input-block-container{display:table;width:100%}.section-title{display:block;margin-bottom:30px;text-transform:uppercase;font-size:20px;font-weight:bold}@media(max-height:600px){body{padding-top:0}}.header{position:absolute;top:0;height:80px;background-image:url(../images/bg.png);background-size:cover;background-position:center center}.header .logo{width:182px;height:35px;margin-top:22.5px;background-position:0 -25px}@media(max-height:600px){.header{position:relative;top:0;height:auto;padding:1px 0 21px}}.footer{position:absolute;bottom:0;height:60px;background-image:url(../images/bg.png);background-size:cover;background-position:center center;color:#fff}.footer .container{position:relative}.footer .logo,.footer .socials{-webkit-transform:translateY(-50%);transform:translateY(-50%);position:absolute;z-index:1;top:50%}.footer .logo{left:0;width:126px;height:24px;background-position:0 0}.footer .rights{color:#fff;line-height:60px;text-align:center;font-size:12px}.footer .socials{right:0}.socials{font-size:0}.socials .social{transition:.3s background-color;position:relative;display:inline-block;vertical-align:top;width:30px;height:30px;border-radius:50%;background-color:rgba(255,255,255,0.2)}.socials .social:not(:first-child){margin-left:10px}.socials .social:hover{background-color:rgba(255,255,255,0.4)}.socials .social:before{-webkit-transform:translate(-50%,-50%);transform:translate(-50%,-50%);content:'';position:absolute;left:50%;top:50%;background-image:url(../images/socials.png)}@media(min--moz-device-pixel-ratio:1.3),(-webkit-min-device-pixel-ratio:1.3),(min-device-pixel-ratio:1.3),(min-resolution:1.3dppx){.socials .social:before{background-image:url("../images/socials@2x.png");background-size:16px 69px}}.socials .social_github:before{width:16px;height:16px;background-position:0 0}.socials .social_oracles:before{width:16px;height:14px;background-position:0 -16px}.socials .social_reddit:before{width:15px;height:13px;background-position:0 -30px}.socials .social_telegram:before{width:16px;height:14px;background-position:0 -43px}.socials .social_twitter:before{width:15px;height:12px;background-position:0 -57px}.step-icons{background-image:url(../images/step-icons.png)}@media(min--moz-device-pixel-ratio:1.3),(-webkit-min-device-pixel-ratio:1.3),(min-device-pixel-ratio:1.3),(min-resolution:1.3dppx){.step-icons{background-image:url("../images/step-icons@2x.png");background-size:100px 480px}}.step-icons_crowdsale-contract{width:80px;height:100px;background-position:0 0}.step-icons_crowdsale-page{width:100px;height:80px;background-position:0 -100px}.step-icons_crowdsale-setup{width:88px;height:100px;background-position:0 -180px}.step-icons_publish{width:100px;height:100px;background-position:0 -280px}.step-icons_token-setup{width:100px;height:100px;background-position:0 -380px}.button{cursor:pointer;display:inline-block;transition:.3s background-color,0.3s color;border-radius:3px;box-sizing:border-box;padding:0 15px;line-height:36px;font-size:13px;text-decoration:none;text-transform:uppercase;font-weight:bold}.button-container{text-align:center}.button_fill{background-color:#08b3f2;color:#fff}.button_fill:hover{background-color:#34c3f8;cursor:pointer}.button_disabled{background-color:#e6e6e6;color:#9c9c9c}.button_disabled:hover{background-color:#e6e6e6;color:#9c9c9c;cursor:default !important}.button_fill_secondary{background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAMAAADXqc3KAAAAFVBMVEX///////////////////////////9nSIHRAAAABnRSTlMASUrk5udXTd49AAAAOUlEQVR42tXQsQEAIAgDQcAn+4+snRZxAK79KokrIcNBwgYdc0Migwxk8Qsd1TJWDf/KQWobqt+9G4coA99W7as5AAAAAElFTkSuQmCC) !important;background-color:#64299d;color:#fff;margin-right:10px}.button_fill_secondary:hover{background-color:#752fb6}.button_fill_plus{background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAMAAADXqc3KAAAAFVBMVEX///////////////////////////9nSIHRAAAABnRSTlMASUrk5udXTd49AAAAOUlEQVR42tXQsQEAIAgDQcAn+4+snRZxAK79KokrIcNBwgYdc0Migwxk8Qsd1TJWDf/KQWobqt+9G4coA99W7as5AAAAAElFTkSuQmCC) !important;width:36px;height:36px;padding-left:0 !important;background-position:center center !important}.button_outline{box-shadow:inset 0 0 0 2px #08b3f2;color:#08b3f2}.button_outline:hover{background-color:#08b3f2;color:#fff}.plus-button-container{display:table-cell;padding-left:20px;padding-top:28.5px;vertical-align:top}.label{display:block;margin-bottom:15px;text-transform:uppercase;font-weight:bold}.input{transition:.3s border-color;width:100%;height:36px;margin-bottom:15px;outline:0;border:1px solid #eee;box-sizing:border-box;border-radius:3px;padding:0 10px;color:#333;font-size:14px;font-family:'Open Sans',sans-serif}.input:focus{border-color:#08b3f2}.input:disabled{background:#f2f2f2}.invest{margin:30px auto;padding-left:30px;border-radius:8px;border:1px solid #eee;background-color:#fff}.invest.container{min-width:960px;max-width:1000px;width:auto}.invest .timer{position:absolute;z-index:2;left:25px;top:25px;width:180px;height:180px;border-radius:50%;background-color:#fff;line-height:230px;text-align:center}.invest .timer-inner{line-height:normal;display:inline-block}.invest .timer-i{display:inline-block;vertical-align:middle;margin:0 5px;font-size:0;text-align:center}.invest .timer-count{display:block;margin-bottom:5px;color:#642f9c;font-size:24px;font-weight:bold}.invest .timer-interval{display:block;text-transform:uppercase;font-size:10px;color:#8197a2}.invest .timer-container{position:absolute;z-index:1;left:0;top:30px;border-radius:50%;background-color:#eee}.invest .hashes{position:relative;min-height:240px;padding-left:260px}.invest .hashes-i{margin-bottom:30px}.invest .hashes-title{margin-bottom:10px;color:#642f9c;font-weight:bold}.invest .hashes-description{color:#8197a2;font-size:12px}.invest .balance{border-bottom:1px solid #eee;padding:30px}.invest .balance-title{margin-bottom:5px;color:#642f9c;font-size:20px;font-weight:bold}.invest .balance-description{margin-bottom:30px;color:#8197a2}.invest .description{color:#8197a2;line-height:20px;font-size:12px}.invest-through{-webkit-appearance:none;-moz-appearance:none;appearance:none;float:left;height:36px;outline:0;padding:0 28px 0 10px;box-sizing:border-box;border:1px solid #eee;border-radius:3px;background-image:url(../images/select.png);background-repeat:no-repeat;background-position:center right 10px;background-color:#fff;color:#8197a2;font-size:12px}@media(min--moz-device-pixel-ratio:1.3),(-webkit-min-device-pixel-ratio:1.3),(min-device-pixel-ratio:1.3),(min-resolution:1.3dppx){.invest-through{background-image:url("../images/select@2x.png");background-size:8px 5px}}.invest-through-container{overflow:hidden;margin-bottom:30px}.invest-form{padding:30px}.invest-form ::-webkit-input-placeholder{color:#8197a2}.invest-form :-ms-input-placeholder{color:#8197a2}.invest-form ::placeholder{color:#8197a2}.invest-form-label{color:#8197a2}.invest-form-input{display:block;width:100%;height:44px;outline:0;padding-right:30px;border:0;border-bottom:1px solid #eee;box-sizing:border-box;color:#8197a2;font-size:14px;font-weight:bold}.invest-form-input-container{position:relative;margin:15px 0 50px}.invest-form-input-container .invest-form-label{-webkit-transform:translateY(-50%);transform:translateY(-50%);position:absolute;right:0;top:50%}.invest-form-input-container .error{color:red;font-weight:bold;font-size:12px;width:100%;height:10px;position:absolute;margin-top:5px}.invest-form .button{float:right}.invest-title{margin-bottom:10px;color:#333;text-transform:uppercase;font-weight:bold}.invest-description{margin-bottom:30px;color:#8197a2;line-height:18px;font-size:12px}.invest-table{display:table}.invest-table-cell{display:table-cell;max-width:293px}.invest-table-cell_left{position:relative;max-width:670px;padding-right:30px;border-right:1px solid #eee}@media(max-height:600px){.invest .qr-selected .balance-description{margin-bottom:0}.invest .qr-selected .description{display:none}.invest .qr-selected .invest-form{padding-bottom:0}.invest .qr-selected .invest-form-label{display:none}.invest .qr-selected .invest-form-input-container{display:none}.invest .qr-selected .payment-process{padding-top:5px}}@-webkit-keyframes paymentLoading{from{-webkit-transform:rotate(0);transform:rotate(0)}to{-webkit-transform:rotate(360deg);transform:rotate(360deg)}}@keyframes paymentLoading{from{-webkit-transform:rotate(0);transform:rotate(0)}to{-webkit-transform:rotate(360deg);transform:rotate(360deg)}}.payment-process{padding:30px}.payment-process-qr{display:block;width:140px;margin:0 auto 20px}.payment-process-description{margin-bottom:10px;color:#8197a2;line-height:18px;font-size:13px}.payment-process-hash{margin-bottom:15px;color:#642f9c;word-break:break-all;line-height:20px;font-size:14px;font-weight:bold}.payment-process-copy,.payment-process-see{display:inline-block;margin-bottom:20px;padding-left:20px;background-position:left center;background-repeat:no-repeat;color:#642f9c;text-decoration:none;line-height:14px;font-size:13px}.payment-process-copy:hover,.payment-process-see:hover{text-decoration:underline}.payment-process-copy{background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABwAAAAcCAMAAABF0y+mAAAAFVBMVEVlKZ5lKZ5lKZ5lKZ5lKZ5lKZ5lKZ4g+6HIAAAABnRSTlMASUrk5udXTd49AAAAS0lEQVR42sXSQQoAIAhEUTP1/kduNSIlIUT1t281KKEmNiXNERbV0ZLqSKGLmO4DpvscLamOFPqC6AR1g52f3vH862Q1dWSdrTNsAP8SCeaGkeh2AAAAAElFTkSuQmCC);background-size:14px 14px}.payment-process-see{background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABwAAAAUCAMAAACpgK3LAAAAvVBMVEVlKZ5lKZ5lKZ5lKZ5lKZ5lKZ5lKZ5lKZ5lKZ5lKZ5lKZ5lKZ5lKZ5lKZ5lKZ5lKZ5lKZ5lKZ5lKZ5lKZ5lKZ5lKZ5lKZ5lKZ5lKZ5lKZ5lKZ5lKZ5lKZ5lKZ5lKZ5lKZ5lKZ5lKZ5lKZ5lKZ5lKZ5lKZ5lKZ5lKZ5lKZ5lKZ5lKZ5lKZ5lKZ5lKZ5lKZ5lKZ5lKZ5lKZ5lKZ5lKZ5lKZ5lKZ5lKZ5lKZ5lKZ5lKZ5lKZ5lKZ5lKZ5lKZ5lKZ6cDY85AAAAPnRSTlMAAgYHCAkQESElJygpLzAxQkNITlBRUlZXW1xdXmFjZGVnaWqTo6WorcTFxtXa29ze3+Hi7/Dx8vP19vn8/uFjRaQAAADzSURBVHjafVLXEoIwEDxQERF7b6goGsFeUMTw/5/lnbEQRt0HZjcbdq4E3lDrzioIVk5DhSQUax89cRgqsqcvohhcPe6VT5GEU/njdUM68Wwzky3ZHvGw8/J6HOWy+pLVJUreE6J2QzFNAeSY77McQGqKB7caeYUAmYXEOFLg0UBq4f2gAKBt0WsDgoliGPEWujsN5qgnQPCFeXkIG9kMZvi1f5naDmMHydjBI1YUxCk4LwrK038cCypKrRjsfGYGtcJFK4T+tyH05fG5YzOdNscu8bD7Z/AVaWVe3PP0xLJHh/eyRwokoTac9fW6cZqfZ3IHucE/W04kek0AAAAASUVORK5CYII=);background-size:14px 10px}.payment-process-loader{position:relative;margin-bottom:20px;padding-left:20px;color:#8197a2;font-size:13px}.payment-process-loader:before{-webkit-animation-duration:1s;animation-duration:1s;-webkit-animation-fill-mode:forwards;animation-fill-mode:forwards;-webkit-animation-iteration-count:infinite;animation-iteration-count:infinite;-webkit-animation-name:paymentLoading;animation-name:paymentLoading;-webkit-animation-timing-function:linear;animation-timing-function:linear;content:"";position:absolute;left:0;top:50%;width:14px;height:14px;margin-top:-7px;background-image:url(../images/payment-loader.png)}@media(min--moz-device-pixel-ratio:1.3),(-webkit-min-device-pixel-ratio:1.3),(min-device-pixel-ratio:1.3),(min-resolution:1.3dppx){.payment-process-loader:before{background-image:url("../images/payment-loader@2x.png");background-size:100% 100%}}.payment-process-notation{border-radius:5px;border:1px solid #6d2eae;padding:10px;background-color:rgba(109,46,174,0.1);color:#6d2eae}.payment-process-notation-title{margin-bottom:5px;padding-left:28px;background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACQAAAAgCAMAAABNTyq8AAABX1BMVEVlKZ5lKZ5lKZ5lKZ5lKZ5lKZ5lKZ5lKZ5lKZ5lKZ5lKZ5lKZ5lKZ5lKZ5lKZ5lKZ5lKZ5lKZ5lKZ5lKZ5lKZ5lKZ5lKZ5lKZ5lKZ5lKZ5lKZ5lKZ5lKZ5lKZ5lKZ5lKZ5lKZ5lKZ5lKZ5lKZ5lKZ5lKZ5lKZ5lKZ5lKZ5lKZ5lKZ5lKZ5lKZ5lKZ5lKZ5lKZ5lKZ5lKZ5lKZ5lKZ5lKZ5lKZ5lKZ5lKZ5lKZ5lKZ5lKZ5lKZ5lKZ5lKZ5lKZ5lKZ5lKZ5lKZ5lKZ5lKZ5lKZ5lKZ5lKZ5lKZ5lKZ5lKZ5lKZ5lKZ5lKZ5lKZ5lKZ5lKZ5lKZ5lKZ5lKZ5lKZ5lKZ5lKZ5lKZ5lKZ5lKZ5lKZ5lKZ5lKZ5lKZ5lKZ5lKZ5lKZ5lKZ5lKZ5lKZ5lKZ5lKZ5lKZ5lKZ5lKZ5lKZ5lKZ5lKZ5lKZ5lKZ5lKZ5lKZ5lKZ5lKZ5lKZ5lKZ5lKZ5lKZ52EUWdAAAAdHRSTlMAAQIEBQcJCgsMDQ4SExUYGx4gISIoKjAxMzY4Oz1AQUtOT1VbXF5haW1ub3Byc3R1eHl7fH2CiImKi4yVlpeZm6Cjpaeoqq+0tre5usLExcbJysvQ0dLT1dbX2Nna29zh4uPn7O/w8fLz9PX29/n6+/z9/n/AovIAAAE8SURBVHgBhcGJmwoBHMfhj1Ub1rLY3PctdyTkJgclHSTkFiKpvv//s83RNL+npt4Xa/FSpfW3nIoxw2pdrvIKkTY15avFiZJTIEOEZFeBP9uYrqCQx0x1TMYhpkh81lA+AUtPNfQ2xqRrcpwCOCvHBSbs6MhxBeC6HD+3AtYTue4CPJLrPliH5XkOUJJrsA8j3pSnAfBVnuoCYZflawOxnnynCVn5rZFl2K6R78uMPVRgNxxQ4DaBgwMFjsJxBXq78K2va+wMpDVWXIcnpZCbcEchJ3Bt+aGQZ/BSId+WcNyT8SAvIwewt6+ZuklYqMqonTvfkPECTsqoxSHxQcYRXsnIANyQUaAtIwtwS0aLnoxfe2B/R0aHj7L+F0t9Wa/Jaq40m79ojk8bYOd7zfRuFWDj1Tf/FKFdubgIazWRPDi5QqQ+AAAAAElFTkSuQmCC);background-size:18px 16px;background-repeat:no-repeat;background-position:left center;line-height:16px;text-transform:uppercase;font-size:14px;font-weight:bold}.payment-process-notation-description{line-height:18px;font-size:12px}.payment-process-success{width:140px;height:146px;margin:0 auto 20px;background-image:url(../images/payment-success.png)}@media(min--moz-device-pixel-ratio:1.3),(-webkit-min-device-pixel-ratio:1.3),(min-device-pixel-ratio:1.3),(min-resolution:1.3dppx){.payment-process-success{background-image:url("../images/payment-success@2x.png");background-size:100% 100%}}.crowdsale,.process{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;-ms-flex-direction:column;flex-direction:column;-webkit-box-pack:center;-ms-flex-pack:center;justify-content:center;position:absolute;height:50%;box-sizing:border-box}.home{min-height:600px}.crowdsale-modal{position:absolute;display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-pack:center;-ms-flex-pack:center;justify-content:center;-webkit-box-align:center;-ms-flex-align:center;align-items:center}.crowdsale-modal .modal{background:white;width:45%;min-width:830px;padding:30px 20px 0;border-radius:10px;position:relative;height:80%;min-height:480px;max-height:615px}.crowdsale-modal .modal .title{margin-bottom:20px;text-transform:uppercase;font-size:24px;font-weight:bold}.crowdsale-modal .modal .description{margin-bottom:25px;color:#8197a2;line-height:24px;font-size:13px}.crowdsale-modal .modal .close-button{position:absolute;top:-40px;right:-40px;z-index:400000;padding:15px}.crowdsale-modal .modal .close-button i.icon:before{content:url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABkAAAAZCAYAAADE6YVjAAAABmJLR0QA/wD/AP+gvaeTAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAB3RJTUUH4gEDETQvtZKRngAAAaJJREFUSMetlcFOwkAURV9NXLjURUNwBbToTlJ04ULDJ7jSjd+hbvkEl3yC/2L8CRODBklMQAwJHhdO8VHeQFu5SdN0+jq3b+6dOyIiAgRAD2i6ZymD9DvgBLjXLwLgmV98AlFZEjdfAkzdfE/pYI9FTIBWSYJzltEVoOk6yBKFBQn2DYIPoJoWRG5ijbFr3auR0qADzDLf94HKvNBdLYNoBMSWGRTBsdHBEKiZPweErgMNrxmAthI5xQDYy+OO0TozeER+Aeq5vA7Eno5CJfK3IXIll/3VWjcM142BW4PgT+QiO9ddRwZRFu9ekQsQWmbQBLuyCQA3HpKrTRFUjY2mNQr/m6ZnhsgYrmutSoZVBIlHg0uPvRuFjgkXFVOfyM4Mlr3jvPukY3TQB2qZOsveIyDJE9ezPDvZadAwlm7ZDEoDXxZV1mgX+7JuwQwekQdAfdUaq2RIjI4mQKQP/alxHuyVSIaJ4brDLRG5FpFtVf8qIu0gCIZFSIIgeBORUxH5UsM7InKR/sVj4bj2L1+kXPeQLejOD/3yEZTeD4C7dPwHuOyCT59BxPUAAAAASUVORK5CYII=")}.crowdsale-modal .modal .close-button:hover{cursor:pointer}.home .crowdsale-modal .modal{max-height:360px}.crowdsale{top:0;padding-top:80px;text-align:center}.crowdsale .container{padding:0 40px;box-sizing:border-box}.crowdsale .title{margin-bottom:20px;text-transform:uppercase;font-size:24px;font-weight:bold}.crowdsale .description{margin-bottom:25px;color:#8197a2;line-height:24px;font-size:13px}.crowdsale .buttons{font-size:0}.crowdsale .button{margin:0 10px}@-webkit-keyframes show-process{from{opacity:0;-webkit-transform:scale(0);transform:scale(0)}to{opacity:1;transition:scale(1)}}@keyframes show-process{from{opacity:0;-webkit-transform:scale(0);transform:scale(0)}to{opacity:1;transition:scale(1)}}.process{bottom:0;padding-bottom:60px;background-color:#f5f5f7;font-size:0}.process .step-icons{-webkit-transform:translateX(-50%);transform:translateX(-50%);position:absolute;left:50%;top:0}.process .step-icons_crowdsale-page{top:10px}.process .process-item{opacity:0;-webkit-animation-name:show-process;animation-name:show-process;-webkit-animation-duration:1s;animation-duration:1s;-webkit-animation-fill-mode:forwards;animation-fill-mode:forwards;position:relative;display:inline-block;vertical-align:top;width:20%;padding:130px 10px 0;box-sizing:border-box;text-align:center}.process .process-item:nth-child(1){-webkit-animation-delay:200ms;animation-delay:200ms}.process .process-item:nth-child(1):after{content:"1"}.process .process-item:nth-child(2){-webkit-animation-delay:400ms;animation-delay:400ms}.process .process-item:nth-child(2):after{content:"2"}.process .process-item:nth-child(3){-webkit-animation-delay:600ms;animation-delay:600ms}.process .process-item:nth-child(3):after{content:"3"}.process .process-item:nth-child(4){-webkit-animation-delay:800ms;animation-delay:800ms}.process .process-item:nth-child(4):after{content:"4"}.process .process-item:nth-child(5){-webkit-animation-delay:1000ms;animation-delay:1000ms}.process .process-item:nth-child(5):after{content:"5"}.process .process-item:after{position:absolute;width:32px;height:32px;right:28px;top:-16px;border-radius:50%;border:4px solid #fff;background-color:#08b3f2;color:#fff;line-height:32px;text-align:center;font-size:14px;font-weight:bold}.process .title{margin-bottom:10px;text-transform:uppercase;font-size:14px;font-weight:bold}.process .description{color:#8197a2;line-height:18px;font-size:12px}.steps pre{display:block;overflow:auto;height:200px;padding:15px;box-sizing:border-box;border:1px solid #eee;border-radius:3px;font-size:14px;word-break:break-all;word-wrap:break-word;white-space:pre-wrap}.steps .button-container{margin:40px 0}.steps .value{margin-bottom:10px}.steps .left,.steps .right,.steps .item{margin-bottom:25px}.steps .publish-title{position:relative;z-index:2;display:inline-block;padding-right:5px;padding-left:30px;text-transform:uppercase;font-size:11px;font-weight:bold;background-color:#fff}.steps .publish-title-container{position:relative;margin-bottom:25px;line-height:20px}.steps .publish-title-container:after{content:"";position:absolute;z-index:1;right:0;left:0;top:50%;height:1px;margin-top:-1px;border-bottom:1px dashed #eee}.steps .publish-title:before{content:attr(data-step);position:absolute;z-index:2;left:0;top:50%;width:20px;height:20px;border-radius:50%;margin-top:-10px;background-color:#08b3f2;color:#fff;line-height:20px;text-align:center}.steps .button{padding-left:30px;background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAUCAMAAACgaw2xAAAAMFBMVEX///////////////////////////////////////////////////////////////9Or7hAAAAAD3RSTlMAPT5BSUpRUt3f5ufp6vUxUI2NAAAASUlEQVR42rWROw7AMAxC3abN3+b+t80WZYAxb/STsAR2kr0+xnAgEhMVwryhTLpjvg5CFOugTCXc/sGj1PM7d12irl0Plb3taRdkbQmzeFkz0wAAAABJRU5ErkJggg==);background-size:12px 10px;background-repeat:no-repeat;background-position:left 10px center;cursor:pointer}.steps .button.no_image{padding-left:15px;background-image:none;background-position:0 0;background-size:auto}.steps-content{padding:30px 30px 0;border-radius:8px;border:1px solid #eee;background-color:#fff}.steps-content .about-step{position:relative;min-height:100px;padding-left:120px;padding-bottom:30px}.steps:not(.steps_publish) .steps-content .about-step{margin-bottom:30px;border-bottom:1px solid #eee}.steps-content .about-step .step-icons{position:absolute;left:0;top:0}.steps-content .about-step .title{display:block;margin-bottom:10px;text-transform:uppercase;font-size:20px;font-weight:bold}.steps-content .about-step .description{line-height:24px;font-size:13px}.steps-content .description{color:#8197a2;line-height:20px;font-size:12px}.steps-navigation{margin-bottom:30px;padding:30px 0;border-bottom:1px solid #eee;background-color:#fff}.steps-navigation .container{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-pack:justify;-ms-flex-pack:justify;justify-content:space-between}.steps-navigation .step-navigation{position:relative;padding-left:30px;color:#8197a2;text-transform:uppercase;line-height:20px;font-size:11px;font-weight:bold}.steps-navigation .step-navigation:nth-child(1):before{content:"1"}.steps-navigation .step-navigation:nth-child(2):before{content:"2"}.steps-navigation .step-navigation:nth-child(3):before{content:"3"}.steps-navigation .step-navigation:nth-child(4):before{content:"4"}.steps-navigation .step-navigation:nth-child(5):before{content:"5"}.steps-navigation .step-navigation:before{-webkit-transform:translateY(-50%);transform:translateY(-50%);position:absolute;width:20px;height:20px;left:0;top:50%;border-radius:50%;background-color:rgba(129,151,162,0.4);color:#fff;line-height:20px;text-align:center;font-size:11px;font-weight:bold}.steps-navigation .step-navigation_active{color:#333}.steps-navigation .step-navigation_active:before{background-color:#08b3f2}.steps .radios input{display:none}.steps .radios input:checked+.title:before{background-color:#642f9c}.steps .radio{cursor:pointer;position:relative;display:block;margin-bottom:60px;padding-left:45px}.steps .radio:not(:last-child):after{cursor:default;content:'';position:absolute;left:0;right:0;bottom:-29px;height:1px;background-color:#eee}.steps .radio:last-child{margin-bottom:30px}.steps .radio .title{position:relative;display:block;margin-bottom:5px;line-height:20px;text-transform:uppercase;font-weight:bold}.steps .radio .title:before{content:'';position:absolute;left:-45px;top:50%;width:10px;height:10px;margin-top:-5px;border:5px solid #fff;box-shadow:0 0 0 1px #cdd5da;border-radius:50%}.steps .radio .title_soon:after{content:'soon';display:inline-block;vertical-align:top;margin-left:5px;padding:0 5px;border-radius:2px;background-color:#642f9c;color:#fff;line-height:20px;text-transform:uppercase;font-size:10px;font-weight:bold}.steps .radio .description{color:#8197a2;line-height:20px;font-size:12px}.steps .radios-inline{height:36px;vertical-align:middle;margin-bottom:15px}.steps .radios-inline input{display:none}.steps .radios-inline input:checked+.title:before{background-color:#642f9c}.steps .radio-inline{cursor:pointer;position:relative;display:table-cell;padding-left:30px;padding-right:20px;line-height:36px}.steps .radio-inline:last-child{margin-bottom:30px}.steps .radio-inline .title{position:relative;display:block}.steps .radio-inline .title:before{content:'';position:absolute;left:-29px;top:50%;width:10px;height:10px;margin-top:-8.5px;border:5px solid #fff;box-shadow:0 0 0 1px #cdd5da;border-radius:50%}.reserved-tokens-title{display:block;margin-bottom:30px;text-transform:uppercase;font-size:20px;font-weight:bold}.reserved-tokens-container{margin-bottom:40px}.reserved-tokens-radio-container{height:36px;vertical-align:middle;display:table;width:100%;margin-bottom:15px}.reserved-tokens-radio-container-item{display:inline;line-height:36px;vertical-align:middle;margin-right:10px}.reserved-tokens-input-property{width:33%}.reserved-tokens-input-property-left{display:table-cell;vertical-align:top;padding-right:5px}.reserved-tokens-input-property-middle{display:table-cell;vertical-align:top;padding-left:5px;padding-right:5px}.reserved-tokens-input-property-right{display:table-cell;vertical-align:top;padding-left:5px}.reserved-tokens-input-container{margin-bottom:20px;display:table}.reserved-tokens-item{display:table-cell;height:60px;line-height:60px;width:33%}.reserved-tokens-item-left{padding-right:5px}.reserved-tokens-item-middle{padding-left:5px;padding-right:5px}.reserved-tokens-item-right{padding-left:5px}.reserved-tokens-item-empty{display:table-cell;line-height:60px;width:56px}.reserved-tokens-item-container{border-top:1px solid #eee;display:table;width:100%}.reserved-tokens-item-container-last{border-bottom:1px solid #eee}.reserved-tokens-item-container-inner{display:table;width:100%}.total-funds .right{text-align:right}.total-funds-title{margin-bottom:15px;color:#642f9c;font-size:24px;font-weight:bold}.total-funds-description{color:#8197a2;font-size:14px}.total-funds-chart{position:relative;z-index:1;height:20px;margin-bottom:30px;border-radius:10px;background-color:#f5f5f5}.total-funds-chart-active{position:absolute;left:0;top:0;bottom:0;border-radius:10px;background-image:linear-gradient(to right,#7738b9,#853ecf)}.total-funds-chart-container{position:relative}.total-funds-chart-division{position:absolute;z-index:0;top:-5px;bottom:-5px;width:1px;background-color:#ddd}.total-funds-chart-division:nth-child(1){left:10%}.total-funds-chart-division:nth-child(2){left:20%}.total-funds-chart-division:nth-child(3){left:30%}.total-funds-chart-division:nth-child(4){left:40%}.total-funds-chart-division:nth-child(5){left:50%}.total-funds-chart-division:nth-child(6){left:60%}.total-funds-chart-division:nth-child(7){left:70%}.total-funds-chart-division:nth-child(8){left:80%}.total-funds-chart-division:nth-child(9){left:90%}.total-funds-statistics .left,.total-funds-statistics .right{width:50%;box-sizing:border-box}.total-funds-statistics .title,.total-funds-statistics .hash{margin-bottom:10px;color:#642f9c;font-weight:bold}.total-funds-statistics .title{font-size:16px}.total-funds-statistics .hash{font-size:14px}.white-list-container{margin-bottom:40px}.white-list-input-property-left{display:table-cell;vertical-align:top;padding-right:5px;width:42%}.white-list-input-property-middle{display:table-cell;vertical-align:top;padding-left:5px;padding-right:5px;width:29%}.white-list-input-property-right{display:table-cell;vertical-align:top;padding-left:5px;width:29%}.white-list-input-container{margin-bottom:20px;display:table}.white-list-item{display:table-cell;height:60px;line-height:60px}.white-list-item-left{padding-right:5px;width:42%}.white-list-item-middle{padding-left:5px;padding-right:5px;width:29%}.white-list-item-right{padding-left:5px;width:29%}.white-list-item-empty{display:table-cell;width:56px}.white-list-item-container{font-weight:bold;border-top:1px solid #eee;display:table;width:100%}.white-list-item-container-last{border-bottom:1px solid #eee}.white-list-item-container-inner{display:table;width:100%}.white-list-item-container.to-be-removed{font-weight:normal;color:orange;text-decoration:line-through}.white-list-item-container.no-style{font-weight:normal;color:#555}.white-list-item-container.duplicated{padding-bottom:15px;border-top:1px dashed orange;margin-top:-15px}.white-list-item-container.duplicated span{line-height:30px;height:12px}@-webkit-keyframes fadeOut{0%{opacity:.2}20%{opacity:1;-webkit-transform:scale(1);transform:scale(1)}100%{opacity:.2;-webkit-transform:scale(0.3);transform:scale(0.3)}}@keyframes fadeOut{0%{opacity:.2}20%{opacity:1;-webkit-transform:scale(1);transform:scale(1)}100%{opacity:.2;-webkit-transform:scale(0.3);transform:scale(0.3)}}.loading{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-pack:justify;-ms-flex-pack:justify;justify-content:space-between;position:absolute;left:50%;top:50%;width:206px;margin:-30px 0 0 -111.5px;padding-top:50px}.loading:before{content:'';position:absolute;left:0;top:0;width:206px;height:35px;background-image:url("../images/loading.png");background-position:0 0}@media(min--moz-device-pixel-ratio:1.3),(-webkit-min-device-pixel-ratio:1.3),(min-device-pixel-ratio:1.3),(min-resolution:1.3dppx){.loading:before{background-image:url("../images/loading@2x.png");background-size:100% 100%}}.loading-container{position:fixed;z-index:1000000;left:0;right:0;top:0;bottom:0;background-color:rgba(35,29,115,0.8)}.loading-text-container{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-pack:justify;-ms-flex-pack:justify;justify-content:space-between;position:absolute;left:50%;top:50%;width:206px;margin:-30px 0 0 -111.5px;padding-top:45px}.loading-text{color:white;font-weight:bold;text-align:justify;word-spacing:2px;width:100%;font-size:14.2px}.loading-progress{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-pack:justify;-ms-flex-pack:justify;justify-content:space-between;position:absolute;left:50%;top:50%;width:206px;margin:-30px 0 0 -111.5px;padding-top:70px}.loading-i{-webkit-animation-duration:2s;animation-duration:2s;-webkit-animation-fill-mode:forwards;animation-fill-mode:forwards;-webkit-animation-iteration-count:infinite;animation-iteration-count:infinite;-webkit-animation-name:fadeOut;animation-name:fadeOut;-webkit-animation-timing-function:linear;animation-timing-function:linear;opacity:.2;width:9px;height:9px;border-radius:50%;background-color:#fff}.loading-i:nth-child(2){-webkit-animation-delay:.1s;animation-delay:.1s}.loading-i:nth-child(3){-webkit-animation-delay:.2s;animation-delay:.2s}.loading-i:nth-child(4){-webkit-animation-delay:.3s;animation-delay:.3s}.loading-i:nth-child(5){-webkit-animation-delay:.4s;animation-delay:.4s}.loading-i:nth-child(6){-webkit-animation-delay:.5s;animation-delay:.5s}.flex-table{height:88%;display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;-ms-flex-direction:column;flex-direction:column;min-height:58vh}.flex-table .container-fluid{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;-ms-flex-direction:column;flex-direction:column;width:100%;height:90%;min-height:380px;max-height:560px}.flex-table .table-row{display:-webkit-box;display:-ms-flexbox;display:flex;display:-webkit-flex;-webkit-box-orient:horizontal;-webkit-box-direction:normal;-ms-flex-direction:row;flex-direction:row;-webkit-box-flex:0;-ms-flex-positive:0;flex-grow:0;-webkit-flex-grow:0;-ms-flex-wrap:wrap;flex-wrap:wrap;-webkit-flex-wrap:wrap;width:100%;padding-left:15px;padding-right:15px}.flex-table .sm-text,.flex-table .text{-webkit-box-flex:2;-ms-flex-positive:2;flex-grow:2;padding-right:0}.flex-table .sm-text{-webkit-box-flex:1;-ms-flex-positive:1;flex-grow:1;-webkit-flex-grow:1}.flex-table .text{width:260px}.flex-table .sm-text{width:100px}.flex-table .num{width:80px}.flex-table .table-row{border-collapse:collapse;padding-bottom:15px;padding-top:15px;-webkit-box-align:center;-ms-flex-align:center;align-items:center}.flex-table .table-row.flex-table-header{padding-top:8px;padding-bottom:8px;width:99%}.flex-table .table-row.selected{background-color:#08b3f2;color:#fff}.flex-table .table-row.clickable:hover{cursor:pointer}.flex-table .table-row.datagrid{padding-bottom:5px;padding-top:5px}.flex-table .table-row.datagrid:nth-child(even){background-color:#e8e8e8}.flex-table .scrollable-content{border:1px solid lightgray;min-height:185px;max-height:100%;height:90%;overflow-x:hidden;overflow-y:auto}.flex-table .scrollable-content::-webkit-scrollbar{width:5px}.flex-table .scrollable-content::-webkit-scrollbar-track{border-left:1px solid lightgray;padding:3px}.flex-table .scrollable-content::-webkit-scrollbar-thumb{background:lightgray}.flex-table .steps{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-pack:center;-ms-flex-pack:center;justify-content:center;-webkit-box-align:center;-ms-flex-align:center;align-items:center}.home .flex-table{height:100%;min-height:56vh}.home .flex-table .scrollable-content{min-height:160px;height:55%;margin-bottom:2%}.manage{min-height:600px}.manage .warning-logo{color:#642f9c !important;border-color:#642f9c !important;position:absolute;left:0;top:0;display:-webkit-box;display:-ms-flexbox;display:flex}.manage .white-list-item-container .remove{display:-webkit-inline-box;display:-ms-inline-flexbox;display:inline-flex;margin-top:8px}.manage .white-list-item-container .swal2-icon{width:14px;height:14px;line-height:14px;border:2px solid transparent}.manage .white-list-item-container .warning-logo{color:orange !important;border-color:orange !important;position:relative;float:right;font-size:14px;font-weight:bold}.manage .steps-content{margin-top:30px}.manage .description{margin-bottom:20px}.manage .crowdsale-page-link{color:#08b3f2;text-decoration:none;font-size:13px}.manage .divisor{margin-bottom:30px;border-bottom:1px solid #eee}.manage .no-arrow{background-image:none;padding:0 15px} \ No newline at end of file diff --git a/src/assets/stylesheets/application/controls.scss b/src/assets/stylesheets/application/controls.scss index c525d6b96..bb2a86265 100644 --- a/src/assets/stylesheets/application/controls.scss +++ b/src/assets/stylesheets/application/controls.scss @@ -31,6 +31,8 @@ color: #9c9c9c; &:hover { + background-color: #e6e6e6; + color: #9c9c9c; cursor: default !important; } } @@ -98,6 +100,6 @@ } &:disabled { - background: #f2f2f2; + background: #f2f2f2; } } diff --git a/src/components/stepThree/index.js b/src/components/stepThree/index.js index 846a126b7..3592f5d0c 100644 --- a/src/components/stepThree/index.js +++ b/src/components/stepThree/index.js @@ -1,30 +1,49 @@ import React from "react"; import "../../assets/stylesheets/application.css"; +import { Field, Form, FormSpy } from 'react-final-form' +import arrayMutators from 'final-form-arrays' +import { FieldArray } from 'react-final-form-arrays' import { Link } from "react-router-dom"; import { setExistingContractParams, getNetworkVersion, getNetWorkNameById } from "../../utils/blockchainHelpers"; import { gweiToWei, weiToGwei } from "../../utils/utils"; import { StepNavigation } from "../Common/StepNavigation"; import { RadioInputField } from "../Common/RadioInputField"; +import { InputField2 } from "../Common/InputField2"; import { CrowdsaleBlock } from "./CrowdsaleBlock"; +import { WhitelistInputBlock } from '../Common/WhitelistInputBlock' +import { defaultCompanyStartDate, defaultCompanyEndDate } from './utils' import { NAVIGATION_STEPS, VALIDATION_MESSAGES, VALIDATION_TYPES, TEXT_FIELDS, CHAINS, + DESCRIPTION, defaultTier, defaultTierValidations } from '../../utils/constants' import { inject, observer } from "mobx-react"; import { Loader } from '../Common/Loader' import { noGasPriceAvailable, warningOnMainnetAlert } from '../../utils/alerts' +import { isAddress, isNonNegative, isPositive, isRequired } from '../../utils/validations' import { NumericInput } from '../Common/NumericInput' import update from 'immutability-helper' import { AddressInput } from '../Common/AddressInput' +import classnames from 'classnames' const { CROWDSALE_SETUP } = NAVIGATION_STEPS; const { VALID, INVALID } = VALIDATION_TYPES; -const { MINCAP, WALLET_ADDRESS, ENABLE_WHITELISTING } = TEXT_FIELDS; +const { + ALLOWMODIFYING, + CROWDSALE_SETUP_NAME, + MINCAP, + WALLET_ADDRESS, + ENABLE_WHITELISTING, + START_TIME, + END_TIME, + RATE, + SUPPLY +} = TEXT_FIELDS; @inject( "contractStore", @@ -70,24 +89,29 @@ export class stepThree extends React.Component { } } - componentDidMount () { - const { gasPriceStore, tierStore } = this.props + componentWillMount () { + // const { gasPriceStore, tierStore } = this.props + if (this.props.tierStore.tiers.length === 0) { + this.addCrowdsale() + this.initialTiers = JSON.parse(JSON.stringify(this.props.tierStore.tiers)) + this.initialTiers[0].startTime = defaultCompanyStartDate() + this.initialTiers[0].endTime = defaultCompanyEndDate() + } - gasPriceStore.updateValues() - .then(() => this.setGasPrice(gasPriceStore.slow)) - .catch(() => noGasPriceAvailable()) - .then(() => { - if (this.props.tierStore.tiers.length === 0) { - this.addCrowdsale() - } - this.setState({ loading: false }) - this.updateWalletAddress({ - address: tierStore.tiers[0].walletAddress, - pristine: true, - valid: VALID, - }) - window.scrollTo(0, 0) - }) + window.scrollTo(0, 0) + + // gasPriceStore.updateValues() + // .then(() => this.setGasPrice(gasPriceStore.slow)) + // .catch(() => noGasPriceAvailable()) + // .then(() => { + // this.setState({ loading: false }) + // this.updateWalletAddress({ + // address: tierStore.tiers[0].walletAddress, + // pristine: true, + // valid: VALID, + // }) + // window.scrollTo(0, 0) + // }) } showErrorMessages = () => { @@ -126,10 +150,7 @@ export class stepThree extends React.Component { this.props.history.push('/4') } - beforeNavigate = e => { - e.preventDefault() - e.stopPropagation() - + beforeNavigate = () => { const { tierStore, gasPriceStore } = this.props const gasPriceIsValid = gasPriceStore.custom.id !== this.state.gasPriceSelected || this.state.validation.gasPrice.valid === VALID const isMinCapLessThanMaxSupply = tierStore.globalMinCap <= tierStore.maxSupply @@ -179,10 +200,7 @@ export class stepThree extends React.Component { }) .catch(error => { console.error(error) - this.showErrorMessages(e) }) - } else { - this.showErrorMessages(e) } } @@ -328,80 +346,329 @@ export class stepThree extends React.Component { this.updateTierStore(e, "whitelistEnabled", 0) } - render() { - const { tierStore } = this.props; + inputErrorStyle = { + color: 'red', + fontWeight: 'bold', + fontSize: '12px', + width: '100%', + height: '10px', + } - const globalSettingsBlock = ( -
-
-

Global settings

-
-
- - {this.renderGasPriceInput()} -
-
- - this.updateWhitelistEnabled(e)} - description="Enables whitelisting. If disabled, anyone can participate in the crowdsale." - /> -
-
- ) + render() { + const { generalStore, tierStore } = this.props return (
-
-
-
-

Crowdsale setup

-

The most important and exciting part of the crowdsale process. Here you can - define parameters of your crowdsale campaign.

-
- {globalSettingsBlock} -
- -
- { tierStore.tiers.map((tier, index) => ) } -
- -
-
this.addCrowdsale()} className="button button_fill_secondary">Add Tier
- this.beforeNavigate(e)} className="button button_fill" to="/4">Continue -
- - +
{ + const submitButtonClass = classnames('button', 'button_fill', { + button_disabled: pristine || invalid + }) + + return ( + +
+
+
+
+

Crowdsale setup

+

The most important and exciting part of the crowdsale process. Here you can + define parameters of your crowdsale campaign.

+
+
+

Global settings

+
+
+ + + +
+
+ + ( +
+ +
+ + +
+

Enables whitelisting. If disabled, anyone can participate in the crowdsale.

+
+ )} + /> +
+
+
+ + + {({ fields }) => ( +
+ {fields.map((name, index) => ( +
+
+
+ + ( +
+ +
+ + +
+

{DESCRIPTION.ALLOW_MODIFYING}

+
+ )} + /> +
+ +
+ + +
+ +
+ + +
+
+
+ ))} +
+
{ + this.addCrowdsale() + const lastTier = this.props.tierStore.tiers[this.props.tierStore.tiers.length - 1] + fields.push(JSON.parse(JSON.stringify(lastTier))) + }}> + Add Tier +
+
+
+ )} +
+ +
+ Continue +
+ + { + tierStore.updateWalletAddress(values.walletAddress, VALID) + generalStore.setGasPrice(gweiToWei(values.gasPrice || 0)) + tierStore.setGlobalMinCap(values.minCap || 0) + tierStore.setTierProperty(values.whitelistEnabled, "whitelistEnabled", 0) + + values.tiers.forEach((tier, index) => { + tierStore.setTierProperty(tier.tier, 'tier', index) + tierStore.setTierProperty(tier.updatable, 'updatable', index) + tierStore.setTierProperty(tier.startTime, 'startTime', index) + tierStore.setTierProperty(tier.endTime, 'endTime', index) + tierStore.updateRate(tier.rate, VALID, index) + tierStore.setTierProperty(tier.supply, 'supply', index) + tierStore.validateTiers('supply', index) + }) + }} + /> + + ) + }} + />
) } + + // render() { + // const { tierStore } = this.props; + + // const globalSettingsBlock = ( + //
+ //
+ //

Global settings

+ //
+ //
+ // + // {this.renderGasPriceInput()} + //
+ //
+ // + // this.updateWhitelistEnabled(e)} + // description="Enables whitelisting. If disabled, anyone can participate in the crowdsale." + // /> + //
+ //
+ // ) + + // return ( + //
+ // + //
+ //
+ //
+ //

Crowdsale setup

+ //

The most important and exciting part of the crowdsale process. Here you can + // define parameters of your crowdsale campaign.

+ //
+ // {globalSettingsBlock} + //
+ + //
+ // { tierStore.tiers.map((tier, index) => ) } + //
+ + //
+ //
this.addCrowdsale()} className="button button_fill_secondary">Add Tier
+ // this.beforeNavigate(e)} className="button button_fill" to="/4">Continue + //
+ + // + //
+ // ) + // } } diff --git a/src/utils/validations.js b/src/utils/validations.js index ee834aa14..fd9f1259b 100644 --- a/src/utils/validations.js +++ b/src/utils/validations.js @@ -1,3 +1,4 @@ +import Web3 from 'web3' import { VALIDATION_MESSAGES } from './constants' export const validators = (type, value) => { @@ -23,4 +24,22 @@ export const validateDecimals = (value) => { return isValid ? undefined : VALIDATION_MESSAGES.DECIMALS } +export const isPositive = (errorMsg = 'Please enter a valid number greater than 0') => (value) => { + const isValid = value > 0 + return isValid ? undefined : errorMsg +} + +export const isNonNegative = (errorMsg = 'Please enter a valid number greater or equal than 0') => (value) => { + const isValid = value >= 0 + return isValid ? undefined : errorMsg +} +export const isAddress = (errorMsg = 'Please enter a valid address') => (value) => { + const isValid = Web3.utils.isAddress(value) + return isValid ? undefined : errorMsg +} + +export const isRequired = (errorMsg = 'This field is required') => (value) => { + const isValid = !!value + return isValid ? undefined : errorMsg +} From de0d86ade9325f66108c9893aafbb71bede4a16f Mon Sep 17 00:00:00 2001 From: Franco Victorio Date: Wed, 28 Mar 2018 10:27:27 -0300 Subject: [PATCH 03/48] Custom GasPriceInput component --- src/components/stepThree/GasPriceInput.js | 91 +++++ src/components/stepThree/index.js | 460 +++++++++++----------- 2 files changed, 326 insertions(+), 225 deletions(-) create mode 100644 src/components/stepThree/GasPriceInput.js diff --git a/src/components/stepThree/GasPriceInput.js b/src/components/stepThree/GasPriceInput.js new file mode 100644 index 000000000..813b11c32 --- /dev/null +++ b/src/components/stepThree/GasPriceInput.js @@ -0,0 +1,91 @@ +import React, { Component } from 'react' +import { InputField2 } from '../Common/InputField2' + +class GasPriceInput extends Component { + constructor(props) { + super(props) + + this.state = { + isCustom: false, + customGasPrice: 0 + } + } + + handleNonCustomSelected = (value) => { + this.setState({ + isCustom: false + }) + this.props.input.onChange(value) + } + + handleCustomSelected = () => { + const { input } = this.props + + this.setState({ + isCustom: true + }) + + input.onChange(Object.assign({}, { + id: 'CUSTOM', + gasPrice: this.state.customGasPrice + })) + } + + handleCustomGasPriceChange = (value) => { + const { input } = this.props + + this.setState({ + customGasPrice: value + }) + + input.onChange(Object.assign({}, { + id: 'CUSTOM', + gasPrice: value + })) + } + + render () { + const { input, side, gasPrices } = this.props + + return ( +
+ + {gasPrices.map((gasPrice, index) => ( + gasPrice.id !== 'CUSTOM' ? ( +
+ +
+ ) : ( +
+ +
+ ) + ))} + {this.state.isCustom ? ( + this.handleCustomGasPriceChange(e.target.value)} + /> + ) : null} +

Slow is cheap, fast is expensive

+
+ ) + } +} + +export default GasPriceInput diff --git a/src/components/stepThree/index.js b/src/components/stepThree/index.js index 3592f5d0c..3edfec0e3 100644 --- a/src/components/stepThree/index.js +++ b/src/components/stepThree/index.js @@ -11,6 +11,7 @@ import { RadioInputField } from "../Common/RadioInputField"; import { InputField2 } from "../Common/InputField2"; import { CrowdsaleBlock } from "./CrowdsaleBlock"; import { WhitelistInputBlock } from '../Common/WhitelistInputBlock' +import GasPriceInput from './GasPriceInput' import { defaultCompanyStartDate, defaultCompanyEndDate } from './utils' import { NAVIGATION_STEPS, @@ -152,7 +153,7 @@ export class stepThree extends React.Component { beforeNavigate = () => { const { tierStore, gasPriceStore } = this.props - const gasPriceIsValid = gasPriceStore.custom.id !== this.state.gasPriceSelected || this.state.validation.gasPrice.valid === VALID + // const gasPriceIsValid = gasPriceStore.custom.id !== this.state.gasPriceSelected || this.state.validation.gasPrice.valid === VALID const isMinCapLessThanMaxSupply = tierStore.globalMinCap <= tierStore.maxSupply const isMinCapValid = this.state.validation.minCap.valid === VALID @@ -171,7 +172,7 @@ export class stepThree extends React.Component { })) } - if (tierStore.areTiersValid && gasPriceIsValid && isMinCapValid && isMinCapLessThanMaxSupply) { + if (tierStore.areTiersValid /* && gasPriceIsValid */ && isMinCapValid && isMinCapLessThanMaxSupply) { const { reservedTokenStore, deploymentStore } = this.props const tiersCount = tierStore.tiers.length const reservedCount = reservedTokenStore.tokens.length @@ -354,243 +355,252 @@ export class stepThree extends React.Component { height: '10px', } + gasPrices = [{ + id: 'SLOW', + price: 1, + description: 'Slow' + }, { + id: 'FAST', + price: 2, + description: 'Fast' + }, { + id: 'CUSTOM', + description: 'Custom' + }] + render() { const { generalStore, tierStore } = this.props return (
-
{ - const submitButtonClass = classnames('button', 'button_fill', { - button_disabled: pristine || invalid - }) - - return ( - -
-
-
-
-

Crowdsale setup

-

The most important and exciting part of the crowdsale process. Here you can - define parameters of your crowdsale campaign.

-
-
-

Global settings

-
-
- - - -
-
- - ( -
- -
- - -
-

Enables whitelisting. If disabled, anyone can participate in the crowdsale.

-
- )} - /> -
+ { + const submitButtonClass = classnames('button', 'button_fill', { + button_disabled: pristine || invalid + }) + + return ( + +
+
+
+
+

Crowdsale setup

+

The most important and exciting part of the crowdsale process. Here you can + define parameters of your crowdsale campaign.

-
- - - {({ fields }) => ( -
- {fields.map((name, index) => ( -
-
-
- - ( -
- -
- - -
-

{DESCRIPTION.ALLOW_MODIFYING}

-
- )} - /> -
- -
- - -
- -
- +

Global settings

+
+
+ + + +
+
+ + ( +
+ +
+ +
+ no +
+

Enables whitelisting. If disabled, anyone can participate in the crowdsale.

- ))} -
-
{ - this.addCrowdsale() - const lastTier = this.props.tierStore.tiers[this.props.tierStore.tiers.length - 1] - fields.push(JSON.parse(JSON.stringify(lastTier))) - }}> - Add Tier + )} + /> +
+
+
+ + + {({ fields }) => ( +
+ {fields.map((name, index) => ( +
+
+
+ + ( +
+ +
+ + +
+

{DESCRIPTION.ALLOW_MODIFYING}

+
+ )} + /> +
+ +
+ + +
+ +
+ + +
+ ))} +
+
{ + this.addCrowdsale() + const lastTier = this.props.tierStore.tiers[this.props.tierStore.tiers.length - 1] + fields.push(JSON.parse(JSON.stringify(lastTier))) + }}> + Add Tier +
- )} - - -
- Continue -
- - { - tierStore.updateWalletAddress(values.walletAddress, VALID) - generalStore.setGasPrice(gweiToWei(values.gasPrice || 0)) - tierStore.setGlobalMinCap(values.minCap || 0) - tierStore.setTierProperty(values.whitelistEnabled, "whitelistEnabled", 0) - - values.tiers.forEach((tier, index) => { - tierStore.setTierProperty(tier.tier, 'tier', index) - tierStore.setTierProperty(tier.updatable, 'updatable', index) - tierStore.setTierProperty(tier.startTime, 'startTime', index) - tierStore.setTierProperty(tier.endTime, 'endTime', index) - tierStore.updateRate(tier.rate, VALID, index) - tierStore.setTierProperty(tier.supply, 'supply', index) - tierStore.validateTiers('supply', index) - }) - }} - /> - - ) - }} - /> +
+ )} +
+ +
+ Continue +
+ + { + tierStore.updateWalletAddress(values.walletAddress, VALID) + generalStore.setGasPrice(gweiToWei(values.gasPrice.price || 0)) + tierStore.setGlobalMinCap(values.minCap || 0) + tierStore.setTierProperty(values.whitelistEnabled, "whitelistEnabled", 0) + + values.tiers.forEach((tier, index) => { + tierStore.setTierProperty(tier.tier, 'tier', index) + tierStore.setTierProperty(tier.updatable, 'updatable', index) + tierStore.setTierProperty(tier.startTime, 'startTime', index) + tierStore.setTierProperty(tier.endTime, 'endTime', index) + tierStore.updateRate(tier.rate, VALID, index) + tierStore.setTierProperty(tier.supply, 'supply', index) + tierStore.validateTiers('supply', index) + }) + }} + /> + + ) + }} + />
) } From b883253bde64e7f54408918026628b7bab97e320 Mon Sep 17 00:00:00 2001 From: Franco Victorio Date: Wed, 28 Mar 2018 11:02:21 -0300 Subject: [PATCH 04/48] Add whitelist block --- src/components/stepThree/index.js | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/src/components/stepThree/index.js b/src/components/stepThree/index.js index 3edfec0e3..0fbc47fce 100644 --- a/src/components/stepThree/index.js +++ b/src/components/stepThree/index.js @@ -94,7 +94,9 @@ export class stepThree extends React.Component { // const { gasPriceStore, tierStore } = this.props if (this.props.tierStore.tiers.length === 0) { this.addCrowdsale() + this.initialTiers = JSON.parse(JSON.stringify(this.props.tierStore.tiers)) + this.initialTiers[0].startTime = defaultCompanyStartDate() this.initialTiers[0].endTime = defaultCompanyEndDate() } @@ -492,8 +494,8 @@ export class stepThree extends React.Component { input.onChange('on')} + value='on' /> on @@ -559,6 +561,16 @@ export class stepThree extends React.Component { /> + { + tierStore.tiers[index].whitelistEnabled === 'yes' ? ( +
+
+

Whitelist

+
+ +
+ ) : null + } ))}
From 02bf232fdc785bf1661bd440b20c74f3d2ea5bb2 Mon Sep 17 00:00:00 2001 From: fernandomg Date: Thu, 29 Mar 2018 10:38:31 -0300 Subject: [PATCH 05/48] Set initial value for wallet address --- src/components/stepThree/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/stepThree/index.js b/src/components/stepThree/index.js index 0fbc47fce..2f5691a21 100644 --- a/src/components/stepThree/index.js +++ b/src/components/stepThree/index.js @@ -380,7 +380,7 @@ export class stepThree extends React.Component { onSubmit={this.beforeNavigate} mutators={{ ...arrayMutators }} initialValues={{ - walletAddress: '', + walletAddress: tierStore.tiers[0].walletAddress, minCap: 0, gasPrice: this.gasPrices[0], whitelistEnabled: "no", From c1f7cec74f8dbcf11a12140c19f44ed6e6d6e433 Mon Sep 17 00:00:00 2001 From: fernandomg Date: Thu, 29 Mar 2018 10:39:29 -0300 Subject: [PATCH 06/48] Set minCap default value to empty string --- src/components/stepThree/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/stepThree/index.js b/src/components/stepThree/index.js index 2f5691a21..a0113457b 100644 --- a/src/components/stepThree/index.js +++ b/src/components/stepThree/index.js @@ -381,7 +381,7 @@ export class stepThree extends React.Component { mutators={{ ...arrayMutators }} initialValues={{ walletAddress: tierStore.tiers[0].walletAddress, - minCap: 0, + minCap: '', gasPrice: this.gasPrices[0], whitelistEnabled: "no", tiers: this.initialTiers From f174929570c287c7b29d298de37808a31b8219d7 Mon Sep 17 00:00:00 2001 From: fernandomg Date: Thu, 29 Mar 2018 10:41:38 -0300 Subject: [PATCH 07/48] Move add tier button to 'button-container' block --- src/components/stepThree/index.js | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/src/components/stepThree/index.js b/src/components/stepThree/index.js index a0113457b..53b93b38e 100644 --- a/src/components/stepThree/index.js +++ b/src/components/stepThree/index.js @@ -386,7 +386,7 @@ export class stepThree extends React.Component { whitelistEnabled: "no", tiers: this.initialTiers }} - render={({ handleSubmit, values, invalid, errors, pristine }) => { + render={({ handleSubmit, values, invalid, errors, pristine, mutators: { push } }) => { const submitButtonClass = classnames('button', 'button_fill', { button_disabled: pristine || invalid }) @@ -573,20 +573,18 @@ export class stepThree extends React.Component { }
))} -
-
{ - this.addCrowdsale() - const lastTier = this.props.tierStore.tiers[this.props.tierStore.tiers.length - 1] - fields.push(JSON.parse(JSON.stringify(lastTier))) - }}> - Add Tier -
-
)}
+
{ + this.addCrowdsale() + const lastTier = this.props.tierStore.tiers[this.props.tierStore.tiers.length - 1] + push('tiers', JSON.parse(JSON.stringify(lastTier))) + }}> + Add Tier +
Continue
From 8f719df58e3108737b0ae334116d4cb24852c3d2 Mon Sep 17 00:00:00 2001 From: fernandomg Date: Thu, 29 Mar 2018 11:43:46 -0300 Subject: [PATCH 08/48] Use GasPriceStore values --- src/components/stepThree/index.js | 28 ++++++++-------------------- src/stores/GasPriceStore.js | 11 +++++++++++ src/stores/GasPriceStore.spec.js | 28 ++++++++++++++++++++++++++++ 3 files changed, 47 insertions(+), 20 deletions(-) diff --git a/src/components/stepThree/index.js b/src/components/stepThree/index.js index 53b93b38e..c523bac5c 100644 --- a/src/components/stepThree/index.js +++ b/src/components/stepThree/index.js @@ -91,7 +91,8 @@ export class stepThree extends React.Component { } componentWillMount () { - // const { gasPriceStore, tierStore } = this.props + const { gasPriceStore, tierStore } = this.props + if (this.props.tierStore.tiers.length === 0) { this.addCrowdsale() @@ -103,9 +104,9 @@ export class stepThree extends React.Component { window.scrollTo(0, 0) - // gasPriceStore.updateValues() - // .then(() => this.setGasPrice(gasPriceStore.slow)) - // .catch(() => noGasPriceAvailable()) + gasPriceStore.updateValues() + .then(() => this.setGasPrice(gasPriceStore.slow)) + .catch(() => noGasPriceAvailable()) // .then(() => { // this.setState({ loading: false }) // this.updateWalletAddress({ @@ -357,21 +358,8 @@ export class stepThree extends React.Component { height: '10px', } - gasPrices = [{ - id: 'SLOW', - price: 1, - description: 'Slow' - }, { - id: 'FAST', - price: 2, - description: 'Fast' - }, { - id: 'CUSTOM', - description: 'Custom' - }] - render() { - const { generalStore, tierStore } = this.props + const { generalStore, tierStore, gasPriceStore } = this.props return (
@@ -382,7 +370,7 @@ export class stepThree extends React.Component { initialValues={{ walletAddress: tierStore.tiers[0].walletAddress, minCap: '', - gasPrice: this.gasPrices[0], + gasPrice: gasPriceStore.gasPrices[0], whitelistEnabled: "no", tiers: this.initialTiers }} @@ -420,7 +408,7 @@ export class stepThree extends React.Component { name="gasPrice" component={GasPriceInput} side="right" - gasPrices={this.gasPrices} + gasPrices={gasPriceStore.gasPrices} />
diff --git a/src/stores/GasPriceStore.js b/src/stores/GasPriceStore.js index c0199b430..00b3b296f 100644 --- a/src/stores/GasPriceStore.js +++ b/src/stores/GasPriceStore.js @@ -89,6 +89,17 @@ class GasPriceStore { get customDescription () { return GAS_PRICE.CUSTOM.DESCRIPTION } + + @computed + get gasPrices () { + return [ + { ...this.slow, description: this.slowDescription }, + { ...this.standard, description: this.standardDescription }, + { ...this.fast, description: this.fastDescription }, + { ...this.instant, description: this.instantDescription }, + { ...this.custom, description: this.customDescription }, + ] + } } export default GasPriceStore diff --git a/src/stores/GasPriceStore.spec.js b/src/stores/GasPriceStore.spec.js index 4e4068667..07a9db97d 100644 --- a/src/stores/GasPriceStore.spec.js +++ b/src/stores/GasPriceStore.spec.js @@ -37,6 +37,34 @@ describe('GasPriceStore', () => { expect(gas.health).toBeUndefined() }) + it('should return gasPrices collection', () => { + expect(gas.gasPrices[0]).toEqual({ + id: GAS_PRICE.SLOW.ID, + price: GAS_PRICE.SLOW.PRICE, + description: gas.slowDescription, + }) + expect(gas.gasPrices[1]).toEqual({ + id: GAS_PRICE.NORMAL.ID, + price: GAS_PRICE.NORMAL.PRICE, + description: gas.standardDescription, + }) + expect(gas.gasPrices[2]).toEqual({ + id: GAS_PRICE.FAST.ID, + price: GAS_PRICE.FAST.PRICE, + description: gas.fastDescription, + }) + expect(gas.gasPrices[3]).toEqual({ + id: GAS_PRICE.INSTANT.ID, + price: GAS_PRICE.INSTANT.PRICE, + description: gas.instantDescription, + }) + expect(gas.gasPrices[4]).toEqual({ + id: GAS_PRICE.CUSTOM.ID, + price: GAS_PRICE.CUSTOM.PRICE, + description: gas.customDescription, + }) + }) + it('should updates values with mocked data', () => { const gasPrice = require('../utils/__mocks__/gasPrice.json') From 2ff0719bf02767943cff5756f7a2808643fad68d Mon Sep 17 00:00:00 2001 From: fernandomg Date: Thu, 29 Mar 2018 11:44:24 -0300 Subject: [PATCH 09/48] Use constant values for CUSTOM field --- src/components/stepThree/GasPriceInput.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/components/stepThree/GasPriceInput.js b/src/components/stepThree/GasPriceInput.js index 813b11c32..5465deeaf 100644 --- a/src/components/stepThree/GasPriceInput.js +++ b/src/components/stepThree/GasPriceInput.js @@ -1,5 +1,5 @@ import React, { Component } from 'react' -import { InputField2 } from '../Common/InputField2' +import { GAS_PRICE } from '../../utils/constants' class GasPriceInput extends Component { constructor(props) { @@ -26,7 +26,7 @@ class GasPriceInput extends Component { }) input.onChange(Object.assign({}, { - id: 'CUSTOM', + id: GAS_PRICE.CUSTOM.ID, gasPrice: this.state.customGasPrice })) } @@ -39,12 +39,12 @@ class GasPriceInput extends Component { }) input.onChange(Object.assign({}, { - id: 'CUSTOM', - gasPrice: value + id: GAS_PRICE.CUSTOM.ID, + price: value })) } - render () { + render() { const { input, side, gasPrices } = this.props return ( From 050b4582334bf1ea0d25a40f923d490c99e04ef9 Mon Sep 17 00:00:00 2001 From: fernandomg Date: Thu, 29 Mar 2018 11:49:06 -0300 Subject: [PATCH 10/48] Unified radios block --- src/components/stepThree/GasPriceInput.js | 37 +++++++++-------------- 1 file changed, 14 insertions(+), 23 deletions(-) diff --git a/src/components/stepThree/GasPriceInput.js b/src/components/stepThree/GasPriceInput.js index 5465deeaf..11e615476 100644 --- a/src/components/stepThree/GasPriceInput.js +++ b/src/components/stepThree/GasPriceInput.js @@ -51,29 +51,20 @@ class GasPriceInput extends Component {
{gasPrices.map((gasPrice, index) => ( - gasPrice.id !== 'CUSTOM' ? ( -
- -
- ) : ( -
- -
- ) +
+ +
))} {this.state.isCustom ? ( Date: Thu, 29 Mar 2018 11:52:13 -0300 Subject: [PATCH 11/48] Fix custom gas input styles --- src/components/stepThree/GasPriceInput.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/components/stepThree/GasPriceInput.js b/src/components/stepThree/GasPriceInput.js index 11e615476..add0f0c40 100644 --- a/src/components/stepThree/GasPriceInput.js +++ b/src/components/stepThree/GasPriceInput.js @@ -69,6 +69,7 @@ class GasPriceInput extends Component { {this.state.isCustom ? ( this.handleCustomGasPriceChange(e.target.value)} /> From cae8ceaff484b209db303c616bd3767897675550 Mon Sep 17 00:00:00 2001 From: fernandomg Date: Thu, 29 Mar 2018 12:11:58 -0300 Subject: [PATCH 12/48] Add loading overlay --- src/components/stepThree/index.js | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/components/stepThree/index.js b/src/components/stepThree/index.js index c523bac5c..51a767fcc 100644 --- a/src/components/stepThree/index.js +++ b/src/components/stepThree/index.js @@ -107,15 +107,15 @@ export class stepThree extends React.Component { gasPriceStore.updateValues() .then(() => this.setGasPrice(gasPriceStore.slow)) .catch(() => noGasPriceAvailable()) - // .then(() => { - // this.setState({ loading: false }) + .then(() => { + this.setState({ loading: false }) // this.updateWalletAddress({ // address: tierStore.tiers[0].walletAddress, // pristine: true, // valid: VALID, // }) // window.scrollTo(0, 0) - // }) + }) } showErrorMessages = () => { @@ -599,6 +599,7 @@ export class stepThree extends React.Component { ) }} /> +
) } From c302fc1f18640a1d1400c781d6b4ff6b4dc7ef19 Mon Sep 17 00:00:00 2001 From: fernandomg Date: Thu, 29 Mar 2018 12:14:01 -0300 Subject: [PATCH 13/48] Fix Tier dates initialization --- src/components/stepThree/index.js | 23 +++++++++++++++++------ 1 file changed, 17 insertions(+), 6 deletions(-) diff --git a/src/components/stepThree/index.js b/src/components/stepThree/index.js index 51a767fcc..c90e42e1c 100644 --- a/src/components/stepThree/index.js +++ b/src/components/stepThree/index.js @@ -93,13 +93,9 @@ export class stepThree extends React.Component { componentWillMount () { const { gasPriceStore, tierStore } = this.props - if (this.props.tierStore.tiers.length === 0) { + if (tierStore.tiers.length === 0) { this.addCrowdsale() - - this.initialTiers = JSON.parse(JSON.stringify(this.props.tierStore.tiers)) - - this.initialTiers[0].startTime = defaultCompanyStartDate() - this.initialTiers[0].endTime = defaultCompanyEndDate() + this.initialTiers = JSON.parse(JSON.stringify(tierStore.tiers)) } window.scrollTo(0, 0) @@ -148,8 +144,23 @@ export class stepThree extends React.Component { } tierStore.addTier(newTier, newTierValidations) + this.setTierDates(num) + } + + setTierDates(num) { + const { tierStore } = this.props + const defaultStartTime = 0 === num ? defaultCompanyStartDate() : this.tierEndTime(num - 1) + const defaultEndTime = 0 === num ? defaultCompanyEndDate(defaultStartTime) : defaultCompanyEndDate(this.tierEndTime(num - 1)) + + const startTime = tierStore.tiers[num].startTime || defaultStartTime + const endTime = tierStore.tiers[num].endTime || defaultEndTime + + tierStore.setTierProperty(startTime, 'startTime', num) + tierStore.setTierProperty(endTime, 'endTime', num) } + tierEndTime = (index) => this.props.tierStore.tiers[index].endTime + goToDeploymentStage = () => { this.props.history.push('/4') } From 5d2c38daa76ecaa869a729916ec2c044988fdeb0 Mon Sep 17 00:00:00 2001 From: fernandomg Date: Thu, 29 Mar 2018 17:22:32 -0300 Subject: [PATCH 14/48] Add ability to display an array of error messages --- src/components/Common/InputField2.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/components/Common/InputField2.js b/src/components/Common/InputField2.js index d66695ec3..ff8935368 100644 --- a/src/components/Common/InputField2.js +++ b/src/components/Common/InputField2.js @@ -1,6 +1,8 @@ import React from 'react' export const InputField2 = ({ input, meta, side, label, type, description, disabled, errorStyle }) => { + const errors = [].concat(meta.error) + return (
@@ -13,7 +15,9 @@ export const InputField2 = ({ input, meta, side, label, type, description, disab {...input} />

{description}

-

{(!meta.pristine || meta.touched) && meta.error}

+ {errors.map((error, index)=> ( +

{(!meta.pristine || meta.touched) && error}

+ ))}
) } From 9a2aadf12df8bbd9a071fa43ce345412985d60d6 Mon Sep 17 00:00:00 2001 From: fernandomg Date: Thu, 29 Mar 2018 17:23:04 -0300 Subject: [PATCH 15/48] Extend validations and tests --- src/utils/constants.js | 8 +- src/utils/validations.js | 29 ++++++- src/utils/validations.spec.js | 152 +++++++++++++++++++++++++++++++++- 3 files changed, 183 insertions(+), 6 deletions(-) diff --git a/src/utils/constants.js b/src/utils/constants.js index 4b2026e32..34f38aa23 100644 --- a/src/utils/constants.js +++ b/src/utils/constants.js @@ -109,7 +109,13 @@ export const VALIDATION_MESSAGES = { EDITED_END_TIME: 'Please enter a valid date later than start time and previous than start time of next tier', EDITED_START_TIME: 'Please enter a valid date later than now, less than end time and later than the end time of the previous tier', RATE: 'Please enter a valid number greater than 0', - MINCAP: 'Value must be positive, decimals should not exceed the amount of decimals specified and min cap should be less or equal than the supply of some tier' + MINCAP: 'Value must be positive, decimals should not exceed the amount of decimals specified and min cap should be less or equal than the supply of some tier', + POSITIVE: 'Please enter a valid number greater than 0', + NON_NEGATIVE: 'Please enter a valid number greater or equal than 0', + ADDRESS: 'Please enter a valid address', + REQUIRED: 'This field is required', + DECIMAL_PLACES: 'Decimals should not exceed the amount of decimals specified', + LESS_OR_EQUAL: 'Should be less or equal than the specified value', } //descriptions of input fields diff --git a/src/utils/validations.js b/src/utils/validations.js index fd9f1259b..c61f86c4d 100644 --- a/src/utils/validations.js +++ b/src/utils/validations.js @@ -1,5 +1,6 @@ import Web3 from 'web3' import { VALIDATION_MESSAGES } from './constants' +import { countDecimalPlaces } from './utils' export const validators = (type, value) => { return { @@ -24,22 +25,42 @@ export const validateDecimals = (value) => { return isValid ? undefined : VALIDATION_MESSAGES.DECIMALS } -export const isPositive = (errorMsg = 'Please enter a valid number greater than 0') => (value) => { +export const isPositive = (errorMsg = VALIDATION_MESSAGES.POSITIVE) => (value) => { const isValid = value > 0 return isValid ? undefined : errorMsg } -export const isNonNegative = (errorMsg = 'Please enter a valid number greater or equal than 0') => (value) => { +export const isNonNegative = (errorMsg = VALIDATION_MESSAGES.NON_NEGATIVE) => (value) => { const isValid = value >= 0 return isValid ? undefined : errorMsg } -export const isAddress = (errorMsg = 'Please enter a valid address') => (value) => { +export const isAddress = (errorMsg = VALIDATION_MESSAGES.ADDRESS) => (value) => { const isValid = Web3.utils.isAddress(value) return isValid ? undefined : errorMsg } -export const isRequired = (errorMsg = 'This field is required') => (value) => { +export const isRequired = (errorMsg = VALIDATION_MESSAGES.REQUIRED) => (value) => { const isValid = !!value return isValid ? undefined : errorMsg } + +export const isDecimalPlacesNotGreaterThan = (errorMsg = VALIDATION_MESSAGES.DECIMAL_PLACES) => (decimalsCount) => (value) => { + const isValid = countDecimalPlaces(value) <= decimalsCount + return isValid ? undefined : errorMsg +} + +export const isLessOrEqualThan = (errorMsg = VALIDATION_MESSAGES.LESS_OR_EQUAL) => (maxValue = Number.Infinity) => (value) => { + console.log('validate', value, maxValue) + const isValid = value <= maxValue + return isValid ? undefined : errorMsg +} + +export const composeValidators = (...validators) => (value) => validators.reduce((errors, validator) => { + const validation = validator(value) + + if (validation) errors.push(validation) + + return errors +}, []) + diff --git a/src/utils/validations.spec.js b/src/utils/validations.spec.js index 8728b4006..ec156cd84 100644 --- a/src/utils/validations.spec.js +++ b/src/utils/validations.spec.js @@ -1,4 +1,15 @@ -import { validators, validateTokenName, validateTicker, validateDecimals } from './validations' +import { + isAddress, + isDecimalPlacesNotGreaterThan, + isLessOrEqualThan, + isNonNegative, + isPositive, + isRequired, + validateDecimals, + validateTicker, + validateTokenName, + validators +} from './validations' import { VALIDATION_MESSAGES } from './constants' describe('validateTokenName', () => { @@ -67,6 +78,145 @@ describe('validateDecimals', () => { }) }) +describe('isPositive', () => { + const testCases = [ + { value: '1.01', errorMessage: undefined, expected: undefined }, + { value: '5', errorMessage: undefined, expected: undefined }, + { value: '1e4', errorMessage: undefined, expected: undefined }, + { value: '100', errorMessage: undefined, expected: undefined }, + { value: '0.1', errorMessage: undefined, expected: undefined }, + { value: Number.MIN_VALUE, errorMessage: undefined, expected: undefined }, + { value: '0', errorMessage: undefined, expected: VALIDATION_MESSAGES.POSITIVE }, + { value: '-1', errorMessage: undefined, expected: VALIDATION_MESSAGES.POSITIVE }, + { value: '-0.1', errorMessage: undefined, expected: VALIDATION_MESSAGES.POSITIVE }, + { value: '-100', errorMessage: undefined, expected: VALIDATION_MESSAGES.POSITIVE }, + { value: '-100', errorMessage: 'Personalized error message', expected: 'Personalized error message' }, + ] + + testCases.forEach(testCase => { + const action = testCase.expected === undefined ? 'pass' : 'fail' + + it(`Should ${action} form '${testCase.value}'`, () => { + expect(isPositive(testCase.errorMessage)(testCase.value)).toBe(testCase.expected) + }) + }) +}) + +describe('isNonNegative', () => { + const testCases = [ + { value: '1.01', errorMessage: undefined, expected: undefined }, + { value: '5', errorMessage: undefined, expected: undefined }, + { value: '1e4', errorMessage: undefined, expected: undefined }, + { value: '100', errorMessage: undefined, expected: undefined }, + { value: '0.1', errorMessage: undefined, expected: undefined }, + { value: Number.MIN_VALUE, errorMessage: undefined, expected: undefined }, + { value: '0', errorMessage: undefined, expected: undefined }, + { value: '-1', errorMessage: undefined, expected: VALIDATION_MESSAGES.NON_NEGATIVE }, + { value: '-0.1', errorMessage: undefined, expected: VALIDATION_MESSAGES.NON_NEGATIVE }, + { value: '-100', errorMessage: undefined, expected: VALIDATION_MESSAGES.NON_NEGATIVE }, + { value: '-100', errorMessage: 'Personalized error message', expected: 'Personalized error message' }, + ] + + testCases.forEach(testCase => { + const action = testCase.expected === undefined ? 'pass' : 'fail' + + it(`Should ${action} form '${testCase.value}'`, () => { + expect(isNonNegative(testCase.errorMessage)(testCase.value)).toBe(testCase.expected) + }) + }) +}) + +describe('isAddress', () => { + const testCases = [ + { value: '0x90f8bf6a479f320ead074411a4b0e7944ea8c9c1', errorMessage: undefined, expected: undefined }, + { value: '0x90F8bf6A479f320ead074411a4B0e7944Ea8c9C1', errorMessage: undefined, expected: undefined }, + { value: '0xffcf8fdee72ac11b5c542428b35eef5769c409f0', errorMessage: undefined, expected: undefined }, + { value: '0x22d491bde2303f2f43325b2108d26f1eaba1e32b', errorMessage: undefined, expected: undefined }, + { value: '0xe11ba2b4d45eaed5996cd0823791e0c93114882d', errorMessage: undefined, expected: undefined }, + { value: '', errorMessage: undefined, expected: VALIDATION_MESSAGES.ADDRESS }, + { value: '0xe11ba2b4d45eaED5996cd0823791e0c93114882d', errorMessage: undefined, expected: VALIDATION_MESSAGES.ADDRESS }, + { value: '0x123', errorMessage: undefined, expected: VALIDATION_MESSAGES.ADDRESS }, + { value: '0x123', errorMessage: 'Personalized error message', expected: 'Personalized error message' }, + ] + + testCases.forEach(testCase => { + const action = testCase.expected === undefined ? 'pass' : 'fail' + + it(`Should ${action} form '${testCase.value}'`, () => { + expect(isAddress(testCase.errorMessage)(testCase.value)).toBe(testCase.expected) + }) + }) +}) + +describe('isRequired', () => { + const testCases = [ + { value: '1', errorMessage: undefined, expected: undefined }, + { value: 'a', errorMessage: undefined, expected: undefined }, + { value: '0', errorMessage: undefined, expected: undefined }, + { value: 'undefined', errorMessage: undefined, expected: undefined }, + { value: 'null', errorMessage: undefined, expected: undefined }, + { value: 'false', errorMessage: undefined, expected: undefined }, + { value: false, errorMessage: undefined, expected: VALIDATION_MESSAGES.REQUIRED }, + { value: undefined, errorMessage: undefined, expected: VALIDATION_MESSAGES.REQUIRED }, + { value: 0, errorMessage: undefined, expected: VALIDATION_MESSAGES.REQUIRED }, + { value: null, errorMessage: undefined, expected: VALIDATION_MESSAGES.REQUIRED }, + { value: '', errorMessage: undefined, expected: VALIDATION_MESSAGES.REQUIRED }, + { value: '', errorMessage: 'Personalized error message', expected: 'Personalized error message' }, + ] + + testCases.forEach(testCase => { + const action = testCase.expected === undefined ? 'pass' : 'fail' + + it(`Should ${action} form '${testCase.value}'`, () => { + expect(isRequired(testCase.errorMessage)(testCase.value)).toBe(testCase.expected) + }) + }) +}) + +describe('isDecimalPlacesNotGreaterThan', () => { + const testCases = [ + { value: '1', errorMessage: undefined, comparedTo: '5',expected: undefined }, + { value: '1.1', errorMessage: undefined, comparedTo: '5',expected: undefined }, + { value: '1.12', errorMessage: undefined, comparedTo: '5',expected: undefined }, + { value: '1.123', errorMessage: undefined, comparedTo: '5',expected: undefined }, + { value: '1.1234', errorMessage: undefined, comparedTo: '5',expected: undefined }, + { value: '1.12345', errorMessage: undefined, comparedTo: '5',expected: undefined }, + { value: '1.123456', errorMessage: undefined, comparedTo: '5',expected: VALIDATION_MESSAGES.DECIMAL_PLACES }, + { value: '1.1234567', errorMessage: undefined, comparedTo: '5',expected: VALIDATION_MESSAGES.DECIMAL_PLACES }, + { value: '1.1234567', errorMessage: 'Personalized error message', comparedTo: '5',expected: 'Personalized error message' }, + ] + + testCases.forEach(testCase => { + const action = testCase.expected === undefined ? 'pass' : 'fail' + + it(`Should ${action} form '${testCase.value}'`, () => { + expect(isDecimalPlacesNotGreaterThan(testCase.errorMessage)(testCase.comparedTo)(testCase.value)).toBe(testCase.expected) + }) + }) +}) + +describe('isLessOrEqualThan', () => { + const testCases = [ + { value: '1', errorMessage: undefined, comparedTo: '5',expected: undefined }, + { value: '1.1', errorMessage: undefined, comparedTo: '5',expected: undefined }, + { value: '2', errorMessage: undefined, comparedTo: '5',expected: undefined }, + { value: '3', errorMessage: undefined, comparedTo: '5',expected: undefined }, + { value: '4', errorMessage: undefined, comparedTo: '5',expected: undefined }, + { value: '5', errorMessage: undefined, comparedTo: '5',expected: undefined }, + { value: '5.1', errorMessage: undefined, comparedTo: '5',expected: VALIDATION_MESSAGES.LESS_OR_EQUAL }, + { value: '6', errorMessage: undefined, comparedTo: '5',expected: VALIDATION_MESSAGES.LESS_OR_EQUAL }, + { value: '6', errorMessage: 'Personalized error message', comparedTo: '5',expected: 'Personalized error message' }, + ] + + testCases.forEach(testCase => { + const action = testCase.expected === undefined ? 'pass' : 'fail' + + it(`Should ${action} form '${testCase.value}'`, () => { + expect(isLessOrEqualThan(testCase.errorMessage)(testCase.comparedTo)(testCase.value)).toBe(testCase.expected) + }) + }) +}) + describe('validators', () => { it('Should return false if validator does not exist', () => { expect(validators('nonExistent', '')).toBeFalsy() From 827fb64488b5d91b382c108ed76efee94121ab50 Mon Sep 17 00:00:00 2001 From: fernandomg Date: Thu, 29 Mar 2018 17:24:03 -0300 Subject: [PATCH 16/48] Add react-final-form-listener as a dependency --- package-lock.json | 5 +++++ package.json | 1 + 2 files changed, 6 insertions(+) diff --git a/package-lock.json b/package-lock.json index 1ef811e24..6289ca987 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12810,6 +12810,11 @@ "resolved": "https://registry.npmjs.org/react-final-form-arrays/-/react-final-form-arrays-1.0.4.tgz", "integrity": "sha512-ZnCze9b5RCXAI2vWeguPSss9Q0Rh3x4UO2rZ3MubyMWWa7MPg64VdD4G+PzYD/9A+TizBR+40xS7bTg2ATQmXQ==" }, + "react-final-form-listeners": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/react-final-form-listeners/-/react-final-form-listeners-1.0.0.tgz", + "integrity": "sha512-6Xzfly49MLPY4GKMy+8gC+bZ8gGqaxtJHhzQ6umvZNYge0LOTGyQNC+vxrIzgHT2QpNTsqbvIh9n/5AOTBGNSw==" + }, "react-html-attributes": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/react-html-attributes/-/react-html-attributes-1.4.1.tgz", diff --git a/package.json b/package.json index b6342c486..c7d7ac657 100755 --- a/package.json +++ b/package.json @@ -95,6 +95,7 @@ "react-error-overlay": "^1.0.9", "react-final-form": "^3.1.4", "react-final-form-arrays": "^1.0.4", + "react-final-form-listeners": "^1.0.0", "react-router-dom": "^4.1.2", "react-tooltip": "^3.4.0", "solc": "^0.4.14", From 48431ac6153ad41f1107d18ba3855223bf1878a8 Mon Sep 17 00:00:00 2001 From: fernandomg Date: Thu, 29 Mar 2018 17:29:56 -0300 Subject: [PATCH 17/48] Create WhenFieldChanges component --- src/components/Common/WhenFieldChanges.js | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 src/components/Common/WhenFieldChanges.js diff --git a/src/components/Common/WhenFieldChanges.js b/src/components/Common/WhenFieldChanges.js new file mode 100644 index 000000000..cbb37d3e1 --- /dev/null +++ b/src/components/Common/WhenFieldChanges.js @@ -0,0 +1,21 @@ +import React from 'react' +import { Field } from 'react-final-form' +import { OnChange } from 'react-final-form-listeners' + +export const WhenFieldChanges = ({ field, becomes, set, to }) => ( + + {( + // No subscription. We only use Field to get to the change function + { input: { onChange } } + ) => ( + + {value => { + if (value === becomes) { + onChange(to) + } + }} + + )} + +) + From a912198e73f87c93cd7ef6801d3edbf3e563ad85 Mon Sep 17 00:00:00 2001 From: fernandomg Date: Thu, 29 Mar 2018 17:30:08 -0300 Subject: [PATCH 18/48] Add validations for minCap --- src/components/stepThree/index.js | 45 +++++++++++++++++++++++++++---- 1 file changed, 40 insertions(+), 5 deletions(-) diff --git a/src/components/stepThree/index.js b/src/components/stepThree/index.js index c90e42e1c..15690596a 100644 --- a/src/components/stepThree/index.js +++ b/src/components/stepThree/index.js @@ -3,10 +3,12 @@ import "../../assets/stylesheets/application.css"; import { Field, Form, FormSpy } from 'react-final-form' import arrayMutators from 'final-form-arrays' import { FieldArray } from 'react-final-form-arrays' +import { OnChange } from 'react-final-form-listeners' import { Link } from "react-router-dom"; import { setExistingContractParams, getNetworkVersion, getNetWorkNameById } from "../../utils/blockchainHelpers"; import { gweiToWei, weiToGwei } from "../../utils/utils"; import { StepNavigation } from "../Common/StepNavigation"; +import { WhenFieldChanges } from '../Common/WhenFieldChanges' import { RadioInputField } from "../Common/RadioInputField"; import { InputField2 } from "../Common/InputField2"; import { CrowdsaleBlock } from "./CrowdsaleBlock"; @@ -15,7 +17,6 @@ import GasPriceInput from './GasPriceInput' import { defaultCompanyStartDate, defaultCompanyEndDate } from './utils' import { NAVIGATION_STEPS, - VALIDATION_MESSAGES, VALIDATION_TYPES, TEXT_FIELDS, CHAINS, @@ -26,7 +27,15 @@ import { import { inject, observer } from "mobx-react"; import { Loader } from '../Common/Loader' import { noGasPriceAvailable, warningOnMainnetAlert } from '../../utils/alerts' -import { isAddress, isNonNegative, isPositive, isRequired } from '../../utils/validations' +import { + isAddress, + isDecimalPlacesNotGreaterThan, + isNonNegative, + isLessOrEqualThan, + isPositive, + isRequired, + composeValidators, +} from '../../utils/validations' import { NumericInput } from '../Common/NumericInput' import update from 'immutability-helper' import { AddressInput } from '../Common/AddressInput' @@ -366,11 +375,11 @@ export class stepThree extends React.Component { fontWeight: 'bold', fontSize: '12px', width: '100%', - height: '10px', + height: '20px', } render() { - const { generalStore, tierStore, gasPriceStore } = this.props + const { generalStore, tierStore, gasPriceStore,tokenStore } = this.props return (
@@ -392,6 +401,12 @@ export class stepThree extends React.Component { return (
+
@@ -426,7 +441,12 @@ export class stepThree extends React.Component { + {/* + * TODO: REVIEW. I'm not sure about this approach. + * But it worked for me to keep the error messages properly updated for the minCap field. + */} + + {({ input: { onChange } }) => ( + + {() => { + const { minCap } = values + onChange(0) + onChange(minCap) + }} + + )} +
{ From a7181725b53a4a7bbc803119325956388710973d Mon Sep 17 00:00:00 2001 From: fernandomg Date: Thu, 29 Mar 2018 18:24:09 -0300 Subject: [PATCH 19/48] Add rate validations --- src/components/stepThree/index.js | 8 +++++-- src/utils/constants.js | 1 + src/utils/validations.js | 20 +++++++++++++--- src/utils/validations.spec.js | 40 +++++++++++++++++++++++++------ 4 files changed, 57 insertions(+), 12 deletions(-) diff --git a/src/components/stepThree/index.js b/src/components/stepThree/index.js index 15690596a..f451d9ded 100644 --- a/src/components/stepThree/index.js +++ b/src/components/stepThree/index.js @@ -34,7 +34,7 @@ import { isLessOrEqualThan, isPositive, isRequired, - composeValidators, + composeValidators, isInteger, } from '../../utils/validations' import { NumericInput } from '../Common/NumericInput' import update from 'immutability-helper' @@ -561,7 +561,11 @@ export class stepThree extends React.Component { (maxValue = Number.Infinity) => (value) => { - console.log('validate', value, maxValue) - const isValid = value <= maxValue - return isValid ? undefined : errorMsg + try { + const max = new BigNumber(String(maxValue)) + const isValid = max.gte(value) + return isValid ? undefined : errorMsg + } catch (e) { + return errorMsg + } +} + +export const isInteger = (errorMsg = VALIDATION_MESSAGES.INTEGER) => (value) => { + try { + const isValid = new BigNumber(value).isInteger() + return isValid ? undefined : errorMsg + } catch (e) { + return errorMsg + } } export const composeValidators = (...validators) => (value) => validators.reduce((errors, validator) => { diff --git a/src/utils/validations.spec.js b/src/utils/validations.spec.js index ec156cd84..966f20ec3 100644 --- a/src/utils/validations.spec.js +++ b/src/utils/validations.spec.js @@ -1,6 +1,6 @@ import { isAddress, - isDecimalPlacesNotGreaterThan, + isDecimalPlacesNotGreaterThan, isInteger, isLessOrEqualThan, isNonNegative, isPositive, @@ -96,7 +96,7 @@ describe('isPositive', () => { testCases.forEach(testCase => { const action = testCase.expected === undefined ? 'pass' : 'fail' - it(`Should ${action} form '${testCase.value}'`, () => { + it(`Should ${action} for '${testCase.value}'`, () => { expect(isPositive(testCase.errorMessage)(testCase.value)).toBe(testCase.expected) }) }) @@ -120,7 +120,7 @@ describe('isNonNegative', () => { testCases.forEach(testCase => { const action = testCase.expected === undefined ? 'pass' : 'fail' - it(`Should ${action} form '${testCase.value}'`, () => { + it(`Should ${action} for '${testCase.value}'`, () => { expect(isNonNegative(testCase.errorMessage)(testCase.value)).toBe(testCase.expected) }) }) @@ -142,7 +142,7 @@ describe('isAddress', () => { testCases.forEach(testCase => { const action = testCase.expected === undefined ? 'pass' : 'fail' - it(`Should ${action} form '${testCase.value}'`, () => { + it(`Should ${action} for '${testCase.value}'`, () => { expect(isAddress(testCase.errorMessage)(testCase.value)).toBe(testCase.expected) }) }) @@ -167,7 +167,7 @@ describe('isRequired', () => { testCases.forEach(testCase => { const action = testCase.expected === undefined ? 'pass' : 'fail' - it(`Should ${action} form '${testCase.value}'`, () => { + it(`Should ${action} for '${testCase.value}'`, () => { expect(isRequired(testCase.errorMessage)(testCase.value)).toBe(testCase.expected) }) }) @@ -189,7 +189,7 @@ describe('isDecimalPlacesNotGreaterThan', () => { testCases.forEach(testCase => { const action = testCase.expected === undefined ? 'pass' : 'fail' - it(`Should ${action} form '${testCase.value}'`, () => { + it(`Should ${action} for '${testCase.value}'`, () => { expect(isDecimalPlacesNotGreaterThan(testCase.errorMessage)(testCase.comparedTo)(testCase.value)).toBe(testCase.expected) }) }) @@ -203,6 +203,9 @@ describe('isLessOrEqualThan', () => { { value: '3', errorMessage: undefined, comparedTo: '5',expected: undefined }, { value: '4', errorMessage: undefined, comparedTo: '5',expected: undefined }, { value: '5', errorMessage: undefined, comparedTo: '5',expected: undefined }, + { value: '1000000000000000000', errorMessage: undefined, comparedTo: '1e18',expected: undefined }, + { value: '999999999999999999', errorMessage: undefined, comparedTo: '1e18',expected: undefined }, + { value: '10000000000000000001', errorMessage: undefined, comparedTo: '1e18',expected: VALIDATION_MESSAGES.LESS_OR_EQUAL }, { value: '5.1', errorMessage: undefined, comparedTo: '5',expected: VALIDATION_MESSAGES.LESS_OR_EQUAL }, { value: '6', errorMessage: undefined, comparedTo: '5',expected: VALIDATION_MESSAGES.LESS_OR_EQUAL }, { value: '6', errorMessage: 'Personalized error message', comparedTo: '5',expected: 'Personalized error message' }, @@ -211,12 +214,35 @@ describe('isLessOrEqualThan', () => { testCases.forEach(testCase => { const action = testCase.expected === undefined ? 'pass' : 'fail' - it(`Should ${action} form '${testCase.value}'`, () => { + it(`Should ${action} for '${testCase.value}'`, () => { expect(isLessOrEqualThan(testCase.errorMessage)(testCase.comparedTo)(testCase.value)).toBe(testCase.expected) }) }) }) +describe('isInteger', () => { + const testCases = [ + { value: '1', errorMessage: undefined, expected: undefined }, + { value: '1.', errorMessage: undefined, expected: undefined }, + { value: '2', errorMessage: undefined, expected: undefined }, + { value: '1000000000000000000', errorMessage: undefined, expected: undefined }, + { value: '5.1', errorMessage: undefined, expected: VALIDATION_MESSAGES.INTEGER }, + { value: '1e-4', errorMessage: undefined, expected: VALIDATION_MESSAGES.INTEGER }, + { value: '.12', errorMessage: undefined, expected: VALIDATION_MESSAGES.INTEGER }, + { value: 'abc', errorMessage: undefined, expected: VALIDATION_MESSAGES.INTEGER }, + { value: '0.12', errorMessage: undefined, expected: VALIDATION_MESSAGES.INTEGER }, + { value: '0.12', errorMessage: 'Personalized error message', expected: 'Personalized error message' }, + ] + + testCases.forEach(testCase => { + const action = testCase.expected === undefined ? 'pass' : 'fail' + + it(`Should ${action} for '${testCase.value}'`, () => { + expect(isInteger(testCase.errorMessage)(testCase.value)).toBe(testCase.expected) + }) + }) +}) + describe('validators', () => { it('Should return false if validator does not exist', () => { expect(validators('nonExistent', '')).toBeFalsy() From cc8093fe42cfdb245da4dc7c4249ea7732ea78e0 Mon Sep 17 00:00:00 2001 From: fernandomg Date: Tue, 3 Apr 2018 09:36:56 -0300 Subject: [PATCH 20/48] Set minCap to 0 (zero) when whitelist is enabled --- src/components/stepThree/index.js | 2 +- src/utils/validations.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/stepThree/index.js b/src/components/stepThree/index.js index f451d9ded..aec283b8f 100644 --- a/src/components/stepThree/index.js +++ b/src/components/stepThree/index.js @@ -405,7 +405,7 @@ export class stepThree extends React.Component { field="whitelistEnabled" becomes={'yes'} set="minCap" - to={''} + to={0} />
diff --git a/src/utils/validations.js b/src/utils/validations.js index b362a74d0..cf38047d0 100644 --- a/src/utils/validations.js +++ b/src/utils/validations.js @@ -51,7 +51,7 @@ export const isDecimalPlacesNotGreaterThan = (errorMsg = VALIDATION_MESSAGES.DEC return isValid ? undefined : errorMsg } -export const isLessOrEqualThan = (errorMsg = VALIDATION_MESSAGES.LESS_OR_EQUAL) => (maxValue = Number.Infinity) => (value) => { +export const isLessOrEqualThan = (errorMsg = VALIDATION_MESSAGES.LESS_OR_EQUAL) => (maxValue = Infinity) => (value) => { try { const max = new BigNumber(String(maxValue)) const isValid = max.gte(value) From 2ba546f5aa8e1cb49c3ed39fa54b5ab003ee6463 Mon Sep 17 00:00:00 2001 From: fernandomg Date: Tue, 3 Apr 2018 14:19:30 -0300 Subject: [PATCH 21/48] Remove 'instant' from collection of Gas Prices --- src/stores/GasPriceStore.js | 1 - src/stores/GasPriceStore.spec.js | 7 +------ 2 files changed, 1 insertion(+), 7 deletions(-) diff --git a/src/stores/GasPriceStore.js b/src/stores/GasPriceStore.js index 00b3b296f..d0628c636 100644 --- a/src/stores/GasPriceStore.js +++ b/src/stores/GasPriceStore.js @@ -96,7 +96,6 @@ class GasPriceStore { { ...this.slow, description: this.slowDescription }, { ...this.standard, description: this.standardDescription }, { ...this.fast, description: this.fastDescription }, - { ...this.instant, description: this.instantDescription }, { ...this.custom, description: this.customDescription }, ] } diff --git a/src/stores/GasPriceStore.spec.js b/src/stores/GasPriceStore.spec.js index 07a9db97d..45c6ef62e 100644 --- a/src/stores/GasPriceStore.spec.js +++ b/src/stores/GasPriceStore.spec.js @@ -1,6 +1,6 @@ import GasPriceStore from './GasPriceStore' import { GAS_PRICE } from '../utils/constants' -import { gweiToWei } from '../utils/utils' +import { gweiToWei, weiToGwei } from '../utils/utils' jest.mock('../utils/api') @@ -54,11 +54,6 @@ describe('GasPriceStore', () => { description: gas.fastDescription, }) expect(gas.gasPrices[3]).toEqual({ - id: GAS_PRICE.INSTANT.ID, - price: GAS_PRICE.INSTANT.PRICE, - description: gas.instantDescription, - }) - expect(gas.gasPrices[4]).toEqual({ id: GAS_PRICE.CUSTOM.ID, price: GAS_PRICE.CUSTOM.PRICE, description: gas.customDescription, From e75b002ebf42096ad3b3a5d18a26bbec61da7752 Mon Sep 17 00:00:00 2001 From: fernandomg Date: Tue, 3 Apr 2018 14:20:53 -0300 Subject: [PATCH 22/48] Add gasPricesInGwei computed method --- src/stores/GasPriceStore.js | 8 ++++++++ src/stores/GasPriceStore.spec.js | 23 +++++++++++++++++++++++ 2 files changed, 31 insertions(+) diff --git a/src/stores/GasPriceStore.js b/src/stores/GasPriceStore.js index d0628c636..4391dccb8 100644 --- a/src/stores/GasPriceStore.js +++ b/src/stores/GasPriceStore.js @@ -99,6 +99,14 @@ class GasPriceStore { { ...this.custom, description: this.customDescription }, ] } + + @computed + get gasPricesInGwei () { + return this.gasPrices.map(gasPrice => ({ + ...gasPrice, + price: weiToGwei(gasPrice.price) + })) + } } export default GasPriceStore diff --git a/src/stores/GasPriceStore.spec.js b/src/stores/GasPriceStore.spec.js index 45c6ef62e..2ca4163e6 100644 --- a/src/stores/GasPriceStore.spec.js +++ b/src/stores/GasPriceStore.spec.js @@ -60,6 +60,29 @@ describe('GasPriceStore', () => { }) }) + it('should return gasPrices collection in Gwei', () => { + expect(gas.gasPricesInGwei[0]).toEqual({ + id: GAS_PRICE.SLOW.ID, + price: weiToGwei(GAS_PRICE.SLOW.PRICE), + description: gas.slowDescription, + }) + expect(gas.gasPricesInGwei[1]).toEqual({ + id: GAS_PRICE.NORMAL.ID, + price: weiToGwei(GAS_PRICE.NORMAL.PRICE), + description: gas.standardDescription, + }) + expect(gas.gasPricesInGwei[2]).toEqual({ + id: GAS_PRICE.FAST.ID, + price: weiToGwei(GAS_PRICE.FAST.PRICE), + description: gas.fastDescription, + }) + expect(gas.gasPricesInGwei[3]).toEqual({ + id: GAS_PRICE.CUSTOM.ID, + price: weiToGwei(GAS_PRICE.CUSTOM.PRICE), + description: gas.customDescription, + }) + }) + it('should updates values with mocked data', () => { const gasPrice = require('../utils/__mocks__/gasPrice.json') From 501a58e587c2eb50d6d0a537cd0a0e1f85bd7f5c Mon Sep 17 00:00:00 2001 From: fernandomg Date: Tue, 3 Apr 2018 14:21:45 -0300 Subject: [PATCH 23/48] Create 'Error' component for final-form error display --- src/components/Common/Error.js | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) create mode 100644 src/components/Common/Error.js diff --git a/src/components/Common/Error.js b/src/components/Common/Error.js new file mode 100644 index 000000000..bf68b6b77 --- /dev/null +++ b/src/components/Common/Error.js @@ -0,0 +1,32 @@ +import React from 'react' +import { Field } from 'react-final-form' + +const defaultErrorStyles = { + color: 'red', + fontWeight: 'bold', + fontSize: '12px', + width: '100%', + height: '20px', +} + +export const Error = ({ name, errorStyle }) => ( + { + const errors = [].concat(error) + + return ( + + { + errors.length + ? errors.map((error, index) => ( +

{(!pristine || touched) && error}

+ )) + : null + } +
+ ) + }} + /> +) From 975db02b2576854934bcfddec511b9e301157072 Mon Sep 17 00:00:00 2001 From: fernandomg Date: Tue, 3 Apr 2018 14:26:04 -0300 Subject: [PATCH 24/48] Modify InputField2 to use Error component --- src/components/Common/InputField2.js | 37 ++++++++++++---------------- 1 file changed, 16 insertions(+), 21 deletions(-) diff --git a/src/components/Common/InputField2.js b/src/components/Common/InputField2.js index ff8935368..4020d9554 100644 --- a/src/components/Common/InputField2.js +++ b/src/components/Common/InputField2.js @@ -1,24 +1,19 @@ import React from 'react' +import { Error } from './Error' -export const InputField2 = ({ input, meta, side, label, type, description, disabled, errorStyle }) => { - const errors = [].concat(meta.error) - - return ( -
- - -

{description}

- {errors.map((error, index)=> ( -

{(!meta.pristine || meta.touched) && error}

- ))} -
- ) -} +export const InputField2 = (props) => ( +
+ + +

{props.description}

+ +
+) From dd4d2f2f0cc3453faabe38eca94cb5d2078de700 Mon Sep 17 00:00:00 2001 From: fernandomg Date: Tue, 3 Apr 2018 14:30:25 -0300 Subject: [PATCH 25/48] Add Error component to GasPriceInput --- src/components/stepThree/GasPriceInput.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/components/stepThree/GasPriceInput.js b/src/components/stepThree/GasPriceInput.js index add0f0c40..ee5ae642c 100644 --- a/src/components/stepThree/GasPriceInput.js +++ b/src/components/stepThree/GasPriceInput.js @@ -1,5 +1,6 @@ import React, { Component } from 'react' import { GAS_PRICE } from '../../utils/constants' +import { Error } from '../Common/Error' class GasPriceInput extends Component { constructor(props) { @@ -75,6 +76,7 @@ class GasPriceInput extends Component { /> ) : null}

Slow is cheap, fast is expensive

+
) } From 1996ca0be2eb93cb4d1572ed04ece93da53a4883 Mon Sep 17 00:00:00 2001 From: fernandomg Date: Tue, 3 Apr 2018 14:32:14 -0300 Subject: [PATCH 26/48] Modify customGasPrice initial value --- src/components/stepThree/GasPriceInput.js | 23 ++++++++++++++--------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/src/components/stepThree/GasPriceInput.js b/src/components/stepThree/GasPriceInput.js index ee5ae642c..9ea4415c0 100644 --- a/src/components/stepThree/GasPriceInput.js +++ b/src/components/stepThree/GasPriceInput.js @@ -8,7 +8,7 @@ class GasPriceInput extends Component { this.state = { isCustom: false, - customGasPrice: 0 + customGasPrice: undefined } } @@ -20,16 +20,21 @@ class GasPriceInput extends Component { } handleCustomSelected = () => { - const { input } = this.props + const { input, gasPrices } = this.props - this.setState({ - isCustom: true - }) + const newState = { isCustom: true } - input.onChange(Object.assign({}, { - id: GAS_PRICE.CUSTOM.ID, - gasPrice: this.state.customGasPrice - })) + if (this.state.customGasPrice === undefined) { + const slow = gasPrices.find(gasPrice => gasPrice.id === GAS_PRICE.SLOW.ID) + newState.customGasPrice = slow.price + } + + this.setState(newState, () => { + input.onChange(Object.assign({}, { + id: GAS_PRICE.CUSTOM.ID, + price: this.state.customGasPrice + })) + }) } handleCustomGasPriceChange = (value) => { From 529364cd189a2b2ac58bd5ba227a482c35185582 Mon Sep 17 00:00:00 2001 From: fernandomg Date: Tue, 3 Apr 2018 14:33:05 -0300 Subject: [PATCH 27/48] Add isGreaterOrEqualThan validation --- src/utils/constants.js | 1 + src/utils/validations.js | 10 ++++++++++ src/utils/validations.spec.js | 29 ++++++++++++++++++++++++++++- 3 files changed, 39 insertions(+), 1 deletion(-) diff --git a/src/utils/constants.js b/src/utils/constants.js index d294e3827..f8d0d1163 100644 --- a/src/utils/constants.js +++ b/src/utils/constants.js @@ -116,6 +116,7 @@ export const VALIDATION_MESSAGES = { REQUIRED: 'This field is required', DECIMAL_PLACES: 'Decimals should not exceed the amount of decimals specified', LESS_OR_EQUAL: 'Should be less or equal than the specified value', + GREATER_OR_EQUAL: 'Should be greater than the specified value', INTEGER: 'Should be integer', } diff --git a/src/utils/validations.js b/src/utils/validations.js index cf38047d0..c2c5f29c8 100644 --- a/src/utils/validations.js +++ b/src/utils/validations.js @@ -61,6 +61,16 @@ export const isLessOrEqualThan = (errorMsg = VALIDATION_MESSAGES.LESS_OR_EQUAL) } } +export const isGreaterOrEqualThan = (errorMsg = VALIDATION_MESSAGES.GREATER_OR_EQUAL) => (minValue = Number.MIN_VALUE) => (value) => { + try { + const min = new BigNumber(String(minValue)) + const isValid = min.lte(value) + return isValid ? undefined : errorMsg + } catch (e) { + return errorMsg + } +} + export const isInteger = (errorMsg = VALIDATION_MESSAGES.INTEGER) => (value) => { try { const isValid = new BigNumber(value).isInteger() diff --git a/src/utils/validations.spec.js b/src/utils/validations.spec.js index 966f20ec3..be1829879 100644 --- a/src/utils/validations.spec.js +++ b/src/utils/validations.spec.js @@ -1,6 +1,8 @@ import { isAddress, - isDecimalPlacesNotGreaterThan, isInteger, + isDecimalPlacesNotGreaterThan, + isGreaterOrEqualThan, + isInteger, isLessOrEqualThan, isNonNegative, isPositive, @@ -220,6 +222,31 @@ describe('isLessOrEqualThan', () => { }) }) +describe('isGreaterOrEqualThan', () => { + const testCases = [ + { value: '10', errorMessage: undefined, comparedTo: '5',expected: undefined }, + { value: '10.1', errorMessage: undefined, comparedTo: '5',expected: undefined }, + { value: '20', errorMessage: undefined, comparedTo: '5',expected: undefined }, + { value: '30', errorMessage: undefined, comparedTo: '5',expected: undefined }, + { value: '40', errorMessage: undefined, comparedTo: '5',expected: undefined }, + { value: '50', errorMessage: undefined, comparedTo: '5',expected: undefined }, + { value: '0.1', errorMessage: undefined, comparedTo: '0.1',expected: undefined }, + { value: '0.1234', errorMessage: undefined, comparedTo: '0.1',expected: undefined }, + { value: '0.0001', errorMessage: undefined, comparedTo: '0.1',expected: VALIDATION_MESSAGES.GREATER_OR_EQUAL }, + { value: '0', errorMessage: undefined, comparedTo: '0.1',expected: VALIDATION_MESSAGES.GREATER_OR_EQUAL }, + { value: '-10', errorMessage: undefined, comparedTo: '0.1',expected: VALIDATION_MESSAGES.GREATER_OR_EQUAL }, + { value: '-10', errorMessage: 'Personalized error message', comparedTo: '0.1', expected: 'Personalized error message' }, + ] + + testCases.forEach(testCase => { + const action = testCase.expected === undefined ? 'pass' : 'fail' + + it(`Should ${action} for '${testCase.value}'`, () => { + expect(isGreaterOrEqualThan(testCase.errorMessage)(testCase.comparedTo)(testCase.value)).toBe(testCase.expected) + }) + }) +}) + describe('isInteger', () => { const testCases = [ { value: '1', errorMessage: undefined, expected: undefined }, From 520be8e5fbe01a6e9abe627045d44f6a32cf3cf5 Mon Sep 17 00:00:00 2001 From: fernandomg Date: Tue, 3 Apr 2018 14:34:27 -0300 Subject: [PATCH 28/48] Validate Custom GasPrice --- src/components/stepThree/index.js | 94 ++++--------------------------- 1 file changed, 11 insertions(+), 83 deletions(-) diff --git a/src/components/stepThree/index.js b/src/components/stepThree/index.js index aec283b8f..2c566cc51 100644 --- a/src/components/stepThree/index.js +++ b/src/components/stepThree/index.js @@ -34,11 +34,11 @@ import { isLessOrEqualThan, isPositive, isRequired, - composeValidators, isInteger, + composeValidators, + isInteger, + isGreaterOrEqualThan, } from '../../utils/validations' -import { NumericInput } from '../Common/NumericInput' import update from 'immutability-helper' -import { AddressInput } from '../Common/AddressInput' import classnames from 'classnames' const { CROWDSALE_SETUP } = NAVIGATION_STEPS; @@ -289,82 +289,6 @@ export class stepThree extends React.Component { this.props.generalStore.setGasPrice(gweiToWei(value)) } - renderGasPriceInput() { - const { generalStore, gasPriceStore } = this.props - - return ( -
- -
- -
-
- -
-
- -
-
- -
- - { - this.state.gasPriceSelected === gasPriceStore.custom.id ? - : - null - } - -

Slow is cheap, fast is expensive

-
- ); - } - updateWhitelistEnabled = (e) => { this.updateMinCap({ value: '', valid: VALID, pristine: false }) this.updateTierStore(e, "whitelistEnabled", 0) @@ -379,7 +303,7 @@ export class stepThree extends React.Component { } render() { - const { generalStore, tierStore, gasPriceStore,tokenStore } = this.props + const { generalStore, tierStore, gasPriceStore, tokenStore } = this.props return (
@@ -390,7 +314,7 @@ export class stepThree extends React.Component { initialValues={{ walletAddress: tierStore.tiers[0].walletAddress, minCap: '', - gasPrice: gasPriceStore.gasPrices[0], + gasPrice: gasPriceStore.gasPricesInGwei[0], whitelistEnabled: "no", tiers: this.initialTiers }} @@ -434,7 +358,11 @@ export class stepThree extends React.Component { name="gasPrice" component={GasPriceInput} side="right" - gasPrices={gasPriceStore.gasPrices} + gasPrices={gasPriceStore.gasPricesInGwei} + validate={(value) => composeValidators( + isDecimalPlacesNotGreaterThan("Should not have more than 9 decimals")(9), + isGreaterOrEqualThan("Should be greater than 0.1")(0.1) + )(value.price)} />
@@ -630,7 +558,7 @@ export class stepThree extends React.Component { subscription={{ values: true }} onChange={({ values }) => { tierStore.updateWalletAddress(values.walletAddress, VALID) - generalStore.setGasPrice(gweiToWei(values.gasPrice.price || 0)) + generalStore.setGasPrice(gweiToWei(values.gasPrice.price)) tierStore.setGlobalMinCap(values.minCap || 0) tierStore.setTierProperty(values.whitelistEnabled, "whitelistEnabled", 0) From 6d57d6291e713c0fc255fe4d8ece96a58f9518bb Mon Sep 17 00:00:00 2001 From: fernandomg Date: Tue, 3 Apr 2018 14:44:45 -0300 Subject: [PATCH 29/48] Cleanup gasPrice related code from stepThree --- src/components/stepThree/index.js | 41 +++---------------------------- 1 file changed, 3 insertions(+), 38 deletions(-) diff --git a/src/components/stepThree/index.js b/src/components/stepThree/index.js index 2c566cc51..9ecb25671 100644 --- a/src/components/stepThree/index.js +++ b/src/components/stepThree/index.js @@ -6,10 +6,9 @@ import { FieldArray } from 'react-final-form-arrays' import { OnChange } from 'react-final-form-listeners' import { Link } from "react-router-dom"; import { setExistingContractParams, getNetworkVersion, getNetWorkNameById } from "../../utils/blockchainHelpers"; -import { gweiToWei, weiToGwei } from "../../utils/utils"; +import { gweiToWei } from "../../utils/utils"; import { StepNavigation } from "../Common/StepNavigation"; import { WhenFieldChanges } from '../Common/WhenFieldChanges' -import { RadioInputField } from "../Common/RadioInputField"; import { InputField2 } from "../Common/InputField2"; import { CrowdsaleBlock } from "./CrowdsaleBlock"; import { WhitelistInputBlock } from '../Common/WhitelistInputBlock' @@ -70,7 +69,7 @@ export class stepThree extends React.Component { constructor(props) { super(props); - const { contractStore, gasPriceStore } = props; + const { contractStore } = props; if (contractStore.crowdsale.addr.length > 0) { contractStore.setContractProperty("pricingStrategy", "addr", []); @@ -79,14 +78,9 @@ export class stepThree extends React.Component { this.state = { loading: true, - gasPriceSelected: gasPriceStore.slow.id, minCap: props.tierStore.globalMinCap || '', walletAddress: '', validation: { - gasPrice: { - pristine: true, - valid: INVALID - }, minCap: { pristine: true, valid: VALID @@ -110,7 +104,6 @@ export class stepThree extends React.Component { window.scrollTo(0, 0) gasPriceStore.updateValues() - .then(() => this.setGasPrice(gasPriceStore.slow)) .catch(() => noGasPriceAvailable()) .then(() => { this.setState({ loading: false }) @@ -175,8 +168,7 @@ export class stepThree extends React.Component { } beforeNavigate = () => { - const { tierStore, gasPriceStore } = this.props - // const gasPriceIsValid = gasPriceStore.custom.id !== this.state.gasPriceSelected || this.state.validation.gasPrice.valid === VALID + const { tierStore } = this.props const isMinCapLessThanMaxSupply = tierStore.globalMinCap <= tierStore.maxSupply const isMinCapValid = this.state.validation.minCap.valid === VALID @@ -262,33 +254,6 @@ export class stepThree extends React.Component { this.props.tierStore.setGlobalMinCap(value) } - setGasPrice({ id, price }) { - this.setState({ - gasPriceSelected: id - }) - - // Don't modify the price when choosing custom - if (id !== this.props.gasPriceStore.custom.id) { - this.props.generalStore.setGasPrice(price) - } - } - - updateGasPrice = ({value, pristine, valid}) => { - const newState = update(this.state, { - validation: { - gasPrice: { - $set: { - pristine: pristine, - valid: valid - } - } - } - }) - - this.setState(newState) - this.props.generalStore.setGasPrice(gweiToWei(value)) - } - updateWhitelistEnabled = (e) => { this.updateMinCap({ value: '', valid: VALID, pristine: false }) this.updateTierStore(e, "whitelistEnabled", 0) From fd4ef7dffcdbe2d6df1c51011ce2a29d536a0db5 Mon Sep 17 00:00:00 2001 From: fernandomg Date: Tue, 3 Apr 2018 14:51:44 -0300 Subject: [PATCH 30/48] Set minCap initial value to 0 (zero) --- src/components/stepThree/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/stepThree/index.js b/src/components/stepThree/index.js index 9ecb25671..03e7bde6c 100644 --- a/src/components/stepThree/index.js +++ b/src/components/stepThree/index.js @@ -278,7 +278,7 @@ export class stepThree extends React.Component { mutators={{ ...arrayMutators }} initialValues={{ walletAddress: tierStore.tiers[0].walletAddress, - minCap: '', + minCap: 0, gasPrice: gasPriceStore.gasPricesInGwei[0], whitelistEnabled: "no", tiers: this.initialTiers From 6396fec1eb4472e7c87e201a364d80ab72a64ee9 Mon Sep 17 00:00:00 2001 From: fernandomg Date: Tue, 3 Apr 2018 16:06:51 -0300 Subject: [PATCH 31/48] WIP (date validations) --- src/components/stepThree/index.js | 11 ++++++++--- src/utils/validations.js | 17 ++++++++++++++++- 2 files changed, 24 insertions(+), 4 deletions(-) diff --git a/src/components/stepThree/index.js b/src/components/stepThree/index.js index 03e7bde6c..96e35c093 100644 --- a/src/components/stepThree/index.js +++ b/src/components/stepThree/index.js @@ -6,7 +6,7 @@ import { FieldArray } from 'react-final-form-arrays' import { OnChange } from 'react-final-form-listeners' import { Link } from "react-router-dom"; import { setExistingContractParams, getNetworkVersion, getNetWorkNameById } from "../../utils/blockchainHelpers"; -import { gweiToWei } from "../../utils/utils"; +import { gweiToWei, validateLaterOrEqualTime, validateLaterTime, validateTime } from "../../utils/utils"; import { StepNavigation } from "../Common/StepNavigation"; import { WhenFieldChanges } from '../Common/WhenFieldChanges' import { InputField2 } from "../Common/InputField2"; @@ -35,7 +35,7 @@ import { isRequired, composeValidators, isInteger, - isGreaterOrEqualThan, + isGreaterOrEqualThan, isInFuture, isPreviousThan, isSameOrLater, } from '../../utils/validations' import update from 'immutability-helper' import classnames from 'classnames' @@ -431,7 +431,12 @@ export class stepThree extends React.Component { composeValidators( + isRequired(), + isInFuture()(values.tiers[index].endTime), + isPreviousThan()(values.tiers[index].endTime), + () => index === 0 ? isRequired() : isSameOrLater()(values.tiers[index - 1].endTime) + )(value, values)} errorStyle={this.inputErrorStyle} type="datetime-local" side="left" diff --git a/src/utils/validations.js b/src/utils/validations.js index c2c5f29c8..b90acfdba 100644 --- a/src/utils/validations.js +++ b/src/utils/validations.js @@ -1,7 +1,7 @@ import Web3 from 'web3' import { BigNumber } from 'bignumber.js' import { VALIDATION_MESSAGES } from './constants' -import { countDecimalPlaces } from './utils' +import { countDecimalPlaces, validateLaterOrEqualTime, validateLaterTime, validateTime } from './utils' export const validators = (type, value) => { return { @@ -71,6 +71,21 @@ export const isGreaterOrEqualThan = (errorMsg = VALIDATION_MESSAGES.GREATER_OR_E } } +export const isInFuture = (errorMsg = "Should be set in the future") => (value) => { + const isValid = validateTime(value) + return isValid ? undefined : errorMsg +} + +export const isPreviousThan = (errorMsg = "Should be previous than same tier's End Time") => (later) => (value) => { + const isValid = validateLaterTime(later, value) + return isValid ? undefined : errorMsg +} + +export const isSameOrLater = (errorMsg = "Should be same or later than previous tier's End Time") => (previous) => (value) => { + const isValid = validateLaterOrEqualTime(value, previous) + return isValid ? undefined : errorMsg +} + export const isInteger = (errorMsg = VALIDATION_MESSAGES.INTEGER) => (value) => { try { const isValid = new BigNumber(value).isInteger() From 877cb82fc8518a1c7ac3ad32703bfaa2ad623477 Mon Sep 17 00:00:00 2001 From: fernandomg Date: Tue, 3 Apr 2018 18:58:28 -0300 Subject: [PATCH 32/48] Validate tiers' startTime --- src/components/stepThree/index.js | 24 +++++++++++++++++------- src/utils/constants.js | 3 +++ src/utils/validations.js | 24 ++++++++++++------------ 3 files changed, 32 insertions(+), 19 deletions(-) diff --git a/src/components/stepThree/index.js b/src/components/stepThree/index.js index 96e35c093..67318258a 100644 --- a/src/components/stepThree/index.js +++ b/src/components/stepThree/index.js @@ -35,7 +35,10 @@ import { isRequired, composeValidators, isInteger, - isGreaterOrEqualThan, isInFuture, isPreviousThan, isSameOrLater, + isGreaterOrEqualThan, + isDateInFuture, + isDatePreviousThan, + isDateSameOrLaterThan, } from '../../utils/validations' import update from 'immutability-helper' import classnames from 'classnames' @@ -431,12 +434,19 @@ export class stepThree extends React.Component { composeValidators( - isRequired(), - isInFuture()(values.tiers[index].endTime), - isPreviousThan()(values.tiers[index].endTime), - () => index === 0 ? isRequired() : isSameOrLater()(values.tiers[index - 1].endTime) - )(value, values)} + validate={(value, values) => { + const listOfValidations = [ + isRequired(), + isDateInFuture(), + isDatePreviousThan("Should be previous than same tier's End Time")(values.tiers[index].endTime), + ] + + if (index > 0) { + listOfValidations.push(isDateSameOrLaterThan("Should be same or later than previous tier's End Time")(values.tiers[index - 1].endTime)) + } + + return composeValidators(...listOfValidations)(value) + }} errorStyle={this.inputErrorStyle} type="datetime-local" side="left" diff --git a/src/utils/constants.js b/src/utils/constants.js index f8d0d1163..5bbff6d37 100644 --- a/src/utils/constants.js +++ b/src/utils/constants.js @@ -118,6 +118,9 @@ export const VALIDATION_MESSAGES = { LESS_OR_EQUAL: 'Should be less or equal than the specified value', GREATER_OR_EQUAL: 'Should be greater than the specified value', INTEGER: 'Should be integer', + DATE_IN_FUTURE: 'Should be set in the future', + DATE_IS_PREVIOUS: 'Should be previous than specified time', + DATE_IS_SAME_OR_LATER: 'Should be same or later than specified time', } //descriptions of input fields diff --git a/src/utils/validations.js b/src/utils/validations.js index b90acfdba..ffdf71482 100644 --- a/src/utils/validations.js +++ b/src/utils/validations.js @@ -71,30 +71,30 @@ export const isGreaterOrEqualThan = (errorMsg = VALIDATION_MESSAGES.GREATER_OR_E } } -export const isInFuture = (errorMsg = "Should be set in the future") => (value) => { +export const isInteger = (errorMsg = VALIDATION_MESSAGES.INTEGER) => (value) => { + try { + const isValid = new BigNumber(value).isInteger() + return isValid ? undefined : errorMsg + } catch (e) { + return errorMsg + } +} + +export const isDateInFuture = (errorMsg = VALIDATION_MESSAGES.DATE_IN_FUTURE) => (value) => { const isValid = validateTime(value) return isValid ? undefined : errorMsg } -export const isPreviousThan = (errorMsg = "Should be previous than same tier's End Time") => (later) => (value) => { +export const isDatePreviousThan = (errorMsg = VALIDATION_MESSAGES.DATE_IS_PREVIOUS) => (later) => (value) => { const isValid = validateLaterTime(later, value) return isValid ? undefined : errorMsg } -export const isSameOrLater = (errorMsg = "Should be same or later than previous tier's End Time") => (previous) => (value) => { +export const isDateSameOrLaterThan = (errorMsg = VALIDATION_MESSAGES.DATE_IS_SAME_OR_LATER) => (previous) => (value) => { const isValid = validateLaterOrEqualTime(value, previous) return isValid ? undefined : errorMsg } -export const isInteger = (errorMsg = VALIDATION_MESSAGES.INTEGER) => (value) => { - try { - const isValid = new BigNumber(value).isInteger() - return isValid ? undefined : errorMsg - } catch (e) { - return errorMsg - } -} - export const composeValidators = (...validators) => (value) => validators.reduce((errors, validator) => { const validation = validator(value) From 39491d92c22d9941bf4e77f6be03c1e55ad2b50b Mon Sep 17 00:00:00 2001 From: fernandomg Date: Wed, 4 Apr 2018 09:16:13 -0300 Subject: [PATCH 33/48] Validate tiers' endTime --- src/components/stepThree/index.js | 16 +++++++++++++++- src/utils/constants.js | 2 ++ src/utils/validations.js | 10 ++++++++++ 3 files changed, 27 insertions(+), 1 deletion(-) diff --git a/src/components/stepThree/index.js b/src/components/stepThree/index.js index 67318258a..0276b29e0 100644 --- a/src/components/stepThree/index.js +++ b/src/components/stepThree/index.js @@ -39,6 +39,8 @@ import { isDateInFuture, isDatePreviousThan, isDateSameOrLaterThan, + isDateLaterThan, + isDateSameOrPreviousThan, } from '../../utils/validations' import update from 'immutability-helper' import classnames from 'classnames' @@ -456,7 +458,19 @@ export class stepThree extends React.Component { { + const listOfValidations = [ + isRequired(), + isDateInFuture(), + isDateLaterThan("Should be later than same tier's Start Time")(values.tiers[index].startTime), + ] + + if (index < values.tiers.length - 1) { + listOfValidations.push(isDateSameOrPreviousThan("Should be same or previous than next tier's Start Time")(values.tiers[index + 1].startTime)) + } + + return composeValidators(...listOfValidations)(value) + }} errorStyle={this.inputErrorStyle} type="datetime-local" side="right" diff --git a/src/utils/constants.js b/src/utils/constants.js index 5bbff6d37..174c8391f 100644 --- a/src/utils/constants.js +++ b/src/utils/constants.js @@ -121,6 +121,8 @@ export const VALIDATION_MESSAGES = { DATE_IN_FUTURE: 'Should be set in the future', DATE_IS_PREVIOUS: 'Should be previous than specified time', DATE_IS_SAME_OR_LATER: 'Should be same or later than specified time', + DATE_IS_LATER: 'Should be later than specified time', + DATE_IS_SAME_OR_PREVIOUS: 'Should be same or previous than specified time', } //descriptions of input fields diff --git a/src/utils/validations.js b/src/utils/validations.js index ffdf71482..7f5af02f4 100644 --- a/src/utils/validations.js +++ b/src/utils/validations.js @@ -95,6 +95,16 @@ export const isDateSameOrLaterThan = (errorMsg = VALIDATION_MESSAGES.DATE_IS_SAM return isValid ? undefined : errorMsg } +export const isDateLaterThan = (errorMsg = VALIDATION_MESSAGES.DATE_IS_LATER) => (previous) => (value) => { + const isValid = validateLaterTime(value, previous) + return isValid ? undefined : errorMsg +} + +export const isDateSameOrPreviousThan = (errorMsg = VALIDATION_MESSAGES.DATE_IS_SAME_OR_LATER) => (later) => (value) => { + const isValid = validateLaterOrEqualTime(later, value) + return isValid ? undefined : errorMsg +} + export const composeValidators = (...validators) => (value) => validators.reduce((errors, validator) => { const validation = validator(value) From f4f6332126db273de7c9f98cca150e775ebcef6d Mon Sep 17 00:00:00 2001 From: fernandomg Date: Wed, 4 Apr 2018 11:07:04 -0300 Subject: [PATCH 34/48] Update StepTwoFrom snapshot --- .../__snapshots__/StepTwoForm.spec.js.snap | 66 ++++++++++--------- 1 file changed, 36 insertions(+), 30 deletions(-) diff --git a/src/components/stepTwo/__snapshots__/StepTwoForm.spec.js.snap b/src/components/stepTwo/__snapshots__/StepTwoForm.spec.js.snap index b2a7e1488..a2eb254b3 100644 --- a/src/components/stepTwo/__snapshots__/StepTwoForm.spec.js.snap +++ b/src/components/stepTwo/__snapshots__/StepTwoForm.spec.js.snap @@ -34,17 +34,19 @@ exports[`StepTwoForm Should render the component 1`] = ` > The name of your token. Will be used by Etherscan and other tokenbrowsers. Be afraid of trademarks.

-

+

+ /> +

-

+

+ /> +

Refers to how divisible a token can be, from 0 (not at all divisible) to 18 (pretty much continuous).

-

+

+ /> +

From 304c9f2d57dde617b9a577ea464f9e853bd43abd Mon Sep 17 00:00:00 2001 From: fernandomg Date: Wed, 4 Apr 2018 11:08:44 -0300 Subject: [PATCH 35/48] Add tests for validations --- src/utils/validations.js | 2 +- src/utils/validations.spec.js | 333 ++++++++++++++++++++++++++++++---- 2 files changed, 301 insertions(+), 34 deletions(-) diff --git a/src/utils/validations.js b/src/utils/validations.js index 7f5af02f4..01bb086be 100644 --- a/src/utils/validations.js +++ b/src/utils/validations.js @@ -100,7 +100,7 @@ export const isDateLaterThan = (errorMsg = VALIDATION_MESSAGES.DATE_IS_LATER) => return isValid ? undefined : errorMsg } -export const isDateSameOrPreviousThan = (errorMsg = VALIDATION_MESSAGES.DATE_IS_SAME_OR_LATER) => (later) => (value) => { +export const isDateSameOrPreviousThan = (errorMsg = VALIDATION_MESSAGES.DATE_IS_SAME_OR_PREVIOUS) => (later) => (value) => { const isValid = validateLaterOrEqualTime(later, value) return isValid ? undefined : errorMsg } diff --git a/src/utils/validations.spec.js b/src/utils/validations.spec.js index be1829879..2394beaca 100644 --- a/src/utils/validations.spec.js +++ b/src/utils/validations.spec.js @@ -1,5 +1,11 @@ import { + composeValidators, isAddress, + isDateInFuture, + isDateLaterThan, + isDatePreviousThan, + isDateSameOrLaterThan, + isDateSameOrPreviousThan, isDecimalPlacesNotGreaterThan, isGreaterOrEqualThan, isInteger, @@ -13,6 +19,7 @@ import { validators } from './validations' import { VALIDATION_MESSAGES } from './constants' +import MockDate from 'mockdate' describe('validateTokenName', () => { [ @@ -65,7 +72,7 @@ describe('validateDecimals', () => { { value: '1.', expected: VALIDATION_MESSAGES.DECIMALS }, { value: '1e1', expected: VALIDATION_MESSAGES.DECIMALS }, { value: '--', expected: VALIDATION_MESSAGES.DECIMALS }, - { value: undefined , expected: undefined }, + { value: undefined, expected: undefined }, { value: '', expected: undefined }, { value: '0', expected: undefined }, { value: '1', expected: undefined }, @@ -177,15 +184,15 @@ describe('isRequired', () => { describe('isDecimalPlacesNotGreaterThan', () => { const testCases = [ - { value: '1', errorMessage: undefined, comparedTo: '5',expected: undefined }, - { value: '1.1', errorMessage: undefined, comparedTo: '5',expected: undefined }, - { value: '1.12', errorMessage: undefined, comparedTo: '5',expected: undefined }, - { value: '1.123', errorMessage: undefined, comparedTo: '5',expected: undefined }, - { value: '1.1234', errorMessage: undefined, comparedTo: '5',expected: undefined }, - { value: '1.12345', errorMessage: undefined, comparedTo: '5',expected: undefined }, - { value: '1.123456', errorMessage: undefined, comparedTo: '5',expected: VALIDATION_MESSAGES.DECIMAL_PLACES }, - { value: '1.1234567', errorMessage: undefined, comparedTo: '5',expected: VALIDATION_MESSAGES.DECIMAL_PLACES }, - { value: '1.1234567', errorMessage: 'Personalized error message', comparedTo: '5',expected: 'Personalized error message' }, + { value: '1', errorMessage: undefined, comparedTo: '5', expected: undefined }, + { value: '1.1', errorMessage: undefined, comparedTo: '5', expected: undefined }, + { value: '1.12', errorMessage: undefined, comparedTo: '5', expected: undefined }, + { value: '1.123', errorMessage: undefined, comparedTo: '5', expected: undefined }, + { value: '1.1234', errorMessage: undefined, comparedTo: '5', expected: undefined }, + { value: '1.12345', errorMessage: undefined, comparedTo: '5', expected: undefined }, + { value: '1.123456', errorMessage: undefined, comparedTo: '5', expected: VALIDATION_MESSAGES.DECIMAL_PLACES }, + { value: '1.1234567', errorMessage: undefined, comparedTo: '5', expected: VALIDATION_MESSAGES.DECIMAL_PLACES }, + { value: '1.1234567', errorMessage: 'Personalized error message', comparedTo: '5', expected: 'Personalized error message' }, ] testCases.forEach(testCase => { @@ -199,18 +206,19 @@ describe('isDecimalPlacesNotGreaterThan', () => { describe('isLessOrEqualThan', () => { const testCases = [ - { value: '1', errorMessage: undefined, comparedTo: '5',expected: undefined }, - { value: '1.1', errorMessage: undefined, comparedTo: '5',expected: undefined }, - { value: '2', errorMessage: undefined, comparedTo: '5',expected: undefined }, - { value: '3', errorMessage: undefined, comparedTo: '5',expected: undefined }, - { value: '4', errorMessage: undefined, comparedTo: '5',expected: undefined }, - { value: '5', errorMessage: undefined, comparedTo: '5',expected: undefined }, - { value: '1000000000000000000', errorMessage: undefined, comparedTo: '1e18',expected: undefined }, - { value: '999999999999999999', errorMessage: undefined, comparedTo: '1e18',expected: undefined }, - { value: '10000000000000000001', errorMessage: undefined, comparedTo: '1e18',expected: VALIDATION_MESSAGES.LESS_OR_EQUAL }, - { value: '5.1', errorMessage: undefined, comparedTo: '5',expected: VALIDATION_MESSAGES.LESS_OR_EQUAL }, - { value: '6', errorMessage: undefined, comparedTo: '5',expected: VALIDATION_MESSAGES.LESS_OR_EQUAL }, - { value: '6', errorMessage: 'Personalized error message', comparedTo: '5',expected: 'Personalized error message' }, + { value: '1', errorMessage: undefined, comparedTo: '5', expected: undefined }, + { value: '1.1', errorMessage: undefined, comparedTo: '5', expected: undefined }, + { value: '2', errorMessage: undefined, comparedTo: '5', expected: undefined }, + { value: '3', errorMessage: undefined, comparedTo: '5', expected: undefined }, + { value: '4', errorMessage: undefined, comparedTo: '5', expected: undefined }, + { value: '5', errorMessage: undefined, comparedTo: undefined, expected: undefined }, + { value: '1000000000000000000', errorMessage: undefined, comparedTo: '1e18', expected: undefined }, + { value: '999999999999999999', errorMessage: undefined, comparedTo: '1e18', expected: undefined }, + { value: 'not-a-number', errorMessage: undefined, comparedTo: '1e18', expected: VALIDATION_MESSAGES.LESS_OR_EQUAL }, + { value: '10000000000000000001', errorMessage: undefined, comparedTo: '1e18', expected: VALIDATION_MESSAGES.LESS_OR_EQUAL }, + { value: '5.1', errorMessage: undefined, comparedTo: '5', expected: VALIDATION_MESSAGES.LESS_OR_EQUAL }, + { value: '6', errorMessage: undefined, comparedTo: '5', expected: VALIDATION_MESSAGES.LESS_OR_EQUAL }, + { value: '6', errorMessage: 'Personalized error message', comparedTo: '5', expected: 'Personalized error message' }, ] testCases.forEach(testCase => { @@ -224,17 +232,18 @@ describe('isLessOrEqualThan', () => { describe('isGreaterOrEqualThan', () => { const testCases = [ - { value: '10', errorMessage: undefined, comparedTo: '5',expected: undefined }, - { value: '10.1', errorMessage: undefined, comparedTo: '5',expected: undefined }, - { value: '20', errorMessage: undefined, comparedTo: '5',expected: undefined }, - { value: '30', errorMessage: undefined, comparedTo: '5',expected: undefined }, - { value: '40', errorMessage: undefined, comparedTo: '5',expected: undefined }, - { value: '50', errorMessage: undefined, comparedTo: '5',expected: undefined }, - { value: '0.1', errorMessage: undefined, comparedTo: '0.1',expected: undefined }, - { value: '0.1234', errorMessage: undefined, comparedTo: '0.1',expected: undefined }, - { value: '0.0001', errorMessage: undefined, comparedTo: '0.1',expected: VALIDATION_MESSAGES.GREATER_OR_EQUAL }, - { value: '0', errorMessage: undefined, comparedTo: '0.1',expected: VALIDATION_MESSAGES.GREATER_OR_EQUAL }, - { value: '-10', errorMessage: undefined, comparedTo: '0.1',expected: VALIDATION_MESSAGES.GREATER_OR_EQUAL }, + { value: '10', errorMessage: undefined, comparedTo: '5', expected: undefined }, + { value: '10.1', errorMessage: undefined, comparedTo: '5', expected: undefined }, + { value: '20', errorMessage: undefined, comparedTo: '5', expected: undefined }, + { value: '30', errorMessage: undefined, comparedTo: '5', expected: undefined }, + { value: '40', errorMessage: undefined, comparedTo: '5', expected: undefined }, + { value: '50', errorMessage: undefined, comparedTo: undefined, expected: undefined }, + { value: '0.1', errorMessage: undefined, comparedTo: '0.1', expected: undefined }, + { value: '0.1234', errorMessage: undefined, comparedTo: '0.1', expected: undefined }, + { value: 'not-a-number', errorMessage: undefined, comparedTo: '0.1', expected: VALIDATION_MESSAGES.GREATER_OR_EQUAL }, + { value: '0.0001', errorMessage: undefined, comparedTo: '0.1', expected: VALIDATION_MESSAGES.GREATER_OR_EQUAL }, + { value: '0', errorMessage: undefined, comparedTo: '0.1', expected: VALIDATION_MESSAGES.GREATER_OR_EQUAL }, + { value: '-10', errorMessage: undefined, comparedTo: '0.1', expected: VALIDATION_MESSAGES.GREATER_OR_EQUAL }, { value: '-10', errorMessage: 'Personalized error message', comparedTo: '0.1', expected: 'Personalized error message' }, ] @@ -275,3 +284,261 @@ describe('validators', () => { expect(validators('nonExistent', '')).toBeFalsy() }) }) + +describe('Date/Time validations', () => { + const TIMESTAMPS = { + CURRENT_TIME: 1520852400000, + PLUS_5_MINUTES: 1520852700000, + PLUS_10_DAYS: 1521716400000, + MINUS_5_MINUTES: 1520852100000, + MINUS_10_DAYS: 1519988400000, + } + + const getKeyByValue = (value) => Object.keys(TIMESTAMPS).find(key => TIMESTAMPS[key] === value) + + beforeEach(() => { + MockDate.set(TIMESTAMPS.CURRENT_TIME) + }) + + afterEach(() => { + MockDate.reset() + }) + + describe('isDateInFuture', () => { + const testCases = [ + { value: TIMESTAMPS.PLUS_5_MINUTES, errorMessage: undefined, expected: undefined }, + { value: TIMESTAMPS.CURRENT_TIME, errorMessage: undefined, expected: VALIDATION_MESSAGES.DATE_IN_FUTURE }, + { value: TIMESTAMPS.MINUS_5_MINUTES, errorMessage: undefined, expected: VALIDATION_MESSAGES.DATE_IN_FUTURE }, + { + value: TIMESTAMPS.CURRENT_TIME, + errorMessage: 'Personalized error message', + expected: 'Personalized error message' + }, + ] + + testCases.forEach(testCase => { + const action = testCase.expected === undefined ? 'pass' : 'fail' + + it(`Should ${action} for '${getKeyByValue(testCase.value)}'`, () => { + expect(isDateInFuture(testCase.errorMessage)(testCase.value)).toBe(testCase.expected) + }) + }) + }) + + describe('isDatePreviousThan', () => { + const testCases = [ + { + value: TIMESTAMPS.MINUS_5_MINUTES, + errorMessage: undefined, + comparedTo: TIMESTAMPS.CURRENT_TIME, + expected: undefined + }, + { + value: TIMESTAMPS.PLUS_5_MINUTES, + errorMessage: undefined, + comparedTo: TIMESTAMPS.PLUS_10_DAYS, + expected: undefined + }, + { + value: TIMESTAMPS.PLUS_5_MINUTES, + errorMessage: undefined, + comparedTo: TIMESTAMPS.CURRENT_TIME, + expected: VALIDATION_MESSAGES.DATE_IS_PREVIOUS + }, + { + value: TIMESTAMPS.PLUS_10_DAYS, + errorMessage: undefined, + comparedTo: TIMESTAMPS.MINUS_10_DAYS, + expected: VALIDATION_MESSAGES.DATE_IS_PREVIOUS + }, + { + value: TIMESTAMPS.CURRENT_TIME, + errorMessage: undefined, + comparedTo: TIMESTAMPS.CURRENT_TIME, + expected: VALIDATION_MESSAGES.DATE_IS_PREVIOUS + }, + { + value: TIMESTAMPS.CURRENT_TIME, + errorMessage: 'Personalized error message', + comparedTo: TIMESTAMPS.CURRENT_TIME, + expected: 'Personalized error message' + }, + ] + + testCases.forEach(testCase => { + const action = testCase.expected === undefined ? 'pass' : 'fail' + + it(`Should ${action} for '${getKeyByValue(testCase.value)} compared to ${getKeyByValue(testCase.comparedTo)}'`, () => { + expect(isDatePreviousThan(testCase.errorMessage)(testCase.comparedTo)(testCase.value)).toBe(testCase.expected) + }) + }) + }) + + describe('isDateSameOrLaterThan', () => { + const testCases = [ + { + value: TIMESTAMPS.PLUS_5_MINUTES, + errorMessage: undefined, + comparedTo: TIMESTAMPS.CURRENT_TIME, + expected: undefined + }, + { + value: TIMESTAMPS.PLUS_5_MINUTES, + errorMessage: undefined, + comparedTo: TIMESTAMPS.MINUS_10_DAYS, + expected: undefined + }, + { + value: TIMESTAMPS.CURRENT_TIME, + errorMessage: undefined, + comparedTo: TIMESTAMPS.CURRENT_TIME, + expected: undefined + }, + { + value: TIMESTAMPS.MINUS_5_MINUTES, + errorMessage: undefined, + comparedTo: TIMESTAMPS.CURRENT_TIME, + expected: VALIDATION_MESSAGES.DATE_IS_SAME_OR_LATER + }, + { + value: TIMESTAMPS.MINUS_10_DAYS, + errorMessage: undefined, + comparedTo: TIMESTAMPS.PLUS_10_DAYS, + expected: VALIDATION_MESSAGES.DATE_IS_SAME_OR_LATER + }, + { + value: TIMESTAMPS.MINUS_10_DAYS, + errorMessage: 'Personalized error message', + comparedTo: TIMESTAMPS.PLUS_10_DAYS, + expected: 'Personalized error message' + }, + ] + + testCases.forEach(testCase => { + const action = testCase.expected === undefined ? 'pass' : 'fail' + + it(`Should ${action} for '${getKeyByValue(testCase.value)} compared to ${getKeyByValue(testCase.comparedTo)}'`, () => { + expect(isDateSameOrLaterThan(testCase.errorMessage)(testCase.comparedTo)(testCase.value)).toBe(testCase.expected) + }) + }) + }) + + describe('isDateLaterThan', () => { + const testCases = [ + { + value: TIMESTAMPS.PLUS_5_MINUTES, + errorMessage: undefined, + comparedTo: TIMESTAMPS.CURRENT_TIME, + expected: undefined + }, + { + value: TIMESTAMPS.PLUS_5_MINUTES, + errorMessage: undefined, + comparedTo: TIMESTAMPS.MINUS_10_DAYS, + expected: undefined + }, + { + value: TIMESTAMPS.CURRENT_TIME, + errorMessage: undefined, + comparedTo: TIMESTAMPS.CURRENT_TIME, + expected: VALIDATION_MESSAGES.DATE_IS_LATER + }, + { + value: TIMESTAMPS.MINUS_5_MINUTES, + errorMessage: undefined, + comparedTo: TIMESTAMPS.CURRENT_TIME, + expected: VALIDATION_MESSAGES.DATE_IS_LATER + }, + { + value: TIMESTAMPS.MINUS_10_DAYS, + errorMessage: undefined, + comparedTo: TIMESTAMPS.PLUS_10_DAYS, + expected: VALIDATION_MESSAGES.DATE_IS_LATER + }, + { + value: TIMESTAMPS.MINUS_10_DAYS, + errorMessage: 'Personalized error message', + comparedTo: TIMESTAMPS.PLUS_10_DAYS, + expected: 'Personalized error message' + }, + ] + + testCases.forEach(testCase => { + const action = testCase.expected === undefined ? 'pass' : 'fail' + + it(`Should ${action} for '${getKeyByValue(testCase.value)} compared to ${getKeyByValue(testCase.comparedTo)}'`, () => { + expect(isDateLaterThan(testCase.errorMessage)(testCase.comparedTo)(testCase.value)).toBe(testCase.expected) + }) + }) + }) + + describe('isDateSameOrPreviousThan', () => { + const testCases = [ + { + value: TIMESTAMPS.MINUS_5_MINUTES, + errorMessage: undefined, + comparedTo: TIMESTAMPS.CURRENT_TIME, + expected: undefined + }, + { + value: TIMESTAMPS.MINUS_5_MINUTES, + errorMessage: undefined, + comparedTo: TIMESTAMPS.PLUS_10_DAYS, + expected: undefined + }, + { + value: TIMESTAMPS.CURRENT_TIME, + errorMessage: undefined, + comparedTo: TIMESTAMPS.CURRENT_TIME, + expected: undefined + }, + { + value: TIMESTAMPS.PLUS_5_MINUTES, + errorMessage: undefined, + comparedTo: TIMESTAMPS.CURRENT_TIME, + expected: VALIDATION_MESSAGES.DATE_IS_SAME_OR_PREVIOUS + }, + { + value: TIMESTAMPS.PLUS_10_DAYS, + errorMessage: undefined, + comparedTo: TIMESTAMPS.MINUS_10_DAYS, + expected: VALIDATION_MESSAGES.DATE_IS_SAME_OR_PREVIOUS + }, + { + value: TIMESTAMPS.PLUS_10_DAYS, + errorMessage: 'Personalized error message', + comparedTo: TIMESTAMPS.MINUS_10_DAYS, + expected: 'Personalized error message' + }, + ] + + testCases.forEach(testCase => { + const action = testCase.expected === undefined ? 'pass' : 'fail' + + it(`Should ${action} for '${getKeyByValue(testCase.value)} compared to ${getKeyByValue(testCase.comparedTo)}'`, () => { + expect(isDateSameOrPreviousThan(testCase.errorMessage)(testCase.comparedTo)(testCase.value)).toBe(testCase.expected) + }) + }) + }) +}) + +describe('composeValidators', () => { + it('Should return an array of errors', () => { + const listOfErrors = composeValidators( + isRequired(), + isNonNegative(), + )(-123) + + expect(Array.isArray(listOfErrors)).toBeTruthy() + }) + + it('Should return an empty array if there is no error', () => { + const listOfErrors = composeValidators( + isRequired(), + isNonNegative(), + )(123) + + expect(Array.isArray(listOfErrors)).toBeTruthy() + expect(listOfErrors.length).toBe(0) + }) +}) From 03974ea70b9f5c27d2b55923ed269af555457c4a Mon Sep 17 00:00:00 2001 From: fernandomg Date: Wed, 4 Apr 2018 15:01:58 -0300 Subject: [PATCH 36/48] Create TierBlock component --- src/components/Common/TierBlock.js | 197 +++++++++++++++++++++++++++++ 1 file changed, 197 insertions(+) create mode 100644 src/components/Common/TierBlock.js diff --git a/src/components/Common/TierBlock.js b/src/components/Common/TierBlock.js new file mode 100644 index 000000000..2f44750fc --- /dev/null +++ b/src/components/Common/TierBlock.js @@ -0,0 +1,197 @@ +import React from 'react' +import { Field } from 'react-final-form' +import { OnChange } from 'react-final-form-listeners' +import { InputField2 } from './InputField2' +import { WhitelistInputBlock } from './WhitelistInputBlock' +import { + composeValidators, + isDateInFuture, + isDateLaterThan, + isDatePreviousThan, + isDateSameOrLaterThan, + isDateSameOrPreviousThan, + isInteger, + isLessOrEqualThan, + isPositive, + isRequired, + validateTokenName, +} from '../../utils/validations' +import { DESCRIPTION, TEXT_FIELDS } from '../../utils/constants' + +const { + ALLOWMODIFYING, + CROWDSALE_SETUP_NAME, + START_TIME, + END_TIME, + RATE, + SUPPLY +} = TEXT_FIELDS + +const inputErrorStyle = { + color: 'red', + fontWeight: 'bold', + fontSize: '12px', + width: '100%', + height: '20px', +} + +export const TierBlock = ({ fields, ...props }) => { + const validateTierName = (value) => validateTokenName(value) + + const validateTierStartDate = (index) => (value, values) => { + const listOfValidations = [ + isRequired(), + isDateInFuture(), + isDatePreviousThan("Should be previous than same tier's End Time")(values.tiers[index].endTime), + ] + + if (index > 0) { + listOfValidations.push(isDateSameOrLaterThan("Should be same or later than previous tier's End Time")(values.tiers[index - 1].endTime)) + } + + return composeValidators(...listOfValidations)(value) + } + + const validateTierEndDate = (index) => (value, values) => { + const listOfValidations = [ + isRequired(), + isDateInFuture(), + isDateLaterThan("Should be later than same tier's Start Time")(values.tiers[index].startTime), + ] + + if (index < values.tiers.length - 1) { + listOfValidations.push(isDateSameOrPreviousThan("Should be same or previous than next tier's Start Time")(values.tiers[index + 1].startTime)) + } + + return composeValidators(...listOfValidations)(value) + } + + return ( +
+ {fields.map((name, index) => ( +
+
+
+ + + ( +
+ +
+ + +
+

{DESCRIPTION.ALLOW_MODIFYING}

+
+ )} + /> +
+ +
+ + +
+ +
+ + + { + /* + * TODO: REVIEW. I'm not sure about this approach. + * But it worked for me to keep the error messages properly updated for the minCap field. + */ + } + + {({ input: { onChange } }) => ( + + {() => { + onChange(0) + onChange(props.minCap) + }} + + )} + +
+
+ { + props.tierStore.tiers[index].whitelistEnabled === 'yes' ? ( +
+
+

Whitelist

+
+ +
+ ) : null + } +
+ ))} +
+ ) +} From 8a872c35edb8e54e81e6f7b1fd8a40a10a6a8317 Mon Sep 17 00:00:00 2001 From: fernandomg Date: Wed, 4 Apr 2018 15:02:37 -0300 Subject: [PATCH 37/48] Create StepThreeForm component --- src/components/stepThree/StepThreeForm.js | 172 ++++++++++++++++++++++ 1 file changed, 172 insertions(+) create mode 100644 src/components/stepThree/StepThreeForm.js diff --git a/src/components/stepThree/StepThreeForm.js b/src/components/stepThree/StepThreeForm.js new file mode 100644 index 000000000..b613c1f3d --- /dev/null +++ b/src/components/stepThree/StepThreeForm.js @@ -0,0 +1,172 @@ +import React from 'react' +import { Field, FormSpy } from 'react-final-form' +import { FieldArray } from 'react-final-form-arrays' +import { WhenFieldChanges } from '../Common/WhenFieldChanges' +import { InputField2 } from '../Common/InputField2' +import GasPriceInput from './GasPriceInput' +import { gweiToWei } from '../../utils/utils' +import classnames from 'classnames' +import { + composeValidators, + isAddress, + isDecimalPlacesNotGreaterThan, + isGreaterOrEqualThan, + isLessOrEqualThan, + isNonNegative, +} from '../../utils/validations' +import { TEXT_FIELDS, VALIDATION_TYPES } from '../../utils/constants' +import { TierBlock } from '../Common/TierBlock' + +const { VALID } = VALIDATION_TYPES +const { MINCAP, WALLET_ADDRESS } = TEXT_FIELDS + +const inputErrorStyle = { + color: 'red', + fontWeight: 'bold', + fontSize: '12px', + width: '100%', + height: '20px', +} + +export const StepThreeForm = ({ handleSubmit, values, invalid, pristine, mutators: { push }, ...props }) => { + const submitButtonClass = classnames('button', 'button_fill', { + button_disabled: pristine || invalid + }) + + const addTier = () => { + props.addCrowdsale() + const lastTier = props.tierStore.tiers[props.tierStore.tiers.length - 1] + push('tiers', JSON.parse(JSON.stringify(lastTier))) + } + + const handleOnChange = ({ values }) => { + props.tierStore.updateWalletAddress(values.walletAddress, VALID) + props.generalStore.setGasPrice(gweiToWei(values.gasPrice.price)) + props.tierStore.setGlobalMinCap(values.minCap || 0) + props.tierStore.setTierProperty(values.whitelistEnabled, "whitelistEnabled", 0) + + values.tiers.forEach((tier, index) => { + props.tierStore.setTierProperty(tier.tier, 'tier', index) + props.tierStore.setTierProperty(tier.updatable, 'updatable', index) + props.tierStore.setTierProperty(tier.startTime, 'startTime', index) + props.tierStore.setTierProperty(tier.endTime, 'endTime', index) + props.tierStore.updateRate(tier.rate, VALID, index) + props.tierStore.setTierProperty(tier.supply, 'supply', index) + props.tierStore.validateTiers('supply', index) + }) + } + + return ( +
+ +
+
+
+
+

Crowdsale setup

+

The most important and exciting part of the crowdsale process. Here you can + define parameters of your crowdsale campaign.

+
+
+

Global settings

+
+
+ + + composeValidators( + isDecimalPlacesNotGreaterThan("Should not have more than 9 decimals")(9), + isGreaterOrEqualThan("Should be greater than 0.1")(0.1) + )(value.price)} + /> +
+
+ + ( +
+ +
+ + +
+

Enables whitelisting. If disabled, anyone can participate in the + crowdsale.

+
+ )} + /> +
+
+
+ + + {({ fields }) => ( + + )} + + +
+
+ Add Tier +
+ Continue +
+ + + + ) +} From c3691ccc89fa0d4c4f242c827d00cc546742497f Mon Sep 17 00:00:00 2001 From: fernandomg Date: Wed, 4 Apr 2018 15:03:15 -0300 Subject: [PATCH 38/48] Refactor StepThree component --- src/components/stepThree/index.js | 613 ++---------------------------- src/stores/TierStore.js | 34 +- 2 files changed, 73 insertions(+), 574 deletions(-) diff --git a/src/components/stepThree/index.js b/src/components/stepThree/index.js index 0276b29e0..525356c46 100644 --- a/src/components/stepThree/index.js +++ b/src/components/stepThree/index.js @@ -1,63 +1,17 @@ import React from "react"; import "../../assets/stylesheets/application.css"; -import { Field, Form, FormSpy } from 'react-final-form' +import { Form } from 'react-final-form' import arrayMutators from 'final-form-arrays' -import { FieldArray } from 'react-final-form-arrays' -import { OnChange } from 'react-final-form-listeners' import { Link } from "react-router-dom"; import { setExistingContractParams, getNetworkVersion, getNetWorkNameById } from "../../utils/blockchainHelpers"; -import { gweiToWei, validateLaterOrEqualTime, validateLaterTime, validateTime } from "../../utils/utils"; import { StepNavigation } from "../Common/StepNavigation"; -import { WhenFieldChanges } from '../Common/WhenFieldChanges' -import { InputField2 } from "../Common/InputField2"; -import { CrowdsaleBlock } from "./CrowdsaleBlock"; -import { WhitelistInputBlock } from '../Common/WhitelistInputBlock' -import GasPriceInput from './GasPriceInput' -import { defaultCompanyStartDate, defaultCompanyEndDate } from './utils' -import { - NAVIGATION_STEPS, - VALIDATION_TYPES, - TEXT_FIELDS, - CHAINS, - DESCRIPTION, - defaultTier, - defaultTierValidations -} from '../../utils/constants' +import { NAVIGATION_STEPS, CHAINS } from '../../utils/constants' import { inject, observer } from "mobx-react"; import { Loader } from '../Common/Loader' import { noGasPriceAvailable, warningOnMainnetAlert } from '../../utils/alerts' -import { - isAddress, - isDecimalPlacesNotGreaterThan, - isNonNegative, - isLessOrEqualThan, - isPositive, - isRequired, - composeValidators, - isInteger, - isGreaterOrEqualThan, - isDateInFuture, - isDatePreviousThan, - isDateSameOrLaterThan, - isDateLaterThan, - isDateSameOrPreviousThan, -} from '../../utils/validations' -import update from 'immutability-helper' -import classnames from 'classnames' +import { StepThreeForm } from './StepThreeForm' const { CROWDSALE_SETUP } = NAVIGATION_STEPS; -const { VALID, INVALID } = VALIDATION_TYPES; -const { - ALLOWMODIFYING, - CROWDSALE_SETUP_NAME, - MINCAP, - WALLET_ADDRESS, - ENABLE_WHITELISTING, - START_TIME, - END_TIME, - RATE, - SUPPLY -} = TEXT_FIELDS; @inject( "contractStore", @@ -83,203 +37,69 @@ export class stepThree extends React.Component { this.state = { loading: true, - minCap: props.tierStore.globalMinCap || '', - walletAddress: '', - validation: { - minCap: { - pristine: true, - valid: VALID - }, - walletAddress: { - pristine: true, - valid: INVALID - } - } } } componentWillMount () { - const { gasPriceStore, tierStore } = this.props + const { gasPriceStore, tierStore, web3Store } = this.props if (tierStore.tiers.length === 0) { - this.addCrowdsale() + tierStore.addCrowdsale(web3Store.curAddress) this.initialTiers = JSON.parse(JSON.stringify(tierStore.tiers)) } - window.scrollTo(0, 0) - gasPriceStore.updateValues() .catch(() => noGasPriceAvailable()) .then(() => { this.setState({ loading: false }) - // this.updateWalletAddress({ - // address: tierStore.tiers[0].walletAddress, - // pristine: true, - // valid: VALID, - // }) - // window.scrollTo(0, 0) + window.scrollTo(0, 0) }) } - showErrorMessages = () => { - const { tierStore } = this.props - - tierStore.invalidateToken() - } - - updateTierStore = (event, property, index) => { - const { tierStore } = this.props - const value = event.target.value - - tierStore.setTierProperty(value, property, index) - tierStore.validateTiers(property, index) - } - - addCrowdsale() { - const { tierStore, web3Store } = this.props - const { curAddress } = web3Store - - const num = tierStore.tiers.length - const newTier = Object.assign({}, defaultTier) - const newTierValidations = Object.assign({}, defaultTierValidations) - - newTier.tier = `Tier ${num + 1}` - - if (num === 0) { - newTier.whitelistEnabled = "no" - newTier.walletAddress = curAddress - } - - tierStore.addTier(newTier, newTierValidations) - this.setTierDates(num) - } - - setTierDates(num) { - const { tierStore } = this.props - const defaultStartTime = 0 === num ? defaultCompanyStartDate() : this.tierEndTime(num - 1) - const defaultEndTime = 0 === num ? defaultCompanyEndDate(defaultStartTime) : defaultCompanyEndDate(this.tierEndTime(num - 1)) - - const startTime = tierStore.tiers[num].startTime || defaultStartTime - const endTime = tierStore.tiers[num].endTime || defaultEndTime - - tierStore.setTierProperty(startTime, 'startTime', num) - tierStore.setTierProperty(endTime, 'endTime', num) - } - - tierEndTime = (index) => this.props.tierStore.tiers[index].endTime - goToDeploymentStage = () => { this.props.history.push('/4') } - beforeNavigate = () => { - const { tierStore } = this.props - const isMinCapLessThanMaxSupply = tierStore.globalMinCap <= tierStore.maxSupply - const isMinCapValid = this.state.validation.minCap.valid === VALID - - for (let index = 0; index < tierStore.tiers.length; index++) { - tierStore.validateTiers('endTime', index) - tierStore.validateTiers('startTime', index) - } - - if (!isMinCapLessThanMaxSupply) { - this.setState(update(this.state, { - validation: { - minCap: { - valid: { $set: INVALID } + handleOnSubmit = () => { + const { tierStore, reservedTokenStore, deploymentStore } = this.props + const tiersCount = tierStore.tiers.length + const reservedCount = reservedTokenStore.tokens.length + const hasWhitelist = tierStore.tiers[0].whitelistEnabled === 'yes' + + deploymentStore.initialize(!!reservedCount, hasWhitelist, tiersCount) + + getNetworkVersion() + .then(networkID => { + if (getNetWorkNameById(networkID) === CHAINS.MAINNET) { + const { generalStore } = this.props + const priceSelected = generalStore.gasPrice + let whitelistCount = 0 + + if (hasWhitelist) { + whitelistCount = tierStore.tiers.reduce((total, tier) => { + total += tier.whitelist.length + return total + }, 0) } - } - })) - } - - if (tierStore.areTiersValid /* && gasPriceIsValid */ && isMinCapValid && isMinCapLessThanMaxSupply) { - const { reservedTokenStore, deploymentStore } = this.props - const tiersCount = tierStore.tiers.length - const reservedCount = reservedTokenStore.tokens.length - const hasWhitelist = tierStore.tiers[0].whitelistEnabled === 'yes' - - deploymentStore.initialize(!!reservedCount, hasWhitelist, tiersCount) - - getNetworkVersion() - .then(networkID => { - if (getNetWorkNameById(networkID) === CHAINS.MAINNET) { - const { generalStore } = this.props - const priceSelected = generalStore.gasPrice - let whitelistCount = 0 - - if (hasWhitelist) { - whitelistCount = tierStore.tiers.reduce((total, tier) => { - total += tier.whitelist.length - return total - }, 0) - } - - return warningOnMainnetAlert(tiersCount, priceSelected, reservedCount, whitelistCount, this.goToDeploymentStage) - } - this.goToDeploymentStage() - }) - .catch(error => { - console.error(error) - }) - } - } - - updateWalletAddress = ({ address, pristine, valid }) => { - const newState = update(this.state, { - walletAddress: { $set: address }, - validation: { - walletAddress: { - $set: { - pristine, - valid, - }, - }, - }, - }) - - this.setState(newState) - this.props.tierStore.updateWalletAddress(address, valid) - } - - updateMinCap = ({ value, pristine, valid }) => { - const newState = update(this.state, { - validation: { - minCap: { - $set: { - pristine: pristine, - valid: valid - } + return warningOnMainnetAlert(tiersCount, priceSelected, reservedCount, whitelistCount, this.goToDeploymentStage) } - } - }) - newState.minCap = value - - this.setState(newState) - this.props.tierStore.setGlobalMinCap(value) - } - updateWhitelistEnabled = (e) => { - this.updateMinCap({ value: '', valid: VALID, pristine: false }) - this.updateTierStore(e, "whitelistEnabled", 0) - } - - inputErrorStyle = { - color: 'red', - fontWeight: 'bold', - fontSize: '12px', - width: '100%', - height: '20px', + this.goToDeploymentStage() + }) + .catch(error => { + console.error(error) + }) } - render() { + render () { const { generalStore, tierStore, gasPriceStore, tokenStore } = this.props return (
{ - const submitButtonClass = classnames('button', 'button_fill', { - button_disabled: pristine || invalid - }) - - return ( - - -
-
-
-
-

Crowdsale setup

-

The most important and exciting part of the crowdsale process. Here you can - define parameters of your crowdsale campaign.

-
-
-

Global settings

-
-
- - - composeValidators( - isDecimalPlacesNotGreaterThan("Should not have more than 9 decimals")(9), - isGreaterOrEqualThan("Should be greater than 0.1")(0.1) - )(value.price)} - /> -
-
- - ( -
- -
- - -
-

Enables whitelisting. If disabled, anyone can participate in the crowdsale.

-
- )} - /> -
-
-
- - - {({ fields }) => ( -
- {fields.map((name, index) => ( -
-
-
- - ( -
- -
- - -
-

{DESCRIPTION.ALLOW_MODIFYING}

-
- )} - /> -
- -
- { - const listOfValidations = [ - isRequired(), - isDateInFuture(), - isDatePreviousThan("Should be previous than same tier's End Time")(values.tiers[index].endTime), - ] - - if (index > 0) { - listOfValidations.push(isDateSameOrLaterThan("Should be same or later than previous tier's End Time")(values.tiers[index - 1].endTime)) - } - - return composeValidators(...listOfValidations)(value) - }} - errorStyle={this.inputErrorStyle} - type="datetime-local" - side="left" - label={START_TIME} - description={DESCRIPTION.START_TIME} - /> - { - const listOfValidations = [ - isRequired(), - isDateInFuture(), - isDateLaterThan("Should be later than same tier's Start Time")(values.tiers[index].startTime), - ] - - if (index < values.tiers.length - 1) { - listOfValidations.push(isDateSameOrPreviousThan("Should be same or previous than next tier's Start Time")(values.tiers[index + 1].startTime)) - } - - return composeValidators(...listOfValidations)(value) - }} - errorStyle={this.inputErrorStyle} - type="datetime-local" - side="right" - label={END_TIME} - description={DESCRIPTION.END_TIME} - /> -
- -
- - - {/* - * TODO: REVIEW. I'm not sure about this approach. - * But it worked for me to keep the error messages properly updated for the minCap field. - */} - - {({ input: { onChange } }) => ( - - {() => { - const { minCap } = values - onChange(0) - onChange(minCap) - }} - - )} - -
-
- { - tierStore.tiers[index].whitelistEnabled === 'yes' ? ( -
-
-

Whitelist

-
- -
- ) : null - } -
- ))} -
- )} -
- -
-
{ - this.addCrowdsale() - const lastTier = this.props.tierStore.tiers[this.props.tierStore.tiers.length - 1] - push('tiers', JSON.parse(JSON.stringify(lastTier))) - }}> - Add Tier -
- Continue -
- - { - tierStore.updateWalletAddress(values.walletAddress, VALID) - generalStore.setGasPrice(gweiToWei(values.gasPrice.price)) - tierStore.setGlobalMinCap(values.minCap || 0) - tierStore.setTierProperty(values.whitelistEnabled, "whitelistEnabled", 0) - - values.tiers.forEach((tier, index) => { - tierStore.setTierProperty(tier.tier, 'tier', index) - tierStore.setTierProperty(tier.updatable, 'updatable', index) - tierStore.setTierProperty(tier.startTime, 'startTime', index) - tierStore.setTierProperty(tier.endTime, 'endTime', index) - tierStore.updateRate(tier.rate, VALID, index) - tierStore.setTierProperty(tier.supply, 'supply', index) - tierStore.validateTiers('supply', index) - }) - }} - /> - - ) - }} + component={StepThreeForm} + addCrowdsale={tierStore.addCrowdsale} + gasPricesInGwei={gasPriceStore.gasPricesInGwei} + decimals={tokenStore.decimals} + tierStore={tierStore} + generalStore={generalStore} />
) } - - // render() { - // const { tierStore } = this.props; - - // const globalSettingsBlock = ( - //
- //
- //

Global settings

- //
- //
- // - // {this.renderGasPriceInput()} - //
- //
- // - // this.updateWhitelistEnabled(e)} - // description="Enables whitelisting. If disabled, anyone can participate in the crowdsale." - // /> - //
- //
- // ) - - // return ( - //
- // - //
- //
- //
- //

Crowdsale setup

- //

The most important and exciting part of the crowdsale process. Here you can - // define parameters of your crowdsale campaign.

- //
- // {globalSettingsBlock} - //
- - //
- // { tierStore.tiers.map((tier, index) => ) } - //
- - //
- //
this.addCrowdsale()} className="button button_fill_secondary">Add Tier
- // this.beforeNavigate(e)} className="button button_fill" to="/4">Continue - //
- - // - //
- // ) - // } } diff --git a/src/stores/TierStore.js b/src/stores/TierStore.js index ddcc0404a..2bc1c332d 100644 --- a/src/stores/TierStore.js +++ b/src/stores/TierStore.js @@ -1,5 +1,5 @@ import { observable, action, computed } from 'mobx'; -import { VALIDATION_TYPES } from '../utils/constants' +import { defaultTier, defaultTierValidations, VALIDATION_TYPES } from '../utils/constants' import { validateTime, validateSupply, @@ -8,6 +8,7 @@ import { validateTier } from '../utils/utils' import autosave from './autosave' +import { defaultCompanyEndDate, defaultCompanyStartDate } from '../components/stepThree/utils' const { VALID, INVALID } = VALIDATION_TYPES class TierStore { @@ -219,6 +220,37 @@ class TierStore { return whitelist.every(address => address.stored) } + @action addCrowdsale = (walletAddress = '') => { + const num = this.tiers.length + const newTier = Object.assign({}, defaultTier) + const newTierValidations = Object.assign({}, defaultTierValidations) + + newTier.tier = `Tier ${num + 1}` + + if (num === 0) { + newTier.whitelistEnabled = "no" + newTier.walletAddress = walletAddress + } + + this.addTier(newTier, newTierValidations) + this.setTierDates(num) + } + + @action setTierDates = (num) => { + const defaultStartTime = 0 === num ? defaultCompanyStartDate() : this.tierEndTime(num - 1) + const defaultEndTime = 0 === num ? defaultCompanyEndDate(defaultStartTime) : defaultCompanyEndDate(this.tierEndTime(num - 1)) + + const startTime = this.tiers[num].startTime || defaultStartTime + const endTime = this.tiers[num].endTime || defaultEndTime + + this.setTierProperty(startTime, 'startTime', num) + this.setTierProperty(endTime, 'endTime', num) + } + + tierEndTime (index) { + return this.tiers[index].endTime + } + @computed get maxSupply () { return this.tiers.map(tier => +tier.supply).reduce((a, b) => Math.max(a, b), 0) } From c4689efc89f3ed75c09e662127e343c3665a4d6a Mon Sep 17 00:00:00 2001 From: fernandomg Date: Wed, 4 Apr 2018 15:13:22 -0300 Subject: [PATCH 39/48] Update validations tests --- src/utils/validations.js | 14 +++++++++----- src/utils/validations.spec.js | 5 ++--- 2 files changed, 11 insertions(+), 8 deletions(-) diff --git a/src/utils/validations.js b/src/utils/validations.js index 01bb086be..9e11afb01 100644 --- a/src/utils/validations.js +++ b/src/utils/validations.js @@ -105,11 +105,15 @@ export const isDateSameOrPreviousThan = (errorMsg = VALIDATION_MESSAGES.DATE_IS_ return isValid ? undefined : errorMsg } -export const composeValidators = (...validators) => (value) => validators.reduce((errors, validator) => { - const validation = validator(value) +export const composeValidators = (...validators) => (value) => { + const errors = validators.reduce((errors, validator) => { + const validation = validator(value) - if (validation) errors.push(validation) + if (validation) errors.push(validation) - return errors -}, []) + return errors + }, []) + + return errors.length ? errors : undefined +} diff --git a/src/utils/validations.spec.js b/src/utils/validations.spec.js index 2394beaca..5659ba0c8 100644 --- a/src/utils/validations.spec.js +++ b/src/utils/validations.spec.js @@ -532,13 +532,12 @@ describe('composeValidators', () => { expect(Array.isArray(listOfErrors)).toBeTruthy() }) - it('Should return an empty array if there is no error', () => { + it('Should return "undefined" if there is no error', () => { const listOfErrors = composeValidators( isRequired(), isNonNegative(), )(123) - expect(Array.isArray(listOfErrors)).toBeTruthy() - expect(listOfErrors.length).toBe(0) + expect(listOfErrors).toBeUndefined() }) }) From d6bd92f405d08c45c8c862fce136683dd2c2f102 Mon Sep 17 00:00:00 2001 From: fernandomg Date: Wed, 4 Apr 2018 15:21:01 -0300 Subject: [PATCH 40/48] Fix whitelist block display --- src/components/Common/TierBlock.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/Common/TierBlock.js b/src/components/Common/TierBlock.js index 2f44750fc..1af9690fb 100644 --- a/src/components/Common/TierBlock.js +++ b/src/components/Common/TierBlock.js @@ -181,7 +181,7 @@ export const TierBlock = ({ fields, ...props }) => {
{ - props.tierStore.tiers[index].whitelistEnabled === 'yes' ? ( + props.tierStore.tiers[0].whitelistEnabled === 'yes' ? (

Whitelist

From fbcfe7676ccd2c22ea0b2f6746ce121526921bfa Mon Sep 17 00:00:00 2001 From: fernandomg Date: Thu, 5 Apr 2018 09:46:47 -0300 Subject: [PATCH 41/48] Update token-wizard-test-automation submodule --- submodules/token-wizard-test-automation | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/submodules/token-wizard-test-automation b/submodules/token-wizard-test-automation index 925a43a39..e11440e01 160000 --- a/submodules/token-wizard-test-automation +++ b/submodules/token-wizard-test-automation @@ -1 +1 @@ -Subproject commit 925a43a3995a2ca23bf23f0cae5f5d532cf7621c +Subproject commit e11440e011a8752300b42d45d9ebd2d59f632706 From 7e5def70871b272c2fad0dd586b0fa5b3628240d Mon Sep 17 00:00:00 2001 From: Franco Victorio Date: Thu, 5 Apr 2018 11:44:46 -0300 Subject: [PATCH 42/48] Initialize walletAddress from web3Store --- src/components/stepThree/index.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/stepThree/index.js b/src/components/stepThree/index.js index 525356c46..6eafcb10a 100644 --- a/src/components/stepThree/index.js +++ b/src/components/stepThree/index.js @@ -93,7 +93,7 @@ export class stepThree extends React.Component { } render () { - const { generalStore, tierStore, gasPriceStore, tokenStore } = this.props + const { generalStore, tierStore, gasPriceStore, tokenStore, web3Store } = this.props return (
@@ -102,7 +102,7 @@ export class stepThree extends React.Component { onSubmit={this.handleOnSubmit} mutators={{ ...arrayMutators }} initialValues={{ - walletAddress: tierStore.tiers[0].walletAddress, + walletAddress: web3Store.curAddress, minCap: 0, gasPrice: gasPriceStore.gasPricesInGwei[0], whitelistEnabled: "no", From a3dcd379a538d29f4204e89c71c76958afb8b86e Mon Sep 17 00:00:00 2001 From: Franco Victorio Date: Thu, 5 Apr 2018 14:35:23 -0300 Subject: [PATCH 43/48] Update index.js --- src/stores/index.js | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/src/stores/index.js b/src/stores/index.js index 78241edfd..cdf177208 100644 --- a/src/stores/index.js +++ b/src/stores/index.js @@ -47,19 +47,3 @@ export { gasPriceStore, deploymentStore }; - -window.stores = { // DONTCOMMIT - generalStore, - crowdsalePageStore, - contractStore, - pricingStrategyStore, - reservedTokenStore, - stepTwoValidationStore, - tierStore, - tokenStore, - web3Store, - investStore, - crowdsaleStore, - gasPriceStore, - deploymentStore -}; From f86453de9c6aa6de0bd6d2a5ab7d2e08087ab909 Mon Sep 17 00:00:00 2001 From: fernandomg Date: Thu, 5 Apr 2018 16:24:55 -0300 Subject: [PATCH 44/48] Fix hardcoded name attr --- src/components/stepThree/GasPriceInput.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/stepThree/GasPriceInput.js b/src/components/stepThree/GasPriceInput.js index 9ea4415c0..5b1c560d8 100644 --- a/src/components/stepThree/GasPriceInput.js +++ b/src/components/stepThree/GasPriceInput.js @@ -81,7 +81,7 @@ class GasPriceInput extends Component { /> ) : null}

Slow is cheap, fast is expensive

- +
) } From d5613826b070754e584be0b6bfe7df710385a2e2 Mon Sep 17 00:00:00 2001 From: fernandomg Date: Thu, 5 Apr 2018 17:04:34 -0300 Subject: [PATCH 45/48] Add className to errors --- src/components/Common/Error.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/Common/Error.js b/src/components/Common/Error.js index bf68b6b77..b1e25b00b 100644 --- a/src/components/Common/Error.js +++ b/src/components/Common/Error.js @@ -21,7 +21,7 @@ export const Error = ({ name, errorStyle }) => ( { errors.length ? errors.map((error, index) => ( -

{(!pristine || touched) && error}

+

{(!pristine || touched) && error}

)) : null } From 704ee293df8389444ce79eafe85dbae20058f5b8 Mon Sep 17 00:00:00 2001 From: fernandomg Date: Fri, 6 Apr 2018 10:18:20 -0300 Subject: [PATCH 46/48] Update token-wizard-test-automation submodule --- submodules/token-wizard-test-automation | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/submodules/token-wizard-test-automation b/submodules/token-wizard-test-automation index e11440e01..ba1c0dbce 160000 --- a/submodules/token-wizard-test-automation +++ b/submodules/token-wizard-test-automation @@ -1 +1 @@ -Subproject commit e11440e011a8752300b42d45d9ebd2d59f632706 +Subproject commit ba1c0dbcef6adc443fec955c5c6335b69cbe1385 From 42cdd02afc00c4ead2f6684d406d24b736008315 Mon Sep 17 00:00:00 2001 From: fernandomg Date: Fri, 6 Apr 2018 10:36:50 -0300 Subject: [PATCH 47/48] Update StepTwoForm snapshot --- src/components/stepTwo/__snapshots__/StepTwoForm.spec.js.snap | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/components/stepTwo/__snapshots__/StepTwoForm.spec.js.snap b/src/components/stepTwo/__snapshots__/StepTwoForm.spec.js.snap index c82cdb839..7fd91e34e 100644 --- a/src/components/stepTwo/__snapshots__/StepTwoForm.spec.js.snap +++ b/src/components/stepTwo/__snapshots__/StepTwoForm.spec.js.snap @@ -36,6 +36,7 @@ exports[`StepTwoForm Should render the component 1`] = `

Date: Fri, 6 Apr 2018 14:46:22 -0300 Subject: [PATCH 48/48] Update e2e tests submodule (token-wizard-test-automation) --- submodules/token-wizard-test-automation | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/submodules/token-wizard-test-automation b/submodules/token-wizard-test-automation index ba1c0dbce..a6f9a208c 160000 --- a/submodules/token-wizard-test-automation +++ b/submodules/token-wizard-test-automation @@ -1 +1 @@ -Subproject commit ba1c0dbcef6adc443fec955c5c6335b69cbe1385 +Subproject commit a6f9a208c748971d7d42983ca9c0c8232fe06a96