lbry-desktop/ui/component/walletTipAmountSelector/view.jsx
2022-03-28 15:30:36 -04:00

327 lines
11 KiB
JavaScript

// @flow
import 'scss/component/_wallet-tip-selector.scss';
import { FormField } from 'component/common/form';
import { Lbryio } from 'lbryinc';
import { MINIMUM_PUBLISH_BID } from 'constants/claim';
import { useIsMobile } from 'effects/use-screensize';
import * as ICONS from 'constants/icons';
import * as PAGES from 'constants/pages';
import Button from 'component/button';
import classnames from 'classnames';
import React from 'react';
import usePersistedState from 'effects/use-persisted-state';
import WalletSpendableBalanceHelp from 'component/walletSpendableBalanceHelp';
import { getStripeEnvironment } from 'util/stripe';
const stripeEnvironment = getStripeEnvironment();
const DEFAULT_TIP_AMOUNTS = [1, 5, 25, 100];
const TAB_FIAT = 'TabFiat';
const TAB_LBC = 'TabLBC';
type Props = {
activeTab: string,
amount: number,
balance: number,
claim: StreamClaim,
convertedAmount?: number,
customTipAmount?: number,
exchangeRate?: any,
fiatConversion?: boolean,
tipError: boolean,
tipError: string,
uri: string,
onChange: (number) => void,
setConvertedAmount?: (number) => void,
setDisableSubmitButton: (boolean) => void,
setTipError: (any) => void,
};
function WalletTipAmountSelector(props: Props) {
const {
activeTab,
amount,
balance,
claim,
convertedAmount,
customTipAmount,
exchangeRate,
fiatConversion,
tipError,
onChange,
setConvertedAmount,
setDisableSubmitButton,
setTipError,
} = props;
const isMobile = useIsMobile();
const [useCustomTip, setUseCustomTip] = usePersistedState('comment-support:useCustomTip', true);
const [hasCardSaved, setHasSavedCard] = usePersistedState('comment-support:hasCardSaved', false);
const [canReceiveFiatTip, setCanReceiveFiatTip] = React.useState(); // dont persist because it needs to be calc'd per creator
const convertToTwoDecimalsOrMore = (number: number, decimals: number = 2) =>
Number((Math.round(number * 10 ** decimals) / 10 ** decimals).toFixed(decimals));
const tipAmountsToDisplay =
customTipAmount && fiatConversion && activeTab === TAB_FIAT
? [customTipAmount]
: customTipAmount && exchangeRate
? [convertToTwoDecimalsOrMore(customTipAmount / exchangeRate)]
: DEFAULT_TIP_AMOUNTS;
// if it's fiat but there's no card saved OR the creator can't receive fiat tips
const shouldDisableFiatSelectors = activeTab === TAB_FIAT && (!hasCardSaved || !canReceiveFiatTip);
// setup variables for tip API
const channelClaimId = claim ? (claim.signing_channel ? claim.signing_channel.claim_id : claim.claim_id) : undefined;
const tipChannelName = claim ? (claim.signing_channel ? claim.signing_channel.name : claim.name) : undefined;
/**
* whether tip amount selection/review functionality should be disabled
* @param [amount] LBC amount (optional)
* @returns {boolean}
*/
function shouldDisableAmountSelector(amount: number) {
// if it's LBC but the balance isn't enough, or fiat conditions met
// $FlowFixMe
return (
((amount > balance || balance === 0) && activeTab !== TAB_FIAT) ||
shouldDisableFiatSelectors ||
(customTipAmount && fiatConversion && activeTab !== TAB_FIAT && exchangeRate
? convertToTwoDecimalsOrMore(amount * exchangeRate) < customTipAmount
: customTipAmount && amount < customTipAmount)
);
}
// parse number as float and sets it in the parent component
function handleCustomPriceChange(amount: number) {
const tipAmountValue = parseFloat(amount);
onChange(tipAmountValue);
if (fiatConversion && exchangeRate && setConvertedAmount && convertedAmount !== tipAmountValue * exchangeRate) {
setConvertedAmount(tipAmountValue * exchangeRate);
}
}
React.useEffect(() => {
if (setDisableSubmitButton) setDisableSubmitButton(shouldDisableFiatSelectors);
}, [setDisableSubmitButton, shouldDisableFiatSelectors]);
React.useEffect(() => {
if (setConvertedAmount && exchangeRate && (!convertedAmount || convertedAmount !== amount * exchangeRate)) {
setConvertedAmount(amount * exchangeRate);
}
}, [amount, convertedAmount, exchangeRate, setConvertedAmount]);
// check if user has a payment method saved
React.useEffect(() => {
if (!stripeEnvironment) return;
Lbryio.call(
'customer',
'status',
{
environment: stripeEnvironment,
},
'post'
).then((customerStatusResponse) => {
const defaultPaymentMethodId =
customerStatusResponse.Customer &&
customerStatusResponse.Customer.invoice_settings &&
customerStatusResponse.Customer.invoice_settings.default_payment_method &&
customerStatusResponse.Customer.invoice_settings.default_payment_method.id;
setHasSavedCard(Boolean(defaultPaymentMethodId));
});
}, [setHasSavedCard]);
// check if creator has a tip account saved
React.useEffect(() => {
if (!stripeEnvironment) return;
Lbryio.call(
'account',
'check',
{
channel_claim_id: channelClaimId,
channel_name: tipChannelName,
environment: stripeEnvironment,
},
'post'
)
.then((accountCheckResponse) => {
if (accountCheckResponse === true && canReceiveFiatTip !== true) {
setCanReceiveFiatTip(true);
}
})
.catch(() => {});
}, [canReceiveFiatTip, channelClaimId, tipChannelName]);
React.useEffect(() => {
let regexp;
if (amount === 0) {
setTipError(__('Amount cannot be zero.'));
} else if (!amount || typeof amount !== 'number') {
setTipError(__('Amount must be a number.'));
} else {
// if it's not fiat, aka it's boost or lbc tip
if (activeTab !== TAB_FIAT) {
regexp = RegExp(/^(\d*([.]\d{0,8})?)$/);
const validTipInput = regexp.test(String(amount));
if (!validTipInput) {
setTipError(__('Amount must have no more than 8 decimal places'));
} else if (amount === balance) {
setTipError(__('Please decrease the amount to account for transaction fees'));
} else if (amount > balance || balance === 0) {
setTipError(__('Not enough Credits'));
} else if (amount < MINIMUM_PUBLISH_BID) {
setTipError(__('Amount must be higher'));
} else if (
convertedAmount &&
exchangeRate &&
customTipAmount &&
amount < convertToTwoDecimalsOrMore(customTipAmount / exchangeRate)
) {
regexp = RegExp(/^(\d*([.]\d{0,2})?)$/);
const validCustomTipInput = regexp.test(String(amount));
if (validCustomTipInput) {
setTipError(
__('Insufficient amount (%input_amount% Credits = %converted_amount% USD).', {
input_amount: amount,
converted_amount: convertToTwoDecimalsOrMore(convertedAmount, 4),
})
);
} else {
setTipError(__('Amount must have no more than 2 decimal places'));
}
} else {
setTipError(false);
}
// if tip fiat tab
} else {
regexp = RegExp(/^(\d*([.]\d{0,2})?)$/);
const validTipInput = regexp.test(String(amount));
if (!validTipInput) {
setTipError(__('Amount must have no more than 2 decimal places'));
} else if (amount < 1) {
setTipError(__('Amount must be at least one dollar'));
} else if (amount > 1000) {
setTipError(__('Amount cannot be over 1000 dollars'));
} else if (customTipAmount && amount < customTipAmount) {
setTipError(
__('Amount is lower than price of $%price_amount%', {
price_amount: convertToTwoDecimalsOrMore(customTipAmount),
})
);
} else {
setTipError(false);
}
}
}
}, [activeTab, amount, balance, convertedAmount, customTipAmount, exchangeRate, setTipError]);
if (!claim) return null;
const getHelpMessage = (helpMessage: any, customClassName) => (
<div className={classnames('help', customClassName)}>{helpMessage}</div>
);
return (
<>
<div className="section">
{tipAmountsToDisplay &&
tipAmountsToDisplay.map((defaultAmount) => (
<Button
key={defaultAmount}
disabled={shouldDisableAmountSelector(defaultAmount)}
button="alt"
className={classnames('button-toggle button-toggle--expandformobile', {
'button-toggle--active':
convertToTwoDecimalsOrMore(defaultAmount) === convertToTwoDecimalsOrMore(amount) && !useCustomTip,
'button-toggle--disabled': amount > balance,
})}
label={defaultAmount}
icon={activeTab === TAB_LBC ? ICONS.LBC : ICONS.FINANCE}
onClick={() => {
handleCustomPriceChange(defaultAmount);
setUseCustomTip(false);
}}
/>
))}
<Button
button="alt"
disabled={shouldDisableFiatSelectors}
className={classnames('button-toggle button-toggle--expandformobile', {
'button-toggle--active': useCustomTip,
})}
icon={activeTab === TAB_LBC ? ICONS.LBC : ICONS.FINANCE}
label={__('Custom')}
onClick={() => setUseCustomTip(true)}
/>
{activeTab === TAB_LBC && DEFAULT_TIP_AMOUNTS.some((val) => val > balance) && (
<Button
button="secondary"
className="button-toggle-group-action"
icon={ICONS.BUY}
title={__('Buy or swap more LBRY Credits')}
navigate={`/$/${PAGES.BUY}`}
/>
)}
</div>
{customTipAmount &&
fiatConversion &&
activeTab !== TAB_FIAT &&
getHelpMessage(
__('This support is priced in $USD.') +
(convertedAmount
? ' ' +
__('The current exchange rate for the submitted LBC amount is ~ $%exchange_amount%.', {
exchange_amount: convertToTwoDecimalsOrMore(convertedAmount),
})
: '')
)}
{/* custom number input form */}
{useCustomTip && (
<div className="walletTipSelector__input">
<FormField
autoFocus={!isMobile}
name="tip-input"
disabled={!customTipAmount && shouldDisableAmountSelector(0)}
error={tipError}
min="0"
step="any"
type="number"
placeholder="1.23"
value={amount}
onChange={(event) => handleCustomPriceChange(event.target.value)}
/>
</div>
)}
{/* lbc tab */}
{activeTab === TAB_LBC && <WalletSpendableBalanceHelp />}
{/* help message */}
{activeTab === TAB_FIAT &&
(!hasCardSaved
? getHelpMessage(
<>
<Button navigate={`/$/${PAGES.SETTINGS_STRIPE_CARD}`} label={__('Add a Card')} button="link" />
{' ' + __('To Tip Creators')}
</>,
'add-a-card-help-message'
)
: !canReceiveFiatTip
? getHelpMessage(__('Only creators that verify cash accounts can receive tips'))
: getHelpMessage(__('Send a tip directly from your attached card')))}
</>
);
}
export default WalletTipAmountSelector;