Merge branch 'tear_down_this_wall'

* tear_down_this_wall:
  bump daemon version
  restore purecomponent, remove console.log
  changelog and bugfix
  reviewable
  mostly done?
  previously uncommitted copy change
  not enough progress
  uncommitted work from last night
  good chunk of progress towards auth and rewards refactor / degating
  Working on rewards refactor
  midway through auth rewrite
  slight progress
This commit is contained in:
Alex Grintsvayg 2017-06-09 10:18:25 -04:00
commit 902fb96878
48 changed files with 1641 additions and 1200 deletions

View file

@ -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
*

View file

@ -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

View file

@ -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());
};
}

View file

@ -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());
};
}

View file

@ -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 },
});
};
}

132
ui/js/actions/user.js Normal file
View file

@ -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);
};
}

View file

@ -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" && <UpgradeModal />}
{modal == "downloading" && <DownloadingModal />}
{modal == "error" && <ErrorModal />}
{modal == "welcome" && <WelcomeModal />}
</div>
);
}

View file

@ -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 (
<section>
<form
onSubmit={event => {
this.handleSubmit(event);
}}
>
<FormRow
ref={ref => {
this._emailRow = ref;
}}
type="text"
label={__("Email")}
placeholder="scrwvwls@lbry.io"
name="email"
value={this.state.email}
onChange={event => {
this.handleEmailChanged(event);
}}
/>
<div className="form-row-submit">
<Link
button="primary"
label={__("Next")}
disabled={this.state.submitting}
onClick={event => {
this.handleSubmit(event);
}}
/>
</div>
</form>
</section>
);
}
}
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 (
<section>
<form
onSubmit={event => {
this.handleSubmit(event);
}}
>
<FormRow
label={__("Verification Code")}
ref={ref => {
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."
)}
/>
<div className="form-row-submit form-row-submit--with-footer">
<Link
button="primary"
label={__("Verify")}
disabled={this.state.submitting}
onClick={event => {
this.handleSubmit(event);
}}
/>
</div>
<div className="form-field__helper">
{__("No code?")}
{" "}
<Link
onClick={() => {
this.props.setStage("nocode");
}}
label={__("Click here")}
/>.
</div>
</form>
</section>
);
}
}
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
? <Modal
type="custom"
isOpen={true}
contentLabel={__("Welcome to LBRY")}
{...this.props}
>
<section>
<h3 className="modal__header">{__("Welcome to LBRY.")}</h3>
<p>
{__(
"Using LBRY is like dating a centaur. Totally normal up top, and way different underneath."
)}
</p>
<p>{__("Up top, LBRY is similar to popular media sites.")}</p>
<p>
{__(
"Below, LBRY is controlled by users -- you -- via blockchain and decentralization."
)}
</p>
<p>
{__(
"Thank you for making content freedom possible! Here's a nickel, kid."
)}
</p>
<div style={{ textAlign: "center", marginBottom: "12px" }}>
<RewardLink
type="new_user"
button="primary"
onRewardClaim={event => {
this.onRewardClaim(event);
}}
onRewardFailure={() => this.props.setStage(null)}
onConfirmed={() => {
this.props.setStage(null);
}}
/>
</div>
</section>
</Modal>
: <Modal
type="alert"
overlayClassName="modal-overlay modal-overlay--clear"
isOpen={true}
contentLabel={__("Welcome to LBRY")}
{...this.props}
onConfirmed={() => {
this.props.setStage(null);
}}
>
<section>
<h3 className="modal__header">{__("About Your Reward")}</h3>
<p>
{__("You earned a reward of ")}
{" "}
<CreditAmount amount={this.state.rewardAmount} label={false} />
{" "}{__('LBRY credits, or "LBC".')}
</p>
<p>
{__(
"This reward will show in your Wallet momentarily, probably while you are reading this message."
)}
</p>
<p>
{__(
"LBC is used to compensate creators, to publish, and to have say in how the network works."
)}
</p>
<p>
{__(
"No need to understand it all just yet! Try watching or downloading something next."
)}
</p>
<p>
{__(
"Finally, know that LBRY is an early beta and that it earns the name."
)}
</p>
</section>
</Modal>;
}
}
const ErrorStage = props => {
return (
<section>
<p>{__("An error was encountered that we cannot continue from.")}</p>
<p>{__("At least we're earning the name beta.")}</p>
{props.errorText ? <p>{__("Message:")} {props.errorText}</p> : ""}
<Link
button="alt"
label={__("Try Reload")}
onClick={() => {
window.location.reload();
}}
/>
</section>
);
};
const PendingStage = props => {
return (
<section>
<p>
{__("Preparing for first access")} <span className="busy-indicator" />
</p>
</section>
);
};
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 (
<div>
<section className="section-spaced">
<p>
{__(
"Access to LBRY is restricted as we build and scale the network."
)}
</p>
<p>{__("There are two ways in:")}</p>
<h3>{__("Own LBRY Credits")}</h3>
<p>{__("If you own at least 1 LBC, you can get in right now.")}</p>
<p style={{ textAlign: "center" }}>
<Link
onClick={() => {
setLocal("auth_bypassed", true);
this.props.setStage(null);
}}
disabled={disabled}
label={__("Let Me In")}
button={disabled ? "alt" : "primary"}
/>
</p>
<p>
{__("Your balance is ")}<CreditAmount
amount={this.state.balance}
/>. {__("To increase your balance, send credits to this address:")}
</p>
<p>
<Address
address={
this.state.address
? this.state.address
: __("Generating Address...")
}
/>
</p>
<p>{__("If you don't understand how to send credits, then...")}</p>
</section>
<section>
<h3>{__("Wait For A Code")}</h3>
<p>
{__(
"If you provide your email, you'll automatically receive a notification when the system is open."
)}
</p>
<p>
<Link
onClick={() => {
this.props.setStage("email");
}}
label={__("Return")}
/>
</p>
</section>
</div>
);
}
}
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 (
<span className="empty">{__("Unknown authentication step.")}</span>
);
}
return this.state.stage != "welcome"
? <ModalPage
className="modal-page--full"
isOpen={true}
contentLabel={__("Authentication")}
>
<h1>{__("LBRY Early Access")}</h1>
<StageContent
{...this.state.stageProps}
setStage={(stage, stageProps) => {
this.setStage(stage, stageProps);
}}
/>
</ModalPage>
: <StageContent
setStage={(stage, stageProps) => {
this.setStage(stage, stageProps);
}}
{...this.state.stageProps}
/>;
}
}

