Remove swap #7659
20 changed files with 24 additions and 1117 deletions
|
@ -5,7 +5,6 @@ import Icon from 'component/common/icon';
|
||||||
import classnames from 'classnames';
|
import classnames from 'classnames';
|
||||||
import { NavLink } from 'react-router-dom';
|
import { NavLink } from 'react-router-dom';
|
||||||
import { formatLbryUrlForWeb } from 'util/url';
|
import { formatLbryUrlForWeb } from 'util/url';
|
||||||
import * as PAGES from 'constants/pages';
|
|
||||||
import useCombinedRefs from 'effects/use-combined-refs';
|
import useCombinedRefs from 'effects/use-combined-refs';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
|
@ -34,7 +33,6 @@ type Props = {
|
||||||
onMouseLeave: ?(any) => any,
|
onMouseLeave: ?(any) => any,
|
||||||
pathname: string,
|
pathname: string,
|
||||||
emailVerified: boolean,
|
emailVerified: boolean,
|
||||||
requiresAuth: ?boolean,
|
|
||||||
myref: any,
|
myref: any,
|
||||||
dispatch: any,
|
dispatch: any,
|
||||||
'aria-label'?: string,
|
'aria-label'?: string,
|
||||||
|
@ -66,7 +64,6 @@ const Button = forwardRef<any, {}>((props: Props, ref: any) => {
|
||||||
iconColor,
|
iconColor,
|
||||||
activeClass,
|
activeClass,
|
||||||
emailVerified,
|
emailVerified,
|
||||||
requiresAuth,
|
|
||||||
myref,
|
myref,
|
||||||
dispatch, // <button> doesn't know what to do with dispatch
|
dispatch, // <button> doesn't know what to do with dispatch
|
||||||
pathname,
|
pathname,
|
||||||
|
@ -75,7 +72,7 @@ const Button = forwardRef<any, {}>((props: Props, ref: any) => {
|
||||||
...otherProps
|
...otherProps
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
const disable = disabled || (user === null && requiresAuth);
|
const disable = disabled;
|
||||||
|
|
||||||
const combinedClassName = classnames(
|
const combinedClassName = classnames(
|
||||||
'button',
|
'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 ? (
|
return path ? (
|
||||||
<NavLink
|
<NavLink
|
||||||
exact
|
exact
|
||||||
|
|
|
@ -89,7 +89,6 @@ export default function SearchChannelField(props: Props) {
|
||||||
return (
|
return (
|
||||||
<Button
|
<Button
|
||||||
ref={addTagRef}
|
ref={addTagRef}
|
||||||
requiresAuth
|
|
||||||
button="primary"
|
button="primary"
|
||||||
label={labelFoundAction}
|
label={labelFoundAction}
|
||||||
onClick={() => handleFoundChannelClick(claim)}
|
onClick={() => handleFoundChannelClick(claim)}
|
||||||
|
|
|
@ -41,7 +41,7 @@ function SyncToggle(props: Props) {
|
||||||
{!verifiedEmail && (
|
{!verifiedEmail && (
|
||||||
<div>
|
<div>
|
||||||
<p className="help">{__('An email address is required to sync your account.')}</p>
|
<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>
|
</div>
|
||||||
)}
|
)}
|
||||||
</SettingsRow>
|
</SettingsRow>
|
||||||
|
|
|
@ -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));
|
|
|
@ -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;
|
|
|
@ -431,11 +431,6 @@ export const COMMENT_SUPER_CHAT_LIST_FAILED = 'COMMENT_SUPER_CHAT_LIST_FAILED';
|
||||||
// Blocked channels
|
// Blocked channels
|
||||||
export const TOGGLE_BLOCK_CHANNEL = 'TOGGLE_BLOCK_CHANNEL';
|
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
|
// Tags
|
||||||
export const TOGGLE_TAG_FOLLOW = 'TOGGLE_TAG_FOLLOW';
|
export const TOGGLE_TAG_FOLLOW = 'TOGGLE_TAG_FOLLOW';
|
||||||
export const TAG_ADD = 'TAG_ADD';
|
export const TAG_ADD = 'TAG_ADD';
|
||||||
|
|
|
@ -40,7 +40,6 @@ export const SYNC_ENABLE = 'SYNC_ENABLE';
|
||||||
export const IMAGE_UPLOAD = 'image_upload';
|
export const IMAGE_UPLOAD = 'image_upload';
|
||||||
export const MOBILE_SEARCH = 'mobile_search';
|
export const MOBILE_SEARCH = 'mobile_search';
|
||||||
export const VIEW_IMAGE = 'view_image';
|
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 BLOCK_CHANNEL = 'block_channel';
|
||||||
export const COLLECTION_ADD = 'collection_add';
|
export const COLLECTION_ADD = 'collection_add';
|
||||||
export const COLLECTION_DELETE = 'collection_delete';
|
export const COLLECTION_DELETE = 'collection_delete';
|
||||||
|
|
|
@ -51,7 +51,6 @@ export const PAGE_TITLE = {
|
||||||
[PAGES.SETTINGS_STRIPE_CARD]: 'Payment Methods',
|
[PAGES.SETTINGS_STRIPE_CARD]: 'Payment Methods',
|
||||||
[PAGES.SETTINGS_UPDATE_PWD]: 'Update password',
|
[PAGES.SETTINGS_UPDATE_PWD]: 'Update password',
|
||||||
[PAGES.SETTINGS_OWN_COMMENTS]: 'Your comments',
|
[PAGES.SETTINGS_OWN_COMMENTS]: 'Your comments',
|
||||||
[PAGES.SWAP]: 'Swap Credits',
|
|
||||||
[PAGES.TAGS_FOLLOWING]: 'Tags',
|
[PAGES.TAGS_FOLLOWING]: 'Tags',
|
||||||
[PAGES.TAGS_FOLLOWING_MANAGE]: 'Manage tags',
|
[PAGES.TAGS_FOLLOWING_MANAGE]: 'Manage tags',
|
||||||
[PAGES.UPLOAD]: 'Upload',
|
[PAGES.UPLOAD]: 'Upload',
|
||||||
|
|
|
@ -70,7 +70,6 @@ exports.CODE_2257 = '2257';
|
||||||
exports.BUY = 'buy';
|
exports.BUY = 'buy';
|
||||||
exports.RECEIVE = 'receive';
|
exports.RECEIVE = 'receive';
|
||||||
exports.SEND = 'send';
|
exports.SEND = 'send';
|
||||||
exports.SWAP = 'swap';
|
|
||||||
exports.CHANNEL_NEW = 'channel/new';
|
exports.CHANNEL_NEW = 'channel/new';
|
||||||
exports.NOTIFICATIONS = 'notifications';
|
exports.NOTIFICATIONS = 'notifications';
|
||||||
exports.YOUTUBE_SYNC = 'youtube';
|
exports.YOUTUBE_SYNC = 'youtube';
|
||||||
|
|
|
@ -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);
|
|
|
@ -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;
|
|
|
@ -41,8 +41,6 @@ import ModalPasswordUnsave from 'modal/modalPasswordUnsave';
|
||||||
import ModalPublish from 'modal/modalPublish';
|
import ModalPublish from 'modal/modalPublish';
|
||||||
import ModalPublishPreview from 'modal/modalPublishPreview';
|
import ModalPublishPreview from 'modal/modalPublishPreview';
|
||||||
|
|
||||||
import ModalRemoveBtcSwapAddress from 'modal/modalRemoveBtcSwapAddress';
|
|
||||||
|
|
||||||
import ModalRemoveCard from 'modal/modalRemoveCard';
|
import ModalRemoveCard from 'modal/modalRemoveCard';
|
||||||
import ModalRemoveComment from 'modal/modalRemoveComment';
|
import ModalRemoveComment from 'modal/modalRemoveComment';
|
||||||
|
|
||||||
|
@ -137,8 +135,6 @@ function getModal(id) {
|
||||||
return ModalViewImage;
|
return ModalViewImage;
|
||||||
case MODALS.MASS_TIP_UNLOCK:
|
case MODALS.MASS_TIP_UNLOCK:
|
||||||
return ModalMassTipsUnlock;
|
return ModalMassTipsUnlock;
|
||||||
case MODALS.CONFIRM_REMOVE_BTC_SWAP_ADDRESS:
|
|
||||||
return ModalRemoveBtcSwapAddress;
|
|
||||||
case MODALS.BLOCK_CHANNEL:
|
case MODALS.BLOCK_CHANNEL:
|
||||||
return ModalBlockChannel;
|
return ModalBlockChannel;
|
||||||
case MODALS.COLLECTION_ADD:
|
case MODALS.COLLECTION_ADD:
|
||||||
|
|
|
@ -313,39 +313,6 @@ export default function SettingsCreatorPage(props: Props) {
|
||||||
/>
|
/>
|
||||||
</SettingsRow>
|
</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>
|
<SettingsRow title={__('Moderators')} subtitle={__(HELP.MODERATORS)} multirow>
|
||||||
<SearchChannelField
|
<SearchChannelField
|
||||||
label={__('Moderators')}
|
label={__('Moderators')}
|
||||||
|
|
|
@ -16,7 +16,6 @@ import rewardsReducer from 'redux/reducers/rewards';
|
||||||
import userReducer from 'redux/reducers/user';
|
import userReducer from 'redux/reducers/user';
|
||||||
import commentsReducer from 'redux/reducers/comments';
|
import commentsReducer from 'redux/reducers/comments';
|
||||||
import blockedReducer from 'redux/reducers/blocked';
|
import blockedReducer from 'redux/reducers/blocked';
|
||||||
import coinSwapReducer from 'redux/reducers/coinSwap';
|
|
||||||
import searchReducer from 'redux/reducers/search';
|
import searchReducer from 'redux/reducers/search';
|
||||||
import reactionsReducer from 'redux/reducers/reactions';
|
import reactionsReducer from 'redux/reducers/reactions';
|
||||||
import syncReducer from 'redux/reducers/sync';
|
import syncReducer from 'redux/reducers/sync';
|
||||||
|
@ -44,7 +43,6 @@ export default (history) =>
|
||||||
subscriptions: subscriptionsReducer,
|
subscriptions: subscriptionsReducer,
|
||||||
tags: tagsReducer,
|
tags: tagsReducer,
|
||||||
blocked: blockedReducer,
|
blocked: blockedReducer,
|
||||||
coinSwap: coinSwapReducer,
|
|
||||||
user: userReducer,
|
user: userReducer,
|
||||||
wallet: walletReducer,
|
wallet: walletReducer,
|
||||||
sync: syncReducer,
|
sync: syncReducer,
|
||||||
|
|
|
@ -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,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
};
|
|
|
@ -98,10 +98,8 @@ export function doSetSync(oldHash: string, newHash: string, data: any) {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export const doGetSyncDesktop = (cb?: (any, any) => void, password?: string) => (
|
export const doGetSyncDesktop =
|
||||||
dispatch: Dispatch,
|
(cb?: (any, any) => void, password?: string) => (dispatch: Dispatch, getState: GetState) => {
|
||||||
getState: GetState
|
|
||||||
) => {
|
|
||||||
const state = getState();
|
const state = getState();
|
||||||
const syncEnabled = makeSelectClientSetting(SETTINGS.ENABLE_SYNC)(state);
|
const syncEnabled = makeSelectClientSetting(SETTINGS.ENABLE_SYNC)(state);
|
||||||
const getSyncPending = selectGetSyncIsPending(state);
|
const getSyncPending = selectGetSyncIsPending(state);
|
||||||
|
@ -115,7 +113,7 @@ export const doGetSyncDesktop = (cb?: (any, any) => void, password?: string) =>
|
||||||
return dispatch(doGetSync(passwordArgument, cb));
|
return dispatch(doGetSync(passwordArgument, cb));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
export function doSyncLoop(noInterval?: boolean) {
|
export function doSyncLoop(noInterval?: boolean) {
|
||||||
return (dispatch: Dispatch, getState: GetState) => {
|
return (dispatch: Dispatch, getState: GetState) => {
|
||||||
|
@ -385,7 +383,6 @@ type SharedData = {
|
||||||
following?: Array<{ uri: string, notificationsDisabled: boolean }>,
|
following?: Array<{ uri: string, notificationsDisabled: boolean }>,
|
||||||
tags?: Array<string>,
|
tags?: Array<string>,
|
||||||
blocked?: Array<string>,
|
blocked?: Array<string>,
|
||||||
coin_swap_codes?: Array<string>,
|
|
||||||
settings?: any,
|
settings?: any,
|
||||||
app_welcome_version?: number,
|
app_welcome_version?: number,
|
||||||
sharing_3P?: boolean,
|
sharing_3P?: boolean,
|
||||||
|
@ -403,7 +400,6 @@ function extractUserState(rawObj: SharedData) {
|
||||||
following,
|
following,
|
||||||
tags,
|
tags,
|
||||||
blocked,
|
blocked,
|
||||||
coin_swap_codes,
|
|
||||||
settings,
|
settings,
|
||||||
app_welcome_version,
|
app_welcome_version,
|
||||||
sharing_3P,
|
sharing_3P,
|
||||||
|
@ -418,7 +414,6 @@ function extractUserState(rawObj: SharedData) {
|
||||||
...(following ? { following } : {}),
|
...(following ? { following } : {}),
|
||||||
...(tags ? { tags } : {}),
|
...(tags ? { tags } : {}),
|
||||||
...(blocked ? { blocked } : {}),
|
...(blocked ? { blocked } : {}),
|
||||||
...(coin_swap_codes ? { coin_swap_codes } : {}),
|
|
||||||
...(settings ? { settings } : {}),
|
...(settings ? { settings } : {}),
|
||||||
...(app_welcome_version ? { app_welcome_version } : {}),
|
...(app_welcome_version ? { app_welcome_version } : {}),
|
||||||
...(sharing_3P ? { sharing_3P } : {}),
|
...(sharing_3P ? { sharing_3P } : {}),
|
||||||
|
@ -445,7 +440,6 @@ export function doPopulateSharedUserState(sharedSettings: any) {
|
||||||
following,
|
following,
|
||||||
tags,
|
tags,
|
||||||
blocked,
|
blocked,
|
||||||
coin_swap_codes,
|
|
||||||
settings,
|
settings,
|
||||||
app_welcome_version,
|
app_welcome_version,
|
||||||
sharing_3P,
|
sharing_3P,
|
||||||
|
@ -470,7 +464,6 @@ export function doPopulateSharedUserState(sharedSettings: any) {
|
||||||
following,
|
following,
|
||||||
tags,
|
tags,
|
||||||
blocked,
|
blocked,
|
||||||
coinSwapCodes: coin_swap_codes,
|
|
||||||
walletPrefSettings: settings,
|
walletPrefSettings: settings,
|
||||||
mergedClientSettings,
|
mergedClientSettings,
|
||||||
welcomeVersion: app_welcome_version,
|
welcomeVersion: app_welcome_version,
|
||||||
|
|
|
@ -82,12 +82,6 @@ export const doNotificationSocketConnect = (enableNotifications) => (dispatch) =
|
||||||
dispatch(doNotificationList());
|
dispatch(doNotificationList());
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case 'swap-status':
|
|
||||||
dispatch({
|
|
||||||
type: ACTIONS.COIN_SWAP_STATUS_RECEIVED,
|
|
||||||
data: data.data,
|
|
||||||
});
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
|
@ -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
|
|
||||||
);
|
|
|
@ -1,8 +0,0 @@
|
||||||
// @flow
|
|
||||||
import { createSelector } from 'reselect';
|
|
||||||
|
|
||||||
const selectState = (state) => state.coinSwap || {};
|
|
||||||
|
|
||||||
export const selectCoinSwaps = createSelector(selectState, (state: CoinSwapState) => {
|
|
||||||
return state.coinSwaps;
|
|
||||||
});
|
|
16
ui/store.js
16
ui/store.js
|
@ -25,7 +25,9 @@ function isNotFunction(object) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function createBulkThunkMiddleware() {
|
function createBulkThunkMiddleware() {
|
||||||
return ({ dispatch, getState }) => (next) => (action) => {
|
return ({ dispatch, getState }) =>
|
||||||
|
(next) =>
|
||||||
|
(action) => {
|
||||||
if (action.type === 'BATCH_ACTIONS') {
|
if (action.type === 'BATCH_ACTIONS') {
|
||||||
action.actions.filter(isFunction).map((actionFn) => actionFn(dispatch, getState));
|
action.actions.filter(isFunction).map((actionFn) => actionFn(dispatch, getState));
|
||||||
}
|
}
|
||||||
|
@ -68,7 +70,6 @@ const searchFilter = createFilter('search', ['options']);
|
||||||
const tagsFilter = createFilter('tags', ['followedTags']);
|
const tagsFilter = createFilter('tags', ['followedTags']);
|
||||||
const subscriptionsFilter = createFilter('subscriptions', ['subscriptions']);
|
const subscriptionsFilter = createFilter('subscriptions', ['subscriptions']);
|
||||||
const blockedFilter = createFilter('blocked', ['blockedChannels']);
|
const blockedFilter = createFilter('blocked', ['blockedChannels']);
|
||||||
const coinSwapsFilter = createFilter('coinSwap', ['coinSwaps']);
|
|
||||||
const settingsFilter = createBlacklistFilter('settings', ['loadedLanguages', 'language']);
|
const settingsFilter = createBlacklistFilter('settings', ['loadedLanguages', 'language']);
|
||||||
const collectionsFilter = createFilter('collections', ['builtin', 'saved', 'unpublished', 'edited', 'pending']);
|
const collectionsFilter = createFilter('collections', ['builtin', 'saved', 'unpublished', 'edited', 'pending']);
|
||||||
const whiteListedReducers = [
|
const whiteListedReducers = [
|
||||||
|
@ -81,7 +82,6 @@ const whiteListedReducers = [
|
||||||
'app',
|
'app',
|
||||||
'search',
|
'search',
|
||||||
'blocked',
|
'blocked',
|
||||||
'coinSwap',
|
|
||||||
'settings',
|
'settings',
|
||||||
'subscriptions',
|
'subscriptions',
|
||||||
'collections',
|
'collections',
|
||||||
|
@ -92,7 +92,6 @@ const transforms = [
|
||||||
fileInfoFilter,
|
fileInfoFilter,
|
||||||
walletFilter,
|
walletFilter,
|
||||||
blockedFilter,
|
blockedFilter,
|
||||||
coinSwapsFilter,
|
|
||||||
tagsFilter,
|
tagsFilter,
|
||||||
appFilter,
|
appFilter,
|
||||||
searchFilter,
|
searchFilter,
|
||||||
|
@ -129,8 +128,6 @@ const triggerSharedStateActions = [
|
||||||
ACTIONS.CHANNEL_SUBSCRIBE,
|
ACTIONS.CHANNEL_SUBSCRIBE,
|
||||||
ACTIONS.CHANNEL_UNSUBSCRIBE,
|
ACTIONS.CHANNEL_UNSUBSCRIBE,
|
||||||
ACTIONS.TOGGLE_BLOCK_CHANNEL,
|
ACTIONS.TOGGLE_BLOCK_CHANNEL,
|
||||||
ACTIONS.ADD_COIN_SWAP,
|
|
||||||
ACTIONS.REMOVE_COIN_SWAP,
|
|
||||||
ACTIONS.TOGGLE_TAG_FOLLOW,
|
ACTIONS.TOGGLE_TAG_FOLLOW,
|
||||||
ACTIONS.CREATE_CHANNEL_COMPLETED,
|
ACTIONS.CREATE_CHANNEL_COMPLETED,
|
||||||
ACTIONS.SYNC_CLIENT_SETTINGS,
|
ACTIONS.SYNC_CLIENT_SETTINGS,
|
||||||
|
@ -168,13 +165,6 @@ const sharedStateFilters = {
|
||||||
property: 'following',
|
property: 'following',
|
||||||
},
|
},
|
||||||
blocked: { source: 'blocked', property: 'blockedChannels' },
|
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' },
|
settings: { source: 'settings', property: 'sharedPreferences' },
|
||||||
app_welcome_version: { source: 'app', property: 'welcomeVersion' },
|
app_welcome_version: { source: 'app', property: 'welcomeVersion' },
|
||||||
sharing_3P: { source: 'app', property: 'allowAnalytics' },
|
sharing_3P: { source: 'app', property: 'allowAnalytics' },
|
||||||
|
|
Loading…
Reference in a new issue