(
+
+)
+
+Joyride.propTypes = {
+ // Callbacks
+ getRef: PropTypes.func.isRequired,
+ callback: PropTypes.func.isRequired
+}
+
+export default Joyride
diff --git a/src/containers/iico/components/joyride/steps.js b/src/containers/iico/components/joyride/steps.js
new file mode 100644
index 0000000..4987cc8
--- /dev/null
+++ b/src/containers/iico/components/joyride/steps.js
@@ -0,0 +1,174 @@
+import React from 'react'
+
+export default [
+ {
+ title: 'Welcome to the Interactive Coin Offering.',
+ text: (
+
+ Get started with the tutorial or skip it if you already know what you
+ are doing.
+
+
+ First, let’s go over the sale’s main data points and explain what they
+ are. A full explanation can be found{' '}
+
+ here
+ .
+
+ ),
+ selector: '#joyrideWelcome'
+ },
+ {
+ text:
+ 'This is the ICO’s contract address. You can hover over it to see it in full and you can click on it to explore the contract on etherscan. Try doing that now.',
+ selector: '#joyrideTokenContractAddress'
+ },
+ {
+ text:
+ 'This is the amount of tokens that are up for sale. They will be distributed proportionally across all bidders relative to the size of their contributions and bonuses.',
+ selector: '#joyrideTokensForSale'
+ },
+ {
+ text:
+ 'This is the sale’s current valuation. That is, if the sale were to end now and personal caps were taken into account to see which ones should be refunded. The number is truncated to two significant digits, but you can hover over it to see it in full.',
+ selector: '#joyrideValuation'
+ },
+ {
+ text: (
+
+ This is the sale’s current phase. There are four phases and the actions
+ you can take are different in each one.
+
+
+ Not Started: The sale has not started yet at this point, so there
+ is nothing you can do.
+
+
+ Full Bonus: You may place bids that take advantage of the whole
+ bonus and withdrawing a bid will not leave any ETH locked in.
+
+
+ Partial Withdrawals: You may still place bids, but the bonus has
+ started decreasing linearly, and the amount of ETH locked in when
+ withdrawing has started increasing linearly.
+
+
+ Withdrawal Lockup: You may still place bids, but manual
+ withdrawals are no longer permitted. The only way for a bid to be
+ refunded is if the valuation exceeds its personal cap.
+
+
+ Finished: You may now redeem your tokens and/or ETH.
+
+ ),
+ selector: '#joyridePhase'
+ },
+ {
+ text:
+ 'This is the sale’s starting bonus. This will start decreasing linearly at the end of the full bonus phase down to zero at the end of the sale. A bid’s bonus increases its token purchasing power. For example, a 20% bonus will give you 20% more tokens for the same amount of ETH.',
+ selector: '#joyrideStartingBonus'
+ },
+ {
+ text: 'This is the sale’s current bonus.',
+ selector: '#joyrideCurrentBonus'
+ },
+ {
+ text:
+ 'This slider lets you preview the bonus throughout the lifetime of the sale. The green bars represent a change of phase and the light blue bar represents the current time. Hover over it now to preview the bonus at any stage of the sale. Let’s skip time to the start of the sale.',
+ selector: '#joyrideSlider'
+ },
+ {
+ text: (
+
+ This is where you can make bids. The personal cap lets you set a max cap
+ at which you are willing to participate in the sale with. If the
+ valuation ends up exceeding this value, your bid will be automatically
+ refunded.
+
+
+ If you want to participate regardless of valuation, just check the “No
+ Personal Cap” checkbox.
+
+
+ Try placing a bid now and take advantage of the “Full Bonus” phase.
+
+
+ Place a bid to continue.
+
+ ),
+ selector: '#joyridePlaceBid',
+ style: { button: { display: 'none' } }
+ },
+ {
+ text:
+ 'Congratulations on placing your first bid! Withdrawing now would give you all of your ETH back, but let’s skip through time to demonstrate the lock in process.',
+ selector: '#joyridePlacedBid'
+ },
+ {
+ text: (
+
+ We are now in the “Partial Withdrawals” phase.
+
+
+ Try withdrawing your bid, you’ll only be able to withdraw whatever
+ percentage of the “Partial Withdrawals” phase is left and your bonus
+ will be reduced by 1/3.
+
+
+ That is, if you are 80% through the phase, you’ll only be able to
+ withdraw 20% of your bid and if your bonus was 15% it will be reduced to
+ 10%. This is to avoid blackout attacks by large players.
+
+
+ See this article to learn more.
+
+ ),
+ selector: '#joyrideWithdraw',
+ style: { button: { display: 'none' } }
+ },
+ {
+ text: "Nice, you've been refunded. Now, let’s skip to the end of the sale.",
+ selector: '#joyrideWithdrew'
+ },
+ {
+ text: (
+
+ Now, the contract needs to iterate over all the bids to finalize the
+ sale. This may be done by anyone so you might not even get to see this
+ part of the sale.
+
+
+ Enter the number of iterations you’d like to pay gas for and submit to
+ finalize the sale.
+
+
+ Five iterations is more than enough since you only made one bid.
+
+ ),
+ selector: '#joyridePlaceBid',
+ style: { button: { display: 'none' } }
+ },
+ {
+ text: (
+
+ Good job! Now you can redeem your tokens (for bids that stayed in the
+ sale) and/or ETH (for bids where the personal cap ended up being under
+ the valuation).
+
+
+ Redeem your bid to continue.
+
+ ),
+ selector: '#joyrideWithdraw',
+ style: { button: { display: 'none' } }
+ },
+ {
+ text:
+ 'Great, you are now more than ready to take part in a real Interactive Coin Offering. Please let us know if you have any questions or concerns, feedback is greatly appreciated.',
+ selector: '#joyrideFinish'
+ }
+]
diff --git a/src/containers/iico/index.js b/src/containers/iico/index.js
index cc6b5e0..f2d1378 100644
--- a/src/containers/iico/index.js
+++ b/src/containers/iico/index.js
@@ -9,6 +9,7 @@ import * as IICOActions from '../../actions/iico'
import Data from './components/data'
import Bids from './components/bids'
+import Joyride from './components/joyride'
import './iico.css'
@@ -30,6 +31,17 @@ class IICO extends PureComponent {
fetchIICOBids: PropTypes.func.isRequired
}
+ state = {
+ hasSeenTutorial: false,
+ inTutorial: false,
+ tutorialNow: null,
+ tutorialIICOData: null,
+ tutorialIICOBids: null
+ }
+
+ pollIICODataInterval = null
+ joyrideRef = null
+
componentDidMount() {
const {
match: { params: { address } },
@@ -43,15 +55,196 @@ class IICO extends PureComponent {
this.pollIICODataInterval = setInterval(() => pollIICOData(address), 5000)
}
+ componentDidUpdate() {
+ const { IICOData, IICOBids } = this.props
+ const { hasSeenTutorial } = this.state
+
+ if (IICOData.data && IICOBids.data && !hasSeenTutorial) {
+ const tutorialIICOData = JSON.parse(JSON.stringify(IICOData))
+ const tutorialIICOBids = JSON.parse(JSON.stringify(IICOBids))
+ const startTime = IICOData.data.startTime.getTime()
+ this.setState(
+ {
+ hasSeenTutorial: true,
+ inTutorial: true,
+ tutorialNow: startTime - 1000,
+ tutorialIICOData: {
+ ...tutorialIICOData,
+ data: {
+ ...tutorialIICOData.data,
+ startTime: new Date(startTime),
+ endFullBonusTime: new Date(
+ IICOData.data.endFullBonusTime.getTime()
+ ),
+ withdrawalLockTime: new Date(
+ IICOData.data.withdrawalLockTime.getTime()
+ ),
+ endTime: new Date(IICOData.data.endTime.getTime()),
+ bonus: IICOData.data.startingBonus
+ }
+ },
+ tutorialIICOBids
+ },
+ () => this.joyrideRef.reset(true)
+ )
+ }
+ }
+
componentWillUnmount() {
clearInterval(this.pollIICODataInterval)
}
+ getJoyrideRef = ref => (this.joyrideRef = ref)
+
+ joyrideCallback = ({ type, step }) => {
+ const { tutorialIICOData } = this.state
+
+ switch (type) {
+ case 'step:after':
+ switch (step.selector.slice('#joyride'.length)) {
+ case 'Slider':
+ this.setState({
+ tutorialNow: tutorialIICOData.data.startTime.getTime()
+ })
+ break
+ case 'PlacedBid': {
+ const endFullBonusTime = tutorialIICOData.data.endFullBonusTime.getTime()
+ const endTime = tutorialIICOData.data.endTime.getTime()
+ const tutorialNow =
+ endFullBonusTime +
+ (tutorialIICOData.data.withdrawalLockTime.getTime() -
+ endFullBonusTime) /
+ 2
+ this.setState({
+ tutorialNow,
+ bonus:
+ tutorialIICOData.data.bonus *
+ ((endTime - tutorialNow) / (endTime - endFullBonusTime))
+ })
+ break
+ }
+ case 'Withdrew':
+ this.setState({
+ tutorialNow: tutorialIICOData.data.endTime.getTime(),
+ bonus: 0
+ })
+ break
+ default:
+ break
+ }
+ break
+ case 'finished':
+ this.setState({
+ inTutorial: false,
+ tutorialNow: null,
+ tutorialIICOData: null,
+ tutorialIICOBids: null
+ })
+ break
+ default:
+ break
+ }
+ }
+
+ tutorialFinalizeIICOData = callback => {
+ const { tutorialIICOData } = this.state
+
+ this.setState(
+ {
+ tutorialIICOData: {
+ ...tutorialIICOData,
+ data: {
+ ...tutorialIICOData.data,
+ finalized: true
+ }
+ }
+ },
+ callback
+ )
+ }
+
+ tutorialEditIICOBids = (IICOBidOrID, callback, lockedIn, newBonus) => {
+ const { tutorialIICOData, tutorialIICOBids } = this.state
+
+ let newBids
+ if (typeof IICOBidOrID === 'number')
+ newBids = tutorialIICOBids.data.map(
+ b =>
+ b.ID === IICOBidOrID
+ ? lockedIn
+ ? { ...b, contrib: lockedIn, bonus: newBonus, withdrawn: true }
+ : { ...b, redeemed: true }
+ : b
+ )
+ else newBids = [...tutorialIICOBids.data, IICOBidOrID]
+ console.log(newBids)
+ // Calculate new tutorial IICO Data
+ const bids = [...newBids].sort((a, b) => {
+ if (b.maxVal === a.maxVal) return b.ID - a.ID
+ return b.maxVal - a.maxVal
+ })
+ let cutOffBidContrib
+ let cutOffBid = bids[bids.length - 1]
+ let valuation = 0
+ let virtualValuation = 0
+ for (const bid of bids) {
+ if (bid.contrib + valuation < bid.maxVal) {
+ // We haven't found the cut-off yet.
+ cutOffBidContrib = bid.contrib
+ cutOffBid = bid
+ valuation += bid.contrib
+ virtualValuation += bid.contrib + bid.contrib * (1 + bid.bonus)
+ } else {
+ // We found the cut-off bid. This bid will be taken partially.
+ cutOffBidContrib = bid.maxVal >= valuation ? bid.maxVal - valuation : 0 // The amount of the contribution of the cut-off bid that can stay in the sale without spilling over the maxVal.
+ cutOffBid = bid
+ valuation += cutOffBidContrib
+ virtualValuation +=
+ cutOffBidContrib + cutOffBidContrib * (1 + bid.bonus)
+ break
+ }
+ }
+
+ // Set new tutorial state
+ this.setState(
+ {
+ tutorialIICOData: {
+ ...tutorialIICOData,
+ data: {
+ ...tutorialIICOData.data,
+ valuation,
+ virtualValuation,
+ cutOffBidID: cutOffBid.ID,
+ cutOffBidMaxVal: cutOffBid.maxVal,
+ cutOffBidContrib
+ }
+ },
+ tutorialIICOBids: {
+ ...tutorialIICOBids,
+ data: newBids
+ }
+ },
+ callback
+ )
+ }
+
+ tutorialNext = () => this.joyrideRef.next()
+
render() {
- const { match: { params: { address } }, IICOData, IICOBids } = this.props
+ const { match } = this.props
+ const {
+ inTutorial,
+ tutorialNow,
+ tutorialIICOData,
+ tutorialIICOBids
+ } = this.state
+ const { match: { params: { address } }, IICOData, IICOBids } = inTutorial
+ ? { match, IICOData: tutorialIICOData, IICOBids: tutorialIICOBids }
+ : this.props
return (
+
-
-
+
+
)
}
@@ -91,8 +290,6 @@ class IICO extends PureComponent {
}
failedLoading="The address or the contract it holds is invalid. Try another one."
/>
- {/* TODO: Render './components/submit-bid-form' and disable submit button if already participated */}
- {/* TODO: Render withdraw button if already participated and in first period */}