// @flow import * as ICONS from 'constants/icons'; import * as PAGES from 'constants/pages'; import React from 'react'; import Button from 'component/button'; import { FormField, Form } from 'component/common/form'; import { MINIMUM_PUBLISH_BID } from 'constants/claim'; import CreditAmount from 'component/common/credit-amount'; import I18nMessage from 'component/i18nMessage'; import { Lbryio } from 'lbryinc'; import Card from 'component/common/card'; import classnames from 'classnames'; import ChannelSelector from 'component/channelSelector'; import LbcSymbol from 'component/common/lbc-symbol'; import { parseURI } from 'lbry-redux'; import usePersistedState from 'effects/use-persisted-state'; import WalletSpendableBalanceHelp from 'component/walletSpendableBalanceHelp'; import { STRIPE_PUBLIC_KEY } from 'config'; let stripeEnvironment = 'test'; // if the key contains pk_live it's a live key // update the environment for the calls to the backend to indicate which environment to hit if (STRIPE_PUBLIC_KEY.indexOf('pk_live') > -1) { stripeEnvironment = 'live'; } const DEFAULT_TIP_AMOUNTS = [1, 5, 25, 100]; const MINIMUM_FIAT_TIP = 1; const MAXIMUM_FIAT_TIP = 1000; const TAB_BOOST = 'TabBoost'; const TAB_FIAT = 'TabFiat'; const TAB_LBC = 'TabLBC'; type SupportParams = { amount: number, claim_id: string, channel_id?: string }; type Props = { uri: string, claimIsMine: boolean, title: string, claim: StreamClaim, isPending: boolean, isSupport: boolean, sendSupport: (SupportParams, boolean) => void, // function that comes from lbry-redux closeModal: () => void, balance: number, fetchingChannels: boolean, instantTipEnabled: boolean, instantTipMax: { amount: number, currency: string }, activeChannelClaim: ?ChannelClaim, incognito: boolean, doToast: ({ message: string }) => void, isAuthenticated: boolean, }; function WalletSendTip(props: Props) { const { uri, title, isPending, claimIsMine, balance, claim = {}, instantTipEnabled, instantTipMax, sendSupport, closeModal, fetchingChannels, incognito, activeChannelClaim, doToast, isAuthenticated, } = props; const [presetTipAmount, setPresetTipAmount] = usePersistedState('comment-support:presetTip', DEFAULT_TIP_AMOUNTS[0]); const [customTipAmount, setCustomTipAmount] = usePersistedState('comment-support:customTip', 1.0); const [useCustomTip, setUseCustomTip] = usePersistedState('comment-support:useCustomTip', false); const [tipError, setTipError] = React.useState(); const [isConfirming, setIsConfirming] = React.useState(false); const { claim_id: claimId } = claim; const { channelName } = parseURI(uri); const activeChannelName = activeChannelClaim && activeChannelClaim.name; const activeChannelId = activeChannelClaim && activeChannelClaim.claim_id; const [canReceiveFiatTip, setCanReceiveFiatTip] = React.useState(); // dont persist because it needs to be calc'd per creator const [hasCardSaved, setHasSavedCard] = usePersistedState('comment-support:hasCardSaved', false); // setup variables for tip API let channelClaimId, tipChannelName; // if there is a signing channel it's on a file if (claim.signing_channel) { channelClaimId = claim.signing_channel.claim_id; tipChannelName = claim.signing_channel.name; // otherwise it's on the channel page } else { channelClaimId = claim.claim_id; tipChannelName = claim.name; } const sourceClaimId = claim.claim_id; // check if creator has a payment method saved React.useEffect(() => { if (channelClaimId && isAuthenticated) { 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)); }); } }, [channelClaimId, isAuthenticated]); // check if creator has an account saved React.useEffect(() => { var tipInputElement = document.getElementById('tip-input'); if (tipInputElement) { tipInputElement.focus() } }, []); React.useEffect(() => { if (channelClaimId) { Lbryio.call( 'account', 'check', { channel_claim_id: channelClaimId, channel_name: tipChannelName, environment: stripeEnvironment, }, 'post' ) .then((accountCheckResponse) => { if (accountCheckResponse === true && canReceiveFiatTip !== true) { setCanReceiveFiatTip(true); } }) .catch(function (error) { // console.log(error); }); } }, [channelClaimId]); const noBalance = balance === 0; const tipAmount = useCustomTip ? customTipAmount : presetTipAmount; const [activeTab, setActiveTab] = React.useState(claimIsMine ? TAB_BOOST : TAB_LBC); function setClaimTypeText() { if (claim.value_type === 'stream') { return __('Content'); } else if (claim.value_type === 'channel') { return __('Channel'); } else if (claim.value_type === 'repost') { return __('Repost'); } else if (claim.value_type === 'collection') { return __('List'); } else { return __('Claim'); } } const claimTypeText = setClaimTypeText(); let iconToUse, explainerText; if (activeTab === TAB_BOOST) { iconToUse = ICONS.LBC; explainerText = __('This refundable boost will improve the discoverability of this %claimTypeText% while active.', {claimTypeText}); } else if (activeTab === TAB_FIAT) { iconToUse = ICONS.FINANCE; explainerText = __('Show this channel your appreciation by sending a donation in USD. '); // if (!hasCardSaved) { // explainerText += __('You must add a card to use this functionality.'); // } } else if (activeTab === TAB_LBC) { iconToUse = ICONS.LBC; explainerText = __('Show this channel your appreciation by sending a donation of Credits. '); } const isSupport = claimIsMine || activeTab === TAB_BOOST; React.useEffect(() => { // Regex for number up to 8 decimal places let regexp; let tipError; if (tipAmount === 0) { tipError = __('Amount must be a positive number'); } else if (!tipAmount || typeof tipAmount !== 'number') { tipError = __('Amount must be a number'); } // if it's not fiat, aka it's boost or lbc tip else if (activeTab !== TAB_FIAT) { regexp = RegExp(/^(\d*([.]\d{0,8})?)$/); const validTipInput = regexp.test(String(tipAmount)); if (!validTipInput) { tipError = __('Amount must have no more than 8 decimal places'); } else if (!validTipInput) { tipError = __('Amount must have no more than 8 decimal places'); } else if (tipAmount === balance) { tipError = __('Please decrease the amount to account for transaction fees'); } else if (tipAmount > balance) { tipError = __('Not enough Credits'); } else if (tipAmount < MINIMUM_PUBLISH_BID) { tipError = __('Amount must be higher'); } // if tip fiat tab } else { regexp = RegExp(/^(\d*([.]\d{0,2})?)$/); const validTipInput = regexp.test(String(tipAmount)); if (!validTipInput) { tipError = __('Amount must have no more than 2 decimal places'); } else if (tipAmount < MINIMUM_FIAT_TIP) { tipError = __('Amount must be at least one dollar'); } else if (tipAmount > MAXIMUM_FIAT_TIP) { tipError = __('Amount cannot be over 1000 dollars'); } } setTipError(tipError); }, [tipAmount, balance, setTipError, activeTab]); // function sendSupportOrConfirm(instantTipMaxAmount = null) { // send a tip if (!isConfirming && (!instantTipMaxAmount || !instantTipEnabled || tipAmount > instantTipMaxAmount)) { setIsConfirming(true); } else { // send a boost const supportParams: SupportParams = { amount: tipAmount, claim_id: claimId }; // include channel name if donation not anonymous if (activeChannelClaim && !incognito) { supportParams.channel_id = activeChannelClaim.claim_id; } // send tip/boost sendSupport(supportParams, isSupport); closeModal(); } } // when the form button is clicked function handleSubmit() { if (tipAmount && claimId) { // send an instant tip (no need to go to an exchange first) if (instantTipEnabled && activeTab !== TAB_FIAT) { if (instantTipMax.currency === 'LBC') { sendSupportOrConfirm(instantTipMax.amount); } else { // Need to convert currency of instant purchase maximum before trying to send support Lbryio.getExchangeRates().then(({ LBC_USD }) => { sendSupportOrConfirm(instantTipMax.amount / LBC_USD); }); } // sending fiat tip } else if (activeTab === TAB_FIAT) { if (!isConfirming) { setIsConfirming(true); } else if (isConfirming) { let sendAnonymously = !activeChannelClaim || incognito; Lbryio.call( 'customer', 'tip', { amount: 100 * tipAmount, // convert from dollars to cents creator_channel_name: tipChannelName, // creator_channel_name creator_channel_claim_id: channelClaimId, tipper_channel_name: sendAnonymously ? '' : activeChannelName, tipper_channel_claim_id: sendAnonymously ? '' : activeChannelId, currency: 'USD', anonymous: sendAnonymously, source_claim_id: sourceClaimId, environment: stripeEnvironment, }, 'post' ) .then((customerTipResponse) => { doToast({ message: __("You sent $%amount% as a tip to %tipChannelName%, I'm sure they appreciate it!", { amount: tipAmount, tipChannelName, }), }); }) .catch(function(error) { var displayError = 'Sorry, there was an error in processing your payment!'; if (error.message !== 'payment intent failed to confirm') { displayError = error.message; } doToast({ message: displayError, isError: true }); }); closeModal(); } // if it's a boost (?) } else { sendSupportOrConfirm(); } } } var countDecimals = function(value) { var text = value.toString(); var index = text.indexOf('.'); return (text.length - index - 1); } function handleCustomPriceChange(event: SyntheticInputEvent<*>) { console.log(event.target.value); let tipAmountAsString = event.target.value; let tipAmount = parseFloat(tipAmountAsString); // allow maximum two decimals if (activeTab === TAB_FIAT) { console.log(tipAmount); console.log(Number.isNaN(tipAmount)) if (Number.isNaN(tipAmount)) { setCustomTipAmount(''); } const howManyDecimals = countDecimals(tipAmountAsString); console.log('how many decimals'); console.log(howManyDecimals) if (howManyDecimals > 2) { tipAmount = Math.floor(tipAmount * 100) / 100; // setTipError('Value can only have two decimal places'); } // else { // tipAmount = ((tipAmount * 100) / 100).toFixed(2); // } // console.log(howManyDecimals); console.log(tipAmount); const howManyDigits = Math.trunc(tipAmount).toString().length; if (howManyDigits > 4 && tipAmount !== 1000) { setTipError('Value must be below 1000 dollars'); } else if (tipAmount > 1000) { setTipError('Value must be below 1000 dollars'); setCustomTipAmount(tipAmount); } else { setCustomTipAmount(tipAmount); } } else { setCustomTipAmount(tipAmount); } } function buildButtonText() { // test if frontend will show up as isNan function isNan(tipAmount) { // testing for NaN ES5 style https://stackoverflow.com/a/35912757/3973137 // also sometimes it's returned as a string // eslint-disable-next-line if (tipAmount !== tipAmount || tipAmount === 'NaN') { return true; } return false; } // if it's a valid number display it, otherwise do an empty string const displayAmount = !isNan(tipAmount) ? tipAmount : ''; if (activeTab === TAB_BOOST) { return (claimIsMine ? __('Boost Your %claimTypeText%', {claimTypeText}) : __('Boost This %claimTypeText%', {claimTypeText})); } else if (activeTab === TAB_FIAT) { return __('Send a $%displayAmount% Tip', { displayAmount }); } else if (activeTab === TAB_LBC) { return __('Send a %displayAmount% Credit Tip', { displayAmount }); } } function shouldDisableAmountSelector(amount) { return ( (amount > balance && activeTab !== TAB_FIAT) || (activeTab === TAB_FIAT && (!hasCardSaved || !canReceiveFiatTip)) ); } function setConfirmLabel() { if (activeTab === TAB_LBC) { return __('Tipping Credit'); } else if (activeTab === TAB_FIAT) { return __('Tipping Fiat (USD)'); } else if (activeTab === TAB_BOOST) { return __('Boosting'); } } return (
); } export default WalletSendTip;