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:
commit
902fb96878
48 changed files with 1641 additions and 1200 deletions
|
@ -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
|
||||
*
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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());
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -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());
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -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
132
ui/js/actions/user.js
Normal 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);
|
||||
};
|
||||
}
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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}
|
||||
/>;
|
||||
}
|
||||
}
|
16
ui/js/component/auth/index.js
Normal file
16
ui/js/component/auth/index.js
Normal 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);
|
22
ui/js/component/auth/view.jsx
Normal file
22
ui/js/component/auth/view.jsx
Normal 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;
|
24
ui/js/component/authOverlay/index.jsx
Normal file
24
ui/js/component/authOverlay/index.jsx
Normal 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);
|
83
ui/js/component/authOverlay/view.jsx
Normal file
83
ui/js/component/authOverlay/view.jsx
Normal 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;
|
|
@ -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">
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
}
|
35
ui/js/component/rewardLink/index.js
Normal file
35
ui/js/component/rewardLink/index.js
Normal 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);
|
44
ui/js/component/rewardLink/view.jsx
Normal file
44
ui/js/component/rewardLink/view.jsx
Normal 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;
|
|
@ -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";
|
||||
|
|
23
ui/js/component/userEmailNew/index.jsx
Normal file
23
ui/js/component/userEmailNew/index.jsx
Normal 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)
|
61
ui/js/component/userEmailNew/view.jsx
Normal file
61
ui/js/component/userEmailNew/view.jsx
Normal 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;
|
21
ui/js/component/userEmailVerify/index.jsx
Normal file
21
ui/js/component/userEmailVerify/index.jsx
Normal 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);
|
68
ui/js/component/userEmailVerify/view.jsx
Normal file
68
ui/js/component/userEmailVerify/view.jsx
Normal 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;
|
28
ui/js/component/welcomeModal/index.jsx
Normal file
28
ui/js/component/welcomeModal/index.jsx
Normal 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);
|
75
ui/js/component/welcomeModal/view.jsx
Normal file
75
ui/js/component/welcomeModal/view.jsx
Normal 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;
|
|
@ -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";
|
||||
|
|
1
ui/js/constants/modal_types.js
Normal file
1
ui/js/constants/modal_types.js
Normal file
|
@ -0,0 +1 @@
|
|||
export const WELCOME = "welcome";
|
330
ui/js/lbryio.js
330
ui/js/lbryio.js
|
@ -1,206 +1,192 @@
|
|||
import { getSession, setSession } from './utils.js';
|
||||
import lbry from './lbry.js';
|
||||
import { getSession, setSession, setLocal } from "./utils.js";
|
||||
import lbry from "./lbry.js";
|
||||
|
||||
const querystring = require('querystring');
|
||||
const querystring = require("querystring");
|
||||
|
||||
const lbryio = {
|
||||
_accessToken: getSession('accessToken'),
|
||||
_authenticationPromise: null,
|
||||
_user: null,
|
||||
enabled: true
|
||||
_accessToken: getSession("accessToken"),
|
||||
_authenticationPromise: null,
|
||||
enabled: true,
|
||||
};
|
||||
|
||||
const CONNECTION_STRING = process.env.LBRY_APP_API_URL
|
||||
? process.env.LBRY_APP_API_URL.replace(/\/*$/, '/') // exactly one slash at the end
|
||||
: 'https://api.lbry.io/';
|
||||
? process.env.LBRY_APP_API_URL.replace(/\/*$/, "/") // exactly one slash at the end
|
||||
: "https://api.lbry.io/";
|
||||
const EXCHANGE_RATE_TIMEOUT = 20 * 60 * 1000;
|
||||
|
||||
lbryio._exchangePromise = null;
|
||||
lbryio._exchangeLastFetched = null;
|
||||
lbryio.getExchangeRates = function() {
|
||||
if (
|
||||
!lbryio._exchangeLastFetched ||
|
||||
Date.now() - lbryio._exchangeLastFetched > EXCHANGE_RATE_TIMEOUT
|
||||
) {
|
||||
lbryio._exchangePromise = new Promise((resolve, reject) => {
|
||||
lbryio
|
||||
.call('lbc', 'exchange_rate', {}, 'get', true)
|
||||
.then(({ lbc_usd, lbc_btc, btc_usd }) => {
|
||||
const rates = { lbc_usd, lbc_btc, btc_usd };
|
||||
resolve(rates);
|
||||
})
|
||||
.catch(reject);
|
||||
});
|
||||
lbryio._exchangeLastFetched = Date.now();
|
||||
}
|
||||
return lbryio._exchangePromise;
|
||||
if (
|
||||
!lbryio._exchangeLastFetched ||
|
||||
Date.now() - lbryio._exchangeLastFetched > EXCHANGE_RATE_TIMEOUT
|
||||
) {
|
||||
lbryio._exchangePromise = new Promise((resolve, reject) => {
|
||||
lbryio
|
||||
.call("lbc", "exchange_rate", {}, "get", true)
|
||||
.then(({ lbc_usd, lbc_btc, btc_usd }) => {
|
||||
const rates = { lbc_usd, lbc_btc, btc_usd };
|
||||
resolve(rates);
|
||||
})
|
||||
.catch(reject);
|
||||
});
|
||||
lbryio._exchangeLastFetched = Date.now();
|
||||
}
|
||||
return lbryio._exchangePromise;
|
||||
};
|
||||
|
||||
lbryio.call = function(
|
||||
resource,
|
||||
action,
|
||||
params = {},
|
||||
method = 'get',
|
||||
evenIfDisabled = false
|
||||
) {
|
||||
// evenIfDisabled is just for development, when we may have some calls working and some not
|
||||
return new Promise((resolve, reject) => {
|
||||
if (
|
||||
!lbryio.enabled &&
|
||||
!evenIfDisabled &&
|
||||
(resource != 'discover' || action != 'list')
|
||||
) {
|
||||
console.log(__('Internal API disabled'));
|
||||
reject(new Error(__('LBRY internal API is disabled')));
|
||||
return;
|
||||
}
|
||||
lbryio.call = function(resource, action, params = {}, method = "get") {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (!lbryio.enabled && (resource != "discover" || action != "list")) {
|
||||
console.log(__("Internal API disabled"));
|
||||
reject(new Error(__("LBRY internal API is disabled")));
|
||||
return;
|
||||
}
|
||||
|
||||
const xhr = new XMLHttpRequest();
|
||||
const xhr = new XMLHttpRequest();
|
||||
|
||||
xhr.addEventListener('error', function(event) {
|
||||
reject(
|
||||
new Error(__('Something went wrong making an internal API call.'))
|
||||
);
|
||||
});
|
||||
xhr.addEventListener("error", function(event) {
|
||||
reject(
|
||||
new Error(__("Something went wrong making an internal API call."))
|
||||
);
|
||||
});
|
||||
|
||||
xhr.addEventListener('timeout', function() {
|
||||
reject(new Error(__('XMLHttpRequest connection timed out')));
|
||||
});
|
||||
xhr.addEventListener("timeout", function() {
|
||||
reject(new Error(__("XMLHttpRequest connection timed out")));
|
||||
});
|
||||
|
||||
xhr.addEventListener('load', function() {
|
||||
const response = JSON.parse(xhr.responseText);
|
||||
xhr.addEventListener("load", function() {
|
||||
const response = JSON.parse(xhr.responseText);
|
||||
|
||||
if (!response.success) {
|
||||
if (reject) {
|
||||
let error = new Error(response.error);
|
||||
error.xhr = xhr;
|
||||
reject(error);
|
||||
} else {
|
||||
document.dispatchEvent(
|
||||
new CustomEvent('unhandledError', {
|
||||
detail: {
|
||||
connectionString: connectionString,
|
||||
method: action,
|
||||
params: params,
|
||||
message: response.error.message,
|
||||
...(response.error.data ? { data: response.error.data } : {})
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
} else {
|
||||
resolve(response.data);
|
||||
}
|
||||
});
|
||||
if (!response.success) {
|
||||
if (reject) {
|
||||
let error = new Error(response.error);
|
||||
error.xhr = xhr;
|
||||
reject(error);
|
||||
} else {
|
||||
document.dispatchEvent(
|
||||
new CustomEvent("unhandledError", {
|
||||
detail: {
|
||||
connectionString: connectionString,
|
||||
method: action,
|
||||
params: params,
|
||||
message: response.error.message,
|
||||
...(response.error.data ? { data: response.error.data } : {}),
|
||||
},
|
||||
})
|
||||
);
|
||||
}
|
||||
} else {
|
||||
resolve(response.data);
|
||||
}
|
||||
});
|
||||
|
||||
// For social media auth:
|
||||
//const accessToken = localStorage.getItem('accessToken');
|
||||
//const fullParams = {...params, ... accessToken ? {access_token: accessToken} : {}};
|
||||
// For social media auth:
|
||||
//const accessToken = localStorage.getItem('accessToken');
|
||||
//const fullParams = {...params, ... accessToken ? {access_token: accessToken} : {}};
|
||||
|
||||
// Temp app ID based auth:
|
||||
const fullParams = { app_id: lbryio.getAccessToken(), ...params };
|
||||
// Temp app ID based auth:
|
||||
const fullParams = { app_id: lbryio.getAccessToken(), ...params };
|
||||
|
||||
if (method == 'get') {
|
||||
xhr.open(
|
||||
'get',
|
||||
CONNECTION_STRING +
|
||||
resource +
|
||||
'/' +
|
||||
action +
|
||||
'?' +
|
||||
querystring.stringify(fullParams),
|
||||
true
|
||||
);
|
||||
xhr.send();
|
||||
} else if (method == 'post') {
|
||||
xhr.open('post', CONNECTION_STRING + resource + '/' + action, true);
|
||||
xhr.setRequestHeader('Content-type', 'application/x-www-form-urlencoded');
|
||||
xhr.send(querystring.stringify(fullParams));
|
||||
} else {
|
||||
reject(new Error(__('Invalid method')));
|
||||
}
|
||||
});
|
||||
if (method == "get") {
|
||||
xhr.open(
|
||||
"get",
|
||||
CONNECTION_STRING +
|
||||
resource +
|
||||
"/" +
|
||||
action +
|
||||
"?" +
|
||||
querystring.stringify(fullParams),
|
||||
true
|
||||
);
|
||||
xhr.send();
|
||||
} else if (method == "post") {
|
||||
xhr.open("post", CONNECTION_STRING + resource + "/" + action, true);
|
||||
xhr.setRequestHeader("Content-type", "application/x-www-form-urlencoded");
|
||||
xhr.send(querystring.stringify(fullParams));
|
||||
} else {
|
||||
reject(new Error(__("Invalid method")));
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
lbryio.getAccessToken = () => {
|
||||
const token = getSession('accessToken');
|
||||
return token ? token.toString().trim() : token;
|
||||
const token = getSession("accessToken");
|
||||
return token ? token.toString().trim() : token;
|
||||
};
|
||||
|
||||
lbryio.setAccessToken = token => {
|
||||
setSession('accessToken', token ? token.toString().trim() : token);
|
||||
setSession("accessToken", token ? token.toString().trim() : token);
|
||||
};
|
||||
|
||||
lbryio.setCurrentUser = (resolve, reject) => {
|
||||
lbryio
|
||||
.call("user", "me")
|
||||
.then(data => {
|
||||
resolve(data);
|
||||
})
|
||||
.catch(function(err) {
|
||||
lbryio.setAccessToken(null);
|
||||
reject(err);
|
||||
});
|
||||
};
|
||||
|
||||
lbryio.authenticate = function() {
|
||||
if (!lbryio.enabled) {
|
||||
return new Promise((resolve, reject) => {
|
||||
resolve({
|
||||
id: 1,
|
||||
has_verified_email: true
|
||||
});
|
||||
});
|
||||
}
|
||||
if (lbryio._authenticationPromise === null) {
|
||||
lbryio._authenticationPromise = new Promise((resolve, reject) => {
|
||||
lbry
|
||||
.status()
|
||||
.then(response => {
|
||||
let installation_id = response.installation_id;
|
||||
if (!lbryio.enabled) {
|
||||
return new Promise((resolve, reject) => {
|
||||
resolve({
|
||||
id: 1,
|
||||
language: "en",
|
||||
has_email: true,
|
||||
has_verified_email: true,
|
||||
is_reward_approved: false,
|
||||
is_reward_eligible: false,
|
||||
});
|
||||
});
|
||||
}
|
||||
if (lbryio._authenticationPromise === null) {
|
||||
lbryio._authenticationPromise = new Promise((resolve, reject) => {
|
||||
lbry
|
||||
.status()
|
||||
.then(response => {
|
||||
let installation_id = response.installation_id;
|
||||
|
||||
function setCurrentUser() {
|
||||
lbryio
|
||||
.call('user', 'me')
|
||||
.then(data => {
|
||||
lbryio.user = data;
|
||||
resolve(data);
|
||||
})
|
||||
.catch(function(err) {
|
||||
lbryio.setAccessToken(null);
|
||||
if (!getSession('reloadedOnFailedAuth')) {
|
||||
setSession('reloadedOnFailedAuth', true);
|
||||
window.location.reload();
|
||||
} else {
|
||||
reject(err);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (!lbryio.getAccessToken()) {
|
||||
lbryio
|
||||
.call(
|
||||
'user',
|
||||
'new',
|
||||
{
|
||||
language: 'en',
|
||||
app_id: installation_id
|
||||
},
|
||||
'post'
|
||||
)
|
||||
.then(function(responseData) {
|
||||
if (!responseData.id) {
|
||||
reject(
|
||||
new Error(__('Received invalid authentication response.'))
|
||||
);
|
||||
}
|
||||
lbryio.setAccessToken(installation_id);
|
||||
setCurrentUser();
|
||||
})
|
||||
.catch(function(error) {
|
||||
/*
|
||||
until we have better error code format, assume all errors are duplicate application id
|
||||
if we're wrong, this will be caught by later attempts to make a valid call
|
||||
*/
|
||||
lbryio.setAccessToken(installation_id);
|
||||
setCurrentUser();
|
||||
});
|
||||
} else {
|
||||
setCurrentUser();
|
||||
}
|
||||
})
|
||||
.catch(reject);
|
||||
});
|
||||
}
|
||||
return lbryio._authenticationPromise;
|
||||
if (!lbryio.getAccessToken()) {
|
||||
lbryio
|
||||
.call(
|
||||
"user",
|
||||
"new",
|
||||
{
|
||||
language: "en",
|
||||
app_id: installation_id,
|
||||
},
|
||||
"post"
|
||||
)
|
||||
.then(function(responseData) {
|
||||
if (!responseData.id) {
|
||||
reject(
|
||||
new Error("Received invalid authentication response.")
|
||||
);
|
||||
}
|
||||
lbryio.setAccessToken(installation_id);
|
||||
lbryio.setCurrentUser(resolve, reject);
|
||||
})
|
||||
.catch(function(error) {
|
||||
/*
|
||||
until we have better error code format, assume all errors are duplicate application id
|
||||
if we're wrong, this will be caught by later attempts to make a valid call
|
||||
*/
|
||||
lbryio.setAccessToken(installation_id);
|
||||
lbryio.setCurrentUser(resolve, reject);
|
||||
});
|
||||
} else {
|
||||
lbryio.setCurrentUser(resolve, reject);
|
||||
}
|
||||
})
|
||||
.catch(reject);
|
||||
});
|
||||
}
|
||||
return lbryio._authenticationPromise;
|
||||
};
|
||||
|
||||
export default lbryio;
|
||||
|
|
|
@ -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
|
||||
);
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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 } : {}),
|
||||
|
|
|
@ -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;
|
20
ui/js/page/rewards/index.js
Normal file
20
ui/js/page/rewards/index.js
Normal 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);
|
92
ui/js/page/rewards/view.jsx
Normal file
92
ui/js/page/rewards/view.jsx
Normal 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;
|
|
@ -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>*/}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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
127
ui/js/reducers/user.js
Normal 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;
|
||||
}
|
345
ui/js/rewards.js
345
ui/js/rewards.js
|
@ -1,222 +1,191 @@
|
|||
const hashes = require('jshashes');
|
||||
import lbry from 'lbry';
|
||||
import lbryio from 'lbryio';
|
||||
import { doShowSnackBar } from 'actions/app';
|
||||
const hashes = require("jshashes");
|
||||
import lbry from "lbry";
|
||||
import lbryio from "lbryio";
|
||||
import { doShowSnackBar } from "actions/app";
|
||||
|
||||
function rewardMessage(type, amount) {
|
||||
return {
|
||||
new_developer: __(
|
||||
'You earned %s for registering as a new developer.',
|
||||
amount
|
||||
),
|
||||
new_user: __('You earned %s LBC new user reward.', amount),
|
||||
confirm_email: __(
|
||||
'You earned %s LBC for verifying your email address.',
|
||||
amount
|
||||
),
|
||||
new_channel: __(
|
||||
'You earned %s LBC for creating a publisher identity.',
|
||||
amount
|
||||
),
|
||||
first_stream: __(
|
||||
'You earned %s LBC for streaming your first video.',
|
||||
amount
|
||||
),
|
||||
many_downloads: __(
|
||||
'You earned %s LBC for downloading some of the things.',
|
||||
amount
|
||||
),
|
||||
first_publish: __(
|
||||
'You earned %s LBC for making your first publication.',
|
||||
amount
|
||||
)
|
||||
}[type];
|
||||
return {
|
||||
new_developer: __(
|
||||
"You earned %s for registering as a new developer.",
|
||||
amount
|
||||
),
|
||||
new_user: __("You earned %s LBC new user reward.", amount),
|
||||
confirm_email: __(
|
||||
"You earned %s LBC for verifying your email address.",
|
||||
amount
|
||||
),
|
||||
new_channel: __(
|
||||
"You earned %s LBC for creating a publisher identity.",
|
||||
amount
|
||||
),
|
||||
first_stream: __(
|
||||
"You earned %s LBC for streaming your first video.",
|
||||
amount
|
||||
),
|
||||
many_downloads: __(
|
||||
"You earned %s LBC for downloading some of the things.",
|
||||
amount
|
||||
),
|
||||
first_publish: __(
|
||||
"You earned %s LBC for making your first publication.",
|
||||
amount
|
||||
),
|
||||
}[type];
|
||||
}
|
||||
|
||||
function toHex(s) {
|
||||
let h = '';
|
||||
for (var i = 0; i < s.length; i++) {
|
||||
let c = s.charCodeAt(i).toString(16);
|
||||
if (c.length < 2) {
|
||||
c = '0'.concat(c);
|
||||
}
|
||||
h += c;
|
||||
}
|
||||
return h;
|
||||
let h = "";
|
||||
for (var i = 0; i < s.length; i++) {
|
||||
let c = s.charCodeAt(i).toString(16);
|
||||
if (c.length < 2) {
|
||||
c = "0".concat(c);
|
||||
}
|
||||
h += c;
|
||||
}
|
||||
return h;
|
||||
}
|
||||
|
||||
function fromHex(h) {
|
||||
let s = '';
|
||||
for (let i = 0; i < h.length; i += 2) {
|
||||
s += String.fromCharCode(parseInt(h.substr(i, 2), 16));
|
||||
}
|
||||
return s;
|
||||
let s = "";
|
||||
for (let i = 0; i < h.length; i += 2) {
|
||||
s += String.fromCharCode(parseInt(h.substr(i, 2), 16));
|
||||
}
|
||||
return s;
|
||||
}
|
||||
|
||||
function reverseString(s) {
|
||||
let o = '';
|
||||
for (let i = s.length - 1; i >= 0; i--) {
|
||||
o += s[i];
|
||||
}
|
||||
return o;
|
||||
let o = "";
|
||||
for (let i = s.length - 1; i >= 0; i--) {
|
||||
o += s[i];
|
||||
}
|
||||
return o;
|
||||
}
|
||||
|
||||
function pack(num) {
|
||||
return (
|
||||
'' +
|
||||
String.fromCharCode(num & 0xff) +
|
||||
String.fromCharCode((num >> 8) & 0xff) +
|
||||
String.fromCharCode((num >> 16) & 0xff) +
|
||||
String.fromCharCode((num >> 24) & 0xff)
|
||||
);
|
||||
return (
|
||||
"" +
|
||||
String.fromCharCode(num & 0xff) +
|
||||
String.fromCharCode((num >> 8) & 0xff) +
|
||||
String.fromCharCode((num >> 16) & 0xff) +
|
||||
String.fromCharCode((num >> 24) & 0xff)
|
||||
);
|
||||
}
|
||||
|
||||
// Returns true if claim is an initial claim, false if it's an update to an existing claim
|
||||
function isInitialClaim(claim) {
|
||||
const reversed = reverseString(fromHex(claim.txid));
|
||||
const concat = reversed.concat(pack(claim.nout));
|
||||
const sha256 = new hashes.SHA256({ utf8: false }).raw(concat);
|
||||
const ripemd160 = new hashes.RMD160({ utf8: false }).raw(sha256);
|
||||
const hash = toHex(reverseString(ripemd160));
|
||||
return hash == claim.claim_id;
|
||||
const reversed = reverseString(fromHex(claim.txid));
|
||||
const concat = reversed.concat(pack(claim.nout));
|
||||
const sha256 = new hashes.SHA256({ utf8: false }).raw(concat);
|
||||
const ripemd160 = new hashes.RMD160({ utf8: false }).raw(sha256);
|
||||
const hash = toHex(reverseString(ripemd160));
|
||||
return hash == claim.claim_id;
|
||||
}
|
||||
|
||||
const rewards = {};
|
||||
|
||||
(rewards.TYPE_NEW_DEVELOPER = 'new_developer'), (rewards.TYPE_NEW_USER =
|
||||
'new_user'), (rewards.TYPE_CONFIRM_EMAIL =
|
||||
'confirm_email'), (rewards.TYPE_FIRST_CHANNEL =
|
||||
'new_channel'), (rewards.TYPE_FIRST_STREAM =
|
||||
'first_stream'), (rewards.TYPE_MANY_DOWNLOADS =
|
||||
'many_downloads'), (rewards.TYPE_FIRST_PUBLISH = 'first_publish');
|
||||
rewards.TYPE_FEATURED_DOWNLOAD = 'featured_download';
|
||||
(rewards.TYPE_NEW_DEVELOPER = "new_developer"), (rewards.TYPE_NEW_USER =
|
||||
"new_user"), (rewards.TYPE_CONFIRM_EMAIL =
|
||||
"confirm_email"), (rewards.TYPE_FIRST_CHANNEL =
|
||||
"new_channel"), (rewards.TYPE_FIRST_STREAM =
|
||||
"first_stream"), (rewards.TYPE_MANY_DOWNLOADS =
|
||||
"many_downloads"), (rewards.TYPE_FIRST_PUBLISH = "first_publish");
|
||||
rewards.TYPE_FEATURED_DOWNLOAD = "featured_download";
|
||||
|
||||
rewards.claimReward = function(type) {
|
||||
function requestReward(resolve, reject, params) {
|
||||
if (!lbryio.enabled) {
|
||||
reject(new Error(__('Rewards are not enabled.')));
|
||||
return;
|
||||
}
|
||||
lbryio.call('reward', 'new', params, 'post').then(({ reward_amount }) => {
|
||||
const message = rewardMessage(type, reward_amount),
|
||||
result = {
|
||||
type: type,
|
||||
amount: reward_amount,
|
||||
message: message
|
||||
};
|
||||
function requestReward(resolve, reject, params) {
|
||||
if (!lbryio.enabled || !lbryio.getAccessToken()) {
|
||||
reject(new Error(__("Rewards are not enabled.")));
|
||||
return;
|
||||
}
|
||||
lbryio.call("reward", "new", params, "post").then(reward => {
|
||||
const message = rewardMessage(type, reward.reward_amount);
|
||||
|
||||
// Display global notice
|
||||
const action = doShowSnackBar({
|
||||
message,
|
||||
linkText: __('Show All'),
|
||||
linkTarget: '/rewards',
|
||||
isError: false
|
||||
});
|
||||
window.app.store.dispatch(action);
|
||||
// Display global notice
|
||||
const action = doShowSnackBar({
|
||||
message,
|
||||
linkText: __("Show All"),
|
||||
linkTarget: "/rewards",
|
||||
isError: false,
|
||||
});
|
||||
window.app.store.dispatch(action);
|
||||
|
||||
// Add more events here to display other places
|
||||
// Add more events here to display other places
|
||||
|
||||
resolve(result);
|
||||
}, reject);
|
||||
}
|
||||
resolve(reward);
|
||||
}, reject);
|
||||
}
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
lbry.wallet_unused_address().then(address => {
|
||||
const params = {
|
||||
reward_type: type,
|
||||
wallet_address: address
|
||||
};
|
||||
return new Promise((resolve, reject) => {
|
||||
lbry.wallet_unused_address().then(address => {
|
||||
const params = {
|
||||
reward_type: type,
|
||||
wallet_address: address,
|
||||
};
|
||||
|
||||
switch (type) {
|
||||
case rewards.TYPE_FIRST_CHANNEL:
|
||||
lbry
|
||||
.claim_list_mine()
|
||||
.then(function(claims) {
|
||||
let claim = claims.find(function(claim) {
|
||||
return (
|
||||
claim.name.length &&
|
||||
claim.name[0] == '@' &&
|
||||
claim.txid.length &&
|
||||
isInitialClaim(claim)
|
||||
);
|
||||
});
|
||||
if (claim) {
|
||||
params.transaction_id = claim.txid;
|
||||
requestReward(resolve, reject, params);
|
||||
} else {
|
||||
reject(
|
||||
new Error(__('Please create a channel identity first.'))
|
||||
);
|
||||
}
|
||||
})
|
||||
.catch(reject);
|
||||
break;
|
||||
switch (type) {
|
||||
case rewards.TYPE_FIRST_CHANNEL:
|
||||
lbry
|
||||
.claim_list_mine()
|
||||
.then(function(claims) {
|
||||
let claim = claims.reverse().find(function(claim) {
|
||||
return (
|
||||
claim.name.length &&
|
||||
claim.name[0] == "@" &&
|
||||
claim.txid.length &&
|
||||
isInitialClaim(claim)
|
||||
);
|
||||
});
|
||||
if (claim) {
|
||||
params.transaction_id = claim.txid;
|
||||
requestReward(resolve, reject, params);
|
||||
} else {
|
||||
reject(
|
||||
new Error(__("Please create a channel identity first."))
|
||||
);
|
||||
}
|
||||
})
|
||||
.catch(reject);
|
||||
break;
|
||||
|
||||
case rewards.TYPE_FIRST_PUBLISH:
|
||||
lbry
|
||||
.claim_list_mine()
|
||||
.then(claims => {
|
||||
let claim = claims.find(function(claim) {
|
||||
return (
|
||||
claim.name.length &&
|
||||
claim.name[0] != '@' &&
|
||||
claim.txid.length &&
|
||||
isInitialClaim(claim)
|
||||
);
|
||||
});
|
||||
if (claim) {
|
||||
params.transaction_id = claim.txid;
|
||||
requestReward(resolve, reject, params);
|
||||
} else {
|
||||
reject(
|
||||
claims.length
|
||||
? new Error(
|
||||
__(
|
||||
'Please publish something and wait for confirmation by the network to claim this reward.'
|
||||
)
|
||||
)
|
||||
: new Error(
|
||||
__('Please publish something to claim this reward.')
|
||||
)
|
||||
);
|
||||
}
|
||||
})
|
||||
.catch(reject);
|
||||
break;
|
||||
case rewards.TYPE_FIRST_PUBLISH:
|
||||
lbry
|
||||
.claim_list_mine()
|
||||
.then(claims => {
|
||||
let claim = claims.reverse().find(function(claim) {
|
||||
return (
|
||||
claim.name.length &&
|
||||
claim.name[0] != "@" &&
|
||||
claim.txid.length &&
|
||||
isInitialClaim(claim)
|
||||
);
|
||||
});
|
||||
if (claim) {
|
||||
params.transaction_id = claim.txid;
|
||||
requestReward(resolve, reject, params);
|
||||
} else {
|
||||
reject(
|
||||
claims.length
|
||||
? new Error(
|
||||
__(
|
||||
"Please publish something and wait for confirmation by the network to claim this reward."
|
||||
)
|
||||
)
|
||||
: new Error(
|
||||
__("Please publish something to claim this reward.")
|
||||
)
|
||||
);
|
||||
}
|
||||
})
|
||||
.catch(reject);
|
||||
break;
|
||||
|
||||
case rewards.TYPE_FIRST_STREAM:
|
||||
case rewards.TYPE_NEW_USER:
|
||||
default:
|
||||
requestReward(resolve, reject, params);
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
rewards.claimEligiblePurchaseRewards = function() {
|
||||
let types = {};
|
||||
types[rewards.TYPE_FIRST_STREAM] = false;
|
||||
types[rewards.TYPE_FEATURED_DOWNLOAD] = false;
|
||||
types[rewards.TYPE_MANY_DOWNLOADS] = false;
|
||||
lbryio.call('reward', 'list', {}).then(
|
||||
userRewards => {
|
||||
userRewards.forEach(reward => {
|
||||
if (types[reward.reward_type] === false && reward.transaction_id) {
|
||||
types[reward.reward_type] = true;
|
||||
}
|
||||
});
|
||||
let unclaimedType = Object.keys(types).find(type => {
|
||||
return types[type] === false && type !== rewards.TYPE_FEATURED_DOWNLOAD; //handled below
|
||||
});
|
||||
if (unclaimedType) {
|
||||
rewards.claimReward(unclaimedType);
|
||||
}
|
||||
if (types[rewards.TYPE_FEATURED_DOWNLOAD] === false) {
|
||||
rewards.claimReward(rewards.TYPE_FEATURED_DOWNLOAD);
|
||||
}
|
||||
},
|
||||
() => {}
|
||||
);
|
||||
case rewards.TYPE_FIRST_STREAM:
|
||||
case rewards.TYPE_NEW_USER:
|
||||
default:
|
||||
requestReward(resolve, reject, params);
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
export default rewards;
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
import { createSelector } from "reselect";
|
||||
import { selectDaemonReady, selectCurrentPage } from "selectors/app";
|
||||
|
||||
const _selectState = state => state.availability;
|
||||
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
import { createSelector } from "reselect";
|
||||
import { selectDaemonReady, selectCurrentPage } from "selectors/app";
|
||||
|
||||
export const _selectState = state => state.content || {};
|
||||
|
||||
|
|
|
@ -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
82
ui/js/selectors/user.js
Normal 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)
|
||||
);
|
|
@ -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 || {}
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -165,3 +165,8 @@ p
|
|||
section.section-spaced {
|
||||
margin-bottom: $spacing-vertical;
|
||||
}
|
||||
|
||||
.text-center
|
||||
{
|
||||
text-align: center;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
Loading…
Reference in a new issue