Remove swap #7659

Merged
jessopb merged 2 commits from removeSwap into master 2022-08-02 23:17:01 +02:00
20 changed files with 24 additions and 1117 deletions

View file

@ -5,7 +5,6 @@ import Icon from 'component/common/icon';
import classnames from 'classnames';
import { NavLink } from 'react-router-dom';
import { formatLbryUrlForWeb } from 'util/url';
import * as PAGES from 'constants/pages';
import useCombinedRefs from 'effects/use-combined-refs';
type Props = {
@ -34,7 +33,6 @@ type Props = {
onMouseLeave: ?(any) => any,
pathname: string,
emailVerified: boolean,
requiresAuth: ?boolean,
myref: any,
dispatch: any,
'aria-label'?: string,
@ -66,7 +64,6 @@ const Button = forwardRef<any, {}>((props: Props, ref: any) => {
iconColor,
activeClass,
emailVerified,
requiresAuth,
myref,
dispatch, // <button> doesn't know what to do with dispatch
pathname,
@ -75,7 +72,7 @@ const Button = forwardRef<any, {}>((props: Props, ref: any) => {
...otherProps
} = props;
const disable = disabled || (user === null && requiresAuth);
const disable = disabled;
const combinedClassName = classnames(
'button',
@ -183,31 +180,6 @@ const Button = forwardRef<any, {}>((props: Props, ref: any) => {
}
}
if (requiresAuth && !emailVerified) {
let redirectUrl = `/$/${PAGES.AUTH}?redirect=${pathname}`;
if (authSrc) {
redirectUrl += `&src=${authSrc}`;
}
return (
<NavLink
exact
onClick={(e) => {
e.stopPropagation();
}}
to={redirectUrl}
title={title || defaultTooltip}
disabled={disable}
className={combinedClassName}
activeClassName={activeClass}
aria-label={ariaLabel}
>
{content}
</NavLink>
);
}
return path ? (
<NavLink
exact

View file

@ -89,7 +89,6 @@ export default function SearchChannelField(props: Props) {
return (
<Button
ref={addTagRef}
requiresAuth
button="primary"
label={labelFoundAction}
onClick={() => handleFoundChannelClick(claim)}

View file

@ -41,7 +41,7 @@ function SyncToggle(props: Props) {
{!verifiedEmail && (
<div>
<p className="help">{__('An email address is required to sync your account.')}</p>
<Button requiresAuth button="primary" label={__('Add Email')} />
<Button button="primary" label={__('Add Email')} />
</div>
)}
</SettingsRow>

View file

@ -1,27 +0,0 @@
import { connect } from 'react-redux';
import { withRouter } from 'react-router';
import WalletSwap from './view';
import { doOpenModal } from 'redux/actions/app';
import { doAddCoinSwap, doQueryCoinSwapStatus } from 'redux/actions/coinSwap';
import { doToast } from 'redux/actions/notifications';
import { selectCoinSwaps } from 'redux/selectors/coinSwap';
import { selectUserVerifiedEmail } from 'redux/selectors/user';
import { doGetNewAddress, doCheckAddressIsMine } from 'redux/actions/wallet';
import { selectReceiveAddress } from 'redux/selectors/wallet';
const select = (state, props) => ({
receiveAddress: selectReceiveAddress(state),
coinSwaps: selectCoinSwaps(state),
isAuthenticated: selectUserVerifiedEmail(state),
});
const perform = (dispatch) => ({
openModal: (modal, props) => dispatch(doOpenModal(modal, props)),
doToast: (options) => dispatch(doToast(options)),
addCoinSwap: (coinSwap) => dispatch(doAddCoinSwap(coinSwap)),
getNewAddress: () => dispatch(doGetNewAddress()),
checkAddressIsMine: (address) => dispatch(doCheckAddressIsMine(address)),
queryCoinSwapStatus: (sendAddress) => dispatch(doQueryCoinSwapStatus(sendAddress)),
});
export default withRouter(connect(select, perform)(WalletSwap));

View file

@ -1,707 +0,0 @@
// @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 Icon from 'component/common/icon';
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 * as PAGES from 'constants/pages';
import { clipboard } from 'electron';
import I18nMessage from 'component/i18nMessage';
import { Redirect, useHistory } from 'react-router';
const ENABLE_ALTERNATIVE_COINS = true;
const BTC_SATOSHIS = 100000000;
const LBC_MAX = 21000000;
const LBC_MIN = 1;
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 = 'NEW'; // Started swap, waiting for coin.
const BTC_API_STATUS_CONFIRMING = 'PENDING'; // Coin receiving, waiting confirmation.
const BTC_API_STATUS_PROCESSING = 'COMPLETED'; // Coin confirmed. Sending LBC.
const BTC_API_STATUS_UNRESOLVED = 'UNRESOLVED'; // Underpaid, overpaid, etc.
const BTC_API_STATUS_EXPIRED = 'EXPIRED'; // Charge expired (60 minutes).
const BTC_API_STATUS_ERROR = 'Error';
const ACTION_MAIN = 'action_main';
const ACTION_STATUS_PENDING = 'action_pending';
const ACTION_STATUS_CONFIRMING = 'action_confirming';
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 crypto.';
const NAG_API_STATUS_CONFIRMING = 'Confirming transaction.';
const NAG_API_STATUS_PROCESSING = 'Crypto received. Sending your Credits.';
const NAG_API_STATUS_SUCCESS = 'Credits 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.';
const NAG_EXPIRED = 'Swap expired.';
type Props = {
receiveAddress: string,
coinSwaps: Array<CoinSwapInfo>,
isAuthenticated: boolean,
doToast: ({ message: string }) => void,
addCoinSwap: (CoinSwapInfo) => void,
getNewAddress: () => void,
checkAddressIsMine: (string) => void,
openModal: (string, {}) => void,
queryCoinSwapStatus: (string) => void,
};
function WalletSwap(props: Props) {
const {
receiveAddress,
doToast,
coinSwaps,
isAuthenticated,
addCoinSwap,
getNewAddress,
checkAddressIsMine,
openModal,
queryCoinSwapStatus,
} = props;
const [btc, setBtc] = React.useState(0);
const [lbcError, setLbcError] = React.useState();
const [lbc, setLbc] = usePersistedState('swap-desired-lbc', LBC_MIN);
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 [isRefreshingStatus, setIsRefreshingStatus] = React.useState(false);
const { location } = useHistory();
const [swap, setSwap] = React.useState({});
const [coin, setCoin] = React.useState('bitcoin');
const [lastStatusQuery, setLastStatusQuery] = React.useState();
const { goBack } = useHistory();
function formatCoinAmountString(amount) {
return amount === 0 ? '---' : amount.toLocaleString(undefined, { minimumFractionDigits: 8 });
}
function returnToMainAction() {
setIsSwapping(false);
setAction(ACTION_MAIN);
setSwap(null);
}
function removeCoinSwap(chargeCode) {
openModal(MODALS.CONFIRM_REMOVE_BTC_SWAP_ADDRESS, {
chargeCode: chargeCode,
});
}
// Ensure 'receiveAddress' is populated
React.useEffect(() => {
if (!receiveAddress) {
getNewAddress();
} else {
checkAddressIsMine(receiveAddress);
}
}, [receiveAddress, getNewAddress, checkAddressIsMine]);
// Get 'btc/rate' and calculate required BTC.
React.useEffect(() => {
if (isNaN(lbc) || lbc === 0) {
setBtc(0);
return;
}
setIsFetchingRate(true);
const timer = setTimeout(() => {
Lbryio.call('btc', 'rate', { satoshi: BTC_SATOSHIS })
.then((rate) => {
setIsFetchingRate(false);
setBtc((lbc * Math.round(BTC_SATOSHIS * rate)) / BTC_SATOSHIS);
})
.catch(() => {
setIsFetchingRate(false);
setBtc(0);
setNag({ msg: NAG_RATE_CALL_FAILED, type: 'error' });
});
}, DEBOUNCE_BTC_CHANGE_MS);
return () => clearTimeout(timer);
}, [lbc]);
// Resolve 'swap' with the latest info from 'coinSwaps'
React.useEffect(() => {
const swapInfo = swap && coinSwaps.find((x) => x.chargeCode === swap.chargeCode);
if (!swapInfo) {
return;
}
const jsonSwap = JSON.stringify(swap);
const jsonSwapInfo = JSON.stringify(swapInfo);
if (jsonSwap !== jsonSwapInfo) {
setSwap({ ...swapInfo });
}
if (!swapInfo.status) {
return;
}
switch (swapInfo.status.status) {
case BTC_API_STATUS_PENDING:
setAction(ACTION_STATUS_PENDING);
setNag({ msg: NAG_API_STATUS_PENDING, type: 'helpful' });
break;
case BTC_API_STATUS_CONFIRMING:
setAction(ACTION_STATUS_CONFIRMING);
setNag({ msg: NAG_API_STATUS_CONFIRMING, type: 'helpful' });
break;
case BTC_API_STATUS_PROCESSING:
if (swapInfo.status.lbcTxid) {
setAction(ACTION_STATUS_SUCCESS);
setNag({ msg: NAG_API_STATUS_SUCCESS, type: 'helpful' });
setIsSwapping(false);
} else {
setAction(ACTION_STATUS_PROCESSING);
setNag({ msg: NAG_API_STATUS_PROCESSING, type: 'helpful' });
}
break;
case BTC_API_STATUS_ERROR:
setNag({ msg: NAG_API_STATUS_ERROR, type: 'error' });
break;
case INTERNAL_APIS_DOWN:
setNag({ msg: NAG_SERVER_DOWN, type: 'error' });
break;
case BTC_API_STATUS_EXPIRED:
setNag({ msg: NAG_EXPIRED, type: 'error' });
if (action === ACTION_PAST_SWAPS) {
setAction(ACTION_STATUS_PENDING);
}
break;
case BTC_API_STATUS_UNRESOLVED:
setNag({
msg: __(
'Received amount did not match order code %chargeCode%. Contact hello@lbry.com to resolve the payment.',
{ chargeCode: swapInfo.chargeCode }
),
type: 'error',
});
if (action === ACTION_PAST_SWAPS) {
setAction(ACTION_STATUS_PENDING);
}
break;
default:
setNag({ msg: swapInfo.status.status, type: 'error' });
break;
}
}, [swap, coinSwaps]);
// Validate entered LBC
React.useEffect(() => {
let msg;
if (lbc < LBC_MIN) {
msg = __('The amount needs to be higher');
} else if (lbc > LBC_MAX) {
msg = __('The amount is too high');
}
setLbcError(msg);
}, [lbc]);
// 'Refresh' button feedback
React.useEffect(() => {
let timer;
if (isRefreshingStatus) {
timer = setTimeout(() => {
setIsRefreshingStatus(false);
}, 1000);
}
return () => clearTimeout(timer);
}, [isRefreshingStatus]);
function getCoinAddress(coin) {
if (swap && swap.sendAddresses) {
return swap.sendAddresses[coin];
}
return '';
}
function getCoinSendAmountStr(coin) {
if (swap && swap.sendAmounts && swap.sendAmounts[coin]) {
return `${swap.sendAmounts[coin].amount} ${swap.sendAmounts[coin].currency}`;
}
return '';
}
function currencyToCoin(currency) {
const MAP = {
DAI: 'dai',
USDC: 'usdc',
BTC: 'bitcoin',
ETH: 'ethereum',
LTC: 'litecoin',
BCH: 'bitcoincash',
};
return MAP[currency] || 'bitcoin';
}
function getSentAmountStr(swapInfo) {
if (swapInfo && swapInfo.status) {
const currency = swapInfo.status.receiptCurrency;
const coin = currencyToCoin(currency);
return getCoinSendAmountStr(coin);
}
return '';
}
function getCoinLabel(coin) {
const COIN_LABEL = {
dai: 'Dai',
usdc: 'USD Coin',
bitcoin: 'Bitcoin',
ethereum: 'Ethereum',
litecoin: 'Litecoin',
bitcoincash: 'Bitcoin Cash',
};
return COIN_LABEL[coin] || coin;
}
function getLbcAmountStrForSwap(swap) {
if (swap && swap.lbcAmount) {
return formatCoinAmountString(swap.lbcAmount);
}
return '---';
}
function handleStartSwap() {
setIsSwapping(true);
setSwap(null);
setNag(null);
Lbryio.call('btc', 'swap', {
lbc_satoshi_requested: parseInt(lbc * BTC_SATOSHIS + 0.5),
btc_satoshi_provided: parseInt(btc * BTC_SATOSHIS + 0.5),
pay_to_wallet_address: receiveAddress,
})
.then((response) => {
const btcAmount = response.Charge.data.pricing['bitcoin'].amount;
const rate = response.Exchange.rate;
const timeline = response.Charge.data.timeline;
const lastTimeline = timeline[timeline.length - 1];
const newSwap = {
chargeCode: response.Exchange.charge_code,
coins: Object.keys(response.Charge.data.addresses),
sendAddresses: response.Charge.data.addresses,
sendAmounts: response.Charge.data.pricing,
lbcAmount: (btcAmount * BTC_SATOSHIS) / rate,
status: {
status: lastTimeline.status,
receiptCurrency: lastTimeline.payment.value.currency,
receiptTxid: lastTimeline.payment.transaction_id,
lbcTxid: response.Exchange.lbc_txid || '',
},
};
setSwap({ ...newSwap });
addCoinSwap({ ...newSwap });
})
.catch((err) => {
const translateError = (err) => {
// TODO: https://github.com/lbryio/lbry.go/issues/87
// Translate error codes instead of strings when it is available.
if (err === 'users are currently limited to 4 transactions per month') {
return __('Users are currently limited to 4 completed swaps per month or 5 pending swaps.');
}
return err;
};
setNag({ msg: err === INTERNAL_APIS_DOWN ? NAG_SWAP_CALL_FAILED : translateError(err.message), type: 'error' });
returnToMainAction();
});
}
function handleViewPastSwaps() {
setAction(ACTION_PAST_SWAPS);
setNag(null);
setIsRefreshingStatus(true);
const now = Date.now();
if (!lastStatusQuery || now - lastStatusQuery > 30000) {
// There is a '200/minute' limit in the commerce API. If the history is
// long, or if the user goes trigger-happy, the limit could be reached
// easily. Statuses don't change often, so just limit it to every 30s.
setLastStatusQuery(now);
coinSwaps.forEach((x) => {
queryCoinSwapStatus(x.chargeCode);
});
}
}
function getShortStatusStr(coinSwap: CoinSwapInfo) {
const swapInfo = coinSwaps.find((x) => x.chargeCode === coinSwap.chargeCode);
if (!swapInfo || !swapInfo.status) {
return '---';
}
let msg;
switch (swapInfo.status.status) {
case BTC_API_STATUS_PENDING:
msg = __('Waiting');
break;
case BTC_API_STATUS_CONFIRMING:
msg = __('Confirming');
break;
case BTC_API_STATUS_PROCESSING:
if (swapInfo.status.lbcTxid) {
msg = __('Credits sent');
} else {
msg = __('Sending Credits');
}
break;
case BTC_API_STATUS_ERROR:
msg = __('Failed');
break;
case BTC_API_STATUS_EXPIRED:
msg = __('Expired');
break;
case BTC_API_STATUS_UNRESOLVED:
msg = __('Unresolved');
break;
default:
msg = swapInfo.status.status;
// if (IS_DEV) throw new Error('Unhandled "status": ' + status.Status);
break;
}
return msg;
}
function getViewTransactionElement(swap, isSend) {
if (!swap || !swap.status) {
return '';
}
const explorerUrl = (coin, txid) => {
// It's unclear whether we can link to sites like blockchain.com.
// Don't do it for now.
return '';
};
if (isSend) {
const sendTxId = swap.status.receiptTxid;
const url = explorerUrl(swap.status.receiptCurrency, sendTxId);
return sendTxId ? (
<>
{url && <Button button="link" href={url} label={__('View transaction')} />}
{!url && (
<Button
button="link"
label={__('Copy transaction ID')}
title={sendTxId}
onClick={() => {
clipboard.writeText(sendTxId);
doToast({
message: __('Transaction ID copied.'),
});
}}
/>
)}
</>
) : null;
} else {
const lbcTxId = swap.status.lbcTxid;
return lbcTxId ? (
<Button button="link" href={`https://explorer.lbry.com/tx/${lbcTxId}`} label={__('View transaction')} />
) : null;
}
}
function getCloseButton() {
return (
<>
<Button autoFocus button="primary" label={__('Close')} onClick={() => goBack()} />
<Icon
className="icon--help"
icon={ICONS.HELP}
tooltip
size={16}
customTooltipText={__(
'This page can be closed while the transactions are in progress.\nYou can view the status later from:\n • Wallet » Swap » View Past Swaps'
)}
/>
</>
);
}
function getGap() {
return <div className="confirm__value" />; // better way?
}
function getActionElement() {
switch (action) {
case ACTION_MAIN:
return actionMain;
case ACTION_STATUS_PENDING:
return actionPending;
case ACTION_STATUS_CONFIRMING:
return actionConfirmingSend;
case ACTION_STATUS_PROCESSING: // fall-through
case ACTION_STATUS_SUCCESS:
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={
<I18nMessage
tokens={{
lbc: <LbcSymbol size={22} />,
}}
>
Enter desired %lbc%
</I18nMessage>
}
type="number"
name="lbc"
className="form-field--price-amount--auto"
affixClass="form-field--fix-no-height"
max={LBC_MAX}
min={LBC_MIN}
step={1 / BTC_SATOSHIS}
placeholder="12.34"
value={lbc}
error={lbcError}
disabled={isSwapping}
onChange={(event) => setLbc(parseFloat(event.target.value))}
/>
{getGap()}
<div className="confirm__label">{__('Estimated BTC price')}</div>
<div className="confirm__value">
{formatCoinAmountString(btc)} {btc === 0 ? '' : 'BTC'}
{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 || lbcError}
label={isSwapping ? __('Processing...') : __('Start Swap')}
/>
{!isSwapping && coinSwaps.length !== 0 && (
<Button button="link" label={__('View Past Swaps')} onClick={handleViewPastSwaps} />
)}
{isSwapping && <Spinner type="small" />}
</div>
</>
);
const actionPending = (
<>
<div className="section section--padded card--inline confirm__wrapper">
<div className="section">
{swap && swap.coins && ENABLE_ALTERNATIVE_COINS && (
<>
<FormField
type="select"
name="select_coin"
value={coin}
label={__('Alternative coins')}
onChange={(e) => setCoin(e.target.value)}
>
{swap.coins.map((x) => (
<option key={x} value={x}>
{getCoinLabel(x)}
</option>
))}
</FormField>
{getGap()}
</>
)}
<div className="confirm__label">{__('Send')}</div>
<CopyableText
primaryButton
copyable={getCoinSendAmountStr(coin)}
snackMessage={__('Amount copied.')}
onCopy={(inputElem) => {
const inputStr = inputElem.value;
const selectEndIndex = inputStr.lastIndexOf(' ');
if (selectEndIndex > -1 && inputStr.substring(0, selectEndIndex).match(/[\d.]/)) {
inputElem.setSelectionRange(0, selectEndIndex, 'forward');
}
}}
/>
<div className="help">{__('Use the copy button to ensure the EXACT amount is sent!')}</div>
{getGap()}
<div className="confirm__label">{__('To')}</div>
<CopyableText primaryButton copyable={getCoinAddress(coin)} snackMessage={__('Address copied.')} />
<div className="confirm__value--subitem">
<Button
button="link"
label={showQr ? __('Hide QR code') : __('Show QR code')}
onClick={() => setShowQr(!showQr)}
/>
{showQr && getCoinAddress(coin) && <QRCode value={getCoinAddress(coin)} />}
</div>
{getGap()}
<div className="confirm__label">{__('Receive')}</div>
<div className="confirm__value">{<LbcSymbol postfix={getLbcAmountStrForSwap(swap)} size={22} />}</div>
</div>
</div>
<div className="section__actions">{getCloseButton()}</div>
</>
);
const actionConfirmingSend = (
<>
<div className="section section--padded card--inline confirm__wrapper">
<div className="section">
<div className="confirm__label">{__('Confirming')}</div>
<div className="confirm__value confirm__value--no-gap">{getSentAmountStr(swap)}</div>
<div className="confirm__value--subitem">{getViewTransactionElement(swap, true)}</div>
</div>
</div>
<div className="section__actions">{getCloseButton()}</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 confirm__value--no-gap">{getSentAmountStr(swap)}</div>
<div className="confirm__value--subitem">{getViewTransactionElement(swap, true)}</div>
{getGap()}
<div className="confirm__label">{action === ACTION_STATUS_SUCCESS ? __('Received') : __('Receiving')}</div>
<div className="confirm__value confirm__value--no-gap">
{<LbcSymbol postfix={getLbcAmountStrForSwap(swap)} size={22} />}
</div>
{action === ACTION_STATUS_SUCCESS && (
<div className="confirm__value--subitem">{getViewTransactionElement(swap, false)}</div>
)}
</div>
</div>
<div className="section__actions">{getCloseButton()}</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>{__('Code')}</th>
<th>{__('Status')}</th>
<th />
</tr>
</thead>
<tbody>
{coinSwaps.length === 0 && (
<tr>
<td>{'---'}</td>
</tr>
)}
{coinSwaps.length !== 0 &&
coinSwaps.map((x) => {
return (
<tr key={x.chargeCode}>
<td>
<Button
button="link"
className="button--hash-id"
title={x.chargeCode}
label={x.chargeCode}
onClick={() => {
setSwap({ ...x });
}}
/>
</td>
<td>{isRefreshingStatus ? '...' : getShortStatusStr(x)}</td>
<td>
<Button
button="link"
icon={ICONS.REMOVE}
title={__('Remove swap')}
onClick={() => removeCoinSwap(x.chargeCode)}
/>
</td>
</tr>
);
})}
</tbody>
</table>
</div>
</div>
</div>
<div className="section__actions">
<Button
autoFocus
button="primary"
label={__('Go Back')}
onClick={() => {
returnToMainAction();
setNag(null);
}}
/>
{coinSwaps.length !== 0 && !isRefreshingStatus && (
<Button button="link" label={__('Refresh')} onClick={handleViewPastSwaps} />
)}
{isRefreshingStatus && <Spinner type="small" />}
</div>
</>
);
if (!isAuthenticated) {
return <Redirect to={`/$/${PAGES.AUTH_SIGNIN}?redirect=${location.pathname}`} />;
}
return (
<Form onSubmit={handleStartSwap}>
<Card
title={<I18nMessage tokens={{ lbc: <LbcSymbol size={22} /> }}>Swap Crypto for %lbc%</I18nMessage>}
subtitle={__(
'Send crypto to the address provided and you will be sent an equivalent amount of Credits. You can pay with BCH, LTC, ETH, USDC or DAI after starting the swap.'
)}
actions={getActionElement()}
nag={nag ? <Nag relative type={nag.type} message={__(nag.msg)} /> : null}
/>
</Form>
);
}
export default WalletSwap;

View file

@ -431,11 +431,6 @@ export const COMMENT_SUPER_CHAT_LIST_FAILED = 'COMMENT_SUPER_CHAT_LIST_FAILED';
// Blocked channels
export const TOGGLE_BLOCK_CHANNEL = 'TOGGLE_BLOCK_CHANNEL';
// Coin swap
export const ADD_COIN_SWAP = 'ADD_COIN_SWAP';
export const REMOVE_COIN_SWAP = 'REMOVE_COIN_SWAP';
export const COIN_SWAP_STATUS_RECEIVED = 'COIN_SWAP_STATUS_RECEIVED';
// Tags
export const TOGGLE_TAG_FOLLOW = 'TOGGLE_TAG_FOLLOW';
export const TAG_ADD = 'TAG_ADD';

View file

@ -40,7 +40,6 @@ 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';
export const BLOCK_CHANNEL = 'block_channel';
export const COLLECTION_ADD = 'collection_add';
export const COLLECTION_DELETE = 'collection_delete';

View file

@ -51,7 +51,6 @@ export const PAGE_TITLE = {
[PAGES.SETTINGS_STRIPE_CARD]: 'Payment Methods',
[PAGES.SETTINGS_UPDATE_PWD]: 'Update password',
[PAGES.SETTINGS_OWN_COMMENTS]: 'Your comments',
[PAGES.SWAP]: 'Swap Credits',
[PAGES.TAGS_FOLLOWING]: 'Tags',
[PAGES.TAGS_FOLLOWING_MANAGE]: 'Manage tags',
[PAGES.UPLOAD]: 'Upload',

View file

@ -70,7 +70,6 @@ exports.CODE_2257 = '2257';
exports.BUY = 'buy';
exports.RECEIVE = 'receive';
exports.SEND = 'send';
exports.SWAP = 'swap';
exports.CHANNEL_NEW = 'channel/new';
exports.NOTIFICATIONS = 'notifications';
exports.YOUTUBE_SYNC = 'youtube';

View file

@ -1,13 +0,0 @@
import { connect } from 'react-redux';
import { doHideModal } from 'redux/actions/app';
import ModalRemoveBtcSwapAddress from './view';
import { doRemoveCoinSwap } from 'redux/actions/coinSwap';
const select = (state, props) => ({});
const perform = (dispatch) => ({
removeCoinSwap: (chargeCode) => dispatch(doRemoveCoinSwap(chargeCode)),
closeModal: () => dispatch(doHideModal()),
});
export default connect(select, perform)(ModalRemoveBtcSwapAddress);

View file

@ -1,43 +0,0 @@
// @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 = {
chargeCode: string,
removeCoinSwap: (string) => void,
closeModal: () => void,
};
function ModalRemoveBtcSwapAddress(props: Props) {
const { chargeCode, removeCoinSwap, closeModal } = props;
return (
<Modal isOpen contentLabel={__('Confirm Swap Removal')} type="card" onAborted={closeModal}>
<Card
title={__('Remove Swap')}
subtitle={<I18nMessage tokens={{ address: <em>{`${chargeCode}`}</em> }}>Remove %address%?</I18nMessage>}
body={<p className="help--warning">{__('This process cannot be reversed.')}</p>}
actions={
<>
<div className="section__actions">
<Button
button="primary"
label={__('OK')}
onClick={() => {
removeCoinSwap(chargeCode);
closeModal();
}}
/>
<Button button="link" label={__('Cancel')} onClick={closeModal} />
</div>
</>
}
/>
</Modal>
);
}
export default ModalRemoveBtcSwapAddress;

View file

@ -41,8 +41,6 @@ import ModalPasswordUnsave from 'modal/modalPasswordUnsave';
import ModalPublish from 'modal/modalPublish';
import ModalPublishPreview from 'modal/modalPublishPreview';
import ModalRemoveBtcSwapAddress from 'modal/modalRemoveBtcSwapAddress';
import ModalRemoveCard from 'modal/modalRemoveCard';
import ModalRemoveComment from 'modal/modalRemoveComment';
@ -137,8 +135,6 @@ function getModal(id) {
return ModalViewImage;
case MODALS.MASS_TIP_UNLOCK:
return ModalMassTipsUnlock;
case MODALS.CONFIRM_REMOVE_BTC_SWAP_ADDRESS:
return ModalRemoveBtcSwapAddress;
case MODALS.BLOCK_CHANNEL:
return ModalBlockChannel;
case MODALS.COLLECTION_ADD:

View file

@ -313,39 +313,6 @@ export default function SettingsCreatorPage(props: Props) {
/>
</SettingsRow>
<SettingsRow
title={
<I18nMessage tokens={{ lbc: <LbcSymbol /> }}>Minimum %lbc% tip amount for hyperchats</I18nMessage>
}
subtitle={
<>
{__(HELP.MIN_SUPER)}
{minTip !== 0 && (
<p className="help--inline">
<em>{__(HELP.MIN_SUPER_OFF)}</em>
</p>
)}
</>
}
>
<FormField
name="min_tip_amount_super_chat"
className="form-field--price-amount"
min={0}
step="any"
type="number"
placeholder="1"
value={minSuper}
disabled={minTip !== 0}
onChange={(e) => {
const newMinSuper = parseFloat(e.target.value);
setMinSuper(newMinSuper);
pushMinSuperDebounced(newMinSuper, activeChannelClaim);
}}
onBlur={() => setLastUpdated(Date.now())}
/>
</SettingsRow>
<SettingsRow title={__('Moderators')} subtitle={__(HELP.MODERATORS)} multirow>
<SearchChannelField
label={__('Moderators')}

View file

@ -16,7 +16,6 @@ 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';
@ -44,7 +43,6 @@ export default (history) =>
subscriptions: subscriptionsReducer,
tags: tagsReducer,
blocked: blockedReducer,
coinSwap: coinSwapReducer,
user: userReducer,
wallet: walletReducer,
sync: syncReducer,

View file

@ -1,44 +0,0 @@
// @flow
import * as ACTIONS from 'constants/action_types';
import { selectPrefsReady } from 'redux/selectors/sync';
import { doAlertWaitingForSync } from 'redux/actions/app';
import { Lbryio } from 'lbryinc';
export const doAddCoinSwap = (coinSwapInfo: CoinSwapInfo) => (dispatch: Dispatch, getState: GetState) => {
const state = getState();
const ready = selectPrefsReady(state);
if (!ready) {
return dispatch(doAlertWaitingForSync());
}
dispatch({
type: ACTIONS.ADD_COIN_SWAP,
data: coinSwapInfo,
});
};
export const doRemoveCoinSwap = (chargeCode: string) => (dispatch: Dispatch, getState: GetState) => {
const state = getState();
const ready = selectPrefsReady(state);
if (!ready) {
return dispatch(doAlertWaitingForSync());
}
dispatch({
type: ACTIONS.REMOVE_COIN_SWAP,
data: {
chargeCode,
},
});
};
export const doQueryCoinSwapStatus = (chargeCode: string) => (dispatch: Dispatch, getState: GetState) => {
Lbryio.call('btc', 'status', { charge_code: chargeCode }).then((response) => {
dispatch({
type: ACTIONS.COIN_SWAP_STATUS_RECEIVED,
data: response,
});
});
};

View file

@ -98,10 +98,8 @@ export function doSetSync(oldHash: string, newHash: string, data: any) {
};
}
export const doGetSyncDesktop = (cb?: (any, any) => void, password?: string) => (
dispatch: Dispatch,
getState: GetState
) => {
export const doGetSyncDesktop =
(cb?: (any, any) => void, password?: string) => (dispatch: Dispatch, getState: GetState) => {
const state = getState();
const syncEnabled = makeSelectClientSetting(SETTINGS.ENABLE_SYNC)(state);
const getSyncPending = selectGetSyncIsPending(state);
@ -115,7 +113,7 @@ export const doGetSyncDesktop = (cb?: (any, any) => void, password?: string) =>
return dispatch(doGetSync(passwordArgument, cb));
}
});
};
};
export function doSyncLoop(noInterval?: boolean) {
return (dispatch: Dispatch, getState: GetState) => {
@ -385,7 +383,6 @@ type SharedData = {
following?: Array<{ uri: string, notificationsDisabled: boolean }>,
tags?: Array<string>,
blocked?: Array<string>,
coin_swap_codes?: Array<string>,
settings?: any,
app_welcome_version?: number,
sharing_3P?: boolean,
@ -403,7 +400,6 @@ function extractUserState(rawObj: SharedData) {
following,
tags,
blocked,
coin_swap_codes,
settings,
app_welcome_version,
sharing_3P,
@ -418,7 +414,6 @@ function extractUserState(rawObj: SharedData) {
...(following ? { following } : {}),
...(tags ? { tags } : {}),
...(blocked ? { blocked } : {}),
...(coin_swap_codes ? { coin_swap_codes } : {}),
...(settings ? { settings } : {}),
...(app_welcome_version ? { app_welcome_version } : {}),
...(sharing_3P ? { sharing_3P } : {}),
@ -445,7 +440,6 @@ export function doPopulateSharedUserState(sharedSettings: any) {
following,
tags,
blocked,
coin_swap_codes,
settings,
app_welcome_version,
sharing_3P,
@ -470,7 +464,6 @@ export function doPopulateSharedUserState(sharedSettings: any) {
following,
tags,
blocked,
coinSwapCodes: coin_swap_codes,
walletPrefSettings: settings,
mergedClientSettings,
welcomeVersion: app_welcome_version,

View file

@ -82,12 +82,6 @@ export const doNotificationSocketConnect = (enableNotifications) => (dispatch) =
dispatch(doNotificationList());
}
break;
case 'swap-status':
dispatch({
type: ACTIONS.COIN_SWAP_STATUS_RECEIVED,
data: data.data,
});
break;
}
});
};

View file

@ -1,152 +0,0 @@
// @flow
import * as ACTIONS from 'constants/action_types';
import { handleActions } from 'util/redux-utils';
const SWAP_HISTORY_LENGTH_LIMIT = 10;
function getBottomEntries(array, count) {
const curCount = array.length;
if (curCount < count) {
return array;
} else {
return array.slice(curCount - count);
}
}
const defaultState: CoinSwapState = {
coinSwaps: [],
};
export default handleActions(
{
[ACTIONS.ADD_COIN_SWAP]: (state: CoinSwapState, action: CoinSwapAddAction): CoinSwapState => {
const { coinSwaps } = state;
const { chargeCode } = action.data;
const newCoinSwaps = coinSwaps.slice();
if (!newCoinSwaps.find((x) => x.chargeCode === chargeCode)) {
newCoinSwaps.push({ ...action.data });
}
return {
...state,
coinSwaps: newCoinSwaps,
};
},
[ACTIONS.REMOVE_COIN_SWAP]: (state: CoinSwapState, action: CoinSwapRemoveAction): CoinSwapState => {
const { coinSwaps } = state;
const { chargeCode } = action.data;
let newCoinSwaps = coinSwaps.slice();
newCoinSwaps = newCoinSwaps.filter((x) => x.chargeCode !== chargeCode);
return {
...state,
coinSwaps: newCoinSwaps,
};
},
[ACTIONS.COIN_SWAP_STATUS_RECEIVED]: (state: CoinSwapState, action: any) => {
const { coinSwaps } = state;
const newCoinSwaps = coinSwaps.slice();
let exchange;
let charge;
if (action.data.event_data) {
// Source: Websocket
exchange = { lbc_txid: action.data.lbc_txid };
charge = action.data.event_data;
} else {
// Source: btc/status
exchange = action.data.Exchange;
charge = action.data.Charge.data;
}
const calculateLbcAmount = (pricing, exchange, fallback) => {
if (!exchange || !exchange.rate) {
return fallback || 0;
}
const btcAmount = pricing['bitcoin'].amount;
const SATOSHIS = 100000000;
return (btcAmount * SATOSHIS) / exchange.rate;
};
const timeline = charge.timeline;
const lastTimeline = timeline[timeline.length - 1];
const index = newCoinSwaps.findIndex((x) => x.chargeCode === charge.code);
if (index > -1) {
newCoinSwaps[index] = {
chargeCode: charge.code,
coins: Object.keys(charge.addresses),
sendAddresses: charge.addresses,
sendAmounts: charge.pricing,
lbcAmount: calculateLbcAmount(charge.pricing, exchange, newCoinSwaps[index].lbcAmount),
status: {
status: lastTimeline.status,
receiptCurrency: lastTimeline.payment.value.currency,
receiptTxid: lastTimeline.payment.transaction_id,
lbcTxid: exchange.lbc_txid || '',
},
};
} else {
// If a pending swap is removed, the websocket will return an update
// when it expires, for example, causing the entry to re-appear. This
// might be a good thing (e.g. to get back accidental removals), but it
// actually causes synchronization confusion across multiple instances.
const IGNORED_DELETED_SWAPS = true;
if (!IGNORED_DELETED_SWAPS) {
newCoinSwaps.push({
chargeCode: charge.code,
coins: Object.keys(charge.addresses),
sendAddresses: charge.addresses,
sendAmounts: charge.pricing,
lbcAmount: calculateLbcAmount(charge.pricing, exchange, 0),
status: {
status: lastTimeline.status,
receiptCurrency: lastTimeline.payment.value.currency,
receiptTxid: lastTimeline.payment.transaction_id,
lbcTxid: exchange.lbc_txid || '',
},
});
}
}
return {
...state,
coinSwaps: newCoinSwaps,
};
},
[ACTIONS.SYNC_STATE_POPULATE]: (state: CoinSwapState, action: { data: { coinSwapCodes: ?Array<string> } }) => {
const { coinSwapCodes } = action.data;
const newCoinSwaps = [];
if (coinSwapCodes) {
coinSwapCodes.forEach((chargeCode) => {
if (chargeCode && typeof chargeCode === 'string') {
const existingSwap = state.coinSwaps.find((x) => x.chargeCode === chargeCode);
if (existingSwap) {
newCoinSwaps.push({ ...existingSwap });
} else {
newCoinSwaps.push({
// Just restore the 'chargeCode', and query the other data
// via 'btc/status' later.
chargeCode: chargeCode,
coins: [],
sendAddresses: {},
sendAmounts: {},
lbcAmount: 0,
});
}
}
});
}
return {
...state,
coinSwaps: getBottomEntries(newCoinSwaps, SWAP_HISTORY_LENGTH_LIMIT),
};
},
},
defaultState
);

View file

@ -1,8 +0,0 @@
// @flow
import { createSelector } from 'reselect';
const selectState = (state) => state.coinSwap || {};
export const selectCoinSwaps = createSelector(selectState, (state: CoinSwapState) => {
return state.coinSwaps;
});

View file

@ -25,7 +25,9 @@ function isNotFunction(object) {
}
function createBulkThunkMiddleware() {
return ({ dispatch, getState }) => (next) => (action) => {
return ({ dispatch, getState }) =>
(next) =>
(action) => {
if (action.type === 'BATCH_ACTIONS') {
action.actions.filter(isFunction).map((actionFn) => actionFn(dispatch, getState));
}
@ -68,7 +70,6 @@ const searchFilter = createFilter('search', ['options']);
const tagsFilter = createFilter('tags', ['followedTags']);
const subscriptionsFilter = createFilter('subscriptions', ['subscriptions']);
const blockedFilter = createFilter('blocked', ['blockedChannels']);
const coinSwapsFilter = createFilter('coinSwap', ['coinSwaps']);
const settingsFilter = createBlacklistFilter('settings', ['loadedLanguages', 'language']);
const collectionsFilter = createFilter('collections', ['builtin', 'saved', 'unpublished', 'edited', 'pending']);
const whiteListedReducers = [
@ -81,7 +82,6 @@ const whiteListedReducers = [
'app',
'search',
'blocked',
'coinSwap',
'settings',
'subscriptions',
'collections',
@ -92,7 +92,6 @@ const transforms = [
fileInfoFilter,
walletFilter,
blockedFilter,
coinSwapsFilter,
tagsFilter,
appFilter,
searchFilter,
@ -129,8 +128,6 @@ const triggerSharedStateActions = [
ACTIONS.CHANNEL_SUBSCRIBE,
ACTIONS.CHANNEL_UNSUBSCRIBE,
ACTIONS.TOGGLE_BLOCK_CHANNEL,
ACTIONS.ADD_COIN_SWAP,
ACTIONS.REMOVE_COIN_SWAP,
ACTIONS.TOGGLE_TAG_FOLLOW,
ACTIONS.CREATE_CHANNEL_COMPLETED,
ACTIONS.SYNC_CLIENT_SETTINGS,
@ -168,13 +165,6 @@ const sharedStateFilters = {
property: 'following',
},
blocked: { source: 'blocked', property: 'blockedChannels' },
coin_swap_codes: {
source: 'coinSwap',
property: 'coinSwaps',
transform: (coinSwaps) => {
return coinSwaps.map((coinSwapInfo) => coinSwapInfo.chargeCode);
},
},
settings: { source: 'settings', property: 'sharedPreferences' },
app_welcome_version: { source: 'app', property: 'welcomeVersion' },
sharing_3P: { source: 'app', property: 'allowAnalytics' },