diff --git a/CHANGELOG.md b/CHANGELOG.md index e1bfc9fc3..0cd102812 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ Web UI version numbers should always match the corresponding version of LBRY App ## [Unreleased] ### Added * More file types, like audio and documents, can be streamed and/or served from the app + * App is no longer gated. Reward authorization re-written. Added basic flows for new users. * Videos now have a classy loading spinner ### Changed @@ -20,6 +21,7 @@ Web UI version numbers should always match the corresponding version of LBRY App * Updated deprecated LBRY API call signatures * App scrolls to the top of the page on navigation * Download progress works properly for purchased but deleted files + * Publish channels for less than 1 LBC ### Deprecated * diff --git a/build/DAEMON_URL b/build/DAEMON_URL index efb0b928e..188944dec 100644 --- a/build/DAEMON_URL +++ b/build/DAEMON_URL @@ -1 +1 @@ -https://github.com/lbryio/lbry/releases/download/v0.11.0rc1/lbrynet-daemon-v0.11.0rc1-OSNAME.zip +https://github.com/lbryio/lbry/releases/download/v0.11.0rc3/lbrynet-daemon-v0.11.0rc3-OSNAME.zip diff --git a/ui/js/actions/app.js b/ui/js/actions/app.js index 709d85ffb..8609e1e3d 100644 --- a/ui/js/actions/app.js +++ b/ui/js/actions/app.js @@ -10,6 +10,10 @@ import { selectCurrentParams, } from "selectors/app"; import { doSearch } from "actions/search"; +import { doFetchDaemonSettings } from "actions/settings"; +import { doAuthenticate } from "actions/user"; +import { doRewardList } from "actions/rewards"; +import { doFileList } from "actions/file_info"; const { remote, ipcRenderer, shell } = require("electron"); const path = require("path"); @@ -216,8 +220,14 @@ export function doAlertError(errorList) { } export function doDaemonReady() { - return { - type: types.DAEMON_READY, + return function(dispatch, getState) { + dispatch(doAuthenticate()); + dispatch({ + type: types.DAEMON_READY, + }); + dispatch(doChangePath("/discover")); + dispatch(doFetchDaemonSettings()); + dispatch(doFileList()); }; } 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 4c12d9c20..14a2a8253 100644 --- a/ui/js/actions/rewards.js +++ b/ui/js/actions/rewards.js @@ -2,8 +2,9 @@ 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 doFetchRewards() { +export function doRewardList() { return function(dispatch, getState) { const state = getState(); @@ -11,25 +12,105 @@ export function doFetchRewards() { type: types.FETCH_REWARDS_STARTED, }); - lbryio.call("reward", "list", {}).then(function(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: [] }, + }); }); - }); }; } -export function doClaimReward(rewardType) { +export function doClaimRewardType(rewardType) { return function(dispatch, getState) { - try { - rewards.claimReward(rewards[rewardType]); - dispatch({ - type: types.REWARD_CLAIMED, - data: { - reward: rewards[rewardType], - }, - }); - } catch (err) {} + 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 }, + }); + + const success = reward => { + dispatch({ + type: types.CLAIM_REWARD_SUCCESS, + data: { + reward, + }, + }); + }; + + const failure = error => { + dispatch({ + type: types.CLAIM_REWARD_FAILURE, + data: { + reward, + 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; + } + + 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 }, + }); }; } diff --git a/ui/js/actions/user.js b/ui/js/actions/user.js new file mode 100644 index 000000000..972e167eb --- /dev/null +++ b/ui/js/actions/user.js @@ -0,0 +1,132 @@ +import * as types from "constants/action_types"; +import lbryio from "lbryio"; +import { setLocal } from "utils"; +import { doRewardList } from "actions/rewards"; +import { selectEmailToVerify } from "selectors/user"; + +export function doAuthenticate() { + return function(dispatch, getState) { + dispatch({ + type: types.AUTHENTICATION_STARTED, + }); + lbryio + .authenticate() + .then(user => { + dispatch({ + type: types.AUTHENTICATION_SUCCESS, + data: { user }, + }); + + dispatch(doRewardList()); //FIXME - where should this happen? + }) + .catch(error => { + dispatch({ + type: types.AUTHENTICATION_FAILURE, + data: { error }, + }); + }); + }; +} + +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({ + type: types.USER_EMAIL_NEW_STARTED, + email: email, + }); + lbryio.call("user_email", "new", { email }, "post").then( + () => { + dispatch({ + type: types.USER_EMAIL_NEW_SUCCESS, + data: { email }, + }); + dispatch(doUserFetch()); + }, + error => { + if ( + error.xhr && + (error.xhr.status == 409 || + error.message == "This email is already in use") + ) { + dispatch({ + type: types.USER_EMAIL_NEW_EXISTS, + data: { email }, + }); + } else { + dispatch({ + type: types.USER_EMAIL_NEW_FAILURE, + data: { error: error.message }, + }); + } + } + ); + }; +} + +export function doUserEmailDecline() { + return function(dispatch, getState) { + setLocal("user_email_declined", true); + dispatch({ + type: types.USER_EMAIL_DECLINE, + }); + }; +} + +export function doUserEmailVerify(verificationToken) { + return function(dispatch, getState) { + const email = selectEmailToVerify(getState()); + + dispatch({ + type: types.USER_EMAIL_VERIFY_STARTED, + code: verificationToken, + }); + + const failure = error => { + dispatch({ + type: types.USER_EMAIL_VERIFY_FAILURE, + data: { error: error.message }, + }); + }; + + lbryio + .call( + "user_email", + "confirm", + { verification_token: verificationToken, email: email }, + "post" + ) + .then(userEmail => { + if (userEmail.is_verified) { + dispatch({ + type: types.USER_EMAIL_VERIFY_SUCCESS, + data: { email }, + }); + dispatch(doUserFetch()); + } else { + failure(new Error("Your email is still not verified.")); //shouldn't happen? + } + }, failure); + }; +} diff --git a/ui/js/component/app/view.jsx b/ui/js/component/app/view.jsx index ff3dea060..f117c5f2e 100644 --- a/ui/js/component/app/view.jsx +++ b/ui/js/component/app/view.jsx @@ -4,6 +4,7 @@ import Header from "component/header"; import ErrorModal from "component/errorModal"; import DownloadingModal from "component/downloadingModal"; import UpgradeModal from "component/upgradeModal"; +import WelcomeModal from "component/welcomeModal"; import lbry from "lbry"; import { Line } from "rc-progress"; @@ -34,6 +35,7 @@ class App extends React.PureComponent { {modal == "upgrade" && } {modal == "downloading" && } {modal == "error" && } + {modal == "welcome" && } ); } diff --git a/ui/js/component/auth.js b/ui/js/component/auth.js deleted file mode 100644 index 3b5070e8f..000000000 --- a/ui/js/component/auth.js +++ /dev/null @@ -1,515 +0,0 @@ -import React from "react"; -import lbry from "../lbry.js"; -import lbryio from "../lbryio.js"; -import Modal from "./modal.js"; -import ModalPage from "./modal-page.js"; -import Link from "component/link"; -import { RewardLink } from "component/reward-link"; -import { FormRow } from "../component/form.js"; -import { CreditAmount, Address } from "../component/common.js"; -import { getLocal, setLocal } from "../utils.js"; -import rewards from "../rewards"; - -class SubmitEmailStage extends React.PureComponent { - constructor(props) { - super(props); - - this.state = { - rewardType: null, - email: "", - submitting: false, - }; - } - - handleEmailChanged(event) { - this.setState({ - email: event.target.value, - }); - } - - onEmailSaved(email) { - this.props.setStage("confirm", { email: email }); - } - - handleSubmit(event) { - event.preventDefault(); - - this.setState({ - submitting: true, - }); - lbryio.call("user_email", "new", { email: this.state.email }, "post").then( - () => { - this.onEmailSaved(this.state.email); - }, - error => { - if ( - error.xhr && - (error.xhr.status == 409 || - error.message == __("This email is already in use")) - ) { - this.onEmailSaved(this.state.email); - return; - } else if (this._emailRow) { - this._emailRow.showError(error.message); - } - this.setState({ submitting: false }); - } - ); - } - - render() { - return ( -
-
{ - this.handleSubmit(event); - }} - > - { - this._emailRow = ref; - }} - type="text" - label={__("Email")} - placeholder="scrwvwls@lbry.io" - name="email" - value={this.state.email} - onChange={event => { - this.handleEmailChanged(event); - }} - /> -
- { - this.handleSubmit(event); - }} - /> -
- -
- ); - } -} - -class ConfirmEmailStage extends React.PureComponent { - constructor(props) { - super(props); - - this.state = { - rewardType: null, - code: "", - submitting: false, - errorMessage: null, - }; - } - - handleCodeChanged(event) { - this.setState({ - code: event.target.value, - }); - } - - handleSubmit(event) { - event.preventDefault(); - this.setState({ - submitting: true, - }); - - const onSubmitError = error => { - if (this._codeRow) { - this._codeRow.showError(error.message); - } - this.setState({ submitting: false }); - }; - - lbryio - .call( - "user_email", - "confirm", - { verification_token: this.state.code, email: this.props.email }, - "post" - ) - .then(userEmail => { - if (userEmail.is_verified) { - this.props.setStage("welcome"); - } else { - onSubmitError(new Error(__("Your email is still not verified."))); //shouldn't happen? - } - }, onSubmitError); - } - - render() { - return ( -
-
{ - this.handleSubmit(event); - }} - > - { - this._codeRow = ref; - }} - type="text" - name="code" - placeholder="a94bXXXXXXXXXXXXXX" - value={this.state.code} - onChange={event => { - this.handleCodeChanged(event); - }} - helper={__( - "A verification code is required to access this version." - )} - /> -
- { - this.handleSubmit(event); - }} - /> -
-
- {__("No code?")} - {" "} - { - this.props.setStage("nocode"); - }} - label={__("Click here")} - />. -
- -
- ); - } -} - -class WelcomeStage extends React.PureComponent { - static propTypes = { - endAuth: React.PropTypes.func, - }; - - constructor(props) { - super(props); - - this.state = { - hasReward: false, - rewardAmount: null, - }; - } - - onRewardClaim(reward) { - this.setState({ - hasReward: true, - rewardAmount: reward.amount, - }); - } - - render() { - return !this.state.hasReward - ? -
-

{__("Welcome to LBRY.")}

-

- {__( - "Using LBRY is like dating a centaur. Totally normal up top, and way different underneath." - )} -

-

{__("Up top, LBRY is similar to popular media sites.")}

-

- {__( - "Below, LBRY is controlled by users -- you -- via blockchain and decentralization." - )} -

-

- {__( - "Thank you for making content freedom possible! Here's a nickel, kid." - )} -

-
- { - this.onRewardClaim(event); - }} - onRewardFailure={() => this.props.setStage(null)} - onConfirmed={() => { - this.props.setStage(null); - }} - /> -
-
-
- : { - this.props.setStage(null); - }} - > -
-

