From 09ecae7e0dc8ad8c2611e1ffdb518be98167bd88 Mon Sep 17 00:00:00 2001 From: Jeremy Kauffman Date: Thu, 8 Jun 2017 17:15:34 -0400 Subject: [PATCH] reviewable --- ui/js/actions/content.js | 5 +- ui/js/actions/rewards.js | 106 +++++-- ui/js/actions/user.js | 26 +- ui/js/component/authOverlay/index.jsx | 2 + ui/js/component/authOverlay/view.jsx | 19 +- ui/js/component/form.js | 14 +- ui/js/component/reward-link.js | 109 ------- ui/js/component/rewardLink/index.js | 4 +- ui/js/component/userEmailNew/view.jsx | 56 ++-- ui/js/component/userEmailVerify/index.jsx | 2 +- ui/js/component/userEmailVerify/view.jsx | 63 ++-- ui/js/component/welcomeModal/index.jsx | 14 +- ui/js/component/welcomeModal/view.jsx | 17 +- ui/js/constants/action_types.js | 37 +-- ui/js/lbryio.js | 17 +- ui/js/main.js | 5 +- ui/js/page/fileListPublished/index.js | 4 + ui/js/page/fileListPublished/view.jsx | 19 +- ui/js/page/publish/index.js | 4 + ui/js/page/publish/view.jsx | 4 +- ui/js/page/rewards/index.js | 7 +- ui/js/reducers/rewards.js | 97 +++--- ui/js/reducers/user.js | 26 ++ ui/js/rewards.js | 354 ++++++++++------------ ui/js/selectors/rewards.js | 18 +- ui/js/selectors/user.js | 13 +- ui/scss/_gui.scss | 5 + 27 files changed, 522 insertions(+), 525 deletions(-) delete mode 100644 ui/js/component/reward-link.js diff --git a/ui/js/actions/content.js b/ui/js/actions/content.js index f9fe0b02b..ca174b261 100644 --- a/ui/js/actions/content.js +++ b/ui/js/actions/content.js @@ -2,7 +2,6 @@ import * as types from "constants/action_types"; import lbry from "lbry"; import lbryio from "lbryio"; import lbryuri from "lbryuri"; -import rewards from "rewards"; import { selectBalance } from "selectors/wallet"; import { selectFileInfoForUri, @@ -10,8 +9,8 @@ import { } from "selectors/file_info"; import { selectResolvingUris } from "selectors/content"; import { selectCostInfoForUri } from "selectors/cost_info"; -import { selectClaimsByUri } from "selectors/claims"; import { doOpenModal } from "actions/app"; +import { doClaimEligiblePurchaseRewards } from "actions/rewards"; export function doResolveUri(uri) { return function(dispatch, getState) { @@ -171,7 +170,7 @@ export function doDownloadFile(uri, streamInfo) { }) .catch(() => {}); - rewards.claimEligiblePurchaseRewards(); + dispatch(doClaimEligiblePurchaseRewards()); }; } diff --git a/ui/js/actions/rewards.js b/ui/js/actions/rewards.js index ac368483d..14a2a8253 100644 --- a/ui/js/actions/rewards.js +++ b/ui/js/actions/rewards.js @@ -2,6 +2,7 @@ import * as types from "constants/action_types"; import lbry from "lbry"; import lbryio from "lbryio"; import rewards from "rewards"; +import { selectRewards, selectRewardsByType } from "selectors/rewards"; export function doRewardList() { return function(dispatch, getState) { @@ -11,56 +12,105 @@ export function doRewardList() { type: types.FETCH_REWARDS_STARTED, }); - lbryio.call('reward', 'list', {}).then((userRewards) => { - dispatch({ - type: types.FETCH_REWARDS_COMPLETED, - data: { userRewards } + lbryio + .call("reward", "list", {}) + .then(userRewards => { + dispatch({ + type: types.FETCH_REWARDS_COMPLETED, + data: { userRewards }, + }); }) - }).catch(() => { - dispatch({ - type: types.FETCH_REWARDS_COMPLETED, - data: { userRewards: [] } - }) - }); + .catch(() => { + dispatch({ + type: types.FETCH_REWARDS_COMPLETED, + data: { userRewards: [] }, + }); + }); }; } -export function doClaimReward(reward) { +export function doClaimRewardType(rewardType) { return function(dispatch, getState) { + const rewardsByType = selectRewardsByType(getState()), + reward = rewardsByType[rewardType]; + + if (reward) { + dispatch(doClaimReward(reward)); + } + }; +} + +export function doClaimReward(reward, saveError = false) { + return function(dispatch, getState) { + if (reward.transaction_id) { + //already claimed, do nothing + return; + } + dispatch({ type: types.CLAIM_REWARD_STARTED, - data: { reward } - }) + data: { reward }, + }); - const success = (a) => { - console.log(a) + const success = reward => { dispatch({ type: types.CLAIM_REWARD_SUCCESS, data: { - a - } - }) - } + reward, + }, + }); + }; - const failure = (error) => { + const failure = error => { dispatch({ type: types.CLAIM_REWARD_FAILURE, data: { reward, - error - } - }) + error: saveError ? error : null, + }, + }); + }; + + rewards.claimReward(reward.reward_type).then(success, failure); + }; +} + +export function doClaimEligiblePurchaseRewards() { + return function(dispatch, getState) { + if (!lbryio.enabled || !lbryio.getAccessToken()) { + return; } - rewards.claimReward(reward.reward_type).then(success, failure) - } + const rewardsByType = selectRewardsByType(getState()); + + let types = {}; + + types[rewards.TYPE_FIRST_STREAM] = false; + types[rewards.TYPE_FEATURED_DOWNLOAD] = false; + types[rewards.TYPE_MANY_DOWNLOADS] = false; + Object.values(rewardsByType).forEach(reward => { + if (types[reward.reward_type] === false && reward.transaction_id) { + types[reward.reward_type] = true; + } + }); + + let unclaimedType = Object.keys(types).find(type => { + return types[type] === false && type !== rewards.TYPE_FEATURED_DOWNLOAD; //handled below + }); + if (unclaimedType) { + dispatch(doClaimRewardType(unclaimedType)); + } + if (types[rewards.TYPE_FEATURED_DOWNLOAD] === false) { + dispatch(doClaimRewardType(rewards.TYPE_FEATURED_DOWNLOAD)); + } + }; } export function doClaimRewardClearError(reward) { return function(dispatch, getState) { dispatch({ type: types.CLAIM_REWARD_CLEAR_ERROR, - data: { reward } - }) - } + data: { reward }, + }); + }; } diff --git a/ui/js/actions/user.js b/ui/js/actions/user.js index 3fd4f8782..972e167eb 100644 --- a/ui/js/actions/user.js +++ b/ui/js/actions/user.js @@ -20,8 +20,6 @@ export function doAuthenticate() { dispatch(doRewardList()); //FIXME - where should this happen? }) .catch(error => { - console.log("auth error"); - console.log(error); dispatch({ type: types.AUTHENTICATION_FAILURE, data: { error }, @@ -30,6 +28,28 @@ export function doAuthenticate() { }; } +export function doUserFetch() { + return function(dispatch, getState) { + dispatch({ + type: types.USER_FETCH_STARTED, + }); + lbryio.setCurrentUser( + user => { + dispatch({ + type: types.USER_FETCH_SUCCESS, + data: { user }, + }); + }, + error => { + dispatch({ + type: types.USER_FETCH_FAILURE, + data: { error }, + }); + } + ); + }; +} + export function doUserEmailNew(email) { return function(dispatch, getState) { dispatch({ @@ -42,6 +62,7 @@ export function doUserEmailNew(email) { type: types.USER_EMAIL_NEW_SUCCESS, data: { email }, }); + dispatch(doUserFetch()); }, error => { if ( @@ -102,6 +123,7 @@ export function doUserEmailVerify(verificationToken) { type: types.USER_EMAIL_VERIFY_SUCCESS, data: { email }, }); + dispatch(doUserFetch()); } else { failure(new Error("Your email is still not verified.")); //shouldn't happen? } diff --git a/ui/js/component/authOverlay/index.jsx b/ui/js/component/authOverlay/index.jsx index d3982be73..ec7d7ee5a 100644 --- a/ui/js/component/authOverlay/index.jsx +++ b/ui/js/component/authOverlay/index.jsx @@ -5,11 +5,13 @@ import { doUserEmailDecline } from "actions/user"; import { doOpenModal } from "actions/app"; import { selectAuthenticationIsPending, + selectUserHasEmail, selectUserIsAuthRequested, } from "selectors/user"; import AuthOverlay from "./view"; const select = state => ({ + hasEmail: selectUserHasEmail(state), isPending: selectAuthenticationIsPending(state), isShowing: selectUserIsAuthRequested(state), }); diff --git a/ui/js/component/authOverlay/view.jsx b/ui/js/component/authOverlay/view.jsx index 0f1c95af4..fcb7e77d4 100644 --- a/ui/js/component/authOverlay/view.jsx +++ b/ui/js/component/authOverlay/view.jsx @@ -32,7 +32,7 @@ export class AuthOverlay extends React.Component { return null; } - const { isPending, isShowing } = this.props; + const { isPending, isShowing, hasEmail } = this.props; if (isShowing) { return ( @@ -46,15 +46,14 @@ export class AuthOverlay extends React.Component { {isPending ? "" :
- {this.state.showNoEmailConfirm - ?
-

+ {!hasEmail && this.state.showNoEmailConfirm + ?

+

{__( "If you continue without an email, you will be ineligible to earn free LBC rewards, as well as unable to receive security related communications." )}

{ this.onEmailSkipConfirm(); }} @@ -62,11 +61,15 @@ export class AuthOverlay extends React.Component { />
: { - this.onEmailSkipClick(); + hasEmail + ? this.onEmailSkipConfirm() + : this.onEmailSkipClick(); }} - label={__("Do I have to?")} + label={ + hasEmail ? __("Skip for now") : __("Do I have to?") + } />}
} diff --git a/ui/js/component/form.js b/ui/js/component/form.js index a9c1ab6bd..d0f192fec 100644 --- a/ui/js/component/form.js +++ b/ui/js/component/form.js @@ -178,9 +178,19 @@ export class FormRow extends React.Component { this._fieldRequiredText = __("This field is required"); - this.state = { + this.state = this.getStateFromProps(props); + } + + componentWillReceiveProps(nextProps) { + this.setState(this.getStateFromProps(nextProps)); + } + + getStateFromProps(props) { + return { isError: !!props.errorMessage, - errorMessage: typeof props.errorMessage === "string" ? props.errorMessage : '', + errorMessage: typeof props.errorMessage === "string" + ? props.errorMessage + : "", }; } diff --git a/ui/js/component/reward-link.js b/ui/js/component/reward-link.js deleted file mode 100644 index baf9df753..000000000 --- a/ui/js/component/reward-link.js +++ /dev/null @@ -1,109 +0,0 @@ -import React from "react"; -import lbry from "lbry"; -import { Icon } from "component/common"; -import Modal from "component/modal"; -import rewards from "rewards"; -import Link from "component/link"; - -export class RewardLink extends React.Component { - static propTypes = { - type: React.PropTypes.string.isRequired, - claimed: React.PropTypes.bool, - onRewardClaim: React.PropTypes.func, - onRewardFailure: React.PropTypes.func, - }; - - constructor(props) { - super(props); - - this.state = { - claimable: true, - pending: false, - errorMessage: null, - }; - } - - refreshClaimable() { - switch (this.props.type) { - case "new_user": - this.setState({ claimable: true }); - return; - - case "first_publish": - lbry.claim_list_mine().then(list => { - this.setState({ - claimable: list.length > 0, - }); - }); - return; - } - } - - componentWillMount() { - this.refreshClaimable(); - } - - claimReward() { - this.setState({ - pending: true, - }); - - rewards - .claimReward(this.props.type) - .then(reward => { - this.setState({ - pending: false, - errorMessage: null, - }); - if (this.props.onRewardClaim) { - this.props.onRewardClaim(reward); - } - }) - .catch(error => { - this.setState({ - errorMessage: error.message, - pending: false, - }); - }); - } - - clearError() { - if (this.props.onRewardFailure) { - this.props.onRewardFailure(); - } - this.setState({ - errorMessage: null, - }); - } - - render() { - return ( -
- {this.props.claimed - ? {__("Reward claimed.")} - : { - this.claimReward(); - }} - />} - {this.state.errorMessage - ? { - this.clearError(); - }} - > - {this.state.errorMessage} - - : ""} -
- ); - } -} diff --git a/ui/js/component/rewardLink/index.js b/ui/js/component/rewardLink/index.js index 631e65165..81b01488b 100644 --- a/ui/js/component/rewardLink/index.js +++ b/ui/js/component/rewardLink/index.js @@ -20,14 +20,14 @@ const makeSelect = () => { isClaimed: selectHasClaimedReward(state, props), errorMessage: selectError(state, props), isPending: selectIsPending(state, props), - reward: select, + reward: selectReward(state, props), }); return select; }; const perform = dispatch => ({ - claimReward: reward => dispatch(doClaimReward(reward)), + claimReward: reward => dispatch(doClaimReward(reward, true)), clearError: reward => dispatch(doClaimRewardClearError(reward)), navigate: path => dispatch(doNavigate(path)), }); diff --git a/ui/js/component/userEmailNew/view.jsx b/ui/js/component/userEmailNew/view.jsx index 502ceaa70..7e0f83b47 100644 --- a/ui/js/component/userEmailNew/view.jsx +++ b/ui/js/component/userEmailNew/view.jsx @@ -1,13 +1,13 @@ -import React from 'react'; -import Link from 'component/link'; -import {FormRow} from 'component/form.js'; +import React from "react"; +import Link from "component/link"; +import { FormRow } from "component/form.js"; class UserEmailNew extends React.Component { constructor(props) { super(props); this.state = { - email: '' + email: "", }; } @@ -19,25 +19,43 @@ class UserEmailNew extends React.Component { handleSubmit(event) { event.preventDefault(); - this.props.addUserEmail(this.state.email) + this.props.addUserEmail(this.state.email); } render() { - const { - errorMessage, - isPending - } = this.props + const { errorMessage, isPending } = this.props; - return
{ this.handleSubmit(event) }}> - { this.handleEmailChanged(event) }} /> -
- { this.handleSubmit(event) }} /> -
- + return ( +
{ + this.handleSubmit(event); + }} + > + { + this.handleEmailChanged(event); + }} + /> +
+ { + this.handleSubmit(event); + }} + /> +
+ + ); } } -export default UserEmailNew \ No newline at end of file +export default UserEmailNew; diff --git a/ui/js/component/userEmailVerify/index.jsx b/ui/js/component/userEmailVerify/index.jsx index 090b41400..f2ae56c86 100644 --- a/ui/js/component/userEmailVerify/index.jsx +++ b/ui/js/component/userEmailVerify/index.jsx @@ -10,7 +10,7 @@ import UserEmailVerify from "./view"; const select = state => ({ isPending: selectEmailVerifyIsPending(state), - email: selectEmailToVerify, + email: selectEmailToVerify(state), errorMessage: selectEmailVerifyErrorMessage(state), }); diff --git a/ui/js/component/userEmailVerify/view.jsx b/ui/js/component/userEmailVerify/view.jsx index 9d6160287..4b9272394 100644 --- a/ui/js/component/userEmailVerify/view.jsx +++ b/ui/js/component/userEmailVerify/view.jsx @@ -1,13 +1,13 @@ -import React from 'react'; -import Link from 'component/link'; -import {FormRow} from 'component/form.js'; +import React from "react"; +import Link from "component/link"; +import { FormRow } from "component/form.js"; class UserEmailVerify extends React.Component { constructor(props) { super(props); this.state = { - code: '', + code: "", }; } @@ -19,25 +19,50 @@ class UserEmailVerify extends React.Component { handleSubmit(event) { event.preventDefault(); - this.props.verifyUserEmail(this.state.code) + this.props.verifyUserEmail(this.state.code); } render() { - const { - errorMessage, - isPending - } = this.props + const { errorMessage, isPending } = this.props; - return
{ this.handleSubmit(event) }}> - { this.handleCodeChanged(event) }} - errorMessage={errorMessage} - helper="A verification code is required to participate in early access rewards."/> -
- { this.handleSubmit(event)}} /> -
- + return ( +
{ + this.handleSubmit(event); + }} + > + { + this.handleCodeChanged(event); + }} + errorMessage={errorMessage} + /> + {/* render help separately so it always shows */} +
+

