Implement Swap BTC page
This commit is contained in:
parent
8e6604cfa6
commit
9c808e2b5e
22 changed files with 751 additions and 10 deletions
10
flow-typed/CoinSwap.js
vendored
Normal file
10
flow-typed/CoinSwap.js
vendored
Normal file
|
@ -0,0 +1,10 @@
|
|||
declare type CoinSwapState = {
|
||||
btcAddresses: Array<string>
|
||||
};
|
||||
|
||||
declare type CoinSwapAction = {
|
||||
type: string,
|
||||
data: {
|
||||
btcAddress: string,
|
||||
},
|
||||
};
|
|
@ -1729,6 +1729,37 @@
|
|||
"lbry.tv is being retired in favor of %odysee% and new sign ups are disabled. Sign up on %odysee% instead": "lbry.tv is being retired in favor of %odysee% and new sign ups are disabled. Sign up on %odysee% instead",
|
||||
"lbry.tv is being retired in favor of %odysee%": "lbry.tv is being retired in favor of %odysee%",
|
||||
"You will have to switch to the %desktop_app% or %odysee% in the near future. Your existing login details will work on %odysee% and all of your %credits% and other settings will be there.": "You will have to switch to the %desktop_app% or %odysee% in the near future. Your existing login details will work on %odysee% and all of your %credits% and other settings will be there.",
|
||||
"Swap": "Swap",
|
||||
"Swap Bitcoin": "Swap Bitcoin",
|
||||
"Bitcoin": "Bitcoin",
|
||||
"Credits": "Credits",
|
||||
"Start Swap": "Start Swap",
|
||||
"To": "To",
|
||||
"Receiving": "Receiving",
|
||||
"Send bitcoin to the address provided and you will be sent an equivalent amount of Credits.": "Send bitcoin to the address provided and you will be sent an equivalent amount of Credits.",
|
||||
"Swap Bitcoin for %lbc%": "Swap Bitcoin for %lbc%",
|
||||
"Processing...": "Processing...",
|
||||
"View Past Swaps": "View Past Swaps",
|
||||
"Remove address": "Remove address",
|
||||
"Waiting for BTC": "Waiting for BTC",
|
||||
"Waiting to receive your bitcoin.": "Waiting to receive your bitcoin.",
|
||||
"Sending LBC": "Sending LBC",
|
||||
"Bitcoin received. Sending your LBC.": "Bitcoin received. Sending your LBC.",
|
||||
"Completed": "Completed",
|
||||
"LBC sent. You should see it in your wallet.": "LBC sent. You should see it in your wallet.",
|
||||
"Failed": "Failed",
|
||||
"An error occurred on the previous swap.": "An error occurred on the previous swap.",
|
||||
"Failed to initiate swap.": "Failed to initiate swap.",
|
||||
"Failed to query swap status.": "Failed to query swap status.",
|
||||
"The system is currently down. Come back later.": "The system is currently down. Come back later.",
|
||||
"Unable to obtain exchange rate. Try again later.": "Unable to obtain exchange rate. Try again later.",
|
||||
"The BTC amount needs to be higher": "The BTC amount needs to be higher",
|
||||
"The BTC amount is too high": "The BTC amount is too high",
|
||||
"Confirm Address Removal": "Confirm Address Removal",
|
||||
"Remove BTC Swap Address": "Remove BTC Swap Address",
|
||||
"This process cannot be reversed.": "This process cannot be reversed.",
|
||||
"Remove %btc_address%?": "Remove %btc_address%?",
|
||||
"View transaction": "View transaction",
|
||||
"We apologize for this inconvenience, but have added this additional step to prevent abuse. Users on VPN or shared connections will continue to see this message and are not eligible for Rewards.": "We apologize for this inconvenience, but have added this additional step to prevent abuse. Users on VPN or shared connections will continue to see this message and are not eligible for Rewards.",
|
||||
"Help LBRY Save Crypto": "Help LBRY Save Crypto",
|
||||
"The US government is attempting to destroy the cryptocurrency industry. Can you help?": "The US government is attempting to destroy the cryptocurrency industry. Can you help?",
|
||||
|
|
|
@ -221,6 +221,12 @@ export const icons = {
|
|||
<line x1="1" y1="10" x2="23" y2="10" />
|
||||
</g>
|
||||
),
|
||||
[ICONS.COIN_SWAP]: buildIcon(
|
||||
<g>
|
||||
<path d="M8 7h12m0 0l-4-4m4 4l-4 4m0 6H4m0 0l4 4m-4-4l4-4" />
|
||||
</g>
|
||||
),
|
||||
|
||||
[ICONS.LIBRARY]: buildIcon(<path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z" />),
|
||||
[ICONS.EDIT]: buildIcon(
|
||||
<g>
|
||||
|
|
|
@ -155,7 +155,7 @@ const WalletBalance = (props: Props) => {
|
|||
<Button button="primary" label={__('Buy')} icon={ICONS.BUY} navigate={`/$/${PAGES.BUY}`} />
|
||||
<Button button="secondary" label={__('Receive')} icon={ICONS.RECEIVE} navigate={`/$/${PAGES.RECEIVE}`} />
|
||||
<Button button="secondary" label={__('Send')} icon={ICONS.SEND} navigate={`/$/${PAGES.SEND}`} />
|
||||
<Button button="secondary" label={__('Swap')} icon={ICONS.REFRESH} navigate={`/$/${PAGES.SWAP}`} />
|
||||
<Button button="secondary" label={__('Swap')} icon={ICONS.COIN_SWAP} navigate={`/$/${PAGES.SWAP}`} />
|
||||
</div>
|
||||
{(otherCount > WALLET_CONSOLIDATE_UTXOS || consolidateIsPending || consolidatingUtxos) && (
|
||||
<p className="help">
|
||||
|
|
23
ui/component/walletSwap/index.js
Normal file
23
ui/component/walletSwap/index.js
Normal file
|
@ -0,0 +1,23 @@
|
|||
import { connect } from 'react-redux';
|
||||
import { withRouter } from 'react-router';
|
||||
import WalletSwap from './view';
|
||||
import { doOpenModal } from 'redux/actions/app';
|
||||
import { doAddBtcAddress } from 'redux/actions/coinSwap';
|
||||
import { doToast } from 'redux/actions/notifications';
|
||||
import { selectBtcAddresses } from 'redux/selectors/coinSwap';
|
||||
import { selectReceiveAddress, doGetNewAddress, doCheckAddressIsMine } from 'lbry-redux';
|
||||
|
||||
const select = (state, props) => ({
|
||||
receiveAddress: selectReceiveAddress(state),
|
||||
btcAddresses: selectBtcAddresses(state),
|
||||
});
|
||||
|
||||
const perform = (dispatch) => ({
|
||||
openModal: (modal, props) => dispatch(doOpenModal(modal, props)),
|
||||
doToast: (options) => dispatch(doToast(options)),
|
||||
doAddBtcAddress: (address) => dispatch(doAddBtcAddress(address)),
|
||||
getNewAddress: () => dispatch(doGetNewAddress()),
|
||||
checkAddressIsMine: (address) => dispatch(doCheckAddressIsMine(address)),
|
||||
});
|
||||
|
||||
export default withRouter(connect(select, perform)(WalletSwap));
|
490
ui/component/walletSwap/view.jsx
Normal file
490
ui/component/walletSwap/view.jsx
Normal file
|
@ -0,0 +1,490 @@
|
|||
// @flow
|
||||
import React from 'react';
|
||||
import Button from 'component/button';
|
||||
import { FormField, Form } from 'component/common/form';
|
||||
import { Lbryio } from 'lbryinc';
|
||||
import Card from 'component/common/card';
|
||||
import LbcSymbol from 'component/common/lbc-symbol';
|
||||
import Spinner from 'component/spinner';
|
||||
import Nag from 'component/common/nag';
|
||||
import CopyableText from 'component/copyableText';
|
||||
import QRCode from 'component/common/qr-code';
|
||||
import usePersistedState from 'effects/use-persisted-state';
|
||||
import * as ICONS from 'constants/icons';
|
||||
import * as MODALS from 'constants/modal_types';
|
||||
import { clipboard } from 'electron';
|
||||
import I18nMessage from 'component/i18nMessage';
|
||||
|
||||
const BTC_SATOSHIS = 100000000;
|
||||
const BTC_MAX = 21000000;
|
||||
const BTC_MIN = 1 / BTC_SATOSHIS;
|
||||
|
||||
const STATUS_FETCH_INTERVAL_MS = 60000;
|
||||
|
||||
const IS_DEV = process.env.NODE_ENV !== 'production';
|
||||
const DEBOUNCE_BTC_CHANGE_MS = 400;
|
||||
|
||||
const INTERNAL_APIS_DOWN = 'internal_apis_down';
|
||||
const BTC_API_STATUS_PENDING = 'Pending';
|
||||
const BTC_API_STATUS_PROCESSING = 'Processing';
|
||||
const BTC_API_STATUS_SUCCESS = 'Success';
|
||||
const BTC_API_STATUS_ERROR = 'Error';
|
||||
|
||||
const ACTION_MAIN = 'action_main';
|
||||
const ACTION_STATUS_PENDING = 'action_pending';
|
||||
const ACTION_STATUS_PROCESSING = 'action_processing';
|
||||
const ACTION_STATUS_SUCCESS = 'action_success';
|
||||
const ACTION_PAST_SWAPS = 'action_past_swaps';
|
||||
|
||||
const NAG_API_STATUS_PENDING = 'Waiting to receive your bitcoin.';
|
||||
const NAG_API_STATUS_PROCESSING = 'Bitcoin received. Sending your LBC.';
|
||||
const NAG_API_STATUS_SUCCESS = 'LBC sent. You should see it in your wallet.';
|
||||
const NAG_API_STATUS_ERROR = 'An error occurred on the previous swap.';
|
||||
const NAG_SWAP_CALL_FAILED = 'Failed to initiate swap.';
|
||||
// const NAG_STATUS_CALL_FAILED = 'Failed to query swap status.';
|
||||
const NAG_SERVER_DOWN = 'The system is currently down. Come back later.';
|
||||
const NAG_RATE_CALL_FAILED = 'Unable to obtain exchange rate. Try again later.';
|
||||
|
||||
type Props = {
|
||||
receiveAddress: string,
|
||||
btcAddresses: Array<string>,
|
||||
doToast: ({ message: string }) => void,
|
||||
doAddBtcAddress: (string) => void,
|
||||
getNewAddress: () => void,
|
||||
checkAddressIsMine: (string) => void,
|
||||
openModal: (string, {}) => void,
|
||||
};
|
||||
|
||||
function WalletSwap(props: Props) {
|
||||
const {
|
||||
receiveAddress,
|
||||
doToast,
|
||||
btcAddresses,
|
||||
doAddBtcAddress,
|
||||
getNewAddress,
|
||||
checkAddressIsMine,
|
||||
openModal,
|
||||
} = props;
|
||||
|
||||
const [btc, setBtc] = usePersistedState('swap-btc-amount', 0.001);
|
||||
const [btcError, setBtcError] = React.useState();
|
||||
const [btcAddress, setBtcAddress] = React.useState();
|
||||
const [lbc, setLbc] = React.useState(0);
|
||||
const [action, setAction] = React.useState(ACTION_MAIN);
|
||||
const [nag, setNag] = React.useState(null);
|
||||
const [showQr, setShowQr] = React.useState(false);
|
||||
const [isFetchingRate, setIsFetchingRate] = React.useState(false);
|
||||
const [isSwapping, setIsSwapping] = React.useState(false);
|
||||
const [statusMap, setStatusMap] = React.useState({});
|
||||
const [isRefreshingStatus, setIsRefreshingStatus] = React.useState(false);
|
||||
|
||||
const status = btcAddress ? statusMap[btcAddress] : null;
|
||||
const btcTxId = status && status.receipt_txid ? status.receipt_txid : null;
|
||||
const lbcTxId = status && status.lbc_txid ? status.lbc_txid : null;
|
||||
|
||||
function formatLbcString(lbc) {
|
||||
return lbc === 0 ? '---' : lbc.toLocaleString(undefined, { minimumFractionDigits: 8 });
|
||||
}
|
||||
|
||||
function returnToMainAction() {
|
||||
setIsSwapping(false);
|
||||
setAction(ACTION_MAIN);
|
||||
setBtcAddress(null);
|
||||
}
|
||||
|
||||
function removeBtcAddress(btcAddress) {
|
||||
openModal(MODALS.CONFIRM_REMOVE_BTC_SWAP_ADDRESS, {
|
||||
btcAddress: btcAddress,
|
||||
});
|
||||
}
|
||||
|
||||
// Ensure 'receiveAddress' is populated
|
||||
React.useEffect(() => {
|
||||
if (!receiveAddress) {
|
||||
getNewAddress();
|
||||
} else {
|
||||
checkAddressIsMine(receiveAddress);
|
||||
}
|
||||
}, [receiveAddress, getNewAddress, checkAddressIsMine]);
|
||||
|
||||
// Get 'btc::rate'
|
||||
React.useEffect(() => {
|
||||
if (isNaN(btc) || btc === 0) {
|
||||
setLbc(0);
|
||||
return;
|
||||
}
|
||||
|
||||
setIsFetchingRate(true);
|
||||
|
||||
const timer = setTimeout(() => {
|
||||
Lbryio.call('btc', 'rate', { satoshi: BTC_SATOSHIS })
|
||||
.then((result) => {
|
||||
setIsFetchingRate(false);
|
||||
setLbc(btc / result);
|
||||
})
|
||||
.catch((e) => {
|
||||
setIsFetchingRate(false);
|
||||
setLbc(0);
|
||||
setNag({ msg: NAG_RATE_CALL_FAILED, type: 'error' });
|
||||
});
|
||||
}, DEBOUNCE_BTC_CHANGE_MS);
|
||||
|
||||
return () => clearTimeout(timer);
|
||||
}, [btc]);
|
||||
|
||||
function queryStatus(btcAddress, successCb, failureCb) {
|
||||
Lbryio.call('btc', 'status', { pay_to_address: btcAddress })
|
||||
.then((result) => {
|
||||
setStatusMap((statusMap) => {
|
||||
const tmpMap = { ...statusMap };
|
||||
if (btcAddress) {
|
||||
tmpMap[btcAddress] = result;
|
||||
}
|
||||
return tmpMap;
|
||||
});
|
||||
if (successCb) successCb(result);
|
||||
})
|
||||
.catch((err) => {
|
||||
if (failureCb) failureCb(err);
|
||||
});
|
||||
}
|
||||
|
||||
// Poll 'btc::status'
|
||||
React.useEffect(() => {
|
||||
function fetchBtcStatus() {
|
||||
queryStatus(
|
||||
btcAddress,
|
||||
(result) => {
|
||||
switch (result.Status) {
|
||||
case BTC_API_STATUS_PENDING:
|
||||
setAction(ACTION_STATUS_PENDING);
|
||||
setNag({ msg: NAG_API_STATUS_PENDING, type: 'helpful' });
|
||||
break;
|
||||
case BTC_API_STATUS_PROCESSING:
|
||||
setAction(ACTION_STATUS_PROCESSING);
|
||||
setNag({ msg: NAG_API_STATUS_PROCESSING, type: 'helpful' });
|
||||
break;
|
||||
case BTC_API_STATUS_SUCCESS:
|
||||
setAction(ACTION_STATUS_SUCCESS);
|
||||
setNag({ msg: NAG_API_STATUS_SUCCESS, type: 'helpful' });
|
||||
setIsSwapping(false);
|
||||
break;
|
||||
case BTC_API_STATUS_ERROR:
|
||||
setNag({ msg: NAG_API_STATUS_ERROR, type: 'error' });
|
||||
returnToMainAction();
|
||||
break;
|
||||
default:
|
||||
if (IS_DEV) throw new Error('Unhandled status: "' + result.Status + '"');
|
||||
break;
|
||||
}
|
||||
},
|
||||
(err) => {
|
||||
returnToMainAction();
|
||||
setNag({
|
||||
msg: err === INTERNAL_APIS_DOWN ? NAG_SERVER_DOWN : err.message /* NAG_STATUS_CALL_FAILED */,
|
||||
type: 'error',
|
||||
});
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
let fetchInterval;
|
||||
if (btcAddress && isSwapping) {
|
||||
fetchBtcStatus();
|
||||
fetchInterval = setInterval(fetchBtcStatus, STATUS_FETCH_INTERVAL_MS);
|
||||
}
|
||||
|
||||
return () => {
|
||||
if (fetchInterval) {
|
||||
clearInterval(fetchInterval);
|
||||
}
|
||||
};
|
||||
}, [btcAddress, isSwapping]);
|
||||
|
||||
// Validate entered BTC
|
||||
React.useEffect(() => {
|
||||
let msg;
|
||||
if (btc < BTC_MIN) {
|
||||
msg = __('The BTC amount needs to be higher');
|
||||
} else if (btc > BTC_MAX) {
|
||||
msg = __('The BTC amount is too high');
|
||||
}
|
||||
setBtcError(msg);
|
||||
}, [btc]);
|
||||
|
||||
// 'Refresh' button feedback
|
||||
React.useEffect(() => {
|
||||
let timer;
|
||||
if (isRefreshingStatus) {
|
||||
timer = setTimeout(() => {
|
||||
setIsRefreshingStatus(false);
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
return () => clearTimeout(timer);
|
||||
}, [isRefreshingStatus]);
|
||||
|
||||
function handleStartSwap() {
|
||||
setIsSwapping(true);
|
||||
setBtcAddress(null);
|
||||
setNag(null);
|
||||
|
||||
Lbryio.call('btc', 'swap', {
|
||||
lbc_satoshi_requested: parseInt(lbc * BTC_SATOSHIS),
|
||||
btc_satoshi_provided: parseInt(btc * BTC_SATOSHIS),
|
||||
pay_to_wallet_address: receiveAddress,
|
||||
})
|
||||
.then((result) => {
|
||||
setBtcAddress(result);
|
||||
doAddBtcAddress(result);
|
||||
})
|
||||
.catch((err) => {
|
||||
setNag({ msg: err === INTERNAL_APIS_DOWN ? NAG_SWAP_CALL_FAILED : err.message, type: 'error' });
|
||||
returnToMainAction();
|
||||
});
|
||||
}
|
||||
|
||||
function handleCancelPending() {
|
||||
returnToMainAction();
|
||||
setNag(null);
|
||||
}
|
||||
|
||||
function handleBtcChange(event: SyntheticInputEvent<*>) {
|
||||
const btc = parseFloat(event.target.value);
|
||||
setBtc(btc);
|
||||
}
|
||||
|
||||
function handleViewPastSwaps() {
|
||||
setAction(ACTION_PAST_SWAPS);
|
||||
setNag(null);
|
||||
setIsRefreshingStatus(true);
|
||||
|
||||
btcAddresses.forEach((x) => {
|
||||
queryStatus(x, null, null);
|
||||
});
|
||||
}
|
||||
|
||||
function getStatusStr(btcAddress) {
|
||||
const status = statusMap[btcAddress];
|
||||
if (!status) {
|
||||
return '---';
|
||||
}
|
||||
|
||||
let msg;
|
||||
switch (status.Status) {
|
||||
case BTC_API_STATUS_PENDING:
|
||||
msg = __('Waiting for BTC');
|
||||
break;
|
||||
case BTC_API_STATUS_PROCESSING:
|
||||
msg = __('Sending LBC');
|
||||
break;
|
||||
case BTC_API_STATUS_SUCCESS:
|
||||
msg = __('Completed');
|
||||
break;
|
||||
case BTC_API_STATUS_ERROR:
|
||||
msg = __('Failed');
|
||||
break;
|
||||
default:
|
||||
msg = '?';
|
||||
// if (IS_DEV) throw new Error('Unhandled "status": ' + status.Status);
|
||||
break;
|
||||
}
|
||||
return msg;
|
||||
}
|
||||
|
||||
function getViewTransactionElement(isSend) {
|
||||
if (isSend) {
|
||||
return btcTxId ? (
|
||||
<Button button="link" href={`https://www.blockchain.com/btc/tx/${btcTxId}`} label={__('View transaction')} />
|
||||
) : null;
|
||||
} else {
|
||||
return lbcTxId ? (
|
||||
<Button button="link" href={`https://explorer.lbry.com/tx/${lbcTxId}`} label={__('View transaction')} />
|
||||
) : null;
|
||||
}
|
||||
}
|
||||
|
||||
function getActionElement() {
|
||||
switch (action) {
|
||||
case ACTION_MAIN:
|
||||
return actionMain;
|
||||
case ACTION_STATUS_PENDING:
|
||||
return actionPending;
|
||||
case ACTION_STATUS_SUCCESS: // fall-through
|
||||
case ACTION_STATUS_PROCESSING:
|
||||
return actionProcessingAndSuccess;
|
||||
case ACTION_PAST_SWAPS:
|
||||
return actionPastSwaps;
|
||||
default:
|
||||
if (IS_DEV) throw new Error('Unhandled action: ' + action);
|
||||
return actionMain;
|
||||
}
|
||||
}
|
||||
|
||||
const actionMain = (
|
||||
<>
|
||||
<div className="section section--padded card--inline confirm__wrapper">
|
||||
<div className="section">
|
||||
<FormField
|
||||
autoFocus
|
||||
label={__('Bitcoin')}
|
||||
type="number"
|
||||
name="btc"
|
||||
className="form-field--price-amount--auto"
|
||||
affixClass="form-field--fix-no-height"
|
||||
max={BTC_MAX}
|
||||
min={BTC_MIN}
|
||||
step={1 / BTC_SATOSHIS}
|
||||
placeholder="12.34"
|
||||
value={btc}
|
||||
error={btcError}
|
||||
disabled={isSwapping}
|
||||
onChange={(event) => handleBtcChange(event)}
|
||||
/>
|
||||
<div className="confirm__value" />
|
||||
<div className="confirm__label">{__('Credits')}</div>
|
||||
<div className="confirm__value">
|
||||
<LbcSymbol postfix={formatLbcString(lbc)} size={22} />
|
||||
{isFetchingRate && <Spinner type="small" />}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="section__actions">
|
||||
<Button
|
||||
autoFocus
|
||||
onClick={handleStartSwap}
|
||||
button="primary"
|
||||
disabled={isSwapping || isNaN(btc) || btc === 0 || lbc === 0 || btcError}
|
||||
label={isSwapping ? __('Processing...') : __('Start Swap')}
|
||||
/>
|
||||
{btcAddresses.length !== 0 && (
|
||||
<Button button="link" label={__('View Past Swaps')} onClick={handleViewPastSwaps} />
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
||||
const actionPending = (
|
||||
<>
|
||||
<div className="section section--padded card--inline confirm__wrapper">
|
||||
<div className="section">
|
||||
<div className="confirm__label">{__('Send')}</div>
|
||||
<div className="confirm__value">{btc} BTC</div>
|
||||
<div className="confirm__label">{__('To')}</div>
|
||||
<CopyableText primaryButton copyable={btcAddress} snackMessage={__('Address copied.')} />
|
||||
<div className="card__actions--inline">
|
||||
<Button
|
||||
button="link"
|
||||
label={showQr ? __('Hide QR code') : __('Show QR code')}
|
||||
onClick={() => setShowQr(!showQr)}
|
||||
/>
|
||||
{showQr && btcAddress && <QRCode value={btcAddress} />}
|
||||
</div>
|
||||
<div className="confirm__value" />
|
||||
<div className="confirm__label">{__('Receive')}</div>
|
||||
<div className="confirm__value">{<LbcSymbol postfix={formatLbcString(lbc)} size={22} />}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="section__actions">
|
||||
<Button autoFocus onClick={handleCancelPending} button="primary" label={__('Go Back')} />
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
||||
const actionProcessingAndSuccess = (
|
||||
<>
|
||||
<div className="section section--padded card--inline confirm__wrapper">
|
||||
<div className="section">
|
||||
<div className="confirm__label">{__('Sent')}</div>
|
||||
<div className="confirm__value">{btc} BTC</div>
|
||||
{getViewTransactionElement(true)}
|
||||
<div className="confirm__value" />
|
||||
<div className="confirm__label">{action === ACTION_STATUS_SUCCESS ? __('Received') : __('Receiving')}</div>
|
||||
<div className="confirm__value">{<LbcSymbol postfix={formatLbcString(lbc)} size={22} />}</div>
|
||||
{action === ACTION_STATUS_SUCCESS && getViewTransactionElement(false)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="section__actions">
|
||||
<Button autoFocus onClick={handleCancelPending} button="primary" label={__('Go Back')} />
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
||||
const actionPastSwaps = (
|
||||
<>
|
||||
<div className="section section--padded card--inline confirm__wrapper">
|
||||
<div className="section">
|
||||
<div className="table__wrapper">
|
||||
<table className="table table--btc-swap">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>BTC address</th>
|
||||
<th>Status</th>
|
||||
<th />
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{btcAddresses.length === 0 && (
|
||||
<tr>
|
||||
<td>{'---'}</td>
|
||||
</tr>
|
||||
)}
|
||||
{btcAddresses.length !== 0 &&
|
||||
btcAddresses.map((x) => {
|
||||
const shortBtcAddress = x.substring(0, 7);
|
||||
return (
|
||||
<tr key={x}>
|
||||
<td>
|
||||
<Button
|
||||
button="link"
|
||||
className="button--hash-id"
|
||||
title={x}
|
||||
label={shortBtcAddress}
|
||||
onClick={() => {
|
||||
clipboard.writeText(x);
|
||||
doToast({
|
||||
message: __('Address copied.'),
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</td>
|
||||
<td>{isRefreshingStatus ? '...' : getStatusStr(x)}</td>
|
||||
<td>
|
||||
<Button
|
||||
button="link"
|
||||
icon={ICONS.REMOVE}
|
||||
title={__('Remove address')}
|
||||
onClick={() => removeBtcAddress(x)}
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="section__actions">
|
||||
<Button autoFocus onClick={handleCancelPending} button="primary" label={__('Go Back')} />
|
||||
{btcAddresses.length !== 0 && !isRefreshingStatus && (
|
||||
<Button button="link" label={__('Refresh')} onClick={handleViewPastSwaps} />
|
||||
)}
|
||||
{isRefreshingStatus && <Spinner type="small" />}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
||||
return (
|
||||
<Form onSubmit={handleStartSwap}>
|
||||
<Card
|
||||
title={<I18nMessage tokens={{ lbc: <LbcSymbol size={22} /> }}>Swap Bitcoin for %lbc%</I18nMessage>}
|
||||
subtitle={__('Send bitcoin to the address provided and you will be sent an equivalent amount of Credits.')}
|
||||
actions={getActionElement()}
|
||||
nag={nag ? <Nag relative type={nag.type} message={__(nag.msg)} /> : null}
|
||||
/>
|
||||
</Form>
|
||||
);
|
||||
}
|
||||
|
||||
export default WalletSwap;
|
|
@ -282,6 +282,10 @@ export const COMMENT_RECEIVED = 'COMMENT_RECEIVED';
|
|||
// Blocked channels
|
||||
export const TOGGLE_BLOCK_CHANNEL = 'TOGGLE_BLOCK_CHANNEL';
|
||||
|
||||
// Coin swap
|
||||
export const ADD_BTC_ADDRESS = 'ADD_BTC_ADDRESS';
|
||||
export const REMOVE_BTC_ADDRESS = 'REMOVE_BTC_ADDRESS';
|
||||
|
||||
// Tags
|
||||
export const TOGGLE_TAG_FOLLOW = 'TOGGLE_TAG_FOLLOW';
|
||||
export const TAG_ADD = 'TAG_ADD';
|
||||
|
|
|
@ -27,6 +27,7 @@ export const HISTORY = 'Clock';
|
|||
export const HOME = 'Home';
|
||||
export const OVERVIEW = 'Activity';
|
||||
export const WALLET = 'List';
|
||||
export const COIN_SWAP = 'CoinSwap';
|
||||
export const PHONE = 'Phone';
|
||||
export const COMPLETE = 'Check';
|
||||
export const COMPLETED = 'CheckCircle';
|
||||
|
|
|
@ -42,3 +42,4 @@ export const SYNC_ENABLE = 'SYNC_ENABLE';
|
|||
export const IMAGE_UPLOAD = 'image_upload';
|
||||
export const MOBILE_SEARCH = 'mobile_search';
|
||||
export const VIEW_IMAGE = 'view_image';
|
||||
export const CONFIRM_REMOVE_BTC_SWAP_ADDRESS = 'confirm_remove_btc_swap_address';
|
||||
|
|
13
ui/modal/modalRemoveBtcSwapAddress/index.js
Normal file
13
ui/modal/modalRemoveBtcSwapAddress/index.js
Normal file
|
@ -0,0 +1,13 @@
|
|||
import { connect } from 'react-redux';
|
||||
import { doHideModal } from 'redux/actions/app';
|
||||
import ModalRemoveBtcSwapAddress from './view';
|
||||
import { doRemoveBtcAddress } from 'redux/actions/coinSwap';
|
||||
|
||||
const select = (state, props) => ({});
|
||||
|
||||
const perform = (dispatch) => ({
|
||||
doRemoveBtcAddress: (btcAddress) => dispatch(doRemoveBtcAddress(btcAddress)),
|
||||
closeModal: () => dispatch(doHideModal()),
|
||||
});
|
||||
|
||||
export default connect(select, perform)(ModalRemoveBtcSwapAddress);
|
43
ui/modal/modalRemoveBtcSwapAddress/view.jsx
Normal file
43
ui/modal/modalRemoveBtcSwapAddress/view.jsx
Normal file
|
@ -0,0 +1,43 @@
|
|||
// @flow
|
||||
import React from 'react';
|
||||
import { Modal } from 'modal/modal';
|
||||
import Button from 'component/button';
|
||||
import Card from 'component/common/card';
|
||||
import I18nMessage from 'component/i18nMessage';
|
||||
|
||||
type Props = {
|
||||
btcAddress: string,
|
||||
doRemoveBtcAddress: (string) => void,
|
||||
closeModal: () => void,
|
||||
};
|
||||
|
||||
function ModalRemoveBtcSwapAddress(props: Props) {
|
||||
const { btcAddress, doRemoveBtcAddress, closeModal } = props;
|
||||
|
||||
return (
|
||||
<Modal isOpen contentLabel={__('Confirm Address Removal')} type="card" onAborted={closeModal}>
|
||||
<Card
|
||||
title={__('Remove BTC Swap Address')}
|
||||
subtitle={<I18nMessage tokens={{ btc_address: <em>{`${btcAddress}`}</em> }}>Remove %btc_address%?</I18nMessage>}
|
||||
body={<p className="help--warning">{__('This process cannot be reversed.')}</p>}
|
||||
actions={
|
||||
<>
|
||||
<div className="section__actions">
|
||||
<Button
|
||||
button="primary"
|
||||
label={__('OK')}
|
||||
onClick={() => {
|
||||
doRemoveBtcAddress(btcAddress);
|
||||
closeModal();
|
||||
}}
|
||||
/>
|
||||
<Button button="link" label={__('Cancel')} onClick={closeModal} />
|
||||
</div>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
export default ModalRemoveBtcSwapAddress;
|
|
@ -39,6 +39,7 @@ import ModalImageUpload from 'modal/modalImageUpload';
|
|||
import ModalMobileSearch from 'modal/modalMobileSearch';
|
||||
import ModalViewImage from 'modal/modalViewImage';
|
||||
import ModalMassTipsUnlock from 'modal/modalMassTipUnlock';
|
||||
import ModalRemoveBtcSwapAddress from 'modal/modalRemoveBtcSwapAddress';
|
||||
|
||||
type Props = {
|
||||
modal: { id: string, modalProps: {} },
|
||||
|
@ -140,6 +141,8 @@ function ModalRouter(props: Props) {
|
|||
return <ModalViewImage {...modalProps} />;
|
||||
case MODALS.MASS_TIP_UNLOCK:
|
||||
return <ModalMassTipsUnlock {...modalProps} />;
|
||||
case MODALS.CONFIRM_REMOVE_BTC_SWAP_ADDRESS:
|
||||
return <ModalRemoveBtcSwapAddress {...modalProps} />;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
// @flow
|
||||
import React from 'react';
|
||||
import Page from 'component/page';
|
||||
import LbcSymbol from 'component/common/lbc-symbol';
|
||||
import WalletSwap from 'component/walletSwap';
|
||||
|
||||
type Props = {};
|
||||
|
||||
|
@ -9,15 +9,13 @@ export default function SwapPage(props: Props) {
|
|||
return (
|
||||
<Page
|
||||
noSideNavigation
|
||||
className="main--buy"
|
||||
className="main--swap"
|
||||
backout={{
|
||||
backoutLabel: __('Done'),
|
||||
title: (
|
||||
<>
|
||||
<LbcSymbol prefix={__('Swap')} size={28} />
|
||||
</>
|
||||
),
|
||||
title: __('Swap Bitcoin'),
|
||||
}}
|
||||
/>
|
||||
>
|
||||
<WalletSwap />
|
||||
</Page>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -12,11 +12,12 @@ import rewardsReducer from 'redux/reducers/rewards';
|
|||
import userReducer from 'redux/reducers/user';
|
||||
import commentsReducer from 'redux/reducers/comments';
|
||||
import blockedReducer from 'redux/reducers/blocked';
|
||||
import coinSwapReducer from 'redux/reducers/coinSwap';
|
||||
import searchReducer from 'redux/reducers/search';
|
||||
import reactionsReducer from 'redux/reducers/reactions';
|
||||
import syncReducer from 'redux/reducers/sync';
|
||||
|
||||
export default history =>
|
||||
export default (history) =>
|
||||
combineReducers({
|
||||
router: connectRouter(history),
|
||||
app: appReducer,
|
||||
|
@ -38,6 +39,7 @@ export default history =>
|
|||
subscriptions: subscriptionsReducer,
|
||||
tags: tagsReducer,
|
||||
blocked: blockedReducer,
|
||||
coinSwap: coinSwapReducer,
|
||||
user: userReducer,
|
||||
wallet: walletReducer,
|
||||
sync: syncReducer,
|
||||
|
|
36
ui/redux/actions/coinSwap.js
Normal file
36
ui/redux/actions/coinSwap.js
Normal file
|
@ -0,0 +1,36 @@
|
|||
// @flow
|
||||
import * as ACTIONS from 'constants/action_types';
|
||||
import { selectPrefsReady } from 'redux/selectors/sync';
|
||||
import { doAlertWaitingForSync } from 'redux/actions/app';
|
||||
|
||||
export const doAddBtcAddress = (btcAddress: string) => (dispatch: Dispatch, getState: GetState) => {
|
||||
const state = getState();
|
||||
const ready = selectPrefsReady(state);
|
||||
|
||||
if (!ready) {
|
||||
return dispatch(doAlertWaitingForSync());
|
||||
}
|
||||
|
||||
dispatch({
|
||||
type: ACTIONS.ADD_BTC_ADDRESS,
|
||||
data: {
|
||||
btcAddress,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
export const doRemoveBtcAddress = (btcAddress: string) => (dispatch: Dispatch, getState: GetState) => {
|
||||
const state = getState();
|
||||
const ready = selectPrefsReady(state);
|
||||
|
||||
if (!ready) {
|
||||
return dispatch(doAlertWaitingForSync());
|
||||
}
|
||||
|
||||
dispatch({
|
||||
type: ACTIONS.REMOVE_BTC_ADDRESS,
|
||||
data: {
|
||||
btcAddress,
|
||||
},
|
||||
});
|
||||
};
|
46
ui/redux/reducers/coinSwap.js
Normal file
46
ui/redux/reducers/coinSwap.js
Normal file
|
@ -0,0 +1,46 @@
|
|||
// @flow
|
||||
import * as ACTIONS from 'constants/action_types';
|
||||
import { ACTIONS as LBRY_REDUX_ACTIONS } from 'lbry-redux';
|
||||
import { handleActions } from 'util/redux-utils';
|
||||
|
||||
const defaultState: CoinSwapState = {
|
||||
btcAddresses: [],
|
||||
};
|
||||
|
||||
export default handleActions(
|
||||
{
|
||||
[ACTIONS.ADD_BTC_ADDRESS]: (state: CoinSwapState, action: CoinSwapAction): CoinSwapState => {
|
||||
const { btcAddresses } = state;
|
||||
const { btcAddress } = action.data;
|
||||
let newBtcAddresses = btcAddresses.slice();
|
||||
if (!newBtcAddresses.includes(btcAddress)) {
|
||||
newBtcAddresses.push(btcAddress);
|
||||
}
|
||||
return {
|
||||
btcAddresses: newBtcAddresses,
|
||||
};
|
||||
},
|
||||
[ACTIONS.REMOVE_BTC_ADDRESS]: (state: CoinSwapState, action: CoinSwapAction): CoinSwapState => {
|
||||
const { btcAddresses } = state;
|
||||
const { btcAddress } = action.data;
|
||||
let newBtcAddresses = btcAddresses.slice();
|
||||
newBtcAddresses = newBtcAddresses.filter((x) => x !== btcAddress);
|
||||
return {
|
||||
btcAddresses: newBtcAddresses,
|
||||
};
|
||||
},
|
||||
[LBRY_REDUX_ACTIONS.USER_STATE_POPULATE]: (
|
||||
state: CoinSwapState,
|
||||
action: { data: { btcAddresses: ?Array<string> } }
|
||||
) => {
|
||||
const { btcAddresses } = action.data;
|
||||
const sanitizedBtcAddresses = btcAddresses && btcAddresses.filter((e) => typeof e === 'string');
|
||||
return {
|
||||
...state,
|
||||
btcAddresses:
|
||||
sanitizedBtcAddresses && sanitizedBtcAddresses.length ? sanitizedBtcAddresses : state.btcAddresses,
|
||||
};
|
||||
},
|
||||
},
|
||||
defaultState
|
||||
);
|
8
ui/redux/selectors/coinSwap.js
Normal file
8
ui/redux/selectors/coinSwap.js
Normal file
|
@ -0,0 +1,8 @@
|
|||
// @flow
|
||||
import { createSelector } from 'reselect';
|
||||
|
||||
const selectState = (state: { coinSwap: CoinSwapState }) => state.coinSwap || {};
|
||||
|
||||
export const selectBtcAddresses = createSelector(selectState, (state: CoinSwapState) => {
|
||||
return state.btcAddresses.filter((x) => typeof x === 'string');
|
||||
});
|
|
@ -359,3 +359,7 @@ svg + .button__label {
|
|||
top: var(--spacing-s);
|
||||
}
|
||||
}
|
||||
|
||||
.button--hash-id {
|
||||
@include font-mono;
|
||||
}
|
||||
|
|
|
@ -406,6 +406,11 @@ fieldset-group {
|
|||
width: 7em;
|
||||
}
|
||||
|
||||
.form-field--price-amount--auto {
|
||||
width: auto;
|
||||
min-width: 100%;
|
||||
}
|
||||
|
||||
.form-field--address {
|
||||
min-width: 18em;
|
||||
}
|
||||
|
|
|
@ -261,6 +261,11 @@
|
|||
max-width: 34rem;
|
||||
}
|
||||
|
||||
.main--swap {
|
||||
@extend .main--buy;
|
||||
max-width: 34rem;
|
||||
}
|
||||
|
||||
.main--empty {
|
||||
align-self: center;
|
||||
display: flex;
|
||||
|
|
|
@ -154,6 +154,12 @@ th {
|
|||
}
|
||||
}
|
||||
|
||||
.table--btc-swap {
|
||||
tr {
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
|
||||
.table--rewards {
|
||||
td:nth-of-type(1) {
|
||||
width: 40%;
|
||||
|
|
|
@ -66,6 +66,7 @@ const searchFilter = createFilter('search', ['options']);
|
|||
const tagsFilter = createFilter('tags', ['followedTags']);
|
||||
const subscriptionsFilter = createFilter('subscriptions', ['subscriptions']);
|
||||
const blockedFilter = createFilter('blocked', ['blockedChannels']);
|
||||
const btcAddressesFilter = createFilter('coinSwap', ['btcAddresses']);
|
||||
const settingsFilter = createBlacklistFilter('settings', ['loadedLanguages', 'language']);
|
||||
const whiteListedReducers = [
|
||||
'fileInfo',
|
||||
|
@ -76,6 +77,7 @@ const whiteListedReducers = [
|
|||
'app',
|
||||
'search',
|
||||
'blocked',
|
||||
'coinSwap',
|
||||
'settings',
|
||||
'subscriptions',
|
||||
];
|
||||
|
@ -84,6 +86,7 @@ const transforms = [
|
|||
fileInfoFilter,
|
||||
walletFilter,
|
||||
blockedFilter,
|
||||
btcAddressesFilter,
|
||||
tagsFilter,
|
||||
appFilter,
|
||||
searchFilter,
|
||||
|
@ -119,6 +122,8 @@ const triggerSharedStateActions = [
|
|||
ACTIONS.CHANNEL_SUBSCRIBE,
|
||||
ACTIONS.CHANNEL_UNSUBSCRIBE,
|
||||
ACTIONS.TOGGLE_BLOCK_CHANNEL,
|
||||
ACTIONS.ADD_BTC_ADDRESS,
|
||||
ACTIONS.REMOVE_BTC_ADDRESS,
|
||||
ACTIONS.TOGGLE_TAG_FOLLOW,
|
||||
LBRY_REDUX_ACTIONS.CREATE_CHANNEL_COMPLETED,
|
||||
ACTIONS.SYNC_CLIENT_SETTINGS,
|
||||
|
@ -151,6 +156,7 @@ const sharedStateFilters = {
|
|||
property: 'following',
|
||||
},
|
||||
blocked: { source: 'blocked', property: 'blockedChannels' },
|
||||
btc_addresses: { source: 'coinSwap', property: 'btcAddresses' },
|
||||
settings: { source: 'settings', property: 'sharedPreferences' },
|
||||
app_welcome_version: { source: 'app', property: 'welcomeVersion' },
|
||||
sharing_3P: { source: 'app', property: 'allowAnalytics' },
|
||||
|
|
Loading…
Reference in a new issue