This commit is contained in:
Sean Yesmunt 2017-12-03 22:27:55 -05:00
parent e7a7754a4f
commit c000ad1bc8
14 changed files with 381 additions and 109 deletions

View file

@ -12,6 +12,9 @@ flow-typed
suppress_comment=\\(.\\|\n\\)*\\$FlowFixMe
suppress_comment=\\(.\\|\n\\)*\\$FlowIssue
module.name_mapper='^constants\(.*\)$' -> '<PROJECT_ROOT>/js/constants\1'
module.name_mapper='^util\(.*\)$' -> '<PROJECT_ROOT>/js/util\1'
module.name_mapper='^redux\(.*\)$' -> '<PROJECT_ROOT>/js/redux\1'
module.name_mapper='^types\(.*\)$' -> '<PROJECT_ROOT>/js/types\1'
module.name_mapper='^component\(.*\)$' -> '<PROJECT_ROOT>/js/component\1'
[strict]

3
src/renderer/flow-typed/bluebird.js vendored Normal file
View file

@ -0,0 +1,3 @@
declare module 'bluebird' {
declare module.exports: any;
}

3
src/renderer/flow-typed/classnames.js vendored Normal file
View file

@ -0,0 +1,3 @@
declare module 'classnames' {
declare module.exports: any;
}

3
src/renderer/flow-typed/formik.js vendored Normal file
View file

@ -0,0 +1,3 @@
declare module 'formik' {
declare module.exports: any;
}

1
src/renderer/flow-typed/i18n.js vendored Normal file
View file

@ -0,0 +1 @@
declare function __(a: string): string;

View file

@ -0,0 +1,3 @@
declare module 'qrcode.react' {
declare module.exports: any;
}

View file

@ -0,0 +1,3 @@
declare module 'shapeshift.io' {
declare module.exports: any;
}

View file