View file

@ -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);

View file

@ -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 <BusyMessage message={__("Authenticating")} />;
} else if (!email) {
return <UserEmailNew />;
} else if (isVerificationCandidate) {
return <UserEmailVerify />;
} else {
return <span className="empty">{__("No further steps.")}</span>;
}
}
}
export default Auth;

View file

@ -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);

View file

@ -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 (
<ModalPage
className="modal-page--full"
isOpen={true}
contentLabel="Authentication"
>
<h1>LBRY Early Access</h1>
<Auth />
{isPending
? ""
: <div className="form-row-submit">
{!hasEmail && this.state.showNoEmailConfirm
? <div className="help form-input-width">
<p>
{__(
"If you continue without an email, you will be ineligible to earn free LBC rewards, as well as unable to receive security related communications."
)}
</p>
<Link
onClick={() => {
this.onEmailSkipConfirm();
}}
label={__("Continue without email")}
/>
</div>
: <Link
className={"button-text-help"}
onClick={() => {
hasEmail
? this.onEmailSkipConfirm()
: this.onEmailSkipClick();
}}
label={
hasEmail ? __("Skip for now") : __("Do I have to?")
}
/>}
</div>}
</ModalPage>
);
}
return null;
}
}
export default AuthOverlay;

View file

@ -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 (
<div className="form-row">

View file

@ -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 (
<div className="reward-link">
{this.props.claimed
? <span><Icon icon="icon-check" /> {__("Reward claimed.")}</span>
: <Link
button={this.props.button ? this.props.button : "alt"}
disabled={this.state.pending || !this.state.claimable}
label={
this.state.pending ? __("Claiming...") : __("Claim Reward")
}
onClick={() => {
this.claimReward();
}}
/>}
{this.state.errorMessage
? <Modal
isOpen={true}
contentLabel={__("Reward Claim Error")}
className="error-modal"
onConfirmed={() => {
this.clearError();
}}
>
{this.state.errorMessage}
</Modal>
: ""}
</div>
);
}
}

View file

@ -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);

View file

