reviewable

This commit is contained in:
Jeremy Kauffman 2017-06-08 17:15:34 -04:00
parent 18a40defba
commit 09ecae7e0d
27 changed files with 522 additions and 525 deletions

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,6 +2,7 @@ import * as types from "constants/action_types";
import lbry from "lbry";
import lbryio from "lbryio";
import rewards from "rewards";
import { selectRewards, selectRewardsByType } from "selectors/rewards";
export function doRewardList() {
return function(dispatch, getState) {
@ -11,56 +12,105 @@ export function doRewardList() {
type: types.FETCH_REWARDS_STARTED,
});
lbryio.call('reward', 'list', {}).then((userRewards) => {
dispatch({
type: types.FETCH_REWARDS_COMPLETED,
data: { userRewards }
lbryio
.call("reward", "list", {})
.then(userRewards => {
dispatch({
type: types.FETCH_REWARDS_COMPLETED,
data: { userRewards },
});
})
}).catch(() => {
dispatch({
type: types.FETCH_REWARDS_COMPLETED,
data: { userRewards: [] }
})
});
.catch(() => {
dispatch({
type: types.FETCH_REWARDS_COMPLETED,
data: { userRewards: [] },
});
});
};
}
export function doClaimReward(reward) {
export function doClaimRewardType(rewardType) {
return function(dispatch, getState) {
const rewardsByType = selectRewardsByType(getState()),
reward = rewardsByType[rewardType];
if (reward) {
dispatch(doClaimReward(reward));
}
};
}
export function doClaimReward(reward, saveError = false) {
return function(dispatch, getState) {
if (reward.transaction_id) {
//already claimed, do nothing
return;
}
dispatch({
type: types.CLAIM_REWARD_STARTED,
data: { reward }
})
data: { reward },
});
const success = (a) => {
console.log(a)
const success = reward => {
dispatch({
type: types.CLAIM_REWARD_SUCCESS,
data: {
a
}
})
}
reward,
},
});
};
const failure = (error) => {
const failure = error => {
dispatch({
type: types.CLAIM_REWARD_FAILURE,
data: {
reward,
error
}
})
error: saveError ? error : null,
},
});
};
rewards.claimReward(reward.reward_type).then(success, failure);
};
}
export function doClaimEligiblePurchaseRewards() {
return function(dispatch, getState) {
if (!lbryio.enabled || !lbryio.getAccessToken()) {
return;
}
rewards.claimReward(reward.reward_type).then(success, failure)
}
const rewardsByType = selectRewardsByType(getState());
let types = {};
types[rewards.TYPE_FIRST_STREAM] = false;
types[rewards.TYPE_FEATURED_DOWNLOAD] = false;
types[rewards.TYPE_MANY_DOWNLOADS] = false;
Object.values(rewardsByType).forEach(reward => {
if (types[reward.reward_type] === false && reward.transaction_id) {
types[reward.reward_type] = true;
}
});
let unclaimedType = Object.keys(types).find(type => {
return types[type] === false && type !== rewards.TYPE_FEATURED_DOWNLOAD; //handled below
});
if (unclaimedType) {
dispatch(doClaimRewardType(unclaimedType));
}
if (types[rewards.TYPE_FEATURED_DOWNLOAD] === false) {
dispatch(doClaimRewardType(rewards.TYPE_FEATURED_DOWNLOAD));
}
};
}
export function doClaimRewardClearError(reward) {
return function(dispatch, getState) {
dispatch({
type: types.CLAIM_REWARD_CLEAR_ERROR,
data: { reward }
})
}
data: { reward },
});
};
}

View file