+ Email if + you did not receive or are having trouble with your code. +

+
+
+ { + this.handleSubmit(event); + }} + /> +
+ + ); } } -export default UserEmailVerify \ No newline at end of file +export default UserEmailVerify; diff --git a/ui/js/component/welcomeModal/index.jsx b/ui/js/component/welcomeModal/index.jsx index 6a6af4f64..bbfc5b6a7 100644 --- a/ui/js/component/welcomeModal/index.jsx +++ b/ui/js/component/welcomeModal/index.jsx @@ -1,17 +1,23 @@ import React from "react"; +import rewards from "rewards"; import { connect } from "react-redux"; import { doCloseModal } from "actions/app"; import { selectUserIsRewardApproved } from "selectors/user"; -import { makeSelectHasClaimedReward } from "selectors/rewards"; +import { + makeSelectHasClaimedReward, + makeSelectClaimRewardError, + makeSelectRewardByType, +} from "selectors/rewards"; import WelcomeModal from "./view"; const select = (state, props) => { - const selectHasReward = makeSelectHasClaimedReward(); + const selectHasClaimed = makeSelectHasClaimedReward(), + selectReward = makeSelectRewardByType(); return { - hasReward: selectHasReward(state, { reward_type: "new_user" }), + hasClaimed: selectHasClaimed(state, { reward_type: rewards.TYPE_NEW_USER }), isRewardApproved: selectUserIsRewardApproved(state), - rewardAmount: 5, + reward: selectReward(state, { reward_type: rewards.TYPE_NEW_USER }), }; }; diff --git a/ui/js/component/welcomeModal/view.jsx b/ui/js/component/welcomeModal/view.jsx index 015df4701..5b0f635c5 100644 --- a/ui/js/component/welcomeModal/view.jsx +++ b/ui/js/component/welcomeModal/view.jsx @@ -6,14 +6,12 @@ import RewardLink from "component/rewardLink"; class WelcomeModal extends React.Component { render() { - const { - closeModal, - hasReward, - isRewardApproved, - rewardAmount, - } = this.props; + const { closeModal, hasClaimed, isRewardApproved, reward } = this.props; - return !hasReward + console.log("welcome"); + console.log(this.props); + + return !hasClaimed ?

Welcome to LBRY.

@@ -30,7 +28,7 @@ class WelcomeModal extends React.Component { Thank you for making content freedom possible! {" "}{isRewardApproved ? __("Here's a nickel, kid.") : ""}

-
+
{isRewardApproved ? : About Your Reward

You earned a reward of - {" "} LBRY + {" "} + {" "}LBRY credits, or LBC.

diff --git a/ui/js/constants/action_types.js b/ui/js/constants/action_types.js index 647c6e108..3450c1c1a 100644 --- a/ui/js/constants/action_types.js +++ b/ui/js/constants/action_types.js @@ -71,22 +71,25 @@ export const SEARCH_CANCELLED = "SEARCH_CANCELLED"; export const DAEMON_SETTINGS_RECEIVED = "DAEMON_SETTINGS_RECEIVED"; // User -export const AUTHENTICATION_STARTED = 'AUTHENTICATION_STARTED' -export const AUTHENTICATION_SUCCESS = 'AUTHENTICATION_SUCCESS' -export const AUTHENTICATION_FAILURE = 'AUTHENTICATION_FAILURE' -export const USER_EMAIL_DECLINE = 'USER_EMAIL_DECLINE' -export const USER_EMAIL_NEW_STARTED = 'USER_EMAIL_NEW_STARTED' -export const USER_EMAIL_NEW_SUCCESS = 'USER_EMAIL_NEW_SUCCESS' -export const USER_EMAIL_NEW_EXISTS = 'USER_EMAIL_NEW_EXISTS' -export const USER_EMAIL_NEW_FAILURE = 'USER_EMAIL_NEW_FAILURE' -export const USER_EMAIL_VERIFY_STARTED = 'USER_EMAIL_VERIFY_STARTED' -export const USER_EMAIL_VERIFY_SUCCESS = 'USER_EMAIL_VERIFY_SUCCESS' -export const USER_EMAIL_VERIFY_FAILURE = 'USER_EMAIL_VERIFY_FAILURE' +export const AUTHENTICATION_STARTED = "AUTHENTICATION_STARTED"; +export const AUTHENTICATION_SUCCESS = "AUTHENTICATION_SUCCESS"; +export const AUTHENTICATION_FAILURE = "AUTHENTICATION_FAILURE"; +export const USER_EMAIL_DECLINE = "USER_EMAIL_DECLINE"; +export const USER_EMAIL_NEW_STARTED = "USER_EMAIL_NEW_STARTED"; +export const USER_EMAIL_NEW_SUCCESS = "USER_EMAIL_NEW_SUCCESS"; +export const USER_EMAIL_NEW_EXISTS = "USER_EMAIL_NEW_EXISTS"; +export const USER_EMAIL_NEW_FAILURE = "USER_EMAIL_NEW_FAILURE"; +export const USER_EMAIL_VERIFY_STARTED = "USER_EMAIL_VERIFY_STARTED"; +export const USER_EMAIL_VERIFY_SUCCESS = "USER_EMAIL_VERIFY_SUCCESS"; +export const USER_EMAIL_VERIFY_FAILURE = "USER_EMAIL_VERIFY_FAILURE"; +export const USER_FETCH_STARTED = "USER_FETCH_STARTED"; +export const USER_FETCH_SUCCESS = "USER_FETCH_SUCCESS"; +export const USER_FETCH_FAILURE = "USER_FETCH_FAILURE"; // Rewards -export const FETCH_REWARDS_STARTED = 'FETCH_REWARDS_STARTED' -export const FETCH_REWARDS_COMPLETED = 'FETCH_REWARDS_COMPLETED' -export const CLAIM_REWARD_STARTED = 'CLAIM_REWARD_STARTED' -export const CLAIM_REWARD_SUCCESS = 'CLAIM_REWARD_SUCCESS' -export const CLAIM_REWARD_FAILURE = 'CLAIM_REWARD_FAILURE' -export const CLAIM_REWARD_CLEAR_ERROR = 'CLAIM_REWARD_CLEAR_ERROR' +export const FETCH_REWARDS_STARTED = "FETCH_REWARDS_STARTED"; +export const FETCH_REWARDS_COMPLETED = "FETCH_REWARDS_COMPLETED"; +export const CLAIM_REWARD_STARTED = "CLAIM_REWARD_STARTED"; +export const CLAIM_REWARD_SUCCESS = "CLAIM_REWARD_SUCCESS"; +export const CLAIM_REWARD_FAILURE = "CLAIM_REWARD_FAILURE"; +export const CLAIM_REWARD_CLEAR_ERROR = "CLAIM_REWARD_CLEAR_ERROR"; diff --git a/ui/js/lbryio.js b/ui/js/lbryio.js index 1ca8f76f1..11416fbd6 100644 --- a/ui/js/lbryio.js +++ b/ui/js/lbryio.js @@ -6,7 +6,6 @@ const querystring = require("querystring"); const lbryio = { _accessToken: getSession("accessToken"), _authenticationPromise: null, - _user: null, enabled: true, }; @@ -36,20 +35,9 @@ lbryio.getExchangeRates = function() { return lbryio._exchangePromise; }; -lbryio.call = function( - resource, - action, - params = {}, - method = "get", - evenIfDisabled = false -) { - // evenIfDisabled is just for development, when we may have some calls working and some not +lbryio.call = function(resource, action, params = {}, method = "get") { return new Promise((resolve, reject) => { - if ( - !lbryio.enabled && - !evenIfDisabled && - (resource != "discover" || action != "list") - ) { + if (!lbryio.enabled && (resource != "discover" || action != "list")) { console.log(__("Internal API disabled")); reject(new Error(__("LBRY internal API is disabled"))); return; @@ -135,7 +123,6 @@ lbryio.setCurrentUser = (resolve, reject) => { lbryio .call("user", "me") .then(data => { - lbryio.user = data; resolve(data); }) .catch(function(err) { diff --git a/ui/js/main.js b/ui/js/main.js index c8b9e76fc..f600843bf 100644 --- a/ui/js/main.js +++ b/ui/js/main.js @@ -51,7 +51,10 @@ ipcRenderer.on("open-uri-requested", (event, uri) => { document.addEventListener("click", event => { var target = event.target; while (target && target !== document) { - if (target.matches('a[href^="http"]')) { + if ( + target.matches('a[href^="http"]') || + target.matches('a[href^="mailto"]') + ) { event.preventDefault(); shell.openExternal(target.href); return; diff --git a/ui/js/page/fileListPublished/index.js b/ui/js/page/fileListPublished/index.js index 41ef8a73f..948b81c7e 100644 --- a/ui/js/page/fileListPublished/index.js +++ b/ui/js/page/fileListPublished/index.js @@ -1,10 +1,12 @@ import React from "react"; +import rewards from "rewards"; import { connect } from "react-redux"; import { doFetchFileInfosAndPublishedClaims } from "actions/file_info"; import { selectFileInfosPublished, selectFileListDownloadedOrPublishedIsPending, } from "selectors/file_info"; +import { doClaimRewardType } from "actions/rewards"; import { doNavigate } from "actions/app"; import FileListPublished from "./view"; @@ -16,6 +18,8 @@ const select = state => ({ const perform = dispatch => ({ navigate: path => dispatch(doNavigate(path)), fetchFileListPublished: () => dispatch(doFetchFileInfosAndPublishedClaims()), + claimFirstPublishReward: () => + dispatch(doClaimRewardType(rewards.TYPE_FIRST_PUBLISH)), }); export default connect(select, perform)(FileListPublished); diff --git a/ui/js/page/fileListPublished/view.jsx b/ui/js/page/fileListPublished/view.jsx index e4dc5f3e9..5bf334cf3 100644 --- a/ui/js/page/fileListPublished/view.jsx +++ b/ui/js/page/fileListPublished/view.jsx @@ -16,24 +16,7 @@ class FileListPublished extends React.Component { } componentDidUpdate() { - if (this.props.fileInfos.length > 0) this._requestPublishReward(); - } - - _requestPublishReward() { - // TODO this is throwing an error now - // Error: LBRY internal API is disabled - // - // lbryio.call('reward', 'list', {}).then(function(userRewards) { - // //already rewarded - // if (userRewards.filter(function (reward) { - // return reward.reward_type == rewards.TYPE_FIRST_PUBLISH && reward.transaction_id - // }).length) { - // return - // } - // else { - // rewards.claimReward(rewards.TYPE_FIRST_PUBLISH).catch(() => {}) - // } - // }) + if (this.props.fileInfos.length > 0) this.props.claimFirstPublishReward(); } render() { diff --git a/ui/js/page/publish/index.js b/ui/js/page/publish/index.js index 4b20b9032..abe8137df 100644 --- a/ui/js/page/publish/index.js +++ b/ui/js/page/publish/index.js @@ -1,7 +1,9 @@ import React from "react"; import { connect } from "react-redux"; import { doNavigate, doHistoryBack } from "actions/app"; +import { doClaimRewardType } from "actions/rewards"; import { selectMyClaims } from "selectors/claims"; +import rewards from "rewards"; import PublishPage from "./view"; const select = state => ({ @@ -11,6 +13,8 @@ const select = state => ({ const perform = dispatch => ({ back: () => dispatch(doHistoryBack()), navigate: path => dispatch(doNavigate(path)), + claimFirstChannelReward: () => + dispatch(doClaimRewardType(rewards.TYPE_FIRST_CHANNEL)), }); export default connect(select, perform)(PublishPage); diff --git a/ui/js/page/publish/view.jsx b/ui/js/page/publish/view.jsx index 19e1e2f25..80fac5ec9 100644 --- a/ui/js/page/publish/view.jsx +++ b/ui/js/page/publish/view.jsx @@ -44,7 +44,7 @@ class PublishPage extends React.Component { // Calls API to update displayed list of channels. If a channel name is provided, will select // that channel at the same time (used immediately after creating a channel) lbry.channel_list_mine().then(channels => { - rewards.claimReward(rewards.TYPE_FIRST_CHANNEL).then(() => {}, () => {}); + this.props.claimFirstChannelReward(); this.setState({ channels: channels, ...(channel ? { channel } : {}), @@ -373,7 +373,7 @@ class PublishPage extends React.Component { lbry .channel_new({ channel_name: newChannelName, - amount: parseInt(this.state.newChannelBid), + amount: parseFloat(this.state.newChannelBid), }) .then( () => { diff --git a/ui/js/page/rewards/index.js b/ui/js/page/rewards/index.js index 51da2f8df..61d0515e9 100644 --- a/ui/js/page/rewards/index.js +++ b/ui/js/page/rewards/index.js @@ -1,15 +1,10 @@ import React from "react"; import { connect } from "react-redux"; import { doNavigate } from "actions/app"; -import { - selectFetchingRewards, - selectIsRewardEligible, - selectRewards, -} from "selectors/rewards"; +import { selectFetchingRewards, selectRewards } from "selectors/rewards"; import { selectUserIsRewardEligible, selectUserHasEmail, - selectUserIsRewardApproved, selectUserIsVerificationCandidate, } from "selectors/user"; import RewardsPage from "./view"; diff --git a/ui/js/reducers/rewards.js b/ui/js/reducers/rewards.js index dd56df320..0994d730b 100644 --- a/ui/js/reducers/rewards.js +++ b/ui/js/reducers/rewards.js @@ -1,87 +1,90 @@ import * as types from "constants/action_types"; -const reducers = {} +const reducers = {}; const defaultState = { fetching: false, + rewardsByType: {}, claimPendingByType: {}, - claimErrorsByType: {} + claimErrorsByType: {}, }; reducers[types.FETCH_REWARDS_STARTED] = function(state, action) { - const newRewards = Object.assign({}, state.rewards, { + return Object.assign({}, state, { fetching: true, - }) - - return Object.assign({}, state, newRewards) -} + }); +}; reducers[types.FETCH_REWARDS_COMPLETED] = function(state, action) { - const { - userRewards, - } = action.data + const { userRewards } = action.data; - const byRewardType = {} - userRewards.forEach(reward => byRewardType[reward.reward_type] = reward) - const newRewards = Object.assign({}, state.rewards, { - byRewardType: byRewardType, - fetching: false - }) + const rewardsByType = {}; + userRewards.forEach(reward => (rewardsByType[reward.reward_type] = reward)); - return Object.assign({}, state, newRewards) -} + return Object.assign({}, state, { + rewardsByType: rewardsByType, + fetching: false, + }); +}; -function setClaimRewardState(state, reward, isClaiming, errorMessage="") { - const newClaimPendingByType = Object.assign({}, state.claimPendingByType) - const newClaimErrorsByType = Object.assign({}, state.claimErrorsByType) +function setClaimRewardState(state, reward, isClaiming, errorMessage = "") { + const newClaimPendingByType = Object.assign({}, state.claimPendingByType); + const newClaimErrorsByType = Object.assign({}, state.claimErrorsByType); if (isClaiming) { - newClaimPendingByType[reward.reward_type] = isClaiming + newClaimPendingByType[reward.reward_type] = isClaiming; } else { - delete newClaimPendingByType[reward.reward_type] + delete newClaimPendingByType[reward.reward_type]; } if (errorMessage) { - newClaimErrorsByType[reward.reward_type] = errorMessage + newClaimErrorsByType[reward.reward_type] = errorMessage; } else { - delete newClaimErrorsByType[reward.reward_type] + delete newClaimErrorsByType[reward.reward_type]; } return Object.assign({}, state, { claimPendingByType: newClaimPendingByType, claimErrorsByType: newClaimErrorsByType, - }) + }); } reducers[types.CLAIM_REWARD_STARTED] = function(state, action) { - const { - reward, - } = action.data + const { reward } = action.data; - return setClaimRewardState(state, reward, true, "") -} + return setClaimRewardState(state, reward, true, ""); +}; reducers[types.CLAIM_REWARD_SUCCESS] = function(state, action) { - const { - reward, - } = action.data + const { reward } = action.data; - return setClaimRewardState(state, reward, false, "") -} + const existingReward = state.rewardsByType[reward.reward_type]; + const newReward = Object.assign({}, reward, { + reward_title: existingReward.reward_title, + reward_description: existingReward.reward_description, + }); + const rewardsByType = Object.assign({}, state.rewardsByType); + + rewardsByType[reward.reward_type] = newReward; + + const newState = Object.assign({}, state, { rewardsByType }); + + return setClaimRewardState(newState, newReward, false, ""); +}; reducers[types.CLAIM_REWARD_FAILURE] = function(state, action) { - const { - reward, - error - } = action.data + const { reward, error } = action.data; - return setClaimRewardState(state, reward, false, error.message) -} + return setClaimRewardState(state, reward, false, error ? error.message : ""); +}; reducers[types.CLAIM_REWARD_CLEAR_ERROR] = function(state, action) { - const { - reward - } = action.data + const { reward } = action.data; - return setClaimRewardState(state, reward, state.claimPendingByType[reward.reward_type], "") -} + return setClaimRewardState( + state, + reward, + state.claimPendingByType[reward.reward_type], + "" + ); +}; export default function reducer(state = defaultState, action) { const handler = reducers[action.type]; diff --git a/ui/js/reducers/user.js b/ui/js/reducers/user.js index 353b9b193..4e4ffda40 100644 --- a/ui/js/reducers/user.js +++ b/ui/js/reducers/user.js @@ -5,6 +5,7 @@ const reducers = {}; const defaultState = { authenticationIsPending: false, + userIsPending: false, emailNewIsPending: false, emailNewErrorMessage: "", emailNewDeclined: getLocal("user_email_declined", false), @@ -15,12 +16,15 @@ const defaultState = { reducers[types.AUTHENTICATION_STARTED] = function(state, action) { return Object.assign({}, state, { authenticationIsPending: true, + userIsPending: true, + user: defaultState.user, }); }; reducers[types.AUTHENTICATION_SUCCESS] = function(state, action) { return Object.assign({}, state, { authenticationIsPending: false, + userIsPending: false, user: action.data.user, }); }; @@ -28,6 +32,28 @@ reducers[types.AUTHENTICATION_SUCCESS] = function(state, action) { reducers[types.AUTHENTICATION_FAILURE] = function(state, action) { return Object.assign({}, state, { authenticationIsPending: false, + userIsPending: false, + user: null, + }); +}; + +reducers[types.USER_FETCH_STARTED] = function(state, action) { + return Object.assign({}, state, { + userIsPending: true, + user: defaultState.user, + }); +}; + +reducers[types.USER_FETCH_SUCCESS] = function(state, action) { + return Object.assign({}, state, { + userIsPending: false, + user: action.data.user, + }); +}; + +reducers[types.USER_FETCH_FAILURE] = function(state, action) { + return Object.assign({}, state, { + userIsPending: true, user: null, }); }; diff --git a/ui/js/rewards.js b/ui/js/rewards.js index 33d5547bc..08d82fd9b 100644 --- a/ui/js/rewards.js +++ b/ui/js/rewards.js @@ -1,225 +1,191 @@ -const hashes = require('jshashes'); -import lbry from 'lbry'; -import lbryio from 'lbryio'; -import { doShowSnackBar } from 'actions/app'; +const hashes = require("jshashes"); +import lbry from "lbry"; +import lbryio from "lbryio"; +import { doShowSnackBar } from "actions/app"; function rewardMessage(type, amount) { - return { - new_developer: __( - 'You earned %s for registering as a new developer.', - amount - ), - new_user: __('You earned %s LBC new user reward.', amount), - confirm_email: __( - 'You earned %s LBC for verifying your email address.', - amount - ), - new_channel: __( - 'You earned %s LBC for creating a publisher identity.', - amount - ), - first_stream: __( - 'You earned %s LBC for streaming your first video.', - amount - ), - many_downloads: __( - 'You earned %s LBC for downloading some of the things.', - amount - ), - first_publish: __( - 'You earned %s LBC for making your first publication.', - amount - ) - }[type]; + return { + new_developer: __( + "You earned %s for registering as a new developer.", + amount + ), + new_user: __("You earned %s LBC new user reward.", amount), + confirm_email: __( + "You earned %s LBC for verifying your email address.", + amount + ), + new_channel: __( + "You earned %s LBC for creating a publisher identity.", + amount + ), + first_stream: __( + "You earned %s LBC for streaming your first video.", + amount + ), + many_downloads: __( + "You earned %s LBC for downloading some of the things.", + amount + ), + first_publish: __( + "You earned %s LBC for making your first publication.", + amount + ), + }[type]; } function toHex(s) { - let h = ''; - for (var i = 0; i < s.length; i++) { - let c = s.charCodeAt(i).toString(16); - if (c.length < 2) { - c = '0'.concat(c); - } - h += c; - } - return h; + let h = ""; + for (var i = 0; i < s.length; i++) { + let c = s.charCodeAt(i).toString(16); + if (c.length < 2) { + c = "0".concat(c); + } + h += c; + } + return h; } function fromHex(h) { - let s = ''; - for (let i = 0; i < h.length; i += 2) { - s += String.fromCharCode(parseInt(h.substr(i, 2), 16)); - } - return s; + let s = ""; + for (let i = 0; i < h.length; i += 2) { + s += String.fromCharCode(parseInt(h.substr(i, 2), 16)); + } + return s; } function reverseString(s) { - let o = ''; - for (let i = s.length - 1; i >= 0; i--) { - o += s[i]; - } - return o; + let o = ""; + for (let i = s.length - 1; i >= 0; i--) { + o += s[i]; + } + return o; } function pack(num) { - return ( - '' + - String.fromCharCode(num & 0xff) + - String.fromCharCode((num >> 8) & 0xff) + - String.fromCharCode((num >> 16) & 0xff) + - String.fromCharCode((num >> 24) & 0xff) - ); + return ( + "" + + String.fromCharCode(num & 0xff) + + String.fromCharCode((num >> 8) & 0xff) + + String.fromCharCode((num >> 16) & 0xff) + + String.fromCharCode((num >> 24) & 0xff) + ); } // Returns true if claim is an initial claim, false if it's an update to an existing claim function isInitialClaim(claim) { - const reversed = reverseString(fromHex(claim.txid)); - const concat = reversed.concat(pack(claim.nout)); - const sha256 = new hashes.SHA256({ utf8: false }).raw(concat); - const ripemd160 = new hashes.RMD160({ utf8: false }).raw(sha256); - const hash = toHex(reverseString(ripemd160)); - return hash == claim.claim_id; + const reversed = reverseString(fromHex(claim.txid)); + const concat = reversed.concat(pack(claim.nout)); + const sha256 = new hashes.SHA256({ utf8: false }).raw(concat); + const ripemd160 = new hashes.RMD160({ utf8: false }).raw(sha256); + const hash = toHex(reverseString(ripemd160)); + return hash == claim.claim_id; } const rewards = {}; -(rewards.TYPE_NEW_DEVELOPER = 'new_developer'), (rewards.TYPE_NEW_USER = - 'new_user'), (rewards.TYPE_CONFIRM_EMAIL = - 'confirm_email'), (rewards.TYPE_FIRST_CHANNEL = - 'new_channel'), (rewards.TYPE_FIRST_STREAM = - 'first_stream'), (rewards.TYPE_MANY_DOWNLOADS = - 'many_downloads'), (rewards.TYPE_FIRST_PUBLISH = 'first_publish'); -rewards.TYPE_FEATURED_DOWNLOAD = 'featured_download'; +(rewards.TYPE_NEW_DEVELOPER = "new_developer"), (rewards.TYPE_NEW_USER = + "new_user"), (rewards.TYPE_CONFIRM_EMAIL = + "confirm_email"), (rewards.TYPE_FIRST_CHANNEL = + "new_channel"), (rewards.TYPE_FIRST_STREAM = + "first_stream"), (rewards.TYPE_MANY_DOWNLOADS = + "many_downloads"), (rewards.TYPE_FIRST_PUBLISH = "first_publish"); +rewards.TYPE_FEATURED_DOWNLOAD = "featured_download"; rewards.claimReward = function(type) { - function requestReward(resolve, reject, params) { - if (!lbryio.enabled || !lbryio.getAccessToken()) { - reject(new Error(__('Rewards are not enabled.'))); - return; - } - lbryio.call('reward', 'new', params, 'post').then(({ reward_amount }) => { - const message = rewardMessage(type, reward_amount), - result = { - type: type, - amount: reward_amount, - message: message - }; + function requestReward(resolve, reject, params) { + if (!lbryio.enabled || !lbryio.getAccessToken()) { + reject(new Error(__("Rewards are not enabled."))); + return; + } + lbryio.call("reward", "new", params, "post").then(reward => { + const message = rewardMessage(type, reward.reward_amount); - // Display global notice - const action = doShowSnackBar({ - message, - linkText: __('Show All'), - linkTarget: '/rewards', - isError: false - }); - window.app.store.dispatch(action); + // Display global notice + const action = doShowSnackBar({ + message, + linkText: __("Show All"), + linkTarget: "/rewards", + isError: false, + }); + window.app.store.dispatch(action); - // Add more events here to display other places + // Add more events here to display other places - resolve(result); - }, reject); - } - - return new Promise((resolve, reject) => { - lbry.wallet_unused_address().then(address => { - const params = { - reward_type: type, - wallet_address: address - }; - - switch (type) { - case rewards.TYPE_FIRST_CHANNEL: - lbry - .claim_list_mine() - .then(function(claims) { - let claim = claims.find(function(claim) { - return ( - claim.name.length && - claim.name[0] == '@' && - claim.txid.length && - isInitialClaim(claim) - ); - }); - if (claim) { - params.transaction_id = claim.txid; - requestReward(resolve, reject, params); - } else { - reject( - new Error(__('Please create a channel identity first.')) - ); - } - }) - .catch(reject); - break; - - case rewards.TYPE_FIRST_PUBLISH: - lbry - .claim_list_mine() - .then(claims => { - let claim = claims.find(function(claim) { - return ( - claim.name.length && - claim.name[0] != '@' && - claim.txid.length && - isInitialClaim(claim) - ); - }); - if (claim) { - params.transaction_id = claim.txid; - requestReward(resolve, reject, params); - } else { - reject( - claims.length - ? new Error( - __( - 'Please publish something and wait for confirmation by the network to claim this reward.' - ) - ) - : new Error( - __('Please publish something to claim this reward.') - ) - ); - } - }) - .catch(reject); - break; - - case rewards.TYPE_FIRST_STREAM: - case rewards.TYPE_NEW_USER: - default: - requestReward(resolve, reject, params); - } - }); - }); -}; - -rewards.claimEligiblePurchaseRewards = () => { - if (!lbryio.enabled || !lbryio.getAccessToken()) { - return; + resolve(reward); + }, reject); } - let types = {}; - types[rewards.TYPE_FIRST_STREAM] = false; - types[rewards.TYPE_FEATURED_DOWNLOAD] = false; - types[rewards.TYPE_MANY_DOWNLOADS] = false; - lbryio.call('reward', 'list', {}).then( - userRewards => { - userRewards.forEach(reward => { - if (types[reward.reward_type] === false && reward.transaction_id) { - types[reward.reward_type] = true; - } - }); - let unclaimedType = Object.keys(types).find(type => { - return types[type] === false && type !== rewards.TYPE_FEATURED_DOWNLOAD; //handled below - }); - if (unclaimedType) { - rewards.claimReward(unclaimedType); - } - if (types[rewards.TYPE_FEATURED_DOWNLOAD] === false) { - rewards.claimReward(rewards.TYPE_FEATURED_DOWNLOAD); - } - }, - () => {} - ); + + return new Promise((resolve, reject) => { + lbry.wallet_unused_address().then(address => { + const params = { + reward_type: type, + wallet_address: address, + }; + + switch (type) { + case rewards.TYPE_FIRST_CHANNEL: + lbry + .claim_list_mine() + .then(function(claims) { + let claim = claims.reverse().find(function(claim) { + return ( + claim.name.length && + claim.name[0] == "@" && + claim.txid.length && + isInitialClaim(claim) + ); + }); + if (claim) { + params.transaction_id = claim.txid; + requestReward(resolve, reject, params); + } else { + reject( + new Error(__("Please create a channel identity first.")) + ); + } + }) + .catch(reject); + break; + + case rewards.TYPE_FIRST_PUBLISH: + lbry + .claim_list_mine() + .then(claims => { + let claim = claims.reverse().find(function(claim) { + return ( + claim.name.length && + claim.name[0] != "@" && + claim.txid.length && + isInitialClaim(claim) + ); + }); + if (claim) { + params.transaction_id = claim.txid; + requestReward(resolve, reject, params); + } else { + reject( + claims.length + ? new Error( + __( + "Please publish something and wait for confirmation by the network to claim this reward." + ) + ) + : new Error( + __("Please publish something to claim this reward.") + ) + ); + } + }) + .catch(reject); + break; + + case rewards.TYPE_FIRST_STREAM: + case rewards.TYPE_NEW_USER: + default: + requestReward(resolve, reject, params); + } + }); + }); }; export default rewards; diff --git a/ui/js/selectors/rewards.js b/ui/js/selectors/rewards.js index d9e17f927..7a6a20429 100644 --- a/ui/js/selectors/rewards.js +++ b/ui/js/selectors/rewards.js @@ -5,7 +5,7 @@ const _selectState = state => state.rewards || {}; export const selectRewardsByType = createSelector( _selectState, - state => state.byRewardType || {} + state => state.rewardsByType || {} ); export const selectRewards = createSelector( @@ -18,26 +18,14 @@ export const selectIsRewardEligible = createSelector( user => user.can_claim_rewards ); -export const selectClaimedRewards = createSelector(selectRewards, rewards => - rewards.filter(reward => reward.transaction_id !== "") -); - -export const selectClaimedRewardsByType = createSelector( - selectClaimedRewards, - claimedRewards => { - const byType = {}; - claimedRewards.forEach(reward => (byType[reward.reward_type] = reward)); - return byType; - } -); - export const selectFetchingRewards = createSelector( _selectState, state => !!state.fetching ); export const selectHasClaimedReward = (state, props) => { - return !!selectClaimedRewardsByType[props.reward_type]; + const reward = selectRewardsByType(state)[props.reward_type]; + return reward && reward.transaction_id !== ""; }; export const makeSelectHasClaimedReward = () => { diff --git a/ui/js/selectors/user.js b/ui/js/selectors/user.js index 015ebae62..f7104d3d4 100644 --- a/ui/js/selectors/user.js +++ b/ui/js/selectors/user.js @@ -7,6 +7,11 @@ export const selectAuthenticationIsPending = createSelector( state => state.authenticationIsPending ); +export const selectUserIsPending = createSelector( + _selectState, + state => state.userIsPending +); + export const selectUser = createSelector( _selectState, state => state.user || {} @@ -20,17 +25,17 @@ export const selectEmailToVerify = createSelector( export const selectUserHasEmail = createSelector( selectUser, selectEmailToVerify, - (user, email) => user.has_email || email + (user, email) => (user && user.has_email) || email ); export const selectUserIsRewardEligible = createSelector( selectUser, - user => user.is_reward_eligible + user => user && user.is_reward_eligible ); export const selectUserIsRewardApproved = createSelector( selectUser, - user => user.is_reward_approved + user => user && user.is_reward_approved ); export const selectEmailNewIsPending = createSelector( @@ -64,7 +69,7 @@ export const selectUserIsVerificationCandidate = createSelector( selectEmailToVerify, selectUser, (isEligible, isApproved, emailToVerify, user) => - (isEligible && !isApproved) || (emailToVerify && !user.has_email) + (isEligible && !isApproved) || (emailToVerify && user && !user.has_email) ); export const selectUserIsAuthRequested = createSelector( diff --git a/ui/scss/_gui.scss b/ui/scss/_gui.scss index f4ededba0..a902fe2da 100644 --- a/ui/scss/_gui.scss +++ b/ui/scss/_gui.scss @@ -165,3 +165,8 @@ p section.section-spaced { margin-bottom: $spacing-vertical; } + +.text-center +{ + text-align: center; +}