Merge branch 'invites_and_rewards'

This commit is contained in:
Jeremy Kauffman 2017-08-30 09:04:53 -04:00
commit fa0d2b5b90
96 changed files with 4253 additions and 1032 deletions

View file

@ -8,19 +8,24 @@ Web UI version numbers should always match the corresponding version of LBRY App
## [Unreleased] ## [Unreleased]
### Added ### Added
* Added an Invites area inside of the Wallet. This allows users to invite others and shows the status of all past invites (including all invite data from the past year).
* Added a forward button and improved history behavior. Back/forward disable when unusable. * Added a forward button and improved history behavior. Back/forward disable when unusable.
* Added a new component, `FormFieldPrice` which is now used in Publish and Settings. * Added new summary components for rewards and invites to the Wallet landing page.
* Added past history of rewards to the rewards page.
* Added wallet backup guide reference. * Added wallet backup guide reference.
* Added a new widget for setting prices (`FormFieldPrice`), used in Publish and Settings.
### Changed ### Changed
* Updated to daemon [0.15](https://github.com/lbryio/lbry/releases). Most relevant changes for app are improved announcing of content and a fix for the daemon getting stuck running. * Updated to daemon [0.15](https://github.com/lbryio/lbry/releases). Most relevant changes for app are improved announcing of content and a fix for the daemon getting stuck running.
* Continued to refine first-run process, process for new users, and introducing people to LBRY and LBRY credits. * Continued to refine first-run process, process for new users, and introducing people to LBRY and LBRY credits.
* Changed the default price settings. * Changed Wallet landing page to summarize status of other areas. Refactored wallet and transaction logic.
* Added icons to missing page, improved icon and title logic.
* Changed the default price settings for priced publishes.
* When an "Open" button is clicked on a show page, if the file fails to open, the app will try to open the file's folder. * When an "Open" button is clicked on a show page, if the file fails to open, the app will try to open the file's folder.
* Updated several packages and fixed warnings in build process (all but the [fsevents warning](https://github.com/yarnpkg/yarn/issues/3738), which is a rather dramatic debate)
* Some form field refactoring as we take baby steps towards form sanity. * Some form field refactoring as we take baby steps towards form sanity.
* Replaced confusing placeholder text from email input. * Replaced confusing placeholder text from email input.
* Refactored modal and settings logic. * Refactored modal and settings logic.
* Updated several packages and fixed warnings in build process (all but the [fsevents warning](https://github.com/yarnpkg/yarn/issues/3738), which is a rather dramatic debate)
### Fixed ### Fixed
* Tiles will no longer be blurry on hover (Windows only bug) * Tiles will no longer be blurry on hover (Windows only bug)

View file

@ -51,7 +51,7 @@ let daemonStopRequested = false;
let readyToQuit = false; let readyToQuit = false;
// If we receive a URI to open from an external app but there's no window to // If we receive a URI to open from an external app but there's no window to
// send it to, it's cached in this variable. // sendCredits it to, it's cached in this variable.
let openUri = null; let openUri = null;
function processRequestedUri(uri) { function processRequestedUri(uri) {

Binary file not shown.

Binary file not shown.

File diff suppressed because it is too large Load diff

Before

Width:  |  Height:  |  Size: 306 KiB

After

Width:  |  Height:  |  Size: 434 KiB

Binary file not shown.

Binary file not shown.

Binary file not shown.

View file

@ -26,6 +26,10 @@ const { lbrySettings: config } = require("../../../app/package.json");
export function doNavigate(path, params = {}, options = {}) { export function doNavigate(path, params = {}, options = {}) {
return function(dispatch, getState) { return function(dispatch, getState) {
if (!path) {
return;
}
let url = path; let url = path;
if (params) url = `${url}?${toQueryString(params)}`; if (params) url = `${url}?${toQueryString(params)}`;

View file

@ -2,7 +2,8 @@ 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";
import { selectUserIsRewardApproved } from "selectors/user";
export function doRewardList() { export function doRewardList() {
return function(dispatch, getState) { return function(dispatch, getState) {
@ -13,7 +14,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,22 +32,23 @@ export function doRewardList() {
export function doClaimRewardType(rewardType) { export function doClaimRewardType(rewardType) {
return function(dispatch, getState) { return function(dispatch, getState) {
const rewardsByType = selectRewardsByType(getState()), const state = getState(),
reward = rewardsByType[rewardType]; rewardsByType = selectUnclaimedRewardsByType(state),
reward = rewardsByType[rewardType],
userIsRewardApproved = selectUserIsRewardApproved(state);
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;
} }
if (!userIsRewardApproved) {
return dispatch({
type: types.OPEN_MODAL,
data: { modal: modals.REWARD_APPROVAL_REQUIRED },
});
}
dispatch({ dispatch({
type: types.CLAIM_REWARD_STARTED, type: types.CLAIM_REWARD_STARTED,
data: { reward }, data: { reward },
@ -70,10 +72,7 @@ export function doClaimReward(reward, saveError = false) {
const failure = error => { const failure = error => {
dispatch({ dispatch({
type: types.CLAIM_REWARD_FAILURE, type: types.CLAIM_REWARD_FAILURE,
data: { data: { reward, error },
reward,
error: saveError ? error : null,
},
}); });
}; };
@ -83,30 +82,24 @@ export function doClaimReward(reward, saveError = false) {
export function doClaimEligiblePurchaseRewards() { export function doClaimEligiblePurchaseRewards() {
return function(dispatch, getState) { return function(dispatch, getState) {
if (!lbryio.enabled) { const state = getState(),
rewardsByType = selectUnclaimedRewardsByType(state),
userIsRewardApproved = selectUserIsRewardApproved(state);
if (!userIsRewardApproved || !lbryio.enabled) {
return; return;
} }
const rewardsByType = selectRewardsByType(getState()); if (rewardsByType[rewards.TYPE_FIRST_STREAM]) {
dispatch(doClaimRewardType(rewards.TYPE_FIRST_STREAM));
let types = {}; } else {
[
types[rewards.TYPE_FIRST_STREAM] = false; rewards.TYPE_MANY_DOWNLOADS,
types[rewards.TYPE_FEATURED_DOWNLOAD] = false; rewards.TYPE_FEATURED_DOWNLOAD,
types[rewards.TYPE_MANY_DOWNLOADS] = false; ].forEach(type => {
Object.values(rewardsByType).forEach(reward => { dispatch(doClaimRewardType(type));
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));
} }
dispatch(doClaimRewardType(rewards.TYPE_FEATURED_DOWNLOAD));
}; };
} }

View file

@ -1,7 +1,7 @@
import * as types from "constants/action_types"; 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 { doOpenModal } from "actions/app"; import { doOpenModal, doShowSnackBar } from "actions/app";
import { doRewardList, doClaimRewardType } from "actions/rewards"; import { doRewardList, doClaimRewardType } from "actions/rewards";
import { selectEmailToVerify, selectUser } from "selectors/user"; import { selectEmailToVerify, selectUser } from "selectors/user";
import rewards from "rewards"; import rewards from "rewards";
@ -19,6 +19,7 @@ export function doAuthenticate() {
data: { user }, data: { user },
}); });
dispatch(doRewardList()); dispatch(doRewardList());
dispatch(doFetchInviteStatus());
}) })
.catch(error => { .catch(error => {
dispatch(doOpenModal(modals.AUTHENTICATION_FAILURE)); dispatch(doOpenModal(modals.AUTHENTICATION_FAILURE));
@ -172,3 +173,62 @@ export function doFetchAccessToken() {
lbryio.getAuthToken().then(success); lbryio.getAuthToken().then(success);
}; };
} }
export function doFetchInviteStatus() {
return function(dispatch, getState) {
dispatch({
type: types.USER_INVITE_STATUS_FETCH_STARTED,
});
lbryio
.call("user", "invite_status")
.then(status => {
dispatch({
type: types.USER_INVITE_STATUS_FETCH_SUCCESS,
data: {
invitesRemaining: status.invites_remaining
? status.invites_remaining
: 0,
invitees: status.invitees,
},
});
})
.catch(error => {
dispatch({
type: types.USER_INVITE_STATUS_FETCH_FAILURE,
data: { error },
});
});
};
}
export function doUserInviteNew(email) {
return function(dispatch, getState) {
dispatch({
type: types.USER_INVITE_NEW_STARTED,
});
lbryio
.call("user", "invite", { email: email }, "post")
.then(invite => {
dispatch({
type: types.USER_INVITE_NEW_SUCCESS,
data: { email },
});
dispatch(
doShowSnackBar({
message: __("Invite sent to %s", email),
})
);
dispatch(doFetchInviteStatus());
})
.catch(error => {
dispatch({
type: types.USER_INVITE_NEW_FAILURE,
data: { error },
});
});
};
}

View file

@ -22,7 +22,6 @@ const app = {
i18n: i18n, i18n: i18n,
logs: logs, logs: logs,
log: function(message) { log: function(message) {
console.log(message);
logs.push(message); logs.push(message);
}, },
}; };

View file

@ -1,30 +1,22 @@
import React from "react"; import React from "react";
import { connect } from "react-redux"; import { connect } from "react-redux";
import { selectCurrentModal } from "selectors/app";
import { import {
doCheckUpgradeAvailable, doCheckUpgradeAvailable,
doOpenModal,
doAlertError, doAlertError,
doRecordScroll, doRecordScroll,
} from "actions/app"; } from "actions/app";
import { doFetchRewardedContent } from "actions/content"; import { doFetchRewardedContent } from "actions/content";
import { doUpdateBalance } from "actions/wallet"; import { doUpdateBalance } from "actions/wallet";
import { selectWelcomeModalAcknowledged } from "selectors/app";
import { selectUser } from "selectors/user"; import { selectUser } from "selectors/user";
import App from "./view"; import App from "./view";
import * as modals from "constants/modal_types";
const select = (state, props) => ({ const select = (state, props) => ({
modal: selectCurrentModal(state),
isWelcomeAcknowledged: selectWelcomeModalAcknowledged(state),
user: selectUser(state), user: selectUser(state),
}); });
const perform = dispatch => ({ const perform = dispatch => ({
alertError: errorList => dispatch(doAlertError(errorList)), alertError: errorList => dispatch(doAlertError(errorList)),
checkUpgradeAvailable: () => dispatch(doCheckUpgradeAvailable()), checkUpgradeAvailable: () => dispatch(doCheckUpgradeAvailable()),
openWelcomeModal: () => dispatch(doOpenModal(modals.WELCOME)),
updateBalance: balance => dispatch(doUpdateBalance(balance)), updateBalance: balance => dispatch(doUpdateBalance(balance)),
fetchRewardedContent: () => dispatch(doFetchRewardedContent()), fetchRewardedContent: () => dispatch(doFetchRewardedContent()),
recordScroll: scrollPosition => dispatch(doRecordScroll(scrollPosition)), recordScroll: scrollPosition => dispatch(doRecordScroll(scrollPosition)),

View file

@ -165,7 +165,7 @@ class CardVerify extends React.Component {
render() { render() {
return ( return (
<Link <Link
button="primary" button="alt"
label={this.props.label} label={this.props.label}
icon="icon-lock" icon="icon-lock"
disabled={ disabled={

View file

@ -69,15 +69,18 @@ export class CreditAmount extends React.PureComponent {
label: React.PropTypes.bool, label: React.PropTypes.bool,
showFree: React.PropTypes.bool, showFree: React.PropTypes.bool,
showFullPrice: React.PropTypes.bool, showFullPrice: React.PropTypes.bool,
showPlus: React.PropTypes.bool,
look: React.PropTypes.oneOf(["indicator", "plain"]), look: React.PropTypes.oneOf(["indicator", "plain"]),
}; };
static defaultProps = { static defaultProps = {
precision: 2, precision: 2,
label: true, label: true,
showFree: false,
look: "indicator", look: "indicator",
showFree: false, showFree: false,
showFullPrice: false, showFullPrice: false,
showPlus: false,
}; };
render() { render() {
@ -98,13 +101,18 @@ export class CreditAmount extends React.PureComponent {
let amountText; let amountText;
if (this.props.showFree && parseFloat(this.props.amount) === 0) { if (this.props.showFree && parseFloat(this.props.amount) === 0) {
amountText = __("free"); amountText = __("free");
} else if (this.props.label) {
amountText =
formattedAmount +
" " +
(parseFloat(amount) == 1 ? __("credit") : __("credits"));
} else { } else {
amountText = formattedAmount; if (this.props.label) {
amountText =
formattedAmount +
" " +
(parseFloat(amount) == 1 ? __("credit") : __("credits"));
} else {
amountText = formattedAmount;
}
if (this.props.showPlus && amount > 0) {
amountText = "+" + amountText;
}
} }
return ( return (

View file

@ -167,18 +167,20 @@ class FileActions extends React.PureComponent {
<section className="file-actions"> <section className="file-actions">
{content} {content}
{showMenu {showMenu
? <DropDownMenu> ? <div className="button-set-item">
<DropDownMenuItem <DropDownMenu>
key={0} <DropDownMenuItem
onClick={() => openInFolder(fileInfo)} key={0}
label={openInFolderMessage} onClick={() => openInFolder(fileInfo)}
/> label={openInFolderMessage}
<DropDownMenuItem />
key={1} <DropDownMenuItem
onClick={() => openModal(modals.CONFIRM_FILE_REMOVE)} key={1}
label={__("Remove...")} onClick={() => openModal(modals.CONFIRM_FILE_REMOVE)}
/> label={__("Remove...")}
</DropDownMenu> />
</DropDownMenu>
</div>
: ""} : ""}
<Modal <Modal
type="confirm" type="confirm"

View file

@ -0,0 +1,16 @@
import React from "react";
import { connect } from "react-redux";
import {
selectUserInvitees,
selectUserInviteStatusIsPending,
} from "selectors/user";
import InviteList from "./view";
const select = state => ({
invitees: selectUserInvitees(state),
isPending: selectUserInviteStatusIsPending(state),
});
const perform = dispatch => ({});
export default connect(select, perform)(InviteList);

View file

@ -0,0 +1,70 @@
import React from "react";
import { Icon } from "component/common";
import RewardLink from "component/rewardLink";
import rewards from "rewards.js";
class InviteList extends React.PureComponent {
render() {
const { invitees } = this.props;
if (!invitees) {
return null;
}
return (
<section className="card">
<div className="card__title-primary">
<h3>{__("Invite History")}</h3>
</div>
<div className="card__content">
{invitees.length === 0 &&
<span className="empty">{__("You haven't invited anyone.")} </span>}
{invitees.length > 0 &&
<table className="table-standard table-stretch">
<thead>
<tr>
<th>
{__("Invitee Email")}
</th>
<th className="text-center">
{__("Invite Status")}
</th>
<th className="text-center">
{__("Reward")}
</th>
</tr>
</thead>
<tbody>
{invitees.map((invitee, index) => {
return (
<tr key={index}>
<td>{invitee.email}</td>
<td className="text-center">
{invitee.invite_accepted
? <Icon icon="icon-check" />
: <span className="empty">{__("unused")}</span>}
</td>
<td className="text-center">
{invitee.invite_reward_claimed
? <Icon icon="icon-check" />
: invitee.invite_accepted
? <RewardLink
label={__("Claim")}
reward_type={rewards.TYPE_FIRST_PUBLISH}
/>
: <span className="empty">
{__("unclaimable")}
</span>}
</td>
</tr>
);
})}
</tbody>
</table>}
</div>
</section>
);
}
}
export default InviteList;

View file

@ -0,0 +1,29 @@
import React from "react";
import { connect } from "react-redux";
import InviteNew from "./view";
import {
selectUserInvitesRemaining,
selectUserInviteNewIsPending,
selectUserInviteNewErrorMessage,
} from "selectors/user";
import rewards from "rewards";
import { makeSelectRewardAmountByType } from "selectors/rewards";
import { doUserInviteNew } from "actions/user";
const select = state => {
const selectReward = makeSelectRewardAmountByType();
return {
errorMessage: selectUserInviteNewErrorMessage(state),
invitesRemaining: selectUserInvitesRemaining(state),
isPending: selectUserInviteNewIsPending(state),
rewardAmount: selectReward(state, { reward_type: rewards.TYPE_REFERRAL }),
};
};
const perform = dispatch => ({
inviteNew: email => dispatch(doUserInviteNew(email)),
});
export default connect(select, perform)(InviteNew);

View file

@ -0,0 +1,100 @@
import React from "react";
import { BusyMessage, CreditAmount } from "component/common";
import Link from "component/link";
import { FormRow } from "component/form.js";
class FormInviteNew extends React.PureComponent {
constructor(props) {
super(props);
this.state = {
email: "",
};
}
handleEmailChanged(event) {
this.setState({
email: event.target.value,
});
}
handleSubmit(event) {
event.preventDefault();
this.props.inviteNew(this.state.email);
}
render() {
const { errorMessage, isPending } = this.props;
return (
<form>
<FormRow
type="text"
label="Email"
placeholder="youremail@example.org"
name="email"
value={this.state.email}
errorMessage={errorMessage}
onChange={event => {
this.handleEmailChanged(event);
}}
/>
<div className="form-row-submit">
<Link
button="primary"
label={__("Send Invite")}
disabled={isPending}
onClick={event => {
this.handleSubmit(event);
}}
/>
</div>
</form>
);
}
}
class InviteNew extends React.PureComponent {
render() {
const {
errorMessage,
invitesRemaining,
inviteNew,
inviteStatusIsPending,
isPending,
rewardAmount,
} = this.props;
return (
<section className="card">
<div className="card__title-primary">
<CreditAmount amount={rewardAmount} />
<h3>
{__("Invite a Friend")}
</h3>
</div>
{/*
<div className="card__content">
{invitesRemaining > 0 &&
<p>{__("You have %s invites remaining.", invitesRemaining)}</p>}
{invitesRemaining <= 0 &&
<p className="empty">{__("You have no invites.")}</p>}
</div> */}
<div className="card__content">
<p>
{__(
"Or an enemy. Or your cousin Jerry, who you're kind of unsure about."
)}
</p>
<FormInviteNew
errorMessage={errorMessage}
inviteNew={inviteNew}
isPending={isPending}
/>
</div>
</section>
);
}
}
export default InviteNew;

View file

@ -1,5 +1,10 @@
import React from "react"; import React from "react";
import { connect } from "react-redux"; import { connect } from "react-redux";
import { doNavigate } from "actions/app";
import Link from "./view"; import Link from "./view";
export default connect(null, null)(Link); const perform = dispatch => ({
doNavigate: path => dispatch(doNavigate(path)),
});
export default connect(null, perform)(Link);

View file

@ -5,7 +5,6 @@ const Link = props => {
const { const {
href, href,
title, title,
onClick,
style, style,
label, label,
icon, icon,
@ -13,6 +12,8 @@ const Link = props => {
button, button,
disabled, disabled,
children, children,
navigate,
doNavigate,
} = props; } = props;
const className = const className =
@ -21,6 +22,12 @@ const Link = props => {
(button ? " button-block button-" + button + " button-set-item" : "") + (button ? " button-block button-" + button + " button-set-item" : "") +
(disabled ? " disabled" : ""); (disabled ? " disabled" : "");
const onClick = !props.onClick && navigate
? () => {
doNavigate(navigate);
}
: props.onClick;
let content; let content;
if (children) { if (children) {
content = children; content = children;

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

@ -16,6 +16,7 @@ class PublishForm extends React.PureComponent {
this._requiredFields = ["name", "bid", "meta_title", "tosAgree"]; this._requiredFields = ["name", "bid", "meta_title", "tosAgree"];
this._defaultCopyrightNotice = "All rights reserved."; this._defaultCopyrightNotice = "All rights reserved.";
this._defaultPaidPrice = 0.01;
this.state = { this.state = {
rawName: "", rawName: "",
@ -318,7 +319,9 @@ class PublishForm extends React.PureComponent {
handleFeePrefChange(feeEnabled) { handleFeePrefChange(feeEnabled) {
this.setState({ this.setState({
isFee: feeEnabled, isFee: feeEnabled,
feeAmount: this.state.feeAmount == "" ? "5.00" : this.state.feeAmount, feeAmount: this.state.feeAmount == ""
? this._defaultPaidPrice
: this.state.feeAmount,
}); });
} }
@ -786,7 +789,6 @@ class PublishForm extends React.PureComponent {
ref="bid" ref="bid"
type="number" type="number"
step="0.01" step="0.01"
min="0"
label={__("Deposit")} label={__("Deposit")}
postfix="LBC" postfix="LBC"
onChange={event => { onChange={event => {

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

@ -9,15 +9,18 @@ const RewardLink = props => {
claimReward, claimReward,
clearError, clearError,
errorMessage, errorMessage,
label,
isPending, isPending,
} = props; } = props;
return ( return (
<div className="reward-link"> <div className="reward-link">
<Link <Link
button={button ? button : "alt"} button={button}
disabled={isPending} disabled={isPending}
label={isPending ? __("Claiming...") : __("Claim Reward")} label={
isPending ? __("Claiming...") : label ? label : __("Claim Reward")
}
onClick={() => { onClick={() => {
claimReward(reward); claimReward(reward);
}} }}

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,11 @@
import React from "react";
import { connect } from "react-redux";
import { doNavigate } from "actions/app";
import { selectUnclaimedRewardValue } from "selectors/rewards";
import RewardSummary from "./view";
const select = state => ({
unclaimedRewardAmount: selectUnclaimedRewardValue(state),
});
export default connect(select, null)(RewardSummary);

View file

@ -0,0 +1,29 @@
import React from "react";
import Link from "component/link";
import { CreditAmount } from "component/common";
const RewardSummary = props => {
const { balance, unclaimedRewardAmount } = props;
return (
<section className="card">
<div className="card__title-primary">
<h3>{__("Rewards")}</h3>
</div>
<div className="card__content">
{unclaimedRewardAmount > 0 &&
<p>
You have{" "}
<CreditAmount amount={unclaimedRewardAmount} precision={8} /> in
unclaimed rewards.
</p>}
</div>
<div className="card__actions card__actions--bottom">
<Link button="text" navigate="/rewards" label={__("Rewards")} />
<Link button="text" navigate="/invite" label={__("Invites")} />
</div>
</section>
);
};
export default RewardSummary;

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,37 @@
import React from "react";
import { CreditAmount, Icon } from "component/common";
import RewardLink from "component/rewardLink";
import Link from "component/link";
import rewards from "rewards";
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__content">{reward.reward_description}</div>
<div className="card__actions card__actions--bottom ">
{reward.reward_type == rewards.TYPE_REFERRAL &&
<Link
button="alt"
navigate="/invite"
label={__("Go To Invites")}
/>}
{reward.reward_type !== rewards.TYPE_REFERRAL &&
(claimed
? <span><Icon icon="icon-check" /> {__("Reward claimed.")}</span>
: <RewardLink button="alt" reward_type={reward.reward_type} />)}
</div>
</div>
</section>
);
};
export default RewardTile;

View file

@ -4,16 +4,20 @@ import HelpPage from "page/help";
import ReportPage from "page/report.js"; import ReportPage from "page/report.js";
import StartPage from "page/start.js"; import StartPage from "page/start.js";
import WalletPage from "page/wallet"; import WalletPage from "page/wallet";
import ShowPage from "page/showPage"; import ReceiveCreditsPage from "page/receiveCredits";
import SendCreditsPage from "page/sendCredits";
import ShowPage from "page/show";
import PublishPage from "page/publish"; import PublishPage from "page/publish";
import DiscoverPage from "page/discover"; import DiscoverPage from "page/discover";
import DeveloperPage from "page/developer.js"; 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";
import InvitePage from "page/invite";
import BackupPage from "page/backup"; import BackupPage from "page/backup";
const route = (page, routesMap) => { const route = (page, routesMap) => {
@ -33,13 +37,15 @@ 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} />,
publish: <PublishPage params={params} />, publish: <PublishPage params={params} />,
published: <FileListPublished params={params} />, published: <FileListPublished params={params} />,
receive: <WalletPage params={params} />, receive: <ReceiveCreditsPage params={params} />,
report: <ReportPage params={params} />, report: <ReportPage params={params} />,
rewards: <RewardsPage params={params} />, rewards: <RewardsPage params={params} />,
search: <SearchPage params={params} />, search: <SearchPage params={params} />,
send: <WalletPage params={params} />, send: <SendCreditsPage params={params} />,
settings: <SettingsPage params={params} />, settings: <SettingsPage params={params} />,
show: <ShowPage params={params} />, show: <ShowPage params={params} />,
start: <StartPage params={params} />, start: <StartPage params={params} />,

View file

@ -1,21 +1,5 @@
import React from "react"; import React from "react";
import { connect } from "react-redux"; import { connect } from "react-redux";
import { doFetchTransactions } from "actions/wallet";
import {
selectBalance,
selectTransactionItems,
selectIsFetchingTransactions,
} from "selectors/wallet";
import TransactionList from "./view"; import TransactionList from "./view";
const select = state => ({ export default connect(null, null)(TransactionList);
fetchingTransactions: selectIsFetchingTransactions(state),
transactionItems: selectTransactionItems(state),
});
const perform = dispatch => ({
fetchTransactions: () => dispatch(doFetchTransactions()),
});
export default connect(select, perform)(TransactionList);

View file

@ -1,73 +1,57 @@
import React from "react"; import React from "react";
import { Address, BusyMessage, CreditAmount } from "component/common"; import LinkTransaction from "component/linkTransaction";
import { CreditAmount } from "component/common";
class TransactionList extends React.PureComponent { const TransactionList = props => {
componentWillMount() { const { emptyMessage, transactions } = props;
this.props.fetchTransactions();
}
render() {
const { fetchingTransactions, transactionItems } = this.props;
const rows = [];
if (transactionItems.length > 0) {
transactionItems.forEach(function(item) {
rows.push(
<tr key={item.id}>
<td>{(item.amount > 0 ? "+" : "") + item.amount}</td>
<td>
{item.date
? item.date.toLocaleDateString()
: <span className="empty">{__("(Transaction pending)")}</span>}
</td>
<td>
{item.date
? item.date.toLocaleTimeString()
: <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>
</td>
</tr>
);
});
}
if (!transactions || !transactions.length) {
return ( return (
<section className="card"> <div className="empty">
<div className="card__title-primary"> {emptyMessage || __("No transactions to list.")}
<h3>{__("Transaction History")}</h3> </div>
</div>
<div className="card__content">
{fetchingTransactions &&
<BusyMessage message={__("Loading transactions")} />}
{!fetchingTransactions && rows.length === 0
? <div className="empty">{__("You have no transactions.")}</div>
: ""}
{rows.length > 0
? <table className="table-standard table-stretch">
<thead>
<tr>
<th>{__("Amount")}</th>
<th>{__("Date")}</th>
<th>{__("Time")}</th>
<th>{__("Transaction")}</th>
</tr>
</thead>
<tbody>
{rows}
</tbody>
</table>
: ""}
</div>
</section>
); );
} }
}
return (
<table className="table-standard table-stretch">
<thead>
<tr>
<th>{__("Date")}</th>
<th>{__("Amount")}</th>
<th>{__("Transaction")}</th>
</tr>
</thead>
<tbody>
{transactions.map(item => {
return (
<tr key={item.id}>
<td>
{item.date
? item.date.toLocaleDateString() +
" " +
item.date.toLocaleTimeString()
: <span className="empty">
{__("(Transaction pending)")}
</span>}
</td>
<td>
<CreditAmount
amount={item.amount}
look="plain"
showPlus={true}
precision={8}
/>{" "}
</td>
<td>
<LinkTransaction id={item.id} />
</td>
</tr>
);
})}
</tbody>
</table>
);
};
export default TransactionList; export default TransactionList;

View file

@ -0,0 +1,23 @@
import React from "react";
import { connect } from "react-redux";
import { doFetchTransactions } from "actions/wallet";
import {
selectBalance,
selectRecentTransactions,
selectHasTransactions,
selectIsFetchingTransactions,
} from "selectors/wallet";
import TransactionListRecent from "./view";
const select = state => ({
fetchingTransactions: selectIsFetchingTransactions(state),
transactions: selectRecentTransactions(state),
hasTransactions: selectHasTransactions(state),
});
const perform = dispatch => ({
fetchTransactions: () => dispatch(doFetchTransactions()),
});
export default connect(select, perform)(TransactionListRecent);

View file

@ -0,0 +1,41 @@
import React from "react";
import { BusyMessage } from "component/common";
import Link from "component/link";
import TransactionList from "component/transactionList";
class TransactionListRecent extends React.PureComponent {
componentWillMount() {
this.props.fetchTransactions();
}
render() {
const { fetchingTransactions, hasTransactions, transactions } = this.props;
return (
<section className="card">
<div className="card__title-primary">
<h3>{__("Recent Transactions")}</h3>
</div>
<div className="card__content">
{fetchingTransactions &&
<BusyMessage message={__("Loading transactions")} />}
{!fetchingTransactions &&
<TransactionList
transactions={transactions}
emptyMessage={__("You have no recent transactions.")}
/>}
</div>
{hasTransactions &&
<div className="card__actions card__actions--bottom">
<Link
navigate="/history"
label={__("See Full History")}
button="text"
/>
</div>}
</section>
);
}
}
export default TransactionListRecent;

View file

@ -1,6 +1,6 @@
import React from "react"; import React from "react";
import { connect } from "react-redux"; import { connect } from "react-redux";
import { doUserEmailNew } from "actions/user"; import { doUserEmailNew, doUserInviteNew } from "actions/user";
import { import {
selectEmailNewIsPending, selectEmailNewIsPending,
selectEmailNewErrorMessage, selectEmailNewErrorMessage,

View file

@ -31,6 +31,16 @@ class UserEmailNew extends React.PureComponent {
this.handleSubmit(event); this.handleSubmit(event);
}} }}
> >
<p>
{__(
"This process is required to prevent abuse of the rewards program."
)}
</p>
<p>
{__(
"We will also contact you about updates and new content, but you can unsubscribe at any time."
)}
</p>
<FormRow <FormRow
type="text" type="text"
label="Email" label="Email"

View file

@ -26,37 +26,125 @@ class UserVerify extends React.PureComponent {
const { errorMessage, isPending, navigate } = this.props; const { errorMessage, isPending, navigate } = this.props;
return ( return (
<div> <div>
<p> <section className="card card--form">
{__( <div className="card__title-primary">
"To ensure you are a real person, we require a valid credit or debit card." <h1>{__("Final Human Proof")}</h1>
) + </div>
" " + <div className="card__content">
__("There is no charge at all, now or in the future.") + <p>
" "} Finally, please complete <strong>one and only one</strong> of the
<Link options below.
href="https://lbry.io/faq/identity-requirements" </p>
label={__("Read More")} </div>
/> </section>
</p> <section className="card card--form">
{errorMessage && <p className="form-field__error">{errorMessage}</p>} <div className="card__title-primary">
<p> <h3>{__("1) Proof via Credit")}</h3>
<CardVerify </div>
label={__("Link Card and Finish")} <div className="card__content">
disabled={isPending} {__(
token={this.onToken.bind(this)} "If you have a valid credit or debit card, you can use it to instantly prove your humanity."
stripeKey={lbryio.getStripeToken()} ) +
/> " " +
</p> __("There is no charge at all for this, now or in the future.") +
<p> " "}
{__( </div>
"You can continue without this step, but you will not be eligible to earn rewards." <div className="card__actions">
)} {errorMessage &&
</p> <p className="form-field__error">{errorMessage}</p>}
<Link <CardVerify
onClick={() => navigate("/discover")} label={__("Perform Card Verification")}
button="alt" disabled={isPending}
label={__("Skip Rewards")} token={this.onToken.bind(this)}
/> stripeKey={lbryio.getStripeToken()}
/>
</div>
<div className="card__content">
<div className="meta">
{__(
"A $1 authorization may temporarily appear with your provider."
)}{" "}
{" "}
<Link
href="https://lbry.io/faq/identity-requirements"
label={__("Read more about why we do this.")}
/>
</div>
</div>
</section>
<section className="card card--form">
<div className="card__title-primary">
<h3>{__("2) Proof via YouTube")}</h3>
</div>
<div className="card__content">
<p>
{__(
"If you have a YouTube account with published videos, you can sync your account to be granted instant verification."
)}
</p>
</div>
<div className="card__actions">
<Link
href="https://api.lbry.io/yt/sync"
button="alt"
icon="icon-youtube"
label={__("YouTube Account Sync")}
/>
</div>
<div className="card__content">
<div className="meta">
This will not automatically refresh after approval. Once you have
synced your account, just navigate away or click
{" "} <Link navigate="/rewards" label="here" />.
</div>
</div>
</section>
<section className="card card--form">
<div className="card__title-primary">
<h3>{__("3) Proof via Chat")}</h3>
</div>
<div className="card__content">
<p>
{__(
"A moderator capable of approving you is typically available in the #verification channel of our chat room."
)}
</p>
<p>
{__(
"This process will likely involve providing proof of a stable and established online or real-life identity."
)}
</p>
</div>
<div className="card__actions">
<Link
href="https://slack.lbry.io"
button="alt"
icon="icon-slack"
label={__("Join LBRY Chat")}
/>
</div>
</section>
<section className="card card--form">
<div className="card__title-primary">
<h5>{__("Or, Skip It Entirely")}</h5>
</div>
<div className="card__content">
<p className="meta">
{__(
"You can continue without this step, but you will not be eligible to earn rewards."
)}
</p>
</div>
<div className="card__actions">
<Link
onClick={() => navigate("/discover")}
button="alt"
label={__("Skip Rewards")}
/>
</div>
</section>
</div> </div>
); );
} }

View file

@ -5,7 +5,7 @@ import {
selectReceiveAddress, selectReceiveAddress,
selectGettingNewAddress, selectGettingNewAddress,
} from "selectors/wallet"; } from "selectors/wallet";
import WalletPage from "./view"; import WalletAddress from "./view";
const select = state => ({ const select = state => ({
receiveAddress: selectReceiveAddress(state), receiveAddress: selectReceiveAddress(state),
@ -17,4 +17,4 @@ const perform = dispatch => ({
getNewAddress: () => dispatch(doGetNewAddress()), getNewAddress: () => dispatch(doGetNewAddress()),
}); });
export default connect(select, perform)(WalletPage); export default connect(select, perform)(WalletAddress);

View file

@ -16,6 +16,11 @@ class WalletAddress extends React.PureComponent {
<h3>{__("Wallet Address")}</h3> <h3>{__("Wallet Address")}</h3>
</div> </div>
<div className="card__content"> <div className="card__content">
<p>
{__(
"Use this address to receive credits sent by another user (or yourself)."
)}
</p>
<Address address={receiveAddress} /> <Address address={receiveAddress} />
</div> </div>
<div className="card__actions"> <div className="card__actions">
@ -29,11 +34,6 @@ class WalletAddress extends React.PureComponent {
</div> </div>
<div className="card__content"> <div className="card__content">
<div className="help"> <div className="help">
<p>
{__(
'Other LBRY users may send credits to you by entering this address on the "Send" page.'
)}
</p>
<p> <p>
{__( {__(
"You can generate a new address at any time, and any previous addresses will continue to work. Using multiple addresses can be helpful for keeping track of incoming payments from multiple sources." "You can generate a new address at any time, and any previous addresses will continue to work. Using multiple addresses can be helpful for keeping track of incoming payments from multiple sources."

View file

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

View file

@ -0,0 +1,43 @@
import React from "react";
import Link from "component/link";
import { CreditAmount } from "component/common";
const WalletBalance = props => {
const { balance, navigate } = props;
/*
<div className="help">
<Link
onClick={() => navigate("/backup")}
label={__("Backup Your Wallet")}
/>
</div>
*/
return (
<section className="card">
<div className="card__title-primary">
<h3>{__("Balance")}</h3>
</div>
<div className="card__content">
{(balance || balance === 0) &&
<CreditAmount amount={balance} precision={8} />}
</div>
<div className="card__actions card__actions--bottom">
<Link
button="text"
navigate="/send"
disabled={balance === 0}
label={__("Send")}
/>
<Link button="text" navigate="/receive" label={__("Receive")} />
<Link
button="text"
disabled={balance === 0}
navigate="/backup"
label={__("Backup")}
/>
</div>
</section>
);
};
export default WalletBalance;

View file

@ -42,15 +42,15 @@ const WalletSend = props => {
onChange={setAddress} onChange={setAddress}
value={address} value={address}
/> />
</div> <div className="form-row-submit">
<div className="card__actions card__actions--form-submit"> <Link
<Link button="primary"
button="primary" label={__("Send")}
label={__("Send")} onClick={sendToAddress}
onClick={sendToAddress} disabled={!(parseFloat(amount) > 0.0) || !address}
disabled={!(parseFloat(amount) > 0.0) || !address} />
/> <input type="submit" className="hidden" />
<input type="submit" className="hidden" /> </div>
</div> </div>
</form> </form>
{modal == "insufficientBalance" && {modal == "insufficientBalance" &&

View file

@ -108,6 +108,15 @@ export const USER_IDENTITY_VERIFY_FAILURE = "USER_IDENTITY_VERIFY_FAILURE";
export const USER_FETCH_STARTED = "USER_FETCH_STARTED"; export const USER_FETCH_STARTED = "USER_FETCH_STARTED";
export const USER_FETCH_SUCCESS = "USER_FETCH_SUCCESS"; export const USER_FETCH_SUCCESS = "USER_FETCH_SUCCESS";
export const USER_FETCH_FAILURE = "USER_FETCH_FAILURE"; export const USER_FETCH_FAILURE = "USER_FETCH_FAILURE";
export const USER_INVITE_STATUS_FETCH_STARTED =
"USER_INVITE_STATUS_FETCH_STARTED";
export const USER_INVITE_STATUS_FETCH_SUCCESS =
"USER_INVITE_STATUS_FETCH_SUCCESS";
export const USER_INVITE_STATUS_FETCH_FAILURE =
"USER_INVITE_STATUS_FETCH_FAILURE";
export const USER_INVITE_NEW_STARTED = "USER_INVITE_NEW_STARTED";
export const USER_INVITE_NEW_SUCCESS = "USER_INVITE_NEW_SUCCESS";
export const USER_INVITE_NEW_FAILURE = "USER_INVITE_NEW_FAILURE";
export const FETCH_ACCESS_TOKEN_SUCCESS = "FETCH_ACCESS_TOKEN_SUCCESS"; export const FETCH_ACCESS_TOKEN_SUCCESS = "FETCH_ACCESS_TOKEN_SUCCESS";
// Rewards // Rewards

View file

@ -7,4 +7,5 @@ export const UPGRADE = "upgrade";
export const WELCOME = "welcome"; export const WELCOME = "welcome";
export const FIRST_REWARD = "first_reward"; export const FIRST_REWARD = "first_reward";
export const AUTHENTICATION_FAILURE = "auth_failure"; export const AUTHENTICATION_FAILURE = "auth_failure";
export const REWARD_APPROVAL_REQUIRED = "reward_approval_required";
export const CREDIT_INTRO = "credit_intro"; export const CREDIT_INTRO = "credit_intro";

View file

@ -1,5 +1,8 @@
/*hardcoded names still exist for these in reducers/settings.js - only discovered when debugging*/
/*Many settings are stored in the localStorage by their name -
be careful about changing the value of a settings constant, as doing so can invalidate existing settings*/
export const CREDIT_INTRO_ACKNOWLEDGED = "credit_intro_acknowledged"; export const CREDIT_INTRO_ACKNOWLEDGED = "credit_intro_acknowledged";
export const FIRST_RUN_ACKNOWLEDGED = "welcome_acknowledged"; export const NEW_USER_ACKNOWLEDGED = "welcome_acknowledged";
export const LANGUAGE = "language"; export const LANGUAGE = "language";
export const SHOW_NSFW = "showNsfw"; export const SHOW_NSFW = "showNsfw";
export const SHOW_UNAVAILABLE = "showUnavailable"; export const SHOW_UNAVAILABLE = "showUnavailable";

View file

@ -1,13 +1,13 @@
import React from "react"; import React from "react";
import rewards from "rewards";
import { connect } from "react-redux"; import { connect } from "react-redux";
import { doCloseModal, doAuthNavigate } from "actions/app"; import { doCloseModal, doAuthNavigate } from "actions/app";
import { doSetClientSetting } from "actions/settings"; import { doSetClientSetting } from "actions/settings";
import { selectUserIsRewardApproved } from "selectors/user"; import { selectUserIsRewardApproved } from "selectors/user";
import { selectBalance } from "selectors/wallet";
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";
@ -17,9 +17,9 @@ const select = (state, props) => {
selectReward = makeSelectRewardByType(); selectReward = makeSelectRewardByType();
return { return {
currentBalance: selectBalance(state),
isRewardApproved: selectUserIsRewardApproved(state), isRewardApproved: selectUserIsRewardApproved(state),
reward: selectReward(state, { reward_type: rewards.TYPE_NEW_USER }), totalRewardValue: selectUnclaimedRewardValue(state),
totalRewardValue: selectTotalRewardValue(state),
}; };
}; };

View file

@ -1,40 +1,64 @@
import React from "react"; import React from "react";
import { Modal } from "modal/modal"; import { Modal } from "modal/modal";
import { CreditAmount } from "component/common"; import { CreditAmount, CurrencySymbol } from "component/common";
import Link from "component/link/index"; import Link from "component/link/index";
import { formatCredits } from "util/formatCredits";
const ModalCreditIntro = props => { const ModalCreditIntro = props => {
const { closeModal, totalRewardValue, verifyAccount } = props; const { closeModal, currentBalance, totalRewardValue, verifyAccount } = props;
const totalRewardRounded = Math.round(totalRewardValue / 10) * 10; const totalRewardRounded = Math.round(totalRewardValue / 10) * 10;
return ( return (
<Modal type="custom" isOpen={true} contentLabel="Welcome to LBRY"> <Modal type="custom" isOpen={true} contentLabel="Welcome to LBRY">
<section> <section>
<h3 className="modal__header">{__("Claim Your Credits")}</h3> <h3 className="modal__header">{__("Blockchain 101")}</h3>
<p>
The LBRY network is controlled and powered by credits called{" "}
<em>LBC</em>, a blockchain asset.
</p>
<p>
{__("New patrons receive ")} {" "}
{totalRewardValue
? <CreditAmount amount={totalRewardRounded} />
: <span className="credit-amount">{__("credits")}</span>}
{" "} {__("in rewards for usage and influence of the network.")}
</p>
<p> <p>
LBRY is controlled and powered by a blockchain asset called {" "}
<em><CurrencySymbol /></em>.{" "}
<CurrencySymbol />{" "}
{__( {__(
"You'll also earn weekly bonuses for checking out the greatest new stuff." "is used to publish content, to have a say in the network rules, and to access paid content."
)} )}
</p> </p>
{currentBalance <= 0
? <div>
<p>
You currently have <CreditAmount amount={currentBalance} />, so
the actions you can take are limited.
</p>
<p>
However, there are a variety of ways to get credits, including
more than {" "}
{totalRewardValue
? <CreditAmount amount={totalRewardRounded} />
: <span className="credit-amount">{__("?? credits")}</span>}
{" "}{" "}
{__(
" in rewards available for being a proven human during the LBRY beta."
)}
</p>
</div>
: <div>
<p>
But you probably knew this, since you've already got{" "}
<CreditAmount amount={currentBalance} />.
</p>
</div>}
<div className="modal__buttons"> <div className="modal__buttons">
<Link <Link
button="primary" button="primary"
onClick={verifyAccount} onClick={verifyAccount}
label={__("You Had Me At Free LBC")} label={__("I'm Totally A Human")}
/>
<Link
button="alt"
onClick={closeModal}
label={
currentBalance <= 0 ? __("Use Without LBC") : __("Meh, Not Now")
}
/> />
<Link button="alt" onClick={closeModal} label={__("I Burn Money")} />
</div> </div>
</section> </section>
</Modal> </Modal>

View file

@ -7,7 +7,7 @@ const select = state => ({});
const perform = dispatch => ({ const perform = dispatch => ({
addFunds: () => { addFunds: () => {
dispatch(doNavigate("/rewards")); dispatch(doNavigate("/wallet"));
dispatch(doCloseModal()); dispatch(doCloseModal());
}, },
closeModal: () => dispatch(doCloseModal()), closeModal: () => dispatch(doCloseModal()),

View file

@ -1,5 +1,6 @@
import React from "react"; import React from "react";
import { Modal } from "modal/modal"; import { Modal } from "modal/modal";
import { CurrencySymbol } from "component/common";
class ModalInsufficientCredits extends React.PureComponent { class ModalInsufficientCredits extends React.PureComponent {
render() { render() {
@ -11,11 +12,12 @@ class ModalInsufficientCredits extends React.PureComponent {
type="confirm" type="confirm"
contentLabel={__("Not enough credits")} contentLabel={__("Not enough credits")}
confirmButtonLabel={__("Get Credits")} confirmButtonLabel={__("Get Credits")}
abortButtonLabel={__("Cancel")} abortButtonLabel={__("Not Now")}
onAborted={closeModal} onAborted={closeModal}
onConfirmed={addFunds} onConfirmed={addFunds}
> >
{__("More LBRY credits are required to purchase this.")} <h3 className="modal__header">{__("More Credits Required")}</h3>
<p>You'll need more <CurrencySymbol /> to do this.</p>
</Modal> </Modal>
); );
} }

View file

@ -0,0 +1,14 @@
import React from "react";
import { connect } from "react-redux";
import { doCloseModal, doAuthNavigate } from "actions/app";
import ModalRewardApprovalRequired from "./view";
const perform = dispatch => ({
doAuth: () => {
dispatch(doCloseModal());
dispatch(doAuthNavigate());
},
closeModal: () => dispatch(doCloseModal()),
});
export default connect(null, perform)(ModalRewardApprovalRequired);

View file

@ -0,0 +1,33 @@
import React from "react";
import { Modal } from "modal/modal";
class ModalRewardApprovalRequired extends React.PureComponent {
render() {
const { closeModal, doAuth } = this.props;
return (
<Modal
isOpen={true}
contentLabel={__("Human Verification Required")}
onConfirmed={doAuth}
onAborted={closeModal}
type="confirm"
confirmButtonLabel={__("I'm Totally Real")}
abortButtonLabel={__("Never Mind")}
>
<section>
<h3 className="modal__header">
{__("This is awkward. Are you real?")}
</h3>
<p>
{__(
"Before we can give you any credits, we need to perform a brief check to make sure you're a new and unique person."
)}
</p>
</section>
</Modal>
);
}
}
export default ModalRewardApprovalRequired;

View file

@ -1,20 +1,30 @@
import React from "react"; import React from "react";
import { connect } from "react-redux"; import { connect } from "react-redux";
import { selectCurrentModal } from "selectors/app"; import { selectCurrentModal, selectCurrentPage } from "selectors/app";
import { doOpenModal } from "actions/app"; import { doOpenModal } from "actions/app";
import { selectWelcomeModalAcknowledged } from "selectors/app"; import { makeSelectClientSetting } from "selectors/settings";
import { selectUser } from "selectors/user"; import { selectUser } from "selectors/user";
import { selectCostForCurrentPageUri } from "selectors/cost_info";
import * as settings from "constants/settings";
import { selectBalance } from "selectors/wallet";
import ModalRouter from "./view"; import ModalRouter from "./view";
import * as modals from "constants/modal_types";
const select = (state, props) => ({ const select = (state, props) => ({
balance: selectBalance(state),
showPageCost: selectCostForCurrentPageUri(state),
modal: selectCurrentModal(state), modal: selectCurrentModal(state),
isWelcomeAcknowledged: selectWelcomeModalAcknowledged(state), page: selectCurrentPage(state),
isWelcomeAcknowledged: makeSelectClientSetting(
settings.NEW_USER_ACKNOWLEDGED
)(state),
isCreditIntroAcknowledged: makeSelectClientSetting(
settings.CREDIT_INTRO_ACKNOWLEDGED
)(state),
user: selectUser(state), user: selectUser(state),
}); });
const perform = dispatch => ({ const perform = dispatch => ({
openWelcomeModal: () => dispatch(doOpenModal(modals.WELCOME)), openModal: modal => dispatch(doOpenModal(modal)),
}); });
export default connect(select, perform)(ModalRouter); export default connect(select, perform)(ModalRouter);

View file

@ -6,31 +6,95 @@ import ModalInsufficientCredits from "modal/modalInsufficientCredits";
import ModalUpgrade from "modal/modalUpgrade"; import ModalUpgrade from "modal/modalUpgrade";
import ModalWelcome from "modal/modalWelcome"; import ModalWelcome from "modal/modalWelcome";
import ModalFirstReward from "modal/modalFirstReward"; import ModalFirstReward from "modal/modalFirstReward";
import ModalRewardApprovalRequired from "modal/modalRewardApprovalRequired";
import * as modals from "constants/modal_types"; import * as modals from "constants/modal_types";
import ModalCreditIntro from "modal/modalCreditIntro"; import ModalCreditIntro from "modal/modalCreditIntro";
class ModalRouter extends React.PureComponent { class ModalRouter extends React.PureComponent {
constructor(props) {
super(props);
this.state = {
lastTransitionModal: null,
lastTransitionPage: null,
};
}
componentWillMount() { componentWillMount() {
this.showWelcome(this.props); this.showTransitionModals(this.props);
} }
componentWillReceiveProps(nextProps) { componentWillReceiveProps(nextProps) {
this.showWelcome(nextProps); this.showTransitionModals(nextProps);
} }
showWelcome(props) { showTransitionModals(props) {
const { isWelcomeAcknowledged, openWelcomeModal, user } = props; const { modal, openModal, page } = props;
if (modal) {
return;
}
const transitionModal = [
this.checkShowWelcome,
this.checkShowCreditIntro,
this.checkShowInsufficientCredits,
].reduce((acc, func) => {
return !acc ? func.bind(this)(props) : acc;
}, false);
if (
transitionModal &&
(transitionModal != this.state.lastTransitionModal ||
page != this.state.lastTransitionPage)
) {
openModal(transitionModal);
this.setState({
lastTransitionModal: transitionModal,
lastTransitionPage: page,
});
}
}
checkShowWelcome(props) {
const { isWelcomeAcknowledged, user } = props;
if ( if (
!isWelcomeAcknowledged && !isWelcomeAcknowledged &&
user && user &&
!user.is_reward_approved && !user.is_reward_approved &&
!user.is_identity_verified !user.is_identity_verified
) { ) {
openWelcomeModal(); return modals.WELCOME;
} }
} }
checkShowCreditIntro(props) {
const { page, isCreditIntroAcknowledged, user } = props;
if (
!isCreditIntroAcknowledged &&
user &&
!user.is_reward_approved &&
(["rewards", "send", "receive", "publish", "wallet"].includes(page) ||
this.isPaidShowPage(props))
) {
return modals.CREDIT_INTRO;
}
}
checkShowInsufficientCredits(props) {
const { balance, page } = props;
if (balance <= 0 && ["send", "publish"].includes(page)) {
return modals.INSUFFICIENT_CREDITS;
}
}
isPaidShowPage(props) {
const { page, showPageCost } = props;
return page === "show" && showPageCost > 0;
}
render() { render() {
const { modal } = this.props; const { modal } = this.props;
@ -51,6 +115,8 @@ class ModalRouter extends React.PureComponent {
return <ModalAuthFailure />; return <ModalAuthFailure />;
case modals.CREDIT_INTRO: case modals.CREDIT_INTRO:
return <ModalCreditIntro />; return <ModalCreditIntro />;
case modals.REWARD_APPROVAL_REQUIRED:
return <ModalRewardApprovalRequired />;
default: default:
return null; return null;
} }

View file

@ -8,9 +8,8 @@ import ModalWelcome from "./view";
const perform = dispatch => () => ({ const perform = dispatch => () => ({
closeModal: () => { closeModal: () => {
dispatch(doSetClientSetting(settings.FIRST_RUN_ACKNOWLEDGED, true)); dispatch(doSetClientSetting(settings.NEW_USER_ACKNOWLEDGED, true));
dispatch(doCloseModal()); dispatch(doCloseModal());
dispatch(doOpenModal(modals.CREDIT_INTRO));
}, },
}); });

View file

@ -22,7 +22,11 @@ const ModalWelcome = props => {
)} )}
</p> </p>
<div className="modal__buttons"> <div className="modal__buttons">
<Link button="primary" onClick={closeModal} label={__("Continue")} /> <Link
button="primary"
onClick={closeModal}
label={__("Blockchain Centaurs? I'm In")}
/>
</div> </div>
</section> </section>
</Modal> </Modal>

View file

@ -30,11 +30,11 @@ export class AuthPage extends React.PureComponent {
const { email, isPending, isVerificationCandidate, user } = this.props; const { email, isPending, isVerificationCandidate, user } = this.props;
if (isPending || (user && !user.has_verified_email && !email)) { if (isPending || (user && !user.has_verified_email && !email)) {
return __("Welcome to LBRY"); return __("Human Proofing");
} else if (user && !user.has_verified_email) { } else if (user && !user.has_verified_email) {
return __("Confirm Email"); return __("Confirm Email");
} else if (user && !user.is_identity_verified && !user.is_reward_approved) { } else if (user && !user.is_identity_verified && !user.is_reward_approved) {
return __("Confirm Identity"); return __("Final Verification");
} else { } else {
return __("Welcome to LBRY"); return __("Welcome to LBRY");
} }
@ -44,51 +44,45 @@ export class AuthPage extends React.PureComponent {
const { email, isPending, isVerificationCandidate, user } = this.props; const { email, isPending, isVerificationCandidate, user } = this.props;
if (isPending) { if (isPending) {
return <BusyMessage message={__("Authenticating")} />; return [<BusyMessage message={__("Authenticating")} />, true];
} else if (user && !user.has_verified_email && !email) { } else if (user && !user.has_verified_email && !email) {
return <UserEmailNew />; return [<UserEmailNew />, true];
} else if (user && !user.has_verified_email) { } else if (user && !user.has_verified_email) {
return <UserEmailVerify />; return [<UserEmailVerify />, true];
} else if (user && !user.is_identity_verified) { } else if (user && !user.is_identity_verified) {
return <UserVerify />; return [<UserVerify />, false];
} else { } else {
return <span className="empty">{__("No further steps.")}</span>; return [<span className="empty">{__("No further steps.")}</span>, true];
} }
} }
render() { render() {
const { email, user, isPending, navigate } = this.props; const { email, user, isPending, navigate } = this.props;
const [innerContent, useTemplate] = this.renderMain();
return ( return useTemplate
<main className=""> ? <main>
<section className="card card--form"> <section className="card card--form">
<div className="card__title-primary"> <div className="card__title-primary">
<h1>{this.getTitle()}</h1> <h1>{this.getTitle()}</h1>
</div>
<div className="card__content">
{!isPending &&
!email &&
user &&
!user.has_verified_email &&
<p>
{__("Create a verified identity and receive LBC rewards.")}
</p>}
{this.renderMain()}
</div>
<div className="card__content">
<div className="help">
{__(
"This information is disclosed only to LBRY, Inc. and not to the LBRY network. It is only required to earn LBRY rewards."
) + " "}
<Link
onClick={() => navigate("/discover")}
label={__("Return home")}
/>.
</div> </div>
</div> <div className="card__content">
</section> {innerContent}
</main> </div>
); <div className="card__content">
<div className="help">
{__(
"This information is disclosed only to LBRY, Inc. and not to the LBRY network. It is only required to earn LBRY rewards."
) + " "}
<Link
onClick={() => navigate("/discover")}
label={__("Return home")}
/>.
</div>
</div>
</section>
</main>
: innerContent;
} }
} }

View file

@ -1,5 +1,5 @@
import React from "react"; import React from "react";
import { doNavigate } from "actions/app"; import { doAuthNavigate } from "actions/app";
import { connect } from "react-redux"; import { connect } from "react-redux";
import { doFetchAccessToken } from "actions/user"; import { doFetchAccessToken } from "actions/user";
import { selectAccessToken, selectUser } from "selectors/user"; import { selectAccessToken, selectUser } from "selectors/user";
@ -11,7 +11,7 @@ const select = state => ({
}); });
const perform = dispatch => ({ const perform = dispatch => ({
navigate: (path, params) => dispatch(doNavigate(path, params)), doAuth: () => dispatch(doAuthNavigate("/help")),
fetchAccessToken: () => dispatch(doFetchAccessToken()), fetchAccessToken: () => dispatch(doFetchAccessToken()),
}); });

View file

@ -3,7 +3,7 @@ import React from "react";
import lbry from "lbry.js"; import lbry from "lbry.js";
import Link from "component/link"; import Link from "component/link";
import SubHeader from "component/subHeader"; import SubHeader from "component/subHeader";
import { BusyMessage } from "component/common"; import { BusyMessage, Icon } from "component/common";
class HelpPage extends React.PureComponent { class HelpPage extends React.PureComponent {
constructor(props) { constructor(props) {
@ -50,7 +50,7 @@ class HelpPage extends React.PureComponent {
render() { render() {
let ver, osName, platform, newVerLink; let ver, osName, platform, newVerLink;
const { navigate, user } = this.props; const { doAuth, user } = this.props;
if (this.state.versionInfo) { if (this.state.versionInfo) {
ver = this.state.versionInfo; ver = this.state.versionInfo;
@ -119,7 +119,7 @@ class HelpPage extends React.PureComponent {
<p>{__("Did you find something wrong?")}</p> <p>{__("Did you find something wrong?")}</p>
<p> <p>
<Link <Link
onClick={() => navigate("report")} navigate="/report"
label={__("Submit a Bug Report")} label={__("Submit a Bug Report")}
icon="icon-bug" icon="icon-bug"
button="alt" button="alt"
@ -143,7 +143,7 @@ class HelpPage extends React.PureComponent {
</p> </p>
: <p>{__("Your copy of LBRY is up to date.")}</p>} : <p>{__("Your copy of LBRY is up to date.")}</p>}
{this.state.uiVersion && ver {this.state.uiVersion && ver
? <table className="table-standard"> ? <table className="table-standard table-stretch table-standard--definition-list">
<tbody> <tbody>
<tr> <tr>
<th>{__("App")}</th> <th>{__("App")}</th>
@ -162,7 +162,21 @@ class HelpPage extends React.PureComponent {
<td> <td>
{user && user.primary_email {user && user.primary_email
? user.primary_email ? user.primary_email
: <span className="empty">{__("none")}</span>} : <span>
<span className="empty">{__("none")} </span>
(<Link
onClick={() => doAuth()}
label={__("set email")}
/>)
</span>}
</td>
</tr>
<tr>
<th>{__("Reward Eligible")}</th>
<td>
{user && user.is_reward_approved
? <Icon icon="icon-check" />
: <Icon icon="icon-ban" />}
</td> </td>
</tr> </tr>
<tr> <tr>

View file

@ -0,0 +1,19 @@
import React from "react";
import { connect } from "react-redux";
import InvitePage from "./view";
import { doFetchInviteStatus } from "actions/user";
import {
selectUserInviteStatusFailed,
selectUserInviteStatusIsPending,
} from "selectors/user";
const select = state => ({
isFailed: selectUserInviteStatusFailed(state),
isPending: selectUserInviteStatusIsPending(state),
});
const perform = dispatch => ({
fetchInviteStatus: () => dispatch(doFetchInviteStatus()),
});
export default connect(select, perform)(InvitePage);

View file

@ -0,0 +1,32 @@
import React from "react";
import { BusyMessage } from "component/common";
import SubHeader from "component/subHeader";
import InviteNew from "component/inviteNew";
import InviteList from "component/inviteList";
class InvitePage extends React.PureComponent {
componentWillMount() {
this.props.fetchInviteStatus();
}
render() {
const { isPending, isFailed } = this.props;
return (
<main className="main--single-column">
<SubHeader />
{isPending &&
<BusyMessage message={__("Checking your invite status")} />}
{!isPending &&
isFailed &&
<span className="empty">
{__("Failed to retrieve invite status.")}
</span>}
{!isPending && !isFailed && <InviteNew />}
{!isPending && !isFailed && <InviteList />}
</main>
);
}
}
export default InvitePage;

View file

@ -16,10 +16,12 @@ import {
doCreateChannel, doCreateChannel,
doPublish, doPublish,
} from "actions/content"; } from "actions/content";
import { selectBalance } from "selectors/wallet";
import rewards from "rewards"; import rewards from "rewards";
import PublishPage from "./view"; import PublishPage from "./view";
const select = state => ({ const select = state => ({
balance: selectBalance(state),
myClaims: selectMyClaims(state), myClaims: selectMyClaims(state),
fetchingChannels: selectFetchingMyChannels(state), fetchingChannels: selectFetchingMyChannels(state),
channels: selectMyChannelClaims(state), channels: selectMyChannelClaims(state),

View file

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

View file

@ -0,0 +1,34 @@
import React from "react";
import SubHeader from "component/subHeader";
import Link from "component/link";
import WalletAddress from "component/walletAddress";
const ReceiveCreditsPage = props => {
return (
<main className="main--single-column">
<SubHeader />
<WalletAddress />
<section className="card">
<div className="card__title-primary">
<h3>{__("Where To Find Credits")}</h3>
</div>
<div className="card__content">
<p>
{
"LBRY credits can be purchased on exchanges, earned for contributions, for mining, and more."
}
</p>
</div>
<div className="card__actions">
<Link
button="alt"
href="https://lbry.io/faq/earn-credits"
label={__("Read More")}
/>
</div>
</section>
</main>
);
};
export default ReceiveCreditsPage;

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,20 +31,27 @@ class RewardsPage extends React.PureComponent {
!user.has_verified_email || !user.has_verified_email ||
!user.is_identity_verified !user.is_identity_verified
) { ) {
cardHeader = ( return (
<div> <section className="card">
<div className="card__title-primary">
<h3>{__("Humans Only")}</h3>
</div>
<div className="card__content empty"> <div className="card__content empty">
<p> <p>
{__("Only verified accounts are eligible to earn rewards.")} {__("Rewards are for human beings only.")}
{" "}
{__(
"You'll have to prove you're one of us before you can claim any rewards."
)}
</p> </p>
</div> </div>
<div className="card__content"> <div className="card__content">
<Link onClick={doAuth} button="primary" label="Become Verified" /> <Link onClick={doAuth} button="primary" label="Prove Humanity" />
</div> </div>
</div> </section>
); );
} else { } else {
cardHeader = ( return (
<div className="card__content"> <div className="card__content">
<p> <p>
{__( {__(
@ -122,25 +83,52 @@ 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 = ( return (
<div> <div className="card__content empty">
<div className="card__content empty"> <p>
<p> {__(
{__( "This application is unable to earn rewards due to an authentication failure."
"This application is unable to earn rewards due to an authentication failure." )}
)} </p>
</p> </div>
</div> );
} else if (!rewards || rewards.length <= 0) {
return (
<div className="card__content empty">
{__("Failed to load rewards.")}
</div>
);
} else {
return (
<div className="card-grid">
{rewards.map(reward =>
<RewardTile key={reward.reward_type} reward={reward} />
)}
</div> </div>
); );
} }
}
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 SendCreditsPage from "./view";
export default connect(null, null)(SendCreditsPage);

View file

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

View file

@ -2,7 +2,7 @@ import React from "react";
import lbryuri from "lbryuri"; import lbryuri from "lbryuri";
import { BusyMessage } from "component/common"; import { BusyMessage } from "component/common";
import ChannelPage from "page/channel"; import ChannelPage from "page/channel";
import FilePage from "page/filePage"; import FilePage from "page/file";
class ShowPage extends React.PureComponent { class ShowPage extends React.PureComponent {
componentWillMount() { componentWillMount() {

View file

@ -0,0 +1,19 @@
import React from "react";
import { connect } from "react-redux";
import { doFetchTransactions } from "actions/wallet";
import {
selectTransactionItems,
selectIsFetchingTransactions,
} from "selectors/wallet";
import TransactionHistoryPage from "./view";
const select = state => ({
fetchingTransactions: selectIsFetchingTransactions(state),
transactions: selectTransactionItems(state),
});
const perform = dispatch => ({
fetchTransactions: () => dispatch(doFetchTransactions()),
});
export default connect(select, perform)(TransactionHistoryPage);

View file

@ -0,0 +1,32 @@
import React from "react";
import { BusyMessage } from "component/common";
import SubHeader from "component/subHeader";
import TransactionList from "component/transactionList";
class TransactionHistoryPage extends React.PureComponent {
componentWillMount() {
this.props.fetchTransactions();
}
render() {
const { fetchingTransactions, transactions } = this.props;
return (
<main className="main--single-column">
<SubHeader />
<section className="card">
<div className="card__title-primary">
<h3>{__("Transaction History")}</h3>
</div>
<div className="card__content">
{fetchingTransactions &&
<BusyMessage message={__("Loading transactions")} />}
{!fetchingTransactions &&
<TransactionList transactions={transactions} />}
</div>
</section>
</main>
);
}
}
export default TransactionHistoryPage;

View file

@ -1,17 +1,5 @@
import React from "react"; import React from "react";
import { connect } from "react-redux"; import { connect } from "react-redux";
import { doNavigate } from "actions/app";
import { selectCurrentPage } from "selectors/app";
import { selectBalance } from "selectors/wallet";
import WalletPage from "./view"; import WalletPage from "./view";
const select = state => ({ export default connect(null, null)(WalletPage);
currentPage: selectCurrentPage(state),
balance: selectBalance(state),
});
const perform = dispatch => ({
navigate: path => dispatch(doNavigate(path)),
});
export default connect(select, perform)(WalletPage);

View file

@ -1,36 +1,18 @@
import React from "react"; import React from "react";
import SubHeader from "component/subHeader"; import SubHeader from "component/subHeader";
import TransactionList from "component/transactionList"; import WalletBalance from "component/walletBalance";
import WalletAddress from "component/walletAddress"; import RewardSummary from "component/rewardSummary";
import WalletSend from "component/walletSend"; import TransactionListRecent from "component/transactionListRecent";
import Link from "component/link";
import { CreditAmount } from "component/common";
const WalletPage = props => { const WalletPage = props => {
const { balance, currentPage, navigate } = props;
return ( return (
<main className="main--single-column"> <main className="main--single-column page--wallet">
<SubHeader /> <SubHeader />
<section className="card"> <div className="card-grid">
<div className="card__title-primary"> <WalletBalance />
<h3>{__("Balance")}</h3> <RewardSummary />
</div> </div>
<div className="card__content"> <TransactionListRecent />
<CreditAmount amount={balance} precision={8} />
</div>
<div className="card__content">
<div className="help">
<Link
onClick={() => navigate("/backup")}
label={__("Backup Your Wallet")}
/>
</div>
</div>
</section>
{currentPage === "wallet" ? <TransactionList {...props} /> : ""}
{currentPage === "send" ? <WalletSend {...props} /> : ""}
{currentPage === "receive" ? <WalletAddress /> : ""}
</main> </main>
); );
}; };

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

@ -1,4 +1,5 @@
import * as types from "constants/action_types"; import * as types from "constants/action_types";
import * as settings from "constants/settings";
import LANGUAGES from "constants/languages"; import LANGUAGES from "constants/languages";
import lbry from "lbry"; import lbry from "lbry";
@ -6,7 +7,11 @@ const reducers = {};
const defaultState = { const defaultState = {
clientSettings: { clientSettings: {
showNsfw: lbry.getClientSetting("showNsfw"), showNsfw: lbry.getClientSetting("showNsfw"),
language: lbry.getClientSetting("language"), welcome_acknowledged: lbry.getClientSetting(settings.NEW_USER_ACKNOWLEDGED),
credit_intro_acknowledged: lbry.getClientSetting(
settings.CREDIT_INTRO_ACKNOWLEDGED
),
language: lbry.getClientSetting(settings.LANGUAGE),
}, },
languages: {}, languages: {},
}; };

View file

@ -8,6 +8,11 @@ const defaultState = {
emailNewIsPending: false, emailNewIsPending: false,
emailNewErrorMessage: "", emailNewErrorMessage: "",
emailToVerify: "", emailToVerify: "",
inviteNewErrorMessage: "",
inviteNewIsPending: false,
inviteStatusIsPending: false,
invitesRemaining: undefined,
invitees: undefined,
user: undefined, user: undefined,
}; };
@ -142,6 +147,49 @@ reducers[types.FETCH_ACCESS_TOKEN_SUCCESS] = function(state, action) {
}); });
}; };
reducers[types.USER_INVITE_STATUS_FETCH_STARTED] = function(state, action) {
return Object.assign({}, state, {
inviteStatusIsPending: true,
});
};
reducers[types.USER_INVITE_STATUS_FETCH_SUCCESS] = function(state, action) {
return Object.assign({}, state, {
inviteStatusIsPending: false,
invitesRemaining: action.data.invitesRemaining,
invitees: action.data.invitees,
});
};
reducers[types.USER_INVITE_NEW_STARTED] = function(state, action) {
return Object.assign({}, state, {
inviteNewIsPending: true,
inviteNewErrorMessage: "",
});
};
reducers[types.USER_INVITE_NEW_SUCCESS] = function(state, action) {
return Object.assign({}, state, {
inviteNewIsPending: false,
inviteNewErrorMessage: "",
});
};
reducers[types.USER_INVITE_NEW_FAILURE] = function(state, action) {
return Object.assign({}, state, {
inviteNewIsPending: false,
inviteNewErrorMessage: action.data.error.message,
});
};
reducers[types.USER_INVITE_STATUS_FETCH_FAILURE] = function(state, action) {
return Object.assign({}, state, {
inviteStatusIsPending: false,
invitesRemaining: null,
invitees: null,
});
};
export default function reducer(state = defaultState, action) { export default function reducer(state = defaultState, action) {
const handler = reducers[action.type]; const handler = reducers[action.type];
if (handler) return handler(state, action); if (handler) return handler(state, action);

View file

@ -95,6 +95,18 @@ rewards.TYPE_FIRST_STREAM = "first_stream";
rewards.TYPE_MANY_DOWNLOADS = "many_downloads"; rewards.TYPE_MANY_DOWNLOADS = "many_downloads";
rewards.TYPE_FIRST_PUBLISH = "first_publish"; rewards.TYPE_FIRST_PUBLISH = "first_publish";
rewards.TYPE_FEATURED_DOWNLOAD = "featured_download"; rewards.TYPE_FEATURED_DOWNLOAD = "featured_download";
rewards.TYPE_REFERRAL = "referral";
rewards.SORT_ORDER = [
rewards.TYPE_NEW_USER,
rewards.TYPE_CONFIRM_EMAIL,
rewards.TYPE_FIRST_STREAM,
rewards.TYPE_FIRST_CHANNEL,
rewards.TYPE_FIRST_PUBLISH,
rewards.TYPE_FEATURED_DOWNLOAD,
rewards.TYPE_MANY_DOWNLOADS,
rewards.TYPE_REFERRAL,
rewards.TYPE_NEW_DEVELOPER,
];
rewards.claimReward = function(type) { rewards.claimReward = function(type) {
function requestReward(resolve, reject, params) { function requestReward(resolve, reject, params) {

View file

@ -1,7 +1,6 @@
import { createSelector } from "reselect"; import { createSelector } from "reselect";
import { parseQueryParams, toQueryString } from "util/query_params"; import { parseQueryParams, toQueryString } from "util/query_params";
import * as settings from "constants/settings.js"; import * as settings from "constants/settings.js";
import lbry from "lbry";
import lbryuri from "lbryuri"; import lbryuri from "lbryuri";
export const _selectState = state => state.app || {}; export const _selectState = state => state.app || {};
@ -39,13 +38,15 @@ export const selectPageTitle = createSelector(
case "wallet": case "wallet":
return __("Wallet"); return __("Wallet");
case "send": case "send":
return __("Send"); return __("Send Credits");
case "receive": case "receive":
return __("Receive"); return __("Wallet Address");
case "backup": case "backup":
return __("Backup"); return __("Backup Your Wallet");
case "rewards": case "rewards":
return __("Rewards"); return __("Rewards");
case "invite":
return __("Invites");
case "start": case "start":
return __("Start"); return __("Start");
case "publish": case "publish":
@ -72,8 +73,12 @@ export const selectPageTitle = createSelector(
return __("Publishes"); return __("Publishes");
case "discover": case "discover":
return __("Home"); return __("Home");
default: case false:
case null:
case "":
return ""; return "";
default:
return page[0].toUpperCase() + (page.length > 0 ? page.substr(1) : "");
} }
} }
); );
@ -136,15 +141,19 @@ 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 "rewards": case "rewards":
case "backup": case "backup":
return { return {
wallet: __("Overview"), wallet: __("Overview"),
history: __("History"),
send: __("Send"), send: __("Send"),
receive: __("Receive"), receive: __("Receive"),
rewards: __("Rewards"), rewards: __("Rewards"),
invite: __("Invites"),
}; };
case "downloaded": case "downloaded":
case "published": case "published":
@ -203,16 +212,6 @@ export const selectSnackBarSnacks = createSelector(
snackBar => snackBar.snacks || [] snackBar => snackBar.snacks || []
); );
export const selectCreditsIntroAcknowledged = createSelector(
_selectState,
state => lbry.getClientSetting(settings.CREDIT_INTRO_ACKNOWLEDGED)
);
export const selectWelcomeModalAcknowledged = createSelector(
_selectState,
state => lbry.getClientSetting(settings.FIRST_RUN_ACKNOWLEDGED)
);
export const selectBadgeNumber = createSelector( export const selectBadgeNumber = createSelector(
_selectState, _selectState,
state => state.badgeNumber state => state.badgeNumber

View file

@ -1,4 +1,5 @@
import { createSelector } from "reselect"; import { createSelector } from "reselect";
import { selectCurrentParams } from "./app";
export const _selectState = state => state.costInfo || {}; export const _selectState = state => state.costInfo || {};
@ -15,6 +16,13 @@ export const makeSelectCostInfoForUri = () => {
return createSelector(selectCostInfoForUri, costInfo => costInfo); return createSelector(selectCostInfoForUri, costInfo => costInfo);
}; };
export const selectCostForCurrentPageUri = createSelector(
selectAllCostInfoByUri,
selectCurrentParams,
(costInfo, params) =>
params.uri && costInfo[params.uri] ? costInfo[params.uri].cost : undefined
);
export const selectFetchingCostInfo = createSelector( export const selectFetchingCostInfo = createSelector(
_selectState, _selectState,
state => state.fetching || {} state => state.fetching || {}

View file

@ -1,16 +1,33 @@
import { createSelector } from "reselect"; import { createSelector } from "reselect";
import { selectUser } from "selectors/user"; import { selectUser } from "selectors/user";
import rewards from "rewards";
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,
byType => Object.values(byType) || [] state => state.claimedRewardsById
);
export const selectClaimedRewards = createSelector(
selectClaimedRewardsById,
byId => Object.values(byId) || []
);
export const selectUnclaimedRewards = createSelector(
selectUnclaimedRewardsByType,
byType =>
Object.values(byType).sort(function(a, b) {
return rewards.SORT_ORDER.indexOf(a.reward_type) <
rewards.SORT_ORDER.indexOf(b.reward_type)
? -1
: 1;
}) || []
); );
export const selectIsRewardEligible = createSelector( export const selectIsRewardEligible = createSelector(
@ -23,10 +40,12 @@ export const selectFetchingRewards = createSelector(
state => !!state.fetching state => !!state.fetching
); );
export const selectTotalRewardValue = createSelector(selectRewards, rewards => export const selectUnclaimedRewardValue = createSelector(
rewards.reduce((sum, reward) => { selectUnclaimedRewards,
return sum + reward.reward_amount; rewards =>
}, 0) rewards.reduce((sum, reward) => {
return sum + reward.reward_amount;
}, 0)
); );
export const selectHasClaimedReward = (state, props) => { export const selectHasClaimedReward = (state, props) => {
@ -65,9 +84,16 @@ 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 = () => {
return createSelector(selectRewardByType, reward => reward); return createSelector(selectRewardByType, reward => reward);
}; };
export const makeSelectRewardAmountByType = () => {
return createSelector(
selectRewardByType,
reward => (reward ? reward.reward_amount : 0)
);
};

View file

@ -38,6 +38,8 @@ export const selectWunderBarAddress = createSelector(
export const selectWunderBarIcon = createSelector(selectCurrentPage, page => { export const selectWunderBarIcon = createSelector(selectCurrentPage, page => {
switch (page) { switch (page) {
case "auth":
return "icon-user";
case "search": case "search":
return "icon-search"; return "icon-search";
case "settings": case "settings":
@ -52,22 +54,29 @@ export const selectWunderBarIcon = createSelector(selectCurrentPage, page => {
return "icon-folder"; return "icon-folder";
case "start": case "start":
return "icon-file"; return "icon-file";
case "rewards": case "history":
return "icon-bank"; return "icon-history";
case "wallet":
case "send": case "send":
return "icon-send";
case "rewards":
return "icon-rocket";
case "invite":
return "icon-envelope-open";
case "address":
case "receive": case "receive":
return "icon-address-book";
case "wallet":
case "backup": case "backup":
return "icon-bank"; return "icon-bank";
case "show": case "show":
return "icon-file"; return "icon-file";
case "publish": case "publish":
return "icon-upload"; return "icon-upload";
case "developer":
return "icon-file";
case "developer": case "developer":
return "icon-code"; return "icon-code";
case "discover": case "discover":
return "icon-home"; return "icon-home";
default:
return "icon-file";
} }
}); });

View file

@ -12,6 +12,13 @@ export const selectClientSettings = createSelector(
state => state.clientSettings || {} state => state.clientSettings || {}
); );
export const makeSelectClientSetting = setting => {
return createSelector(
selectClientSettings,
settings => (settings ? settings[setting] : undefined)
);
};
export const selectSettingsIsGenerous = createSelector( export const selectSettingsIsGenerous = createSelector(
selectDaemonSettings, selectDaemonSettings,
settings => settings && settings.is_generous_host settings => settings && settings.is_generous_host

View file

@ -68,3 +68,33 @@ export const selectAccessToken = createSelector(
_selectState, _selectState,
state => state.accessToken state => state.accessToken
); );
export const selectUserInviteStatusIsPending = createSelector(
_selectState,
state => state.inviteStatusIsPending
);
export const selectUserInvitesRemaining = createSelector(
_selectState,
state => state.invitesRemaining
);
export const selectUserInvitees = createSelector(
_selectState,
state => state.invitees
);
export const selectUserInviteStatusFailed = createSelector(
selectUserInvitesRemaining,
inviteStatus => selectUserInvitesRemaining === null
);
export const selectUserInviteNewIsPending = createSelector(
_selectState,
state => state.inviteNewIsPending
);
export const selectUserInviteNewErrorMessage = createSelector(
_selectState,
state => state.inviteNewErrorMessage
);

View file

@ -35,6 +35,24 @@ export const selectTransactionItems = createSelector(
} }
); );
export const selectRecentTransactions = createSelector(
selectTransactionItems,
transactions => {
let threshold = new Date();
threshold.setDate(threshold.getDate() - 7);
return transactions.filter(transaction => {
return transaction.date > threshold;
});
}
);
export const selectHasTransactions = createSelector(
selectTransactionItems,
transactions => {
return transactions && transactions.length > 0;
}
);
export const selectIsFetchingTransactions = createSelector( export const selectIsFetchingTransactions = createSelector(
_selectState, _selectState,
state => state.fetchingTransactions state => state.fetchingTransactions

View file

@ -2,8 +2,8 @@
$spacing-vertical: 24px; $spacing-vertical: 24px;
$padding-button: 12px; $padding-button: $spacing-vertical * 2/3;
$padding-text-link: 4px; $padding-text-link: 0px;
$color-primary: #155B4A; $color-primary: #155B4A;
$color-primary-light: saturate(lighten($color-primary, 50%), 20%); $color-primary-light: saturate(lighten($color-primary, 50%), 20%);

View file

@ -134,15 +134,6 @@ p
} }
} }
/*should this be here or work this way? had to hack additional rule below*/
.icon:only-child {
position: relative;
top: 0.16em;
}
.icon-featured > .icon {
top: 0;
}
.help { .help {
font-size: .85em; font-size: .85em;
color: $color-help; color: $color-help;

View file

@ -2,8 +2,8 @@
@font-face { @font-face {
font-family: 'FontAwesome'; font-family: 'FontAwesome';
src: url('../font/fontawesome-webfont.eot?v=4.3.0'); src: url('../font/fontawesome-webfont.eot?v=4.7.0');
src: url('../font/fontawesome-webfont.eot?#iefix&v=4.3.0') format('embedded-opentype'), url('../font/fontawesome-webfont.woff2?v=4.3.0') format('woff2'), url('../font/fontawesome-webfont.woff?v=4.3.0') format('woff'), url('../font/fontawesome-webfont.ttf?v=4.3.0') format('truetype'), url('../font/fontawesome-webfont.svg?v=4.3.0#fontawesomeregular') format('svg'); src: url('../font/fontawesome-webfont.eot?#iefix&v=4.7.0') format('embedded-opentype'), url('../font/fontawesome-webfont.woff2?v=4.7.0') format('woff2'), url('../font/fontawesome-webfont.woff?v=4.7.0') format('woff'), url('../font/fontawesome-webfont.ttf?v=4.7.0') format('truetype'), url('../font/fontawesome-webfont.svg?v=4.7.0#fontawesomeregular') format('svg');
font-weight: normal; font-weight: normal;
font-style: normal; font-style: normal;
} }

View file

@ -8,7 +8,7 @@ $button-focus-shift: 12%;
+ .button-set-item + .button-set-item
{ {
margin-left: $padding-button; margin-left: $spacing-vertical;
} }
} }

View file

@ -23,10 +23,11 @@ $width-card-small: $spacing-vertical * 10;
} }
.card__title-primary, .card__title-primary,
.card__title-identity, .card__title-identity,
.card__actions,
.card__content, .card__content,
.card__subtext { .card__subtext,
padding: 0 $padding-card-horizontal; .card__actions {
padding-left: $padding-card-horizontal;
padding-right: $padding-card-horizontal;
} }
.card--small { .card--small {
.card__title-primary, .card__title-primary,
@ -39,6 +40,7 @@ $width-card-small: $spacing-vertical * 10;
} }
.card__title-primary { .card__title-primary {
margin-top: $spacing-vertical * 2/3; margin-top: $spacing-vertical * 2/3;
margin-bottom: $spacing-vertical * 2/3;
} }
.card__title-identity { .card__title-identity {
margin-top: $spacing-vertical * 1/3; margin-top: $spacing-vertical * 1/3;
@ -46,13 +48,6 @@ $width-card-small: $spacing-vertical * 10;
} }
.card__actions { .card__actions {
margin-top: $spacing-vertical * 2/3; margin-top: $spacing-vertical * 2/3;
}
.card__actions--bottom {
margin-top: $spacing-vertical * 1/3;
margin-bottom: $spacing-vertical * 1/3;
}
.card__actions--form-submit {
margin-top: $spacing-vertical;
margin-bottom: $spacing-vertical * 2/3; margin-bottom: $spacing-vertical * 2/3;
} }
.card__content { .card__content {
@ -259,4 +254,21 @@ $padding-right-card-hover-hack: 30px;
.card__icon-featured-content { .card__icon-featured-content {
color: orangered; color: orangered;
}
/*
if we keep doing things like this, we should add a real grid system, but I'm going to be a selective dick about it - Jeremy
*/
.card-grid {
$margin-card-grid: $spacing-vertical * 2/3;
display:flex;
flex-wrap: wrap;
> .card {
width: $width-page-constrained / 2 - $margin-card-grid / 2;
flex-grow:1;
}
> .card:nth-of-type(2n - 1) {
margin-right: $margin-card-grid;
}
} }

View file

@ -24,6 +24,9 @@ table.table-standard {
img { img {
vertical-align: text-bottom; vertical-align: text-bottom;
} }
&.text-center {
text-align: center;
}
} }
tr.thead:not(:first-child) th { tr.thead:not(:first-child) th {
border-top: 1px solid #e2e2e2; border-top: 1px solid #e2e2e2;
@ -49,6 +52,11 @@ table.table-standard {
} }
} }
} }
.table-standard--definition-list {
th {
text-align: right;
}
}
table.table-stretch { table.table-stretch {
width: 100%; width: 100%;