@ -20,8 +20,6 @@ export function doAuthenticate() {
dispatch(doRewardList()); //FIXME - where should this happen?
})
.catch(error => {
console.log("auth error");
console.log(error);
dispatch({
type: types.AUTHENTICATION_FAILURE,
data: { error },
@ -30,6 +28,28 @@ export function doAuthenticate() {
};
}
export function doUserFetch() {
return function(dispatch, getState) {
dispatch({
type: types.USER_FETCH_STARTED,
});
lbryio.setCurrentUser(
user => {
dispatch({
type: types.USER_FETCH_SUCCESS,
data: { user },
});
},
error => {
dispatch({
type: types.USER_FETCH_FAILURE,
data: { error },
});
}
);
};
}
export function doUserEmailNew(email) {
return function(dispatch, getState) {
dispatch({
@ -42,6 +62,7 @@ export function doUserEmailNew(email) {
type: types.USER_EMAIL_NEW_SUCCESS,
data: { email },
});
dispatch(doUserFetch());
},
error => {
if (
@ -102,6 +123,7 @@ export function doUserEmailVerify(verificationToken) {
type: types.USER_EMAIL_VERIFY_SUCCESS,
data: { email },
});
dispatch(doUserFetch());
} else {
failure(new Error("Your email is still not verified.")); //shouldn't happen?
}

View file

@ -5,11 +5,13 @@ import { doUserEmailDecline } from "actions/user";
import { doOpenModal } from "actions/app";
import {
selectAuthenticationIsPending,
selectUserHasEmail,
selectUserIsAuthRequested,
} from "selectors/user";
import AuthOverlay from "./view";
const select = state => ({
hasEmail: selectUserHasEmail(state),
isPending: selectAuthenticationIsPending(state),
isShowing: selectUserIsAuthRequested(state),
});

View file

@ -32,7 +32,7 @@ export class AuthOverlay extends React.Component {
return null;
}
const { isPending, isShowing } = this.props;
const { isPending, isShowing, hasEmail } = this.props;
if (isShowing) {
return (
@ -46,15 +46,14 @@ export class AuthOverlay extends React.Component {
{isPending
? ""
: <div className="form-row-submit">
{this.state.showNoEmailConfirm
? <div>
<p className="help form-input-width">
{!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
className="button-text-help"
onClick={() => {
this.onEmailSkipConfirm();
}}
@ -62,11 +61,15 @@ export class AuthOverlay extends React.Component {
/>
</div>
: <Link
className="button-text-help"
className={"button-text-help"}
onClick={() => {
this.onEmailSkipClick();
hasEmail
? this.onEmailSkipConfirm()
: this.onEmailSkipClick();
}}
label={__("Do I have to?")}
label={
hasEmail ? __("Skip for now") : __("Do I have to?")
}
/>}
</div>}
</ModalPage>

View file

@ -178,9 +178,19 @@ export class FormRow extends React.Component {
this._fieldRequiredText = __("This field is required");
this.state = {
this.state = this.getStateFromProps(props);
}
componentWillReceiveProps(nextProps) {
this.setState(this.getStateFromProps(nextProps));
}
getStateFromProps(props) {
return {
isError: !!props.errorMessage,
errorMessage: typeof props.errorMessage === "string" ? props.errorMessage : '',
errorMessage: typeof props.errorMessage === "string"
? props.errorMessage
: "",
};
}

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.Component {
static propTypes = {
type: React.PropTypes.string.isRequired,
claimed: React.PropTypes.bool,
onRewardClaim: React.PropTypes.func,
onRewardFailure: React.PropTypes.func,
};
constructor(props) {
super(props);
this.state = {
claimable: true,
pending: false,
errorMessage: null,
};
}
refreshClaimable() {
switch (this.props.type) {
case "new_user":
this.setState({ claimable: true });
return;
case "first_publish":
lbry.claim_list_mine().then(list => {
this.setState({
claimable: list.length > 0,
});
});
return;
}
}
componentWillMount() {
this.refreshClaimable();
}
claimReward() {
this.setState({
pending: true,
});
rewards
.claimReward(this.props.type)
.then(reward => {
this.setState({
pending: false,
errorMessage: null,
});
if (this.props.onRewardClaim) {
this.props.onRewardClaim(reward);
}
})
.catch(error => {
this.setState({
errorMessage: error.message,
pending: false,
});
});
}
clearError() {
if (this.props.onRewardFailure) {
this.props.onRewardFailure();
}
this.setState({
errorMessage: null,
});
}
render() {
return (
<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

@ -20,14 +20,14 @@ const makeSelect = () => {
isClaimed: selectHasClaimedReward(state, props),
errorMessage: selectError(state, props),
isPending: selectIsPending(state, props),
reward: select,
reward: selectReward(state, props),
});
return select;
};
const perform = dispatch => ({
claimReward: reward => dispatch(doClaimReward(reward)),
claimReward: reward => dispatch(doClaimReward(reward, true)),
clearError: reward => dispatch(doClaimRewardClearError(reward)),
navigate: path => dispatch(doNavigate(path)),
});

View file

@ -1,13 +1,13 @@
import React from 'react';
import Link from 'component/link';
import {FormRow} from 'component/form.js';
import React from "react";
import Link from "component/link";
import { FormRow } from "component/form.js";
class UserEmailNew extends React.Component {
constructor(props) {
super(props);
this.state = {
email: ''
email: "",
};
}
@ -19,25 +19,43 @@ class UserEmailNew extends React.Component {
handleSubmit(event) {
event.preventDefault();
this.props.addUserEmail(this.state.email)
this.props.addUserEmail(this.state.email);
}
render() {
const {
errorMessage,
isPending
} = this.props
const { errorMessage, isPending } = this.props;
return <form 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>
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
export default UserEmailNew;

View file

@ -10,7 +10,7 @@ import UserEmailVerify from "./view";
const select = state => ({
isPending: selectEmailVerifyIsPending(state),
email: selectEmailToVerify,
email: selectEmailToVerify(state),
errorMessage: selectEmailVerifyErrorMessage(state),
});

View file

@ -1,13 +1,13 @@
import React from 'react';
import Link from 'component/link';
import {FormRow} from 'component/form.js';
import React from "react";
import Link from "component/link";
import { FormRow } from "component/form.js";
class UserEmailVerify extends React.Component {
constructor(props) {
super(props);
this.state = {
code: '',
code: "",
};
}
@ -19,25 +19,50 @@ class UserEmailVerify extends React.Component {
handleSubmit(event) {
event.preventDefault();
this.props.verifyUserEmail(this.state.code)
this.props.verifyUserEmail(this.state.code);
}
render() {
const {
errorMessage,
isPending
} = this.props
const { errorMessage, isPending } = this.props;
return <form 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}
helper="A verification code is required to participate in early access rewards."/>
<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>
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
export default UserEmailVerify;

View file

@ -1,17 +1,23 @@
import React from "react";
import rewards from "rewards";
import { connect } from "react-redux";
import { doCloseModal } from "actions/app";
import { selectUserIsRewardApproved } from "selectors/user";
import { makeSelectHasClaimedReward } from "selectors/rewards";
import {
makeSelectHasClaimedReward,
makeSelectClaimRewardError,
makeSelectRewardByType,
} from "selectors/rewards";
import WelcomeModal from "./view";
const select = (state, props) => {
const selectHasReward = makeSelectHasClaimedReward();
const selectHasClaimed = makeSelectHasClaimedReward(),
selectReward = makeSelectRewardByType();
return {
hasReward: selectHasReward(state, { reward_type: "new_user" }),
hasClaimed: selectHasClaimed(state, { reward_type: rewards.TYPE_NEW_USER }),
isRewardApproved: selectUserIsRewardApproved(state),
rewardAmount: 5,
reward: selectReward(state, { reward_type: rewards.TYPE_NEW_USER }),
};
};

View file

@ -6,14 +6,12 @@ import RewardLink from "component/rewardLink";
class WelcomeModal extends React.Component {
render() {
const {
closeModal,
hasReward,
isRewardApproved,
rewardAmount,
} = this.props;
const { closeModal, hasClaimed, isRewardApproved, reward } = this.props;
return !hasReward
console.log("welcome");
console.log(this.props);
return !hasClaimed
? <Modal type="custom" isOpen={true} contentLabel="Welcome to LBRY">
<section>
<h3 className="modal__header">Welcome to LBRY.</h3>
@ -30,7 +28,7 @@ class WelcomeModal extends React.Component {
Thank you for making content freedom possible!
{" "}{isRewardApproved ? __("Here's a nickel, kid.") : ""}
</p>
<div style={{ textAlign: "center", marginBottom: "12px" }}>
<div className="text-center">
{isRewardApproved
? <RewardLink reward_type="new_user" button="primary" />
: <Link
@ -52,7 +50,8 @@ class WelcomeModal extends React.Component {
<h3 className="modal__header">About Your Reward</h3>
<p>
You earned a reward of
{" "}<CreditAmount amount={rewardAmount} label={false} /> LBRY
{" "}<CreditAmount amount={reward.reward_amount} label={false} />
{" "}LBRY
credits, or <em>LBC</em>.
</p>
<p>

View file

@ -71,22 +71,25 @@ export const SEARCH_CANCELLED = "SEARCH_CANCELLED";
export const DAEMON_SETTINGS_RECEIVED = "DAEMON_SETTINGS_RECEIVED";
// User
export const AUTHENTICATION_STARTED = 'AUTHENTICATION_STARTED'
export const AUTHENTICATION_SUCCESS = 'AUTHENTICATION_SUCCESS'
export const AUTHENTICATION_FAILURE = 'AUTHENTICATION_FAILURE'
export const USER_EMAIL_DECLINE = 'USER_EMAIL_DECLINE'
export const USER_EMAIL_NEW_STARTED = 'USER_EMAIL_NEW_STARTED'
export const USER_EMAIL_NEW_SUCCESS = 'USER_EMAIL_NEW_SUCCESS'
export const USER_EMAIL_NEW_EXISTS = 'USER_EMAIL_NEW_EXISTS'
export const USER_EMAIL_NEW_FAILURE = 'USER_EMAIL_NEW_FAILURE'
export const USER_EMAIL_VERIFY_STARTED = 'USER_EMAIL_VERIFY_STARTED'
export const USER_EMAIL_VERIFY_SUCCESS = 'USER_EMAIL_VERIFY_SUCCESS'
export const USER_EMAIL_VERIFY_FAILURE = 'USER_EMAIL_VERIFY_FAILURE'
export const AUTHENTICATION_STARTED = "AUTHENTICATION_STARTED";
export const AUTHENTICATION_SUCCESS = "AUTHENTICATION_SUCCESS";
export const AUTHENTICATION_FAILURE = "AUTHENTICATION_FAILURE";
export const USER_EMAIL_DECLINE = "USER_EMAIL_DECLINE";
export const USER_EMAIL_NEW_STARTED = "USER_EMAIL_NEW_STARTED";
export const USER_EMAIL_NEW_SUCCESS = "USER_EMAIL_NEW_SUCCESS";
export const USER_EMAIL_NEW_EXISTS = "USER_EMAIL_NEW_EXISTS";
export const USER_EMAIL_NEW_FAILURE = "USER_EMAIL_NEW_FAILURE";
export const USER_EMAIL_VERIFY_STARTED = "USER_EMAIL_VERIFY_STARTED";
export const USER_EMAIL_VERIFY_SUCCESS = "USER_EMAIL_VERIFY_SUCCESS";
export const USER_EMAIL_VERIFY_FAILURE = "USER_EMAIL_VERIFY_FAILURE";
export const USER_FETCH_STARTED = "USER_FETCH_STARTED";
export const USER_FETCH_SUCCESS = "USER_FETCH_SUCCESS";
export const USER_FETCH_FAILURE = "USER_FETCH_FAILURE";
// Rewards
export const FETCH_REWARDS_STARTED = 'FETCH_REWARDS_STARTED'
export const FETCH_REWARDS_COMPLETED = 'FETCH_REWARDS_COMPLETED'
export const CLAIM_REWARD_STARTED = 'CLAIM_REWARD_STARTED'
export const CLAIM_REWARD_SUCCESS = 'CLAIM_REWARD_SUCCESS'
export const CLAIM_REWARD_FAILURE = 'CLAIM_REWARD_FAILURE'
export const CLAIM_REWARD_CLEAR_ERROR = 'CLAIM_REWARD_CLEAR_ERROR'
export const FETCH_REWARDS_STARTED = "FETCH_REWARDS_STARTED";
export const FETCH_REWARDS_COMPLETED = "FETCH_REWARDS_COMPLETED";
export const CLAIM_REWARD_STARTED = "CLAIM_REWARD_STARTED";
export const CLAIM_REWARD_SUCCESS = "CLAIM_REWARD_SUCCESS";
export const CLAIM_REWARD_FAILURE = "CLAIM_REWARD_FAILURE";
export const CLAIM_REWARD_CLEAR_ERROR = "CLAIM_REWARD_CLEAR_ERROR";

View file

@ -6,7 +6,6 @@ const querystring = require("querystring");
const lbryio = {
_accessToken: getSession("accessToken"),
_authenticationPromise: null,
_user: null,
enabled: true,
};
@ -36,20 +35,9 @@ lbryio.getExchangeRates = function() {
return lbryio._exchangePromise;
};
lbryio.call = function(
resource,
action,
params = {},
method = "get",
evenIfDisabled = false
) {
// evenIfDisabled is just for development, when we may have some calls working and some not
lbryio.call = function(resource, action, params = {}, method = "get") {
return new Promise((resolve, reject) => {
if (
!lbryio.enabled &&
!evenIfDisabled &&
(resource != "discover" || action != "list")
) {
if (!lbryio.enabled && (resource != "discover" || action != "list")) {
console.log(__("Internal API disabled"));
reject(new Error(__("LBRY internal API is disabled")));
return;
@ -135,7 +123,6 @@ lbryio.setCurrentUser = (resolve, reject) => {
lbryio
.call("user", "me")
.then(data => {
lbryio.user = data;
resolve(data);
})
.catch(function(err) {

View file

@ -51,7 +51,10 @@ ipcRenderer.on("open-uri-requested", (event, uri) => {
document.addEventListener("click", event => {
var target = event.target;
while (target && target !== document) {
if (target.matches('a[href^="http"]')) {
if (
target.matches('a[href^="http"]') ||
target.matches('a[href^="mailto"]')
) {
event.preventDefault();
shell.openExternal(target.href);
return;

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.Component {
}
componentDidUpdate() {
if (this.props.fileInfos.length > 0) this._requestPublishReward();
}
_requestPublishReward() {
// TODO this is throwing an error now
// Error: LBRY internal API is disabled
//
// lbryio.call('reward', 'list', {}).then(function(userRewards) {
// //already rewarded
// if (userRewards.filter(function (reward) {
// return reward.reward_type == rewards.TYPE_FIRST_PUBLISH && reward.transaction_id
// }).length) {
// return
// }
// else {
// rewards.claimReward(rewards.TYPE_FIRST_PUBLISH).catch(() => {})
// }
// })
if (this.props.fileInfos.length > 0) this.props.claimFirstPublishReward();
}
render() {

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.Component {
// Calls API to update displayed list of channels. If a channel name is provided, will select
// that channel at the same time (used immediately after creating a channel)
lbry.channel_list_mine().then(channels => {
rewards.claimReward(rewards.TYPE_FIRST_CHANNEL).then(() => {}, () => {});
this.props.claimFirstChannelReward();
this.setState({
channels: channels,
...(channel ? { channel } : {}),
@ -373,7 +373,7 @@ class PublishPage extends React.Component {
lbry
.channel_new({
channel_name: newChannelName,
amount: parseInt(this.state.newChannelBid),
amount: parseFloat(this.state.newChannelBid),
})
.then(
() => {

View file

@ -1,15 +1,10 @@
import React from "react";
import { connect } from "react-redux";
import { doNavigate } from "actions/app";
import {
selectFetchingRewards,
selectIsRewardEligible,
selectRewards,
} from "selectors/rewards";
import { selectFetchingRewards, selectRewards } from "selectors/rewards";
import {
selectUserIsRewardEligible,
selectUserHasEmail,
selectUserIsRewardApproved,
selectUserIsVerificationCandidate,
} from "selectors/user";
import RewardsPage from "./view";

View file

@ -1,87 +1,90 @@
import * as types from "constants/action_types";
const reducers = {}
const reducers = {};
const defaultState = {
fetching: false,
rewardsByType: {},
claimPendingByType: {},
claimErrorsByType: {}
claimErrorsByType: {},
};
reducers[types.FETCH_REWARDS_STARTED] = function(state, action) {
const newRewards = Object.assign({}, state.rewards, {
return Object.assign({}, state, {
fetching: true,
})
return Object.assign({}, state, newRewards)
}
});
};
reducers[types.FETCH_REWARDS_COMPLETED] = function(state, action) {
const {
userRewards,
} = action.data
const { userRewards } = action.data;
const byRewardType = {}
userRewards.forEach(reward => byRewardType[reward.reward_type] = reward)
const newRewards = Object.assign({}, state.rewards, {
byRewardType: byRewardType,
fetching: false
})
const rewardsByType = {};
userRewards.forEach(reward => (rewardsByType[reward.reward_type] = reward));
return Object.assign({}, state, newRewards)
}
return Object.assign({}, state, {
rewardsByType: rewardsByType,
fetching: false,
});
};
function setClaimRewardState(state, reward, isClaiming, errorMessage="") {
const newClaimPendingByType = Object.assign({}, state.claimPendingByType)
const newClaimErrorsByType = Object.assign({}, state.claimErrorsByType)
function setClaimRewardState(state, reward, isClaiming, errorMessage = "") {
const newClaimPendingByType = Object.assign({}, state.claimPendingByType);
const newClaimErrorsByType = Object.assign({}, state.claimErrorsByType);
if (isClaiming) {
newClaimPendingByType[reward.reward_type] = isClaiming
newClaimPendingByType[reward.reward_type] = isClaiming;
} else {
delete newClaimPendingByType[reward.reward_type]
delete newClaimPendingByType[reward.reward_type];
}
if (errorMessage) {
newClaimErrorsByType[reward.reward_type] = errorMessage
newClaimErrorsByType[reward.reward_type] = errorMessage;
} else {
delete newClaimErrorsByType[reward.reward_type]
delete newClaimErrorsByType[reward.reward_type];
}
return Object.assign({}, state, {
claimPendingByType: newClaimPendingByType,
claimErrorsByType: newClaimErrorsByType,
})
});
}
reducers[types.CLAIM_REWARD_STARTED] = function(state, action) {
const {
reward,
} = action.data
const { reward } = action.data;
return setClaimRewardState(state, reward, true, "")
}
return setClaimRewardState(state, reward, true, "");
};
reducers[types.CLAIM_REWARD_SUCCESS] = function(state, action) {
const {
reward,
} = action.data
const { reward } = action.data;
return setClaimRewardState(state, reward, false, "")
}
const existingReward = state.rewardsByType[reward.reward_type];
const newReward = Object.assign({}, reward, {
reward_title: existingReward.reward_title,
reward_description: existingReward.reward_description,
});
const rewardsByType = Object.assign({}, state.rewardsByType);
rewardsByType[reward.reward_type] = newReward;
const newState = Object.assign({}, state, { rewardsByType });
return setClaimRewardState(newState, newReward, false, "");
};
reducers[types.CLAIM_REWARD_FAILURE] = function(state, action) {
const {
reward,
error
} = action.data
const { reward, error } = action.data;
return setClaimRewardState(state, reward, false, error.message)
}
return setClaimRewardState(state, reward, false, error ? error.message : "");
};
reducers[types.CLAIM_REWARD_CLEAR_ERROR] = function(state, action) {
const {
reward
} = action.data
const { reward } = action.data;
return setClaimRewardState(state, reward, state.claimPendingByType[reward.reward_type], "")
}
return setClaimRewardState(
state,
reward,
state.claimPendingByType[reward.reward_type],
""
);
};
export default function reducer(state = defaultState, action) {
const handler = reducers[action.type];

View file

@ -5,6 +5,7 @@ const reducers = {};
const defaultState = {
authenticationIsPending: false,
userIsPending: false,
emailNewIsPending: false,
emailNewErrorMessage: "",
emailNewDeclined: getLocal("user_email_declined", false),
@ -15,12 +16,15 @@ const defaultState = {
reducers[types.AUTHENTICATION_STARTED] = function(state, action) {
return Object.assign({}, state, {
authenticationIsPending: true,
userIsPending: true,
user: defaultState.user,
});
};
reducers[types.AUTHENTICATION_SUCCESS] = function(state, action) {
return Object.assign({}, state, {
authenticationIsPending: false,
userIsPending: false,
user: action.data.user,
});
};
@ -28,6 +32,28 @@ reducers[types.AUTHENTICATION_SUCCESS] = function(state, action) {
reducers[types.AUTHENTICATION_FAILURE] = function(state, action) {
return Object.assign({}, state, {
authenticationIsPending: false,
userIsPending: false,
user: null,
});
};
reducers[types.USER_FETCH_STARTED] = function(state, action) {
return Object.assign({}, state, {
userIsPending: true,
user: defaultState.user,
});
};
reducers[types.USER_FETCH_SUCCESS] = function(state, action) {
return Object.assign({}, state, {
userIsPending: false,
user: action.data.user,
});
};
reducers[types.USER_FETCH_FAILURE] = function(state, action) {
return Object.assign({}, state, {
userIsPending: true,
user: null,
});
};

View file

@ -1,225 +1,191 @@
const hashes = require('jshashes');
import lbry from 'lbry';
import lbryio from 'lbryio';
import { doShowSnackBar } from 'actions/app';
const hashes = require("jshashes");
import lbry from "lbry";
import lbryio from "lbryio";
import { doShowSnackBar } from "actions/app";
function rewardMessage(type, amount) {
return {
new_developer: __(
'You earned %s for registering as a new developer.',
amount
),
new_user: __('You earned %s LBC new user reward.', amount),
confirm_email: __(
'You earned %s LBC for verifying your email address.',
amount
),
new_channel: __(
'You earned %s LBC for creating a publisher identity.',
amount
),
first_stream: __(
'You earned %s LBC for streaming your first video.',
amount
),
many_downloads: __(
'You earned %s LBC for downloading some of the things.',
amount
),
first_publish: __(
'You earned %s LBC for making your first publication.',
amount
)
}[type];
return {
new_developer: __(
"You earned %s for registering as a new developer.",
amount
),
new_user: __("You earned %s LBC new user reward.", amount),
confirm_email: __(
"You earned %s LBC for verifying your email address.",
amount
),
new_channel: __(
"You earned %s LBC for creating a publisher identity.",
amount
),
first_stream: __(
"You earned %s LBC for streaming your first video.",
amount
),
many_downloads: __(
"You earned %s LBC for downloading some of the things.",
amount
),
first_publish: __(
"You earned %s LBC for making your first publication.",
amount
),
}[type];
}
function toHex(s) {
let h = '';
for (var i = 0; i < s.length; i++) {
let c = s.charCodeAt(i).toString(16);
if (c.length < 2) {
c = '0'.concat(c);
}
h += c;
}
return h;
let h = "";
for (var i = 0; i < s.length; i++) {
let c = s.charCodeAt(i).toString(16);
if (c.length < 2) {
c = "0".concat(c);
}
h += c;
}
return h;
}
function fromHex(h) {
let s = '';
for (let i = 0; i < h.length; i += 2) {
s += String.fromCharCode(parseInt(h.substr(i, 2), 16));
}
return s;
let s = "";
for (let i = 0; i < h.length; i += 2) {
s += String.fromCharCode(parseInt(h.substr(i, 2), 16));
}
return s;
}
function reverseString(s) {
let o = '';
for (let i = s.length - 1; i >= 0; i--) {
o += s[i];
}
return o;
let o = "";
for (let i = s.length - 1; i >= 0; i--) {
o += s[i];
}
return o;
}
function pack(num) {
return (
'' +
String.fromCharCode(num & 0xff) +
String.fromCharCode((num >> 8) & 0xff) +
String.fromCharCode((num >> 16) & 0xff) +
String.fromCharCode((num >> 24) & 0xff)
);
return (
"" +
String.fromCharCode(num & 0xff) +
String.fromCharCode((num >> 8) & 0xff) +
String.fromCharCode((num >> 16) & 0xff) +
String.fromCharCode((num >> 24) & 0xff)
);
}
// Returns true if claim is an initial claim, false if it's an update to an existing claim
function isInitialClaim(claim) {
const reversed = reverseString(fromHex(claim.txid));
const concat = reversed.concat(pack(claim.nout));
const sha256 = new hashes.SHA256({ utf8: false }).raw(concat);
const ripemd160 = new hashes.RMD160({ utf8: false }).raw(sha256);
const hash = toHex(reverseString(ripemd160));
return hash == claim.claim_id;
const reversed = reverseString(fromHex(claim.txid));
const concat = reversed.concat(pack(claim.nout));
const sha256 = new hashes.SHA256({ utf8: false }).raw(concat);
const ripemd160 = new hashes.RMD160({ utf8: false }).raw(sha256);
const hash = toHex(reverseString(ripemd160));
return hash == claim.claim_id;
}
const rewards = {};
(rewards.TYPE_NEW_DEVELOPER = 'new_developer'), (rewards.TYPE_NEW_USER =
'new_user'), (rewards.TYPE_CONFIRM_EMAIL =
'confirm_email'), (rewards.TYPE_FIRST_CHANNEL =
'new_channel'), (rewards.TYPE_FIRST_STREAM =
'first_stream'), (rewards.TYPE_MANY_DOWNLOADS =
'many_downloads'), (rewards.TYPE_FIRST_PUBLISH = 'first_publish');
rewards.TYPE_FEATURED_DOWNLOAD = 'featured_download';
(rewards.TYPE_NEW_DEVELOPER = "new_developer"), (rewards.TYPE_NEW_USER =
"new_user"), (rewards.TYPE_CONFIRM_EMAIL =
"confirm_email"), (rewards.TYPE_FIRST_CHANNEL =
"new_channel"), (rewards.TYPE_FIRST_STREAM =
"first_stream"), (rewards.TYPE_MANY_DOWNLOADS =
"many_downloads"), (rewards.TYPE_FIRST_PUBLISH = "first_publish");
rewards.TYPE_FEATURED_DOWNLOAD = "featured_download";
rewards.claimReward = function(type) {
function requestReward(resolve, reject, params) {
if (!lbryio.enabled || !lbryio.getAccessToken()) {
reject(new Error(__('Rewards are not enabled.')));
return;
}
lbryio.call('reward', 'new', params, 'post').then(({ reward_amount }) => {
const message = rewardMessage(type, reward_amount),
result = {
type: type,
amount: reward_amount,
message: message
};
function requestReward(resolve, reject, params) {
if (!lbryio.enabled || !lbryio.getAccessToken()) {
reject(new Error(__("Rewards are not enabled.")));
return;
}
lbryio.call("reward", "new", params, "post").then(reward => {
const message = rewardMessage(type, reward.reward_amount);
// Display global notice
const action = doShowSnackBar({
message,
linkText: __('Show All'),
linkTarget: '/rewards',
isError: false
});
window.app.store.dispatch(action);
// Display global notice
const action = doShowSnackBar({
message,
linkText: __("Show All"),
linkTarget: "/rewards",
isError: false,
});
window.app.store.dispatch(action);
// Add more events here to display other places
// Add more events here to display other places
resolve(result);
}, reject);
}
return new Promise((resolve, reject) => {
lbry.wallet_unused_address().then(address => {
const params = {
reward_type: type,
wallet_address: address
};
switch (type) {
case rewards.TYPE_FIRST_CHANNEL:
lbry
.claim_list_mine()
.then(function(claims) {
let claim = claims.find(function(claim) {
return (
claim.name.length &&
claim.name[0] == '@' &&
claim.txid.length &&
isInitialClaim(claim)
);
});
if (claim) {
params.transaction_id = claim.txid;
requestReward(resolve, reject, params);
} else {
reject(
new Error(__('Please create a channel identity first.'))
);
}
})
.catch(reject);
break;
case rewards.TYPE_FIRST_PUBLISH:
lbry
.claim_list_mine()
.then(claims => {
let claim = claims.find(function(claim) {
return (
claim.name.length &&
claim.name[0] != '@' &&
claim.txid.length &&
isInitialClaim(claim)
);
});
if (claim) {
params.transaction_id = claim.txid;
requestReward(resolve, reject, params);
} else {
reject(
claims.length
? new Error(
__(
'Please publish something and wait for confirmation by the network to claim this reward.'
)
)
: new Error(
__('Please publish something to claim this reward.')
)
);
}
})
.catch(reject);
break;
case rewards.TYPE_FIRST_STREAM:
case rewards.TYPE_NEW_USER:
default:
requestReward(resolve, reject, params);
}
});
});
};
rewards.claimEligiblePurchaseRewards = () => {
if (!lbryio.enabled || !lbryio.getAccessToken()) {
return;
resolve(reward);
}, reject);
}
let types = {};
types[rewards.TYPE_FIRST_STREAM] = false;
types[rewards.TYPE_FEATURED_DOWNLOAD] = false;
types[rewards.TYPE_MANY_DOWNLOADS] = false;
lbryio.call('reward', 'list', {}).then(
userRewards => {
userRewards.forEach(reward => {
if (types[reward.reward_type] === false && reward.transaction_id) {
types[reward.reward_type] = true;
}
});
let unclaimedType = Object.keys(types).find(type => {
return types[type] === false && type !== rewards.TYPE_FEATURED_DOWNLOAD; //handled below
});
if (unclaimedType) {
rewards.claimReward(unclaimedType);
}
if (types[rewards.TYPE_FEATURED_DOWNLOAD] === false) {
rewards.claimReward(rewards.TYPE_FEATURED_DOWNLOAD);
}
},
() => {}
);
return new Promise((resolve, reject) => {
lbry.wallet_unused_address().then(address => {
const params = {
reward_type: type,
wallet_address: address,
};
switch (type) {
case rewards.TYPE_FIRST_CHANNEL:
lbry
.claim_list_mine()
.then(function(claims) {
let claim = claims.reverse().find(function(claim) {
return (
claim.name.length &&
claim.name[0] == "@" &&
claim.txid.length &&
isInitialClaim(claim)
);
});
if (claim) {
params.transaction_id = claim.txid;
requestReward(resolve, reject, params);
} else {
reject(
new Error(__("Please create a channel identity first."))
);
}
})
.catch(reject);
break;
case rewards.TYPE_FIRST_PUBLISH:
lbry
.claim_list_mine()
.then(claims => {
let claim = claims.reverse().find(function(claim) {
return (
claim.name.length &&
claim.name[0] != "@" &&
claim.txid.length &&
isInitialClaim(claim)
);
});
if (claim) {
params.transaction_id = claim.txid;
requestReward(resolve, reject, params);
} else {
reject(
claims.length
? new Error(
__(
"Please publish something and wait for confirmation by the network to claim this reward."
)
)
: new Error(
__("Please publish something to claim this reward.")
)
);
}
})
.catch(reject);
break;
case rewards.TYPE_FIRST_STREAM:
case rewards.TYPE_NEW_USER:
default:
requestReward(resolve, reject, params);
}
});
});
};
export default rewards;

View file

@ -5,7 +5,7 @@ const _selectState = state => state.rewards || {};
export const selectRewardsByType = createSelector(
_selectState,
state => state.byRewardType || {}
state => state.rewardsByType || {}
);
export const selectRewards = createSelector(
@ -18,26 +18,14 @@ export const selectIsRewardEligible = createSelector(
user => user.can_claim_rewards
);
export const selectClaimedRewards = createSelector(selectRewards, rewards =>
rewards.filter(reward => reward.transaction_id !== "")
);
export const selectClaimedRewardsByType = createSelector(
selectClaimedRewards,
claimedRewards => {
const byType = {};
claimedRewards.forEach(reward => (byType[reward.reward_type] = reward));
return byType;
}
);
export const selectFetchingRewards = createSelector(
_selectState,
state => !!state.fetching
);
export const selectHasClaimedReward = (state, props) => {
return !!selectClaimedRewardsByType[props.reward_type];
const reward = selectRewardsByType(state)[props.reward_type];
return reward && reward.transaction_id !== "";
};
export const makeSelectHasClaimedReward = () => {

View file

@ -7,6 +7,11 @@ export const selectAuthenticationIsPending = createSelector(
state => state.authenticationIsPending
);
export const selectUserIsPending = createSelector(
_selectState,
state => state.userIsPending
);
export const selectUser = createSelector(
_selectState,
state => state.user || {}
@ -20,17 +25,17 @@ export const selectEmailToVerify = createSelector(
export const selectUserHasEmail = createSelector(
selectUser,
selectEmailToVerify,
(user, email) => user.has_email || email
(user, email) => (user && user.has_email) || email
);
export const selectUserIsRewardEligible = createSelector(
selectUser,
user => user.is_reward_eligible
user => user && user.is_reward_eligible
);
export const selectUserIsRewardApproved = createSelector(
selectUser,
user => user.is_reward_approved
user => user && user.is_reward_approved
);
export const selectEmailNewIsPending = createSelector(
@ -64,7 +69,7 @@ export const selectUserIsVerificationCandidate = createSelector(
selectEmailToVerify,
selectUser,
(isEligible, isApproved, emailToVerify, user) =>
(isEligible && !isApproved) || (emailToVerify && !user.has_email)
(isEligible && !isApproved) || (emailToVerify && user && !user.has_email)
);
export const selectUserIsAuthRequested = createSelector(

View file

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