@ -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 (
<div className="reward-link">
{isClaimed
? <span><Icon icon="icon-check" /> Reward claimed.</span>
: <Link
button={button ? button : "alt"}
disabled={isPending}
label={isPending ? "Claiming..." : "Claim Reward"}
onClick={() => {
claimReward(reward);
}}
/>}
{errorMessage
? <Modal
isOpen={true}
contentLabel="Reward Claim Error"
className="error-modal"
onConfirmed={() => {
clearError(reward);
}}
>
{errorMessage}
</Modal>
: ""}
</div>
);
};
export default RewardLink;

View file

@ -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";

View file

@ -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)

View file

@ -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 (
<form
className="form-input-width"
onSubmit={event => {
this.handleSubmit(event);
}}
>
<FormRow
type="text"
label="Email"
placeholder="scrwvwls@lbry.io"
name="email"
value={this.state.email}
errorMessage={errorMessage}
onChange={event => {
this.handleEmailChanged(event);
}}
/>
<div className="form-row-submit">
<Link
button="primary"
label="Next"
disabled={isPending}
onClick={event => {
this.handleSubmit(event);
}}
/>
</div>
</form>
);
}
}
export default UserEmailNew;

View file

@ -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);

View file

@ -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 (
<form
className="form-input-width"
onSubmit={event => {
this.handleSubmit(event);
}}
>
<FormRow
type="text"
label="Verification Code"
placeholder="a94bXXXXXXXXXXXXXX"
name="code"
value={this.state.code}
onChange={event => {
this.handleCodeChanged(event);
}}
errorMessage={errorMessage}
/>
{/* render help separately so it always shows */}
<div className="form-field__helper">
<p>
Email <Link href="mailto:help@lbry.io" label="help@lbry.io" /> if
you did not receive or are having trouble with your code.
</p>
</div>
<div className="form-row-submit form-row-submit--with-footer">
<Link
button="primary"
label="Verify"
disabled={this.state.submitting}
onClick={event => {
this.handleSubmit(event);
}}
/>
</div>
</form>
);
}
}
export default UserEmailVerify;

View file

@ -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);

View file

@ -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
? <Modal type="custom" isOpen={true} contentLabel="Welcome to LBRY">
<section>
<h3 className="modal__header">Welcome to LBRY.</h3>
<p>
Using LBRY is like dating a centaur. Totally normal up top, and
{" "}<em>way different</em> underneath.
</p>
<p>Up top, LBRY is similar to popular media sites.</p>
<p>
Below, LBRY is controlled by users -- you -- via blockchain and
decentralization.
</p>
<p>
Thank you for making content freedom possible!
{" "}{isRewardApproved ? __("Here's a nickel, kid.") : ""}
</p>
<div className="text-center">
{isRewardApproved
? <RewardLink reward_type="new_user" button="primary" />
: <Link
button="primary"
onClick={closeModal}
label="Continue"
/>}
</div>
</section>
</Modal>
: <Modal
type="alert"
overlayClassName="modal-overlay modal-overlay--clear"
isOpen={true}
contentLabel="Welcome to LBRY"
onConfirmed={closeModal}
>
<section>
<h3 className="modal__header">About Your Reward</h3>
<p>
You earned a reward of
{" "}<CreditAmount amount={reward.reward_amount} label={false} />
{" "}LBRY
credits, or <em>LBC</em>.
</p>
<p>
This reward will show in your Wallet momentarily, probably while
you are reading this message.
</p>
<p>
LBC is used to compensate creators, to publish, and to have say in
how the network works.
</p>
<p>
No need to understand it all just yet! Try watching or downloading
something next.
</p>
<p>
Finally, know that LBRY is an early beta and that it earns the
name.
</p>
</section>
</Modal>;
}
}
export default WelcomeModal;

View file

@ -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";

View file

@ -0,0 +1 @@
export const WELCOME = "welcome";

View file

@ -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;

View file

@ -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(
<Provider store={store}>
<div>{lbryio.enabled ? <AuthOverlay /> : ""}<App /><SnackBar /></div>
<div><AuthOverlay /><App /><SnackBar /></div>
</Provider>,
canvas
);

View file

@ -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);

View file

@ -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() {

View file

@ -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);

View file

@ -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 } : {}),

View file

