rewards refactor to support types, add list

This commit is contained in:
Jeremy Kauffman 2017-08-18 23:08:01 -04:00
parent 31fb723d87
commit 2b8296fcc3
18 changed files with 240 additions and 131 deletions

View file

@ -2,7 +2,7 @@ import * as types from "constants/action_types";
import * as modals from "constants/modal_types"; import * as modals from "constants/modal_types";
import lbryio from "lbryio"; import lbryio from "lbryio";
import rewards from "rewards"; import rewards from "rewards";
import { selectRewardsByType } from "selectors/rewards"; import { selectUnclaimedRewardsByType } from "selectors/rewards";
export function doRewardList() { export function doRewardList() {
return function(dispatch, getState) { return function(dispatch, getState) {
@ -13,7 +13,7 @@ export function doRewardList() {
}); });
lbryio lbryio
.call("reward", "list", {}) .call("reward", "list", { multiple_rewards_per_type: true })
.then(userRewards => { .then(userRewards => {
dispatch({ dispatch({
type: types.FETCH_REWARDS_COMPLETED, type: types.FETCH_REWARDS_COMPLETED,
@ -31,17 +31,9 @@ export function doRewardList() {
export function doClaimRewardType(rewardType) { export function doClaimRewardType(rewardType) {
return function(dispatch, getState) { return function(dispatch, getState) {
const rewardsByType = selectRewardsByType(getState()), const rewardsByType = selectUnclaimedRewardsByType(getState()),
reward = rewardsByType[rewardType]; reward = rewardsByType[rewardType];
if (reward) {
dispatch(doClaimReward(reward));
}
};
}
export function doClaimReward(reward, saveError = false) {
return function(dispatch, getState) {
if (reward.transaction_id) { if (reward.transaction_id) {
//already claimed, do nothing //already claimed, do nothing
return; return;
@ -72,7 +64,7 @@ export function doClaimReward(reward, saveError = false) {
type: types.CLAIM_REWARD_FAILURE, type: types.CLAIM_REWARD_FAILURE,
data: { data: {
reward, reward,
error: saveError ? error : null, error: error ? error : null,
}, },
}); });
}; };
@ -87,26 +79,18 @@ export function doClaimEligiblePurchaseRewards() {
return; return;
} }
const rewardsByType = selectRewardsByType(getState()); const rewardsByType = selectUnclaimedRewardsByType(getState());
let types = {}; if (rewardsByType[rewards.TYPE_FIRST_STREAM]) {
dispatch(doClaimRewardType(rewards.TYPE_FIRST_STREAM));
types[rewards.TYPE_FIRST_STREAM] = false; } else {
types[rewards.TYPE_FEATURED_DOWNLOAD] = false; [
types[rewards.TYPE_MANY_DOWNLOADS] = false; rewards.TYPE_MANY_DOWNLOADS,
Object.values(rewardsByType).forEach(reward => { rewards.TYPE_FEATURED_DOWNLOAD,
if (types[reward.reward_type] === false && reward.transaction_id) { ].forEach(type => {
types[reward.reward_type] = true; dispatch(doClaimRewardType(type));
}
}); });
let unclaimedType = Object.keys(types).find(type => {
return types[type] === false && type !== rewards.TYPE_FEATURED_DOWNLOAD; //handled below
});
if (unclaimedType) {
dispatch(doClaimRewardType(unclaimedType));
} }
dispatch(doClaimRewardType(rewards.TYPE_FEATURED_DOWNLOAD));
}; };
} }

View file

@ -0,0 +1,5 @@
import React from "react";
import { connect } from "react-redux";
import Link from "./view";
export default connect(null, null)(Link);

View file

@ -0,0 +1,14 @@
import React from "react";
import Link from "component/link";
const LinkTransaction = props => {
const { id } = props;
const linkProps = Object.assign({}, props);
linkProps.href = "https://explorer.lbry.io/#!/transaction/" + id;
linkProps.label = id.substr(0, 7);
return <Link {...linkProps} />;
};
export default LinkTransaction;

View file

@ -6,7 +6,7 @@ import {
makeSelectIsRewardClaimPending, makeSelectIsRewardClaimPending,
} from "selectors/rewards"; } from "selectors/rewards";
import { doNavigate } from "actions/app"; import { doNavigate } from "actions/app";
import { doClaimReward, doClaimRewardClearError } from "actions/rewards"; import { doClaimRewardType, doClaimRewardClearError } from "actions/rewards";
import RewardLink from "./view"; import RewardLink from "./view";
const makeSelect = () => { const makeSelect = () => {
@ -24,7 +24,7 @@ const makeSelect = () => {
}; };
const perform = dispatch => ({ const perform = dispatch => ({
claimReward: reward => dispatch(doClaimReward(reward, true)), claimReward: reward => dispatch(doClaimRewardType(reward.reward_type, true)),
clearError: reward => dispatch(doClaimRewardClearError(reward)), clearError: reward => dispatch(doClaimRewardClearError(reward)),
navigate: path => dispatch(doNavigate(path)), navigate: path => dispatch(doNavigate(path)),
}); });

View file

@ -0,0 +1,10 @@
import React from "react";
import { connect } from "react-redux";
import { selectClaimedRewards } from "selectors/rewards";
import RewardListClaimed from "./view";
const select = state => ({
rewards: selectClaimedRewards(state),
});
export default connect(select, null)(RewardListClaimed);

View file

@ -0,0 +1,44 @@
import React from "react";
import LinkTransaction from "component/linkTransaction";
const RewardListClaimed = props => {
const { rewards } = props;
if (!rewards || !rewards.length) {
return null;
}
return (
<section className="card">
<div className="card__title-identity"><h3>Claimed Rewards</h3></div>
<div className="card__content">
<table className="table-standard table-stretch">
<thead>
<tr>
<th>{__("Title")}</th>
<th>{__("Amount")}</th>
<th>{__("Transaction")}</th>
<th>{__("Date")}</th>
</tr>
</thead>
<tbody>
{rewards.map(reward => {
return (
<tr key={reward.id}>
<td>{reward.reward_title}</td>
<td>{reward.reward_amount}</td>
<td><LinkTransaction id={reward.transaction_id} /></td>
<td>
{reward.created_at.replace("Z", " ").replace("T", " ")}
</td>
</tr>
);
})}
</tbody>
</table>
</div>
</section>
);
};
export default RewardListClaimed;

View file

@ -0,0 +1,5 @@
import React from "react";
import { connect } from "react-redux";
import RewardTile from "./view";
export default connect(null, null)(RewardTile);

View file

@ -0,0 +1,28 @@
import React from "react";
import { CreditAmount, Icon } from "component/common";
import RewardLink from "component/rewardLink";
const RewardTile = props => {
const { reward } = props;
const claimed = !!reward.transaction_id;
return (
<section className="card">
<div className="card__inner">
<div className="card__title-primary">
<CreditAmount amount={reward.reward_amount} />
<h3>{reward.reward_title}</h3>
</div>
<div className="card__actions">
{claimed
? <span><Icon icon="icon-check" /> {__("Reward claimed.")}</span>
: <RewardLink reward_type={reward.reward_type} />}
</div>
<div className="card__content">{reward.reward_description}</div>
</div>
</section>
);
};
export default RewardTile;

View file

@ -13,6 +13,7 @@ import DeveloperPage from "page/developer.js";
import RewardsPage from "page/rewards"; import RewardsPage from "page/rewards";
import FileListDownloaded from "page/fileListDownloaded"; import FileListDownloaded from "page/fileListDownloaded";
import FileListPublished from "page/fileListPublished"; import FileListPublished from "page/fileListPublished";
import TransactionHistoryPage from "page/transactionHistory";
import ChannelPage from "page/channel"; import ChannelPage from "page/channel";
import SearchPage from "page/search"; import SearchPage from "page/search";
import AuthPage from "page/auth"; import AuthPage from "page/auth";
@ -36,6 +37,7 @@ const Router = props => {
discover: <DiscoverPage params={params} />, discover: <DiscoverPage params={params} />,
downloaded: <FileListDownloaded params={params} />, downloaded: <FileListDownloaded params={params} />,
help: <HelpPage params={params} />, help: <HelpPage params={params} />,
history: <TransactionHistoryPage params={params} />,
invite: <InvitePage params={params} />, invite: <InvitePage params={params} />,
publish: <PublishPage params={params} />, publish: <PublishPage params={params} />,
published: <FileListPublished params={params} />, published: <FileListPublished params={params} />,

View file

@ -1,5 +1,6 @@
import React from "react"; import React from "react";
import { Address, BusyMessage, CreditAmount } from "component/common"; import { BusyMessage } from "component/common";
import LinkTransaction from "component/linkTransaction";
class TransactionList extends React.PureComponent { class TransactionList extends React.PureComponent {
componentWillMount() { componentWillMount() {
@ -26,12 +27,7 @@ class TransactionList extends React.PureComponent {
: <span className="empty">{__("(Transaction pending)")}</span>} : <span className="empty">{__("(Transaction pending)")}</span>}
</td> </td>
<td> <td>
<a <LinkTransaction id={item.id} />
className="button-text"
href={"https://explorer.lbry.io/#!/transaction/" + item.id}
>
{item.id.substr(0, 7)}
</a>
</td> </td>
</tr> </tr>
); );

View file

@ -7,7 +7,7 @@ import { selectUserIsRewardApproved } from "selectors/user";
import { import {
makeSelectHasClaimedReward, makeSelectHasClaimedReward,
makeSelectRewardByType, makeSelectRewardByType,
selectTotalRewardValue, selectUnclaimedRewardValue,
} from "selectors/rewards"; } from "selectors/rewards";
import * as settings from "constants/settings"; import * as settings from "constants/settings";
import ModalCreditIntro from "./view"; import ModalCreditIntro from "./view";
@ -19,7 +19,7 @@ const select = (state, props) => {
return { return {
isRewardApproved: selectUserIsRewardApproved(state), isRewardApproved: selectUserIsRewardApproved(state),
reward: selectReward(state, { reward_type: rewards.TYPE_NEW_USER }), reward: selectReward(state, { reward_type: rewards.TYPE_NEW_USER }),
totalRewardValue: selectTotalRewardValue(state), totalRewardValue: selectUnclaimedRewardValue(state),
}; };
}; };

View file

@ -1,23 +1,18 @@
import React from "react"; import React from "react";
import { connect } from "react-redux"; import { connect } from "react-redux";
import { import {
makeSelectRewardByType,
selectFetchingRewards, selectFetchingRewards,
selectRewards, selectUnclaimedRewards,
} from "selectors/rewards"; } from "selectors/rewards";
import { selectUser } from "selectors/user"; import { selectUser } from "selectors/user";
import { doAuthNavigate, doNavigate } from "actions/app"; import { doAuthNavigate, doNavigate } from "actions/app";
import { doRewardList } from "actions/rewards"; import { doRewardList } from "actions/rewards";
import rewards from "rewards";
import RewardsPage from "./view"; import RewardsPage from "./view";
const select = (state, props) => { const select = (state, props) => {
const selectReward = makeSelectRewardByType();
return { return {
fetching: selectFetchingRewards(state), fetching: selectFetchingRewards(state),
rewards: selectRewards(state), rewards: selectUnclaimedRewards(state),
newUserReward: selectReward(state, { reward_type: rewards.TYPE_NEW_USER }),
user: selectUser(state), user: selectUser(state),
}; };
}; };

View file

@ -1,31 +1,9 @@
import React from "react"; import React from "react";
import { BusyMessage, CreditAmount, Icon } from "component/common"; import { BusyMessage } from "component/common";
import RewardListClaimed from "component/rewardListClaimed";
import RewardTile from "component/rewardTile";
import SubHeader from "component/subHeader"; import SubHeader from "component/subHeader";
import Link from "component/link"; import Link from "component/link";
import RewardLink from "component/rewardLink";
const RewardTile = props => {
const { reward } = props;
const claimed = !!reward.transaction_id;
return (
<section className="card">
<div className="card__inner">
<div className="card__title-primary">
<CreditAmount amount={reward.reward_amount} />
<h3>{reward.reward_title}</h3>
</div>
<div className="card__actions">
{claimed
? <span><Icon icon="icon-check" /> {__("Reward claimed.")}</span>
: <RewardLink reward_type={reward.reward_type} />}
</div>
<div className="card__content">{reward.reward_description}</div>
</div>
</section>
);
};
class RewardsPage extends React.PureComponent { class RewardsPage extends React.PureComponent {
componentDidMount() { componentDidMount() {
@ -44,32 +22,8 @@ class RewardsPage extends React.PureComponent {
} }
} }
render() { renderPageHeader() {
const { doAuth, fetching, navigate, rewards, user } = this.props; const { doAuth, navigate, user } = this.props;
let content, cardHeader;
if (fetching) {
content = (
<div className="card__content">
<BusyMessage message={__("Fetching rewards")} />
</div>
);
} else if (rewards.length > 0) {
content = (
<div>
{rewards.map(reward =>
<RewardTile key={reward.reward_type} reward={reward} />
)}
</div>
);
} else {
content = (
<div className="card__content empty">
{__("Failed to load rewards.")}
</div>
);
}
if (user && !user.is_reward_approved) { if (user && !user.is_reward_approved) {
if ( if (
@ -77,7 +31,7 @@ class RewardsPage extends React.PureComponent {
!user.has_verified_email || !user.has_verified_email ||
!user.is_identity_verified !user.is_identity_verified
) { ) {
cardHeader = ( return (
<div> <div>
<div className="card__content empty"> <div className="card__content empty">
<p> <p>
@ -90,7 +44,7 @@ class RewardsPage extends React.PureComponent {
</div> </div>
); );
} else { } else {
cardHeader = ( return (
<div className="card__content"> <div className="card__content">
<p> <p>
{__( {__(
@ -122,9 +76,21 @@ class RewardsPage extends React.PureComponent {
</div> </div>
); );
} }
}
}
renderUnclaimedRewards() {
const { fetching, rewards, user } = this.props;
if (fetching) {
return (
<div className="card__content">
<BusyMessage message={__("Fetching rewards")} />
</div>
);
} else if (user === null) { } else if (user === null) {
cardHeader = (
<div> return (
<div className="card__content empty"> <div className="card__content empty">
<p> <p>
{__( {__(
@ -132,15 +98,27 @@ class RewardsPage extends React.PureComponent {
)} )}
</p> </p>
</div> </div>
);
} else if (!rewards || rewards.length <= 0) {
return (
<div className="card__content empty">
{__("Failed to load rewards.")}
</div> </div>
); );
} else {
return rewards.map(reward =>
<RewardTile key={reward.reward_type} reward={reward} />
);
}
} }
render() {
return ( return (
<main className="main--single-column"> <main className="main--single-column">
<SubHeader /> <SubHeader />
{cardHeader && <section className="card">{cardHeader}</section>} {this.renderPageHeader()}
{content} {this.renderUnclaimedRewards()}
{<RewardListClaimed />}
</main> </main>
); );
} }

View file

@ -0,0 +1,5 @@
import React from "react";
import { connect } from "react-redux";
import WalletPage from "./view";
export default connect(null, null)(WalletPage);

View file

@ -0,0 +1,14 @@
import React from "react";
import SubHeader from "component/subHeader";
import TransactionList from "component/transactionList";
const TransactionHistoryPage = props => {
return (
<main className="main--single-column">
<SubHeader />
<TransactionList />
</main>
);
};
export default TransactionHistoryPage;

View file

@ -3,7 +3,8 @@ import * as types from "constants/action_types";
const reducers = {}; const reducers = {};
const defaultState = { const defaultState = {
fetching: false, fetching: false,
rewardsByType: {}, claimedRewardsById: {}, //id => reward
unclaimedRewardsByType: {},
claimPendingByType: {}, claimPendingByType: {},
claimErrorsByType: {}, claimErrorsByType: {},
}; };
@ -17,11 +18,19 @@ reducers[types.FETCH_REWARDS_STARTED] = function(state, action) {
reducers[types.FETCH_REWARDS_COMPLETED] = function(state, action) { reducers[types.FETCH_REWARDS_COMPLETED] = function(state, action) {
const { userRewards } = action.data; const { userRewards } = action.data;
const rewardsByType = {}; let unclaimedRewards = {},
userRewards.forEach(reward => (rewardsByType[reward.reward_type] = reward)); claimedRewards = {};
userRewards.forEach(reward => {
if (reward.transaction_id) {
claimedRewards[reward.id] = reward;
} else {
unclaimedRewards[reward.reward_type] = reward;
}
});
return Object.assign({}, state, { return Object.assign({}, state, {
rewardsByType: rewardsByType, claimedRewardsById: claimedRewards,
unclaimedRewardsByType: unclaimedRewards,
fetching: false, fetching: false,
}); });
}; };
@ -55,16 +64,22 @@ reducers[types.CLAIM_REWARD_STARTED] = function(state, action) {
reducers[types.CLAIM_REWARD_SUCCESS] = function(state, action) { reducers[types.CLAIM_REWARD_SUCCESS] = function(state, action) {
const { reward } = action.data; const { reward } = action.data;
const existingReward = state.rewardsByType[reward.reward_type]; let unclaimedRewardsByType = Object.assign({}, state.unclaimedRewardsByType);
const existingReward = unclaimedRewardsByType[reward.reward_type];
delete state.unclaimedRewardsByType[reward.reward_type];
const newReward = Object.assign({}, reward, { const newReward = Object.assign({}, reward, {
reward_title: existingReward.reward_title, reward_title: existingReward.reward_title,
reward_description: existingReward.reward_description, reward_description: existingReward.reward_description,
}); });
const rewardsByType = Object.assign({}, state.rewardsByType);
rewardsByType[reward.reward_type] = newReward; let claimedRewardsById = Object.assign({}, state.claimedRewardsById);
claimedRewardsById[reward.id] = newReward;
const newState = Object.assign({}, state, { rewardsByType }); const newState = Object.assign({}, state, {
unclaimedRewardsByType,
claimedRewardsById,
});
return setClaimRewardState(newState, newReward, false, ""); return setClaimRewardState(newState, newReward, false, "");
}; };

View file

@ -136,6 +136,7 @@ export const selectHeaderLinks = createSelector(selectCurrentPage, page => {
// This contains intentional fall throughs // This contains intentional fall throughs
switch (page) { switch (page) {
case "wallet": case "wallet":
case "history":
case "send": case "send":
case "receive": case "receive":
case "invite": case "invite":
@ -143,6 +144,7 @@ export const selectHeaderLinks = createSelector(selectCurrentPage, page => {
case "backup": case "backup":
return { return {
wallet: __("Overview"), wallet: __("Overview"),
history: __("History"),
send: __("Send"), send: __("Send"),
receive: __("Receive"), receive: __("Receive"),
invite: __("Invites"), invite: __("Invites"),

View file

@ -3,13 +3,23 @@ import { selectUser } from "selectors/user";
const _selectState = state => state.rewards || {}; const _selectState = state => state.rewards || {};
export const selectRewardsByType = createSelector( export const selectUnclaimedRewardsByType = createSelector(
_selectState, _selectState,
state => state.rewardsByType || {} state => state.unclaimedRewardsByType
); );
export const selectRewards = createSelector( export const selectClaimedRewardsById = createSelector(
selectRewardsByType, _selectState,
state => state.claimedRewardsById
);
export const selectClaimedRewards = createSelector(
selectClaimedRewardsById,
byId => Object.values(byId) || []
);
export const selectUnclaimedRewards = createSelector(
selectUnclaimedRewardsByType,
byType => Object.values(byType) || [] byType => Object.values(byType) || []
); );
@ -23,7 +33,9 @@ export const selectFetchingRewards = createSelector(
state => !!state.fetching state => !!state.fetching
); );
export const selectTotalRewardValue = createSelector(selectRewards, rewards => export const selectUnclaimedRewardValue = createSelector(
selectUnclaimedRewards,
rewards =>
rewards.reduce((sum, reward) => { rewards.reduce((sum, reward) => {
return sum + reward.reward_amount; return sum + reward.reward_amount;
}, 0) }, 0)
@ -65,7 +77,7 @@ export const makeSelectClaimRewardError = () => {
}; };
const selectRewardByType = (state, props) => { const selectRewardByType = (state, props) => {
return selectRewardsByType(state)[props.reward_type]; return selectUnclaimedRewardsByType(state)[props.reward_type];
}; };
export const makeSelectRewardByType = () => { export const makeSelectRewardByType = () => {