diff --git a/common/actions/swap/actionCreators.ts b/common/actions/swap/actionCreators.ts index 06010438..a2d0c108 100644 --- a/common/actions/swap/actionCreators.ts +++ b/common/actions/swap/actionCreators.ts @@ -27,6 +27,16 @@ export function loadBityRatesSucceededSwap( }; } +export type TLoadShapeshiftSucceededSwap = typeof loadShapeshiftRatesSucceededSwap; +export function loadShapeshiftRatesSucceededSwap( + payload +): interfaces.LoadShapshiftRatesSucceededSwapAction { + return { + type: TypeKeys.SWAP_LOAD_SHAPESHIFT_RATES_SUCCEEDED, + payload + }; +} + export type TDestinationAddressSwap = typeof destinationAddressSwap; export function destinationAddressSwap(payload?: string): interfaces.DestinationAddressSwapAction { return { @@ -49,6 +59,13 @@ export function loadBityRatesRequestedSwap(): interfaces.LoadBityRatesRequestedS }; } +export type TLoadShapeshiftRequestedSwap = typeof loadShapeshiftRatesRequestedSwap; +export function loadShapeshiftRatesRequestedSwap(): interfaces.LoadShapeshiftRequestedSwapAction { + return { + type: TypeKeys.SWAP_LOAD_SHAPESHIFT_RATES_REQUESTED + }; +} + export type TStopLoadBityRatesSwap = typeof stopLoadBityRatesSwap; export function stopLoadBityRatesSwap(): interfaces.StopLoadBityRatesSwapAction { return { @@ -56,6 +73,13 @@ export function stopLoadBityRatesSwap(): interfaces.StopLoadBityRatesSwapAction }; } +export type TStopLoadShapeshiftRatesSwap = typeof stopLoadShapeshiftRatesSwap; +export function stopLoadShapeshiftRatesSwap(): interfaces.StopLoadShapeshiftRatesSwapAction { + return { + type: TypeKeys.SWAP_STOP_LOAD_SHAPESHIFT_RATES + }; +} + export type TOrderTimeSwap = typeof orderTimeSwap; export function orderTimeSwap(payload: number): interfaces.OrderSwapTimeSwapAction { return { @@ -74,6 +98,16 @@ export function bityOrderCreateSucceededSwap( }; } +export type TShapeshiftOrderCreateSucceededSwap = typeof shapeshiftOrderCreateSucceededSwap; +export function shapeshiftOrderCreateSucceededSwap( + payload: interfaces.ShapeshiftOrderResponse +): interfaces.ShapeshiftOrderCreateSucceededSwapAction { + return { + type: TypeKeys.SWAP_SHAPESHIFT_ORDER_CREATE_SUCCEEDED, + payload + }; +} + export type TBityOrderCreateRequestedSwap = typeof bityOrderCreateRequestedSwap; export function bityOrderCreateRequestedSwap( amount: number, @@ -82,7 +116,7 @@ export function bityOrderCreateRequestedSwap( mode: number = 0 ): interfaces.BityOrderCreateRequestedSwapAction { return { - type: TypeKeys.SWAP_ORDER_CREATE_REQUESTED, + type: TypeKeys.SWAP_BITY_ORDER_CREATE_REQUESTED, payload: { amount, destinationAddress, @@ -92,29 +126,70 @@ export function bityOrderCreateRequestedSwap( }; } -export function bityOrderCreateFailedSwap(): interfaces.BityOrderCreateFailedSwapAction { +export type TShapeshiftOrderCreateRequestedSwap = typeof shapeshiftOrderCreateRequestedSwap; +export function shapeshiftOrderCreateRequestedSwap( + withdrawal: string, + originKind: string, + destinationKind: string, + destinationAmount: number +): interfaces.ShapeshiftOrderCreateRequestedSwapAction { return { - type: TypeKeys.SWAP_ORDER_CREATE_FAILED + type: TypeKeys.SWAP_SHAPESHIFT_ORDER_CREATE_REQUESTED, + payload: { + withdrawal, + originKind, + destinationKind, + destinationAmount + } }; } -export type TOrderStatusSucceededSwap = typeof orderStatusSucceededSwap; -export function orderStatusSucceededSwap( +export function bityOrderCreateFailedSwap(): interfaces.BityOrderCreateFailedSwapAction { + return { + type: TypeKeys.SWAP_BITY_ORDER_CREATE_FAILED + }; +} + +export function shapeshiftOrderCreateFailedSwap(): interfaces.ShapeshiftOrderCreateFailedSwapAction { + return { + type: TypeKeys.SWAP_SHAPESHIFT_ORDER_CREATE_FAILED + }; +} + +export type TBityOrderStatusSucceededSwap = typeof bityOrderStatusSucceededSwap; +export function bityOrderStatusSucceededSwap( payload: interfaces.BityOrderResponse -): interfaces.OrderStatusSucceededSwapAction { +): interfaces.BityOrderStatusSucceededSwapAction { return { type: TypeKeys.SWAP_BITY_ORDER_STATUS_SUCCEEDED, payload }; } -export type TOrderStatusRequestedSwap = typeof orderStatusRequestedSwap; -export function orderStatusRequestedSwap(): interfaces.OrderStatusRequestedSwapAction { +export type TShapeshiftOrderStatusSucceededSwap = typeof shapeshiftOrderStatusSucceededSwap; +export function shapeshiftOrderStatusSucceededSwap( + payload: interfaces.ShapeshiftStatusResponse +): interfaces.ShapeshiftOrderStatusSucceededSwapAction { + return { + type: TypeKeys.SWAP_SHAPESHIFT_ORDER_STATUS_SUCCEEDED, + payload + }; +} + +export type TBityOrderStatusRequestedSwap = typeof bityOrderStatusRequested; +export function bityOrderStatusRequested(): interfaces.BityOrderStatusRequestedSwapAction { return { type: TypeKeys.SWAP_BITY_ORDER_STATUS_REQUESTED }; } +export type TShapeshiftOrderStatusRequestedSwap = typeof shapeshiftOrderStatusRequested; +export function shapeshiftOrderStatusRequested(): interfaces.ShapeshiftOrderStatusRequestedSwapAction { + return { + type: TypeKeys.SWAP_SHAPESHIFT_ORDER_STATUS_REQUESTED + }; +} + export type TStartOrderTimerSwap = typeof startOrderTimerSwap; export function startOrderTimerSwap(): interfaces.StartOrderTimerSwapAction { return { @@ -136,9 +211,45 @@ export function startPollBityOrderStatus(): interfaces.StartPollBityOrderStatusA }; } +export type TStartPollShapeshiftOrderStatus = typeof startPollShapeshiftOrderStatus; +export function startPollShapeshiftOrderStatus(): interfaces.StartPollShapeshiftOrderStatusAction { + return { + type: TypeKeys.SWAP_START_POLL_SHAPESHIFT_ORDER_STATUS + }; +} + export type TStopPollBityOrderStatus = typeof stopPollBityOrderStatus; export function stopPollBityOrderStatus(): interfaces.StopPollBityOrderStatusAction { return { type: TypeKeys.SWAP_STOP_POLL_BITY_ORDER_STATUS }; } + +export type TStopPollShapeshiftOrderStatus = typeof stopPollShapeshiftOrderStatus; +export function stopPollShapeshiftOrderStatus(): interfaces.StopPollShapeshiftOrderStatusAction { + return { + type: TypeKeys.SWAP_STOP_POLL_SHAPESHIFT_ORDER_STATUS + }; +} + +export type TConfigureLiteSend = typeof configureLiteSend; +export function configureLiteSend(): interfaces.ConfigureLiteSendAction { + return { type: TypeKeys.SWAP_CONFIGURE_LITE_SEND }; +} + +export type TShowLiteSend = typeof showLiteSend; +export function showLiteSend( + payload: interfaces.ShowLiteSendAction['payload'] +): interfaces.ShowLiteSendAction { + return { type: TypeKeys.SWAP_SHOW_LITE_SEND, payload }; +} + +export type TChangeSwapProvider = typeof changeSwapProvider; +export function changeSwapProvider( + payload: interfaces.ProviderName +): interfaces.ChangeProviderSwapAcion { + return { + type: TypeKeys.SWAP_CHANGE_PROVIDER, + payload + }; +} diff --git a/common/actions/swap/actionTypes.ts b/common/actions/swap/actionTypes.ts index ca0609ca..f22e8143 100644 --- a/common/actions/swap/actionTypes.ts +++ b/common/actions/swap/actionTypes.ts @@ -9,7 +9,7 @@ export interface Pairs { export interface SwapInput { id: string; - amount: number; + amount: number | string; } export interface SwapInputs { @@ -24,6 +24,8 @@ export interface InitSwap { export interface Option { id: string; + status?: string; + image?: string; } export interface ApiResponseObj { @@ -41,6 +43,11 @@ export interface LoadBityRatesSucceededSwapAction { payload: ApiResponse; } +export interface LoadShapshiftRatesSucceededSwapAction { + type: TypeKeys.SWAP_LOAD_SHAPESHIFT_RATES_SUCCEEDED; + payload: ApiResponse; +} + export interface DestinationAddressSwapAction { type: TypeKeys.SWAP_DESTINATION_ADDRESS; payload?: string; @@ -55,6 +62,11 @@ export interface LoadBityRatesRequestedSwapAction { payload?: null; } +export interface LoadShapeshiftRequestedSwapAction { + type: TypeKeys.SWAP_LOAD_SHAPESHIFT_RATES_REQUESTED; + payload?: null; +} + export interface ChangeStepSwapAction { type: TypeKeys.SWAP_STEP; payload: number; @@ -64,13 +76,17 @@ export interface StopLoadBityRatesSwapAction { type: TypeKeys.SWAP_STOP_LOAD_BITY_RATES; } +export interface StopLoadShapeshiftRatesSwapAction { + type: TypeKeys.SWAP_STOP_LOAD_SHAPESHIFT_RATES; +} + export interface OrderSwapTimeSwapAction { type: TypeKeys.SWAP_ORDER_TIME; payload: number; } export interface BityOrderCreateRequestedSwapAction { - type: TypeKeys.SWAP_ORDER_CREATE_REQUESTED; + type: TypeKeys.SWAP_BITY_ORDER_CREATE_REQUESTED; payload: { amount: number; destinationAddress: string; @@ -79,6 +95,16 @@ export interface BityOrderCreateRequestedSwapAction { }; } +export interface ShapeshiftOrderCreateRequestedSwapAction { + type: TypeKeys.SWAP_SHAPESHIFT_ORDER_CREATE_REQUESTED; + payload: { + withdrawal: string; + originKind: string; + destinationKind: string; + destinationAmount: number; + }; +} + export interface BityOrderInput { amount: string; currency: string; @@ -99,6 +125,31 @@ export interface BityOrderResponse { status: string; } +export interface ShapeshiftOrderResponse { + apiPubKey?: string; + deposit: string; + depositAmount: string; + expiration: number; + expirationFormatted?: string; + inputCurrency?: string; + maxLimit: number; + minerFee: string; + orderId: string; + outputCurrency?: string; + pair: string; // e.g. eth_bat + provider?: ProviderName; // shapeshift + quotedRate: string; + withdrawal: string; + withdrawalAmount: string; +} + +export interface ShapeshiftStatusResponse { + status: string; + address?: string; + withdraw?: string; + transaction: string; +} + export type BityOrderPostResponse = BityOrderResponse & { payment_address: string; status: string; @@ -109,23 +160,44 @@ export type BityOrderPostResponse = BityOrderResponse & { id: string; }; +export type ProviderName = 'shapeshift' | 'bity'; + export interface BityOrderCreateSucceededSwapAction { type: TypeKeys.SWAP_BITY_ORDER_CREATE_SUCCEEDED; payload: BityOrderPostResponse; } -export interface BityOrderCreateFailedSwapAction { - type: TypeKeys.SWAP_ORDER_CREATE_FAILED; +export interface ShapeshiftOrderCreateSucceededSwapAction { + type: TypeKeys.SWAP_SHAPESHIFT_ORDER_CREATE_SUCCEEDED; + payload: ShapeshiftOrderResponse; } -export interface OrderStatusRequestedSwapAction { + +export interface BityOrderCreateFailedSwapAction { + type: TypeKeys.SWAP_BITY_ORDER_CREATE_FAILED; +} + +export interface ShapeshiftOrderCreateFailedSwapAction { + type: TypeKeys.SWAP_SHAPESHIFT_ORDER_CREATE_FAILED; +} + +export interface BityOrderStatusRequestedSwapAction { type: TypeKeys.SWAP_BITY_ORDER_STATUS_REQUESTED; } -export interface OrderStatusSucceededSwapAction { +export interface ShapeshiftOrderStatusRequestedSwapAction { + type: TypeKeys.SWAP_SHAPESHIFT_ORDER_STATUS_REQUESTED; +} + +export interface BityOrderStatusSucceededSwapAction { type: TypeKeys.SWAP_BITY_ORDER_STATUS_SUCCEEDED; payload: BityOrderResponse; } +export interface ShapeshiftOrderStatusSucceededSwapAction { + type: TypeKeys.SWAP_SHAPESHIFT_ORDER_STATUS_SUCCEEDED; + payload: ShapeshiftStatusResponse; +} + export interface StartOrderTimerSwapAction { type: TypeKeys.SWAP_ORDER_START_TIMER; } @@ -138,22 +210,55 @@ export interface StartPollBityOrderStatusAction { type: TypeKeys.SWAP_START_POLL_BITY_ORDER_STATUS; } +export interface StartPollShapeshiftOrderStatusAction { + type: TypeKeys.SWAP_START_POLL_SHAPESHIFT_ORDER_STATUS; +} + export interface StopPollBityOrderStatusAction { type: TypeKeys.SWAP_STOP_POLL_BITY_ORDER_STATUS; } +export interface StopPollShapeshiftOrderStatusAction { + type: TypeKeys.SWAP_STOP_POLL_SHAPESHIFT_ORDER_STATUS; +} + +export interface ChangeProviderSwapAcion { + type: TypeKeys.SWAP_CHANGE_PROVIDER; + payload: ProviderName; +} + +export interface ConfigureLiteSendAction { + type: TypeKeys.SWAP_CONFIGURE_LITE_SEND; +} + +export interface ShowLiteSendAction { + type: TypeKeys.SWAP_SHOW_LITE_SEND; + payload: boolean; +} + /*** Action Type Union ***/ export type SwapAction = | ChangeStepSwapAction | InitSwap | LoadBityRatesSucceededSwapAction + | LoadShapshiftRatesSucceededSwapAction | DestinationAddressSwapAction | RestartSwapAction | LoadBityRatesRequestedSwapAction + | LoadShapeshiftRequestedSwapAction | StopLoadBityRatesSwapAction + | StopLoadShapeshiftRatesSwapAction | BityOrderCreateRequestedSwapAction + | ShapeshiftOrderCreateRequestedSwapAction | BityOrderCreateSucceededSwapAction - | OrderStatusSucceededSwapAction + | ShapeshiftOrderCreateSucceededSwapAction + | BityOrderStatusSucceededSwapAction + | ShapeshiftOrderStatusSucceededSwapAction | StartPollBityOrderStatusAction + | StartPollShapeshiftOrderStatusAction | BityOrderCreateFailedSwapAction - | OrderSwapTimeSwapAction; + | ShapeshiftOrderCreateFailedSwapAction + | OrderSwapTimeSwapAction + | ChangeProviderSwapAcion + | ConfigureLiteSendAction + | ShowLiteSendAction; diff --git a/common/actions/swap/constants.ts b/common/actions/swap/constants.ts index 6192ec53..11d4ee34 100644 --- a/common/actions/swap/constants.ts +++ b/common/actions/swap/constants.ts @@ -2,18 +2,31 @@ export enum TypeKeys { SWAP_STEP = 'SWAP_STEP', SWAP_INIT = 'SWAP_INIT', SWAP_LOAD_BITY_RATES_SUCCEEDED = 'SWAP_LOAD_BITY_RATES_SUCCEEDED', + SWAP_LOAD_SHAPESHIFT_RATES_SUCCEEDED = 'SWAP_LOAD_SHAPESHIFT_RATES_SUCCEEDED', SWAP_DESTINATION_ADDRESS = 'SWAP_DESTINATION_ADDRESS', SWAP_RESTART = 'SWAP_RESTART', SWAP_LOAD_BITY_RATES_REQUESTED = 'SWAP_LOAD_BITY_RATES_REQUESTED', + SWAP_LOAD_SHAPESHIFT_RATES_REQUESTED = 'SWAP_LOAD_SHAPESHIFT_RATES_REQUESTED', SWAP_STOP_LOAD_BITY_RATES = 'SWAP_STOP_LOAD_BITY_RATES', + SWAP_STOP_LOAD_SHAPESHIFT_RATES = 'SWAP_STOP_LOAD_SHAPESHIFT_RATES', SWAP_ORDER_TIME = 'SWAP_ORDER_TIME', SWAP_BITY_ORDER_CREATE_SUCCEEDED = 'SWAP_BITY_ORDER_CREATE_SUCCEEDED', + SWAP_SHAPESHIFT_ORDER_CREATE_SUCCEEDED = 'SWAP_SHAPESHIFT_ORDER_CREATE_SUCCEEDED', SWAP_BITY_ORDER_STATUS_SUCCEEDED = 'SWAP_BITY_ORDER_STATUS_SUCCEEDED', + SWAP_SHAPESHIFT_ORDER_STATUS_SUCCEEDED = 'SWAP_SHAPESHIFT_ORDER_STATUS_SUCCEEDED', SWAP_BITY_ORDER_STATUS_REQUESTED = 'SWAP_BITY_ORDER_STATUS_REQUESTED', + SWAP_SHAPESHIFT_ORDER_STATUS_REQUESTED = 'SWAP_SHAPESHIFT_ORDER_STATUS_REQUESTED', SWAP_ORDER_START_TIMER = 'SWAP_ORDER_START_TIMER', SWAP_ORDER_STOP_TIMER = 'SWAP_ORDER_STOP_TIMER', SWAP_START_POLL_BITY_ORDER_STATUS = 'SWAP_START_POLL_BITY_ORDER_STATUS', + SWAP_START_POLL_SHAPESHIFT_ORDER_STATUS = 'SWAP_START_POLL_SHAPESHIFT_ORDER_STATUS', SWAP_STOP_POLL_BITY_ORDER_STATUS = 'SWAP_STOP_POLL_BITY_ORDER_STATUS', - SWAP_ORDER_CREATE_REQUESTED = 'SWAP_ORDER_CREATE_REQUESTED', - SWAP_ORDER_CREATE_FAILED = 'SWAP_ORDER_CREATE_FAILED' + SWAP_STOP_POLL_SHAPESHIFT_ORDER_STATUS = 'SWAP_STOP_POLL_SHAPESHIFT_ORDER_STATUS', + SWAP_BITY_ORDER_CREATE_REQUESTED = 'SWAP_ORDER_CREATE_REQUESTED', + SWAP_SHAPESHIFT_ORDER_CREATE_REQUESTED = 'SWAP_SHAPESHIFT_ORDER_CREATE_REQUESTED', + SWAP_BITY_ORDER_CREATE_FAILED = 'SWAP_ORDER_CREATE_FAILED', + SWAP_SHAPESHIFT_ORDER_CREATE_FAILED = 'SWAP_SHAPESHIFT_ORDER_CREATE_FAILED', + SWAP_CHANGE_PROVIDER = 'SWAP_CHANGE_PROVIDER', + SWAP_CONFIGURE_LITE_SEND = 'SWAP_CONFIGURE_LITE_SEND', + SWAP_SHOW_LITE_SEND = 'SWAP_SHOW_LITE_SEND' } diff --git a/common/actions/wallet/actionCreators.ts b/common/actions/wallet/actionCreators.ts index 7435c972..ce25069d 100644 --- a/common/actions/wallet/actionCreators.ts +++ b/common/actions/wallet/actionCreators.ts @@ -88,6 +88,34 @@ export function setTokenBalancesRejected(): types.SetTokenBalancesRejectedAction }; } +export function setTokenBalancePending( + payload: types.SetTokenBalancePendingAction['payload'] +): types.SetTokenBalancePendingAction { + return { + type: TypeKeys.WALLET_SET_TOKEN_BALANCE_PENDING, + payload + }; +} + +export type TSetTokenBalanceFulfilled = typeof setTokenBalanceFulfilled; +export function setTokenBalanceFulfilled(payload: { + [key: string]: { + balance: TokenValue; + error: string | null; + }; +}): types.SetTokenBalanceFulfilledAction { + return { + type: TypeKeys.WALLET_SET_TOKEN_BALANCE_FULFILLED, + payload + }; +} + +export function setTokenBalanceRejected(): types.SetTokenBalanceRejectedAction { + return { + type: TypeKeys.WALLET_SET_TOKEN_BALANCE_REJECTED + }; +} + export type TScanWalletForTokens = typeof scanWalletForTokens; export function scanWalletForTokens(wallet: IWallet): types.ScanWalletForTokensAction { return { diff --git a/common/actions/wallet/actionTypes.ts b/common/actions/wallet/actionTypes.ts index a0a2d773..2f02b354 100644 --- a/common/actions/wallet/actionTypes.ts +++ b/common/actions/wallet/actionTypes.ts @@ -63,6 +63,25 @@ export interface SetTokenBalancesRejectedAction { type: TypeKeys.WALLET_SET_TOKEN_BALANCES_REJECTED; } +export interface SetTokenBalancePendingAction { + type: TypeKeys.WALLET_SET_TOKEN_BALANCE_PENDING; + payload: { tokenSymbol: string }; +} + +export interface SetTokenBalanceFulfilledAction { + type: TypeKeys.WALLET_SET_TOKEN_BALANCE_FULFILLED; + payload: { + [key: string]: { + balance: TokenValue; + error: string | null; + }; + }; +} + +export interface SetTokenBalanceRejectedAction { + type: TypeKeys.WALLET_SET_TOKEN_BALANCE_REJECTED; +} + export interface ScanWalletForTokensAction { type: TypeKeys.WALLET_SCAN_WALLET_FOR_TOKENS; payload: IWallet; @@ -108,6 +127,9 @@ export type WalletAction = | SetTokenBalancesPendingAction | SetTokenBalancesFulfilledAction | SetTokenBalancesRejectedAction + | SetTokenBalancePendingAction + | SetTokenBalanceFulfilledAction + | SetTokenBalanceRejectedAction | ScanWalletForTokensAction | SetWalletTokensAction | SetWalletConfigAction; diff --git a/common/actions/wallet/constants.ts b/common/actions/wallet/constants.ts index cf213d28..bf2c547b 100644 --- a/common/actions/wallet/constants.ts +++ b/common/actions/wallet/constants.ts @@ -10,6 +10,9 @@ export enum TypeKeys { WALLET_SET_TOKEN_BALANCES_PENDING = 'WALLET_SET_TOKEN_BALANCES_PENDING', WALLET_SET_TOKEN_BALANCES_FULFILLED = 'WALLET_SET_TOKEN_BALANCES_FULFILLED', WALLET_SET_TOKEN_BALANCES_REJECTED = 'WALLET_SET_TOKEN_BALANCES_REJECTED', + WALLET_SET_TOKEN_BALANCE_PENDING = 'WALLET_SET_TOKEN_BALANCE_PENDING', + WALLET_SET_TOKEN_BALANCE_FULFILLED = 'WALLET_SET_TOKEN_BALANCE_FULFILLED', + WALLET_SET_TOKEN_BALANCE_REJECTED = 'WALLET_SET_TOKEN_BALANCE_REJECTED', WALLET_SCAN_WALLET_FOR_TOKENS = 'WALLET_SCAN_WALLET_FOR_TOKENS', WALLET_SET_WALLET_TOKENS = 'WALLET_SET_WALLET_TOKENS', WALLET_SET_CONFIG = 'WALLET_SET_CONFIG', diff --git a/common/api/bity.ts b/common/api/bity.ts index e10a8e74..979d47b4 100644 --- a/common/api/bity.ts +++ b/common/api/bity.ts @@ -1,10 +1,34 @@ import bityConfig, { WhitelistedCoins } from 'config/bity'; import { checkHttpStatus, parseJSON, filter } from './utils'; +import bitcoinIcon from 'assets/images/bitcoin.png'; +import repIcon from 'assets/images/augur.png'; +import etherIcon from 'assets/images/ether.png'; const isCryptoPair = (from: string, to: string, arr: WhitelistedCoins[]) => { return filter(from, arr) && filter(to, arr); }; +const btcOptions = { + id: 'BTC', + status: 'available', + image: bitcoinIcon, + name: 'Bitcoin' +}; + +const ethOptions = { + id: 'ETH', + status: 'available', + image: etherIcon, + name: 'Ether' +}; + +const repOptions = { + id: 'REP', + status: 'available', + image: repIcon, + name: 'Augur' +}; + export function getAllRates() { const mappedRates = {}; return _getAllRates().then(bityRates => { @@ -14,9 +38,31 @@ export function getAllRates() { const to = { id: pairName.substring(3, 6) }; // Check if rate exists= && check if the pair only crypto to crypto, not crypto to fiat, or any other combination if (parseFloat(each.rate_we_sell) && isCryptoPair(from.id, to.id, ['BTC', 'ETH', 'REP'])) { + let fromOptions; + let toOptions; + switch (from.id) { + case 'BTC': + fromOptions = btcOptions; + break; + case 'ETH': + fromOptions = ethOptions; + break; + case 'REP': + fromOptions = repOptions; + } + switch (to.id) { + case 'BTC': + toOptions = btcOptions; + break; + case 'ETH': + toOptions = ethOptions; + break; + case 'REP': + toOptions = repOptions; + } mappedRates[pairName] = { id: pairName, - options: [from, to], + options: [fromOptions, toOptions], rate: parseFloat(each.rate_we_sell) }; } diff --git a/common/api/shapeshift.ts b/common/api/shapeshift.ts new file mode 100644 index 00000000..ba97be59 --- /dev/null +++ b/common/api/shapeshift.ts @@ -0,0 +1,175 @@ +import { checkHttpStatus, parseJSON } from 'api/utils'; + +const SHAPESHIFT_BASE_URL = 'https://shapeshift.io'; + +export const SHAPESHIFT_TOKEN_WHITELIST = [ + 'OMG', + 'REP', + 'SNT', + 'SNGLS', + 'ZRX', + 'SWT', + 'ANT', + 'BAT', + 'BNT', + 'CVC', + 'DNT', + '1ST', + 'GNO', + 'GNT', + 'EDG', + 'FUN', + 'RLC', + 'TRST', + 'GUP', + 'ETH' +]; +export const SHAPESHIFT_WHITELIST = [...SHAPESHIFT_TOKEN_WHITELIST, 'ETC', 'BTC']; + +class ShapeshiftService { + public whitelist = SHAPESHIFT_WHITELIST; + private url = SHAPESHIFT_BASE_URL; + private apiKey = '0ca1ccd50b708a3f8c02327f0caeeece06d3ddc1b0ac749a987b453ee0f4a29bdb5da2e53bc35e57fb4bb7ae1f43c93bb098c3c4716375fc1001c55d8c94c160'; + private postHeaders = { + 'Content-Type': 'application/json' + }; + + public checkStatus(address) { + return fetch(`${this.url}/txStat/${address}`) + .then(checkHttpStatus) + .then(parseJSON); + } + + public sendAmount(withdrawal, originKind, destinationKind, destinationAmount) { + const pair = `${originKind.toLowerCase()}_${destinationKind.toLowerCase()}`; + + return fetch(`${this.url}/sendamount`, { + method: 'POST', + body: JSON.stringify({ + amount: destinationAmount, + pair, + apiKey: this.apiKey, + withdrawal + }), + headers: new Headers(this.postHeaders) + }) + .then(checkHttpStatus) + .then(parseJSON); + } + + public getCoins() { + return fetch(`${this.url}/getcoins`) + .then(checkHttpStatus) + .then(parseJSON); + } + + public getAllRates = async () => { + const marketInfo = await this.getMarketInfo(); + const pairRates = await this.getPairRates(marketInfo); + const checkAvl = await this.checkAvl(pairRates); + const mappedRates = this.mapMarketInfo(checkAvl); + return mappedRates; + }; + + private getPairRates(marketInfo) { + const filteredMarketInfo = marketInfo.filter(obj => { + const { pair } = obj; + const pairArr = pair.split('_'); + return this.whitelist.includes(pairArr[0]) && this.whitelist.includes(pairArr[1]) + ? true + : false; + }); + const pairRates = filteredMarketInfo.map(p => { + const { pair } = p; + const singlePair = Promise.resolve(this.getSinglePairRate(pair)); + return { ...p, ...singlePair }; + }); + return pairRates; + } + + private async checkAvl(pairRates) { + const avlCoins = await this.getAvlCoins(); + const mapAvl = pairRates.map(p => { + const { pair } = p; + const pairArr = pair.split('_'); + + if (pairArr[0] in avlCoins && pairArr[1] in avlCoins) { + return { + ...p, + ...{ + [pairArr[0]]: { + name: avlCoins[pairArr[0]].name, + status: avlCoins[pairArr[0]].status, + image: avlCoins[pairArr[0]].image + }, + [pairArr[1]]: { + name: avlCoins[pairArr[1]].name, + status: avlCoins[pairArr[1]].status, + image: avlCoins[pairArr[1]].image + } + } + }; + } + }); + return mapAvl; + } + + private getAvlCoins() { + return fetch(`${this.url}/getcoins`) + .then(checkHttpStatus) + .then(parseJSON); + } + + private getSinglePairRate(pair) { + return fetch(`${this.url}/rate/${pair}`) + .then(checkHttpStatus) + .then(parseJSON); + } + + private getMarketInfo() { + return fetch(`${this.url}/marketinfo`) + .then(checkHttpStatus) + .then(parseJSON); + } + + private isWhitelisted(coin) { + return this.whitelist.includes(coin); + } + + private mapMarketInfo(marketInfo) { + const tokenMap = {}; + marketInfo.forEach(m => { + const originKind = m.pair.substring(0, 3); + const destinationKind = m.pair.substring(4, 7); + if (this.isWhitelisted(originKind) && this.isWhitelisted(destinationKind)) { + const pairName = originKind + destinationKind; + const { rate, limit, min } = m; + tokenMap[pairName] = { + id: pairName, + options: [ + { + id: originKind, + status: m[originKind].status, + image: m[originKind].image, + name: m[originKind].name + }, + { + id: destinationKind, + status: m[destinationKind].status, + image: m[destinationKind].image, + name: m[destinationKind].name + } + ], + rate, + limit, + min + }; + } + }); + return tokenMap; + } +} + +const shapeshift = new ShapeshiftService(); + +export default shapeshift; diff --git a/common/assets/images/augur.png b/common/assets/images/augur.png new file mode 100644 index 00000000..8e69dd53 Binary files /dev/null and b/common/assets/images/augur.png differ diff --git a/common/assets/images/bitcoin.png b/common/assets/images/bitcoin.png new file mode 100644 index 00000000..19470098 Binary files /dev/null and b/common/assets/images/bitcoin.png differ diff --git a/common/assets/images/ether.png b/common/assets/images/ether.png new file mode 100644 index 00000000..e2b73362 Binary files /dev/null and b/common/assets/images/ether.png differ diff --git a/common/assets/images/logo-shapeshift.svg b/common/assets/images/logo-shapeshift.svg new file mode 100644 index 00000000..6cdbce03 --- /dev/null +++ b/common/assets/images/logo-shapeshift.svg @@ -0,0 +1,147 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/common/assets/images/shapeshift-dark.svg b/common/assets/images/shapeshift-dark.svg new file mode 100644 index 00000000..bec6eeed --- /dev/null +++ b/common/assets/images/shapeshift-dark.svg @@ -0,0 +1 @@ +shapeshift_logo \ No newline at end of file diff --git a/common/assets/images/swap.svg b/common/assets/images/swap.svg new file mode 100644 index 00000000..07c4b0f3 --- /dev/null +++ b/common/assets/images/swap.svg @@ -0,0 +1,44 @@ + + + + swap + Created with Sketch. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/common/components/AddressField.tsx b/common/components/AddressField.tsx new file mode 100644 index 00000000..ad62a4b2 --- /dev/null +++ b/common/components/AddressField.tsx @@ -0,0 +1,18 @@ +import React from 'react'; +import { AddressFieldFactory } from './AddressFieldFactory'; +import { donationAddressMap } from 'config/data'; + +export const AddressField: React.SFC<{}> = () => ( + ( + + )} + /> +); diff --git a/common/components/AddressField/index.ts b/common/components/AddressField/index.ts deleted file mode 100644 index 3164294b..00000000 --- a/common/components/AddressField/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './AddressField'; diff --git a/common/components/AddressField/AddressField.tsx b/common/components/AddressFieldFactory/AddressFieldFactory.tsx similarity index 52% rename from common/components/AddressField/AddressField.tsx rename to common/components/AddressFieldFactory/AddressFieldFactory.tsx index 87e098a4..813c8ea1 100644 --- a/common/components/AddressField/AddressField.tsx +++ b/common/components/AddressFieldFactory/AddressFieldFactory.tsx @@ -1,8 +1,9 @@ import { Query } from 'components/renderCbs'; import { setCurrentTo, TSetCurrentTo } from 'actions/transaction'; -import { AddressInput } from './AddressInput'; +import { AddressInputFactory } from './AddressInputFactory'; import React from 'react'; import { connect } from 'react-redux'; +import { ICurrentTo } from 'selectors/transaction'; interface DispatchProps { setCurrentTo: TSetCurrentTo; @@ -10,12 +11,20 @@ interface DispatchProps { interface OwnProps { to: string | null; + withProps(props: CallbackProps): React.ReactElement | null; +} + +export interface CallbackProps { + isValid: boolean; + readOnly: boolean; + currentTo: ICurrentTo; + onChange(ev: React.FormEvent): void; } type Props = DispatchProps & DispatchProps & OwnProps; //TODO: add ens resolving -class AddressFieldClass extends React.Component { +class AddressFieldFactoryClass extends React.Component { public componentDidMount() { // this 'to' parameter can be either token or actual field related const { to } = this.props; @@ -25,7 +34,7 @@ class AddressFieldClass extends React.Component { } public render() { - return ; + return ; } private setAddress = (ev: React.FormEvent) => { @@ -34,10 +43,14 @@ class AddressFieldClass extends React.Component { }; } -const AddressField = connect(null, { setCurrentTo })(AddressFieldClass); +const AddressField = connect(null, { setCurrentTo })(AddressFieldFactoryClass); -const DefaultAddressField: React.SFC<{}> = () => ( - } /> +interface DefaultAddressFieldProps { + withProps(props: CallbackProps): React.ReactElement | null; +} + +const DefaultAddressField: React.SFC = ({ withProps }) => ( + } /> ); -export { DefaultAddressField as AddressField }; +export { DefaultAddressField as AddressFieldFactory }; diff --git a/common/components/AddressField/AddressInput.tsx b/common/components/AddressFieldFactory/AddressInputFactory.tsx similarity index 65% rename from common/components/AddressField/AddressInput.tsx rename to common/components/AddressFieldFactory/AddressInputFactory.tsx index 9d91c1aa..3005de68 100644 --- a/common/components/AddressField/AddressInput.tsx +++ b/common/components/AddressFieldFactory/AddressInputFactory.tsx @@ -3,10 +3,10 @@ import { Identicon } from 'components/ui'; import translate from 'translations'; //import { EnsAddress } from './components'; import { Query } from 'components/renderCbs'; -import { donationAddressMap } from 'config/data'; import { ICurrentTo, getCurrentTo, isValidCurrentTo } from 'selectors/transaction'; import { connect } from 'react-redux'; import { AppState } from 'reducers'; +import { CallbackProps } from 'components/AddressFieldFactory'; interface StateProps { currentTo: ICurrentTo; @@ -14,14 +14,15 @@ interface StateProps { } interface OwnProps { onChange(ev: React.FormEvent): void; + withProps(props: CallbackProps): React.ReactElement | null; } type Props = OwnProps & StateProps; //TODO: ENS handling -class AddressInputClass extends Component { +class AddressInputFactoryClass extends Component { public render() { - const { currentTo, onChange, isValid } = this.props; + const { currentTo, onChange, isValid, withProps } = this.props; const { raw } = currentTo; return (
@@ -29,16 +30,9 @@ class AddressInputClass extends Component { ( - - )} + withQuery={({ readOnly }) => + withProps({ currentTo, isValid, onChange, readOnly: !!readOnly }) + } /> {/**/}
@@ -50,7 +44,7 @@ class AddressInputClass extends Component { } } -export const AddressInput = connect((state: AppState) => ({ +export const AddressInputFactory = connect((state: AppState) => ({ currentTo: getCurrentTo(state), isValid: isValidCurrentTo(state) -}))(AddressInputClass); +}))(AddressInputFactoryClass); diff --git a/common/components/AddressField/components/EnsAddress.tsx b/common/components/AddressFieldFactory/components/EnsAddress.tsx similarity index 100% rename from common/components/AddressField/components/EnsAddress.tsx rename to common/components/AddressFieldFactory/components/EnsAddress.tsx diff --git a/common/components/AddressField/components/index.ts b/common/components/AddressFieldFactory/components/index.ts similarity index 100% rename from common/components/AddressField/components/index.ts rename to common/components/AddressFieldFactory/components/index.ts diff --git a/common/components/AddressFieldFactory/index.ts b/common/components/AddressFieldFactory/index.ts new file mode 100644 index 00000000..3bff0133 --- /dev/null +++ b/common/components/AddressFieldFactory/index.ts @@ -0,0 +1 @@ +export * from './AddressFieldFactory'; diff --git a/common/components/Header/components/NavigationLink.scss b/common/components/Header/components/NavigationLink.scss index 2b481ee8..e04c4571 100644 --- a/common/components/Header/components/NavigationLink.scss +++ b/common/components/Header/components/NavigationLink.scss @@ -44,3 +44,17 @@ } } } + +#NAV_Swap a:before { + content:""; + display: inline-block; + margin-top: -.1rem; + width: 1.3rem; + height: 1.3rem; + background-image: url('~assets/images/swap.svg'); + background-position: center; + background-repeat: no-repeat; + background-size: contain; + vertical-align: middle; + margin-right: 4px; +} diff --git a/common/components/Header/components/NavigationLink.tsx b/common/components/Header/components/NavigationLink.tsx index 6761fe58..8d6f0a48 100644 --- a/common/components/Header/components/NavigationLink.tsx +++ b/common/components/Header/components/NavigationLink.tsx @@ -44,7 +44,11 @@ class NavigationLink extends React.Component { ); - return
  • {linkEl}
  • ; + return ( +
  • + {linkEl} +
  • + ); } } diff --git a/common/components/ui/SwapDropdown.scss b/common/components/ui/SwapDropdown.scss new file mode 100644 index 00000000..8181500d --- /dev/null +++ b/common/components/ui/SwapDropdown.scss @@ -0,0 +1,112 @@ +@import 'common/sass/variables'; + +.SwapDropdown { + position: relative; + button { + box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05); + border: 1px solid #ccc; + padding: 0.4rem 1rem; + border-radius: 2px; + &:focus { + outline: none; + } + &:active, &:hover { + opacity: 0.8; + } + > li { + margin: 0; + &:first-child { + padding-top: 4px; + } + &:last-child { + padding-bottom: 4px; + } + > a { + font-weight: 300; + &.active { + color: $link-color; + } + } + } + } +} + +.SwapDropdown-grid { + position: absolute; + display: none; + padding: 0; + margin-bottom: 0; + min-width: 500px; + left: 50%; + + top: 50px; + transform: translateX(-50%); + list-style: none; + font-size: 0.8rem; + text-align: left; + z-index: 500; + background: white; + box-shadow: 2px 1px 60px rgba(0,0,0,.175); + + &::before { + content: ""; + position: absolute; + top: -20px; + left: 50%; + transform: translateX(-50%); + border-right: 10px solid transparent; + border-left: 10px solid transparent; + border-top: 10px solid transparent; + border-bottom: 10px solid #fff; + } + + &.open { + display: block; + } + li { + display: inline-block; + width: 33.3%; + margin-bottom: 0; + } + li > a { + display: block; + clear: both; + padding: 5px 20px; + color: #163151; + &:hover { + opacity: .8; + background-color: #163151; + color: #fff; + } + } + .inactive { + a { + color: grey; + &:hover { + background-color: #fff; + color:#163151; + cursor: not-allowed; + } + } + img { + filter: grayscale(100%); + } + } + strong { + margin-left: 5px; + } + @media screen and (max-width: 800px) { + min-width: 300px; + } +} + +.SwapDropdown-desc { + display: inline-block; +} + +.SwapDropdown-item { + position: relative; + img { + padding-right: 1px; + } +} \ No newline at end of file diff --git a/common/components/ui/SwapDropdown.tsx b/common/components/ui/SwapDropdown.tsx new file mode 100644 index 00000000..2347ff2c --- /dev/null +++ b/common/components/ui/SwapDropdown.tsx @@ -0,0 +1,100 @@ +import React, { Component } from 'react'; +import './SwapDropdown.scss'; +import classnames from 'classnames'; + +export interface SingleCoin { + id: string; + name: string; + image: string; + status: string; +} + +interface Props { + ariaLabel: string; + options: SingleCoin[]; + value: string; + onChange(value: T): void; +} + +class SwapDropdown extends Component, {}> { + public state = { + open: false + }; + + private dropdown: HTMLElement | null; + + public componentDidMount() { + document.addEventListener('click', this.clickHandler); + } + + public componentWillUnmount() { + document.removeEventListener('click', this.clickHandler); + } + + public handleClickOutside() { + this.toggleDropdown(); + } + + public render() { + const { open } = this.state; + const { options, value } = this.props; + const dropdownGrid = classnames(open && 'open', 'SwapDropdown-grid'); + + const mappedCoins = options.sort((a, b) => (a.id > b.id ? 1 : -1)).map((coin: SingleCoin) => { + const cn = classnames(coin.status !== 'available' && 'inactive', 'SwapDropdown-item'); + return ( +
  • + + + {/*
    */} + {coin.id} +
    + {coin.name} + {/*
    */} +
    +
  • + ); + }); + return ( +
    (this.dropdown = el)}> + +
      {mappedCoins}
    +
    + ); + } + private toggleDropdown = () => { + this.setState({ + open: !this.state.open + }); + }; + + private onChange = (value: any) => { + this.props.onChange(value); + if (this.state.open) { + this.setState({ + open: false + }); + } + }; + + private clickHandler = (ev: Event) => { + if (!this.state.open || !this.dropdown) { + return; + } + + if ( + this.dropdown !== ev.target && + ev.target instanceof HTMLElement && + !this.dropdown.contains(ev.target) + ) { + this.setState({ + open: false + }); + } + }; +} + +export default SwapDropdown; diff --git a/common/components/ui/index.ts b/common/components/ui/index.ts index 64461f20..759477fb 100644 --- a/common/components/ui/index.ts +++ b/common/components/ui/index.ts @@ -8,6 +8,7 @@ export { default as QRCode } from './QRCode'; export { default as NewTabLink } from './NewTabLink'; export { default as UnitDisplay } from './UnitDisplay'; export { default as Spinner } from './Spinner'; +export { default as SwapDropdown } from './SwapDropdown'; export { default as Tooltip } from './Tooltip'; export * from './ConditionalInput'; export * from './Aux'; diff --git a/common/config/data.ts b/common/config/data.ts index 9d6eb2ff..2c001b48 100644 --- a/common/config/data.ts +++ b/common/config/data.ts @@ -46,6 +46,8 @@ export const MINIMUM_PASSWORD_LENGTH = 9; export const knowledgeBaseURL = 'https://myetherwallet.github.io/knowledge-base'; export const bityReferralURL = 'https://bity.com/af/jshkb37v'; +// Note: add the real referral url once you know it +export const shapeshiftReferralURL = 'https://shapeshift.io'; export const ledgerReferralURL = 'https://www.ledgerwallet.com/r/fa4b?path=/products/'; export const trezorReferralURL = 'https://trezor.io/?a=myetherwallet.com'; export const bitboxReferralURL = 'https://digitalbitbox.com/?ref=mew'; diff --git a/common/containers/Tabs/Swap/components/CurrencySwap.scss b/common/containers/Tabs/Swap/components/CurrencySwap.scss index 244f21cc..42883eb0 100644 --- a/common/containers/Tabs/Swap/components/CurrencySwap.scss +++ b/common/containers/Tabs/Swap/components/CurrencySwap.scss @@ -54,3 +54,6 @@ margin-top: $space * 2.5; } } + + + diff --git a/common/containers/Tabs/Swap/components/CurrencySwap.tsx b/common/containers/Tabs/Swap/components/CurrencySwap.tsx index b9566877..368f4a25 100644 --- a/common/containers/Tabs/Swap/components/CurrencySwap.tsx +++ b/common/containers/Tabs/Swap/components/CurrencySwap.tsx @@ -1,34 +1,42 @@ -import { TChangeStepSwap, TInitSwap } from 'actions/swap'; -import { NormalizedBityRates, NormalizedOptions, SwapInput } from 'reducers/swap/types'; +import { TChangeStepSwap, TInitSwap, TChangeSwapProvider, ProviderName } from 'actions/swap'; +import { + NormalizedBityRates, + NormalizedShapeshiftRates, + NormalizedOptions, + SwapInput +} from 'reducers/swap/types'; import SimpleButton from 'components/ui/SimpleButton'; import bityConfig, { generateKindMax, generateKindMin, WhitelistedCoins } from 'config/bity'; import React, { Component } from 'react'; import translate from 'translations'; import { combineAndUpper } from 'utils/formatters'; -import { Dropdown } from 'components/ui'; +import { SwapDropdown } from 'components/ui'; import Spinner from 'components/ui/Spinner'; -import intersection from 'lodash/intersection'; -import without from 'lodash/without'; +import { merge, reject, debounce } from 'lodash'; import './CurrencySwap.scss'; export interface StateProps { bityRates: NormalizedBityRates; + shapeshiftRates: NormalizedShapeshiftRates; + provider: ProviderName; options: NormalizedOptions; } export interface ActionProps { changeStepSwap: TChangeStepSwap; initSwap: TInitSwap; + swapProvider: TChangeSwapProvider; } interface State { disabled: boolean; origin: SwapInput; destination: SwapInput; - originKindOptions: WhitelistedCoins[]; - destinationKindOptions: WhitelistedCoins[]; + originKindOptions: any[]; + destinationKindOptions: any[]; originErr: string; destinationErr: string; + timeout: boolean; } type Props = StateProps & ActionProps; @@ -36,26 +44,63 @@ type Props = StateProps & ActionProps; export default class CurrencySwap extends Component { public state = { disabled: true, - origin: { id: 'BTC', amount: NaN } as SwapInput, - destination: { id: 'ETH', amount: NaN } as SwapInput, - originKindOptions: ['BTC', 'ETH'] as WhitelistedCoins[], - destinationKindOptions: ['ETH'] as WhitelistedCoins[], + origin: { + id: 'BTC', + status: 'available', + image: 'https://shapeshift.io/images/coins/bitcoin.png', + amount: NaN + } as SwapInput, + destination: { + id: 'ETH', + status: 'available', + image: 'https://shapeshift.io/images/coins/ether.png', + amount: NaN + } as SwapInput, + originKindOptions: [], + destinationKindOptions: [], originErr: '', - destinationErr: '' + destinationErr: '', + timeout: false }; - public componentDidUpdate(prevProps: Props, prevState: State) { - const { origin, destination } = this.state; + public debouncedCreateErrString = debounce((origin, destination, showError) => { + const createErrString = ( + originKind: WhitelistedCoins, + amount: number, + destKind: WhitelistedCoins + ) => { + const rate = this.getMinMax(originKind, destKind); + let errString; + if (amount > rate.max) { + errString = `Maximum ${rate.max} ${originKind}`; + } else { + errString = `Minimum ${rate.min} ${originKind}`; + } + return errString; + }; + const originErr = showError ? createErrString(origin.id, origin.amount, destination.id) : ''; + const destinationErr = showError + ? createErrString(destination.id, destination.amount, origin.id) + : ''; + this.setErrorMessages(originErr, destinationErr); + }, 1000); + + public componentDidMount() { + setTimeout(() => { + this.setState({ + timeout: true + }); + }, 10000); + + const { origin } = this.state; const { options } = this.props; - if (origin !== prevState.origin) { - this.setDisabled(origin, destination); - } - if (options.allIds !== prevProps.options.allIds) { - const originKindOptions: WhitelistedCoins[] = intersection( - options.allIds, - this.state.originKindOptions + + if (options.allIds && options.byId) { + const originKindOptions: any[] = Object.values(options.byId); + const destinationKindOptions: any[] = Object.values( + reject(options.byId, o => o.id === origin.id) ); - const destinationKindOptions: WhitelistedCoins[] = without(options.allIds, origin.id); + this.setState({ originKindOptions, destinationKindOptions @@ -63,54 +108,110 @@ export default class CurrencySwap extends Component { } } - public getMinMax = (kind: WhitelistedCoins) => { + public componentDidUpdate(prevProps: Props, prevState: State) { + const { origin, destination } = this.state; + const { options, bityRates, shapeshiftRates } = this.props; + if (origin !== prevState.origin) { + this.setDisabled(origin, destination); + } + + const originCap = origin.id.toUpperCase(); + const destCap = destination.id.toUpperCase(); + const { provider } = this.props; + + const ensureCorrectProvider = + (originCap === 'BTC' && destCap === 'ETH') || (destCap === 'BTC' && originCap === 'ETH'); + const ensureBityRatesLoaded = + bityRates.allIds.includes('ETHBTC') && bityRates.allIds.includes('BTCETH'); + const ensureShapeshiftRatesLoaded = shapeshiftRates.allIds.length > 0; + + if (ensureBityRatesLoaded && ensureCorrectProvider) { + if (provider === 'shapeshift') { + this.props.swapProvider('bity'); + } + } else if (ensureShapeshiftRatesLoaded) { + if (provider !== 'shapeshift') { + this.props.swapProvider('shapeshift'); + } + } + + if (options.allIds !== prevProps.options.allIds && options.byId) { + const originKindOptions: any[] = Object.values(options.byId); + const destinationKindOptions: any[] = Object.values( + reject(options.byId, o => o.id === origin.id) + ); + + this.setState({ + originKindOptions, + destinationKindOptions + }); + } + } + + public rateMixer = () => { + const { shapeshiftRates, bityRates } = this.props; + return merge(shapeshiftRates, bityRates); + }; + + public getMinMax = (originKind: WhitelistedCoins, destinationKind) => { let min; let max; - if (kind !== 'BTC') { - const bityPairRate = this.props.bityRates.byId['BTC' + kind].rate; - min = generateKindMin(bityPairRate, kind); - max = generateKindMax(bityPairRate, kind); + + const { provider, bityRates } = this.props; + + if (provider === 'bity' && bityRates.allIds.length > 2) { + if (originKind !== 'BTC') { + const pairRate = this.rateMixer().byId['BTC' + originKind].rate; + min = generateKindMin(pairRate, originKind); + max = generateKindMax(pairRate, originKind); + } else { + min = bityConfig.BTCMin; + max = bityConfig.BTCMax; + } } else { - min = bityConfig.BTCMin; - max = bityConfig.BTCMax; + const pair = (this.rateMixer() as NormalizedShapeshiftRates).byId[ + originKind + destinationKind + ]; + min = pair.min; + max = pair.limit; } return { min, max }; }; - public isMinMaxValid = (amount: number, kind: WhitelistedCoins) => { - const rate = this.getMinMax(kind); - const higherThanMin = amount >= rate.min; - const lowerThanMax = amount <= rate.max; + public isMinMaxValid = (originAmount: number, originKind: WhitelistedCoins, destinationKind) => { + const rate = this.getMinMax(originKind, destinationKind); + const higherThanMin = originAmount >= rate.min; + const lowerThanMax = originAmount <= rate.max; return higherThanMin && lowerThanMax; }; public setDisabled(origin: SwapInput, destination: SwapInput) { + this.clearErrMessages(); const amountsValid = origin.amount && destination.amount; - const minMaxValid = this.isMinMaxValid(origin.amount, origin.id); - + const minMaxValid = this.isMinMaxValid(origin.amount as number, origin.id, destination.id); const disabled = !(amountsValid && minMaxValid); - - const createErrString = (kind: WhitelistedCoins, amount: number) => { - const rate = this.getMinMax(kind); - let errString; - if (amount > rate.max) { - errString = `Maximum ${rate.max} ${kind}`; - } else { - errString = `Minimum ${rate.min} ${kind}`; - } - return errString; - }; - const showError = disabled && amountsValid; - const originErr = showError ? createErrString(origin.id, origin.amount) : ''; - const destinationErr = showError ? createErrString(destination.id, destination.amount) : ''; this.setState({ - disabled, + disabled + }); + + this.debouncedCreateErrString(origin, destination, showError); + } + + public setErrorMessages = (originErr, destinationErr) => { + this.setState({ originErr, destinationErr }); - } + }; + + public clearErrMessages = () => { + this.setState({ + originErr: '', + destinationErr: '' + }); + }; public onClickStartSwap = () => { const { origin, destination } = this.state; @@ -129,8 +230,8 @@ export default class CurrencySwap extends Component { public updateOriginAmount = (origin: SwapInput, destination: SwapInput, amount: number) => { if (amount || amount === 0) { const pairName = combineAndUpper(origin.id, destination.id); - const bityRate = this.props.bityRates.byId[pairName].rate; - const destinationAmount = amount * bityRate; + const rate = this.rateMixer().byId[pairName].rate; + const destinationAmount = amount * rate; this.setState({ origin: { ...this.state.origin, amount }, destination: { ...this.state.destination, amount: destinationAmount } @@ -143,8 +244,8 @@ export default class CurrencySwap extends Component { public updateDestinationAmount = (origin: SwapInput, destination: SwapInput, amount: number) => { if (amount || amount === 0) { const pairNameReversed = combineAndUpper(destination.id, origin.id); - const bityRate = this.props.bityRates.byId[pairNameReversed].rate; - const originAmount = amount * bityRate; + const rate = this.rateMixer().byId[pairNameReversed].rate; + const originAmount = amount * rate; this.setState({ origin: { ...this.state.origin, amount: originAmount }, destination: { @@ -168,69 +269,80 @@ export default class CurrencySwap extends Component { public onChangeOriginKind = (newOption: WhitelistedCoins) => { const { origin, destination, destinationKindOptions } = this.state; - const newDestinationAmount = () => { - const pairName = combineAndUpper(destination.id, origin.id); - const bityRate = this.props.bityRates.byId[pairName].rate; - return bityRate * origin.amount; + const { options, initSwap } = this.props; + + const newOrigin = { ...origin, id: newOption, amount: '' }; + const newDest = { + id: newOption === destination.id ? origin.id : destination.id, + amount: '' }; this.setState({ - origin: { ...origin, id: newOption }, - destination: { - id: newOption === destination.id ? origin.id : destination.id, - amount: newDestinationAmount() ? newDestinationAmount() : destination.amount - }, - destinationKindOptions: without([...destinationKindOptions, origin.id], newOption) + origin: newOrigin, + destination: newDest, + destinationKindOptions: reject( + [...destinationKindOptions, options.byId[origin.id]], + o => o.id === newOption + ) }); + + initSwap({ origin: newOrigin, destination: newDest }); }; public onChangeDestinationKind = (newOption: WhitelistedCoins) => { + const { initSwap } = this.props; const { origin, destination } = this.state; - const newOriginAmount = () => { - const pairName = combineAndUpper(newOption, origin.id); - const bityRate = this.props.bityRates.byId[pairName].rate; - return bityRate * destination.amount; + + const newOrigin = { + ...origin, + amount: '' }; + + const newDest = { ...destination, id: newOption, amount: '' }; this.setState({ - origin: { - ...origin, - amount: newOriginAmount() ? newOriginAmount() : origin.amount - }, - destination: { ...destination, id: newOption } + origin: newOrigin, + destination: newDest }); + + initSwap({ origin: newOrigin, destination: newDest }); }; public render() { - const { bityRates } = this.props; + const { bityRates, shapeshiftRates, provider } = this.props; const { origin, destination, originKindOptions, destinationKindOptions, originErr, - destinationErr + destinationErr, + timeout } = this.state; - - const OriginKindDropDown = Dropdown as new () => Dropdown; - const DestinationKindDropDown = Dropdown as new () => Dropdown; + const OriginKindDropDown = SwapDropdown as new () => SwapDropdown; + const DestinationKindDropDown = SwapDropdown as new () => SwapDropdown; const pairName = combineAndUpper(origin.id, destination.id); - const bityLoaded = bityRates.byId[pairName] ? bityRates.byId[pairName].id : false; + const bityLoaded = bityRates.byId && bityRates.byId[pairName] ? true : false; + const shapeshiftLoaded = shapeshiftRates.byId && shapeshiftRates.byId[pairName] ? true : false; + // This ensures both are loaded + const loaded = provider === 'shapeshift' ? shapeshiftLoaded : bityLoaded && shapeshiftLoaded; + const timeoutLoaded = (bityLoaded && timeout) || (shapeshiftLoaded && timeout); return (

    {translate('SWAP_init_1')}

    - {bityLoaded ? ( + {loaded || timeoutLoaded ? (
    {originErr && {originErr}}
    @@ -239,7 +351,6 @@ export default class CurrencySwap extends Component { options={originKindOptions} value={origin.id} onChange={this.onChangeOriginKind} - color="default" />
    @@ -251,13 +362,14 @@ export default class CurrencySwap extends Component {
    @@ -266,7 +378,6 @@ export default class CurrencySwap extends Component { options={destinationKindOptions} value={destination.id} onChange={this.onChangeDestinationKind} - color="default" />
    diff --git a/common/containers/Tabs/Swap/components/CurrentRates.scss b/common/containers/Tabs/Swap/components/CurrentRates.scss index 50d7dd10..413c3b81 100644 --- a/common/containers/Tabs/Swap/components/CurrentRates.scss +++ b/common/containers/Tabs/Swap/components/CurrentRates.scss @@ -9,7 +9,7 @@ &-panel { position: relative; - margin: 0 auto $space * 2; + margin: 0 auto 0; background: linear-gradient(150deg, $ether-blue, $ether-navy); @include mono; diff --git a/common/containers/Tabs/Swap/components/CurrentRates.tsx b/common/containers/Tabs/Swap/components/CurrentRates.tsx index 66d017c7..6c7686c3 100644 --- a/common/containers/Tabs/Swap/components/CurrentRates.tsx +++ b/common/containers/Tabs/Swap/components/CurrentRates.tsx @@ -1,84 +1,136 @@ -import { NormalizedBityRate } from 'reducers/swap/types'; +import { + NormalizedBityRates, + NormalizedShapeshiftRates, + NormalizedShapeshiftRate +} from 'reducers/swap/types'; import bityLogoWhite from 'assets/images/logo-bity-white.svg'; +import shapeshiftLogoWhite from 'assets/images/logo-shapeshift.svg'; import Spinner from 'components/ui/Spinner'; -import { bityReferralURL } from 'config/data'; +import { bityReferralURL, shapeshiftReferralURL } from 'config/data'; import React, { Component } from 'react'; import translate from 'translations'; -import { toFixedIfLarger } from 'utils/formatters'; import './CurrentRates.scss'; +import { SHAPESHIFT_WHITELIST } from 'api/shapeshift'; +import { ProviderName } from 'actions/swap'; +import sample from 'lodash/sample'; +import times from 'lodash/times'; +import Rates from './Rates'; interface Props { - [id: string]: NormalizedBityRate; + provider: ProviderName; + bityRates: NormalizedBityRates; + shapeshiftRates: NormalizedShapeshiftRates; } -interface State { - ETHBTCAmount: number; - ETHREPAmount: number; - BTCETHAmount: number; - BTCREPAmount: number; -} +export default class CurrentRates extends Component { + private shapeShiftRateCache = null; -export default class CurrentRates extends Component { - public state = { - ETHBTCAmount: 1, - ETHREPAmount: 1, - BTCETHAmount: 1, - BTCREPAmount: 1 + public getRandomSSPairData = ( + shapeshiftRates: NormalizedShapeshiftRates + ): NormalizedShapeshiftRate => { + const coinOne = sample(SHAPESHIFT_WHITELIST) as string; + const coinTwo = sample(SHAPESHIFT_WHITELIST) as string; + const pair = coinOne + coinTwo; + const pairData = shapeshiftRates.byId[pair]; + if (pairData) { + return pairData; + } else { + // if random pairing is unavailable / missing in state + return this.getRandomSSPairData(shapeshiftRates); + } }; - public onChange = (event: any) => { - const { value } = event.target; - const { name } = event.target; - this.setState({ - [name]: value - }); + public buildSSPairs = (shapeshiftRates: NormalizedShapeshiftRates, n: number = 4) => { + const pairCollection = times(n, () => this.getRandomSSPairData(shapeshiftRates)); + const byId = pairCollection.reduce((acc, cur) => { + acc[cur.id] = cur; + return acc; + }, {}); + const allIds = pairCollection.map(SSData => SSData.id); + return { + byId, + allIds + }; }; - public buildPairRate = (origin: string, destination: string) => { - const pair = origin + destination; - const statePair = this.state[(pair + 'Amount') as keyof State]; - const propsPair = this.props[pair] ? this.props[pair].rate : null; - return ( -
    - {propsPair ? ( -
    - - - {` ${origin} = ${toFixedIfLarger(statePair * propsPair, 6)} ${destination}`} - -
    - ) : ( - - )} -
    - ); + public isValidRates = rates => { + return rates && rates.allIds && rates.allIds.length > 0; }; - public render() { + public setupRates = () => { + const { shapeshiftRates, bityRates, provider } = this.props; + + let fixedRates; + if (provider === 'bity') { + fixedRates = bityRates; + } else if (provider === 'shapeshift') { + // if ShapeShift rates are valid, filter to 4 random pairs + if (this.isValidRates(shapeshiftRates)) { + if (!this.shapeShiftRateCache) { + fixedRates = this.buildSSPairs(shapeshiftRates); + this.shapeShiftRateCache = fixedRates; + } else { + fixedRates = this.shapeShiftRateCache; + } + } else { + // else, pass along invalid rates. Child component will handle showing spinner until they become valid + fixedRates = shapeshiftRates; + } + } + + return fixedRates; + }; + + public swapEl = (providerURL, providerLogo, children) => { return (

    {translate('SWAP_rates')}

    -
    - {this.buildPairRate('ETH', 'BTC')} - {this.buildPairRate('ETH', 'REP')} -
    - -
    - {this.buildPairRate('BTC', 'ETH')} - {this.buildPairRate('BTC', 'REP')} -
    - - + {children} + +
    ); + }; + + public render() { + const { provider } = this.props; + const rates = this.setupRates(); + const providerLogo = provider === 'shapeshift' ? shapeshiftLogoWhite : bityLogoWhite; + const providerURL = provider === 'shapeshift' ? shapeshiftReferralURL : bityReferralURL; + + let children; + + if (this.isValidRates(rates)) { + children = ; + } else { + // TODO - de-dup + children = ( + <> +
    +
    + +
    +
    + +
    +
    + +
    +
    + +
    +
    + +
    +
    + + ); + } + + return this.swapEl(providerURL, providerLogo, children); } } diff --git a/common/containers/Tabs/Swap/components/LiteSend/Fields.tsx b/common/containers/Tabs/Swap/components/LiteSend/Fields.tsx new file mode 100644 index 00000000..45d4e08e --- /dev/null +++ b/common/containers/Tabs/Swap/components/LiteSend/Fields.tsx @@ -0,0 +1,100 @@ +import React, { Component } from 'react'; +import { AmountFieldFactory } from 'components/AmountFieldFactory'; +import { GasFieldFactory } from 'components/GasFieldFactory'; +import { AddressFieldFactory } from 'components/AddressFieldFactory'; +import { connect } from 'react-redux'; +import { AppState } from 'reducers'; + +import { Aux } from 'components/ui'; +import { GenerateTransaction, SendButton, SigningStatus } from 'components'; +import { resetWallet, TResetWallet } from 'actions/wallet'; +import translate from 'translations'; +import { getUnit } from 'selectors/transaction'; + +interface StateProps { + unit: string; + resetWallet: TResetWallet; +} + +type Props = StateProps; +class FieldsClass extends Component { + public render() { + return ( +
    +
    +
    + +
    +
    + ( + + )} + /> +
    +
    +
    + +
    +
    + + + ( + + {!isValid && ( +
    + WARNING: Your ether or token balance is not high enough to complete this + transaction! Please send more funds or switch to a different wallet +
    + )} + {isValid && ( + + )} +
    + )} + /> +
    +
    +
    +
    + + + ( + + )} + /> +
    +
    + +
    +
    + +
    +
    +
    + +
    +
    + ); + } + private changeWallet = () => { + this.props.resetWallet(); + }; +} + +export const Fields = connect((state: AppState) => ({ unit: getUnit(state) }), { resetWallet })( + FieldsClass +); diff --git a/common/containers/Tabs/Swap/components/LiteSend/LiteSend.tsx b/common/containers/Tabs/Swap/components/LiteSend/LiteSend.tsx new file mode 100644 index 00000000..385a2dbe --- /dev/null +++ b/common/containers/Tabs/Swap/components/LiteSend/LiteSend.tsx @@ -0,0 +1,62 @@ +import React, { Component } from 'react'; +import WalletDecrypt from 'components/WalletDecrypt'; +import { OnlyUnlocked } from 'components/renderCbs'; +import { Aux } from 'components/ui'; +import { Fields } from './Fields'; +import { isUnlocked as isUnlockedSelector } from 'selectors/wallet'; +import { getNetworkConfig } from 'selectors/config'; +import { configureLiteSend, TConfigureLiteSend } from 'actions/swap'; +import { connect } from 'react-redux'; +import { AppState } from 'reducers'; +import { shouldDisplayLiteSend } from 'selectors/swap'; +import { NetworkConfig } from 'config/data'; + +interface DispatchProps { + configureLiteSend: TConfigureLiteSend; +} + +interface StateProps { + shouldDisplay: boolean; + isUnlocked: boolean; + network: NetworkConfig; +} + +type Props = StateProps & DispatchProps; +class LiteSendClass extends Component { + public componentDidMount() { + this.props.configureLiteSend(); + } + + public render() { + if (!this.props.shouldDisplay) { + return null; + } + const { network, isUnlocked } = this.props; + let renderMe; + if (network.chainId !== 1) { + renderMe = ( +
    +
    +
    + WARNING: You are currently not on the Ethereum Mainnet. Please switch nodes in order + for the token swap to function as intended. +
    +
    +
    + ); + } else { + renderMe = isUnlocked ? } /> : ; + } + + return {renderMe}; + } +} + +export const LiteSend = connect( + (state: AppState) => ({ + shouldDisplay: shouldDisplayLiteSend(state), + isUnlocked: isUnlockedSelector(state), + network: getNetworkConfig(state) + }), + { configureLiteSend } +)(LiteSendClass); diff --git a/common/containers/Tabs/Swap/components/LiteSend/index.ts b/common/containers/Tabs/Swap/components/LiteSend/index.ts new file mode 100644 index 00000000..9aa0af19 --- /dev/null +++ b/common/containers/Tabs/Swap/components/LiteSend/index.ts @@ -0,0 +1 @@ +export * from './LiteSend'; diff --git a/common/containers/Tabs/Swap/components/PartThree.tsx b/common/containers/Tabs/Swap/components/PartThree.tsx index 81f155aa..4889b9cc 100644 --- a/common/containers/Tabs/Swap/components/PartThree.tsx +++ b/common/containers/Tabs/Swap/components/PartThree.tsx @@ -3,14 +3,17 @@ import { TRestartSwap, TStartOrderTimerSwap, TStartPollBityOrderStatus, + TStartPollShapeshiftOrderStatus, TStopOrderTimerSwap, - TStopPollBityOrderStatus + TStopPollBityOrderStatus, + TStopPollShapeshiftOrderStatus } from 'actions/swap'; import { SwapInput } from 'reducers/swap/types'; import React, { Component } from 'react'; import BitcoinQR from './BitcoinQR'; import PaymentInfo from './PaymentInfo'; import SwapProgress from './SwapProgress'; +import { LiteSend } from './LiteSend'; interface ReduxStateProps { destinationAddress: string; @@ -19,7 +22,9 @@ interface ReduxStateProps { reference: string; secondsRemaining: number | null; paymentAddress: string | null; - orderStatus: string | null; + provider: string; + bityOrderStatus: string | null; + shapeshiftOrderStatus: string | null; outputTx: any; } @@ -27,20 +32,28 @@ interface ReduxActionProps { restartSwap: TRestartSwap; startOrderTimerSwap: TStartOrderTimerSwap; startPollBityOrderStatus: TStartPollBityOrderStatus; - stopOrderTimerSwap: TStopOrderTimerSwap; stopPollBityOrderStatus: TStopPollBityOrderStatus; + startPollShapeshiftOrderStatus: TStartPollShapeshiftOrderStatus; + stopPollShapeshiftOrderStatus: TStopPollShapeshiftOrderStatus; + stopOrderTimerSwap: TStopOrderTimerSwap; showNotification: TShowNotification; } export default class PartThree extends Component { public componentDidMount() { - this.props.startPollBityOrderStatus(); + const { provider } = this.props; + if (provider === 'shapeshift') { + this.props.startPollShapeshiftOrderStatus(); + } else { + this.props.startPollBityOrderStatus(); + } this.props.startOrderTimerSwap(); } public componentWillUnmount() { this.props.stopOrderTimerSwap(); this.props.stopPollBityOrderStatus(); + this.props.stopPollShapeshiftOrderStatus(); } public render() { @@ -49,7 +62,9 @@ export default class PartThree extends Component + - {orderStatus === 'OPEN' && origin.id === 'BTC' && } + + + {OpenOrder && origin.id === 'BTC' && }
    ); } diff --git a/common/containers/Tabs/Swap/components/PaymentInfo.scss b/common/containers/Tabs/Swap/components/PaymentInfo.scss index 51ac2ab6..63a88736 100644 --- a/common/containers/Tabs/Swap/components/PaymentInfo.scss +++ b/common/containers/Tabs/Swap/components/PaymentInfo.scss @@ -9,14 +9,14 @@ margin: $space auto 0; max-width: 620px; width: 100%; - font-size: $font-size-medium-bump; + font-size: $font-size-medium; text-align: center; @include mono; } @media screen and (max-width: $screen-sm) { h1 { - font-size: $font-size-medium; + font-size: $font-size-base; } } } diff --git a/common/containers/Tabs/Swap/components/Rates.tsx b/common/containers/Tabs/Swap/components/Rates.tsx new file mode 100644 index 00000000..8a3c0ff0 --- /dev/null +++ b/common/containers/Tabs/Swap/components/Rates.tsx @@ -0,0 +1,129 @@ +import { NormalizedRates } from 'reducers/swap/types'; +import React, { Component } from 'react'; +import { toFixedIfLarger } from 'utils/formatters'; +import './CurrentRates.scss'; +import { ProviderName } from 'actions/swap'; +import { objectContainsObjectKeys } from 'utils/helpers'; + +interface RateInputProps { + rate: number; + amount: number | string; + pair: string; + origin: string; + destination: string; + onChange: any; +} + +export const RateInput: React.SFC = ({ + rate, + amount, + pair, + origin, + destination, + onChange +}) => { + return amount || amount === 0 || amount === '' ? ( +
    + + + {` ${origin} = ${toFixedIfLarger(+amount * rate, 6)} ${destination}`} + +
    + ) : null; +}; + +interface Props { + provider: ProviderName; + rates: NormalizedRates; +} + +interface State { + pairs: { [pair: string]: number }; +} + +export default class Rates extends Component { + public state = { + pairs: {} + }; + + public componentDidMount() { + this.setState({ pairs: this.getPairs() }); + } + + public componentDidUpdate() { + const newPairs = this.getPairs(); + // prevents endless loop. if state already contains new pairs, don't set state + if (!objectContainsObjectKeys(newPairs, this.state.pairs)) { + const pairs = { + ...this.state.pairs, + ...newPairs + }; + this.setState({ + pairs + }); + } + } + + public getPairs = () => { + const { rates } = this.props; + const { allIds } = rates; + return allIds.reduce((acc, cur) => { + acc[cur] = 1; + return acc; + }, {}); + }; + + public onChange = (event: any) => { + const { value } = event.target; + const { name } = event.target; + this.setState({ + pairs: { + ...this.state.pairs, + [name]: value + } + }); + }; + + public buildRateInputs = () => { + const { rates } = this.props; + const { pairs } = this.state; + + const fullData: RateInputProps[] = []; + + rates.allIds.forEach(each => { + fullData.push({ + rate: rates.byId[each].rate, + amount: pairs[each], + pair: each, + origin: rates.byId[each].options[0], + destination: rates.byId[each].options[1], + onChange: this.onChange + }); + }); + + // TODO - don't hardcode only first 4 elements of array. + // not likely to change until significant UI revamp, so not worth spending time on now + return ( +
    +
    + + +
    + +
    + + +
    +
    + ); + }; + + public render() { + return this.buildRateInputs(); + } +} diff --git a/common/containers/Tabs/Swap/components/ReceivingAddress.tsx b/common/containers/Tabs/Swap/components/ReceivingAddress.tsx index 9fdc4cad..302c6587 100644 --- a/common/containers/Tabs/Swap/components/ReceivingAddress.tsx +++ b/common/containers/Tabs/Swap/components/ReceivingAddress.tsx @@ -2,6 +2,7 @@ import { TBityOrderCreateRequestedSwap, TChangeStepSwap, TDestinationAddressSwap, + TShapeshiftOrderCreateRequestedSwap, TStopLoadBityRatesSwap } from 'actions/swap'; import { SwapInput } from 'reducers/swap/types'; @@ -19,6 +20,8 @@ export interface StateProps { destinationId: keyof typeof donationAddressMap; isPostingOrder: boolean; destinationAddress: string; + destinationKind: number; + provider: string; } export interface ActionProps { @@ -26,6 +29,7 @@ export interface ActionProps { changeStepSwap: TChangeStepSwap; stopLoadBityRatesSwap: TStopLoadBityRatesSwap; bityOrderCreateRequestedSwap: TBityOrderCreateRequestedSwap; + shapeshiftOrderCreateRequestedSwap: TShapeshiftOrderCreateRequestedSwap; } export default class ReceivingAddress extends Component { @@ -35,15 +39,24 @@ export default class ReceivingAddress extends Component { - const { origin, destinationId } = this.props; + const { origin, destinationId, destinationAddress, destinationKind, provider } = this.props; if (!origin) { return; } - this.props.bityOrderCreateRequestedSwap( - origin.amount, - this.props.destinationAddress, - combineAndUpper(origin.id, destinationId) - ); + if (provider === 'shapeshift') { + this.props.shapeshiftOrderCreateRequestedSwap( + destinationAddress, + origin.id, + destinationId, + destinationKind + ); + } else { + this.props.bityOrderCreateRequestedSwap( + origin.amount as number, + this.props.destinationAddress, + combineAndUpper(origin.id, destinationId) + ); + } }; public render() { @@ -77,7 +90,11 @@ export default class ReceivingAddress extends Component diff --git a/common/containers/Tabs/Swap/components/ShapeshiftBanner.scss b/common/containers/Tabs/Swap/components/ShapeshiftBanner.scss new file mode 100644 index 00000000..32421ece --- /dev/null +++ b/common/containers/Tabs/Swap/components/ShapeshiftBanner.scss @@ -0,0 +1,24 @@ +.ShapeshiftBanner { + display: block; + width: fit-content; + margin: auto; + margin-top: 16px; + margin-bottom: 16px; + padding: 0px 16px; + background-color: #3a526d; + border-radius: 3px; + box-shadow: 0 3px 8px 0 rgba(0,0,0,0.1), inset 0 0 3px 0 rgba(0,0,0,0.1); + p { + display: inline-block; + color: white; + vertical-align: middle; + margin-bottom: 0px; + } + img { + display: inline-block; + height: 32px; + padding: 8px; + box-sizing: content-box; + margin-left: 16px; + } +} \ No newline at end of file diff --git a/common/containers/Tabs/Swap/components/ShapeshiftBanner.tsx b/common/containers/Tabs/Swap/components/ShapeshiftBanner.tsx new file mode 100644 index 00000000..bd90b798 --- /dev/null +++ b/common/containers/Tabs/Swap/components/ShapeshiftBanner.tsx @@ -0,0 +1,12 @@ +import React from 'react'; +import './ShapeshiftBanner.scss'; +import shapeshiftSvg from 'assets/images/logo-shapeshift.svg'; + +export default () => ( +
    +

    + New Feature: Exchange coins & tokens +

    + +
    +); diff --git a/common/containers/Tabs/Swap/components/SupportFooter.scss b/common/containers/Tabs/Swap/components/SupportFooter.scss new file mode 100644 index 00000000..b0ca4a8e --- /dev/null +++ b/common/containers/Tabs/Swap/components/SupportFooter.scss @@ -0,0 +1,11 @@ +.SupportFooter { + text-align: center; + padding-top: 80px; + &-fallback { + padding: 20px 0; + textarea { + max-width: 35rem; + margin: auto; + } + } +} \ No newline at end of file diff --git a/common/containers/Tabs/Swap/components/SupportFooter.tsx b/common/containers/Tabs/Swap/components/SupportFooter.tsx new file mode 100644 index 00000000..34617a1f --- /dev/null +++ b/common/containers/Tabs/Swap/components/SupportFooter.tsx @@ -0,0 +1,99 @@ +import React from 'react'; +import './SupportFooter.scss'; +import { SwapInput } from 'actions/swap'; +import { NormalizedBityRates, NormalizedShapeshiftRates } from 'reducers/swap/types'; + +interface Props { + origin: SwapInput; + destination: SwapInput; + destinationAddress: string | null; + paymentAddress: string | null; + reference: string | null; + provider: string; + shapeshiftRates: NormalizedShapeshiftRates; + bityRates: NormalizedBityRates; +} + +class SupportFooter extends React.Component { + public state = { + open: false + }; + public render() { + const { open } = this.state; + const { + origin, + destination, + destinationAddress, + paymentAddress, + reference, + provider, + shapeshiftRates, + bityRates + } = this.props; + const pair = origin && destination ? origin.id + destination.id : 'BTCETH'; + const rates = provider === 'shapeshift' ? shapeshiftRates.byId : bityRates.byId; + const emailTo = + provider === 'shapeshift' + ? 'support@myetherwallet.com' + : 'support@myetherwallet.com,mew@bity.com'; + const mailSubject = encodeURI('Issue regarding my Swap via MEW'); + const serviceProvider = provider.charAt(0).toUpperCase() + provider.slice(1); + let mailBody; + let fallbackBody; + if (pair && rates && rates[pair]) { + mailBody = encodeURI(`Please include the below if this issue is regarding your order. + +Provider: ${serviceProvider} + +REF ID#: ${reference || ''} + +Amount to send: ${origin.amount || ''} ${origin.id} + +Amount to receive: ${destination.amount || ''} ${destination.id} + +Payment Address: ${paymentAddress || ''} + +Receiving Address: ${destinationAddress || ''} + +Rate: ${rates[pair].rate} ${origin.id}/${destination.id} + `); + fallbackBody = `To: ${emailTo} +Subject: Issue regarding my Swap via MEW +Message: +Provider: ${serviceProvider} +REF ID#: ${reference || ''} +Amount to send: ${origin.amount || ''} ${origin.id} +Amount to receive: ${destination.amount || ''} ${destination.id} +Payment Address: ${paymentAddress || ''} +Receiving Address: ${destinationAddress || ''} +Rate: ${rates[pair].rate} ${origin.id}/${destination.id}`; + } + return ( +
    + + Issue with your Swap? Contact support + +
    +

    + Click here if link doesn't work +

    + {open ? ( +