@ -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 (
<section className="card">
<div className="card__inner">
<div className="card__title-primary">
<CreditAmount amount={this.props.value} />
<h3>{this.props.title}</h3>
</div>
<div className="card__actions">
{this.props.claimed
? <span><Icon icon="icon-check" /> {__("Reward claimed.")}</span>
: <RewardLink {...this.props} />}
</div>
<div className="card__content">{this.props.description}</div>
</div>
</section>
);
}
}
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 (
<main className="main--single-column">
<SubHeader />
<div>
{!this.state.userRewards
? this.state.failed
? <div className="empty">{__("Failed to load rewards.")}</div>
: ""
: this.state.userRewards.map(
({
reward_type,
reward_title,
reward_description,
transaction_id,
reward_amount,
}) => {
return (
<RewardTile
key={reward_type}
onRewardClaim={this.loadRewards}
type={reward_type}
title={__(reward_title)}
description={__(reward_description)}
claimed={!!transaction_id}
value={reward_amount}
/>
);
}
)}
</div>
</main>
);
}
}
export default RewardsPage;

View file

@ -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);

View file

@ -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 (
<section className="card">
<div className="card__inner">
<div className="card__title-primary">
<CreditAmount amount={reward.reward_amount} />
<h3>{reward.reward_title}</h3>
</div>
<div className="card__actions">
{claimed
? <span><Icon icon="icon-check" /> Reward claimed.</span>
: <RewardLink reward_type={reward.reward_type} />}
</div>
<div className="card__content">{reward.reward_description}</div>
</div>
</section>
);
};
const RewardsPage = props => {
const {
fetching,
isEligible,
isVerificationCandidate,
hasEmail,
rewards,
} = props;
let content,
isCard = false;
if (!hasEmail || isVerificationCandidate) {
content = (
<div>
<p>
{__(
"Additional information is required to be eligible for the rewards program."
)}
</p>
<Auth />
</div>
);
isCard = true;
} else if (!isEligible) {
isCard = true;
content = (
<div className="empty">
<p>{__("You are not eligible to claim rewards.")}</p>
<p>
To become eligible, email
{" "}<Link href="mailto:help@lbry.io" label="help@lbry.io" /> with a
link to a public social media profile.
</p>
</div>
);
} else if (fetching) {
content = <BusyMessage message="Fetching rewards" />;
} else if (rewards.length > 0) {
content = rewards.map(reward =>
<RewardTile key={reward.reward_type} reward={reward} />
);
} else {
content = <div className="empty">{__("Failed to load rewards.")}</div>;
}
return (
<main className="main--single-column">
<SubHeader />
{isCard
? <section className="card">
<div className="card__content">
{content}
</div>
</section>
: content}
</main>
);
};
export default RewardsPage;

View file

@ -229,25 +229,32 @@ class SettingsPage extends React.PureComponent {
</div>
</section>
{/*}
<section className="card">
<div className="card__content">
<h3>{__("Language")}</h3>
</div>
<div className="card__content">
<div className="form-row">
<FormField type="radio"
name="language"
label={__("English")}
onChange={() => { this.onLanguageChange('en') }}
defaultChecked={this.state.language=='en'} />
<FormField
type="radio"
name="language"
label={__("English")}
onChange={() => {
this.onLanguageChange("en");
}}
defaultChecked={this.state.language == "en"}
/>
</div>
<div className="form-row">
<FormField type="radio"
name="language"
label="Serbian"
onChange={() => { this.onLanguageChange('rs') }}
defaultChecked={this.state.language=='rs'} />
<FormField
type="radio"
name="language"
label="Serbian"
onChange={() => {
this.onLanguageChange("rs");
}}
defaultChecked={this.state.language == "rs"}
/>
</div>
</div>
</section>*/}

View file

@ -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);

View file

@ -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];

127
ui/js/reducers/user.js Normal file
View file

@ -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;
}

View file

@ -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;

View file

@ -1,5 +1,4 @@
import { createSelector } from "reselect";
import { selectDaemonReady, selectCurrentPage } from "selectors/app";
const _selectState = state => state.availability;

View file

@ -1,5 +1,4 @@
import { createSelector } from "reselect";
import { selectDaemonReady, selectCurrentPage } from "selectors/app";
export const _selectState = state => state.content || {};

View file

@ -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);
};

82
ui/js/selectors/user.js Normal file
View file

@ -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)
);

View file

@ -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 || {}

View file

@ -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();

View file

@ -165,3 +165,8 @@ p
section.section-spaced {
margin-bottom: $spacing-vertical;
}
.text-center
{
text-align: center;
}

View file

@ -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;

View file

@ -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;