Swap Part 4 (#101)
### Re-implements: * min/max validators on initial currency swap selection * polling of order status * timer that persists across refreshes via localStorage (computed based on `createdTime` and `validFor` amount) * swap persists across refreshes once order is created. * various type refactors ### New additions: * *SimpleButton* (can be PRd separately on request) * clear loading state after order create (via SimpleButton and font-awesome) * buffers for non-BTC swaps (bity does not actually accept 0.01 BTC worth of ETH as they claim they do in their JSON response, so a magic number of 10% is added to the minimum).
This commit is contained in:
parent
e3505fd958
commit
a66337ac0a
|
@ -2,11 +2,12 @@
|
|||
|
||||
/*** Shared types ***/
|
||||
export type NOTIFICATION_LEVEL = 'danger' | 'warning' | 'success' | 'info';
|
||||
export type INFINITY = 'infinity';
|
||||
|
||||
export type Notification = {
|
||||
level: NOTIFICATION_LEVEL,
|
||||
msg: string,
|
||||
duration?: number
|
||||
duration?: number | INFINITY
|
||||
};
|
||||
|
||||
/*** Show Notification ***/
|
||||
|
|
|
@ -1,10 +1,27 @@
|
|||
// @flow
|
||||
|
||||
/*** Change Step ***/
|
||||
export type ChangeStepSwapAction = {
|
||||
type: 'SWAP_STEP',
|
||||
value: number
|
||||
};
|
||||
import type {
|
||||
OriginKindSwapAction,
|
||||
DestinationKindSwapAction,
|
||||
OriginAmountSwapAction,
|
||||
DestinationAmountSwapAction,
|
||||
LoadBityRatesSucceededSwapAction,
|
||||
DestinationAddressSwapAction,
|
||||
BityOrderCreateSucceededSwapAction,
|
||||
BityOrderCreateRequestedSwapAction,
|
||||
OrderStatusSucceededSwapAction,
|
||||
ChangeStepSwapAction,
|
||||
Pairs,
|
||||
RestartSwapAction,
|
||||
LoadBityRatesRequestedSwapAction,
|
||||
StopLoadBityRatesSwapAction,
|
||||
BityOrderResponse,
|
||||
BityOrderPostResponse,
|
||||
OrderStatusRequestedSwapAction,
|
||||
StopOrderTimerSwapAction,
|
||||
StartOrderTimerSwapAction,
|
||||
StartPollBityOrderStatusAction,
|
||||
StopPollBityOrderStatusAction
|
||||
} from './swapTypes';
|
||||
|
||||
export function changeStepSwap(value: number): ChangeStepSwapAction {
|
||||
return {
|
||||
|
@ -13,25 +30,6 @@ export function changeStepSwap(value: number): ChangeStepSwapAction {
|
|||
};
|
||||
}
|
||||
|
||||
/*** Change Reference Number ***/
|
||||
export type ReferenceNumberSwapAction = {
|
||||
type: 'SWAP_REFERENCE_NUMBER',
|
||||
value: string
|
||||
};
|
||||
|
||||
export function referenceNumberSwap(value: string): ReferenceNumberSwapAction {
|
||||
return {
|
||||
type: 'SWAP_REFERENCE_NUMBER',
|
||||
value
|
||||
};
|
||||
}
|
||||
|
||||
/*** Change Origin Kind ***/
|
||||
export type OriginKindSwapAction = {
|
||||
type: 'SWAP_ORIGIN_KIND',
|
||||
value: string
|
||||
};
|
||||
|
||||
export function originKindSwap(value: string): OriginKindSwapAction {
|
||||
return {
|
||||
type: 'SWAP_ORIGIN_KIND',
|
||||
|
@ -39,12 +37,6 @@ export function originKindSwap(value: string): OriginKindSwapAction {
|
|||
};
|
||||
}
|
||||
|
||||
/*** Change Destination Kind ***/
|
||||
export type DestinationKindSwapAction = {
|
||||
type: 'SWAP_DESTINATION_KIND',
|
||||
value: string
|
||||
};
|
||||
|
||||
export function destinationKindSwap(value: string): DestinationKindSwapAction {
|
||||
return {
|
||||
type: 'SWAP_DESTINATION_KIND',
|
||||
|
@ -52,12 +44,6 @@ export function destinationKindSwap(value: string): DestinationKindSwapAction {
|
|||
};
|
||||
}
|
||||
|
||||
/*** Change Origin Amount ***/
|
||||
export type OriginAmountSwapAction = {
|
||||
type: 'SWAP_ORIGIN_AMOUNT',
|
||||
value: ?number
|
||||
};
|
||||
|
||||
export function originAmountSwap(value: ?number): OriginAmountSwapAction {
|
||||
return {
|
||||
type: 'SWAP_ORIGIN_AMOUNT',
|
||||
|
@ -65,12 +51,6 @@ export function originAmountSwap(value: ?number): OriginAmountSwapAction {
|
|||
};
|
||||
}
|
||||
|
||||
/*** Change Destination Amount ***/
|
||||
export type DestinationAmountSwapAction = {
|
||||
type: 'SWAP_DESTINATION_AMOUNT',
|
||||
value: ?number
|
||||
};
|
||||
|
||||
export function destinationAmountSwap(
|
||||
value: ?number
|
||||
): DestinationAmountSwapAction {
|
||||
|
@ -80,32 +60,15 @@ export function destinationAmountSwap(
|
|||
};
|
||||
}
|
||||
|
||||
/*** Update Bity Rates ***/
|
||||
export type Pairs = {
|
||||
ETHBTC: number,
|
||||
ETHREP: number,
|
||||
BTCETH: number,
|
||||
BTCREP: number
|
||||
};
|
||||
|
||||
export type BityRatesSwapAction = {
|
||||
type: 'SWAP_UPDATE_BITY_RATES',
|
||||
export function loadBityRatesSucceededSwap(
|
||||
value: Pairs
|
||||
};
|
||||
|
||||
export function updateBityRatesSwap(value: Pairs): BityRatesSwapAction {
|
||||
): LoadBityRatesSucceededSwapAction {
|
||||
return {
|
||||
type: 'SWAP_UPDATE_BITY_RATES',
|
||||
type: 'SWAP_LOAD_BITY_RATES_SUCCEEDED',
|
||||
value
|
||||
};
|
||||
}
|
||||
|
||||
/*** Change Destination Address ***/
|
||||
export type DestinationAddressSwapAction = {
|
||||
type: 'SWAP_DESTINATION_ADDRESS',
|
||||
value: ?string
|
||||
};
|
||||
|
||||
export function destinationAddressSwap(
|
||||
value: ?string
|
||||
): DestinationAddressSwapAction {
|
||||
|
@ -115,49 +78,92 @@ export function destinationAddressSwap(
|
|||
};
|
||||
}
|
||||
|
||||
/*** Restart ***/
|
||||
export type RestartSwapAction = {
|
||||
type: 'SWAP_RESTART'
|
||||
};
|
||||
|
||||
export function restartSwap(): RestartSwapAction {
|
||||
return {
|
||||
type: 'SWAP_RESTART'
|
||||
};
|
||||
}
|
||||
|
||||
/*** Load Bity Rates ***/
|
||||
export type LoadBityRatesSwapAction = {
|
||||
type: 'SWAP_LOAD_BITY_RATES'
|
||||
};
|
||||
|
||||
export function loadBityRatesSwap(): LoadBityRatesSwapAction {
|
||||
export function loadBityRatesRequestedSwap(): LoadBityRatesRequestedSwapAction {
|
||||
return {
|
||||
type: 'SWAP_LOAD_BITY_RATES'
|
||||
type: 'SWAP_LOAD_BITY_RATES_REQUESTED'
|
||||
};
|
||||
}
|
||||
|
||||
/*** Stop Loading Bity Rates ***/
|
||||
export type StopLoadBityRatesSwapAction = {
|
||||
type: 'SWAP_STOP_LOAD_BITY_RATES'
|
||||
};
|
||||
|
||||
export function stopLoadBityRatesSwap(): StopLoadBityRatesSwapAction {
|
||||
return {
|
||||
type: 'SWAP_STOP_LOAD_BITY_RATES'
|
||||
};
|
||||
}
|
||||
|
||||
/*** Action Type Union ***/
|
||||
export type SwapAction =
|
||||
| ChangeStepSwapAction
|
||||
| ReferenceNumberSwapAction
|
||||
| OriginKindSwapAction
|
||||
| DestinationKindSwapAction
|
||||
| OriginAmountSwapAction
|
||||
| DestinationAmountSwapAction
|
||||
| BityRatesSwapAction
|
||||
| DestinationAddressSwapAction
|
||||
| RestartSwapAction
|
||||
| LoadBityRatesSwapAction
|
||||
| StopLoadBityRatesSwapAction;
|
||||
export function orderTimeSwap(value: number) {
|
||||
return {
|
||||
type: 'SWAP_ORDER_TIME',
|
||||
value
|
||||
};
|
||||
}
|
||||
|
||||
export function bityOrderCreateSucceededSwap(
|
||||
payload: BityOrderPostResponse
|
||||
): BityOrderCreateSucceededSwapAction {
|
||||
return {
|
||||
type: 'SWAP_BITY_ORDER_CREATE_SUCCEEDED',
|
||||
payload
|
||||
};
|
||||
}
|
||||
|
||||
export function bityOrderCreateRequestedSwap(
|
||||
amount: number,
|
||||
destinationAddress: string,
|
||||
pair: string,
|
||||
mode: number = 0
|
||||
): BityOrderCreateRequestedSwapAction {
|
||||
return {
|
||||
type: 'SWAP_ORDER_CREATE_REQUESTED',
|
||||
payload: {
|
||||
amount,
|
||||
destinationAddress,
|
||||
pair,
|
||||
mode
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export function orderStatusSucceededSwap(
|
||||
payload: BityOrderResponse
|
||||
): OrderStatusSucceededSwapAction {
|
||||
return {
|
||||
type: 'SWAP_BITY_ORDER_STATUS_SUCCEEDED',
|
||||
payload
|
||||
};
|
||||
}
|
||||
|
||||
export function orderStatusRequestedSwap(): OrderStatusRequestedSwapAction {
|
||||
return {
|
||||
type: 'SWAP_BITY_ORDER_STATUS_REQUESTED'
|
||||
};
|
||||
}
|
||||
|
||||
export function startOrderTimerSwap(): StartOrderTimerSwapAction {
|
||||
return {
|
||||
type: 'SWAP_ORDER_START_TIMER'
|
||||
};
|
||||
}
|
||||
|
||||
export function stopOrderTimerSwap(): StopOrderTimerSwapAction {
|
||||
return {
|
||||
type: 'SWAP_ORDER_STOP_TIMER'
|
||||
};
|
||||
}
|
||||
|
||||
export function startPollBityOrderStatus(): StartPollBityOrderStatusAction {
|
||||
return {
|
||||
type: 'SWAP_START_POLL_BITY_ORDER_STATUS'
|
||||
};
|
||||
}
|
||||
|
||||
export function stopPollBityOrderStatus(): StopPollBityOrderStatusAction {
|
||||
return {
|
||||
type: 'SWAP_STOP_POLL_BITY_ORDER_STATUS'
|
||||
};
|
||||
}
|
||||
|
|
|
@ -0,0 +1,128 @@
|
|||
export type Pairs = {
|
||||
ETHBTC: number,
|
||||
ETHREP: number,
|
||||
BTCETH: number,
|
||||
BTCREP: number
|
||||
};
|
||||
|
||||
export type OriginKindSwapAction = {
|
||||
type: 'SWAP_ORIGIN_KIND',
|
||||
value: string
|
||||
};
|
||||
export type DestinationKindSwapAction = {
|
||||
type: 'SWAP_DESTINATION_KIND',
|
||||
value: string
|
||||
};
|
||||
export type OriginAmountSwapAction = {
|
||||
type: 'SWAP_ORIGIN_AMOUNT',
|
||||
value: ?number
|
||||
};
|
||||
export type DestinationAmountSwapAction = {
|
||||
type: 'SWAP_DESTINATION_AMOUNT',
|
||||
value: ?number
|
||||
};
|
||||
export type LoadBityRatesSucceededSwapAction = {
|
||||
type: 'SWAP_LOAD_BITY_RATES_SUCCEEDED',
|
||||
value: Pairs
|
||||
};
|
||||
export type DestinationAddressSwapAction = {
|
||||
type: 'SWAP_DESTINATION_ADDRESS',
|
||||
value: ?number
|
||||
};
|
||||
|
||||
export type RestartSwapAction = {
|
||||
type: 'SWAP_RESTART'
|
||||
};
|
||||
|
||||
export type LoadBityRatesRequestedSwapAction = {
|
||||
type: 'SWAP_LOAD_BITY_RATES_REQUESTED'
|
||||
};
|
||||
|
||||
export type ChangeStepSwapAction = {
|
||||
type: 'SWAP_STEP',
|
||||
value: number
|
||||
};
|
||||
|
||||
export type StopLoadBityRatesSwapAction = {
|
||||
type: 'SWAP_STOP_LOAD_BITY_RATES'
|
||||
};
|
||||
|
||||
export type BityOrderCreateRequestedSwapAction = {
|
||||
type: 'SWAP_ORDER_CREATE_REQUESTED',
|
||||
payload: {
|
||||
amount: number,
|
||||
destinationAddress: string,
|
||||
pair: string,
|
||||
mode: number
|
||||
}
|
||||
};
|
||||
|
||||
type BityOrderInput = {
|
||||
amount: string
|
||||
};
|
||||
|
||||
type BityOrderOutput = {
|
||||
amount: string
|
||||
};
|
||||
|
||||
export type BityOrderResponse = {
|
||||
status: string
|
||||
};
|
||||
|
||||
export type BityOrderPostResponse = BityOrderResponse & {
|
||||
payment_address: string,
|
||||
status: string,
|
||||
input: BityOrderInput,
|
||||
output: BityOrderOutput,
|
||||
timestamp_created: string,
|
||||
validFor: number
|
||||
};
|
||||
|
||||
export type BityOrderCreateSucceededSwapAction = {
|
||||
type: 'SWAP_BITY_ORDER_CREATE_SUCCEEDED',
|
||||
payload: BityOrderPostResponse
|
||||
};
|
||||
|
||||
export type OrderStatusRequestedSwapAction = {
|
||||
type: 'SWAP_BITY_ORDER_STATUS_REQUESTED',
|
||||
payload: BityOrderResponse
|
||||
};
|
||||
|
||||
export type OrderStatusSucceededSwapAction = {
|
||||
type: 'SWAP_BITY_ORDER_STATUS_SUCCEEDED',
|
||||
payload: BityOrderResponse
|
||||
};
|
||||
|
||||
export type StartOrderTimerSwapAction = {
|
||||
type: 'SWAP_ORDER_START_TIMER'
|
||||
};
|
||||
|
||||
export type StopOrderTimerSwapAction = {
|
||||
type: 'SWAP_ORDER_STOP_TIMER'
|
||||
};
|
||||
|
||||
export type StartPollBityOrderStatusAction = {
|
||||
type: 'SWAP_START_POLL_BITY_ORDER_STATUS'
|
||||
};
|
||||
|
||||
export type StopPollBityOrderStatusAction = {
|
||||
type: 'SWAP_STOP_POLL_BITY_ORDER_STATUS'
|
||||
};
|
||||
|
||||
/*** Action Type Union ***/
|
||||
export type SwapAction =
|
||||
| ChangeStepSwapAction
|
||||
| OriginKindSwapAction
|
||||
| DestinationKindSwapAction
|
||||
| OriginAmountSwapAction
|
||||
| DestinationAmountSwapAction
|
||||
| LoadBityRatesSucceededSwapAction
|
||||
| DestinationAddressSwapAction
|
||||
| RestartSwapAction
|
||||
| LoadBityRatesRequestedSwapAction
|
||||
| StopLoadBityRatesSwapAction
|
||||
| BityOrderCreateRequestedSwapAction
|
||||
| BityOrderCreateSucceededSwapAction
|
||||
| BityOrderResponse
|
||||
| OrderStatusSucceededSwapAction
|
||||
| StartPollBityOrderStatusAction;
|
|
@ -1,38 +1,18 @@
|
|||
// @flow
|
||||
import bityConfig from 'config/bity';
|
||||
import {combineAndUpper} from 'utils/formatters'
|
||||
import { checkHttpStatus, parseJSON } from './utils';
|
||||
import { combineAndUpper } from 'utils/formatters';
|
||||
|
||||
|
||||
function findRateFromBityRateList(rateObjects, pairName) {
|
||||
function findRateFromBityRateList(rateObjects, pairName: string) {
|
||||
return rateObjects.find(x => x.pair === pairName);
|
||||
}
|
||||
|
||||
// FIXME better types
|
||||
function _getRate(bityRates, origin: string, destination: string) {
|
||||
const pairName = combineAndUpper(origin, destination);
|
||||
function _getRate(bityRates, originKind: string, destinationKind: string) {
|
||||
const pairName = combineAndUpper(originKind, destinationKind);
|
||||
const rateObjects = bityRates.objects;
|
||||
return findRateFromBityRateList(rateObjects, pairName);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gives you multiple rates from Bitys API without making multiple API calls
|
||||
* @param arrayOfOriginAndDestinationDicts - [{origin: 'BTC', destination: 'ETH'}, {origin: 'BTC', destination: 'REP}]
|
||||
*/
|
||||
function getMultipleRates(arrayOfOriginAndDestinationDicts) {
|
||||
const mappedRates = {};
|
||||
return _getAllRates().then(bityRates => {
|
||||
arrayOfOriginAndDestinationDicts.forEach(each => {
|
||||
const origin = each.origin;
|
||||
const destination = each.destination;
|
||||
const pairName = combineAndUpper(origin, destination);
|
||||
const rate = _getRate(bityRates, origin, destination);
|
||||
mappedRates[pairName] = parseFloat(rate.rate_we_sell);
|
||||
});
|
||||
return mappedRates;
|
||||
});
|
||||
// TODO - catch errors
|
||||
}
|
||||
|
||||
export function getAllRates() {
|
||||
const mappedRates = {};
|
||||
return _getAllRates().then(bityRates => {
|
||||
|
@ -42,11 +22,44 @@ export function getAllRates() {
|
|||
});
|
||||
return mappedRates;
|
||||
});
|
||||
// TODO - catch errors
|
||||
}
|
||||
|
||||
export function postOrder(
|
||||
amount: number,
|
||||
destAddress: string,
|
||||
mode: number,
|
||||
pair: string
|
||||
) {
|
||||
return fetch(`${bityConfig.serverURL}/order`, {
|
||||
method: 'post',
|
||||
body: JSON.stringify({
|
||||
amount,
|
||||
destAddress,
|
||||
mode,
|
||||
pair
|
||||
}),
|
||||
headers: bityConfig.postConfig.headers
|
||||
})
|
||||
.then(checkHttpStatus)
|
||||
.then(parseJSON);
|
||||
}
|
||||
|
||||
export function getOrderStatus(orderid: string) {
|
||||
return fetch(`${bityConfig.serverURL}/status`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
orderid
|
||||
}),
|
||||
headers: bityConfig.postConfig.headers
|
||||
})
|
||||
.then(checkHttpStatus)
|
||||
.then(parseJSON);
|
||||
}
|
||||
|
||||
function _getAllRates() {
|
||||
return fetch(`${bityConfig.bityAPI}/v1/rate2/`).then(r => r.json());
|
||||
return fetch(`${bityConfig.bityAPI}/v1/rate2/`)
|
||||
.then(checkHttpStatus)
|
||||
.then(parseJSON);
|
||||
}
|
||||
|
||||
function requestStatus() {}
|
||||
function requestOrderStatus() {}
|
||||
|
|
|
@ -1,117 +1,13 @@
|
|||
// Request utils,
|
||||
// feel free to replace with your code
|
||||
// (get, post are used in ApiServices)
|
||||
|
||||
import { getLocalToken } from 'api/AuthSvc';
|
||||
import config from 'config';
|
||||
|
||||
window.BASE_API = config.BASE_API;
|
||||
|
||||
function requestWrapper(method) {
|
||||
return async function(url, data = null, params = {}) {
|
||||
if (method === 'GET') {
|
||||
// is it a GET?
|
||||
// GET doesn't have data
|
||||
params = data;
|
||||
data = null;
|
||||
} else if (data === Object(data)) {
|
||||
// (data === Object(data)) === _.isObject(data)
|
||||
data = JSON.stringify(data);
|
||||
} else {
|
||||
throw new Error(`XHR invalid, check ${method} on ${url}`);
|
||||
}
|
||||
|
||||
// default params for fetch = method + (Content-Type)
|
||||
let defaults = {
|
||||
method: method,
|
||||
headers: {
|
||||
'Content-Type': 'application/json; charset=UTF-8'
|
||||
}
|
||||
};
|
||||
|
||||
// check that req url is relative and request was sent to our domain
|
||||
if (url.match(/^https?:\/\//gi) > -1) {
|
||||
let token = getLocalToken();
|
||||
if (token) {
|
||||
defaults.headers['Authorization'] = `JWT ${token}`;
|
||||
}
|
||||
url = window.BASE_API + url;
|
||||
}
|
||||
|
||||
if (data) {
|
||||
defaults.body = data;
|
||||
}
|
||||
|
||||
let paramsObj = {
|
||||
...defaults,
|
||||
headers: { ...params, ...defaults.headers }
|
||||
};
|
||||
|
||||
return await fetch(url, paramsObj).then(parseJSON).catch(err => {
|
||||
console.error(err);
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
// middlewares
|
||||
// parse fetch json, add ok property and return request result
|
||||
|
||||
/**
|
||||
* 1. parse response
|
||||
* 2. add "ok" property to result
|
||||
* 3. return request result
|
||||
* @param {Object} res - response from server
|
||||
* @return {Object} response result with "ok" property
|
||||
*/
|
||||
async function parseJSON(res) {
|
||||
let json;
|
||||
try {
|
||||
json = await res.json();
|
||||
} catch (e) {
|
||||
return { data: {}, ok: false };
|
||||
}
|
||||
|
||||
// simplest validation ever, ahah :)
|
||||
if (!res.ok) {
|
||||
return { data: json, ok: false };
|
||||
}
|
||||
// resultOK - is a function with side effects
|
||||
// It removes ok property from result object
|
||||
return { data: json, ok: true };
|
||||
}
|
||||
|
||||
export const get = requestWrapper('GET');
|
||||
export const post = requestWrapper('POST');
|
||||
export const put = requestWrapper('PUT');
|
||||
export const patch = requestWrapper('PATCH');
|
||||
export const del = requestWrapper('DELETE');
|
||||
|
||||
// USAGE:
|
||||
// get('https://www.google.com', {
|
||||
// Authorization: 'JWT LOL',
|
||||
// headers: {
|
||||
// 'Content-Type': 'text/html'
|
||||
// }
|
||||
// })
|
||||
|
||||
// FUNCTION WITH SIDE-EFFECTS
|
||||
/**
|
||||
* `parseJSON()` adds property "ok"
|
||||
* that identicates that response is OK
|
||||
*
|
||||
* `resultOK`removes result.ok from result and returns "ok" property
|
||||
* It widely used in `/actions/*`
|
||||
* for choosing action to dispatch after request to API
|
||||
*
|
||||
* @param {Object} result - response result that
|
||||
* @return {bool} - indicates was request successful or not
|
||||
*/
|
||||
export function resultOK(result) {
|
||||
if (result) {
|
||||
let ok = result.ok;
|
||||
delete result.ok;
|
||||
return ok; //look at parseJSON
|
||||
export function checkHttpStatus(response) {
|
||||
if (response.status >= 200 && response.status < 300) {
|
||||
return response;
|
||||
} else {
|
||||
return false;
|
||||
let error = new Error(response.statusText);
|
||||
error.response = response;
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
export function parseJSON(response) {
|
||||
return response.json();
|
||||
}
|
||||
|
|
|
@ -74,10 +74,11 @@ export default class TabsOptions extends Component {
|
|||
<ul className="Navigation-links">
|
||||
{tabs.map((object, i) => {
|
||||
// if the window pathname is the same or similar to the tab objects name, set the active toggle
|
||||
const activeOrNot = location.pathname === object.link ||
|
||||
const activeOrNot =
|
||||
location.pathname === object.link ||
|
||||
location.pathname.substring(1) === object.link
|
||||
? 'is-active'
|
||||
: '';
|
||||
? 'is-active'
|
||||
: '';
|
||||
return (
|
||||
<li
|
||||
className={'Navigation-links-item'}
|
||||
|
|
|
@ -0,0 +1,60 @@
|
|||
// @flow
|
||||
import React, { Component } from 'react';
|
||||
|
||||
const DEFAULT_BUTTON_TYPE = 'primary';
|
||||
const DEFAULT_BUTTON_SIZE = 'lg';
|
||||
|
||||
const Spinner = () => {
|
||||
return <i className="fa fa-spinner fa-spin fa-fw" />;
|
||||
};
|
||||
|
||||
type ButtonType =
|
||||
| 'default'
|
||||
| 'primary'
|
||||
| 'success'
|
||||
| 'info'
|
||||
| 'warning'
|
||||
| 'danger';
|
||||
type ButtonSize = 'lg' | 'sm' | 'xs';
|
||||
|
||||
type Props = {
|
||||
onClick: () => any,
|
||||
text: string,
|
||||
loading?: boolean,
|
||||
disabled?: boolean,
|
||||
loadingText?: string,
|
||||
size?: ButtonSize,
|
||||
type?: ButtonType
|
||||
};
|
||||
|
||||
export default class SimpleButton extends Component {
|
||||
props: Props;
|
||||
|
||||
computedClass = () => {
|
||||
return `btn btn-${this.props.size || DEFAULT_BUTTON_TYPE} btn-${this.props
|
||||
.type || DEFAULT_BUTTON_SIZE}`;
|
||||
};
|
||||
|
||||
render() {
|
||||
let { loading, disabled, loadingText, text, onClick } = this.props;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<button
|
||||
onClick={onClick}
|
||||
disabled={loading || disabled}
|
||||
className={this.computedClass()}
|
||||
>
|
||||
{loading
|
||||
? <div>
|
||||
<Spinner />
|
||||
{` ${loadingText || text}`}
|
||||
</div>
|
||||
: <div>
|
||||
{text}
|
||||
</div>}
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -1,18 +1,39 @@
|
|||
export default {
|
||||
serverURL: 'https://bity.myetherapi.com',
|
||||
bityAPI: 'https://bity.com/api',
|
||||
decimals: 6,
|
||||
ethExplorer: 'https://etherscan.io/tx/[[txHash]]',
|
||||
btcExplorer: 'https://blockchain.info/tx/[[txHash]]',
|
||||
validStatus: ['RCVE', 'FILL', 'CONF', 'EXEC'],
|
||||
invalidStatus: ['CANC'],
|
||||
mainPairs: ['REP', 'ETH'],
|
||||
min: 0.01,
|
||||
max: 3,
|
||||
priceLoaded: false,
|
||||
// while Bity is supposedly OK with any order that is at least 0.01 BTC Worth, the order will fail if you send 0.01 BTC worth of ETH.
|
||||
// This is a bad magic number, but will suffice for now
|
||||
ETHBuffer: 0.1, // percent higher/lower than 0.01 BTC worth
|
||||
REPBuffer: 0.2, // percent higher/lower than 0.01 BTC worth
|
||||
BTCMin: 0.01,
|
||||
BTCMax: 3,
|
||||
ETHMin: function(BTCETHRate: number) {
|
||||
const ETHMin = BTCETHRate * this.BTCMin;
|
||||
const ETHMinWithPadding = ETHMin + ETHMin * this.ETHBuffer;
|
||||
return ETHMinWithPadding;
|
||||
},
|
||||
ETHMax: function(BTCETHRate: number) {
|
||||
const ETHMax = BTCETHRate * this.BTCMax;
|
||||
const ETHMaxWithPadding = ETHMax - ETHMax * this.ETHBuffer;
|
||||
return ETHMaxWithPadding;
|
||||
},
|
||||
REPMin: function(BTCREPRate: number) {
|
||||
const REPMin = BTCREPRate * this.BTCMin;
|
||||
const REPMinWithPadding = REPMin + REPMin * this.REPBuffer;
|
||||
return REPMinWithPadding;
|
||||
},
|
||||
REPMax: function(BTCREPRate: number) {
|
||||
const REPMax = BTCREPRate * this.BTCMax;
|
||||
const REPMaxWithPadding = REPMax - REPMax * this.ETHBuffer;
|
||||
return REPMaxWithPadding;
|
||||
},
|
||||
postConfig: {
|
||||
headers: {
|
||||
'Content-Type': 'application/json; charse:UTF-8'
|
||||
'Content-Type': 'application/json; charset:UTF-8'
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
|
@ -1,155 +0,0 @@
|
|||
import React, { Component } from 'react';
|
||||
import translate from 'translations';
|
||||
import { combineAndUpper } from 'utils/formatters';
|
||||
import SimpleDropDown from 'components/ui/SimpleDropdown';
|
||||
import type {
|
||||
OriginKindSwapAction,
|
||||
DestinationKindSwapAction,
|
||||
OriginAmountSwapAction,
|
||||
DestinationAmountSwapAction,
|
||||
ChangeStepSwapAction
|
||||
} from 'actions/swap';
|
||||
|
||||
export type StateProps = {
|
||||
bityRates: {},
|
||||
originAmount: ?number,
|
||||
destinationAmount: ?number,
|
||||
originKind: string,
|
||||
destinationKind: string,
|
||||
destinationKindOptions: String[],
|
||||
originKindOptions: String[]
|
||||
};
|
||||
|
||||
export type ActionProps = {
|
||||
originKindSwap: (value: string) => OriginKindSwapAction,
|
||||
destinationKindSwap: (value: string) => DestinationKindSwapAction,
|
||||
originAmountSwap: (value: ?number) => OriginAmountSwapAction,
|
||||
destinationAmountSwap: (value: ?number) => DestinationAmountSwapAction,
|
||||
changeStepSwap: () => ChangeStepSwapAction
|
||||
};
|
||||
|
||||
export default class CurrencySwap extends Component {
|
||||
props: StateProps & ActionProps;
|
||||
|
||||
state = {
|
||||
disabled: false
|
||||
};
|
||||
|
||||
onClickStartSwap = () => {
|
||||
this.props.changeStepSwap(2);
|
||||
};
|
||||
|
||||
setOriginAndDestinationToNull = () => {
|
||||
this.props.originAmountSwap(null);
|
||||
this.props.destinationAmountSwap(null);
|
||||
};
|
||||
|
||||
onChangeOriginAmount = (event: SyntheticInputEvent) => {
|
||||
const amount = event.target.value;
|
||||
let originAmountAsNumber = parseFloat(amount);
|
||||
if (originAmountAsNumber) {
|
||||
let pairName = combineAndUpper(
|
||||
this.props.originKind,
|
||||
this.props.destinationKind
|
||||
);
|
||||
let bityRate = this.props.bityRates[pairName];
|
||||
this.props.originAmountSwap(originAmountAsNumber);
|
||||
this.props.destinationAmountSwap(originAmountAsNumber * bityRate);
|
||||
} else {
|
||||
this.setOriginAndDestinationToNull();
|
||||
}
|
||||
};
|
||||
|
||||
onChangeDestinationAmount = (event: SyntheticInputEvent) => {
|
||||
const amount = event.target.value;
|
||||
let destinationAmountAsNumber = parseFloat(amount);
|
||||
if (destinationAmountAsNumber) {
|
||||
this.props.destinationAmountSwap(destinationAmountAsNumber);
|
||||
let pairName = combineAndUpper(
|
||||
this.props.destinationKind,
|
||||
this.props.originKind
|
||||
);
|
||||
let bityRate = this.props.bityRates[pairName];
|
||||
this.props.originAmountSwap(destinationAmountAsNumber * bityRate);
|
||||
} else {
|
||||
this.setOriginAndDestinationToNull();
|
||||
}
|
||||
};
|
||||
|
||||
onChangeDestinationKind = (event: SyntheticInputEvent) => {
|
||||
let newDestinationKind = event.target.value;
|
||||
this.props.destinationKindSwap(newDestinationKind);
|
||||
};
|
||||
|
||||
onChangeOriginKind = (event: SyntheticInputEvent) => {
|
||||
let newOriginKind = event.target.value;
|
||||
this.props.originKindSwap(newOriginKind);
|
||||
};
|
||||
|
||||
render() {
|
||||
const {
|
||||
originAmount,
|
||||
destinationAmount,
|
||||
originKind,
|
||||
destinationKind,
|
||||
destinationKindOptions,
|
||||
originKindOptions
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<article className="swap-panel">
|
||||
<h1>
|
||||
{translate('SWAP_init_1')}
|
||||
</h1>
|
||||
<input
|
||||
className={`form-control ${this.props.originAmount !== '' &&
|
||||
this.props.originAmount > 0
|
||||
? 'is-valid'
|
||||
: 'is-invalid'}`}
|
||||
type="number"
|
||||
placeholder="Amount"
|
||||
value={originAmount || ''}
|
||||
onChange={this.onChangeOriginAmount}
|
||||
/>
|
||||
|
||||
<SimpleDropDown
|
||||
value={originKind}
|
||||
onChange={this.onChangeOriginKind.bind(this)}
|
||||
options={originKindOptions}
|
||||
/>
|
||||
|
||||
<h1>
|
||||
{translate('SWAP_init_2')}
|
||||
</h1>
|
||||
|
||||
<input
|
||||
className={`form-control ${this.props.destinationAmount !== '' &&
|
||||
this.props.destinationAmount > 0
|
||||
? 'is-valid'
|
||||
: 'is-invalid'}`}
|
||||
type="number"
|
||||
placeholder="Amount"
|
||||
value={destinationAmount || ''}
|
||||
onChange={this.onChangeDestinationAmount}
|
||||
/>
|
||||
<SimpleDropDown
|
||||
value={destinationKind}
|
||||
onChange={this.onChangeDestinationKind}
|
||||
options={destinationKindOptions}
|
||||
/>
|
||||
|
||||
<div className="col-xs-12 clearfix text-center">
|
||||
<button
|
||||
disabled={this.state.disabled}
|
||||
onClick={this.onClickStartSwap}
|
||||
className="btn btn-info btn-lg"
|
||||
>
|
||||
<span>
|
||||
{translate('SWAP_init_CTA')}
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</article>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,224 @@
|
|||
import React, { Component } from 'react';
|
||||
import translate from 'translations';
|
||||
import { combineAndUpper } from 'utils/formatters';
|
||||
import SimpleDropDown from 'components/ui/SimpleDropdown';
|
||||
import SimpleButton from 'components/ui/SimpleButton';
|
||||
import type {
|
||||
OriginKindSwapAction,
|
||||
DestinationKindSwapAction,
|
||||
OriginAmountSwapAction,
|
||||
DestinationAmountSwapAction,
|
||||
ChangeStepSwapAction
|
||||
} from 'actions/swapTypes';
|
||||
import bityConfig from 'config/bity';
|
||||
import { toFixedIfLarger } from 'utils/formatters';
|
||||
|
||||
export type StateProps = {
|
||||
bityRates: {},
|
||||
originAmount: ?number,
|
||||
destinationAmount: ?number,
|
||||
originKind: string,
|
||||
destinationKind: string,
|
||||
destinationKindOptions: String[],
|
||||
originKindOptions: String[]
|
||||
};
|
||||
|
||||
export type ActionProps = {
|
||||
originKindSwap: (value: string) => OriginKindSwapAction,
|
||||
destinationKindSwap: (value: string) => DestinationKindSwapAction,
|
||||
originAmountSwap: (value: ?number) => OriginAmountSwapAction,
|
||||
destinationAmountSwap: (value: ?number) => DestinationAmountSwapAction,
|
||||
changeStepSwap: () => ChangeStepSwapAction,
|
||||
showNotification: Function
|
||||
};
|
||||
|
||||
export default class CurrencySwap extends Component {
|
||||
props: StateProps & ActionProps;
|
||||
|
||||
state = {
|
||||
disabled: true,
|
||||
showedMinMaxError: false
|
||||
};
|
||||
|
||||
isMinMaxValid = (amount, kind) => {
|
||||
let bityMin;
|
||||
let bityMax;
|
||||
if (kind !== 'BTC') {
|
||||
const bityPairRate = this.props.bityRates['BTC' + kind];
|
||||
bityMin = bityConfig[kind + 'Min'](bityPairRate);
|
||||
bityMax = bityConfig[kind + 'Max'](bityPairRate);
|
||||
} else {
|
||||
bityMin = bityConfig.BTCMin;
|
||||
bityMax = bityConfig.BTCMax;
|
||||
}
|
||||
let higherThanMin = amount >= bityMin;
|
||||
let lowerThanMax = amount <= bityMax;
|
||||
return higherThanMin && lowerThanMax;
|
||||
};
|
||||
|
||||
isDisabled = (originAmount, originKind, destinationAmount) => {
|
||||
const hasOriginAmountAndDestinationAmount =
|
||||
originAmount && destinationAmount;
|
||||
const minMaxIsValid = this.isMinMaxValid(originAmount, originKind);
|
||||
return !(hasOriginAmountAndDestinationAmount && minMaxIsValid);
|
||||
};
|
||||
|
||||
setDisabled(originAmount, originKind, destinationAmount) {
|
||||
const disabled = this.isDisabled(
|
||||
originAmount,
|
||||
originKind,
|
||||
destinationAmount
|
||||
);
|
||||
|
||||
if (disabled && originAmount && !this.state.showedMinMaxError) {
|
||||
const { bityRates } = this.props;
|
||||
const ETHMin = bityConfig.ETHMin(bityRates.BTCETH);
|
||||
const ETHMax = bityConfig.ETHMax(bityRates.BTCETH);
|
||||
const REPMin = bityConfig.REPMax(bityRates.BTCREP);
|
||||
|
||||
const notificationMessage = `
|
||||
Minimum amount ${bityConfig.BTCMin} BTC,
|
||||
${toFixedIfLarger(ETHMin, 3)} ETH.
|
||||
Max amount ${bityConfig.BTCMax} BTC,
|
||||
${toFixedIfLarger(ETHMax, 3)} ETH, or
|
||||
${toFixedIfLarger(REPMin, 3)} REP
|
||||
`;
|
||||
|
||||
this.setState(
|
||||
{
|
||||
disabled: disabled,
|
||||
showedMinMaxError: true
|
||||
},
|
||||
() => {
|
||||
this.props.showNotification('danger', notificationMessage, 10000);
|
||||
}
|
||||
);
|
||||
} else {
|
||||
this.setState({
|
||||
disabled: disabled
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
onClickStartSwap = () => {
|
||||
this.props.changeStepSwap(2);
|
||||
};
|
||||
|
||||
setOriginAndDestinationToNull = () => {
|
||||
this.props.originAmountSwap(null);
|
||||
this.props.destinationAmountSwap(null);
|
||||
this.setDisabled(null, this.props.originKind, null);
|
||||
};
|
||||
|
||||
onChangeOriginAmount = (event: SyntheticInputEvent) => {
|
||||
const { destinationKind, originKind } = this.props;
|
||||
const amount = event.target.value;
|
||||
let originAmountAsNumber = parseFloat(amount);
|
||||
if (originAmountAsNumber || originAmountAsNumber === 0) {
|
||||
let pairName = combineAndUpper(originKind, destinationKind);
|
||||
let bityRate = this.props.bityRates[pairName];
|
||||
this.props.originAmountSwap(originAmountAsNumber);
|
||||
let destinationAmount = originAmountAsNumber * bityRate;
|
||||
this.props.destinationAmountSwap(destinationAmount);
|
||||
this.setDisabled(originAmountAsNumber, originKind, destinationAmount);
|
||||
} else {
|
||||
this.setOriginAndDestinationToNull();
|
||||
}
|
||||
};
|
||||
|
||||
onChangeDestinationAmount = (event: SyntheticInputEvent) => {
|
||||
const { destinationKind, originKind } = this.props;
|
||||
const amount = event.target.value;
|
||||
let destinationAmountAsNumber = parseFloat(amount);
|
||||
if (destinationAmountAsNumber || destinationAmountAsNumber === 0) {
|
||||
this.props.destinationAmountSwap(destinationAmountAsNumber);
|
||||
let pairNameReversed = combineAndUpper(destinationKind, originKind);
|
||||
let bityRate = this.props.bityRates[pairNameReversed];
|
||||
let originAmount = destinationAmountAsNumber * bityRate;
|
||||
this.props.originAmountSwap(originAmount, originKind);
|
||||
this.setDisabled(originAmount, originKind, destinationAmountAsNumber);
|
||||
} else {
|
||||
this.setOriginAndDestinationToNull();
|
||||
}
|
||||
};
|
||||
|
||||
onChangeDestinationKind = (event: SyntheticInputEvent) => {
|
||||
let newDestinationKind = event.target.value;
|
||||
this.props.destinationKindSwap(newDestinationKind);
|
||||
};
|
||||
|
||||
onChangeOriginKind = (event: SyntheticInputEvent) => {
|
||||
let newOriginKind = event.target.value;
|
||||
this.props.originKindSwap(newOriginKind);
|
||||
};
|
||||
|
||||
render() {
|
||||
const {
|
||||
originAmount,
|
||||
destinationAmount,
|
||||
originKind,
|
||||
destinationKind,
|
||||
destinationKindOptions,
|
||||
originKindOptions
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<article className="swap-panel">
|
||||
<h1>
|
||||
{translate('SWAP_init_1')}
|
||||
</h1>
|
||||
<input
|
||||
className={`form-control ${originAmount !== '' &&
|
||||
this.isMinMaxValid(originAmount, originKind)
|
||||
? 'is-valid'
|
||||
: 'is-invalid'}`}
|
||||
type="number"
|
||||
placeholder="Amount"
|
||||
value={
|
||||
parseFloat(originAmount) === 0 ? originAmount : originAmount || ''
|
||||
}
|
||||
onChange={this.onChangeOriginAmount}
|
||||
/>
|
||||
|
||||
<SimpleDropDown
|
||||
value={originKind}
|
||||
onChange={this.onChangeOriginKind.bind(this)}
|
||||
options={originKindOptions}
|
||||
/>
|
||||
|
||||
<h1>
|
||||
{translate('SWAP_init_2')}
|
||||
</h1>
|
||||
|
||||
<input
|
||||
className={`form-control ${destinationAmount !== '' &&
|
||||
this.isMinMaxValid(originAmount, originKind)
|
||||
? 'is-valid'
|
||||
: 'is-invalid'}`}
|
||||
type="number"
|
||||
placeholder="Amount"
|
||||
value={
|
||||
parseFloat(destinationAmount) === 0
|
||||
? destinationAmount
|
||||
: destinationAmount || ''
|
||||
}
|
||||
onChange={this.onChangeDestinationAmount}
|
||||
/>
|
||||
|
||||
<SimpleDropDown
|
||||
value={destinationKind}
|
||||
onChange={this.onChangeDestinationKind}
|
||||
options={destinationKindOptions}
|
||||
/>
|
||||
|
||||
<div className="col-xs-12 clearfix text-center">
|
||||
<SimpleButton
|
||||
onClick={this.onClickStartSwap}
|
||||
text={translate('SWAP_init_CTA')}
|
||||
disabled={this.state.disabled}
|
||||
/>
|
||||
</div>
|
||||
</article>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -2,7 +2,7 @@
|
|||
import React, { Component } from 'react';
|
||||
import translate from 'translations';
|
||||
import { toFixedIfLarger } from 'utils/formatters';
|
||||
import type { Pairs } from 'actions/swap';
|
||||
import type { Pairs } from 'actions/swapTypes';
|
||||
import { bityReferralURL } from 'config/data';
|
||||
import bityLogoWhite from 'assets/images/logo-bity-white.svg';
|
||||
|
|
@ -0,0 +1,83 @@
|
|||
import React, { Component } from 'react';
|
||||
import type {
|
||||
LoadBityRatesRequestedSwapAction,
|
||||
RestartSwapAction,
|
||||
StopLoadBityRatesSwapAction
|
||||
} from 'actions/swap';
|
||||
import SwapProgress from './SwapProgress';
|
||||
import PaymentInfo from './PaymentInfo';
|
||||
|
||||
type ReduxStateProps = {
|
||||
destinationAddress: string,
|
||||
destinationKind: string,
|
||||
originKind: string,
|
||||
originAmount: ?number,
|
||||
destinationAmount: ?number,
|
||||
isPostingOrder: boolean,
|
||||
reference: string,
|
||||
secondsRemaining: ?number,
|
||||
paymentAddress: ?string,
|
||||
orderStatus: ?string
|
||||
};
|
||||
|
||||
type ReduxActionProps = {
|
||||
loadBityRatesRequestedSwap: () => LoadBityRatesRequestedSwapAction,
|
||||
restartSwap: () => RestartSwapAction,
|
||||
stopLoadBityRatesSwap: () => StopLoadBityRatesSwapAction,
|
||||
startOrderTimerSwap: Function,
|
||||
startPollBityOrderStatus: Function,
|
||||
stopOrderTimerSwap: Function,
|
||||
stopPollBityOrderStatus: Function,
|
||||
showNotification: Function
|
||||
};
|
||||
|
||||
export default class PartThree extends Component {
|
||||
props: ReduxActionProps & ReduxStateProps;
|
||||
|
||||
componentDidMount() {
|
||||
this.props.startPollBityOrderStatus();
|
||||
this.props.startOrderTimerSwap();
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this.props.stopOrderTimerSwap();
|
||||
this.props.stopPollBityOrderStatus();
|
||||
}
|
||||
|
||||
render() {
|
||||
let {
|
||||
// STATE
|
||||
originAmount,
|
||||
originKind,
|
||||
destinationKind,
|
||||
paymentAddress,
|
||||
orderStatus,
|
||||
destinationAddress,
|
||||
outputTx,
|
||||
// ACTIONS
|
||||
showNotification
|
||||
} = this.props;
|
||||
|
||||
let SwapProgressProps = {
|
||||
originKind,
|
||||
destinationKind,
|
||||
orderStatus,
|
||||
showNotification,
|
||||
destinationAddress,
|
||||
outputTx
|
||||
};
|
||||
|
||||
const PaymentInfoProps = {
|
||||
originKind,
|
||||
originAmount,
|
||||
paymentAddress
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<SwapProgress {...SwapProgressProps} />
|
||||
<PaymentInfo {...PaymentInfoProps} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,34 @@
|
|||
import React, { Component } from 'react';
|
||||
import translate from 'translations';
|
||||
|
||||
export type Props = {
|
||||
originKind: string,
|
||||
originAmount: string,
|
||||
paymentAddress: string
|
||||
};
|
||||
|
||||
export default class PaymentInfo extends Component {
|
||||
props: Props;
|
||||
|
||||
render() {
|
||||
return (
|
||||
<section className="row text-center">
|
||||
<h1>
|
||||
<span>
|
||||
{translate('SWAP_order_CTA')}
|
||||
</span>
|
||||
<strong>
|
||||
{this.props.originAmount} {this.props.originKind}{' '}
|
||||
</strong>
|
||||
<span>
|
||||
{translate('SENDModal_Content_2')}
|
||||
</span>
|
||||
<br />
|
||||
<strong className="mono text-primary">
|
||||
{this.props.paymentAddress}
|
||||
</strong>
|
||||
</h1>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -4,13 +4,18 @@ import type {
|
|||
DestinationAddressSwapAction,
|
||||
ChangeStepSwapAction,
|
||||
StopLoadBityRatesSwapAction,
|
||||
ReferenceNumberSwapAction
|
||||
} from 'actions/swap';
|
||||
BityOrderCreateRequestedSwapAction
|
||||
} from 'actions/swapTypes';
|
||||
import { donationAddressMap } from 'config/data';
|
||||
import { isValidBTCAddress, isValidETHAddress } from 'libs/validators';
|
||||
import translate from 'translations';
|
||||
import { combineAndUpper } from 'utils/formatters';
|
||||
import SimpleButton from 'components/ui/SimpleButton';
|
||||
|
||||
export type StateProps = {
|
||||
isPostingOrder: boolean,
|
||||
originAmount: number,
|
||||
originKind: string,
|
||||
destinationKind: string,
|
||||
destinationAddress: string
|
||||
};
|
||||
|
@ -19,7 +24,12 @@ export type ActionProps = {
|
|||
destinationAddressSwap: (value: ?string) => DestinationAddressSwapAction,
|
||||
changeStepSwap: (value: number) => ChangeStepSwapAction,
|
||||
stopLoadBityRatesSwap: () => StopLoadBityRatesSwapAction,
|
||||
referenceNumberSwap: (value: string) => ReferenceNumberSwapAction
|
||||
bityOrderCreateRequestedSwap: (
|
||||
amount: number,
|
||||
destinationAddress: string,
|
||||
pair: string,
|
||||
mode: ?number
|
||||
) => BityOrderCreateRequestedSwapAction
|
||||
};
|
||||
|
||||
export default class ReceivingAddress extends Component {
|
||||
|
@ -31,14 +41,15 @@ export default class ReceivingAddress extends Component {
|
|||
};
|
||||
|
||||
onClickPartTwoComplete = () => {
|
||||
this.props.stopLoadBityRatesSwap();
|
||||
// temporarily here for testing purposes. will live in saga
|
||||
this.props.referenceNumberSwap('');
|
||||
this.props.changeStepSwap(3);
|
||||
this.props.bityOrderCreateRequestedSwap(
|
||||
this.props.originAmount,
|
||||
this.props.destinationAddress,
|
||||
combineAndUpper(this.props.originKind, this.props.destinationKind)
|
||||
);
|
||||
};
|
||||
|
||||
render() {
|
||||
const { destinationKind, destinationAddress } = this.props;
|
||||
const { destinationKind, destinationAddress, isPostingOrder } = this.props;
|
||||
let validAddress;
|
||||
// TODO - find better pattern here once currencies move beyond BTC, ETH, REP
|
||||
if (this.props.destinationKind === 'BTC') {
|
||||
|
@ -72,15 +83,12 @@ export default class ReceivingAddress extends Component {
|
|||
</div>
|
||||
</section>
|
||||
<section className="row text-center">
|
||||
<button
|
||||
disabled={!validAddress}
|
||||
<SimpleButton
|
||||
text={translate('SWAP_start_CTA')}
|
||||
onClick={this.onClickPartTwoComplete}
|
||||
className="btn btn-primary btn-lg"
|
||||
>
|
||||
<span>
|
||||
{translate('SWAP_start_CTA')}
|
||||
</span>
|
||||
</button>
|
||||
disabled={!validAddress}
|
||||
loading={isPostingOrder}
|
||||
/>
|
||||
</section>
|
||||
</section>
|
||||
</article>
|
|
@ -1,139 +0,0 @@
|
|||
// @flow
|
||||
import React, { Component } from 'react';
|
||||
import { toFixedIfLarger } from 'utils/formatters';
|
||||
import translate from 'translations';
|
||||
import type { RestartSwapAction } from 'actions/swap';
|
||||
import bityLogo from 'assets/images/logo-bity.svg';
|
||||
import { bityReferralURL } from 'config/data';
|
||||
|
||||
export type StateProps = {
|
||||
timeRemaining: string,
|
||||
originAmount: number,
|
||||
originKind: string,
|
||||
destinationKind: string,
|
||||
destinationAmount: number,
|
||||
referenceNumber: string
|
||||
};
|
||||
|
||||
export type ActionProps = {
|
||||
restartSwap: () => RestartSwapAction
|
||||
};
|
||||
|
||||
export default class SwapInfoHeader extends Component {
|
||||
props: StateProps & ActionProps;
|
||||
|
||||
computedOriginDestinationRatio = () => {
|
||||
return toFixedIfLarger(
|
||||
this.props.destinationAmount / this.props.originAmount,
|
||||
6
|
||||
);
|
||||
};
|
||||
|
||||
isExpanded = () => {
|
||||
const { referenceNumber, timeRemaining, restartSwap } = this.props;
|
||||
return referenceNumber && timeRemaining && restartSwap;
|
||||
};
|
||||
|
||||
computedClass = () => {
|
||||
if (this.isExpanded()) {
|
||||
return 'col-sm-3 order-info';
|
||||
} else {
|
||||
return 'col-sm-4 order-info';
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
const {
|
||||
referenceNumber,
|
||||
timeRemaining,
|
||||
originAmount,
|
||||
destinationAmount,
|
||||
originKind,
|
||||
destinationKind,
|
||||
restartSwap
|
||||
} = this.props;
|
||||
return (
|
||||
<div>
|
||||
<section className="row text-center">
|
||||
<div className="col-xs-3 text-left">
|
||||
<button className="btn btn-danger btn-xs" onClick={restartSwap}>
|
||||
Start New Swap
|
||||
</button>
|
||||
</div>
|
||||
<h5 className="col-xs-6">
|
||||
{translate('SWAP_information')}
|
||||
</h5>
|
||||
<div className="col-xs-3">
|
||||
<a
|
||||
className="link"
|
||||
href={bityReferralURL}
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
>
|
||||
<img
|
||||
className="pull-right"
|
||||
src={bityLogo}
|
||||
width={100}
|
||||
height={38}
|
||||
/>
|
||||
</a>
|
||||
</div>
|
||||
</section>
|
||||
<section className="row order-info-wrap">
|
||||
{/*Amount to send */}
|
||||
{!this.isExpanded() &&
|
||||
<div className={this.computedClass()}>
|
||||
<h4>
|
||||
{` ${toFixedIfLarger(originAmount, 6)} ${originKind}`}
|
||||
</h4>
|
||||
<p>
|
||||
{translate('SEND_amount')}
|
||||
</p>
|
||||
</div>}
|
||||
|
||||
{/* Reference Number*/}
|
||||
{this.isExpanded() &&
|
||||
<div className={this.computedClass()}>
|
||||
<h4>
|
||||
{referenceNumber}
|
||||
</h4>
|
||||
<p>
|
||||
{translate('SWAP_ref_num')}
|
||||
</p>
|
||||
</div>}
|
||||
|
||||
{/*Time remaining*/}
|
||||
{this.isExpanded() &&
|
||||
<div className={this.computedClass()}>
|
||||
<h4>
|
||||
{timeRemaining}
|
||||
</h4>
|
||||
<p>
|
||||
{translate('SWAP_time')}
|
||||
</p>
|
||||
</div>}
|
||||
|
||||
{/*Amount to Receive*/}
|
||||
<div className={this.computedClass()}>
|
||||
<h4>
|
||||
{` ${toFixedIfLarger(destinationAmount, 6)} ${destinationKind}`}
|
||||
</h4>
|
||||
<p>
|
||||
{translate('SWAP_rec_amt')}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/*Your rate*/}
|
||||
<div className={this.computedClass()}>
|
||||
<h4>
|
||||
{` ${this.computedOriginDestinationRatio()} ${originKind}/${destinationKind} `}
|
||||
</h4>
|
||||
<p>
|
||||
{translate('SWAP_your_rate')}
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,164 @@
|
|||
// @flow
|
||||
import React, { Component } from 'react';
|
||||
import translate from 'translations';
|
||||
import type { RestartSwapAction } from 'actions/swapTypes';
|
||||
import bityLogo from 'assets/images/logo-bity.svg';
|
||||
import { bityReferralURL } from 'config/data';
|
||||
import { toFixedIfLarger } from 'utils/formatters';
|
||||
|
||||
export type SwapInfoHeaderTitleProps = {
|
||||
restartSwap: () => RestartSwapAction
|
||||
};
|
||||
|
||||
class SwapInfoHeaderTitle extends Component {
|
||||
props: SwapInfoHeaderTitleProps;
|
||||
|
||||
render() {
|
||||
return (
|
||||
<section className="row text-center">
|
||||
<div className="col-xs-3 text-left">
|
||||
<button
|
||||
className="btn btn-danger btn-xs"
|
||||
onClick={this.props.restartSwap}
|
||||
>
|
||||
Start New Swap
|
||||
</button>
|
||||
</div>
|
||||
<h5 className="col-xs-6">
|
||||
{translate('SWAP_information')}
|
||||
</h5>
|
||||
<div className="col-xs-3">
|
||||
<a
|
||||
className="link"
|
||||
href={bityReferralURL}
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
>
|
||||
<img
|
||||
className="pull-right"
|
||||
src={bityLogo}
|
||||
width={100}
|
||||
height={38}
|
||||
/>
|
||||
</a>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export type SwapInfoHeaderProps = {
|
||||
originAmount: number,
|
||||
originKind: string,
|
||||
destinationKind: string,
|
||||
destinationAmount: number,
|
||||
reference: string,
|
||||
secondsRemaining: ?number,
|
||||
restartSwap: () => RestartSwapAction
|
||||
};
|
||||
|
||||
export default class SwapInfoHeader extends Component {
|
||||
props: SwapInfoHeaderProps;
|
||||
|
||||
computedOriginDestinationRatio = () => {
|
||||
return this.props.destinationAmount / this.props.originAmount;
|
||||
};
|
||||
|
||||
isExpanded = () => {
|
||||
const { reference, restartSwap } = this.props;
|
||||
return reference && restartSwap;
|
||||
};
|
||||
|
||||
computedClass = () => {
|
||||
if (this.isExpanded()) {
|
||||
return 'col-sm-3 order-info';
|
||||
} else {
|
||||
return 'col-sm-4 order-info';
|
||||
}
|
||||
};
|
||||
|
||||
formattedTime = () => {
|
||||
const { secondsRemaining } = this.props;
|
||||
if (secondsRemaining || secondsRemaining === 0) {
|
||||
let minutes = Math.floor(secondsRemaining / 60);
|
||||
let seconds = secondsRemaining - minutes * 60;
|
||||
minutes = minutes < 10 ? '0' + minutes : minutes;
|
||||
seconds = seconds < 10 ? '0' + seconds : seconds;
|
||||
return minutes + ':' + seconds;
|
||||
} else {
|
||||
throw Error('secondsRemaining must be a number');
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
const {
|
||||
reference,
|
||||
originAmount,
|
||||
destinationAmount,
|
||||
originKind,
|
||||
destinationKind,
|
||||
restartSwap
|
||||
} = this.props;
|
||||
return (
|
||||
<div>
|
||||
<SwapInfoHeaderTitle restartSwap={restartSwap} />
|
||||
<section className="row order-info-wrap">
|
||||
{/*Amount to send*/}
|
||||
{!this.isExpanded() &&
|
||||
<div className={this.computedClass()}>
|
||||
<h4>
|
||||
{` ${originAmount} ${originKind}`}
|
||||
</h4>
|
||||
<p>
|
||||
{translate('SEND_amount')}
|
||||
</p>
|
||||
</div>}
|
||||
|
||||
{/*Reference Number*/}
|
||||
{this.isExpanded() &&
|
||||
<div className={this.computedClass()}>
|
||||
<h4>
|
||||
{reference}
|
||||
</h4>
|
||||
<p>
|
||||
{translate('SWAP_ref_num')}
|
||||
</p>
|
||||
</div>}
|
||||
|
||||
{/*Time remaining*/}
|
||||
{this.isExpanded() &&
|
||||
<div className={this.computedClass()}>
|
||||
<h4>
|
||||
{this.formattedTime()}
|
||||
</h4>
|
||||
<p>
|
||||
{translate('SWAP_time')}
|
||||
</p>
|
||||
</div>}
|
||||
|
||||
{/*Amount to Receive*/}
|
||||
<div className={this.computedClass()}>
|
||||
<h4>
|
||||
{` ${destinationAmount} ${destinationKind}`}
|
||||
</h4>
|
||||
<p>
|
||||
{translate('SWAP_rec_amt')}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/*Your rate*/}
|
||||
<div className={this.computedClass()}>
|
||||
<h4>
|
||||
{` ${toFixedIfLarger(
|
||||
this.computedOriginDestinationRatio()
|
||||
)} ${originKind}/${destinationKind} `}
|
||||
</h4>
|
||||
<p>
|
||||
{translate('SWAP_your_rate')}
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -1,79 +0,0 @@
|
|||
//flow
|
||||
import React, { Component } from 'react';
|
||||
import translate from 'translations';
|
||||
|
||||
export type StateProps = {
|
||||
numberOfConfirmations: number,
|
||||
destinationKind: string,
|
||||
originKind: string,
|
||||
orderStep: number
|
||||
};
|
||||
|
||||
export default class SwapProgress extends Component {
|
||||
props: StateProps;
|
||||
|
||||
computedClass(i: number) {
|
||||
const { orderStep } = this.props;
|
||||
let cssClass = 'progress-item';
|
||||
if (orderStep > i) {
|
||||
cssClass += ' progress-true';
|
||||
} else if (i === orderStep) {
|
||||
cssClass += ' progress-active';
|
||||
}
|
||||
return cssClass;
|
||||
}
|
||||
|
||||
render() {
|
||||
const { numberOfConfirmations, destinationKind, originKind } = this.props;
|
||||
return (
|
||||
<section className="row swap-progress">
|
||||
<div className="sep" />
|
||||
|
||||
<div className={this.computedClass(1)}>
|
||||
<div className="progress-circle">
|
||||
<i>1</i>
|
||||
</div>
|
||||
<p>
|
||||
{translate('SWAP_progress_1')}
|
||||
</p>
|
||||
</div>
|
||||
<div className={this.computedClass(2)}>
|
||||
<div className="progress-circle">
|
||||
<i>2</i>
|
||||
</div>
|
||||
<p>
|
||||
<span>{translate('SWAP_progress_2')}</span>
|
||||
{originKind}...
|
||||
</p>
|
||||
</div>
|
||||
<div className={this.computedClass(3)}>
|
||||
<div className="progress-circle">
|
||||
<i>3</i>
|
||||
</div>
|
||||
<p>
|
||||
{originKind} <span>{translate('SWAP_progress_3')}</span>
|
||||
</p>
|
||||
</div>
|
||||
<div className={this.computedClass(4)}>
|
||||
<div className="progress-circle">
|
||||
<i>4</i>
|
||||
</div>
|
||||
<p>
|
||||
<span>Sending your </span>
|
||||
{destinationKind}
|
||||
<br />
|
||||
<small>
|
||||
Waiting for {numberOfConfirmations} confirmations...
|
||||
</small>
|
||||
</p>
|
||||
</div>
|
||||
<div className={this.computedClass(5)}>
|
||||
<div className="progress-circle">
|
||||
<i>5</i>
|
||||
</div>
|
||||
<p>Order Complete</p>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,146 @@
|
|||
//flow
|
||||
import React, { Component } from 'react';
|
||||
import translate from 'translations';
|
||||
import bityConfig from 'config/bity';
|
||||
|
||||
export type Props = {
|
||||
destinationKind: string,
|
||||
destinationAddress: string,
|
||||
outputTx: string,
|
||||
originKind: string,
|
||||
orderStatus: string,
|
||||
// actions
|
||||
showNotification: Function
|
||||
};
|
||||
|
||||
export default class SwapProgress extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
hasShownViewTx: false
|
||||
};
|
||||
}
|
||||
|
||||
props: Props;
|
||||
|
||||
componentDidMount() {
|
||||
this.showNotification();
|
||||
}
|
||||
|
||||
showNotification = () => {
|
||||
const { hasShownViewTx } = this.state;
|
||||
const {
|
||||
destinationKind,
|
||||
outputTx,
|
||||
showNotification,
|
||||
orderStatus
|
||||
} = this.props;
|
||||
|
||||
if (orderStatus === 'FILL') {
|
||||
if (!hasShownViewTx) {
|
||||
let linkElement;
|
||||
let link;
|
||||
// everything but BTC is a token
|
||||
if (destinationKind !== 'BTC') {
|
||||
link = bityConfig.ethExplorer.replace('[[txHash]]', outputTx);
|
||||
linkElement = `<a href="${link}" target='_blank' rel='noopener'> View your transaction </a>`;
|
||||
// BTC uses a different explorer
|
||||
} else {
|
||||
link = bityConfig.btcExplorer.replace('[[txHash]]', outputTx);
|
||||
linkElement = `<a href="${link}" target='_blank' rel='noopener'> View your transaction </a>`;
|
||||
}
|
||||
this.setState({ hasShownViewTx: true }, () => {
|
||||
showNotification('success', linkElement);
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
computedClass = (step: number) => {
|
||||
const { orderStatus } = this.props;
|
||||
|
||||
let cssClass = 'progress-item';
|
||||
|
||||
switch (orderStatus) {
|
||||
case 'OPEN':
|
||||
if (step < 2) {
|
||||
return cssClass + ' progress-true';
|
||||
} else if (step === 2) {
|
||||
return cssClass + ' progress-active';
|
||||
} else {
|
||||
return cssClass;
|
||||
}
|
||||
case 'RCVE':
|
||||
if (step < 4) {
|
||||
return cssClass + ' progress-true';
|
||||
} else if (step === 4) {
|
||||
return cssClass + ' progress-active';
|
||||
} else {
|
||||
return cssClass;
|
||||
}
|
||||
case 'FILL':
|
||||
cssClass += ' progress-true';
|
||||
return cssClass;
|
||||
case 'CANC':
|
||||
return cssClass;
|
||||
default:
|
||||
return cssClass;
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
const { destinationKind, originKind } = this.props;
|
||||
|
||||
const numberOfConfirmations = originKind === 'BTC' ? '3' : '10';
|
||||
return (
|
||||
<section className="row swap-progress">
|
||||
<div className="sep" />
|
||||
|
||||
<div className={this.computedClass(1)}>
|
||||
<div className="progress-circle">
|
||||
<i>1</i>
|
||||
</div>
|
||||
<p>
|
||||
{translate('SWAP_progress_1')}
|
||||
</p>
|
||||
</div>
|
||||
<div className={this.computedClass(2)}>
|
||||
<div className="progress-circle">
|
||||
<i>2</i>
|
||||
</div>
|
||||
<p>
|
||||
<span>{translate('SWAP_progress_2')}</span>
|
||||
{originKind}...
|
||||
</p>
|
||||
</div>
|
||||
<div className={this.computedClass(3)}>
|
||||
<div className="progress-circle">
|
||||
<i>3</i>
|
||||
</div>
|
||||
<p>
|
||||
{originKind} <span>{translate('SWAP_progress_3')}</span>
|
||||
</p>
|
||||
</div>
|
||||
<div className={this.computedClass(4)}>
|
||||
<div className="progress-circle">
|
||||
<i>4</i>
|
||||
</div>
|
||||
<p>
|
||||
<span>Sending your </span>
|
||||
{destinationKind}
|
||||
<br />
|
||||
<small>
|
||||
Waiting for {numberOfConfirmations} confirmations...
|
||||
</small>
|
||||
</p>
|
||||
</div>
|
||||
<div className={this.computedClass(5)}>
|
||||
<div className="progress-circle">
|
||||
<i>5</i>
|
||||
</div>
|
||||
<p>Order Complete</p>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -1,5 +1,6 @@
|
|||
import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { showNotification } from 'actions/notifications';
|
||||
import * as swapActions from 'actions/swap';
|
||||
import type {
|
||||
ChangeStepSwapAction,
|
||||
|
@ -7,16 +8,20 @@ import type {
|
|||
DestinationKindSwapAction,
|
||||
OriginAmountSwapAction,
|
||||
DestinationAmountSwapAction,
|
||||
LoadBityRatesSwapAction,
|
||||
LoadBityRatesRequestedSwapAction,
|
||||
DestinationAddressSwapAction,
|
||||
RestartSwapAction,
|
||||
StopLoadBityRatesSwapAction
|
||||
StopLoadBityRatesSwapAction,
|
||||
BityOrderCreateRequestedSwapAction,
|
||||
StartPollBityOrderStatusAction,
|
||||
StopOrderTimerSwapAction,
|
||||
StopPollBityOrderStatusAction
|
||||
} from 'actions/swap';
|
||||
import CurrencySwap from './components/CurrencySwap';
|
||||
import CurrentRates from './components/CurrentRates';
|
||||
import ReceivingAddress from './components/ReceivingAddress';
|
||||
import SwapInfoHeader from './components/SwapInfoHeader';
|
||||
import SwapProgress from './components/SwapProgress';
|
||||
import PartThree from './components/PartThree';
|
||||
|
||||
type ReduxStateProps = {
|
||||
step: string,
|
||||
|
@ -25,16 +30,16 @@ type ReduxStateProps = {
|
|||
originKind: string,
|
||||
destinationKindOptions: String[],
|
||||
originKindOptions: String[],
|
||||
bityRates: boolean,
|
||||
bityRates: {},
|
||||
originAmount: ?number,
|
||||
destinationAmount: ?number,
|
||||
isPostingOrder: boolean,
|
||||
isFetchingRates: boolean,
|
||||
// PART 3
|
||||
referenceNumber: string,
|
||||
timeRemaining: string,
|
||||
numberOfConfirmation: number,
|
||||
orderStep: number,
|
||||
orderStarted: boolean
|
||||
bityOrder: {},
|
||||
secondsRemaining: ?number,
|
||||
paymentAddress: ?string,
|
||||
orderStatus: ?string,
|
||||
outputTx: ?string
|
||||
};
|
||||
|
||||
type ReduxActionProps = {
|
||||
|
@ -43,12 +48,19 @@ type ReduxActionProps = {
|
|||
destinationKindSwap: (value: string) => DestinationKindSwapAction,
|
||||
originAmountSwap: (value: ?number) => OriginAmountSwapAction,
|
||||
destinationAmountSwap: (value: ?number) => DestinationAmountSwapAction,
|
||||
loadBityRatesSwap: () => LoadBityRatesSwapAction,
|
||||
loadBityRatesRequestedSwap: () => LoadBityRatesRequestedSwapAction,
|
||||
destinationAddressSwap: (value: ?string) => DestinationAddressSwapAction,
|
||||
restartSwap: () => RestartSwapAction,
|
||||
stopLoadBityRatesSwap: () => StopLoadBityRatesSwapAction,
|
||||
// PART 3 (IGNORE FOR NOW)
|
||||
referenceNumberSwap: typeof swapActions.referenceNumberSwap
|
||||
bityOrderCreateRequestedSwap: (
|
||||
amount: number,
|
||||
destinationAddress: string,
|
||||
pair: string,
|
||||
mode: number
|
||||
) => BityOrderCreateRequestedSwapAction,
|
||||
startPollBityOrderStatus: () => StartPollBityOrderStatusAction,
|
||||
stopOrderTimerSwap: () => StopOrderTimerSwapAction,
|
||||
stopPollBityOrderStatus: () => StopPollBityOrderStatusAction
|
||||
};
|
||||
|
||||
class Swap extends Component {
|
||||
|
@ -56,7 +68,7 @@ class Swap extends Component {
|
|||
|
||||
componentDidMount() {
|
||||
// TODO: Use `isFetchingRates` to show a loader
|
||||
this.props.loadBityRatesSwap();
|
||||
this.props.loadBityRatesRequestedSwap();
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
|
@ -75,10 +87,12 @@ class Swap extends Component {
|
|||
originKindOptions,
|
||||
destinationAddress,
|
||||
step,
|
||||
referenceNumber,
|
||||
timeRemaining,
|
||||
numberOfConfirmations,
|
||||
orderStep,
|
||||
bityOrder,
|
||||
secondsRemaining,
|
||||
paymentAddress,
|
||||
orderStatus,
|
||||
isPostingOrder,
|
||||
outputTx,
|
||||
// ACTIONS
|
||||
restartSwap,
|
||||
stopLoadBityRatesSwap,
|
||||
|
@ -88,35 +102,44 @@ class Swap extends Component {
|
|||
originAmountSwap,
|
||||
destinationAmountSwap,
|
||||
destinationAddressSwap,
|
||||
referenceNumberSwap
|
||||
bityOrderCreateRequestedSwap,
|
||||
showNotification,
|
||||
startOrderTimerSwap,
|
||||
startPollBityOrderStatus,
|
||||
stopOrderTimerSwap,
|
||||
stopPollBityOrderStatus
|
||||
} = this.props;
|
||||
|
||||
const { reference } = bityOrder;
|
||||
|
||||
let ReceivingAddressProps = {
|
||||
isPostingOrder,
|
||||
originAmount,
|
||||
originKind,
|
||||
destinationKind,
|
||||
destinationAddressSwap,
|
||||
destinationAddress,
|
||||
stopLoadBityRatesSwap,
|
||||
changeStepSwap,
|
||||
referenceNumberSwap
|
||||
bityOrderCreateRequestedSwap
|
||||
};
|
||||
|
||||
let SwapInfoHeaderProps = {
|
||||
referenceNumber,
|
||||
timeRemaining,
|
||||
reference,
|
||||
secondsRemaining,
|
||||
originAmount,
|
||||
originKind,
|
||||
destinationKind,
|
||||
destinationAmount,
|
||||
restartSwap,
|
||||
numberOfConfirmations,
|
||||
orderStep
|
||||
orderStatus
|
||||
};
|
||||
|
||||
const { ETHBTC, ETHREP, BTCETH, BTCREP } = bityRates;
|
||||
|
||||
const CurrentRatesProps = { ETHBTC, ETHREP, BTCETH, BTCREP };
|
||||
|
||||
const CurrencySwapProps = {
|
||||
showNotification,
|
||||
bityRates,
|
||||
originAmount,
|
||||
destinationAmount,
|
||||
|
@ -131,6 +154,25 @@ class Swap extends Component {
|
|||
changeStepSwap
|
||||
};
|
||||
|
||||
const PaymentInfoProps = {
|
||||
originKind,
|
||||
originAmount,
|
||||
paymentAddress
|
||||
};
|
||||
|
||||
const PartThreeProps = {
|
||||
...SwapInfoHeaderProps,
|
||||
...PaymentInfoProps,
|
||||
reference,
|
||||
startOrderTimerSwap,
|
||||
startPollBityOrderStatus,
|
||||
stopOrderTimerSwap,
|
||||
stopPollBityOrderStatus,
|
||||
showNotification,
|
||||
destinationAddress,
|
||||
outputTx
|
||||
};
|
||||
|
||||
return (
|
||||
<section className="container" style={{ minHeight: '50%' }}>
|
||||
<div className="tab-content">
|
||||
|
@ -143,7 +185,7 @@ class Swap extends Component {
|
|||
{(step === 2 || step === 3) &&
|
||||
<SwapInfoHeader {...SwapInfoHeaderProps} />}
|
||||
{step === 2 && <ReceivingAddress {...ReceivingAddressProps} />}
|
||||
{step === 3 && <SwapProgress {...SwapInfoHeaderProps} />}
|
||||
{step === 3 && <PartThree {...PartThreeProps} />}
|
||||
</main>
|
||||
</div>
|
||||
</section>
|
||||
|
@ -153,6 +195,10 @@ class Swap extends Component {
|
|||
|
||||
function mapStateToProps(state) {
|
||||
return {
|
||||
outputTx: state.swap.outputTx,
|
||||
isPostingOrder: state.swap.isPostingOrder,
|
||||
orderStatus: state.swap.orderStatus,
|
||||
paymentAddress: state.swap.paymentAddress,
|
||||
step: state.swap.step,
|
||||
destinationAddress: state.swap.destinationAddress,
|
||||
originAmount: state.swap.originAmount,
|
||||
|
@ -162,13 +208,12 @@ function mapStateToProps(state) {
|
|||
destinationKindOptions: state.swap.destinationKindOptions,
|
||||
originKindOptions: state.swap.originKindOptions,
|
||||
bityRates: state.swap.bityRates,
|
||||
referenceNumber: state.swap.referenceNumber,
|
||||
timeRemaining: state.swap.timeRemaining,
|
||||
numberOfConfirmations: state.swap.numberOfConfirmations,
|
||||
orderStep: state.swap.orderStep,
|
||||
orderStarted: state.swap.orderStarted,
|
||||
bityOrder: state.swap.bityOrder,
|
||||
secondsRemaining: state.swap.secondsRemaining,
|
||||
isFetchingRates: state.swap.isFetchingRates
|
||||
};
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps, swapActions)(Swap);
|
||||
export default connect(mapStateToProps, { ...swapActions, showNotification })(
|
||||
Swap
|
||||
);
|
|
@ -1,5 +1,6 @@
|
|||
// Application styles must come first in order, to allow for overrides
|
||||
import 'assets/styles/etherwallet-master.less';
|
||||
import 'font-awesome/scss/font-awesome.scss';
|
||||
|
||||
import React from 'react';
|
||||
import { render } from 'react-dom';
|
||||
|
|
|
@ -1,12 +1,14 @@
|
|||
// @flow
|
||||
import { combineAndUpper } from 'utils/formatters';
|
||||
import type { SwapAction } from 'actions/swap';
|
||||
|
||||
import type { SwapAction } from 'actions/swapTypes';
|
||||
import without from 'lodash/without';
|
||||
export const ALL_CRYPTO_KIND_OPTIONS = ['BTC', 'ETH', 'REP'];
|
||||
const DEFAULT_ORIGIN_KIND = 'BTC';
|
||||
const DEFAULT_DESTINATION_KIND = 'ETH';
|
||||
|
||||
type State = {
|
||||
originAmount: number,
|
||||
destinationAmount: number,
|
||||
originAmount: ?number,
|
||||
destinationAmount: ?number,
|
||||
originKind: string,
|
||||
destinationKind: string,
|
||||
destinationKindOptions: Array<string>,
|
||||
|
@ -14,32 +16,37 @@ type State = {
|
|||
step: number,
|
||||
bityRates: Object,
|
||||
destinationAddress: string,
|
||||
referenceNumber: string,
|
||||
timeRemaining: string,
|
||||
numberOfConfirmations: ?number,
|
||||
orderStep: ?number,
|
||||
isFetchingRates: boolean
|
||||
isFetchingRates: ?boolean,
|
||||
secondsRemaining: ?number,
|
||||
outputTx: ?string,
|
||||
isPostingOrder: ?boolean,
|
||||
orderStatus: ?string,
|
||||
orderTimestampCreatedISOString: ?string,
|
||||
paymentAddress: ?string,
|
||||
validFor: ?number,
|
||||
orderId: string
|
||||
};
|
||||
|
||||
export const INITIAL_STATE: State = {
|
||||
originAmount: 0,
|
||||
destinationAmount: 0,
|
||||
originKind: 'BTC',
|
||||
destinationKind: 'ETH',
|
||||
destinationKindOptions: ALL_CRYPTO_KIND_OPTIONS.filter(element => {
|
||||
return element !== 'BTC';
|
||||
}),
|
||||
originKindOptions: ALL_CRYPTO_KIND_OPTIONS.filter(element => {
|
||||
return element !== 'REP';
|
||||
}),
|
||||
originAmount: null,
|
||||
destinationAmount: null,
|
||||
originKind: DEFAULT_ORIGIN_KIND,
|
||||
destinationKind: DEFAULT_DESTINATION_KIND,
|
||||
destinationKindOptions: without(ALL_CRYPTO_KIND_OPTIONS, DEFAULT_ORIGIN_KIND),
|
||||
originKindOptions: without(ALL_CRYPTO_KIND_OPTIONS, 'REP'),
|
||||
step: 1,
|
||||
bityRates: {},
|
||||
destinationAddress: '',
|
||||
referenceNumber: '',
|
||||
timeRemaining: '',
|
||||
numberOfConfirmations: null,
|
||||
orderStep: null,
|
||||
isFetchingRates: false
|
||||
bityOrder: {},
|
||||
isFetchingRates: null,
|
||||
secondsRemaining: null,
|
||||
outputTx: null,
|
||||
isPostingOrder: false,
|
||||
orderStatus: null,
|
||||
orderTimestampCreatedISOString: null,
|
||||
paymentAddress: null,
|
||||
validFor: null,
|
||||
orderId: null
|
||||
};
|
||||
|
||||
const buildDestinationAmount = (
|
||||
|
@ -50,7 +57,7 @@ const buildDestinationAmount = (
|
|||
) => {
|
||||
let pairName = combineAndUpper(originKind, destinationKind);
|
||||
let bityRate = bityRates[pairName];
|
||||
return originAmount * bityRate;
|
||||
return originAmount ? originAmount * bityRate : 0;
|
||||
};
|
||||
|
||||
const buildDestinationKind = (
|
||||
|
@ -58,7 +65,7 @@ const buildDestinationKind = (
|
|||
destinationKind: string
|
||||
): string => {
|
||||
if (originKind === destinationKind) {
|
||||
return ALL_CRYPTO_KIND_OPTIONS.filter(element => element !== originKind)[0];
|
||||
return without(ALL_CRYPTO_KIND_OPTIONS, originKind)[0];
|
||||
} else {
|
||||
return destinationKind;
|
||||
}
|
||||
|
@ -75,10 +82,7 @@ export function swap(state: State = INITIAL_STATE, action: SwapAction) {
|
|||
...state,
|
||||
originKind: action.value,
|
||||
destinationKind: newDestinationKind,
|
||||
destinationKindOptions: ALL_CRYPTO_KIND_OPTIONS.filter(element => {
|
||||
// $FlowFixMe
|
||||
return element !== action.value;
|
||||
}),
|
||||
destinationKindOptions: without(ALL_CRYPTO_KIND_OPTIONS, action.value),
|
||||
destinationAmount: buildDestinationAmount(
|
||||
state.originAmount,
|
||||
action.value,
|
||||
|
@ -109,7 +113,7 @@ export function swap(state: State = INITIAL_STATE, action: SwapAction) {
|
|||
...state,
|
||||
destinationAmount: action.value
|
||||
};
|
||||
case 'SWAP_UPDATE_BITY_RATES':
|
||||
case 'SWAP_LOAD_BITY_RATES_SUCCEEDED':
|
||||
return {
|
||||
...state,
|
||||
bityRates: {
|
||||
|
@ -134,16 +138,48 @@ export function swap(state: State = INITIAL_STATE, action: SwapAction) {
|
|||
...INITIAL_STATE,
|
||||
bityRates: state.bityRates
|
||||
};
|
||||
case 'SWAP_REFERENCE_NUMBER':
|
||||
case 'SWAP_ORDER_CREATE_REQUESTED':
|
||||
return {
|
||||
...state,
|
||||
referenceNumber: '2341asdfads',
|
||||
timeRemaining: '2:30',
|
||||
numberOfConfirmations: 3,
|
||||
orderStep: 2
|
||||
isPostingOrder: true
|
||||
};
|
||||
case 'SWAP_ORDER_CREATE_FAILED':
|
||||
return {
|
||||
...state,
|
||||
isPostingOrder: false
|
||||
};
|
||||
case 'SWAP_BITY_ORDER_CREATE_SUCCEEDED':
|
||||
return {
|
||||
...state,
|
||||
bityOrder: {
|
||||
...action.payload
|
||||
},
|
||||
isPostingOrder: false,
|
||||
originAmount: parseFloat(action.payload.input.amount),
|
||||
destinationAmount: parseFloat(action.payload.output.amount),
|
||||
secondsRemaining: action.payload.validFor, // will get update
|
||||
validFor: action.payload.validFor, // to build from local storage
|
||||
orderTimestampCreatedISOString: action.payload.timestamp_created,
|
||||
paymentAddress: action.payload.payment_address,
|
||||
orderStatus: action.payload.status,
|
||||
orderId: action.payload.id
|
||||
};
|
||||
case 'SWAP_BITY_ORDER_STATUS_SUCCEEDED':
|
||||
return {
|
||||
...state,
|
||||
outputTx: action.payload.output.reference,
|
||||
orderStatus:
|
||||
action.payload.output.status === 'FILL'
|
||||
? action.payload.output.status
|
||||
: action.payload.input.status
|
||||
};
|
||||
case 'SWAP_ORDER_TIME':
|
||||
return {
|
||||
...state,
|
||||
secondsRemaining: action.value
|
||||
};
|
||||
|
||||
case 'SWAP_LOAD_BITY_RATES':
|
||||
case 'SWAP_LOAD_BITY_RATES_REQUESTED':
|
||||
return {
|
||||
...state,
|
||||
isFetchingRates: true
|
||||
|
@ -156,7 +192,6 @@ export function swap(state: State = INITIAL_STATE, action: SwapAction) {
|
|||
};
|
||||
|
||||
default:
|
||||
(action: empty);
|
||||
return state;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,8 +1,23 @@
|
|||
import bity from './bity';
|
||||
import {
|
||||
postBityOrderSaga,
|
||||
bityTimeRemaining,
|
||||
pollBityOrderStatusSaga
|
||||
} from './swap/orders';
|
||||
import { getBityRatesSaga } from './swap/rates';
|
||||
import contracts from './contracts';
|
||||
import ens from './ens';
|
||||
import notifications from './notifications';
|
||||
import rates from './rates';
|
||||
import wallet from './wallet';
|
||||
|
||||
export default { bity, contracts, ens, notifications, rates, wallet };
|
||||
export default {
|
||||
bityTimeRemaining,
|
||||
postBityOrderSaga,
|
||||
pollBityOrderStatusSaga,
|
||||
getBityRatesSaga,
|
||||
contracts,
|
||||
ens,
|
||||
notifications,
|
||||
rates,
|
||||
wallet
|
||||
};
|
||||
|
|
|
@ -9,7 +9,7 @@ function* handleNotification(action?: ShowNotificationAction) {
|
|||
if (!action) return;
|
||||
const { duration } = action.payload;
|
||||
// show forever
|
||||
if (duration === 0) {
|
||||
if (duration === 0 || duration === 'infinity') {
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,178 @@
|
|||
import { showNotification } from 'actions/notifications';
|
||||
import { delay } from 'redux-saga';
|
||||
import { postOrder, getOrderStatus } from 'api/bity';
|
||||
import {
|
||||
call,
|
||||
put,
|
||||
fork,
|
||||
take,
|
||||
cancel,
|
||||
select,
|
||||
cancelled,
|
||||
takeEvery,
|
||||
Effect
|
||||
} from 'redux-saga/effects';
|
||||
import {
|
||||
orderTimeSwap,
|
||||
bityOrderCreateSucceededSwap,
|
||||
stopLoadBityRatesSwap,
|
||||
changeStepSwap,
|
||||
orderStatusRequestedSwap,
|
||||
orderStatusSucceededSwap,
|
||||
startOrderTimerSwap,
|
||||
startPollBityOrderStatus,
|
||||
stopPollBityOrderStatus
|
||||
} from 'actions/swap';
|
||||
import moment from 'moment';
|
||||
|
||||
export const getSwap = state => state.swap;
|
||||
const ONE_SECOND = 1000;
|
||||
const TEN_SECONDS = ONE_SECOND * 10;
|
||||
const BITY_TIMEOUT_MESSAGE = `
|
||||
Time has run out.
|
||||
If you have already sent, please wait 1 hour.
|
||||
If your order has not be processed after 1 hour,
|
||||
please press the orange 'Issue with your Swap?' button.
|
||||
`;
|
||||
|
||||
export function* pollBityOrderStatus(): Generator<Effect, void, any> {
|
||||
try {
|
||||
let swap = yield select(getSwap);
|
||||
while (true) {
|
||||
yield put(orderStatusRequestedSwap());
|
||||
const orderStatus = yield call(getOrderStatus, swap.orderId);
|
||||
if (orderStatus.error) {
|
||||
yield put(
|
||||
showNotification(
|
||||
'danger',
|
||||
`Bity Error: ${orderStatus.msg}`,
|
||||
TEN_SECONDS
|
||||
)
|
||||
);
|
||||
} else {
|
||||
yield put(orderStatusSucceededSwap(orderStatus.data));
|
||||
yield call(delay, ONE_SECOND * 5);
|
||||
swap = yield select(getSwap);
|
||||
if (swap === 'CANC') {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
if (yield cancelled()) {
|
||||
// TODO - implement request cancel if needed
|
||||
// yield put(actions.requestFailure('Request cancelled!'))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function* pollBityOrderStatusSaga(): Generator<Effect, void, any> {
|
||||
while (yield take('SWAP_START_POLL_BITY_ORDER_STATUS')) {
|
||||
// starts the task in the background
|
||||
const pollBityOrderStatusTask = yield fork(pollBityOrderStatus);
|
||||
// wait for the user to get to point where refresh is no longer needed
|
||||
yield take('SWAP_STOP_POLL_BITY_ORDER_STATUS');
|
||||
// cancel the background task
|
||||
// this will cause the forked loadBityRates task to jump into its finally block
|
||||
yield cancel(pollBityOrderStatusTask);
|
||||
}
|
||||
}
|
||||
|
||||
function* postBityOrderCreate(action) {
|
||||
const payload = action.payload;
|
||||
try {
|
||||
yield put(stopLoadBityRatesSwap());
|
||||
const order = yield call(
|
||||
postOrder,
|
||||
payload.amount,
|
||||
payload.destAddress,
|
||||
payload.mode,
|
||||
payload.pair
|
||||
);
|
||||
if (order.error) {
|
||||
// TODO - handle better / like existing site?
|
||||
yield put(
|
||||
showNotification('danger', `Bity Error: ${order.msg}`, TEN_SECONDS)
|
||||
);
|
||||
yield put({ type: 'SWAP_ORDER_CREATE_FAILED' });
|
||||
} else {
|
||||
yield put(bityOrderCreateSucceededSwap(order.data));
|
||||
yield put(changeStepSwap(3));
|
||||
// start countdown
|
||||
yield put(startOrderTimerSwap());
|
||||
// start bity order status polling
|
||||
yield put(startPollBityOrderStatus());
|
||||
}
|
||||
} catch (e) {
|
||||
const message =
|
||||
'Connection Error. Please check the developer console for more details and/or contact support';
|
||||
yield put(showNotification('danger', message, TEN_SECONDS));
|
||||
yield put({ type: 'SWAP_ORDER_CREATE_FAILED' });
|
||||
}
|
||||
}
|
||||
|
||||
export function* postBityOrderSaga(): Generator<Effect, void, any> {
|
||||
yield takeEvery('SWAP_ORDER_CREATE_REQUESTED', postBityOrderCreate);
|
||||
}
|
||||
|
||||
export function* bityTimeRemaining() {
|
||||
while (yield take('SWAP_ORDER_START_TIMER')) {
|
||||
let hasShownNotification = false;
|
||||
while (true) {
|
||||
yield call(delay, ONE_SECOND);
|
||||
const swap = yield select(getSwap);
|
||||
// if (swap.bityOrder.status === 'OPEN') {
|
||||
const createdTimeStampMoment = moment(
|
||||
swap.orderTimestampCreatedISOString
|
||||
);
|
||||
let validUntil = moment(createdTimeStampMoment).add(swap.validFor, 's');
|
||||
let now = moment();
|
||||
if (validUntil.isAfter(now)) {
|
||||
let duration = moment.duration(validUntil.diff(now));
|
||||
let seconds = duration.asSeconds();
|
||||
yield put(orderTimeSwap(parseInt(seconds)));
|
||||
// TODO (!Important) - check orderStatus here and stop polling / show notifications based on status
|
||||
} else {
|
||||
switch (swap.orderStatus) {
|
||||
case 'OPEN':
|
||||
yield put(orderTimeSwap(0));
|
||||
yield put(stopPollBityOrderStatus());
|
||||
yield put({ type: 'SWAP_STOP_LOAD_BITY_RATES' });
|
||||
|
||||
if (!hasShownNotification) {
|
||||
hasShownNotification = true;
|
||||
yield put(
|
||||
showNotification('danger', BITY_TIMEOUT_MESSAGE, 'infinity')
|
||||
);
|
||||
}
|
||||
break;
|
||||
case 'CANC':
|
||||
yield put(orderTimeSwap(0));
|
||||
yield put(stopPollBityOrderStatus());
|
||||
yield put({ type: 'SWAP_STOP_LOAD_BITY_RATES' });
|
||||
if (!hasShownNotification) {
|
||||
hasShownNotification = true;
|
||||
yield put(
|
||||
showNotification('danger', BITY_TIMEOUT_MESSAGE, 'infinity')
|
||||
);
|
||||
}
|
||||
break;
|
||||
case 'RCVE':
|
||||
yield put(orderTimeSwap(0));
|
||||
if (!hasShownNotification) {
|
||||
hasShownNotification = true;
|
||||
yield put(
|
||||
showNotification('warning', BITY_TIMEOUT_MESSAGE, 'infinity')
|
||||
);
|
||||
}
|
||||
break;
|
||||
case 'FILL':
|
||||
yield put(orderTimeSwap(0));
|
||||
yield put(stopPollBityOrderStatus());
|
||||
yield put({ type: 'SWAP_STOP_LOAD_BITY_RATES' });
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,19 +1,26 @@
|
|||
// @flow
|
||||
import { call, put, fork, take, cancel, cancelled } from 'redux-saga/effects';
|
||||
|
||||
import type { Effect } from 'redux-saga/effects';
|
||||
import { delay } from 'redux-saga';
|
||||
import { updateBityRatesSwap } from 'actions/swap';
|
||||
import { getAllRates } from 'api/bity';
|
||||
import {
|
||||
call,
|
||||
put,
|
||||
fork,
|
||||
take,
|
||||
cancel,
|
||||
cancelled,
|
||||
takeLatest
|
||||
} from 'redux-saga/effects';
|
||||
import type { Effect } from 'redux-saga/effects';
|
||||
import { loadBityRatesSucceededSwap } from 'actions/swap';
|
||||
|
||||
export function* loadBityRates(_action?: any): Generator<Effect, void, any> {
|
||||
try {
|
||||
while (true) {
|
||||
// TODO - yield put(actions.requestStart()) if we want to display swap refresh status
|
||||
// TODO - BITY_RATE_REQUESTED
|
||||
// network request
|
||||
const data = yield call(getAllRates);
|
||||
// action
|
||||
yield put(updateBityRatesSwap(data));
|
||||
yield put(loadBityRatesSucceededSwap(data));
|
||||
// wait 5 seconds before refreshing rates
|
||||
yield call(delay, 5000);
|
||||
}
|
||||
|
@ -25,11 +32,10 @@ export function* loadBityRates(_action?: any): Generator<Effect, void, any> {
|
|||
}
|
||||
}
|
||||
|
||||
export default function* bitySaga(): Generator<Effect, void, any> {
|
||||
while (yield take('SWAP_LOAD_BITY_RATES')) {
|
||||
export function* getBityRatesSaga(): Generator<Effect, void, any> {
|
||||
while (yield take('SWAP_LOAD_BITY_RATES_REQUESTED')) {
|
||||
// starts the task in the background
|
||||
const loadBityRatesTask = yield fork(loadBityRates);
|
||||
|
||||
// wait for the user to get to point where refresh is no longer needed
|
||||
yield take('SWAP_STOP_LOAD_BITY_RATES');
|
||||
// cancel the background task
|
|
@ -8,6 +8,7 @@ import createSagaMiddleware from 'redux-saga';
|
|||
import sagas from './sagas';
|
||||
import { INITIAL_STATE as configInitialState } from 'reducers/config';
|
||||
import { INITIAL_STATE as customTokensInitialState } from 'reducers/customTokens';
|
||||
import { INITIAL_STATE as swapInitialState } from 'reducers/swap';
|
||||
import throttle from 'lodash/throttle';
|
||||
import { composeWithDevTools } from 'redux-devtools-extension';
|
||||
import Perf from 'react-addons-perf';
|
||||
|
@ -37,7 +38,15 @@ const configureStore = () => {
|
|||
...configInitialState,
|
||||
...loadStatePropertyOrEmptyObject('config')
|
||||
},
|
||||
customTokens: (loadState() || {}).customTokens || customTokensInitialState
|
||||
customTokens: (loadState() || {}).customTokens || customTokensInitialState,
|
||||
// ONLY LOAD SWAP STATE FROM LOCAL STORAGE IF STEP WAS 3
|
||||
swap:
|
||||
loadStatePropertyOrEmptyObject('swap').step === 3
|
||||
? {
|
||||
...swapInitialState,
|
||||
...loadStatePropertyOrEmptyObject('swap')
|
||||
}
|
||||
: { ...swapInitialState }
|
||||
};
|
||||
|
||||
store = createStore(RootReducer, persistedInitialState, middleware);
|
||||
|
@ -53,6 +62,7 @@ const configureStore = () => {
|
|||
config: {
|
||||
languageSelection: store.getState().config.languageSelection
|
||||
},
|
||||
swap: store.getState().swap,
|
||||
customTokens: store.getState().customTokens
|
||||
});
|
||||
}),
|
||||
|
|
|
@ -4252,6 +4252,11 @@
|
|||
"integrity": "sha1-1M2yQw3uGjWZ8Otv5VEUbjAnJWo=",
|
||||
"dev": true
|
||||
},
|
||||
"font-awesome": {
|
||||
"version": "4.7.0",
|
||||
"resolved": "https://registry.npmjs.org/font-awesome/-/font-awesome-4.7.0.tgz",
|
||||
"integrity": "sha1-j6jPBBGhoxr9B7BtKQK7n8gVoTM="
|
||||
},
|
||||
"for-in": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/for-in/-/for-in-1.0.2.tgz",
|
||||
|
@ -8224,6 +8229,11 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"moment": {
|
||||
"version": "2.18.1",
|
||||
"resolved": "https://registry.npmjs.org/moment/-/moment-2.18.1.tgz",
|
||||
"integrity": "sha1-w2GT3Tzhwu7SrbfIAtu8d6gbHA8="
|
||||
},
|
||||
"mozjpeg": {
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmjs.org/mozjpeg/-/mozjpeg-4.1.1.tgz",
|
||||
|
|
|
@ -9,11 +9,13 @@
|
|||
},
|
||||
"dependencies": {
|
||||
"big.js": "^3.1.3",
|
||||
"ethereum-blockies": "https://github.com/MyEtherWallet/blockies.git",
|
||||
"ethereum-blockies": "git+https://github.com/MyEtherWallet/blockies.git",
|
||||
"ethereumjs-util": "^5.1.2",
|
||||
"ethereumjs-wallet": "^0.6.0",
|
||||
"font-awesome": "^4.7.0",
|
||||
"idna-uts46": "^1.1.0",
|
||||
"lodash": "^4.17.4",
|
||||
"moment": "^2.18.1",
|
||||
"prop-types": "^15.5.8",
|
||||
"qrcode": "^0.8.2",
|
||||
"react": "^15.4.2",
|
||||
|
|
|
@ -40,10 +40,6 @@ module.exports = {
|
|||
loaders: ['babel-loader'],
|
||||
exclude: [/node_modules\/(?!ethereum-blockies|idna-uts46)/]
|
||||
},
|
||||
{
|
||||
test: /\.(ico|webp|eot|otf|ttf|woff|woff2)(\?.*)?$/,
|
||||
loader: 'file-loader?limit=100000'
|
||||
},
|
||||
{
|
||||
test: /\.(gif|png|jpe?g|svg)$/i,
|
||||
loaders: [
|
||||
|
|
Loading…
Reference in New Issue