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 lbryio from "lbryio";
import rewards from "rewards";
import { selectRewardsByType } from "selectors/rewards";
import { selectUnclaimedRewardsByType } from "selectors/rewards";
export function doRewardList() {
return function(dispatch, getState) {
@ -13,7 +13,7 @@ export function doRewardList() {
});
lbryio
.call("reward", "list", {})
.call("reward", "list", { multiple_rewards_per_type: true })
.then(userRewards => {
dispatch({
type: types.FETCH_REWARDS_COMPLETED,
@ -31,17 +31,9 @@ export function doRewardList() {
export function doClaimRewardType(rewardType) {
return function(dispatch, getState) {
const rewardsByType = selectRewardsByType(getState()),
const rewardsByType = selectUnclaimedRewardsByType(getState()),
reward = rewardsByType[rewardType];
if (reward) {
dispatch(doClaimReward(reward));
}
};
}
export function doClaimReward(reward, saveError = false) {
return function(dispatch, getState) {
if (reward.transaction_id) {
//already claimed, do nothing
return;
@ -72,7 +64,7 @@ export function doClaimReward(reward, saveError = false) {
type: types.CLAIM_REWARD_FAILURE,
data: {
reward,
error: saveError ? error : null,
error: error ? error : null,
},
});
};
@ -87,26 +79,18 @@ export function doClaimEligiblePurchaseRewards() {
return;
}
const rewardsByType = selectRewardsByType(getState());
const rewardsByType = selectUnclaimedRewardsByType(getState());
let types = {};
types[rewards.TYPE_FIRST_STREAM] = false;
types[rewards.TYPE_FEATURED_DOWNLOAD] = false;
types[rewards.TYPE_MANY_DOWNLOADS] = false;
Object.values(rewardsByType).forEach(reward => {
if (types[reward.reward_type] === false && reward.transaction_id) {
types[reward.reward_type] = true;
}
});
let unclaimedType = Object.keys(types).find(type => {
return types[type] === false && type !== rewards.TYPE_FEATURED_DOWNLOAD; //handled below
});
if (unclaimedType) {
dispatch(doClaimRewardType(unclaimedType));
if (rewardsByType[rewards.TYPE_FIRST_STREAM]) {
dispatch(doClaimRewardType(rewards.TYPE_FIRST_STREAM));
} else {
[
rewards.TYPE_MANY_DOWNLOADS,
rewards.TYPE_FEATURED_DOWNLOAD,
].forEach(type => {
dispatch(doClaimRewardType(type));
});
}
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,
} from "selectors/rewards";
import { doNavigate } from "actions/app";
import { doClaimReward, doClaimRewardClearError } from "actions/rewards";
import { doClaimRewardType, doClaimRewardClearError } from "actions/rewards";
import RewardLink from "./view";
const makeSelect = () => {
@ -24,7 +24,7 @@ const makeSelect = () => {
};
const perform = dispatch => ({
claimReward: reward => dispatch(doClaimReward(reward, true)),
claimReward: reward => dispatch(doClaimRewardType(reward.reward_type, true)),
clearError: reward => dispatch(doClaimRewardClearError(reward)),
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 FileListDownloaded from "page/fileListDownloaded";
import FileListPublished from "page/fileListPublished";
import TransactionHistoryPage from "page/transactionHistory";
import ChannelPage from "page/channel";
import SearchPage from "page/search";
import AuthPage from "page/auth";
@ -36,6 +37,7 @@ const Router = props => {
discover: <DiscoverPage params={params} />,
downloaded: <FileListDownloaded params={params} />,
help: <HelpPage params={params} />,
history: <TransactionHistoryPage params={params} />,
invite: <InvitePage params={params} />,
publish: <PublishPage params={params} />,
published: <FileListPublished params={params} />,

View file

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

View file

@ -7,7 +7,7 @@ import { selectUserIsRewardApproved } from "selectors/user";
import {
makeSelectHasClaimedReward,
makeSelectRewardByType,
selectTotalRewardValue,
selectUnclaimedRewardValue,
} from "selectors/rewards";
import * as settings from "constants/settings";
import ModalCreditIntro from "./view";
@ -19,7 +19,7 @@ const select = (state, props) => {
return {
isRewardApproved: selectUserIsRewardApproved(state),
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 { connect } from "react-redux";
import {
makeSelectRewardByType,
selectFetchingRewards,
selectRewards,
selectUnclaimedRewards,
} from "selectors/rewards";
import { selectUser } from "selectors/user";
import { doAuthNavigate, doNavigate } from "actions/app";
import { doRewardList } from "actions/rewards";
import rewards from "rewards";
import RewardsPage from "./view";
const select = (state, props) => {
const selectReward = makeSelectRewardByType();
return {
fetching: selectFetchingRewards(state),
rewards: selectRewards(state),
newUserReward: selectReward(state, { reward_type: rewards.TYPE_NEW_USER }),
rewards: selectUnclaimedRewards(state),
user: selectUser(state),
};
};

View file

@ -1,31 +1,9 @@
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 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 {
componentDidMount() {
@ -44,32 +22,8 @@ class RewardsPage extends React.PureComponent {
}
}
render() {
const { doAuth, fetching, navigate, rewards, 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>
);
}
renderPageHeader() {
const { doAuth, navigate, user } = this.props;
if (user && !user.is_reward_approved) {
if (
@ -77,7 +31,7 @@ class RewardsPage extends React.PureComponent {
!user.has_verified_email ||
!user.is_identity_verified
) {
cardHeader = (
return (
<div>
<div className="card__content empty">
<p>
@ -90,7 +44,7 @@ class RewardsPage extends React.PureComponent {
</div>
);
} else {
cardHeader = (
return (
<div className="card__content">
<p>
{__(
@ -122,25 +76,49 @@ class RewardsPage extends React.PureComponent {
</div>
);
}
} else if (user === null) {
cardHeader = (
<div>
<div className="card__content empty">
<p>
{__(
"This application is unable to earn rewards due to an authentication failure."
)}
</p>
</div>
}
}
renderUnclaimedRewards() {
const { fetching, rewards, user } = this.props;
if (fetching) {
return (
<div className="card__content">
<BusyMessage message={__("Fetching rewards")} />
</div>
);
}
} else if (user === null) {
return (
<div className="card__content empty">
<p>
{__(
"This application is unable to earn rewards due to an authentication failure."
)}
</p>
</div>
);
} else if (!rewards || rewards.length <= 0) {
return (
<div className="card__content empty">
{__("Failed to load rewards.")}
</div>
);
} else {
return rewards.map(reward =>
<RewardTile key={reward.reward_type} reward={reward} />
);
}
}
render() {
return (
<main className="main--single-column">
<SubHeader />
{cardHeader && <section className="card">{cardHeader}</section>}
{content}
{this.renderPageHeader()}
{this.renderUnclaimedRewards()}
{<RewardListClaimed />}
</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 defaultState = {
fetching: false,
rewardsByType: {},
claimedRewardsById: {}, //id => reward
unclaimedRewardsByType: {},
claimPendingByType: {},
claimErrorsByType: {},
};
@ -17,11 +18,19 @@ reducers[types.FETCH_REWARDS_STARTED] = function(state, action) {
reducers[types.FETCH_REWARDS_COMPLETED] = function(state, action) {
const { userRewards } = action.data;
const rewardsByType = {};
userRewards.forEach(reward => (rewardsByType[reward.reward_type] = reward));
let unclaimedRewards = {},
claimedRewards = {};
userRewards.forEach(reward => {
if (reward.transaction_id) {
claimedRewards[reward.id] = reward;
} else {
unclaimedRewards[reward.reward_type] = reward;
}
});
return Object.assign({}, state, {
rewardsByType: rewardsByType,
claimedRewardsById: claimedRewards,
unclaimedRewardsByType: unclaimedRewards,
fetching: false,
});
};
@ -55,16 +64,22 @@ reducers[types.CLAIM_REWARD_STARTED] = function(state, action) {
reducers[types.CLAIM_REWARD_SUCCESS] = function(state, action) {
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, {
reward_title: existingReward.reward_title,
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, "");
};

View file

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

View file

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