{__("About Your Reward")}

-

- {__("You earned a reward of ")} - {" "} - - {" "}{__('LBRY credits, or "LBC".')} -

-

- {__( - "This reward will show in your Wallet momentarily, probably while you are reading this message." - )} -

-

- {__( - "LBC is used to compensate creators, to publish, and to have say in how the network works." - )} -

-

- {__( - "No need to understand it all just yet! Try watching or downloading something next." - )} -

-

- {__( - "Finally, know that LBRY is an early beta and that it earns the name." - )} -

-
-
; - } -} - -const ErrorStage = props => { - return ( -
-

{__("An error was encountered that we cannot continue from.")}

-

{__("At least we're earning the name beta.")}

- {props.errorText ?

{__("Message:")} {props.errorText}

: ""} - { - window.location.reload(); - }} - /> -
- ); -}; - -const PendingStage = props => { - return ( -
-

- {__("Preparing for first access")} -

-
- ); -}; - -class CodeRequiredStage extends React.PureComponent { - constructor(props) { - super(props); - - this._balanceSubscribeId = null; - - this.state = { - balance: 0, - address: getLocal("wallet_address"), - }; - } - - componentWillMount() { - this._balanceSubscribeId = lbry.balanceSubscribe(balance => { - this.setState({ - balance: balance, - }); - }); - - if (!this.state.address) { - lbry.wallet_unused_address().then(address => { - setLocal("wallet_address", address); - this.setState({ address: address }); - }); - } - } - - componentWillUnmount() { - if (this._balanceSubscribeId) { - lbry.balanceUnsubscribe(this._balanceSubscribeId); - } - } - - render() { - const disabled = this.state.balance < 1; - return ( -
-
-

- {__( - "Access to LBRY is restricted as we build and scale the network." - )} -

-

{__("There are two ways in:")}

-

{__("Own LBRY Credits")}

-

{__("If you own at least 1 LBC, you can get in right now.")}

-

- { - setLocal("auth_bypassed", true); - this.props.setStage(null); - }} - disabled={disabled} - label={__("Let Me In")} - button={disabled ? "alt" : "primary"} - /> -

-

- {__("Your balance is ")}. {__("To increase your balance, send credits to this address:")} -

-

-

-

-

{__("If you don't understand how to send credits, then...")}

-
-
-

{__("Wait For A Code")}

-

- {__( - "If you provide your email, you'll automatically receive a notification when the system is open." - )} -

-

- { - this.props.setStage("email"); - }} - label={__("Return")} - /> -

-
-
- ); - } -} - -export class AuthOverlay extends React.PureComponent { - constructor(props) { - super(props); - - this._stages = { - pending: PendingStage, - error: ErrorStage, - nocode: CodeRequiredStage, - email: SubmitEmailStage, - confirm: ConfirmEmailStage, - welcome: WelcomeStage, - }; - - this.state = { - stage: "pending", - stageProps: {}, - }; - } - - setStage(stage, stageProps = {}) { - this.setState({ - stage: stage, - stageProps: stageProps, - }); - } - - componentWillMount() { - lbryio - .authenticate() - .then(user => { - if (!user.has_verified_email) { - if (getLocal("auth_bypassed")) { - this.setStage(null); - } else { - this.setStage("email", {}); - } - } else { - lbryio.call("reward", "list", {}).then(userRewards => { - userRewards.filter(function(reward) { - return ( - reward.reward_type == rewards.TYPE_NEW_USER && - reward.transaction_id - ); - }).length - ? this.setStage(null) - : this.setStage("welcome"); - }); - } - }) - .catch(err => { - this.setStage("error", { errorText: err.message }); - document.dispatchEvent( - new CustomEvent("unhandledError", { - detail: { - message: err.message, - data: err.stack, - }, - }) - ); - }); - } - - render() { - if (!this.state.stage) { - return null; - } - const StageContent = this._stages[this.state.stage]; - - if (!StageContent) { - return ( - {__("Unknown authentication step.")} - ); - } - - return this.state.stage != "welcome" - ? -

{__("LBRY Early Access")}

- { - this.setStage(stage, stageProps); - }} - /> -
- : { - this.setStage(stage, stageProps); - }} - {...this.state.stageProps} - />; - } -} diff --git a/ui/js/component/auth/index.js b/ui/js/component/auth/index.js new file mode 100644 index 000000000..37af9f90f --- /dev/null +++ b/ui/js/component/auth/index.js @@ -0,0 +1,16 @@ +import React from "react"; +import { connect } from "react-redux"; +import { + selectAuthenticationIsPending, + selectEmailToVerify, + selectUserIsVerificationCandidate, +} from "selectors/user"; +import Auth from "./view"; + +const select = state => ({ + isPending: selectAuthenticationIsPending(state), + email: selectEmailToVerify(state), + isVerificationCandidate: selectUserIsVerificationCandidate(state), +}); + +export default connect(select, null)(Auth); diff --git a/ui/js/component/auth/view.jsx b/ui/js/component/auth/view.jsx new file mode 100644 index 000000000..551113ffa --- /dev/null +++ b/ui/js/component/auth/view.jsx @@ -0,0 +1,22 @@ +import React from "react"; +import { BusyMessage } from "component/common"; +import UserEmailNew from "component/userEmailNew"; +import UserEmailVerify from "component/userEmailVerify"; + +export class Auth extends React.PureComponent { + render() { + const { isPending, email, isVerificationCandidate } = this.props; + + if (isPending) { + return ; + } else if (!email) { + return ; + } else if (isVerificationCandidate) { + return ; + } else { + return {__("No further steps.")}; + } + } +} + +export default Auth; diff --git a/ui/js/component/authOverlay/index.jsx b/ui/js/component/authOverlay/index.jsx new file mode 100644 index 000000000..ec7d7ee5a --- /dev/null +++ b/ui/js/component/authOverlay/index.jsx @@ -0,0 +1,24 @@ +import React from "react"; +import * as modal from "constants/modal_types"; +import { connect } from "react-redux"; +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), +}); + +const perform = dispatch => ({ + userEmailDecline: () => dispatch(doUserEmailDecline()), + openWelcomeModal: () => dispatch(doOpenModal(modal.WELCOME)), +}); + +export default connect(select, perform)(AuthOverlay); diff --git a/ui/js/component/authOverlay/view.jsx b/ui/js/component/authOverlay/view.jsx new file mode 100644 index 000000000..0cce29f9b --- /dev/null +++ b/ui/js/component/authOverlay/view.jsx @@ -0,0 +1,83 @@ +import React from "react"; +import lbryio from "lbryio.js"; +import ModalPage from "component/modal-page.js"; +import Auth from "component/auth"; +import Link from "component/link"; + +export class AuthOverlay extends React.PureComponent { + constructor(props) { + super(props); + + this.state = { + showNoEmailConfirm: false, + }; + } + + componentWillReceiveProps(nextProps) { + if (this.props.isShowing && !this.props.isPending && !nextProps.isShowing) { + setTimeout(() => this.props.openWelcomeModal(), 1); + } + } + + onEmailSkipClick() { + this.setState({ showNoEmailConfirm: true }); + } + + onEmailSkipConfirm() { + this.props.userEmailDecline(); + } + + render() { + if (!lbryio.enabled) { + return null; + } + + const { isPending, isShowing, hasEmail } = this.props; + + if (isShowing) { + return ( + +

LBRY Early Access

+ + {isPending + ? "" + :
+ {!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(); + }} + label={__("Continue without email")} + /> +
+ : { + hasEmail + ? this.onEmailSkipConfirm() + : this.onEmailSkipClick(); + }} + label={ + hasEmail ? __("Skip for now") : __("Do I have to?") + } + />} +
} +
+ ); + } + + return null; + } +} + +export default AuthOverlay; diff --git a/ui/js/component/form.js b/ui/js/component/form.js index 8ebbc2861..7ab78325c 100644 --- a/ui/js/component/form.js +++ b/ui/js/component/form.js @@ -178,9 +178,19 @@ export class FormRow extends React.PureComponent { this._fieldRequiredText = __("This field is required"); - this.state = { - isError: false, - errorMessage: null, + 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 + : "", }; } @@ -225,6 +235,7 @@ export class FormRow extends React.PureComponent { delete fieldProps.label; } delete fieldProps.helper; + delete fieldProps.errorMessage; return (
diff --git a/ui/js/component/reward-link.js b/ui/js/component/reward-link.js deleted file mode 100644 index 93486f406..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.PureComponent { - 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 new file mode 100644 index 000000000..81b01488b --- /dev/null +++ b/ui/js/component/rewardLink/index.js @@ -0,0 +1,35 @@ +import React from "react"; +import { connect } from "react-redux"; +import { + makeSelectHasClaimedReward, + makeSelectClaimRewardError, + makeSelectRewardByType, + makeSelectIsRewardClaimPending, +} from "selectors/rewards"; +import { doNavigate } from "actions/app"; +import { doClaimReward, doClaimRewardClearError } from "actions/rewards"; +import RewardLink from "./view"; + +const makeSelect = () => { + const selectHasClaimedReward = makeSelectHasClaimedReward(); + const selectIsPending = makeSelectIsRewardClaimPending(); + const selectReward = makeSelectRewardByType(); + const selectError = makeSelectClaimRewardError(); + + const select = (state, props) => ({ + isClaimed: selectHasClaimedReward(state, props), + errorMessage: selectError(state, props), + isPending: selectIsPending(state, props), + reward: selectReward(state, props), + }); + + return select; +}; + +const perform = dispatch => ({ + claimReward: reward => dispatch(doClaimReward(reward, true)), + clearError: reward => dispatch(doClaimRewardClearError(reward)), + navigate: path => dispatch(doNavigate(path)), +}); + +export default connect(makeSelect, perform)(RewardLink); diff --git a/ui/js/component/rewardLink/view.jsx b/ui/js/component/rewardLink/view.jsx new file mode 100644 index 000000000..6ebd50a20 --- /dev/null +++ b/ui/js/component/rewardLink/view.jsx @@ -0,0 +1,44 @@ +import React from "react"; +import { Icon } from "component/common"; +import Modal from "component/modal"; +import Link from "component/link"; + +const RewardLink = props => { + const { + reward, + button, + claimReward, + clearError, + errorMessage, + isClaimed, + isPending, + } = props; + + return ( +
+ {isClaimed + ? Reward claimed. + : { + claimReward(reward); + }} + />} + {errorMessage + ? { + clearError(reward); + }} + > + {errorMessage} + + : ""} +
+ ); +}; +export default RewardLink; diff --git a/ui/js/component/router/view.jsx b/ui/js/component/router/view.jsx index 131f222e6..1890992e1 100644 --- a/ui/js/component/router/view.jsx +++ b/ui/js/component/router/view.jsx @@ -7,9 +7,8 @@ import WalletPage from "page/wallet"; import ShowPage from "page/showPage"; import PublishPage from "page/publish"; import DiscoverPage from "page/discover"; -import SplashScreen from "component/splash.js"; import DeveloperPage from "page/developer.js"; -import RewardsPage from "page/rewards.js"; +import RewardsPage from "page/rewards"; import FileListDownloaded from "page/fileListDownloaded"; import FileListPublished from "page/fileListPublished"; import ChannelPage from "page/channel"; diff --git a/ui/js/component/userEmailNew/index.jsx b/ui/js/component/userEmailNew/index.jsx new file mode 100644 index 000000000..0b5e93d3e --- /dev/null +++ b/ui/js/component/userEmailNew/index.jsx @@ -0,0 +1,23 @@ +import React from 'react' +import { + connect +} from 'react-redux' +import { + doUserEmailNew +} from 'actions/user' +import { + selectEmailNewIsPending, + selectEmailNewErrorMessage, +} from 'selectors/user' +import UserEmailNew from './view' + +const select = (state) => ({ + isPending: selectEmailNewIsPending(state), + errorMessage: selectEmailNewErrorMessage(state), +}) + +const perform = (dispatch) => ({ + addUserEmail: (email) => dispatch(doUserEmailNew(email)) +}) + +export default connect(select, perform)(UserEmailNew) diff --git a/ui/js/component/userEmailNew/view.jsx b/ui/js/component/userEmailNew/view.jsx new file mode 100644 index 000000000..5391bdb3f --- /dev/null +++ b/ui/js/component/userEmailNew/view.jsx @@ -0,0 +1,61 @@ +import React from "react"; +import Link from "component/link"; +import { FormRow } from "component/form.js"; + +class UserEmailNew extends React.PureComponent { + constructor(props) { + super(props); + + this.state = { + email: "", + }; + } + + handleEmailChanged(event) { + this.setState({ + email: event.target.value, + }); + } + + handleSubmit(event) { + event.preventDefault(); + this.props.addUserEmail(this.state.email); + } + + render() { + const { errorMessage, isPending } = this.props; + + return ( +
{ + this.handleSubmit(event); + }} + > + { + this.handleEmailChanged(event); + }} + /> +
+ { + this.handleSubmit(event); + }} + /> +
+ + ); + } +} + +export default UserEmailNew; diff --git a/ui/js/component/userEmailVerify/index.jsx b/ui/js/component/userEmailVerify/index.jsx new file mode 100644 index 000000000..f2ae56c86 --- /dev/null +++ b/ui/js/component/userEmailVerify/index.jsx @@ -0,0 +1,21 @@ +import React from "react"; +import { connect } from "react-redux"; +import { doUserEmailVerify } from "actions/user"; +import { + selectEmailVerifyIsPending, + selectEmailToVerify, + selectEmailVerifyErrorMessage, +} from "selectors/user"; +import UserEmailVerify from "./view"; + +const select = state => ({ + isPending: selectEmailVerifyIsPending(state), + email: selectEmailToVerify(state), + errorMessage: selectEmailVerifyErrorMessage(state), +}); + +const perform = dispatch => ({ + verifyUserEmail: code => dispatch(doUserEmailVerify(code)), +}); + +export default connect(select, perform)(UserEmailVerify); diff --git a/ui/js/component/userEmailVerify/view.jsx b/ui/js/component/userEmailVerify/view.jsx new file mode 100644 index 000000000..c6cc65f34 --- /dev/null +++ b/ui/js/component/userEmailVerify/view.jsx @@ -0,0 +1,68 @@ +import React from "react"; +import Link from "component/link"; +import { FormRow } from "component/form.js"; + +class UserEmailVerify extends React.PureComponent { + constructor(props) { + super(props); + + this.state = { + code: "", + }; + } + + handleCodeChanged(event) { + this.setState({ + code: event.target.value, + }); + } + + handleSubmit(event) { + event.preventDefault(); + this.props.verifyUserEmail(this.state.code); + } + + render() { + const { errorMessage, isPending } = this.props; + + 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; diff --git a/ui/js/component/welcomeModal/index.jsx b/ui/js/component/welcomeModal/index.jsx new file mode 100644 index 000000000..bbfc5b6a7 --- /dev/null +++ b/ui/js/component/welcomeModal/index.jsx @@ -0,0 +1,28 @@ +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, + makeSelectClaimRewardError, + makeSelectRewardByType, +} from "selectors/rewards"; +import WelcomeModal from "./view"; + +const select = (state, props) => { + const selectHasClaimed = makeSelectHasClaimedReward(), + selectReward = makeSelectRewardByType(); + + return { + hasClaimed: selectHasClaimed(state, { reward_type: rewards.TYPE_NEW_USER }), + isRewardApproved: selectUserIsRewardApproved(state), + reward: selectReward(state, { reward_type: rewards.TYPE_NEW_USER }), + }; +}; + +const perform = dispatch => ({ + closeModal: () => dispatch(doCloseModal()), +}); + +export default connect(select, perform)(WelcomeModal); diff --git a/ui/js/component/welcomeModal/view.jsx b/ui/js/component/welcomeModal/view.jsx new file mode 100644 index 000000000..13e0cd5f1 --- /dev/null +++ b/ui/js/component/welcomeModal/view.jsx @@ -0,0 +1,75 @@ +import React from "react"; +import { Modal } from "component/modal"; +import { CreditAmount } from "component/common"; +import Link from "component/link"; +import RewardLink from "component/rewardLink"; + +class WelcomeModal extends React.PureComponent { + render() { + const { closeModal, hasClaimed, isRewardApproved, reward } = this.props; + + return !hasClaimed + ? +
+

Welcome to LBRY.

+

+ Using LBRY is like dating a centaur. Totally normal up top, and + {" "}way different underneath. +

+

Up top, LBRY is similar to popular media sites.

+

+ Below, LBRY is controlled by users -- you -- via blockchain and + decentralization. +

+

+ Thank you for making content freedom possible! + {" "}{isRewardApproved ? __("Here's a nickel, kid.") : ""} +

+
+ {isRewardApproved + ? + : } +
+
+
+ : +
+

About Your Reward

+

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

+

+ This reward will show in your Wallet momentarily, probably while + you are reading this message. +

+

+ LBC is used to compensate creators, to publish, and to have say in + how the network works. +

+

+ No need to understand it all just yet! Try watching or downloading + something next. +

+

+ Finally, know that LBRY is an early beta and that it earns the + name. +

+
+
; + } +} + +export default WelcomeModal; diff --git a/ui/js/constants/action_types.js b/ui/js/constants/action_types.js index 9e6a168a4..3450c1c1a 100644 --- a/ui/js/constants/action_types.js +++ b/ui/js/constants/action_types.js @@ -69,3 +69,27 @@ export const SEARCH_CANCELLED = "SEARCH_CANCELLED"; // Settings 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 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"; diff --git a/ui/js/constants/modal_types.js b/ui/js/constants/modal_types.js new file mode 100644 index 000000000..b34bb9afb --- /dev/null +++ b/ui/js/constants/modal_types.js @@ -0,0 +1 @@ +export const WELCOME = "welcome"; diff --git a/ui/js/lbryio.js b/ui/js/lbryio.js index b23697d15..11416fbd6 100644 --- a/ui/js/lbryio.js +++ b/ui/js/lbryio.js @@ -1,206 +1,192 @@ -import { getSession, setSession } from './utils.js'; -import lbry from './lbry.js'; +import { getSession, setSession, setLocal } from "./utils.js"; +import lbry from "./lbry.js"; -const querystring = require('querystring'); +const querystring = require("querystring"); const lbryio = { - _accessToken: getSession('accessToken'), - _authenticationPromise: null, - _user: null, - enabled: true + _accessToken: getSession("accessToken"), + _authenticationPromise: null, + enabled: true, }; const CONNECTION_STRING = process.env.LBRY_APP_API_URL - ? process.env.LBRY_APP_API_URL.replace(/\/*$/, '/') // exactly one slash at the end - : 'https://api.lbry.io/'; + ? process.env.LBRY_APP_API_URL.replace(/\/*$/, "/") // exactly one slash at the end + : "https://api.lbry.io/"; const EXCHANGE_RATE_TIMEOUT = 20 * 60 * 1000; lbryio._exchangePromise = null; lbryio._exchangeLastFetched = null; lbryio.getExchangeRates = function() { - if ( - !lbryio._exchangeLastFetched || - Date.now() - lbryio._exchangeLastFetched > EXCHANGE_RATE_TIMEOUT - ) { - lbryio._exchangePromise = new Promise((resolve, reject) => { - lbryio - .call('lbc', 'exchange_rate', {}, 'get', true) - .then(({ lbc_usd, lbc_btc, btc_usd }) => { - const rates = { lbc_usd, lbc_btc, btc_usd }; - resolve(rates); - }) - .catch(reject); - }); - lbryio._exchangeLastFetched = Date.now(); - } - return lbryio._exchangePromise; + if ( + !lbryio._exchangeLastFetched || + Date.now() - lbryio._exchangeLastFetched > EXCHANGE_RATE_TIMEOUT + ) { + lbryio._exchangePromise = new Promise((resolve, reject) => { + lbryio + .call("lbc", "exchange_rate", {}, "get", true) + .then(({ lbc_usd, lbc_btc, btc_usd }) => { + const rates = { lbc_usd, lbc_btc, btc_usd }; + resolve(rates); + }) + .catch(reject); + }); + lbryio._exchangeLastFetched = Date.now(); + } + 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 - return new Promise((resolve, reject) => { - if ( - !lbryio.enabled && - !evenIfDisabled && - (resource != 'discover' || action != 'list') - ) { - console.log(__('Internal API disabled')); - reject(new Error(__('LBRY internal API is disabled'))); - return; - } +lbryio.call = function(resource, action, params = {}, method = "get") { + return new Promise((resolve, reject) => { + if (!lbryio.enabled && (resource != "discover" || action != "list")) { + console.log(__("Internal API disabled")); + reject(new Error(__("LBRY internal API is disabled"))); + return; + } - const xhr = new XMLHttpRequest(); + const xhr = new XMLHttpRequest(); - xhr.addEventListener('error', function(event) { - reject( - new Error(__('Something went wrong making an internal API call.')) - ); - }); + xhr.addEventListener("error", function(event) { + reject( + new Error(__("Something went wrong making an internal API call.")) + ); + }); - xhr.addEventListener('timeout', function() { - reject(new Error(__('XMLHttpRequest connection timed out'))); - }); + xhr.addEventListener("timeout", function() { + reject(new Error(__("XMLHttpRequest connection timed out"))); + }); - xhr.addEventListener('load', function() { - const response = JSON.parse(xhr.responseText); + xhr.addEventListener("load", function() { + const response = JSON.parse(xhr.responseText); - if (!response.success) { - if (reject) { - let error = new Error(response.error); - error.xhr = xhr; - reject(error); - } else { - document.dispatchEvent( - new CustomEvent('unhandledError', { - detail: { - connectionString: connectionString, - method: action, - params: params, - message: response.error.message, - ...(response.error.data ? { data: response.error.data } : {}) - } - }) - ); - } - } else { - resolve(response.data); - } - }); + if (!response.success) { + if (reject) { + let error = new Error(response.error); + error.xhr = xhr; + reject(error); + } else { + document.dispatchEvent( + new CustomEvent("unhandledError", { + detail: { + connectionString: connectionString, + method: action, + params: params, + message: response.error.message, + ...(response.error.data ? { data: response.error.data } : {}), + }, + }) + ); + } + } else { + resolve(response.data); + } + }); - // For social media auth: - //const accessToken = localStorage.getItem('accessToken'); - //const fullParams = {...params, ... accessToken ? {access_token: accessToken} : {}}; + // For social media auth: + //const accessToken = localStorage.getItem('accessToken'); + //const fullParams = {...params, ... accessToken ? {access_token: accessToken} : {}}; - // Temp app ID based auth: - const fullParams = { app_id: lbryio.getAccessToken(), ...params }; + // Temp app ID based auth: + const fullParams = { app_id: lbryio.getAccessToken(), ...params }; - if (method == 'get') { - xhr.open( - 'get', - CONNECTION_STRING + - resource + - '/' + - action + - '?' + - querystring.stringify(fullParams), - true - ); - xhr.send(); - } else if (method == 'post') { - xhr.open('post', CONNECTION_STRING + resource + '/' + action, true); - xhr.setRequestHeader('Content-type', 'application/x-www-form-urlencoded'); - xhr.send(querystring.stringify(fullParams)); - } else { - reject(new Error(__('Invalid method'))); - } - }); + if (method == "get") { + xhr.open( + "get", + CONNECTION_STRING + + resource + + "/" + + action + + "?" + + querystring.stringify(fullParams), + true + ); + xhr.send(); + } else if (method == "post") { + xhr.open("post", CONNECTION_STRING + resource + "/" + action, true); + xhr.setRequestHeader("Content-type", "application/x-www-form-urlencoded"); + xhr.send(querystring.stringify(fullParams)); + } else { + reject(new Error(__("Invalid method"))); + } + }); }; lbryio.getAccessToken = () => { - const token = getSession('accessToken'); - return token ? token.toString().trim() : token; + const token = getSession("accessToken"); + return token ? token.toString().trim() : token; }; lbryio.setAccessToken = token => { - setSession('accessToken', token ? token.toString().trim() : token); + setSession("accessToken", token ? token.toString().trim() : token); +}; + +lbryio.setCurrentUser = (resolve, reject) => { + lbryio + .call("user", "me") + .then(data => { + resolve(data); + }) + .catch(function(err) { + lbryio.setAccessToken(null); + reject(err); + }); }; lbryio.authenticate = function() { - if (!lbryio.enabled) { - return new Promise((resolve, reject) => { - resolve({ - id: 1, - has_verified_email: true - }); - }); - } - if (lbryio._authenticationPromise === null) { - lbryio._authenticationPromise = new Promise((resolve, reject) => { - lbry - .status() - .then(response => { - let installation_id = response.installation_id; + if (!lbryio.enabled) { + return new Promise((resolve, reject) => { + resolve({ + id: 1, + language: "en", + has_email: true, + has_verified_email: true, + is_reward_approved: false, + is_reward_eligible: false, + }); + }); + } + if (lbryio._authenticationPromise === null) { + lbryio._authenticationPromise = new Promise((resolve, reject) => { + lbry + .status() + .then(response => { + let installation_id = response.installation_id; - function setCurrentUser() { - lbryio - .call('user', 'me') - .then(data => { - lbryio.user = data; - resolve(data); - }) - .catch(function(err) { - lbryio.setAccessToken(null); - if (!getSession('reloadedOnFailedAuth')) { - setSession('reloadedOnFailedAuth', true); - window.location.reload(); - } else { - reject(err); - } - }); - } - - if (!lbryio.getAccessToken()) { - lbryio - .call( - 'user', - 'new', - { - language: 'en', - app_id: installation_id - }, - 'post' - ) - .then(function(responseData) { - if (!responseData.id) { - reject( - new Error(__('Received invalid authentication response.')) - ); - } - lbryio.setAccessToken(installation_id); - setCurrentUser(); - }) - .catch(function(error) { - /* - until we have better error code format, assume all errors are duplicate application id - if we're wrong, this will be caught by later attempts to make a valid call - */ - lbryio.setAccessToken(installation_id); - setCurrentUser(); - }); - } else { - setCurrentUser(); - } - }) - .catch(reject); - }); - } - return lbryio._authenticationPromise; + if (!lbryio.getAccessToken()) { + lbryio + .call( + "user", + "new", + { + language: "en", + app_id: installation_id, + }, + "post" + ) + .then(function(responseData) { + if (!responseData.id) { + reject( + new Error("Received invalid authentication response.") + ); + } + lbryio.setAccessToken(installation_id); + lbryio.setCurrentUser(resolve, reject); + }) + .catch(function(error) { + /* + until we have better error code format, assume all errors are duplicate application id + if we're wrong, this will be caught by later attempts to make a valid call + */ + lbryio.setAccessToken(installation_id); + lbryio.setCurrentUser(resolve, reject); + }); + } else { + lbryio.setCurrentUser(resolve, reject); + } + }) + .catch(reject); + }); + } + return lbryio._authenticationPromise; }; export default lbryio; diff --git a/ui/js/main.js b/ui/js/main.js index 0c7c465e1..741c441da 100644 --- a/ui/js/main.js +++ b/ui/js/main.js @@ -1,18 +1,13 @@ import React from "react"; import ReactDOM from "react-dom"; -import whyDidYouUpdate from "why-did-you-update"; import lbry from "./lbry.js"; -import lbryio from "./lbryio.js"; -import lighthouse from "./lighthouse.js"; import App from "component/app/index.js"; import SnackBar from "component/snackBar"; import { Provider } from "react-redux"; import store from "store.js"; import SplashScreen from "component/splash.js"; -import { AuthOverlay } from "component/auth.js"; +import AuthOverlay from "component/authOverlay"; import { doChangePath, doNavigate, doDaemonReady } from "actions/app"; -import { doFetchDaemonSettings } from "actions/settings"; -import { doFileList } from "actions/file_info"; import { toQueryString } from "util/query_params"; const env = ENV; @@ -57,7 +52,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; @@ -68,31 +66,27 @@ document.addEventListener("click", event => { const initialState = app.store.getState(); -if (env === "development") { - /* - https://github.com/garbles/why-did-you-update - "A function that monkey patches React and notifies you in the console when - potentially unnecessary re-renders occur." - - Just checks if props change between updates. Can be fixed by manually - adding a check in shouldComponentUpdate or using React.PureComponent - */ - whyDidYouUpdate(React); -} +// import whyDidYouUpdate from "why-did-you-update"; +// if (env === "development") { +// /* +// https://github.com/garbles/why-did-you-update +// "A function that monkey patches React and notifies you in the console when +// potentially unnecessary re-renders occur." +// +// Just checks if props change between updates. Can be fixed by manually +// adding a check in shouldComponentUpdate or using React.PureComponent +// */ +// whyDidYouUpdate(React); +// } var init = function() { function onDaemonReady() { window.sessionStorage.setItem("loaded", "y"); //once we've made it here once per session, we don't need to show splash again - const actions = []; - app.store.dispatch(doDaemonReady()); - app.store.dispatch(doChangePath("/discover")); - app.store.dispatch(doFetchDaemonSettings()); - app.store.dispatch(doFileList()); ReactDOM.render( -
{lbryio.enabled ? : ""}
+
, canvas ); 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 19ac43ec4..90c7aad47 100644 --- a/ui/js/page/fileListPublished/view.jsx +++ b/ui/js/page/fileListPublished/view.jsx @@ -16,24 +16,7 @@ class FileListPublished extends React.PureComponent { } 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 89db3c109..bf31a3b5d 100644 --- a/ui/js/page/publish/view.jsx +++ b/ui/js/page/publish/view.jsx @@ -44,7 +44,7 @@ class PublishPage extends React.PureComponent { // 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 } : {}), diff --git a/ui/js/page/rewards.js b/ui/js/page/rewards.js deleted file mode 100644 index 1199ed0df..000000000 --- a/ui/js/page/rewards.js +++ /dev/null @@ -1,100 +0,0 @@ -import React from "react"; -import lbryio from "lbryio"; -import { CreditAmount, Icon } from "component/common.js"; -import SubHeader from "component/subHeader"; -import { RewardLink } from "component/reward-link"; - -export class RewardTile extends React.PureComponent { - static propTypes = { - type: React.PropTypes.string.isRequired, - title: React.PropTypes.string.isRequired, - description: React.PropTypes.string.isRequired, - claimed: React.PropTypes.bool.isRequired, - value: React.PropTypes.number.isRequired, - onRewardClaim: React.PropTypes.func, - }; - - render() { - return ( -
-
-
- -

{this.props.title}

-
-
- {this.props.claimed - ? {__("Reward claimed.")} - : } -
-
{this.props.description}
-
-
- ); - } -} - -export class RewardsPage extends React.PureComponent { - constructor(props) { - super(props); - - this.state = { - userRewards: null, - failed: null, - }; - } - - componentWillMount() { - this.loadRewards(); - } - - loadRewards() { - lbryio.call("reward", "list", {}).then( - userRewards => { - this.setState({ - userRewards: userRewards, - }); - }, - () => { - this.setState({ failed: true }); - } - ); - } - - render() { - return ( -
- -
- {!this.state.userRewards - ? this.state.failed - ?
{__("Failed to load rewards.")}
- : "" - : this.state.userRewards.map( - ({ - reward_type, - reward_title, - reward_description, - transaction_id, - reward_amount, - }) => { - return ( - - ); - } - )} -
-
- ); - } -} - -export default RewardsPage; diff --git a/ui/js/page/rewards/index.js b/ui/js/page/rewards/index.js new file mode 100644 index 000000000..61d0515e9 --- /dev/null +++ b/ui/js/page/rewards/index.js @@ -0,0 +1,20 @@ +import React from "react"; +import { connect } from "react-redux"; +import { doNavigate } from "actions/app"; +import { selectFetchingRewards, selectRewards } from "selectors/rewards"; +import { + selectUserIsRewardEligible, + selectUserHasEmail, + selectUserIsVerificationCandidate, +} from "selectors/user"; +import RewardsPage from "./view"; + +const select = state => ({ + fetching: selectFetchingRewards(state), + rewards: selectRewards(state), + hasEmail: selectUserHasEmail(state), + isEligible: selectUserIsRewardEligible(state), + isVerificationCandidate: selectUserIsVerificationCandidate(state), +}); + +export default connect(select, null)(RewardsPage); diff --git a/ui/js/page/rewards/view.jsx b/ui/js/page/rewards/view.jsx new file mode 100644 index 000000000..6f78a6490 --- /dev/null +++ b/ui/js/page/rewards/view.jsx @@ -0,0 +1,92 @@ +import React from "react"; +import lbryio from "lbryio"; +import { BusyMessage, CreditAmount, Icon } from "component/common"; +import SubHeader from "component/subHeader"; +import Auth from "component/auth"; +import Link from "component/link"; +import RewardLink from "component/rewardLink"; + +const RewardTile = props => { + const { reward } = props; + + const claimed = !!reward.transaction_id; + + return ( +
+
+
+ +

{reward.reward_title}

+
+
+ {claimed + ? Reward claimed. + : } +
+
{reward.reward_description}
+
+
+ ); +}; + +const RewardsPage = props => { + const { + fetching, + isEligible, + isVerificationCandidate, + hasEmail, + rewards, + } = props; + + let content, + isCard = false; + + if (!hasEmail || isVerificationCandidate) { + content = ( +
+

+ {__( + "Additional information is required to be eligible for the rewards program." + )} +

+ +
+ ); + isCard = true; + } else if (!isEligible) { + isCard = true; + content = ( +
+

{__("You are not eligible to claim rewards.")}

+

+ To become eligible, email + {" "} with a + link to a public social media profile. +

+
+ ); + } else if (fetching) { + content = ; + } else if (rewards.length > 0) { + content = rewards.map(reward => + + ); + } else { + content =
{__("Failed to load rewards.")}
; + } + + return ( +
+ + {isCard + ?
+
+ {content} +
+
+ : content} +
+ ); +}; + +export default RewardsPage; diff --git a/ui/js/page/settings/view.jsx b/ui/js/page/settings/view.jsx index f1476d5d3..b5139119e 100644 --- a/ui/js/page/settings/view.jsx +++ b/ui/js/page/settings/view.jsx @@ -229,25 +229,32 @@ class SettingsPage extends React.PureComponent {
- {/*}

{__("Language")}

- { this.onLanguageChange('en') }} - defaultChecked={this.state.language=='en'} /> + { + this.onLanguageChange("en"); + }} + defaultChecked={this.state.language == "en"} + />
- { this.onLanguageChange('rs') }} - defaultChecked={this.state.language=='rs'} /> + { + this.onLanguageChange("rs"); + }} + defaultChecked={this.state.language == "rs"} + />
*/} diff --git a/ui/js/reducers/app.js b/ui/js/reducers/app.js index 373f13c3c..d5c95bde5 100644 --- a/ui/js/reducers/app.js +++ b/ui/js/reducers/app.js @@ -81,12 +81,6 @@ reducers[types.UPGRADE_DOWNLOAD_PROGRESSED] = function(state, action) { }); }; -reducers[types.DAEMON_READY] = function(state, action) { - return Object.assign({}, state, { - daemonReady: true, - }); -}; - reducers[types.SHOW_SNACKBAR] = function(state, action) { const { message, linkText, linkTarget, isError } = action.data; const snackBar = Object.assign({}, state.snackBar); diff --git a/ui/js/reducers/rewards.js b/ui/js/reducers/rewards.js index c9c3efc21..0994d730b 100644 --- a/ui/js/reducers/rewards.js +++ b/ui/js/reducers/rewards.js @@ -1,7 +1,90 @@ import * as types from "constants/action_types"; const reducers = {}; -const defaultState = {}; +const defaultState = { + fetching: false, + rewardsByType: {}, + claimPendingByType: {}, + claimErrorsByType: {}, +}; + +reducers[types.FETCH_REWARDS_STARTED] = function(state, action) { + return Object.assign({}, state, { + fetching: true, + }); +}; + +reducers[types.FETCH_REWARDS_COMPLETED] = function(state, action) { + const { userRewards } = action.data; + + const rewardsByType = {}; + userRewards.forEach(reward => (rewardsByType[reward.reward_type] = reward)); + + 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); + if (isClaiming) { + newClaimPendingByType[reward.reward_type] = isClaiming; + } else { + delete newClaimPendingByType[reward.reward_type]; + } + if (errorMessage) { + newClaimErrorsByType[reward.reward_type] = errorMessage; + } else { + 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; + + return setClaimRewardState(state, reward, true, ""); +}; + +reducers[types.CLAIM_REWARD_SUCCESS] = function(state, action) { + const { reward } = action.data; + + 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; + + return setClaimRewardState(state, reward, false, error ? error.message : ""); +}; + +reducers[types.CLAIM_REWARD_CLEAR_ERROR] = function(state, action) { + const { reward } = action.data; + + 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 new file mode 100644 index 000000000..4e4ffda40 --- /dev/null +++ b/ui/js/reducers/user.js @@ -0,0 +1,127 @@ +import * as types from "constants/action_types"; +import { getLocal } from "utils"; + +const reducers = {}; + +const defaultState = { + authenticationIsPending: false, + userIsPending: false, + emailNewIsPending: false, + emailNewErrorMessage: "", + emailNewDeclined: getLocal("user_email_declined", false), + emailToVerify: "", + user: undefined, +}; + +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, + }); +}; + +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, + }); +}; + +reducers[types.USER_EMAIL_DECLINE] = function(state, action) { + return Object.assign({}, state, { + emailNewDeclined: true, + }); +}; + +reducers[types.USER_EMAIL_NEW_STARTED] = function(state, action) { + return Object.assign({}, state, { + emailNewIsPending: true, + emailNewErrorMessage: "", + }); +}; + +reducers[types.USER_EMAIL_NEW_SUCCESS] = function(state, action) { + let user = Object.assign({}, state.user); + user.has_email = true; + return Object.assign({}, state, { + emailToVerify: action.data.email, + emailNewIsPending: false, + user: user, + }); +}; + +reducers[types.USER_EMAIL_NEW_EXISTS] = function(state, action) { + let user = Object.assign({}, state.user); + return Object.assign({}, state, { + emailToVerify: action.data.email, + emailNewIsPending: false, + }); +}; + +reducers[types.USER_EMAIL_NEW_FAILURE] = function(state, action) { + return Object.assign({}, state, { + emailNewIsPending: false, + emailNewErrorMessage: action.data.error, + }); +}; + +reducers[types.USER_EMAIL_VERIFY_STARTED] = function(state, action) { + return Object.assign({}, state, { + emailVerifyIsPending: true, + emailVerifyErrorMessage: "", + }); +}; + +reducers[types.USER_EMAIL_VERIFY_SUCCESS] = function(state, action) { + let user = Object.assign({}, state.user); + user.has_email = true; + return Object.assign({}, state, { + emailToVerify: "", + emailVerifyIsPending: false, + user: user, + }); +}; + +reducers[types.USER_EMAIL_VERIFY_FAILURE] = function(state, action) { + return Object.assign({}, state, { + emailVerifyIsPending: false, + emailVerifyErrorMessage: action.data.error, + }); +}; + +export default function reducer(state = defaultState, action) { + const handler = reducers[action.type]; + if (handler) return handler(state, action); + return state; +} diff --git a/ui/js/rewards.js b/ui/js/rewards.js index 25f6dbe82..08d82fd9b 100644 --- a/ui/js/rewards.js +++ b/ui/js/rewards.js @@ -1,222 +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) { - 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); - } + resolve(reward); + }, reject); + } - return new Promise((resolve, reject) => { - lbry.wallet_unused_address().then(address => { - const params = { - reward_type: type, - wallet_address: address - }; + 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; + 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.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_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); - } - }); - }); -}; - -rewards.claimEligiblePurchaseRewards = function() { - 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); - } - }, - () => {} - ); + case rewards.TYPE_FIRST_STREAM: + case rewards.TYPE_NEW_USER: + default: + requestReward(resolve, reject, params); + } + }); + }); }; export default rewards; diff --git a/ui/js/selectors/availability.js b/ui/js/selectors/availability.js index df330fed0..1200dd904 100644 --- a/ui/js/selectors/availability.js +++ b/ui/js/selectors/availability.js @@ -1,5 +1,4 @@ import { createSelector } from "reselect"; -import { selectDaemonReady, selectCurrentPage } from "selectors/app"; const _selectState = state => state.availability; diff --git a/ui/js/selectors/content.js b/ui/js/selectors/content.js index 78d81ab99..75162a454 100644 --- a/ui/js/selectors/content.js +++ b/ui/js/selectors/content.js @@ -1,5 +1,4 @@ import { createSelector } from "reselect"; -import { selectDaemonReady, selectCurrentPage } from "selectors/app"; export const _selectState = state => state.content || {}; diff --git a/ui/js/selectors/rewards.js b/ui/js/selectors/rewards.js index 481b1f184..7a6a20429 100644 --- a/ui/js/selectors/rewards.js +++ b/ui/js/selectors/rewards.js @@ -1,3 +1,67 @@ import { createSelector } from "reselect"; +import { selectUser } from "selectors/user"; -export const _selectState = state => state.rewards || {}; +const _selectState = state => state.rewards || {}; + +export const selectRewardsByType = createSelector( + _selectState, + state => state.rewardsByType || {} +); + +export const selectRewards = createSelector( + selectRewardsByType, + byType => Object.values(byType) || [] +); + +export const selectIsRewardEligible = createSelector( + selectUser, + user => user.can_claim_rewards +); + +export const selectFetchingRewards = createSelector( + _selectState, + state => !!state.fetching +); + +export const selectHasClaimedReward = (state, props) => { + const reward = selectRewardsByType(state)[props.reward_type]; + return reward && reward.transaction_id !== ""; +}; + +export const makeSelectHasClaimedReward = () => { + return createSelector(selectHasClaimedReward, claimed => claimed); +}; + +export const selectClaimsPendingByType = createSelector( + _selectState, + state => state.claimPendingByType +); + +const selectIsClaimRewardPending = (state, props) => { + return selectClaimsPendingByType(state, props)[props.reward_type]; +}; + +export const makeSelectIsRewardClaimPending = () => { + return createSelector(selectIsClaimRewardPending, isClaiming => isClaiming); +}; + +export const selectClaimErrorsByType = createSelector( + _selectState, + state => state.claimErrorsByType +); + +const selectClaimRewardError = (state, props) => { + return selectClaimErrorsByType(state, props)[props.reward_type]; +}; + +export const makeSelectClaimRewardError = () => { + return createSelector(selectClaimRewardError, errorMessage => errorMessage); +}; + +const selectRewardByType = (state, props) => { + return selectRewardsByType(state)[props.reward_type]; +}; + +export const makeSelectRewardByType = () => { + return createSelector(selectRewardByType, reward => reward); +}; diff --git a/ui/js/selectors/user.js b/ui/js/selectors/user.js new file mode 100644 index 000000000..f7104d3d4 --- /dev/null +++ b/ui/js/selectors/user.js @@ -0,0 +1,82 @@ +import { createSelector } from "reselect"; + +export const _selectState = state => state.user || {}; + +export const selectAuthenticationIsPending = createSelector( + _selectState, + state => state.authenticationIsPending +); + +export const selectUserIsPending = createSelector( + _selectState, + state => state.userIsPending +); + +export const selectUser = createSelector( + _selectState, + state => state.user || {} +); + +export const selectEmailToVerify = createSelector( + _selectState, + state => state.emailToVerify +); + +export const selectUserHasEmail = createSelector( + selectUser, + selectEmailToVerify, + (user, email) => (user && user.has_email) || email +); + +export const selectUserIsRewardEligible = createSelector( + selectUser, + user => user && user.is_reward_eligible +); + +export const selectUserIsRewardApproved = createSelector( + selectUser, + user => user && user.is_reward_approved +); + +export const selectEmailNewIsPending = createSelector( + _selectState, + state => state.emailNewIsPending +); + +export const selectEmailNewErrorMessage = createSelector( + _selectState, + state => state.emailNewErrorMessage +); + +export const selectEmailNewDeclined = createSelector( + _selectState, + state => state.emailNewDeclined +); + +export const selectEmailVerifyIsPending = createSelector( + _selectState, + state => state.emailVerifyIsPending +); + +export const selectEmailVerifyErrorMessage = createSelector( + _selectState, + state => state.emailVerifyErrorMessage +); + +export const selectUserIsVerificationCandidate = createSelector( + selectUserIsRewardEligible, + selectUserIsRewardApproved, + selectEmailToVerify, + selectUser, + (isEligible, isApproved, emailToVerify, user) => + (isEligible && !isApproved) || (emailToVerify && user && !user.has_email) +); + +export const selectUserIsAuthRequested = createSelector( + selectEmailNewDeclined, + selectAuthenticationIsPending, + selectUserIsVerificationCandidate, + selectUserHasEmail, + (isEmailDeclined, isPending, isVerificationCandidate, hasEmail) => + !isEmailDeclined && (isPending || !hasEmail || isVerificationCandidate) +); diff --git a/ui/js/selectors/wallet.js b/ui/js/selectors/wallet.js index b74cd01da..1027bb6e7 100644 --- a/ui/js/selectors/wallet.js +++ b/ui/js/selectors/wallet.js @@ -50,20 +50,6 @@ export const selectGettingNewAddress = createSelector( state => state.gettingNewAddress ); -export const shouldCheckAddressIsMine = createSelector( - _selectState, - selectCurrentPage, - selectReceiveAddress, - selectDaemonReady, - (state, page, address, daemonReady) => { - if (!daemonReady) return false; - if (address === undefined) return false; - if (state.addressOwnershipChecked) return false; - - return true; - } -); - export const selectDraftTransaction = createSelector( _selectState, state => state.draftTransaction || {} diff --git a/ui/js/store.js b/ui/js/store.js index 112f19015..9bc70dc25 100644 --- a/ui/js/store.js +++ b/ui/js/store.js @@ -13,6 +13,7 @@ import rewardsReducer from 'reducers/rewards'; import searchReducer from 'reducers/search'; import settingsReducer from 'reducers/settings'; import walletReducer from 'reducers/wallet'; +import userReducer from 'reducers/user'; function isFunction(object) { return typeof object === 'function'; @@ -47,16 +48,17 @@ function enableBatching(reducer) { } const reducers = redux.combineReducers({ - app: appReducer, - availability: availabilityReducer, - claims: claimsReducer, - fileInfo: fileInfoReducer, - content: contentReducer, - costInfo: costInfoReducer, - rewards: rewardsReducer, - search: searchReducer, - settings: settingsReducer, - wallet: walletReducer + app: appReducer, + availability: availabilityReducer, + claims: claimsReducer, + fileInfo: fileInfoReducer, + content: contentReducer, + costInfo: costInfoReducer, + rewards: rewardsReducer, + search: searchReducer, + settings: settingsReducer, + wallet: walletReducer, + user: userReducer, }); const bulkThunk = createBulkThunkMiddleware(); 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; +} diff --git a/ui/scss/component/_card.scss b/ui/scss/component/_card.scss index 2e325d827..4dd88a91e 100644 --- a/ui/scss/component/_card.scss +++ b/ui/scss/component/_card.scss @@ -25,7 +25,7 @@ $padding-card-horizontal: $spacing-vertical * 2/3; } .card__title-primary { padding: 0 $padding-card-horizontal; - margin-top: $spacing-vertical; + margin-top: $spacing-vertical * 2/3; } .card__title-identity { padding: 0 $padding-card-horizontal; diff --git a/ui/scss/component/_form-field.scss b/ui/scss/component/_form-field.scss index 2d471f760..8fd86efef 100644 --- a/ui/scss/component/_form-field.scss +++ b/ui/scss/component/_form-field.scss @@ -3,6 +3,10 @@ $width-input-border: 2px; $width-input-text: 330px; +.form-input-width { + width: $width-input-text +} + .form-row-submit { margin-top: $spacing-vertical;