improve support/tip for 0 balance and own claims

This commit is contained in:
Sean Yesmunt 2020-06-09 18:09:24 -04:00
parent 019d1f9176
commit 01e0a2a5db
7 changed files with 212 additions and 191 deletions

View file

@ -47,8 +47,8 @@ class ChannelSelection extends React.PureComponent<Props, State> {
} }
componentDidUpdate() { componentDidUpdate() {
const { channels, fetchingChannels } = this.props; const { channels, fetchingChannels, hideAnon } = this.props;
if (!fetchingChannels && !channels) { if (!fetchingChannels && !channels && hideAnon) {
this.setState({ addingChannel: true }); this.setState({ addingChannel: true });
} }
} }

View file

@ -1,5 +1,5 @@
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { selectBalance } from 'lbry-redux'; import { selectBalance, selectMyChannelClaims } from 'lbry-redux';
import { doOpenModal } from 'redux/actions/app'; import { doOpenModal } from 'redux/actions/app';
import WalletSend from './view'; import WalletSend from './view';
import { withRouter } from 'react-router'; import { withRouter } from 'react-router';
@ -10,11 +10,7 @@ const perform = dispatch => ({
const select = state => ({ const select = state => ({
balance: selectBalance(state), balance: selectBalance(state),
channels: selectMyChannelClaims(state),
}); });
export default withRouter( export default withRouter(connect(select, perform)(WalletSend));
connect(
select,
perform
)(WalletSend)
);

View file

@ -6,6 +6,8 @@ import {
selectIsSendingSupport, selectIsSendingSupport,
selectBalance, selectBalance,
SETTINGS, SETTINGS,
selectMyChannelClaims,
makeSelectClaimIsMine,
} from 'lbry-redux'; } from 'lbry-redux';
import WalletSendTip from './view'; import WalletSendTip from './view';
import { doOpenModal, doHideModal } from 'redux/actions/app'; import { doOpenModal, doHideModal } from 'redux/actions/app';
@ -19,6 +21,8 @@ const select = (state, props) => ({
balance: selectBalance(state), balance: selectBalance(state),
instantTipEnabled: makeSelectClientSetting(SETTINGS.INSTANT_PURCHASE_ENABLED)(state), instantTipEnabled: makeSelectClientSetting(SETTINGS.INSTANT_PURCHASE_ENABLED)(state),
instantTipMax: makeSelectClientSetting(SETTINGS.INSTANT_PURCHASE_MAX)(state), instantTipMax: makeSelectClientSetting(SETTINGS.INSTANT_PURCHASE_MAX)(state),
channels: selectMyChannelClaims(state),
claimIsMine: makeSelectClaimIsMine(props.uri)(state),
}); });
const perform = dispatch => ({ const perform = dispatch => ({

View file

@ -1,6 +1,7 @@
// @flow // @flow
import * as MODALS from 'constants/modal_types'; import * as MODALS from 'constants/modal_types';
import * as ICONS from 'constants/icons'; import * as ICONS from 'constants/icons';
import * as PAGES from 'constants/pages';
import React from 'react'; import React from 'react';
import Button from 'component/button'; import Button from 'component/button';
import { FormField, Form } from 'component/common/form'; import { FormField, Form } from 'component/common/form';
@ -11,12 +12,15 @@ import I18nMessage from 'component/i18nMessage';
import { Lbryio } from 'lbryinc'; import { Lbryio } from 'lbryinc';
import Card from 'component/common/card'; import Card from 'component/common/card';
import classnames from 'classnames'; import classnames from 'classnames';
import SelectChannel from 'component/selectChannel';
import { parseURI } from 'lbry-redux';
import usePersistedState from 'effects/use-persisted-state';
const DEFAULT_TIP_AMOUNTS = [5, 10, 50]; const DEFAULT_TIP_AMOUNTS = [5, 25, 100, 1000];
type Props = { type Props = {
uri: string, uri: string,
// claimIsMine: boolean, claimIsMine: boolean,
title: string, title: string,
claim: StreamClaim, claim: StreamClaim,
isPending: boolean, isPending: boolean,
@ -27,6 +31,7 @@ type Props = {
openModal: (id: string, { tipAmount: number, claimId: string, isSupport: boolean }) => void, openModal: (id: string, { tipAmount: number, claimId: string, isSupport: boolean }) => void,
instantTipEnabled: boolean, instantTipEnabled: boolean,
instantTipMax: { amount: number, currency: string }, instantTipMax: { amount: number, currency: string },
channels: ?Array<ChannelClaim>,
}; };
function WalletSendTip(props: Props) { function WalletSendTip(props: Props) {
@ -34,7 +39,7 @@ function WalletSendTip(props: Props) {
uri, uri,
title, title,
isPending, isPending,
// claimIsMine, claimIsMine,
balance, balance,
claim = {}, claim = {},
instantTipEnabled, instantTipEnabled,
@ -42,13 +47,46 @@ function WalletSendTip(props: Props) {
openModal, openModal,
sendSupport, sendSupport,
closeModal, closeModal,
channels,
} = props; } = props;
const [tipAmount, setTipAmount] = React.useState(DEFAULT_TIP_AMOUNTS[0]); const [tipAmount, setTipAmount] = React.useState(DEFAULT_TIP_AMOUNTS[0]);
const [tipError, setTipError] = React.useState(); const [tipError, setTipError] = React.useState();
const [isSupport, setIsSupport] = React.useState(false); const [isSupport, setIsSupport] = React.useState(claimIsMine);
const [showMore, setShowMore] = React.useState(false); const [showMore, setShowMore] = React.useState(false);
const { claim_id: claimId } = claim;
const isMobile = useIsMobile(); const isMobile = useIsMobile();
const [selectedChannel, setSelectedChannel] = usePersistedState('comment-support:channel');
const { claim_id: claimId } = claim;
const channelStrings = channels && channels.map(channel => channel.permanent_url).join(',');
React.useEffect(() => {
if (!selectedChannel && channelStrings) {
const channels = channelStrings.split(',');
const newChannelUrl = channels[0];
const { claimName } = parseURI(newChannelUrl);
setSelectedChannel(claimName);
}
}, [channelStrings]);
React.useEffect(() => {
const regexp = RegExp(/^(\d*([.]\d{0,8})?)$/);
const validTipInput = regexp.test(String(tipAmount));
let tipError;
if (!tipAmount) {
tipError = __('Amount must be a number');
} else if (tipAmount <= 0) {
tipError = __('Amount must be a positive number');
} else if (tipAmount < MINIMUM_PUBLISH_BID) {
tipError = __('Amount must be higher');
} 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');
}
setTipError(tipError);
}, [tipAmount, balance, setTipError]);
function sendSupportOrConfirm(instantTipMaxAmount = null) { function sendSupportOrConfirm(instantTipMaxAmount = null) {
if (!isSupport && (!instantTipMaxAmount || !instantTipEnabled || tipAmount > instantTipMaxAmount)) { if (!isSupport && (!instantTipMaxAmount || !instantTipEnabled || tipAmount > instantTipMaxAmount)) {
@ -78,160 +116,130 @@ function WalletSendTip(props: Props) {
} }
function handleSupportPriceChange(event: SyntheticInputEvent<*>) { function handleSupportPriceChange(event: SyntheticInputEvent<*>) {
const regexp = RegExp(/^(\d*([.]\d{0,8})?)$/);
const validTipInput = regexp.test(event.target.value);
const tipAmount = parseFloat(event.target.value); const tipAmount = parseFloat(event.target.value);
let tipError;
if (!tipAmount) {
tipError = __('Amount must be a number');
} else if (tipAmount <= 0) {
tipError = __('Amount must be a positive number');
} else if (tipAmount < MINIMUM_PUBLISH_BID) {
tipError = __('Amount must be higher');
} 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');
}
setTipAmount(tipAmount); setTipAmount(tipAmount);
setTipError(tipError);
} }
// const label =
// tipAmount && tipAmount !== 0
// ? __(isSupport ? 'Support %amount% LBC' : 'Tip %amount% LBC', {
// amount: tipAmount.toFixed(8).replace(/\.?0+$/, ''),
// })
// : __('Amount');
return ( return (
<Form onSubmit={handleSubmit}> <Form onSubmit={handleSubmit}>
<Card {balance === 0 ? (
title={__('Support This Content')} <Card
subtitle={ title={__('Supporting Content Requires LBC')}
<React.Fragment> subtitle={__(
{__( 'With LBC, you can send tips to your favorite creators, or help boost their content for more people to see.'
'This will increase the overall bid amount for this content, which will boost its ability to be discovered while active.' )}
)}{' '} actions={
<Button label={__('Learn more')} button="link" href="https://lbry.com/faq/tipping" />. <div className="section__actions">
</React.Fragment> <Button icon={ICONS.BUY} button="primary" label={__('Buy LBC')} navigate={`/$/${PAGES.BUY}`} />
} <Button button="link" label={__('Nevermind')} onClick={closeModal} />
actions={
<>
<div className="section">
{DEFAULT_TIP_AMOUNTS.map(amount => (
<Button
key={amount}
button="alt"
className={classnames('button-toggle', { 'button-toggle--active': tipAmount === amount })}
label={`${amount} LBC`}
onClick={() => setTipAmount(amount)}
/>
))}
<Button
button="alt"
className={classnames('button-toggle', {
'button-toggle--active': !DEFAULT_TIP_AMOUNTS.includes(tipAmount),
})}
label={__('Custom')}
onClick={() => setShowMore(true)}
/>
</div> </div>
}
{showMore && ( />
) : (
<Card
title={claimIsMine ? __('Boost Your Content') : __('Support This Content')}
subtitle={
<React.Fragment>
{__(
'This will increase the overall bid amount for this content, which will boost its ability to be discovered while active.'
)}{' '}
<Button label={__('Learn more')} button="link" href="https://lbry.com/faq/tipping" />.
</React.Fragment>
}
actions={
<>
<div className="section"> <div className="section">
<FormField <SelectChannel
autoFocus label="Channel to show support as"
name="tip-input" channel={selectedChannel}
label={ onChannelChange={newChannel => setSelectedChannel(newChannel)}
<React.Fragment>
{'Custom support amount'}{' '}
{isMobile && (
<I18nMessage tokens={{ lbc_balance: <CreditAmount badge={false} amount={balance} /> }}>
(%lbc_balance% available)
</I18nMessage>
)}
</React.Fragment>
}
className="form-field--price-amount"
error={tipError}
min="0"
step="any"
type="number"
placeholder="1.23"
onChange={event => handleSupportPriceChange(event)}
/> />
</div> </div>
)}
<div className="section__actions"> <div className="section">
<Button {DEFAULT_TIP_AMOUNTS.map(amount => (
autoFocus <Button
icon={isSupport ? undefined : ICONS.SUPPORT} key={amount}
button="primary" disabled={amount > balance}
type="submit" button="alt"
label={ className={classnames('button-toggle', {
isSupport 'button-toggle--active': tipAmount === amount,
? __('Send Revokable Support') 'button-toggle--disabled': amount > balance,
: __('Send a %amount% Tip', { amount: tipAmount ? `${tipAmount} LBC` : '' }) })}
} label={`${amount} LBC`}
disabled={isPending || tipError || !tipAmount} onClick={() => setTipAmount(amount)}
/> />
<FormField ))}
name="toggle-is-support" <Button
type="checkbox" button="alt"
label={__('Make this support permanent')} className={classnames('button-toggle', {
checked={!isSupport} 'button-toggle--active': !DEFAULT_TIP_AMOUNTS.includes(tipAmount),
onChange={() => setIsSupport(!isSupport)} })}
/> label={__('Custom')}
</div> onClick={() => setShowMore(true)}
</> />
} </div>
/>
{showMore && (
<div className="section">
<FormField
autoFocus
name="tip-input"
label={
<React.Fragment>
{'Custom support amount'}{' '}
{isMobile && (
<I18nMessage tokens={{ lbc_balance: <CreditAmount badge={false} amount={balance} /> }}>
(%lbc_balance% available)
</I18nMessage>
)}
</React.Fragment>
}
className="form-field--price-amount"
error={tipError}
min="0"
step="any"
type="number"
placeholder="1.23"
onChange={event => handleSupportPriceChange(event)}
/>
</div>
)}
<div className="section__actions">
<Button
autoFocus
icon={isSupport ? undefined : ICONS.SUPPORT}
button="primary"
type="submit"
disabled={isPending || tipError || !tipAmount}
label={
isSupport
? __('Send Revokable Support')
: __('Send a %amount% Tip', { amount: tipAmount ? `${tipAmount} LBC` : '' })
}
/>
{!claimIsMine && (
<FormField
name="toggle-is-support"
type="checkbox"
label={__('Make this support permanent')}
checked={!isSupport}
onChange={() => setIsSupport(!isSupport)}
/>
)}
</div>
{DEFAULT_TIP_AMOUNTS.some(val => val > balance) && (
<div className="section">
<Button button="link" label={__('Buy More LBC')} />
</div>
)}
</>
}
/>
)}
</Form> </Form>
); );
} }
export default WalletSendTip; export default WalletSendTip;
// <Button
// button="primary"
// type="submit"
// label={__('Confirm')}
// disabled={isPending || tipError || !tipAmount}
// />;
// <div className="section__actions">
// <FormField
// autoFocus
// name="tip-input"
// label={
// <React.Fragment>
// {label}{' '}
// {isMobile && (
// <I18nMessage tokens={{ lbc_balance: <CreditAmount badge={false} amount={balance} /> }}>
// (%lbc_balance% available)
// </I18nMessage>
// )}
// </React.Fragment>
// }
// className="form-field--price-amount"
// error={tipError}
// min="0"
// step="any"
// type="number"
// placeholder="1.23"
// onChange={event => handleSupportPriceChange(event)}
// />
// <FormField
// name="toggle-is-support"
// type="checkbox"
// label={__('Send this as a tip instead')}
// checked={!isSupport}
// onChange={() => setIsSupport(!isSupport)}
// />
// </div>;

View file

@ -3,6 +3,7 @@ import React from 'react';
import { Modal } from 'modal/modal'; import { Modal } from 'modal/modal';
import Button from 'component/button'; import Button from 'component/button';
import * as PAGES from 'constants/pages'; import * as PAGES from 'constants/pages';
import Card from 'component/common/card';
type Props = { type Props = {
closeModal: () => void, closeModal: () => void,
@ -22,42 +23,48 @@ const ModalFirstSubscription = (props: Props) => {
location: { pathname }, location: { pathname },
} = props; } = props;
const title = __('You Followed Your First Channel!');
return ( return (
<Modal type="custom" isOpen contentLabel="Subscriptions 101" title={__('Subscriptions 101')}> <Modal type="card" isOpen contentLabel={title}>
<div className="section__subtitle"> <Card
<p>{__('Awesome! You just subscribed to your first channel.')}{' '} title={title}
{ user && user.primary_email ? ( subtitle={
<>
{__('Awesome! You just followed your first first channel.')}{' '}
{user && user.primary_email
? __('You will receive notifications related to new content.')
: __('Sign in with lbry.tv to receive notifications about new content.')}
</>
}
actions={
<div className="section__actions">
<Button button="primary" onClick={closeModal} label={__('Got it')} />
<React.Fragment> <React.Fragment>
{__('You will receive notifications related to new content.')} {user && user.primary_email ? (
<React.Fragment>
<Button
button="link"
href={`https://lbry.com/list/edit/${accessToken}`}
label={__('Update email preferences')}
/>
</React.Fragment>
) : (
<React.Fragment>
<Button
button="link"
onClick={() => {
closeModal();
history.push(`/$/${PAGES.AUTH}?redirect=${pathname}`);
}}
label={__('Sign in')}
/>
</React.Fragment>
)}
</React.Fragment> </React.Fragment>
) : ( </div>
<React.Fragment> }
{ __('Sign in with lbry.tv to receive notifications about new content.')} />
</React.Fragment>
)}
</p>
</div>
<div className="section__actions">
<Button button="primary" onClick={closeModal} label={__('Got it')} />
<React.Fragment>
{user && user.primary_email ? (
<React.Fragment>
<Button
button="link"
href={`https://lbry.com/list/edit/${accessToken}`}
label={__('Update email preferences')}
/>
</React.Fragment>
) : (
<React.Fragment>
<Button button="link" onClick={() => {
closeModal()
history.push(`/$/${PAGES.AUTH}?redirect=${pathname}`);
}} label={__('Sign in')} />
</React.Fragment>
)}
</React.Fragment>
</div>
</Modal> </Modal>
); );
}; };

View file

@ -142,10 +142,16 @@ svg + .button__label,
} }
} }
.button-toggle__label {
@extend label;
display: inline-block;
margin-top: -4px;
}
.button-group { .button-group {
display: flex; display: flex;
.button:first-child { .button:first-child:not(:only-child) {
border-top-right-radius: 0; border-top-right-radius: 0;
border-bottom-right-radius: 0; border-bottom-right-radius: 0;
border-right: 1px solid var(--color-button-border); border-right: 1px solid var(--color-button-border);

View file

@ -8,7 +8,7 @@
font-family: sans-serif; font-family: sans-serif;
display: block; display: block;
position: absolute; position: absolute;
z-index: 2; z-index: 10000;
font-size: var(--font-body); font-size: var(--font-body);
} }
@ -22,7 +22,7 @@
[data-reach-menu-item] { [data-reach-menu-item] {
display: block; display: block;
z-index: 2; z-index: 10000;
&:focus { &:focus {
box-shadow: none; box-shadow: none;