diff --git a/CHANGELOG.md b/CHANGELOG.md index 1273376a1..4d265b9d6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,12 +8,14 @@ Web UI version numbers should always match the corresponding version of LBRY App ## [Unreleased] ### Added - * + * ShapeShift integration * ### Changed * The credit balance displayed in the main app navigation displays two decimal places instead of one. * Moved all redux code into /redux folder + * Channel names in pages are highlighted to indicate them being clickable(#814) + * ### Fixed * Long channel names causing inconsistent thumbnail sizes (#721) diff --git a/package.json b/package.json index 04a2678e2..61e9e87b9 100644 --- a/package.json +++ b/package.json @@ -57,7 +57,12 @@ "semver": "^5.3.0", "source-map-support": "^0.5.0", "tree-kill": "^1.1.0", - "y18n": "^3.2.1" + "y18n": "^3.2.1", + "amplitude-js": "^4.0.0", + "classnames": "^2.2.5", + "formik": "^0.10.4", + "qrcode.react": "^0.7.2", + "shapeshift.io": "^1.3.1" }, "devDependencies": { "babel-plugin-module-resolver": "^3.0.0", @@ -80,7 +85,8 @@ "prettier": "^1.4.2", "sass-loader": "^6.0.6", "webpack": "^3.10.0", - "webpack-build-notifier": "^0.1.18" + "webpack-build-notifier": "^0.1.18", + "bluebird": "^3.5.1" }, "resolutions": { "webpack/webpack-sources": "1.0.1" diff --git a/src/renderer/.flowconfig b/src/renderer/.flowconfig index 18e154135..21d1a300a 100644 --- a/src/renderer/.flowconfig +++ b/src/renderer/.flowconfig @@ -12,6 +12,9 @@ flow-typed suppress_comment=\\(.\\|\n\\)*\\$FlowFixMe suppress_comment=\\(.\\|\n\\)*\\$FlowIssue module.name_mapper='^constants\(.*\)$' -> '/js/constants\1' +module.name_mapper='^util\(.*\)$' -> '/js/util\1' module.name_mapper='^redux\(.*\)$' -> '/js/redux\1' +module.name_mapper='^types\(.*\)$' -> '/js/types\1' +module.name_mapper='^component\(.*\)$' -> '/js/component\1' [strict] diff --git a/src/renderer/component/address/index.js b/src/renderer/component/address/index.js new file mode 100644 index 000000000..00bec962e --- /dev/null +++ b/src/renderer/component/address/index.js @@ -0,0 +1,7 @@ +import { connect } from "react-redux"; +import { doShowSnackBar } from "redux/actions/app"; +import Address from "./view"; + +export default connect(null, { + doShowSnackBar, +})(Address); diff --git a/src/renderer/component/address/view.jsx b/src/renderer/component/address/view.jsx new file mode 100644 index 000000000..96049eb35 --- /dev/null +++ b/src/renderer/component/address/view.jsx @@ -0,0 +1,52 @@ +import React from "react"; +import PropTypes from "prop-types"; +import { clipboard } from "electron"; +import Link from "component/link"; +import classnames from "classnames"; + +export default class Address extends React.PureComponent { + static propTypes = { + address: PropTypes.string, + }; + + constructor(props) { + super(props); + + this._inputElem = null; + } + + render() { + const { address, showCopyButton, doShowSnackBar } = this.props; + + return ( +
+ { + this._inputElem = input; + }} + onFocus={() => { + this._inputElem.select(); + }} + readOnly="readonly" + value={address || ""} + /> + {showCopyButton && ( + + { + clipboard.writeText(address); + doShowSnackBar({ message: __("Address copied") }); + }} + /> + + )} +
+ ); + } +} diff --git a/src/renderer/component/common.js b/src/renderer/component/common.js index c5352637b..2f8ae5489 100644 --- a/src/renderer/component/common.js +++ b/src/renderer/component/common.js @@ -137,42 +137,6 @@ export class CreditAmount extends React.PureComponent { } } -let addressStyle = { - fontFamily: - '"Consolas", "Lucida Console", "Adobe Source Code Pro", monospace', -}; -export class Address extends React.PureComponent { - static propTypes = { - address: PropTypes.string, - }; - - constructor(props) { - super(props); - - this._inputElem = null; - } - - render() { - return ( -
- { - this._inputElem = input; - }} - onFocus={() => { - this._inputElem.select(); - }} - style={addressStyle} - readOnly="readonly" - value={this.props.address || ""} - /> -
- ); - } -} - export class Thumbnail extends React.PureComponent { static propTypes = { src: PropTypes.string, diff --git a/src/renderer/component/common/spinner.jsx b/src/renderer/component/common/spinner.jsx new file mode 100644 index 000000000..8863459c8 --- /dev/null +++ b/src/renderer/component/common/spinner.jsx @@ -0,0 +1,16 @@ +import React from "react"; +import classnames from "classnames"; + +export default ({ dark, className }) => { + return ( +
+ ); +}; diff --git a/src/renderer/component/link/view.jsx b/src/renderer/component/link/view.jsx index 9b949bf75..c9355f84d 100644 --- a/src/renderer/component/link/view.jsx +++ b/src/renderer/component/link/view.jsx @@ -14,11 +14,12 @@ const Link = props => { navigate, navigateParams, doNavigate, + className, } = props; - const className = - (props.className || "") + - (!props.className && !button ? "button-text" : "") + // Non-button links get the same look as text buttons + const combinedClassName = + (className || "") + + (!className && !button ? "button-text" : "") + // Non-button links get the same look as text buttons (button ? " button-block button-" + button + " button-set-item" : "") + (disabled ? " disabled" : ""); @@ -43,7 +44,7 @@ const Link = props => { return ( ({ + receiveAddress: selectReceiveAddress(state), + shapeShift: selectShapeShift(state), +}); + +export default connect(select, { + shapeShiftInit, + getCoinStats, + createShapeShift, + clearShapeShift, + getActiveShift, + doShowSnackBar, +})(ShapeShift); diff --git a/src/renderer/component/shapeShift/internal/active-shift.jsx b/src/renderer/component/shapeShift/internal/active-shift.jsx new file mode 100644 index 000000000..40e5cad40 --- /dev/null +++ b/src/renderer/component/shapeShift/internal/active-shift.jsx @@ -0,0 +1,161 @@ +// @flow +import * as React from "react"; +import QRCode from "qrcode.react"; +import * as statuses from "constants/shape_shift"; +import Address from "component/address"; +import Link from "component/link"; +import type { Dispatch } from "redux/actions/shape_shift"; +import ShiftMarketInfo from "./market_info"; + +type Props = { + shiftState: ?string, + shiftCoinType: ?string, + shiftDepositAddress: ?string, + shiftReturnAddress: ?string, + shiftOrderId: ?string, + originCoinDepositMax: ?number, + clearShapeShift: Dispatch, + doShowSnackBar: Dispatch, + getActiveShift: Dispatch, + shapeShiftRate: ?number, + originCoinDepositMax: ?number, + originCoinDepositFee: ?number, + originCoinDepositMin: ?string, +}; + +class ActiveShapeShift extends React.PureComponent { + continousFetch: ?number; + + constructor() { + super(); + this.continousFetch = undefined; + } + + componentDidMount() { + const { getActiveShift, shiftDepositAddress } = this.props; + + getActiveShift(shiftDepositAddress); + this.continousFetch = setInterval(() => { + getActiveShift(shiftDepositAddress); + }, 10000); + } + + componentWillUpdate(nextProps: Props) { + const { shiftState } = nextProps; + if (shiftState === statuses.COMPLETE) { + this.clearContinuousFetch(); + } + } + + componentWillUnmount() { + this.clearContinuousFetch(); + } + + clearContinuousFetch() { + if (this.continousFetch) { + clearInterval(this.continousFetch); + this.continousFetch = null; + } + } + + render() { + const { + shiftCoinType, + shiftDepositAddress, + shiftReturnAddress, + shiftOrderId, + shiftState, + originCoinDepositMax, + clearShapeShift, + doShowSnackBar, + shapeShiftRate, + originCoinDepositFee, + originCoinDepositMin, + } = this.props; + + return ( +
+ {shiftState === statuses.NO_DEPOSITS && ( +
+

+ Send up to{" "} + + {originCoinDepositMax} {shiftCoinType} + {" "} + to the address below. +

+ + +
+
+
+ +
+
+
+ )} + + {shiftState === statuses.RECEIVED && ( +
+

+ {__( + "ShapeShift has received your payment! Sending the funds to your LBRY wallet." + )} +

+ + {__("This can take a while, especially with BTC.")} + +
+ )} + + {shiftState === statuses.COMPLETE && ( +
+

+ {__( + "Transaction complete! You should see the new LBC in your wallet." + )} +

+
+ )} +
+ + {shiftOrderId && ( + + + + )} + {shiftState === statuses.NO_DEPOSITS && + shiftReturnAddress && ( +
+ + If the transaction doesn't go through, ShapeShift will return + your {shiftCoinType} back to {shiftReturnAddress} + +
+ )} +
+
+ ); + } +} + +export default ActiveShapeShift; diff --git a/src/renderer/component/shapeShift/internal/form.jsx b/src/renderer/component/shapeShift/internal/form.jsx new file mode 100644 index 000000000..8a59c3a4c --- /dev/null +++ b/src/renderer/component/shapeShift/internal/form.jsx @@ -0,0 +1,109 @@ +import React from "react"; +import Link from "component/link"; +import { getExampleAddress } from "util/shape_shift"; +import { Submit, FormRow } from "component/form"; +import type { ShapeShiftFormValues, Dispatch } from "redux/actions/shape_shift"; +import ShiftMarketInfo from "./market_info"; + +type ShapeShiftFormErrors = { + returnAddress?: string, +}; + +type Props = { + values: ShapeShiftFormValues, + errors: ShapeShiftFormErrors, + touched: boolean, + handleChange: Event => any, + handleBlur: Event => any, + handleSubmit: Event => any, + isSubmitting: boolean, + shiftSupportedCoins: Array, + originCoin: string, + updating: boolean, + getCoinStats: Dispatch, + receiveAddress: string, + originCoinDepositFee: number, + originCoinDepositMin: string, + originCoinDepositMax: number, + shapeShiftRate: number, +}; + +export default (props: Props) => { + const { + values, + errors, + touched, + handleChange, + handleBlur, + handleSubmit, + isSubmitting, + shiftSupportedCoins, + originCoin, + updating, + getCoinStats, + receiveAddress, + originCoinDepositMax, + originCoinDepositMin, + originCoinDepositFee, + shapeShiftRate, + } = props; + return ( +
+
+ {__("Exchange")} + + {__("for LBC")} +
+ {!updating && + originCoinDepositMax && ( + + )} +
+
+ + + + + ({__("optional but recommended")}) {__("We will return your")}{" "} + {originCoin}{" "} + {__("to this address if the transaction doesn't go through.")} + + +
+ +
+ + ); +}; diff --git a/src/renderer/component/shapeShift/internal/market_info.jsx b/src/renderer/component/shapeShift/internal/market_info.jsx new file mode 100644 index 000000000..2d7395b81 --- /dev/null +++ b/src/renderer/component/shapeShift/internal/market_info.jsx @@ -0,0 +1,34 @@ +// @flow +import React from "react"; + +type Props = { + shapeShiftRate: ?number, + originCoin: ?string, + originCoinDepositFee: ?number, + originCoinDepositMax: ?number, + originCoinDepositMin: ?string, +}; + +export default (props: Props) => { + const { + shapeShiftRate, + originCoin, + originCoinDepositFee, + originCoinDepositMax, + originCoinDepositMin, + } = props; + + return ( +
+ + {__("Receive")} {shapeShiftRate} LBC + {" / "} + {"1"} {originCoin} {__("less")} {originCoinDepositFee} LBC {__("fee")}. +
+ {__("Exchange max")}: {originCoinDepositMax} {originCoin} +
+ {__("Exchange min")}: {originCoinDepositMin} {originCoin} +
+
+ ); +}; diff --git a/src/renderer/component/shapeShift/view.jsx b/src/renderer/component/shapeShift/view.jsx new file mode 100644 index 000000000..a90a203cb --- /dev/null +++ b/src/renderer/component/shapeShift/view.jsx @@ -0,0 +1,150 @@ +// @flow +import * as React from "react"; +import { shell } from "electron"; +import { Formik } from "formik"; +import classnames from "classnames"; +import * as statuses from "constants/shape_shift"; +import { validateShapeShiftForm } from "util/shape_shift"; +import Link from "component/link"; +import Spinner from "component/common/spinner"; +import { BusyMessage } from "component/common"; +import ShapeShiftForm from "./internal/form"; +import ActiveShapeShift from "./internal/active-shift"; + +import type { ShapeShiftState } from "redux/reducers/shape_shift"; +import type { Dispatch, ShapeShiftFormValues } from "redux/actions/shape_shift"; + +type Props = { + shapeShift: ShapeShiftState, + getCoinStats: Dispatch, + createShapeShift: Dispatch, + clearShapeShift: Dispatch, + getActiveShift: Dispatch, + doShowSnackBar: Dispatch, + shapeShiftInit: Dispatch, + receiveAddress: string, +}; + +class ShapeShift extends React.PureComponent { + componentDidMount() { + const { + shapeShiftInit, + shapeShift: { hasActiveShift, shiftSupportedCoins }, + } = this.props; + + if (!hasActiveShift && !shiftSupportedCoins.length) { + // calls shapeshift to see list of supported coins for shifting + shapeShiftInit(); + } + } + + render() { + const { + getCoinStats, + receiveAddress, + createShapeShift, + shapeShift, + clearShapeShift, + getActiveShift, + doShowSnackBar, + } = this.props; + + const { + loading, + updating, + error, + shiftSupportedCoins, + hasActiveShift, + originCoin, + // ShapeShift response values + originCoinDepositMax, + originCoinDepositMin, + originCoinDepositFee, + shiftDepositAddress, + shiftReturnAddress, + shiftCoinType, + shiftOrderId, + shiftState, + shapeShiftRate, + } = shapeShift; + + const initialFormValues: ShapeShiftFormValues = { + receiveAddress, + originCoin: "BTC", + returnAddress: "", + }; + + return ( + // add the "shapeshift__intital-wrapper class so we can avoid content jumping once everything loads" + // it just gives the section a min-height equal to the height of the content when the form is rendered + // if the markup below changes for the initial render (form.jsx) there will be content jumping + // the styling in shapeshift.scss will need to be updated to the correct min-height +
+
+