@ -1,12 +1,27 @@
import React, { PureComponent } from "react";
// @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";
export default class ActiveShapeShift extends PureComponent {
type Props = {
shiftState: ?string,
shiftCoinType: ?string,
shiftDepositAddress: ?string,
shiftReturnAddress: ?string,
shiftOrderId: ?string,
originCoinDepositMax: ?number,
clearShapeShift: Dispatch,
doShowSnackBar: Dispatch,
getActiveShift: Dispatch,
};
class ActiveShapeShift extends React.PureComponent<Props> {
constructor() {
super();
// $FlowFixMe
this.continousFetch = undefined;
}
@ -14,12 +29,13 @@ export default class ActiveShapeShift extends PureComponent {
const { getActiveShift, shiftDepositAddress } = this.props;
getActiveShift(shiftDepositAddress);
// $FlowFixMe
this.continousFetch = setInterval(() => {
getActiveShift(shiftDepositAddress);
}, 10000);
}
componentWillUpdate(nextProps) {
componentWillUpdate(nextProps: Props) {
const { shiftState } = nextProps;
if (shiftState === statuses.COMPLETE) {
this.clearContinuousFetch();
@ -31,7 +47,9 @@ export default class ActiveShapeShift extends PureComponent {
}
clearContinuousFetch() {
/// $FlowFixMe
clearInterval(this.continousFetch);
// $FlowFixMe
this.continousFetch = null;
}
@ -42,7 +60,7 @@ export default class ActiveShapeShift extends PureComponent {
shiftReturnAddress,
shiftOrderId,
shiftState,
shiftDepositLimit,
originCoinDepositMax,
clearShapeShift,
doShowSnackBar,
} = this.props;
@ -54,7 +72,7 @@ export default class ActiveShapeShift extends PureComponent {
<p>
Send up to{" "}
<span className="credit-amount--bold">
{shiftDepositLimit}{" "}
{originCoinDepositMax}{" "}
<span className="credit-amount--colored">{shiftCoinType}</span>
</span>{" "}
to the address below.
@ -102,13 +120,15 @@ export default class ActiveShapeShift extends PureComponent {
: __("Cancel")
}
/>
<span className="shapeshift__link">
<Link
button="text"
label={__("View the status on Shapeshift.io")}
href={`https://shapeshift.io/#/status/${shiftOrderId}`}
/>
</span>
{shiftOrderId && (
<span className="shapeshift__link">
<Link
button="text"
label={__("View the status on Shapeshift.io")}
href={`https://shapeshift.io/#/status/${shiftOrderId}`}
/>
</span>
)}
{shiftState === statuses.NO_DEPOSITS &&
shiftReturnAddress && (
<div className="shapeshift__actions-help">
@ -123,3 +143,5 @@ export default class ActiveShapeShift extends PureComponent {
);
}
}
export default ActiveShapeShift;

View file

@ -2,25 +2,48 @@ 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";
export default ({
values,
errors,
touched,
handleChange,
handleBlur,
handleSubmit,
resetForm,
isSubmitting,
shiftSupportedCoins,
originCoin,
updating,
getCoinStats,
receiveAddress,
originCoinDepositMax,
originCoinDepositMin,
originCoinDepositFee,
}) => {
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<string>,
originCoin: string,
updating: boolean,
getCoinStats: Dispatch,
receiveAddress: string,
originCoinDepositFee: number,
originCoinDepositMin: string,
originCoinDepositMax: number,
};
export default (props: Props) => {
const {
values,
errors,
touched,
handleChange,
handleBlur,
handleSubmit,
isSubmitting,
shiftSupportedCoins,
originCoin,
updating,
getCoinStats,
receiveAddress,
originCoinDepositMax,
originCoinDepositMin,
originCoinDepositFee,
} = props;
return (
<form onSubmit={handleSubmit}>
<div className="form-field">

View file

@ -1,4 +1,5 @@
import React from "react";
// @flow
import * as React from "react";
import { shell } from "electron";
import { Formik } from "formik";
import classnames from "classnames";
@ -8,9 +9,23 @@ import Link from "component/link";
import Spinner from "component/common/spinner";
import { BusyMessage } from "component/common";
import ShapeShiftForm from "./internal/form";
import ActiveShift from "./internal/active-shift";
import ActiveShapeShift from "./internal/active-shift";
class ShapeShift extends React.PureComponent {
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<Props> {
componentDidMount() {
const {
shapeShiftInit,
@ -49,11 +64,15 @@ class ShapeShift extends React.PureComponent {
shiftReturnAddress,
shiftCoinType,
shiftOrderId,
cancelShapeShift,
shiftState,
origin,
} = 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
@ -85,11 +104,7 @@ class ShapeShift extends React.PureComponent {
<Formik
onSubmit={createShapeShift}
validate={validateShapeShiftForm}
initialValues={{
receiveAddress,
originCoin: "BTC",
returnAddress: "",
}}
initialValues={initialFormValues}
render={formProps => (
<ShapeShiftForm
{...formProps}
@ -106,12 +121,12 @@ class ShapeShift extends React.PureComponent {
/>
)}
{hasActiveShift && (
<ActiveShift
<ActiveShapeShift
getActiveShift={getActiveShift}
shiftCoinType={shiftCoinType}
shiftReturnAddress={shiftReturnAddress}
shiftDepositAddress={shiftDepositAddress}
shiftDepositLimit={originCoinDepositMax}
originCoinDepositMax={originCoinDepositMax}
shiftOrderId={shiftOrderId}
shiftState={shiftState}
clearShapeShift={clearShapeShift}

View file

@ -1,11 +1,56 @@
import { createAction } from "util/redux-utils";
// @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"));
export const shapeShiftInit = () => dispatch => {
// 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<Action>;
type ThunkAction = (dispatch: Dispatch) => any;
export type Dispatch = (
action: Action | ThunkAction | PromiseAction | Array<Action>
) => 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
@ -34,8 +79,9 @@ export const shapeShiftInit = () => dispatch => {
);
};
export const getCoinStats = coin => dispatch => {
// TODO: get ShapeShift fee
export const getCoinStats = (coin: string) => (
dispatch: Dispatch
): ThunkAction => {
const pair = `${coin.toLowerCase()}_lbc`;
dispatch({ type: types.GET_COIN_STATS_START, data: coin });
@ -48,7 +94,10 @@ export const getCoinStats = coin => dispatch => {
.catch(err => dispatch({ type: types.GET_COIN_STATS_FAIL, data: err }));
};
export const createShapeShift = (values, actions) => dispatch => {
export const createShapeShift = (
values: ShapeShiftFormValues,
actions: FormikActions
) => (dispatch: Dispatch): ThunkAction => {
const {
originCoin,
returnAddress,
@ -68,12 +117,14 @@ export const createShapeShift = (values, actions) => dispatch => {
)
.catch(err => {
dispatch({ type: types.PREPARE_SHAPE_SHIFT_FAIL, data: err });
// for formik
// for formik to stop the submit
actions.setSubmitting(false);
});
};
export const getActiveShift = depositAddress => dispatch => {
export const getActiveShift = (depositAddress: string) => (
dispatch: Dispatch
): ThunkAction => {
dispatch({ type: types.GET_ACTIVE_SHIFT_START });
return shapeShift
@ -82,5 +133,5 @@ export const getActiveShift = depositAddress => dispatch => {
.catch(err => dispatch({ type: types.GET_ACTIVE_SHIFT_FAIL, data: err }));
};
export const clearShapeShift = () => dispatch =>
export const clearShapeShift = () => (dispatch: Dispatch): Action =>
dispatch({ type: types.CLEAR_SHAPE_SHIFT });

View file

@ -1,14 +1,88 @@
// @flow
import { handleActions } from "util/redux-utils";
import * as types from "constants/action_types";
import * as actions from "constants/action_types";
import * as statuses from "constants/shape_shift";
const defaultState = {
export type ShapeShiftState = {
loading: boolean,
updating: boolean,
shiftSupportedCoins: Array<string>,
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,
};
// All ShapeShift actions that will have some payload
export type GetSupportedCoinsSuccess = {
type: actions.GET_SUPPORTED_COINS_SUCCESS,
data: Array<string>,
};
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,
};
type ActiveShiftInfo = {
deposit: string,
depositType: string,
returnAddress: string,
orderId: string,
};
type ShapeShiftErrorResponse = {
message: string,
};
const defaultState: ShapeShiftState = {
loading: true,
updating: false,
error: undefined,
shiftSupportedCoins: [],
originCoin: undefined,
hasActiveShift: false,
originCoin: undefined,
error: undefined,
shiftDepositAddress: undefined, // shapeshift address to send your coins to
shiftReturnAddress: undefined,
shiftCoinType: undefined,
@ -21,61 +95,108 @@ const defaultState = {
export default handleActions(
{
[types.GET_SUPPORTED_COINS_START]: () => ({
[actions.GET_SUPPORTED_COINS_START]: (
state: ShapeShiftState
): ShapeShiftState => ({
...state,
loading: true,
error: undefined,
}),
[types.GET_SUPPORTED_COINS_SUCCESS]: (
state,
{ data: shiftSupportedCoins }
) => ({
error: undefined,
shiftSupportedCoins,
}),
[types.GET_SUPPORTED_COINS_FAIL]: () => ({
[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: true,
error: "Error getting available coins",
}),
[types.GET_COIN_STATS_START]: (state, { data: coin }) => ({
updating: true,
originCoin: coin,
}),
[types.GET_COIN_STATS_SUCCESS]: (state, { data: marketInfo }) => ({
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,
}),
[types.GET_COIN_STATS_FAIL]: (state, { data: error }) => ({
loading: false,
error,
}),
[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;
[types.PREPARE_SHAPE_SHIFT_START]: () => ({
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,
};
},
[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,
}),
[types.PREPARE_SHAPE_SHIFT_SUCCESS]: (
state,
{ data: { deposit, depositType, returnAddress, orderId } }
) => ({
hasActiveShift: true,
shiftDepositAddress: deposit,
shiftCoinType: depositType,
shiftReturnAddress: returnAddress,
shiftOrderId: orderId,
shiftState: statuses.NO_DEPOSITS,
}),
[types.PREPARE_SHAPE_SHIFT_FAIL]: (state, { data: error }) => ({
error: error.message,
}),
[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,
};
},
[types.CLEAR_SHAPE_SHIFT]: () => ({
[actions.CLEAR_SHAPE_SHIFT]: (state: ShapeShiftState): ShapeShiftState => ({
...state,
loading: false,
updating: false,
hasActiveShift: false,
@ -83,21 +204,38 @@ export default handleActions(
shiftReturnAddress: undefined,
shiftCoinType: undefined,
shiftOrderId: undefined,
originCoin: "BTC",
originCoin: state.shiftSupportedCoins[0],
}),
[types.GET_ACTIVE_SHIFT_START]: () => ({
[actions.GET_ACTIVE_SHIFT_START]: (
state: ShapeShiftState
): ShapeShiftState => ({
...state,
error: undefined,
updating: true,
}),
[types.GET_ACTIVE_SHIFT_SUCCESS]: (state, { data: status }) => ({
updating: false,
shiftState: status,
}),
[types.GET_ACTIVE_SHIFT_FAIL]: (state, { data: error }) => ({
updating: false,
error: error.message,
}),
[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
);

View file

@ -0,0 +1,3 @@
export type FormikActions = {
setSubmitting: boolean => mixed,
};

View file

@ -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];