diff --git a/ui/component/commentCreate/view.jsx b/ui/component/commentCreate/view.jsx index 3d511dd69..238187cb4 100644 --- a/ui/component/commentCreate/view.jsx +++ b/ui/component/commentCreate/view.jsx @@ -131,6 +131,7 @@ export function CommentCreate(props: Props) { return; } + // if comment post didn't work, but tip was already made, try again to create comment if (commentFailure && tipAmount === successTip.tipAmount) { handleCreateComment(successTip.txid); return; @@ -147,6 +148,19 @@ export function CommentCreate(props: Props) { const activeChannelName = activeChannelClaim && activeChannelClaim.name; const activeChannelId = activeChannelClaim && activeChannelClaim.claim_id; + // 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; + } + setIsSubmitting(true); if (activeTab === TAB_LBC) { @@ -160,6 +174,14 @@ export function CommentCreate(props: Props) { setTimeout(() => { handleCreateComment(txid); }, 1500); + + doToast({ + message: __("You sent %tipAmount% LBRY Credits as a tip to %tipChannelName%, I'm sure they appreciate it!", { + tipAmount: tipAmount, // force show decimal places + tipChannelName, + }), + }); + setSuccessTip({ txid, tipAmount }); }, () => { @@ -168,27 +190,14 @@ export function CommentCreate(props: Props) { } ); } else { - // 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; const roundedAmount = Math.round(tipAmount * 100) / 100; Lbryio.call( 'customer', 'tip', - { - amount: 100 * roundedAmount, // convert from dollars to cents + { // round to deal with floating point precision + amount: Math.round(100 * roundedAmount), // convert from dollars to cents creator_channel_name: tipChannelName, // creator_channel_name creator_channel_claim_id: channelClaimId, tipper_channel_name: activeChannelName, diff --git a/ui/component/livestreamComments/view.jsx b/ui/component/livestreamComments/view.jsx index f4b073fef..2e3c73cdb 100644 --- a/ui/component/livestreamComments/view.jsx +++ b/ui/component/livestreamComments/view.jsx @@ -22,7 +22,6 @@ type Props = { fetchingComments: boolean, doSuperChatList: (string) => void, superChats: Array, - superChatsTotalAmount: number, myChannels: ?Array, }; @@ -38,38 +37,27 @@ export default function LivestreamComments(props: Props) { embed, doCommentSocketConnect, doCommentSocketDisconnect, - comments, + comments: commentsByChronologicalOrder, doCommentList, fetchingComments, doSuperChatList, - superChats, - superChatsTotalAmount, myChannels, + superChats: superChatsByTipAmount, } = props; + let superChatsFiatAmount, superChatsTotalAmount; + const commentsRef = React.createRef(); const [scrollBottom, setScrollBottom] = React.useState(true); const [viewMode, setViewMode] = React.useState(VIEW_MODE_CHAT); const [performedInitialScroll, setPerformedInitialScroll] = React.useState(false); const claimId = claim && claim.claim_id; - const commentsLength = comments && comments.length; - const commentsToDisplay = viewMode === VIEW_MODE_CHAT ? comments : superChats; + const commentsLength = commentsByChronologicalOrder && commentsByChronologicalOrder.length; + const commentsToDisplay = viewMode === VIEW_MODE_CHAT ? commentsByChronologicalOrder : superChatsByTipAmount; const discussionElement = document.querySelector('.livestream__comments'); const commentElement = document.querySelector('.livestream-comment'); - // todo: implement comment_list --mine in SDK so redux can grab with selectCommentIsMine - function isMyComment(channelId: string) { - if (myChannels != null && channelId != null) { - for (let i = 0; i < myChannels.length; i++) { - if (myChannels[i].claim_id === channelId) { - return true; - } - } - } - return false; - } - React.useEffect(() => { if (claimId) { doCommentList(uri, '', 1, 75); @@ -115,6 +103,51 @@ export default function LivestreamComments(props: Props) { } }, [commentsLength, discussionElement, handleScroll, performedInitialScroll, setPerformedInitialScroll]); + // sum total amounts for fiat tips and lbc tips + if (superChatsByTipAmount) { + let fiatAmount = 0; + let LBCAmount = 0; + for (const superChat of superChatsByTipAmount) { + if (superChat.is_fiat) { + fiatAmount = fiatAmount + superChat.support_amount; + } else { + LBCAmount = LBCAmount + superChat.support_amount; + } + } + + superChatsFiatAmount = fiatAmount; + superChatsTotalAmount = LBCAmount; + } + + let superChatsReversed; + // array of superchats organized by fiat or not first, then support amount + if (superChatsByTipAmount) { + const clonedSuperchats = JSON.parse(JSON.stringify(superChatsByTipAmount)); + + // sort by fiat first then by support amount + superChatsReversed = clonedSuperchats.sort(function(a, b) { + // if both are fiat, organize by support + if (a.is_fiat === b.is_fiat) { + return b.support_amount - a.support_amount; + // otherwise, if they are not both fiat, put the fiat transaction first + } else { + return (a.is_fiat === b.is_fiat) ? 0 : a.is_fiat ? -1 : 1; + } + }).reverse(); + } + + // todo: implement comment_list --mine in SDK so redux can grab with selectCommentIsMine + function isMyComment(channelId: string) { + if (myChannels != null && channelId != null) { + for (let i = 0; i < myChannels.length; i++) { + if (myChannels[i].claim_id === channelId) { + return true; + } + } + } + return false; + } + if (!claim) { return null; } @@ -130,41 +163,55 @@ export default function LivestreamComments(props: Props) {
{__('Live discussion')}
- {superChatsTotalAmount > 0 && ( + {(superChatsTotalAmount || 0) > 0 && (
+ + {/* the superchats in chronological order button */}
)}
<> - {fetchingComments && !comments && ( + {fetchingComments && !commentsByChronologicalOrder && (
)}
- {viewMode === VIEW_MODE_CHAT && superChatsTotalAmount > 0 && superChats && ( + {viewMode === VIEW_MODE_CHAT && superChatsByTipAmount && (superChatsTotalAmount || 0) > 0 && (
- {superChats.map((superChat: Comment) => ( + {superChatsByTipAmount.map((superChat: Comment) => (
@@ -187,9 +234,23 @@ export default function LivestreamComments(props: Props) {
)} - {!fetchingComments && comments.length > 0 ? ( + {/* top to bottom comment display */} + {!fetchingComments && commentsByChronologicalOrder.length > 0 ? (
- {commentsToDisplay.map((comment) => ( + {viewMode === VIEW_MODE_CHAT && commentsToDisplay.map((comment) => ( + + ))} + + {viewMode === VIEW_MODE_SUPER_CHAT && superChatsReversed && superChatsReversed.map((comment) => ( { - var tipInputElement = document.getElementById('tip-input'); + const tipInputElement = document.getElementById('tip-input'); if (tipInputElement) { tipInputElement.focus() } }, []); @@ -141,6 +141,8 @@ function WalletSendTip(props: Props) { .then((accountCheckResponse) => { if (accountCheckResponse === true && canReceiveFiatTip !== true) { setCanReceiveFiatTip(true); + } else { + setCanReceiveFiatTip(false); } }) .catch(function (error) { @@ -169,7 +171,8 @@ function WalletSendTip(props: Props) { } const claimTypeText = setClaimTypeText(); - let iconToUse, explainerText; + 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}); @@ -273,8 +276,8 @@ function WalletSendTip(props: Props) { Lbryio.call( 'customer', 'tip', - { - amount: 100 * tipAmount, // convert from dollars to cents + { // 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, @@ -295,7 +298,7 @@ function WalletSendTip(props: Props) { }); }) .catch(function(error) { - var displayError = 'Sorry, there was an error in processing your payment!'; + let displayError = 'Sorry, there was an error in processing your payment!'; if (error.message !== 'payment intent failed to confirm') { displayError = error.message; @@ -313,10 +316,53 @@ function WalletSendTip(props: Props) { } } - function handleCustomPriceChange(event: SyntheticInputEvent<*>) { - const tipAmount = parseFloat(event.target.value); + const countDecimals = function(value) { + const text = value.toString(); + const index = text.indexOf('.'); + return (text.length - index - 1); + }; - setCustomTipAmount(tipAmount); + 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); + } else { + setCustomTipAmount(tipAmount); + } + // LBC tip input + } 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); + } } function buildButtonText() { @@ -331,8 +377,14 @@ function WalletSendTip(props: Props) { return false; } + function convertToTwoDecimals(number) { + return (Math.round(number * 100) / 100).toFixed(2); + } + + const amountToShow = activeTab === TAB_FIAT ? convertToTwoDecimals(tipAmount) : tipAmount; + // if it's a valid number display it, otherwise do an empty string - const displayAmount = !isNan(tipAmount) ? tipAmount : ''; + const displayAmount = !isNan(tipAmount) ? amountToShow : ''; if (activeTab === TAB_BOOST) { return (claimIsMine ? __('Boost Your %claimTypeText%', {claimTypeText}) : __('Boost This %claimTypeText%', {claimTypeText})); @@ -362,240 +414,242 @@ function WalletSendTip(props: Props) { return (
{/* if there is no LBC balance, show user frontend to get credits */} - {noBalance ? ( - }}>Supporting content requires %lbc%} - subtitle={ - }}> - With %lbc%, you can send tips to your favorite creators, or help boost their content for more people to - see. - - } - actions={ -
-
+ )} + + {/* short explainer under the button */} +
+ {explainerText + ' '} + {/* {activeTab === TAB_FIAT && !hasCardSaved &&
- } - /> - ) : ( - // if there is lbc, the main tip/boost gui with the 3 tabs at the top - } - subtitle={ - - {!claimIsMine && ( + + } + actions={ + // confirmation modal, allow user to confirm or cancel transaction + isConfirming ? ( + <> +
- {/* tip LBC tab button */} +
{__('To --[the tip recipient]--')}
+
{channelName || title}
+
{__('From --[the tip sender]--')}
+
+ {activeChannelClaim && !incognito ? activeChannelClaim.name : __('Anonymous')} +
+
{setConfirmLabel()}
+
+ {activeTab === TAB_FIAT ?

$ {(Math.round(tipAmount * 100) / 100).toFixed(2)}

: } +
+
+
+
+
+ + // only show the prompt to earn more if its lbc or boost tab and no balance + // otherwise you can show the full prompt + ) : (!((activeTab === TAB_LBC || activeTab === TAB_BOOST) && noBalance) + ? <> +
+ +
+ + {activeTab === TAB_FIAT && !hasCardSaved && ( +

+

+ )} + + {/* section to pick tip/boost amount */} +
+ {DEFAULT_TIP_AMOUNTS.map((amount) => (
+ + {useCustomTip && ( +
+ + {__('Custom support amount')}{' '} + {activeTab !== TAB_FIAT ? ( + }} + > + (%lbc_balance% Credits available) + + ) : ( + 'in USD' + )} + + } + error={tipError} + min="0" + step="any" + type="number" + style={{ + width: activeTab === TAB_FIAT ? '99px' : '160px', }} - className={classnames('button-toggle', { 'button-toggle--active': activeTab === TAB_BOOST })} + placeholder="1.23" + value={customTipAmount} + onChange={(event) => handleCustomPriceChange(event)} />
)} - {/* short explainer under the button */} -
- {explainerText} - {/* {activeTab === TAB_FIAT && !hasCardSaved &&
- - } - actions={ - // confirmation modal, allow user to confirm or cancel transaction - isConfirming ? ( - <> -
-
-
{__('To --[the tip recipient]--')}
-
{channelName || title}
-
{__('From --[the tip sender]--')}
-
- {activeChannelClaim && !incognito ? activeChannelClaim.name : __('Anonymous')} + {activeTab !== TAB_FIAT ? ( + + ) : !canReceiveFiatTip ? ( +
{__('Only select creators can receive tips at this time')}
+ ) : ( +
{__('The payment will be made from your saved card')}
+ )} + + // if it's LBC and there is no balance, you can prompt to purchase LBC + : }}>Supporting content requires %lbc%} + subtitle={ + }}> + With %lbc%, you can send tips to your favorite creators, or help boost their content for more people to + see. + + } + actions={ +
+
-
{setConfirmLabel()}
-
- {activeTab === TAB_FIAT ?

$ {tipAmount}

: } -
-
-
-
-
- - ) : ( - <> -
- -
- - {activeTab === TAB_FIAT && !hasCardSaved && ( -

-

- )} - - {/* section to pick tip/boost amount */} -
- {DEFAULT_TIP_AMOUNTS.map((amount) => ( -
- - {useCustomTip && ( -
- - {__('Custom support amount')}{' '} - {activeTab !== TAB_FIAT ? ( - }} - > - (%lbc_balance% Credits available) - - ) : ( - 'in USD' - )} - - } - className="form-field--price-amount" - error={tipError} - min="0" - step="any" - type="number" - placeholder="1.23" - value={customTipAmount} - onChange={(event) => handleCustomPriceChange(event)} - /> -
- )} - - {/* send tip/boost button */} -
-
- {activeTab !== TAB_FIAT ? ( - - ) : !canReceiveFiatTip ? ( -
{__('Only select creators can receive tips at this time')}
- ) : ( -
{__('The payment will be made from your saved card')}
- )} - - ) - } - /> - )} + } + /> + ) + } + /> ); } diff --git a/ui/component/walletTipAmountSelector/view.jsx b/ui/component/walletTipAmountSelector/view.jsx index db5c4abd2..76a571da3 100644 --- a/ui/component/walletTipAmountSelector/view.jsx +++ b/ui/component/walletTipAmountSelector/view.jsx @@ -263,7 +263,6 @@ function WalletTipAmountSelector(props: Props) { //
// } - className="form-field--price-amount" error={tipError} min="0" step="any" diff --git a/ui/page/settingsStripeAccount/index.js b/ui/page/settingsStripeAccount/index.js index 633726701..fd03b4f49 100644 --- a/ui/page/settingsStripeAccount/index.js +++ b/ui/page/settingsStripeAccount/index.js @@ -2,12 +2,14 @@ import { connect } from 'react-redux'; import { withRouter } from 'react-router'; import StripeAccountConnection from './view'; import { selectUser } from 'redux/selectors/user'; +import { doToast } from 'redux/actions/notifications'; -// function that receives state parameter and returns object of functions that accept state const select = (state) => ({ user: selectUser(state), }); -// const perform = (dispatch) => ({}); +const perform = (dispatch) => ({ + doToast: (options) => dispatch(doToast(options)), +}); -export default withRouter(connect(select)(StripeAccountConnection)); +export default withRouter(connect(select, perform)(StripeAccountConnection)); diff --git a/ui/page/settingsStripeAccount/view.jsx b/ui/page/settingsStripeAccount/view.jsx index 4276ca306..2821b4423 100644 --- a/ui/page/settingsStripeAccount/view.jsx +++ b/ui/page/settingsStripeAccount/view.jsx @@ -31,6 +31,8 @@ if (isDev) { type Props = { source: string, user: User, + doOpenModal: (string, {}) => void, + doToast: ({ message: string }) => void, }; type State = { @@ -68,6 +70,8 @@ class StripeAccountConnection extends React.Component { componentDidMount() { const { user } = this.props; + let doToast = this.props.doToast; + // $FlowFixMe this.experimentalUiEnabled = user && user.experimental_ui; @@ -127,10 +131,8 @@ class StripeAccountConnection extends React.Component { ).then((accountListResponse: any) => { // TODO type this that.setState({ - accountTransactions: accountListResponse, + accountTransactions: accountListResponse.reverse(), }); - - console.log(accountListResponse); }); } @@ -165,6 +167,9 @@ class StripeAccountConnection extends React.Component { // get stripe link and set it on the frontend getAndSetAccountLink(true); } else { + // probably an error from stripe + const displayString = __('There was an error getting your account setup, please try again later'); + doToast({ message: displayString, isError: true }); // not an error from Beamer, throw it throw new Error(error); } @@ -296,7 +301,7 @@ class StripeAccountConnection extends React.Component { {accountTransactions && - accountTransactions.reverse().map((transaction) => ( + accountTransactions.map((transaction) => ( {moment(transaction.created_at).format('LLL')} diff --git a/ui/page/settingsStripeCard/view.jsx b/ui/page/settingsStripeCard/view.jsx index 7fd138038..ba8763143 100644 --- a/ui/page/settingsStripeCard/view.jsx +++ b/ui/page/settingsStripeCard/view.jsx @@ -19,6 +19,9 @@ if (STRIPE_PUBLIC_KEY.indexOf('pk_live') > -1) { stripeEnvironment = 'live'; } +const APIS_DOWN_ERROR_RESPONSE = __('There was an error from the server, please try again later'); +const CARD_SETUP_ERROR_RESPONSE = __('There was an error getting your card setup, please try again later'); + // eslint-disable-next-line flowtype/no-types-missing-file-annotation type Props = { disabled: boolean, @@ -57,8 +60,6 @@ class SettingsStripeCard extends React.Component { componentDidMount() { let that = this; - console.log(this.props); - let doToast = this.props.doToast; const script = document.createElement('script'); @@ -100,8 +101,6 @@ class SettingsStripeCard extends React.Component { let topOfDisplay = customer.email.split('@')[0]; let bottomOfDisplay = '@' + customer.email.split('@')[1]; - console.log(customerStatusResponse.Customer); - let cardDetails = { brand: card.brand, expiryYear: card.exp_year, @@ -133,8 +132,6 @@ class SettingsStripeCard extends React.Component { }, 'post' ).then((customerSetupResponse) => { - console.log(customerSetupResponse); - clientSecret = customerSetupResponse.client_secret; // instantiate stripe elements @@ -154,14 +151,10 @@ class SettingsStripeCard extends React.Component { that.setState({ customerTransactions: customerTransactionsResponse, }); - - console.log(customerTransactionsResponse); }); // if the status call fails, either an actual error or need to run setup first }) .catch(function (error) { - console.log(error); - // errorString passed from the API (with a 403 error) const errorString = 'user as customer is not setup yet'; @@ -181,18 +174,17 @@ class SettingsStripeCard extends React.Component { }, 'post' ).then((customerSetupResponse) => { - console.log(customerSetupResponse); - clientSecret = customerSetupResponse.client_secret; // instantiate stripe elements setupStripe(); }); + // 500 error from the backend being down } else if (error === 'internal_apis_down') { - var displayString = 'There was an error from the server, please let support know'; - doToast({ message: displayString, isError: true }); + doToast({ message: APIS_DOWN_ERROR_RESPONSE, isError: true }); } else { - console.log('Unseen before error'); + // probably an error from stripe + doToast({ message: CARD_SETUP_ERROR_RESPONSE, isError: true }); } }); }, 250); @@ -261,8 +253,6 @@ class SettingsStripeCard extends React.Component { }) .then(function (result) { if (result.error) { - console.log(result); - changeLoadingState(false); var displayError = document.getElementById('card-errors'); displayError.textContent = result.error.message; @@ -346,8 +336,6 @@ class SettingsStripeCard extends React.Component { }); }); - console.log(result); - changeLoadingState(false); }); };