- {viewMode === VIEW_MODE_CHAT && superChatsByTipAmount && (superChatsTotalAmount || 0) > 0 && (
+ {viewMode === VIEW_MODE_CHAT && superChatsByTipAmount && hasSuperChats && (
- {superChatsByTipAmount.map((superChat: Comment) => (
-
-
-
-
-
+ {superChatsByTipAmount.map((superChat: Comment) => {
+ const isSticker = stickerSuperChats && stickerSuperChats.includes(superChat);
-
-
-
+ const SuperChatWrapper = !isSticker
+ ? ({ children }) =>
{children}
+ : ({ children }) => <>{children}>;
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+ {stickerSuperChats.includes(superChat) && getStickerUrl(superChat.comment) && (
+
+
+
+ )}
+
-
-
- ))}
+
+ );
+ })}
)}
diff --git a/ui/component/walletSendTip/index.js b/ui/component/walletSendTip/index.js
index 5cf567057..b1636ed05 100644
--- a/ui/component/walletSendTip/index.js
+++ b/ui/component/walletSendTip/index.js
@@ -5,36 +5,26 @@ import {
makeSelectClaimIsMine,
selectFetchingMyChannels,
} from 'redux/selectors/claims';
-import { selectBalance, selectIsSendingSupport } from 'redux/selectors/wallet';
-import { doSendTip } from 'redux/actions/wallet';
-import * as SETTINGS from 'constants/settings';
-import WalletSendTip from './view';
-import { doOpenModal, doHideModal } from 'redux/actions/app';
-import { withRouter } from 'react-router';
+import { doHideModal } from 'redux/actions/app';
+import { doSendTip, doSendCashTip } from 'redux/actions/wallet';
import { makeSelectClientSetting } from 'redux/selectors/settings';
import { selectActiveChannelClaim, selectIncognito } from 'redux/selectors/app';
-import { doToast } from 'redux/actions/notifications';
-import { selectUserVerifiedEmail } from 'redux/selectors/user';
+import { selectBalance, selectIsSendingSupport } from 'redux/selectors/wallet';
+import { withRouter } from 'react-router';
+import * as SETTINGS from 'constants/settings';
+import WalletSendTip from './view';
const select = (state, props) => ({
- isPending: selectIsSendingSupport(state),
- title: makeSelectTitleForUri(props.uri)(state),
- claim: makeSelectClaimForUri(props.uri, false)(state),
+ activeChannelClaim: selectActiveChannelClaim(state),
balance: selectBalance(state),
- instantTipEnabled: makeSelectClientSetting(SETTINGS.INSTANT_PURCHASE_ENABLED)(state),
- instantTipMax: makeSelectClientSetting(SETTINGS.INSTANT_PURCHASE_MAX)(state),
+ claim: makeSelectClaimForUri(props.uri, false)(state),
claimIsMine: makeSelectClaimIsMine(props.uri)(state),
fetchingChannels: selectFetchingMyChannels(state),
- activeChannelClaim: selectActiveChannelClaim(state),
incognito: selectIncognito(state),
- isAuthenticated: Boolean(selectUserVerifiedEmail(state)),
+ instantTipEnabled: makeSelectClientSetting(SETTINGS.INSTANT_PURCHASE_ENABLED)(state),
+ instantTipMax: makeSelectClientSetting(SETTINGS.INSTANT_PURCHASE_MAX)(state),
+ isPending: selectIsSendingSupport(state),
+ title: makeSelectTitleForUri(props.uri)(state),
});
-const perform = (dispatch) => ({
- openModal: (modal, props) => dispatch(doOpenModal(modal, props)),
- closeModal: () => dispatch(doHideModal()),
- sendSupport: (params, isSupport) => dispatch(doSendTip(params, isSupport)),
- doToast: (options) => dispatch(doToast(options)),
-});
-
-export default withRouter(connect(select, perform)(WalletSendTip));
+export default withRouter(connect(select, { doHideModal, doSendTip, doSendCashTip })(WalletSendTip));
diff --git a/ui/component/walletSendTip/view.jsx b/ui/component/walletSendTip/view.jsx
index a78be84bf..c2c28958d 100644
--- a/ui/component/walletSendTip/view.jsx
+++ b/ui/component/walletSendTip/view.jsx
@@ -1,403 +1,174 @@
// @flow
+import { Form } from 'component/common/form';
+import { Lbryio } from 'lbryinc';
+import { parseURI } from 'util/lbryURI';
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 classnames from 'classnames';
+import I18nMessage from 'component/i18nMessage';
import LbcSymbol from 'component/common/lbc-symbol';
-import { parseURI } from 'util/lbryURI';
+import React from 'react';
import usePersistedState from 'effects/use-persisted-state';
-import WalletSpendableBalanceHelp from 'component/walletSpendableBalanceHelp';
+import WalletTipAmountSelector from 'component/walletTipAmountSelector';
+
import { getStripeEnvironment } from 'util/stripe';
-let stripeEnvironment = getStripeEnvironment();
-
-const DEFAULT_TIP_AMOUNTS = [1, 5, 25, 100];
-const MINIMUM_FIAT_TIP = 1;
-const MAXIMUM_FIAT_TIP = 1000;
-
-const DEFAULT_TIP_ERROR = __('Sorry, there was an error in processing your payment!');
+const stripeEnvironment = getStripeEnvironment();
const TAB_BOOST = 'TabBoost';
const TAB_FIAT = 'TabFiat';
const TAB_LBC = 'TabLBC';
type SupportParams = { amount: number, claim_id: string, channel_id?: string };
+type TipParams = { tipAmount: number, tipChannelName: string, channelClaimId: string };
+type UserParams = { activeChannelName: ?string, activeChannelId: ?string };
type Props = {
- uri: string,
- claimIsMine: boolean,
- title: string,
- claim: StreamClaim,
- isPending: boolean,
- isSupport: boolean,
- sendSupport: (SupportParams, boolean) => void,
- closeModal: () => void,
+ activeChannelClaim: ?ChannelClaim,
balance: number,
+ claim: StreamClaim,
+ claimIsMine: boolean,
fetchingChannels: boolean,
+ incognito: boolean,
instantTipEnabled: boolean,
instantTipMax: { amount: number, currency: string },
- activeChannelClaim: ?ChannelClaim,
- incognito: boolean,
- doToast: ({ message: string }) => void,
- isAuthenticated: boolean,
+ isPending: boolean,
+ isSupport: boolean,
+ title: string,
+ uri: string,
+ doHideModal: () => void,
+ doSendCashTip: (TipParams, boolean, UserParams, string, ?string) => string,
+ doSendTip: (SupportParams, boolean) => void, // function that comes from lbry-redux
};
function WalletSendTip(props: Props) {
const {
- uri,
- title,
- isPending,
- claimIsMine,
+ activeChannelClaim,
balance,
claim = {},
- instantTipEnabled,
- instantTipMax,
- sendSupport,
- closeModal,
+ claimIsMine,
fetchingChannels,
incognito,
- activeChannelClaim,
- doToast,
- isAuthenticated,
+ instantTipEnabled,
+ instantTipMax,
+ isPending,
+ title,
+ uri,
+ doHideModal,
+ doSendCashTip,
+ doSendTip,
} = props;
- /** REACT STATE **/
- 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 [isConfirming, setIsConfirming] = React.useState(false);
+ /** STATE **/
- // only allow certain creators to receive tips
- const [canReceiveFiatTip, setCanReceiveFiatTip] = React.useState(); // dont persist because it needs to be calc'd per creator
-
- // show things conditionally based on if a user has a card already
- const [hasCardSaved, setHasSavedCard] = usePersistedState('comment-support:hasCardSaved', false);
-
- // show the tip error on the frontend
+ const [tipAmount, setTipAmount] = usePersistedState('comment-support:customTip', 1.0);
+ const [isOnConfirmationPage, setConfirmationPage] = React.useState(false);
const [tipError, setTipError] = React.useState();
-
- // denote which tab to show on the frontend
const [activeTab, setActiveTab] = usePersistedState(TAB_BOOST);
+ const [disableSubmitButton, setDisableSubmitButton] = React.useState();
- // handle default active tab
- React.useEffect(() => {
- // force to boost tab if it's someone's own upload
- if (claimIsMine) {
- setActiveTab(TAB_BOOST);
- } else {
- // or set LBC tip as the default if none is set yet
- if (!activeTab || activeTab === 'undefined') {
- setActiveTab(TAB_LBC);
- }
- }
- }, []);
+ /** CONSTS **/
- // alphanumeric claim id
+ const claimTypeText = getClaimTypeText();
+ const isSupport = claimIsMine || activeTab === TAB_BOOST;
+ const titleText = claimIsMine
+ ? __('Boost Your %claimTypeText%', { claimTypeText })
+ : __('Boost This %claimTypeText%', { claimTypeText });
const { claim_id: claimId } = claim;
-
- // channel name used in url
- const { channelName } = parseURI(uri);
-
+ let channelName;
+ try {
+ ({ channelName } = parseURI(uri));
+ } catch (e) {}
const activeChannelName = activeChannelClaim && activeChannelClaim.name;
const activeChannelId = activeChannelClaim && activeChannelClaim.claim_id;
// setup variables for backend 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 && stripeEnvironment) {
- 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, stripeEnvironment]);
-
- // focus tip element if it exists
- React.useEffect(() => {
- const tipInputElement = document.getElementById('tip-input');
- if (tipInputElement) {
- tipInputElement.focus();
- }
- }, []);
-
- // check if user can receive tips
- React.useEffect(() => {
- if (channelClaimId && stripeEnvironment) {
- Lbryio.call(
- 'account',
- 'check',
- {
- channel_claim_id: channelClaimId,
- channel_name: tipChannelName,
- environment: stripeEnvironment,
- },
- 'post'
- )
- .then((accountCheckResponse) => {
- if (accountCheckResponse === true && canReceiveFiatTip !== true) {
- setCanReceiveFiatTip(true);
- } else {
- setCanReceiveFiatTip(false);
- }
- })
- .catch(function (error) {
- // console.log(error);
- });
- }
- }, [channelClaimId, stripeEnvironment]);
-
- // if user has no balance, used to show conditional frontend
- const noBalance = balance === 0;
-
- // the tip amount, based on if a preset or custom tip amount is being used
- const tipAmount = useCustomTip ? customTipAmount : presetTipAmount;
-
- // get type of claim (stream/channel/repost/collection) for display on frontend
- function getClaimTypeText() {
- 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 = getClaimTypeText();
+ const channelClaimId = claim.signing_channel ? claim.signing_channel.claim_id : claim.claim_id;
+ const tipChannelName = claim.signing_channel ? claim.signing_channel.name : claim.name;
// icon to use or explainer text to show per tab
- let iconToUse;
- let 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.');
+ let explainerText = '',
+ confirmLabel = '';
+ switch (activeTab) {
+ case TAB_BOOST:
+ explainerText = __(
+ 'This refundable boost will improve the discoverability of this %claimTypeText% while active. ',
+ { claimTypeText }
+ );
+ confirmLabel = __('Boosting');
+ break;
+ case TAB_FIAT:
+ explainerText = __('Show this channel your appreciation by sending a donation in USD. ');
+ confirmLabel = __('Tipping Fiat (USD)');
+ break;
+ case TAB_LBC:
+ explainerText = __('Show this channel your appreciation by sending a donation of Credits. ');
+ confirmLabel = __('Tipping Credit');
+ break;
}
- const isSupport = claimIsMine || activeTab === TAB_BOOST;
+ /** FUNCTIONS **/
- 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');
+ function getClaimTypeText() {
+ switch (claim.value_type) {
+ case 'stream':
+ return __('Content');
+ case 'channel':
+ return __('Channel');
+ case 'repost':
+ return __('Repost');
+ case 'collection':
+ return __('List');
+ default:
+ return __('Claim');
}
-
- // 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]);
+ }
// make call to the backend to send lbc or fiat
function sendSupportOrConfirm(instantTipMaxAmount = null) {
- // send a tip
- if (!isConfirming && (!instantTipMaxAmount || !instantTipEnabled || tipAmount > instantTipMaxAmount)) {
- setIsConfirming(true);
+ if (!isOnConfirmationPage && (!instantTipMaxAmount || !instantTipEnabled || tipAmount > instantTipMaxAmount)) {
+ setConfirmationPage(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;
- }
+ const supportParams: SupportParams = {
+ amount: tipAmount,
+ claim_id: claimId,
+ channel_id: activeChannelClaim && !incognito ? activeChannelClaim.claim_id : undefined,
+ };
// send tip/boost
- sendSupport(supportParams, isSupport);
- closeModal();
+ doSendTip(supportParams, isSupport);
+ doHideModal();
}
}
// 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;
+ if (!tipAmount || !claimId) return;
- // hit backend to send tip
- Lbryio.call(
- 'customer',
- 'tip',
- {
- // round to fix issues with floating point numbers
- amount: Math.round(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) {
- // show error message from Stripe if one exists (being passed from backend by Beamer's API currently)
- let displayError;
- if (error.message) {
- displayError = error.message;
- } else {
- displayError = DEFAULT_TIP_ERROR;
- }
-
- doToast({ message: displayError, isError: true });
- });
-
- closeModal();
- }
- // if it's a boost (?)
+ // 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 {
- sendSupportOrConfirm();
+ // Need to convert currency of instant purchase maximum before trying to send support
+ Lbryio.getExchangeRates().then(({ LBC_USD }) => sendSupportOrConfirm(instantTipMax.amount / LBC_USD));
}
- }
- }
-
- const countDecimals = function (value) {
- const text = value.toString();
- const index = text.indexOf('.');
- return text.length - index - 1;
- };
-
- function handleCustomPriceChange(event: SyntheticInputEvent<*>) {
- let tipAmountAsString = event.target.value;
-
- let tipAmount = parseFloat(tipAmountAsString);
-
- const howManyDecimals = countDecimals(tipAmountAsString);
-
- // fiat tip input
- if (activeTab === TAB_FIAT) {
- if (Number.isNaN(tipAmount)) {
- setCustomTipAmount('');
- }
-
- // allow maximum of two decimal places
- if (howManyDecimals > 2) {
- tipAmount = Math.floor(tipAmount * 100) / 100;
- }
-
- // remove decimals, and then get number of digits
- const howManyDigits = Math.trunc(tipAmount).toString().length;
-
- if (howManyDigits > 4 && tipAmount !== 1000) {
- setTipError('Amount cannot be over 1000 dollars');
- setCustomTipAmount(tipAmount);
- } else if (tipAmount > 1000) {
- setTipError('Amount cannot be over 1000 dollars');
- setCustomTipAmount(tipAmount);
+ // sending fiat tip
+ } else if (activeTab === TAB_FIAT) {
+ if (!isOnConfirmationPage) {
+ setConfirmationPage(true);
} else {
- setCustomTipAmount(tipAmount);
+ const tipParams: TipParams = { tipAmount, tipChannelName, channelClaimId };
+ const userParams: UserParams = { activeChannelName, activeChannelId };
+
+ // hit backend to send tip
+ doSendCashTip(tipParams, !activeChannelClaim || incognito, userParams, claimId, stripeEnvironment);
+ doHideModal();
}
- // LBC tip input
+ // if it's a boost (?)
} else {
- // TODO: this is a bit buggy, needs a touchup
- // if (howManyDecimals > 9) {
- // // only allows up to 8 decimal places
- // tipAmount = Number(tipAmount.toString().match(/^-?\d+(?:\.\d{0,8})?/)[0]);
- //
- // setTipError('Please only use up to 8 decimals');
- // }
- setCustomTipAmount(tipAmount);
+ sendSupportOrConfirm();
}
}
@@ -407,10 +178,7 @@ function WalletSendTip(props: Props) {
// 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;
+ return tipAmount !== tipAmount || tipAmount === 'NaN';
}
function convertToTwoDecimals(number) {
@@ -423,129 +191,65 @@ function WalletSendTip(props: Props) {
const displayAmount = !isNan(tipAmount) ? amountToShow : '';
// build button text based on tab
- 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 });
+ switch (activeTab) {
+ case TAB_BOOST:
+ return titleText;
+ case TAB_FIAT:
+ return __('Send a $%displayAmount% Tip', { displayAmount });
+ case TAB_LBC:
+ return __('Send a %displayAmount% Credit Tip', { displayAmount });
}
}
- // dont allow user to click send button
- function shouldDisableAmountSelector(amount) {
- return (
- (amount > balance && activeTab !== TAB_FIAT) || (activeTab === TAB_FIAT && (!hasCardSaved || !canReceiveFiatTip))
- );
- }
+ /** RENDER **/
- // showed on confirm page above amount
- 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');
- }
- }
+ const getTabButton = (tabIcon: string, tabLabel: string, tabName: string) => (
+