new referrals

enable visiting /$/CODE or /$/invite/CHANNEL
enable visiting /ANY?=[CODE|CHANNEL] to set and claim
enable /$/invite selecting channels for referral codes
add ?r=CODE to share modal
enable setting referrer and claiming reward from rewards page
This commit is contained in:
jessop 2020-01-08 17:33:47 -05:00 committed by Sean Yesmunt
parent 92af68d912
commit b16688754b
15 changed files with 221 additions and 120 deletions

View file

@ -8,6 +8,8 @@ import {
doFetchAccessToken,
selectGetSyncErrorMessage,
selectUploadCount,
selectUnclaimedRewards,
doUserSetReferrer,
} from 'lbryinc';
import { doFetchTransactions, doFetchChannelListMine } from 'lbry-redux';
import { makeSelectClientSetting, selectLoadedLanguages, selectThemePath } from 'redux/selectors/settings';
@ -32,6 +34,7 @@ const select = state => ({
syncEnabled: makeSelectClientSetting(SETTINGS.ENABLE_SYNC)(state),
syncError: selectGetSyncErrorMessage(state),
uploadCount: selectUploadCount(state),
rewards: selectUnclaimedRewards(state),
});
const perform = dispatch => ({
@ -44,6 +47,7 @@ const perform = dispatch => ({
requestDownloadUpgrade: () => dispatch(doDownloadUpgradeRequested()),
checkSync: () => dispatch(doSyncWithPreferences()),
updatePreferences: () => dispatch(doGetAndPopulatePreferences()),
setReferrer: (referrer, doClaim) => dispatch(doUserSetReferrer(referrer, doClaim)),
});
export default hot(

View file

@ -14,6 +14,7 @@ import FloatingViewer from 'component/floatingViewer';
import { withRouter } from 'react-router';
import usePrevious from 'effects/use-previous';
import Nag from 'component/common/nag';
import { rewards as REWARDS } from 'lbryinc';
// @if TARGET='web'
import OpenInAppLink from 'component/openInAppLink';
import YoutubeWelcome from 'component/youtubeWelcome';
@ -32,7 +33,7 @@ type Props = {
languages: Array<string>,
theme: string,
user: ?{ id: string, has_verified_email: boolean, is_reward_approved: boolean },
location: { pathname: string, hash: string },
location: { pathname: string, hash: string, search: string },
history: { push: string => void },
fetchRewards: () => void,
fetchTransactions: (number, number) => void,
@ -52,6 +53,8 @@ type Props = {
balance: ?number,
accessToken: ?string,
syncError: ?string,
rewards: Array<Reward>,
setReferrer: (string, boolean) => void,
};
function App(props: Props) {
@ -76,6 +79,8 @@ function App(props: Props) {
languages,
setLanguage,
updatePreferences,
rewards,
setReferrer,
} = props;
const appRef = useRef();
@ -87,8 +92,13 @@ function App(props: Props) {
const previousUserId = usePrevious(userId);
const previousHasVerifiedEmail = usePrevious(hasVerifiedEmail);
const previousRewardApproved = usePrevious(isRewardApproved);
const { pathname, hash } = props.location;
const { pathname, hash, search } = props.location;
const showUpgradeButton = autoUpdateDownloaded || (process.platform === 'linux' && isUpgradeAvailable);
// referral claiming
const referredRewardAvailable = rewards && rewards.some(reward => reward.reward_type === REWARDS.TYPE_REFEREE);
const urlParams = new URLSearchParams(search);
const rawReferrerParam = urlParams.get('r');
const sanitizedReferrerParam = rawReferrerParam && rawReferrerParam.replace(':', '#');
let uri;
try {
@ -106,6 +116,14 @@ function App(props: Props) {
return () => window.removeEventListener('beforeunload', handleBeforeUnload);
}, [uploadCount]);
useEffect(() => {
if (referredRewardAvailable && sanitizedReferrerParam && isRewardApproved) {
setReferrer(sanitizedReferrerParam, true);
} else if (referredRewardAvailable && sanitizedReferrerParam) {
setReferrer(sanitizedReferrerParam, false);
}
}, [sanitizedReferrerParam, isRewardApproved, referredRewardAvailable]);
useEffect(() => {
ReactModal.setAppElement(appRef.current);
fetchAccessToken();

View file

@ -59,7 +59,7 @@ export function CommentCreate(props: Props) {
return (
<Form onSubmit={handleSubmit}>
<ChannelSection channel={channel} onChannelChange={handleChannelChange} />
<ChannelSection channel={channel} onChannelChange={handleChannelChange} includeAnonymous includeNew />
<FormField
disabled={channel === CHANNEL_NEW}
type="textarea"

View file

@ -4,19 +4,25 @@ import {
selectUserInviteNewIsPending,
selectUserInviteNewErrorMessage,
selectUserInviteReferralLink,
selectUserInviteReferralCode,
doUserInviteNew,
} from 'lbryinc';
import { selectMyChannelClaims, selectFetchingMyChannels, doFetchChannelListMine } from 'lbry-redux';
import InviteNew from './view';
const select = state => ({
errorMessage: selectUserInviteNewErrorMessage(state),
invitesRemaining: selectUserInvitesRemaining(state),
referralLink: selectUserInviteReferralLink(state),
referralCode: selectUserInviteReferralCode(state),
isPending: selectUserInviteNewIsPending(state),
channels: selectMyChannelClaims(state),
fetchingChannels: selectFetchingMyChannels(state),
});
const perform = dispatch => ({
inviteNew: email => dispatch(doUserInviteNew(email)),
fetchChannelListMine: () => dispatch(doFetchChannelListMine()),
});
export default connect(

View file

@ -1,88 +1,107 @@
// @flow
import React from 'react';
import React, { useEffect, useState } from 'react';
import Button from 'component/button';
import { Form, FormField } from 'component/common/form';
import CopyableText from 'component/copyableText';
import Card from 'component/common/card';
import { URL } from 'config';
import SelectChannel from 'component/selectChannel';
import I18nMessage from 'component/i18nMessage';
type FormState = {
email: string,
};
type Props = {
errorMessage: ?string,
inviteNew: string => void,
isPending: boolean,
referralLink: string,
referralCode: string,
channels: any,
fetchChannelListMine: () => void,
fetchingChannels: boolean,
};
class InviteNew extends React.PureComponent<Props, FormState> {
constructor() {
super();
function InviteNew(props: Props) {
const { inviteNew, fetchChannelListMine, errorMessage, isPending, referralCode, channels } = props;
const [email, setEmail] = useState('');
const [referralSource, setReferralSource] = useState(referralCode);
this.state = {
email: '',
};
(this: any).handleSubmit = this.handleSubmit.bind(this);
}
handleEmailChanged(event: any) {
this.setState({
email: event.target.value,
});
}
handleSubmit() {
const { email } = this.state;
this.props.inviteNew(email);
}
render() {
const { errorMessage, isPending, referralLink } = this.props;
return (
<Card
title={__('Invite a Friend')}
subtitle={__('When your friends start using LBRY, the network gets stronger!')}
actions={
<React.Fragment>
<Form onSubmit={this.handleSubmit}>
<FormField
type="text"
label="Email"
placeholder="youremail@example.org"
name="email"
value={this.state.email}
error={errorMessage}
inputButton={
<Button button="secondary" type="submit" label="Invite" disabled={isPending || !this.state.email} />
}
onChange={event => {
this.handleEmailChanged(event);
}}
/>
<CopyableText label={__('Or share this link with your friends')} copyable={referralLink} />
<p className="help">
<I18nMessage
tokens={{
rewards_link: <Button button="link" navigate="/$/rewards" label={__('rewards')} />,
referral_faq_link: <Button button="link" label={__('FAQ')} href="https://lbry.com/faq/referrals" />,
}}
>
Earn %rewards_link% for inviting your friends. Read our %referral_faq_link% to learn more about
referrals.
</I18nMessage>
</p>
</Form>
</React.Fragment>
}
/>
const topChannel =
channels &&
channels.reduce((top, channel) =>
(top && top.meta && top.meta.claims_in_channel) > channel.meta.claims_in_channel ? top : channel
);
const referralString =
channels && channels.length && referralSource !== referralCode
? getUrlFromName(referralSource, channels)
: referralSource;
const referral = `${URL}/$/invite/${referralString}`;
useEffect(() => {
// check emailverified and is_web?
fetchChannelListMine();
}, []);
useEffect(() => {
if (topChannel) {
setReferralSource(topChannel.name);
}
}, [topChannel]);
function handleEmailChanged(event: any) {
setEmail(event.target.value);
}
function handleSubmit() {
inviteNew(email);
}
function getUrlFromName(name, channels) {
const claim = channels.find(channel => channel.name === name);
return claim ? claim.permanent_url.replace('lbry://', '') : name;
}
return (
<Card
title={__('Invite a Friend')}
subtitle={__('When your friends start using LBRY, the network gets stronger!')}
actions={
<React.Fragment>
<Form onSubmit={handleSubmit}>
<SelectChannel
channel={referralSource}
onChannelChange={channel => setReferralSource(channel)}
label={'Code or Channel'}
injected={[referralCode]}
/>
<CopyableText label={__('Or share this link with your friends')} copyable={referral} />
<FormField
type="text"
label="Email"
placeholder="youremail@example.org"
name="email"
value={email}
error={errorMessage}
inputButton={<Button button="secondary" type="submit" label="Invite" disabled={isPending || !email} />}
onChange={event => {
handleEmailChanged(event);
}}
/>
<p className="help">
<I18nMessage
tokens={{
rewards_link: <Button button="link" navigate="/$/rewards" label={__('rewards')} />,
referral_faq_link: <Button button="link" label={__('FAQ')} href="https://lbry.com/faq/referrals" />,
}}
>
Earn %rewards_link% for inviting your friends. Read our %referral_faq_link% to learn more about
referrals.
</I18nMessage>
</p>
</Form>
</React.Fragment>
}
/>
);
}
export default InviteNew;

View file

@ -8,7 +8,9 @@ import {
selectSetReferrerError,
rewards as REWARDS,
} from 'lbryinc';
import { doChannelSubscribe } from 'redux/actions/subscriptions';
import Invited from './view';
import { withRouter } from 'react-router';
const select = state => ({
user: selectUser(state),
@ -20,9 +22,12 @@ const perform = dispatch => ({
claimReward: () => dispatch(doClaimRewardType(REWARDS.TYPE_REFEREE)),
fetchUser: () => dispatch(doUserFetch()),
setReferrer: referrer => dispatch(doUserSetReferrer(referrer)),
channelSubscribe: uri => dispatch(doChannelSubscribe(uri)),
});
export default connect(
select,
perform
)(Invited);
export default withRouter(
connect(
select,
perform
)(Invited)
);

View file

@ -14,16 +14,26 @@ type Props = {
setReferrer: string => void,
setReferrerPending: boolean,
setReferrerError: string,
channelSubscribe: (sub: Subscription) => void,
history: { push: string => void },
};
function Invited(props: Props) {
const { user, fetchUser, claimReward, setReferrer, setReferrerPending, setReferrerError } = props;
const {
user,
fetchUser,
claimReward,
setReferrer,
setReferrerPending,
setReferrerError,
channelSubscribe,
history,
} = props;
// useParams requires react-router-dom ^v5.1.0
const { referrer } = useParams();
const refUri = 'lbry://' + referrer.replace(':', '#');
const referrerIsChannel = parseURI(refUri).isChannel;
// const hasReferrer = user && user['invited_by_id'] ? true : false;
const rewardsApproved = user && user.is_reward_approved;
const hasVerifiedEmail = user && user.has_verified_email;
@ -32,32 +42,40 @@ function Invited(props: Props) {
}, []);
useEffect(() => {
if (setReferrerPending && hasVerifiedEmail) {
if (!setReferrerPending && hasVerifiedEmail) {
claimReward();
}
}, [setReferrerPending, hasVerifiedEmail]);
//follow
// !signed in or approved
useEffect(() => {
if (referrer) {
setReferrer(referrer.replace(':', '#'));
}
}, [referrer, hasVerifiedEmail]);
}, [referrer]);
function handleDone() {
if (hasVerifiedEmail && referrerIsChannel) {
channelSubscribe({
channelName: parseURI(refUri).claimName,
uri: refUri,
});
}
history.push(`/$/${PAGES.DISCOVER}`);
}
if (setReferrerError) {
return (
<Card
title={__(`Welcome!`)}
subtitle={__(`You can visit your referrer, or discover new stuff.`)}
subtitle={__(
`Something went wrong with this referral link. Take a look around. You can get earn rewards by setting your referrer later.`
)}
actions={
<>
<p className="error-text">{__('Not a valid referral')}</p>
<p className="error-text">{setReferrerError}</p>
<div className="card__actions">
<Button button="primary" label={__('Discover')} navigate={`/$/${PAGES.DISCOVER}`} />
<Button button="primary" label={__('Explore')} onClick={handleDone} />
</div>
</>
}
@ -83,7 +101,7 @@ function Invited(props: Props) {
label={hasVerifiedEmail ? __('Verify') : __('Sign in')}
navigate={`/$/${PAGES.AUTH}?redirect=/$/${PAGES.INVITE}/${referrer}`}
/>
<Button button="primary" label={__('Not now')} navigate={`/$/${PAGES.DISCOVER}`} />
<Button button="primary" label={__('Skip')} onClick={handleDone} />
</div>
</>
}
@ -103,8 +121,7 @@ function Invited(props: Props) {
</div>
)}
<div className="card__actions">
<Button button="primary" label={__('Referrer')} navigate={`/$/${PAGES.DISCOVER}`} />
<Button button="primary" label={__('Discover')} navigate={`/$/${PAGES.DISCOVER}`} />
<Button button="primary" label={__('Done!')} onClick={handleDone} />
</div>
</>
}

View file

@ -165,7 +165,12 @@ function PublishForm(props: Props) {
<Card
actions={
<React.Fragment>
<SelectChannel channel={channel} onChannelChange={channel => updatePublishForm({ channel })} />
<SelectChannel
channel={channel}
onChannelChange={channel => updatePublishForm({ channel })}
includeAnonymous
includeNew
/>
<p className="help">
{__('This is a username or handle that your content can be found under.')}{' '}
{__('Ex. @Marvel, @TheBeatles, @BooksByJoe')}

View file

@ -2,13 +2,17 @@ import * as MODALS from 'constants/modal_types';
import { connect } from 'react-redux';
import { doOpenModal } from 'redux/actions/app';
import RewardTile from './view';
import { selectUser } from 'lbryinc';
const select = state => ({
user: selectUser(state),
});
const perform = dispatch => ({
openRewardCodeModal: () => dispatch(doOpenModal(MODALS.REWARD_GENERATED_CODE)),
openSetReferrerModal: () => dispatch(doOpenModal(MODALS.SET_REFERRER)),
});
export default connect(
null,
select,
perform
)(RewardTile);

View file

@ -9,6 +9,7 @@ import { rewards } from 'lbryinc';
type Props = {
openRewardCodeModal: () => void,
openSetReferrerModal: () => void,
reward: {
id: string,
reward_title: string,
@ -19,10 +20,12 @@ type Props = {
reward_description: string,
reward_type: string,
},
user: User,
};
const RewardTile = (props: Props) => {
const { reward, openRewardCodeModal } = props;
const { reward, openRewardCodeModal, openSetReferrerModal, user } = props;
const referrerSet = user && user.invited_by_id;
const claimed = !!reward.transaction_id;
return (
@ -38,7 +41,11 @@ const RewardTile = (props: Props) => {
<Button button="primary" navigate="/$/invite" label={__('Go To Invites')} />
)}
{reward.reward_type === rewards.TYPE_REFEREE && (
<Button button="primary" onClick={openRewardCodeModal} label={__('Set Referree')} />
<Button
button="primary"
onClick={openSetReferrerModal}
label={referrerSet ? __('Change Referrer') : __('Set Referrer')}
/>
)}
{reward.reward_type !== rewards.TYPE_REFERRAL &&
(claimed ? (

View file

@ -15,6 +15,10 @@ type Props = {
fetchChannelListMine: () => void,
fetchingChannels: boolean,
emailVerified: boolean,
includeAnonymous?: boolean,
includeNew?: boolean,
label?: string,
injected?: Array<string>,
};
type State = {
@ -69,7 +73,7 @@ class ChannelSection extends React.PureComponent<Props, State> {
render() {
const channel = this.state.addingChannel ? 'new' : this.props.channel;
const { fetchingChannels, channels = [] } = this.props;
const { fetchingChannels, channels = [], includeAnonymous, includeNew, label, injected = [] } = this.props;
const { addingChannel } = this.state;
return (
@ -81,19 +85,25 @@ class ChannelSection extends React.PureComponent<Props, State> {
<div className="section">
<FormField
name="channel"
label={__('Channel')}
label={label || __('Channel')}
type="select"
onChange={this.handleChannelChange}
value={channel}
>
<option value={CHANNEL_ANONYMOUS}>{__('Anonymous')}</option>
{includeAnonymous && <option value={CHANNEL_ANONYMOUS}>{__('Anonymous')}</option>}
{channels &&
channels.map(({ name, claim_id: claimId }) => (
<option key={claimId} value={name}>
{name}
</option>
))}
<option value={CHANNEL_NEW}>{__('New channel...')}</option>
{injected &&
injected.map(item => (
<option key={item} value={item}>
{item}
</option>
))}
{includeNew && <option value={CHANNEL_NEW}>{__('New channel...')}</option>}
</FormField>
</div>
{addingChannel && (

View file

@ -1,9 +1,12 @@
import { connect } from 'react-redux';
import { makeSelectClaimForUri } from 'lbry-redux';
import SocialShare from './view';
import { selectUserInviteReferralCode, selectUser } from 'lbryinc';
const select = (state, props) => ({
claim: makeSelectClaimForUri(props.uri)(state),
referralCode: selectUserInviteReferralCode(state),
user: selectUser(state),
});
export default connect(select)(SocialShare);

View file

@ -9,6 +9,8 @@ type Props = {
claim: Claim,
webShareable: boolean,
isChannel: boolean,
referralCode: string,
user: any,
};
class SocialShare extends React.PureComponent<Props> {
@ -25,14 +27,16 @@ class SocialShare extends React.PureComponent<Props> {
input: ?HTMLInputElement;
render() {
const { claim, isChannel } = this.props;
const { claim, isChannel, referralCode, user } = this.props;
const { canonical_url: canonicalUrl, permanent_url: permanentUrl } = claim;
const { webShareable } = this.props;
const rewardsApproved = user && user.is_reward_approved;
const OPEN_URL = 'https://open.lbry.com/';
const lbryUrl = canonicalUrl ? canonicalUrl.split('lbry://')[1] : permanentUrl.split('lbry://')[1];
const lbryWebUrl = lbryUrl.replace(/#/g, ':');
const encodedLbryURL: string = `${OPEN_URL}${encodeURIComponent(lbryWebUrl)}`;
const lbryURL: string = `${OPEN_URL}${lbryWebUrl}`;
const referralParam: string = referralCode && rewardsApproved ? `?r=${referralCode}` : '';
const lbryURL: string = `${OPEN_URL}${lbryWebUrl}${referralParam}`;
const shareOnFb = __('Share on Facebook');
const shareOnTwitter = __('Share On Twitter');

View file

@ -1,13 +1,16 @@
import { connect } from 'react-redux';
import { doHideModal } from 'redux/actions/app';
import { doUserSetReferrer } from 'lbryinc';
import { doUserSetReferrer, selectSetReferrerError, selectSetReferrerPending } from 'lbryinc';
import ModalSetReferrer from './view';
const select = state => ({});
const select = state => ({
setReferrerPending: selectSetReferrerPending(state),
setReferrerError: selectSetReferrerError(state),
});
const perform = dispatch => ({
closeModal: () => dispatch(doHideModal()),
submitReferrer: referrer => dispatch(doUserSetReferrer(referrer)),
setReferrer: (referrer, doClaim) => dispatch(doUserSetReferrer(referrer, doClaim)),
});
export default connect(

View file

@ -8,11 +8,13 @@ type Props = {
closeModal: () => void,
error: ?string,
rewardIsPending: boolean,
submitRewardCode: string => void,
setReferrer: (string, boolean) => void,
setReferrerPending: boolean,
setReferrerError?: string,
};
type State = {
rewardCode: string,
referrer: string,
};
class ModalSetReferrer extends React.PureComponent<Props, State> {
@ -20,21 +22,21 @@ class ModalSetReferrer extends React.PureComponent<Props, State> {
super();
this.state = {
rewardCode: '',
referrer: '',
};
(this: any).handleSubmit = this.handleSubmit.bind(this);
}
handleSubmit() {
const { rewardCode } = this.state;
const { submitRewardCode } = this.props;
submitRewardCode(rewardCode);
const { referrer } = this.state;
const { setReferrer } = this.props;
setReferrer(referrer, true);
}
render() {
const { closeModal, rewardIsPending, error } = this.props;
const { rewardCode } = this.state;
const { closeModal, rewardIsPending } = this.props;
const { referrer } = this.state;
return (
<Modal
@ -47,29 +49,23 @@ class ModalSetReferrer extends React.PureComponent<Props, State> {
<Form onSubmit={this.handleSubmit}>
<p>
{__('Tell us who referred you and get a reward!')}
{'. '}
<Button button="link" href="https://lbry.com/faq/rewards#reward-code" label={__('Fake Help Link')} />.
<Button button="link" href="https://lbry.com/faq/referrals" label={__('?')} />.
</p>
<FormField
autoFocus
type="text"
name="referrer-code"
inputButton={
<Button
button="primary"
type="submit"
disabled={!rewardCode || rewardIsPending}
label={rewardIsPending ? __('Redeeming') : __('Redeem')}
/>
<Button button="primary" type="submit" disabled={!referrer || rewardIsPending} label={__('Set')} />
}
label={__('Code')}
placeholder="0123abc"
error={error}
value={rewardCode}
onChange={e => this.setState({ rewardCode: e.target.value })}
value={referrer}
onChange={e => this.setState({ referrer: e.target.value })}
/>
</Form>
<div className="card__actions">
<Button button="primary" label={__('Done')} onClick={closeModal} />
<Button button="link" label={__('Cancel')} onClick={closeModal} />
</div>
</Modal>