{__("Convert Crypto to LBC")}

+

+ {__("Powered by ShapeShift. Read our FAQ")}{" "} + {__("here")}. + {hasActiveShift && + shiftState !== "complete" && ( + {__("This will update automatically.")} + )} +

+
+ +
+ {error &&
{error}
} + {loading && } + {!loading && + !hasActiveShift && + !!shiftSupportedCoins.length && ( + ( + + )} + /> + )} + {hasActiveShift && ( + + )} +
+
+ ); + } +} + +export default ShapeShift; diff --git a/src/renderer/component/uriIndicator/view.jsx b/src/renderer/component/uriIndicator/view.jsx index cff4bd073..3bf850d11 100644 --- a/src/renderer/component/uriIndicator/view.jsx +++ b/src/renderer/component/uriIndicator/view.jsx @@ -1,7 +1,8 @@ import React from "react"; import { Icon } from "component/common"; import Link from "component/link"; -import lbryuri from "lbryuri.js"; +import lbryuri from "lbryuri"; +import classnames from "classnames"; class UriIndicator extends React.PureComponent { componentWillMount() { @@ -60,7 +61,12 @@ class UriIndicator extends React.PureComponent { const inner = ( - + {channelName} {" "} {!signatureIsValid ? ( diff --git a/src/renderer/component/video/internal/loading-screen.jsx b/src/renderer/component/video/internal/loading-screen.jsx index bc388833c..94cc7ce4d 100644 --- a/src/renderer/component/video/internal/loading-screen.jsx +++ b/src/renderer/component/video/internal/loading-screen.jsx @@ -1,9 +1,10 @@ import React from "react"; +import Spinner from "component/common/spinner"; const LoadingScreen = ({ status, spinner = true }) => (
- {spinner &&
} + {spinner && }
{status}
diff --git a/src/renderer/component/walletAddress/view.jsx b/src/renderer/component/walletAddress/view.jsx index bdf6f81fe..63afa2b31 100644 --- a/src/renderer/component/walletAddress/view.jsx +++ b/src/renderer/component/walletAddress/view.jsx @@ -1,6 +1,6 @@ import React from "react"; import Link from "component/link"; -import { Address } from "component/common"; +import Address from "component/address"; class WalletAddress extends React.PureComponent { componentWillMount() { diff --git a/src/renderer/constants/action_types.js b/src/renderer/constants/action_types.js index b2f88f31f..3670912fd 100644 --- a/src/renderer/constants/action_types.js +++ b/src/renderer/constants/action_types.js @@ -144,3 +144,18 @@ export const FETCH_REWARD_CONTENT_COMPLETED = "FETCH_REWARD_CONTENT_COMPLETED"; //Language export const DOWNLOAD_LANGUAGE_SUCCEEDED = "DOWNLOAD_LANGUAGE_SUCCEEDED"; export const DOWNLOAD_LANGUAGE_FAILED = "DOWNLOAD_LANGUAGE_FAILED"; + +// ShapeShift +export const GET_SUPPORTED_COINS_START = "GET_SUPPORTED_COINS_START"; +export const GET_SUPPORTED_COINS_SUCCESS = "GET_SUPPORTED_COINS_SUCCESS"; +export const GET_SUPPORTED_COINS_FAIL = "GET_SUPPORTED_COINS_FAIL"; +export const GET_COIN_STATS_START = "GET_COIN_STATS_START"; +export const GET_COIN_STATS_SUCCESS = "GET_COIN_STATS_SUCCESS"; +export const GET_COIN_STATS_FAIL = "GET_COIN_STATS_FAIL"; +export const PREPARE_SHAPE_SHIFT_START = "PREPARE_SHAPE_SHIFT_START"; +export const PREPARE_SHAPE_SHIFT_SUCCESS = "PREPARE_SHAPE_SHIFT_SUCCESS"; +export const PREPARE_SHAPE_SHIFT_FAIL = "PREPARE_SHAPE_SHIFT_FAIL"; +export const GET_ACTIVE_SHIFT_START = "GET_ACTIVE_SHIFT_START"; +export const GET_ACTIVE_SHIFT_SUCCESS = "GET_ACTIVE_SHIFT_SUCCESS"; +export const GET_ACTIVE_SHIFT_FAIL = "GET_ACTIVE_SHIFT_FAIL"; +export const CLEAR_SHAPE_SHIFT = "CLEAR_SHAPE_SHIFT"; diff --git a/src/renderer/constants/shape_shift.js b/src/renderer/constants/shape_shift.js new file mode 100644 index 000000000..d898f2fc2 --- /dev/null +++ b/src/renderer/constants/shape_shift.js @@ -0,0 +1,3 @@ +export const NO_DEPOSITS = "no_deposits"; +export const RECEIVED = "received"; +export const COMPLETE = "complete"; diff --git a/src/renderer/flow-typed/bluebird.js b/src/renderer/flow-typed/bluebird.js new file mode 100644 index 000000000..b6ea52b19 --- /dev/null +++ b/src/renderer/flow-typed/bluebird.js @@ -0,0 +1,3 @@ +declare module 'bluebird' { + declare module.exports: any; +} diff --git a/src/renderer/flow-typed/classnames.js b/src/renderer/flow-typed/classnames.js new file mode 100644 index 000000000..8ed60a607 --- /dev/null +++ b/src/renderer/flow-typed/classnames.js @@ -0,0 +1,3 @@ +declare module 'classnames' { + declare module.exports: any; +} diff --git a/src/renderer/flow-typed/formik.js b/src/renderer/flow-typed/formik.js new file mode 100644 index 000000000..020efafd4 --- /dev/null +++ b/src/renderer/flow-typed/formik.js @@ -0,0 +1,3 @@ +declare module 'formik' { + declare module.exports: any; +} diff --git a/src/renderer/flow-typed/i18n.js b/src/renderer/flow-typed/i18n.js new file mode 100644 index 000000000..cbceb4b59 --- /dev/null +++ b/src/renderer/flow-typed/i18n.js @@ -0,0 +1 @@ +declare function __(a: string): string; diff --git a/src/renderer/flow-typed/qrcode.react.js b/src/renderer/flow-typed/qrcode.react.js new file mode 100644 index 000000000..43a589258 --- /dev/null +++ b/src/renderer/flow-typed/qrcode.react.js @@ -0,0 +1,3 @@ +declare module 'qrcode.react' { + declare module.exports: any; +} diff --git a/src/renderer/flow-typed/shapeshift.io.js b/src/renderer/flow-typed/shapeshift.io.js new file mode 100644 index 000000000..2d32593ee --- /dev/null +++ b/src/renderer/flow-typed/shapeshift.io.js @@ -0,0 +1,3 @@ +declare module 'shapeshift.io' { + declare module.exports: any; +} diff --git a/src/renderer/main.js b/src/renderer/main.js index 419482a4e..defd3c686 100644 --- a/src/renderer/main.js +++ b/src/renderer/main.js @@ -9,6 +9,8 @@ import { doDaemonReady } from "redux/actions/app"; import { doNavigate } from "redux/actions/navigation"; import { doDownloadLanguages } from "redux/actions/settings"; import * as types from "constants/action_types"; +import amplitude from "amplitude-js"; +import lbry from "lbry"; import "scss/all.scss"; const env = process.env.NODE_ENV || "production"; @@ -56,6 +58,23 @@ ipcRenderer.on("window-is-focused", (event, data) => { document.addEventListener("click", event => { var target = event.target; while (target && target !== document) { + if (target.matches("a") || target.matches("button")) { + // TODO: Look into using accessiblity labels (this would also make the app more accessible) + let hrefParts = window.location.href.split("#"); + let element = target.title || target.text.trim(); + if (element) { + amplitude.getInstance().logEvent("CLICK", { + target: element, + location: + hrefParts.length > 1 ? hrefParts[hrefParts.length - 1] : "/", + }); + } else { + amplitude.getInstance().logEvent("UNMARKED_CLICK", { + location: + hrefParts.length > 1 ? hrefParts[hrefParts.length - 1] : "/", + }); + } + } if ( target.matches('a[href^="http"]') || target.matches('a[href^="mailto"]') @@ -74,18 +93,28 @@ var init = function() { app.store.dispatch(doDownloadLanguages()); function onDaemonReady() { - window.sessionStorage.setItem("loaded", "y"); //once we've made it here once per session, we don't need to show splash again - app.store.dispatch(doDaemonReady()); + lbry.status().then(info => { + amplitude.getInstance().init( + // Amplitude API Key + "0b130efdcbdbf86ec2f7f9eff354033e", + info.lbry_id, + null, + function() { + window.sessionStorage.setItem("loaded", "y"); //once we've made it here once per session, we don't need to show splash again + app.store.dispatch(doDaemonReady()); - ReactDOM.render( - -
- - -
-
, - document.getElementById('app') - ); + ReactDOM.render( + +
+ + +
+
, + document.getElementById('app') + ); + } + ); + }); } if (window.sessionStorage.getItem("loaded") == "y") { diff --git a/src/renderer/page/receiveCredits/view.jsx b/src/renderer/page/receiveCredits/view.jsx index 120071748..5052b868e 100644 --- a/src/renderer/page/receiveCredits/view.jsx +++ b/src/renderer/page/receiveCredits/view.jsx @@ -2,15 +2,17 @@ import React from "react"; import SubHeader from "component/subHeader"; import Link from "component/link"; import WalletAddress from "component/walletAddress"; +import ShapeShift from "component/shapeShift"; const ReceiveCreditsPage = props => { return (
+
-

{__("Where To Find Credits")}

+

{__("More ways to get LBRY Credits")}

diff --git a/src/renderer/redux/actions/shape_shift.js b/src/renderer/redux/actions/shape_shift.js new file mode 100644 index 000000000..8f3e683f6 --- /dev/null +++ b/src/renderer/redux/actions/shape_shift.js @@ -0,0 +1,137 @@ +// @flow +import Promise from "bluebird"; +import * as types from "constants/action_types"; +import { coinRegexPatterns } from "util/shape_shift"; +import type { + GetSupportedCoinsSuccess, + GetCoinStatsStart, + GetCoinStatsSuccess, + GetCoinStatsFail, + PrepareShapeShiftSuccess, + PrepareShapeShiftFail, + GetActiveShiftSuccess, + GetActiveShiftFail, +} from "redux/reducers/shape_shift"; +import type { FormikActions } from "types/common"; + +// use promise chains instead of callbacks for shapeshift api +const shapeShift = Promise.promisifyAll(require("shapeshift.io")); + +// All ShapeShift actions +// Action types defined in the reducer will contain some payload +export type Action = + | { type: types.GET_SUPPORTED_COINS_START } + | { type: types.GET_SUPPORTED_COINS_FAIL } + | GetSupportedCoinsSuccess + | GetCoinStatsStart + | { type: types.GET_COIN_STATS_START } + | GetCoinStatsFail + | GetCoinStatsSuccess + | { type: types.PREPARE_SHAPE_SHIFT_START } + | PrepareShapeShiftFail + | PrepareShapeShiftSuccess + | { type: types.GET_ACTIVE_SHIFT_START } + | GetActiveShiftFail + | GetActiveShiftSuccess; + +// Basic thunk types +// It would be nice to import these from types/common +// Not sure how that would work since they rely on the Action type +type PromiseAction = Promise; +type ThunkAction = (dispatch: Dispatch) => any; +export type Dispatch = ( + action: Action | ThunkAction | PromiseAction | Array +) => any; + +// ShapeShift form values +export type ShapeShiftFormValues = { + originCoin: string, + returnAddress: ?string, + receiveAddress: string, +}; + +export const shapeShiftInit = () => (dispatch: Dispatch): ThunkAction => { + dispatch({ type: types.GET_SUPPORTED_COINS_START }); + + return shapeShift + .coinsAsync() + .then(coinData => { + let supportedCoins = []; + Object.keys(coinData).forEach(symbol => { + if (coinData[symbol].status === "available") { + supportedCoins.push(coinData[symbol]); + } + }); + + // only use larger coins with client side validation + supportedCoins = supportedCoins + .filter(coin => coinRegexPatterns[coin.symbol]) + .map(coin => coin.symbol); + + dispatch({ + type: types.GET_SUPPORTED_COINS_SUCCESS, + data: supportedCoins, + }); + dispatch(getCoinStats(supportedCoins[0])); + }) + .catch(err => + dispatch({ type: types.GET_SUPPORTED_COINS_FAIL, data: err }) + ); +}; + +export const getCoinStats = (coin: string) => ( + dispatch: Dispatch +): ThunkAction => { + const pair = `${coin.toLowerCase()}_lbc`; + + dispatch({ type: types.GET_COIN_STATS_START, data: coin }); + + return shapeShift + .marketInfoAsync(pair) + .then(marketInfo => + dispatch({ type: types.GET_COIN_STATS_SUCCESS, data: marketInfo }) + ) + .catch(err => dispatch({ type: types.GET_COIN_STATS_FAIL, data: err })); +}; + +export const createShapeShift = ( + values: ShapeShiftFormValues, + actions: FormikActions +) => (dispatch: Dispatch): ThunkAction => { + const { + originCoin, + returnAddress, + receiveAddress: withdrawalAddress, + } = values; + + const pair = `${originCoin.toLowerCase()}_lbc`; + const options = { + returnAddress: returnAddress, + }; + + dispatch({ type: types.PREPARE_SHAPE_SHIFT_START }); + return shapeShift + .shiftAsync(withdrawalAddress, pair, options) + .then(res => + dispatch({ type: types.PREPARE_SHAPE_SHIFT_SUCCESS, data: res }) + ) + .catch(err => { + dispatch({ type: types.PREPARE_SHAPE_SHIFT_FAIL, data: err }); + // for formik to stop the submit + actions.setSubmitting(false); + }); +}; + +export const getActiveShift = (depositAddress: string) => ( + dispatch: Dispatch +): ThunkAction => { + dispatch({ type: types.GET_ACTIVE_SHIFT_START }); + + return shapeShift + .statusAsync(depositAddress) + .then(res => dispatch({ type: types.GET_ACTIVE_SHIFT_SUCCESS, data: res })) + .catch(err => dispatch({ type: types.GET_ACTIVE_SHIFT_FAIL, data: err })); +}; + +export const clearShapeShift = () => (dispatch: Dispatch): Action => + dispatch({ type: types.CLEAR_SHAPE_SHIFT }); diff --git a/src/renderer/redux/reducers/navigation.js b/src/renderer/redux/reducers/navigation.js index 00453f9de..e32658b76 100644 --- a/src/renderer/redux/reducers/navigation.js +++ b/src/renderer/redux/reducers/navigation.js @@ -1,5 +1,6 @@ import * as types from "constants/action_types"; import { parseQueryParams } from "util/query_params"; +import amplitude from "amplitude-js"; const currentPath = () => { const hash = document.location.hash; @@ -69,6 +70,14 @@ reducers[types.WINDOW_SCROLLED] = (state, action) => { export default function reducer(state = defaultState, action) { const handler = reducers[action.type]; - if (handler) return handler(state, action); + if (handler) { + let nextState = handler(state, action); + if (nextState.currentPath !== state.currentPath) { + amplitude + .getInstance() + .logEvent("NAVIGATION", { destination: nextState.currentPath }); + } + return nextState; + } return state; } diff --git a/src/renderer/redux/reducers/shape_shift.js b/src/renderer/redux/reducers/shape_shift.js new file mode 100644 index 000000000..71b59addd --- /dev/null +++ b/src/renderer/redux/reducers/shape_shift.js @@ -0,0 +1,245 @@ +// @flow +import { handleActions } from "util/redux-utils"; +import * as actions from "constants/action_types"; +import * as statuses from "constants/shape_shift"; + +export type ShapeShiftState = { + loading: boolean, + updating: boolean, + shiftSupportedCoins: Array, + hasActiveShift: boolean, + originCoin: ?string, + error: ?string, + shiftDepositAddress: ?string, + shiftReturnAddress: ?string, + shiftCoinType: ?string, + shiftOrderId: ?string, + shiftState: ?string, + originCoinDepositMax: ?number, + // originCoinDepositMin is a string because we need to convert it from scientifc notation + // it will usually be something like 0.00000001 coins + // using Number(x) or parseInt(x) will either change it back to scientific notation or round to zero + originCoinDepositMin: ?string, + originCoinDepositFee: ?number, + shapeShiftRate: ?number, +}; + +// All ShapeShift actions that will have some payload +export type GetSupportedCoinsSuccess = { + type: actions.GET_SUPPORTED_COINS_SUCCESS, + data: Array, +}; +export type GetCoinStatsStart = { + type: actions.GET_SUPPORTED_COINS_SUCCESS, + data: string, +}; +export type GetCoinStatsSuccess = { + type: actions.GET_COIN_STATS_SUCCESS, + data: ShapeShiftMarketInfo, +}; +export type GetCoinStatsFail = { + type: actions.GET_COIN_STATS_FAIL, + data: string, +}; +export type PrepareShapeShiftSuccess = { + type: actions.PREPARE_SHAPE_SHIFT_SUCCESS, + data: ActiveShiftInfo, +}; +export type PrepareShapeShiftFail = { + type: actions.PREPARE_SHAPE_SHIFT_FAIL, + data: ShapeShiftErrorResponse, +}; +export type GetActiveShiftSuccess = { + type: actions.GET_ACTIVE_SHIFT_SUCCESS, + data: string, +}; +export type GetActiveShiftFail = { + type: actions.GET_ACTIVE_SHIFT_FAIL, + data: ShapeShiftErrorResponse, +}; + +// ShapeShift sub-types +// Defined for actions that contain an object in the payload +type ShapeShiftMarketInfo = { + limit: number, + minimum: number, + minerFee: number, + rate: number, +}; + +type ActiveShiftInfo = { + deposit: string, + depositType: string, + returnAddress: string, + orderId: string, +}; + +type ShapeShiftErrorResponse = { + message: string, +}; + +const defaultState: ShapeShiftState = { + loading: true, + updating: false, + shiftSupportedCoins: [], + hasActiveShift: false, + originCoin: undefined, + error: undefined, + shiftDepositAddress: undefined, // shapeshift address to send your coins to + shiftReturnAddress: undefined, + shiftCoinType: undefined, + shiftOrderId: undefined, + shiftState: undefined, + originCoinDepositMax: undefined, + originCoinDepositMin: undefined, + originCoinDepositFee: undefined, + shapeShiftRate: undefined, +}; + +export default handleActions( + { + [actions.GET_SUPPORTED_COINS_START]: ( + state: ShapeShiftState + ): ShapeShiftState => ({ + ...state, + loading: true, + error: undefined, + }), + [actions.GET_SUPPORTED_COINS_SUCCESS]: ( + state: ShapeShiftState, + action: GetSupportedCoinsSuccess + ): ShapeShiftState => { + const shiftSupportedCoins = action.data; + return { + ...state, + error: undefined, + shiftSupportedCoins, + }; + }, + [actions.GET_SUPPORTED_COINS_FAIL]: ( + state: ShapeShiftState + ): ShapeShiftState => ({ + ...state, + loading: false, + error: "Error getting available coins", + }), + + [actions.GET_COIN_STATS_START]: ( + state: ShapeShiftState, + action: GetCoinStatsStart + ): ShapeShiftState => { + const coin = action.data; + return { + ...state, + updating: true, + originCoin: coin, + }; + }, + [actions.GET_COIN_STATS_SUCCESS]: ( + state: ShapeShiftState, + action: GetCoinStatsSuccess + ): ShapeShiftState => { + const marketInfo: ShapeShiftMarketInfo = action.data; + + return { + ...state, + loading: false, + updating: false, + originCoinDepositMax: marketInfo.limit, + // this will come in scientific notation + // toFixed shows the real number, then regex to remove trailing zeros + originCoinDepositMin: marketInfo.minimum + .toFixed(10) + .replace(/\.?0+$/, ""), + originCoinDepositFee: marketInfo.minerFee, + shapeShiftRate: marketInfo.rate, + }; + }, + [actions.GET_COIN_STATS_FAIL]: ( + state: ShapeShiftState, + action: GetCoinStatsFail + ): ShapeShiftState => { + const error = action.data; + return { + ...state, + loading: false, + error, + }; + }, + + [actions.PREPARE_SHAPE_SHIFT_START]: ( + state: ShapeShiftState + ): ShapeShiftState => ({ + ...state, + error: undefined, + }), + [actions.PREPARE_SHAPE_SHIFT_SUCCESS]: ( + state: ShapeShiftState, + action: PrepareShapeShiftSuccess + ) => { + const activeShiftInfo: ActiveShiftInfo = action.data; + return { + ...state, + hasActiveShift: true, + shiftDepositAddress: activeShiftInfo.deposit, + shiftCoinType: activeShiftInfo.depositType, + shiftReturnAddress: activeShiftInfo.returnAddress, + shiftOrderId: activeShiftInfo.orderId, + shiftState: statuses.NO_DEPOSITS, + }; + }, + [actions.PREPARE_SHAPE_SHIFT_FAIL]: ( + state: ShapeShiftState, + action: PrepareShapeShiftFail + ) => { + const error: ShapeShiftErrorResponse = action.data; + return { + ...state, + error: error.message, + }; + }, + + [actions.CLEAR_SHAPE_SHIFT]: (state: ShapeShiftState): ShapeShiftState => ({ + ...state, + loading: false, + updating: false, + hasActiveShift: false, + shiftDepositAddress: undefined, + shiftReturnAddress: undefined, + shiftCoinType: undefined, + shiftOrderId: undefined, + originCoin: state.shiftSupportedCoins[0], + }), + + [actions.GET_ACTIVE_SHIFT_START]: ( + state: ShapeShiftState + ): ShapeShiftState => ({ + ...state, + error: undefined, + updating: true, + }), + [actions.GET_ACTIVE_SHIFT_SUCCESS]: ( + state: ShapeShiftState, + action: GetActiveShiftSuccess + ): ShapeShiftState => { + const status = action.data; + return { + ...state, + updating: false, + shiftState: status, + }; + }, + [actions.GET_ACTIVE_SHIFT_FAIL]: ( + state: ShapeShiftState, + action: GetActiveShiftFail + ): ShapeShiftState => { + const error: ShapeShiftErrorResponse = action.data; + return { + ...state, + updating: false, + error: error.message, + }; + }, + }, + defaultState +); diff --git a/src/renderer/redux/selectors/navigation.js b/src/renderer/redux/selectors/navigation.js index 4f91467eb..7633e81c2 100644 --- a/src/renderer/redux/selectors/navigation.js +++ b/src/renderer/redux/selectors/navigation.js @@ -44,8 +44,8 @@ export const selectHeaderLinks = createSelector(selectCurrentPage, page => { return { wallet: __("Overview"), history: __("History"), - send: __("Send"), - receive: __("Receive"), + send: __("Send Credits"), + receive: __("Get Credits"), rewards: __("Rewards"), invite: __("Invites"), }; @@ -78,9 +78,9 @@ export const selectPageTitle = createSelector( case "wallet": return __("Wallet"); case "send": - return __("Send Credits"); + return __("Send LBRY Credits"); case "receive": - return __("Wallet Address"); + return __("Get LBRY Credits"); case "backup": return __("Backup Your Wallet"); case "rewards": diff --git a/src/renderer/redux/selectors/search.js b/src/renderer/redux/selectors/search.js index d64289d78..fc78e50f4 100644 --- a/src/renderer/redux/selectors/search.js +++ b/src/renderer/redux/selectors/search.js @@ -65,7 +65,7 @@ export const selectWunderBarIcon = createSelector( return "icon-envelope-open"; case "address": case "receive": - return "icon-address-book"; + return "icon-credit-card"; case "wallet": case "backup": return "icon-bank"; diff --git a/src/renderer/redux/selectors/shape_shift.js b/src/renderer/redux/selectors/shape_shift.js new file mode 100644 index 000000000..e60af9fb4 --- /dev/null +++ b/src/renderer/redux/selectors/shape_shift.js @@ -0,0 +1,7 @@ +import { createSelector } from "reselect"; + +const _selectState = state => state.shapeShift; + +export const selectShapeShift = createSelector(_selectState, state => ({ + ...state, +})); diff --git a/src/renderer/scss/_gui.scss b/src/renderer/scss/_gui.scss index ed1aebe8b..4cfc20bdf 100644 --- a/src/renderer/scss/_gui.scss +++ b/src/renderer/scss/_gui.scss @@ -36,6 +36,11 @@ body color: var(--color-meta-light); } +.credit-amount--bold +{ + font-weight: 700; +} + #main-content { margin: auto; diff --git a/src/renderer/scss/all.scss b/src/renderer/scss/all.scss index dc4be7179..40c63b1f4 100644 --- a/src/renderer/scss/all.scss +++ b/src/renderer/scss/all.scss @@ -26,4 +26,6 @@ @import "component/_divider.scss"; @import "component/_checkbox.scss"; @import "component/_radio.scss"; +@import "component/_shapeshift.scss"; +@import "component/_spinner.scss"; @import "page/_show.scss"; diff --git a/src/renderer/scss/component/_button.scss b/src/renderer/scss/component/_button.scss index 642e3de92..e3dad6330 100644 --- a/src/renderer/scss/component/_button.scss +++ b/src/renderer/scss/component/_button.scss @@ -98,4 +98,4 @@ $button-focus-shift: 12%; .button--submit { font-family: inherit; line-height: 0; -} +} \ No newline at end of file diff --git a/src/renderer/scss/component/_card.scss b/src/renderer/scss/component/_card.scss index a630d5558..53d8c6d1e 100644 --- a/src/renderer/scss/component/_card.scss +++ b/src/renderer/scss/component/_card.scss @@ -26,6 +26,7 @@ .card__actions { padding: 0 var(--card-padding); } + .card--small { .card__title-primary, .card__title-identity, @@ -50,7 +51,7 @@ .card__actions { margin-top: var(--card-margin); margin-bottom: var(--card-margin); - user-select: none; + user-select: none; } .card__actions--bottom { @@ -72,6 +73,18 @@ margin-bottom: var(--card-margin); } } + +.card__actions--only-vertical { + margin-left: 0; + margin-right: 0; + padding-left: 0; + padding-right: 0; +} + +.card__content--extra-vertical-space { + margin: $spacing-vertical 0; +} + $font-size-subtext-multiple: 0.82; .card__subtext { color: var(--color-meta-light); diff --git a/src/renderer/scss/component/_channel-indicator.scss b/src/renderer/scss/component/_channel-indicator.scss index 6c63a4235..50b7648d0 100644 --- a/src/renderer/scss/component/_channel-indicator.scss +++ b/src/renderer/scss/component/_channel-indicator.scss @@ -1,9 +1,9 @@ .channel-name { - display: inline-block; + display: inline-flex; overflow: hidden; white-space: nowrap; - text-overflow: ellipsis + text-overflow: ellipsis; } // this shouldn't know about the card width diff --git a/src/renderer/scss/component/_form-field.scss b/src/renderer/scss/component/_form-field.scss index c7aedf480..1d7398d7a 100644 --- a/src/renderer/scss/component/_form-field.scss +++ b/src/renderer/scss/component/_form-field.scss @@ -56,6 +56,11 @@ padding-left: 5px; padding-right: 5px; width: 100%; + font-family: "Consolas", "Lucida Console", "Adobe Source Code Pro", monospace; + + &.input-copyable--with-copy-btn { + width: 85%; + } } input[readonly] { diff --git a/src/renderer/scss/component/_header.scss b/src/renderer/scss/component/_header.scss index 769622fd8..bdc72dcbd 100644 --- a/src/renderer/scss/component/_header.scss +++ b/src/renderer/scss/component/_header.scss @@ -43,6 +43,8 @@ .wunderbar--active .icon-search { color: var(--color-primary); } +// below styles should be inside the common input styling +// will come back to this with the redesign - sean .wunderbar__input { background: var(--search-bg); width: 100%; diff --git a/src/renderer/scss/component/_shapeshift.scss b/src/renderer/scss/component/_shapeshift.scss new file mode 100644 index 000000000..cb69676a7 --- /dev/null +++ b/src/renderer/scss/component/_shapeshift.scss @@ -0,0 +1,40 @@ +// Can't think of a better way to do this +// The initial shapeshift form is 311px tall +// the .shapeshift__initial-wrapper class is only added when the form is being loaded +// Once the form is rendered, there is a very smooth transition because the height doesn't change +.shapeshift__wrapper.shapeshift__initial-wrapper { + min-height: 346px; +} + +.shapeshift__content { + .spinner { + margin-top: $spacing-vertical * 3; + } +} + +.shapeshift__tx-info { + min-height: 55px; +} + +.shapeshift__deposit-address-wrapper { + display: flex; + flex-direction: row; + + * { + align-self: center; + } +} + +// this should be pulled out into it's own styling when we add more qr codes +.shapeshift__qrcode { + // don't use a variable here. adds a white border for easier reading in dark mode + // needs to stay the same no matter what theme is present + background-color: #fff; + padding: 2px; + margin-left: 40px; +} + + +.shapeshift__link { + padding-left: 10px; +} diff --git a/src/renderer/scss/component/_spinner.scss b/src/renderer/scss/component/_spinner.scss new file mode 100644 index 000000000..ec339e5a1 --- /dev/null +++ b/src/renderer/scss/component/_spinner.scss @@ -0,0 +1,58 @@ +.spinner { + position: relative; + width: 11em; + height: 11em; + margin: 20px auto; + font-size: 3px; + border-radius: 50%; + + background: linear-gradient(to right, #fff 10%, rgba(255, 255, 255, 0) 50%); + animation: spin 1.4s infinite linear; + transform: translateZ(0); + + @keyframes spin { + from { + transform: rotate(0deg); + } + to { + transform: rotate(360deg); + } + } + + &:before, + &:after { + content: ''; + position: absolute; + top: 0; + left: 0; + } + + &:before { + width: 50%; + height: 50%; + background: #fff; + border-radius: 100% 0 0 0; + } + + &:after { + height: 75%; + width: 75%; + margin: auto; + bottom: 0; + right: 0; + background: #000; + border-radius: 50%; + } +} + +.spinner.spinner--dark { + background: linear-gradient(to right, var(--button-primary-bg) 10%, var(--color-bg) 50%); + + &:before { + background: var(--button-primary-bg); + } + + &:after { + background: var(--color-bg); + } +} \ No newline at end of file diff --git a/src/renderer/scss/component/_video.scss b/src/renderer/scss/component/_video.scss index 3f3204308..d6021849e 100644 --- a/src/renderer/scss/component/_video.scss +++ b/src/renderer/scss/component/_video.scss @@ -46,53 +46,6 @@ video { align-items: center; } -.video__loading-spinner { - position: relative; - width: 11em; - height: 11em; - margin: 20px auto; - font-size: 3px; - border-radius: 50%; - - background: linear-gradient(to right, #ffffff 10%, rgba(255, 255, 255, 0) 50%); - animation: spin 1.4s infinite linear; - transform: translateZ(0); - - @keyframes spin { - from { - transform: rotate(0deg); - } - to { - transform: rotate(360deg); - } - } - - &:before, - &:after { - content: ''; - position: absolute; - top: 0; - left: 0; - } - - &:before { - width: 50%; - height: 50%; - background: #ffffff; - border-radius: 100% 0 0 0; - } - - &:after { - height: 75%; - width: 75%; - margin: auto; - bottom: 0; - right: 0; - background: black; - border-radius: 50%; - } -} - .video__loading-status { padding-top: 20px; color: white; diff --git a/src/renderer/store.js b/src/renderer/store.js index 4ed0dfbbc..586411687 100644 --- a/src/renderer/store.js +++ b/src/renderer/store.js @@ -11,6 +11,7 @@ import searchReducer from "redux/reducers/search"; import settingsReducer from "redux/reducers/settings"; import userReducer from "redux/reducers/user"; import walletReducer from "redux/reducers/wallet"; +import shapeShiftReducer from "redux/reducers/shape_shift"; import { persistStore, autoRehydrate } from "redux-persist"; import createCompressor from "redux-persist-transform-compress"; import createFilter from "redux-persist-transform-filter"; @@ -67,6 +68,7 @@ const reducers = redux.combineReducers({ settings: settingsReducer, wallet: walletReducer, user: userReducer, + shapeShift: shapeShiftReducer, }); const bulkThunk = createBulkThunkMiddleware(); diff --git a/src/renderer/types/common.js b/src/renderer/types/common.js new file mode 100644 index 000000000..ea8c0d815 --- /dev/null +++ b/src/renderer/types/common.js @@ -0,0 +1,3 @@ +export type FormikActions = { + setSubmitting: boolean => mixed, +}; diff --git a/src/renderer/util/redux-utils.js b/src/renderer/util/redux-utils.js index 6875ec550..17aa4fdaf 100644 --- a/src/renderer/util/redux-utils.js +++ b/src/renderer/util/redux-utils.js @@ -1,6 +1,7 @@ // util for creating reducers // based off of redux-actions // https://redux-actions.js.org/docs/api/handleAction.html#handleactions + export const handleActions = (actionMap, defaultState) => { return (state = defaultState, action) => { const handler = actionMap[action.type]; diff --git a/src/renderer/util/shape_shift.js b/src/renderer/util/shape_shift.js new file mode 100644 index 000000000..7adfc1abc --- /dev/null +++ b/src/renderer/util/shape_shift.js @@ -0,0 +1,50 @@ +// these don't need to be exact +// shapeshift does a more thourough check on validity +// just general matches to prevent unneccesary api calls +export const coinRegexPatterns = { + BTC: /^[13][a-km-zA-HJ-NP-Z1-9]{25,34}$/, + BCH: /^[13][a-km-zA-HJ-NP-Z1-9]{25,34}$/, + ETH: /^(0x)?[0-9a-fA-F]{40}$/, + DASH: /^X([0-9a-zA-Z]){33}/, + LTC: /^L[a-zA-Z0-9]{26,33}$/, + XMR: /^4[0-9ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{94}$/, +}; + +const validateAddress = (coinType, address) => { + if (!coinType || !address) return false; + + const coinRegex = coinRegexPatterns[coinType.toUpperCase()]; + if (!coinRegex) return false; + + return coinRegex.test(address); +}; + +export const validateShapeShiftForm = (vals, props) => { + let errors = {}; + + if (!vals.returnAddress) { + return errors; + } + + const isValidAddress = validateAddress(vals.originCoin, vals.returnAddress); + + if (!isValidAddress) { + errors.returnAddress = `Enter a valid ${vals.originCoin} address`; + } + + return errors; +}; + +const exampleCoinAddresses = { + BTC: "1745oPaHeW7Fmpb1fUKTtasYfxr4zu9bwq", + BCH: "1745oPaHeW7Fmpb1fUKTtasYfxr4zu9bwq", + ETH: "0x8507cA6a274123fC8f80d929AF9D83602bC4e8cC", + DASH: "XedBP7vLPFXbS3URjrH2Z57Fg9SWftBmQ6", + LTC: "LgZivMvFMTDoqcA5weCQ2QrmRp7pa56bBk", + XMR: + "466XMeJEcowYGx7RzUJj3VDWBZgRWErVQQX6tHYbsacS5QF6v3tidE6LZZnTJgzeEh6bKEEJ6GC9jHirrUKvJwVKVj9e7jm", +}; + +export const getExampleAddress = coin => { + return exampleCoinAddresses[coin]; +}; diff --git a/yarn.lock b/yarn.lock index 00a191098..7dcd6127d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -34,6 +34,13 @@ version "0.0.6" resolved "https://registry.yarnpkg.com/7zip/-/7zip-0.0.6.tgz#9cafb171af82329490353b4816f03347aa150a30" +"@segment/top-domain@^3.0.0": + version "3.0.0" + resolved "https://registry.yarnpkg.com/@segment/top-domain/-/top-domain-3.0.0.tgz#02e5a5a4fd42a9f6cf886b05e82f104012a3c3a7" + dependencies: + component-cookie "^1.1.2" + component-url "^0.2.1" + "@types/node@^7.0.18": version "7.0.43" resolved "https://registry.yarnpkg.com/@types/node/-/node-7.0.43.tgz#a187e08495a075f200ca946079c914e1a5fe962c" @@ -146,6 +153,16 @@ amdefine@>=0.0.4: version "1.0.1" resolved "https://registry.yarnpkg.com/amdefine/-/amdefine-1.0.1.tgz#4a5282ac164729e93619bcfd3ad151f817ce91f5" +amplitude-js@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/amplitude-js/-/amplitude-js-4.0.0.tgz#70bbc0ec893b01d00453d3765f78bc0f32a395cc" + dependencies: + "@segment/top-domain" "^3.0.0" + blueimp-md5 "^2.10.0" + json3 "^3.3.2" + lodash "^4.17.4" + ua-parser-js "github:amplitude/ua-parser-js#ed538f1" + ansi-align@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/ansi-align/-/ansi-align-2.0.0.tgz#c36aeccba563b89ceb556f3690f0b1d9e3547f7f" @@ -1269,6 +1286,10 @@ bluebird@^3.5.0, bluebird@^3.5.1, bluebird@~3.5.0: version "3.5.1" resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.5.1.tgz#d9551f9de98f1fcda1e683d17ee91a0602ee2eb9" +blueimp-md5@^2.10.0: + version "2.10.0" + resolved "https://registry.yarnpkg.com/blueimp-md5/-/blueimp-md5-2.10.0.tgz#02f0843921f90dca14f5b8920a38593201d6964d" + bn.js@^4.0.0, bn.js@^4.1.0, bn.js@^4.1.1, bn.js@^4.4.0: version "4.11.8" resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-4.11.8.tgz#2cde09eb5ee341f484746bb0309b3253b1b1442f" @@ -1886,6 +1907,16 @@ compare-version@^0.1.2: version "0.1.2" resolved "https://registry.yarnpkg.com/compare-version/-/compare-version-0.1.2.tgz#0162ec2d9351f5ddd59a9202cba935366a725080" +component-cookie@^1.1.2: + version "1.1.3" + resolved "https://registry.yarnpkg.com/component-cookie/-/component-cookie-1.1.3.tgz#053e14a3bd7748154f55724fd39a60c01994ebed" + dependencies: + debug "*" + +component-url@^0.2.1: + version "0.2.1" + resolved "https://registry.yarnpkg.com/component-url/-/component-url-0.2.1.tgz#4e4f4799c43ead9fd3ce91b5a305d220208fee47" + compressible@~2.0.11: version "2.0.12" resolved "https://registry.yarnpkg.com/compressible/-/compressible-2.0.12.tgz#c59a5c99db76767e9876500e271ef63b3493bd66" @@ -2235,6 +2266,12 @@ date-now@^0.1.4: version "0.1.4" resolved "https://registry.yarnpkg.com/date-now/-/date-now-0.1.4.tgz#eaf439fd4d4848ad74e5cc7dbef200672b9e345b" +debug@*, debug@^3.0.0, debug@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/debug/-/debug-3.1.0.tgz#5bb5a0672628b64149566ba16819e61518c67261" + dependencies: + ms "2.0.0" + debug@2, debug@2.6.9, debug@^2.6.6: version "2.6.9" resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" @@ -2253,12 +2290,6 @@ debug@^2.1.3, debug@^2.2.0, debug@^2.6.8: dependencies: ms "2.0.0" -debug@^3.0.0, debug@^3.1.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/debug/-/debug-3.1.0.tgz#5bb5a0672628b64149566ba16819e61518c67261" - dependencies: - ms "2.0.0" - debuglog@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/debuglog/-/debuglog-1.0.1.tgz#aa24ffb9ac3df9a2351837cfb2d279360cd78492" @@ -2424,6 +2455,10 @@ dom-serializer@0: domelementtype "~1.1.1" entities "~1.1.1" +dom-walk@^0.1.0: + version "0.1.1" + resolved "https://registry.yarnpkg.com/dom-walk/-/dom-walk-0.1.1.tgz#672226dc74c8f799ad35307df936aba11acd6018" + domain-browser@^1.1.1: version "1.1.7" resolved "https://registry.yarnpkg.com/domain-browser/-/domain-browser-1.1.7.tgz#867aa4b093faa05f1de08c06f4d7b21fdf8698bc" @@ -3215,6 +3250,12 @@ flush-write-stream@^1.0.0: inherits "^2.0.1" readable-stream "^2.0.4" +for-each@^0.3.2: + version "0.3.2" + resolved "https://registry.yarnpkg.com/for-each/-/for-each-0.3.2.tgz#2c40450b9348e97f281322593ba96704b9abd4d4" + dependencies: + is-function "~1.0.0" + for-in@^0.1.3: version "0.1.8" resolved "https://registry.yarnpkg.com/for-in/-/for-in-0.1.8.tgz#d8773908e31256109952b1fdb9b3fa867d2775e1" @@ -3259,6 +3300,14 @@ form-data@~2.3.1: combined-stream "^1.0.5" mime-types "^2.1.12" +formik@^0.10.4: + version "0.10.5" + resolved "https://registry.yarnpkg.com/formik/-/formik-0.10.5.tgz#6984d2f22e918c6d2264a3cb86b8582f7277faca" + dependencies: + lodash.isequal "4.5.0" + prop-types "^15.5.10" + warning "^3.0.0" + forwarded@~0.1.2: version "0.1.2" resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.1.2.tgz#98c23dab1175657b8c0573e8ceccd91b0ff18c84" @@ -3473,6 +3522,13 @@ global-dirs@^0.1.0: dependencies: ini "^1.3.4" +global@~4.3.0: + version "4.3.2" + resolved "https://registry.yarnpkg.com/global/-/global-4.3.2.tgz#e76989268a6c74c38908b1305b10fc0e394e9d0f" + dependencies: + min-document "^2.19.0" + process "~0.5.1" + globals@^9.18.0: version "9.18.0" resolved "https://registry.yarnpkg.com/globals/-/globals-9.18.0.tgz#aa3896b3e69b487f17e31ed2143d69a8e30c2d8a" @@ -4060,6 +4116,10 @@ is-fullwidth-code-point@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz#a3b30a5c4f199183167aaab93beefae3ddfb654f" +is-function@^1.0.1, is-function@~1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/is-function/-/is-function-1.0.1.tgz#12cfb98b65b57dd3d193a3121f5f6e2f437602b5" + is-glob@^2.0.0, is-glob@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-2.0.1.tgz#d096f926a3ded5600f3fdfd91198cb0888c2d863" @@ -4622,6 +4682,10 @@ lodash.isempty@^4.4.0: version "4.4.0" resolved "https://registry.yarnpkg.com/lodash.isempty/-/lodash.isempty-4.4.0.tgz#6f86cbedd8be4ec987be9aaf33c9684db1b31e7e" +lodash.isequal@4.5.0: + version "4.5.0" + resolved "https://registry.yarnpkg.com/lodash.isequal/-/lodash.isequal-4.5.0.tgz#415c4478f2bcc30120c22ce10ed3226f7d3e18e0" + lodash.isplainobject@^4.0.6: version "4.0.6" resolved "https://registry.yarnpkg.com/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz#7c526a52d89b45c45cc690b88163be0497f550cb" @@ -4901,6 +4965,12 @@ mimic-fn@^1.0.0: version "1.1.0" resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-1.1.0.tgz#e667783d92e89dbd342818b5230b9d62a672ad18" +min-document@^2.19.0: + version "2.19.0" + resolved "https://registry.yarnpkg.com/min-document/-/min-document-2.19.0.tgz#7bd282e3f5842ed295bb748cdd9f1ffa2c824685" + dependencies: + dom-walk "^0.1.0" + minimalistic-assert@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/minimalistic-assert/-/minimalistic-assert-1.0.0.tgz#702be2dda6b37f4836bcb3f5db56641b64a1d3d3" @@ -5683,6 +5753,13 @@ parse-glob@^3.0.4: is-extglob "^1.0.0" is-glob "^2.0.0" +parse-headers@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/parse-headers/-/parse-headers-2.0.1.tgz#6ae83a7aa25a9d9b700acc28698cd1f1ed7e9536" + dependencies: + for-each "^0.3.2" + trim "0.0.1" + parse-json@^2.2.0: version "2.2.0" resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-2.2.0.tgz#f480f40434ef80741f8469099f8dea18f55a4dc9" @@ -6116,6 +6193,10 @@ process@^0.11.10: version "0.11.10" resolved "https://registry.yarnpkg.com/process/-/process-0.11.10.tgz#7332300e840161bda3e69a1d1d91a7d4bc16f182" +process@~0.5.1: + version "0.5.2" + resolved "https://registry.yarnpkg.com/process/-/process-0.5.2.tgz#1638d8a8e34c2f440a91db95ab9aeb677fc185cf" + progress-stream@^1.1.0: version "1.2.0" resolved "https://registry.yarnpkg.com/progress-stream/-/progress-stream-1.2.0.tgz#2cd3cfea33ba3a89c9c121ec3347abe9ab125f77" @@ -6220,10 +6301,21 @@ q@^1.1.2: version "1.5.1" resolved "https://registry.yarnpkg.com/q/-/q-1.5.1.tgz#7e32f75b41381291d04611f1bf14109ac00651d7" +qr.js@0.0.0: + version "0.0.0" + resolved "https://registry.yarnpkg.com/qr.js/-/qr.js-0.0.0.tgz#cace86386f59a0db8050fa90d9b6b0e88a1e364f" + qrcode-terminal@~0.11.0: version "0.11.0" resolved "https://registry.yarnpkg.com/qrcode-terminal/-/qrcode-terminal-0.11.0.tgz#ffc6c28a2fc0bfb47052b47e23f4f446a5fbdb9e" +qrcode.react@^0.7.2: + version "0.7.2" + resolved "https://registry.yarnpkg.com/qrcode.react/-/qrcode.react-0.7.2.tgz#72a5718fd56baafe15c2c153fe436628d83aa286" + dependencies: + prop-types "^15.5.8" + qr.js "0.0.0" + qs@6.5.1, qs@~6.5.1: version "6.5.1" resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.1.tgz#349cdf6eef89ec45c12d7d5eb3fc0c870343a6d8" @@ -6772,7 +6864,7 @@ repeating@^2.0.0: dependencies: is-finite "^1.0.0" -request@2, request@^2.74.0, request@~2.83.0: +request@2, request@^2.55.0, request@^2.74.0, request@~2.83.0: version "2.83.0" resolved "https://registry.yarnpkg.com/request/-/request-2.83.0.tgz#ca0b65da02ed62935887808e6f510381034e3356" dependencies: @@ -7087,6 +7179,13 @@ shallow-clone@^0.1.2: lazy-cache "^0.2.3" mixin-object "^2.0.1" +shapeshift.io@^1.3.1: + version "1.3.1" + resolved "https://registry.yarnpkg.com/shapeshift.io/-/shapeshift.io-1.3.1.tgz#939f7d89e6a93fad4b556567d3fcdab45d5cc021" + dependencies: + request "^2.55.0" + xhr "^2.0.1" + shebang-command@^1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-1.2.0.tgz#44aac65b695b03398968c39f363fee5deafdf1ea" @@ -7685,6 +7784,10 @@ trim-right@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/trim-right/-/trim-right-1.0.1.tgz#cb2e1203067e0c8de1f614094b9fe45704ea6003" +trim@0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/trim/-/trim-0.0.1.tgz#5858547f6b290757ee95cccc666fb50084c460dd" + "true-case-path@^1.0.2": version "1.0.2" resolved "https://registry.yarnpkg.com/true-case-path/-/true-case-path-1.0.2.tgz#7ec91130924766c7f573be3020c34f8fdfd00d62" @@ -7734,6 +7837,10 @@ ua-parser-js@^0.7.9: version "0.7.17" resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-0.7.17.tgz#e9ec5f9498b9ec910e7ae3ac626a805c4d09ecac" +"ua-parser-js@github:amplitude/ua-parser-js#ed538f1": + version "0.7.10" + resolved "https://codeload.github.com/amplitude/ua-parser-js/tar.gz/ed538f16f5c6ecd8357da989b617d4f156dcf35d" + uglify-js@3.2.x: version "3.2.1" resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-3.2.1.tgz#d6427fd45a25fefc5d196689c0c772a6915e10fe" @@ -7991,6 +8098,12 @@ vm-browserify@0.0.4: dependencies: indexof "0.0.1" +warning@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/warning/-/warning-3.0.0.tgz#32e5377cb572de4ab04753bdf8821c01ed605b7c" + dependencies: + loose-envify "^1.0.0" + watchpack@^1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/watchpack/-/watchpack-1.4.0.tgz#4a1472bcbb952bd0a9bb4036801f954dfb39faac" @@ -8202,6 +8315,15 @@ xdg-basedir@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/xdg-basedir/-/xdg-basedir-3.0.0.tgz#496b2cc109eca8dbacfe2dc72b603c17c5870ad4" +xhr@^2.0.1: + version "2.4.0" + resolved "https://registry.yarnpkg.com/xhr/-/xhr-2.4.0.tgz#e16e66a45f869861eeefab416d5eff722dc40993" + dependencies: + global "~4.3.0" + is-function "^1.0.1" + parse-headers "^2.0.0" + xtend "^4.0.0" + xml-char-classes@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/xml-char-classes/-/xml-char-classes-1.0.0.tgz#64657848a20ffc5df583a42ad8a277b4512bbc4d"