Add identity verification to app #366

Merged
kauffj merged 15 commits from rewards3 into master 2017-07-25 01:02:40 +02:00
58 changed files with 1088 additions and 500 deletions

View file

@ -4,7 +4,6 @@ import {
selectUpdateUrl, selectUpdateUrl,
selectUpgradeDownloadPath, selectUpgradeDownloadPath,
selectUpgradeDownloadItem, selectUpgradeDownloadItem,
selectUpgradeFilename,
selectPageTitle, selectPageTitle,
selectCurrentPage, selectCurrentPage,
selectCurrentParams, selectCurrentParams,
@ -35,6 +34,20 @@ export function doNavigate(path, params = {}) {
}; };
} }
export function doAuthNavigate(pathAfterAuth = null, params = {}) {
return function(dispatch, getState) {
if (pathAfterAuth) {
dispatch({
type: types.CHANGE_AFTER_AUTH_PATH,
data: {
path: `${pathAfterAuth}?${toQueryString(params)}`,
},
});
}
dispatch(doNavigate("/auth"));
};
}
export function doChangePath(path, options = {}) { export function doChangePath(path, options = {}) {
return function(dispatch, getState) { return function(dispatch, getState) {
dispatch({ dispatch({
@ -237,8 +250,6 @@ export function doCheckDaemonVersion() {
export function doAlertError(errorList) { export function doAlertError(errorList) {
return function(dispatch, getState) { return function(dispatch, getState) {
const state = getState(); const state = getState();
console.log("do alert error");
console.log(errorList);
dispatch({ dispatch({
type: types.OPEN_MODAL, type: types.OPEN_MODAL,
data: { data: {

View file

@ -15,8 +15,8 @@ import { selectBadgeNumber } from "selectors/app";
import { selectTotalDownloadProgress } from "selectors/file_info"; import { selectTotalDownloadProgress } from "selectors/file_info";
import setBadge from "util/setBadge"; import setBadge from "util/setBadge";
import setProgressBar from "util/setProgressBar"; import setProgressBar from "util/setProgressBar";
import { doFileList } from "actions/file_info";
import batchActions from "util/batchActions"; import batchActions from "util/batchActions";
import * as modals from "constants/modal_types";
const { ipcRenderer } = require("electron"); const { ipcRenderer } = require("electron");
@ -294,7 +294,7 @@ export function doPurchaseUri(uri, purchaseModalName) {
} }
if (cost > balance) { if (cost > balance) {
dispatch(doOpenModal("notEnoughCredits")); dispatch(doOpenModal(modals.INSUFFICIENT_CREDITS));
} else { } else {
dispatch(doOpenModal(purchaseModalName)); dispatch(doOpenModal(purchaseModalName));
} }

View file

@ -1,4 +1,5 @@
import * as types from "constants/action_types"; import * as types from "constants/action_types";
import * as modals from "constants/modal_types";
import lbryio from "lbryio"; import lbryio from "lbryio";
import rewards from "rewards"; import rewards from "rewards";
import { selectRewardsByType } from "selectors/rewards"; import { selectRewardsByType } from "selectors/rewards";
@ -58,6 +59,12 @@ export function doClaimReward(reward, saveError = false) {
reward, reward,
}, },
}); });
if (reward.reward_type == rewards.TYPE_NEW_USER) {
dispatch({
type: types.OPEN_MODAL,
data: { modal: modals.FIRST_REWARD },
});
}
}; };
const failure = error => { const failure = error => {
@ -99,9 +106,7 @@ export function doClaimEligiblePurchaseRewards() {
if (unclaimedType) { if (unclaimedType) {
dispatch(doClaimRewardType(unclaimedType)); dispatch(doClaimRewardType(unclaimedType));
} }
if (types[rewards.TYPE_FEATURED_DOWNLOAD] === false) {
dispatch(doClaimRewardType(rewards.TYPE_FEATURED_DOWNLOAD)); dispatch(doClaimRewardType(rewards.TYPE_FEATURED_DOWNLOAD));
}
}; };
} }

View file

@ -1,8 +1,9 @@
import * as types from "constants/action_types"; import * as types from "constants/action_types";
import lbryio from "lbryio"; import lbryio from "lbryio";
import { setLocal } from "utils"; import { setLocal } from "utils";
import { doRewardList } from "actions/rewards"; import { doRewardList, doClaimRewardType } from "actions/rewards";
import { selectEmailToVerify } from "selectors/user"; import { selectEmailToVerify, selectUser } from "selectors/user";
import rewards from "rewards";
export function doAuthenticate() { export function doAuthenticate() {
return function(dispatch, getState) { return function(dispatch, getState) {
@ -137,6 +138,37 @@ export function doUserEmailVerify(verificationToken) {
}; };
} }
export function doUserIdentityVerify(stripeToken) {
return function(dispatch, getState) {
dispatch({
type: types.USER_IDENTITY_VERIFY_STARTED,
token: stripeToken,
});
lbryio
.call("user", "verify_identity", { stripe_token: stripeToken }, "post")
.then(user => {
if (user.is_identity_verified) {
dispatch({
type: types.USER_IDENTITY_VERIFY_SUCCESS,
data: { user },
});
dispatch(doClaimRewardType(rewards.TYPE_NEW_USER));
} else {
throw new Error(
"Your identity is still not verified. This should not happen."
); //shouldn't happen
}
})
.catch(error => {
dispatch({
type: types.USER_IDENTITY_VERIFY_FAILURE,
data: { error: error.toString() },
});
});
};
}
export function doFetchAccessToken() { export function doFetchAccessToken() {
return function(dispatch, getState) { return function(dispatch, getState) {
const success = token => const success = token =>

View file

@ -1,23 +1,41 @@
import React from "react"; import React from "react";
import { connect } from "react-redux"; import { connect } from "react-redux";
import { selectCurrentModal } from "selectors/app"; import { selectCurrentModal } from "selectors/app";
import { import {
doCheckUpgradeAvailable, doCheckUpgradeAvailable,
doOpenModal,
doAlertError, doAlertError,
doRecordScroll, doRecordScroll,
doCheckDaemonVersion,
} from "actions/app"; } from "actions/app";
import { doUpdateBalance } from "actions/wallet"; import { doUpdateBalance } from "actions/wallet";
import { selectWelcomeModalAcknowledged } from "selectors/app";
import rewards from "rewards";
import {
selectFetchingRewards,
makeSelectHasClaimedReward,
} from "selectors/rewards";
import { selectUser } from "selectors/user";
import App from "./view"; import App from "./view";
import * as modals from "constants/modal_types";
const select = state => ({ const select = (state, props) => {
const selectHasClaimed = makeSelectHasClaimedReward();
return {
modal: selectCurrentModal(state), modal: selectCurrentModal(state),
}); isWelcomeAcknowledged: selectWelcomeModalAcknowledged(state),
isFetchingRewards: selectFetchingRewards(state),
isWelcomeRewardClaimed: selectHasClaimed(state, {
reward_type: rewards.TYPE_NEW_USER,
}),
user: selectUser(state),
};
};
const perform = dispatch => ({ const perform = dispatch => ({
alertError: errorList => dispatch(doAlertError(errorList)), alertError: errorList => dispatch(doAlertError(errorList)),
checkUpgradeAvailable: () => dispatch(doCheckUpgradeAvailable()), checkUpgradeAvailable: () => dispatch(doCheckUpgradeAvailable()),
openWelcomeModal: () => dispatch(doOpenModal(modals.WELCOME)),
updateBalance: balance => dispatch(doUpdateBalance(balance)), updateBalance: balance => dispatch(doUpdateBalance(balance)),
recordScroll: scrollPosition => dispatch(doRecordScroll(scrollPosition)), recordScroll: scrollPosition => dispatch(doRecordScroll(scrollPosition)),
}); });

View file

@ -3,30 +3,58 @@ import Router from "component/router";
import Header from "component/header"; import Header from "component/header";
import ModalError from "component/modalError"; import ModalError from "component/modalError";
import ModalDownloading from "component/modalDownloading"; import ModalDownloading from "component/modalDownloading";
import ModalInsufficientCredits from "component/modalInsufficientCredits";
import ModalUpgrade from "component/modalUpgrade"; import ModalUpgrade from "component/modalUpgrade";
import ModalWelcome from "component/modalWelcome"; import ModalWelcome from "component/modalWelcome";
import ModalFirstReward from "component/modalFirstReward";
import lbry from "lbry"; import lbry from "lbry";
import { Line } from "rc-progress"; import * as modals from "constants/modal_types";
class App extends React.PureComponent { class App extends React.PureComponent {
componentWillMount() { componentWillMount() {
const { alertError, checkUpgradeAvailable, updateBalance } = this.props;
document.addEventListener("unhandledError", event => { document.addEventListener("unhandledError", event => {
this.props.alertError(event.detail); alertError(event.detail);
}); });
if (!this.props.upgradeSkipped) { if (!this.props.upgradeSkipped) {
this.props.checkUpgradeAvailable(); checkUpgradeAvailable();
} }
lbry.balanceSubscribe(balance => { lbry.balanceSubscribe(balance => {
this.props.updateBalance(balance); updateBalance(balance);
}); });
this.showWelcome(this.props);
this.scrollListener = () => this.props.recordScroll(window.scrollY); this.scrollListener = () => this.props.recordScroll(window.scrollY);
window.addEventListener("scroll", this.scrollListener); window.addEventListener("scroll", this.scrollListener);
} }
componentWillReceiveProps(nextProps) {
this.showWelcome(nextProps);
}
showWelcome(props) {
const {
isFetchingRewards,
isWelcomeAcknowledged,
isWelcomeRewardClaimed,
openWelcomeModal,
user,
} = props;
if (
!isWelcomeAcknowledged &&
user &&
(isFetchingRewards === false && isWelcomeRewardClaimed === false)
) {
openWelcomeModal();
}
}
componentWillUnmount() { componentWillUnmount() {
window.removeEventListener("scroll", this.scrollListener); window.removeEventListener("scroll", this.scrollListener);
} }
@ -40,10 +68,12 @@ class App extends React.PureComponent {
<div id="main-content"> <div id="main-content">
<Router /> <Router />
</div> </div>
{modal == "upgrade" && <ModalUpgrade />} {modal == modals.UPGRADE && <ModalUpgrade />}
{modal == "downloading" && <ModalDownloading />} {modal == modals.DOWNLOADING && <ModalDownloading />}
{modal == "error" && <ModalError />} {modal == modals.ERROR && <ModalError />}
{modal == "welcome" && <ModalWelcome />} {modal == modals.INSUFFICIENT_CREDITS && <ModalInsufficientCredits />}
{modal == modals.WELCOME && <ModalWelcome />}
{modal == modals.FIRST_REWARD && <ModalFirstReward />}
</div> </div>
); );
} }

View file

@ -1,16 +0,0 @@
import React from "react";
import { connect } from "react-redux";
import {
selectAuthenticationIsPending,
selectEmailToVerify,
selectUserIsVerificationCandidate,
} from "selectors/user";
import Auth from "./view";
const select = state => ({
isPending: selectAuthenticationIsPending(state),
email: selectEmailToVerify(state),
isVerificationCandidate: selectUserIsVerificationCandidate(state),
});
export default connect(select, null)(Auth);

View file

@ -1,22 +0,0 @@
import React from "react";
import { BusyMessage } from "component/common";
import UserEmailNew from "component/userEmailNew";
import UserEmailVerify from "component/userEmailVerify";
export class Auth extends React.PureComponent {
render() {
const { isPending, email, isVerificationCandidate } = this.props;
if (isPending) {
return <BusyMessage message={__("Authenticating")} />;
} else if (!email) {
return <UserEmailNew />;
} else if (isVerificationCandidate) {
return <UserEmailVerify />;
} else {
return <span className="empty">{__("No further steps.")}</span>;
}
}
}
export default Auth;

View file

@ -1,33 +0,0 @@
import React from "react";
import * as modal from "constants/modal_types";
import rewards from "rewards.js";
import { connect } from "react-redux";
import { doUserEmailDecline } from "actions/user";
import { doOpenModal } from "actions/app";
import {
selectAuthenticationIsPending,
selectUserHasEmail,
selectUserIsAuthRequested,
} from "selectors/user";
import { makeSelectHasClaimedReward } from "selectors/rewards";
import AuthOverlay from "./view";
const select = (state, props) => {
const selectHasClaimed = makeSelectHasClaimedReward();
return {
hasEmail: selectUserHasEmail(state),
isPending: selectAuthenticationIsPending(state),
isShowing: selectUserIsAuthRequested(state),
hasNewUserReward: selectHasClaimed(state, {
reward_type: rewards.TYPE_NEW_USER,
}),
};
};
const perform = dispatch => ({
userEmailDecline: () => dispatch(doUserEmailDecline()),
openWelcomeModal: () => dispatch(doOpenModal(modal.WELCOME)),
});
export default connect(select, perform)(AuthOverlay);

View file

@ -1,90 +0,0 @@
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";
import { getLocal, setLocal } from "utils";
export class AuthOverlay extends React.PureComponent {
constructor(props) {
super(props);
this.state = {
showNoEmailConfirm: false,
};
}
componentWillReceiveProps(nextProps) {
if (
this.props.isShowing &&
!this.props.isPending &&
!nextProps.hasNewUserReward &&
!nextProps.isShowing /* && !getLocal("welcome_screen_shown")*/
) {
setLocal("welcome_screen_shown", true);
setTimeout(() => this.props.openWelcomeModal(), 1);
}
}
onEmailSkipClick() {
this.setState({ showNoEmailConfirm: true });
}
onEmailSkipConfirm() {
this.props.userEmailDecline();
}
render() {
if (!lbryio.enabled) {
return null;
}
const { isPending, isShowing, hasEmail } = this.props;
if (isShowing) {
return (
<ModalPage
className="modal-page--full"
isOpen={true}
contentLabel="Authentication"
>
<h1>LBRY Early Access</h1>
<Auth />
{isPending
? ""
: <div className="form-row-submit">
{!hasEmail && this.state.showNoEmailConfirm
? <div className="help form-input-width">
<p>
{__(
"If you continue without an email, you will be ineligible to earn free LBC rewards, as well as unable to receive security related communications."
)}
</p>
<Link
onClick={() => {
this.onEmailSkipConfirm();
}}
label={__("Continue without email")}
/>
</div>
: <Link
className={"button-text-help"}
onClick={() => {
hasEmail
? this.onEmailSkipConfirm()
: this.onEmailSkipClick();
}}
label={
hasEmail ? __("Skip for now") : __("Do I have to?")
}
/>}
</div>}
</ModalPage>
);
}
return null;
}
}
export default AuthOverlay;

View file

@ -0,0 +1,12 @@
import React from "react";
import { connect } from "react-redux";
import { selectUserEmail } from "selectors/user";
import CardVerify from "./view";
const select = state => ({
email: selectUserEmail(state),
});
const perform = dispatch => ({});
export default connect(select, perform)(CardVerify);

View file

@ -0,0 +1,377 @@
import React from "react";
import PropTypes from "prop-types";
import Link from "component/link";
let scriptLoading = false;
let scriptLoaded = false;
let scriptDidError = false;
class CardVerify extends React.Component {
static defaultProps = {
label: "Verify",
locale: "auto",
};
static propTypes = {
// If included, will render the default blue button with label text.
// (Requires including stripe-checkout.css or adding the .styl file
// to your pipeline)
label: PropTypes.string,
// =====================================================
// Required by stripe
// see Stripe docs for more info:
// https://stripe.com/docs/checkout#integration-custom
// =====================================================
// Your publishable key (test or live).
// can't use "key" as a prop in react, so have to change the keyname
stripeKey: PropTypes.string.isRequired,
// The callback to invoke when the Checkout process is complete.
// function(token)
// token is the token object created.
// token.id can be used to create a charge or customer.
// token.email contains the email address entered by the user.
token: PropTypes.func.isRequired,
// ==========================
// Highly Recommended Options
// ==========================
// Name of the company or website.
name: PropTypes.string,
// A description of the product or service being purchased.
description: PropTypes.string,
// Specify auto to display Checkout in the user's preferred language, if
// available. English will be used by default.
//
// https://stripe.com/docs/checkout#supported-languages
// for more info.
locale: PropTypes.oneOf([
"auto", // (Default) Automatically chosen by checkout
"zh", // Simplified Chinese
"da", // Danish
"nl", // Dutch
"en", // English
"fr", // French
"de", // German
"it", // Italian
"ja", // Japanease
"no", // Norwegian
"es", // Spanish
"sv", // Swedish
]),
// ==============
// Optional Props
// ==============
// The currency of the amount (3-letter ISO code). The default is USD.
currency: PropTypes.oneOf([
"AED",
"AFN",
"ALL",
"AMD",
"ANG",
"AOA",
"ARS",
"AUD",
"AWG",
"AZN",
"BAM",
"BBD",
"BDT",
"BGN",
"BIF",
"BMD",
"BND",
"BOB",
"BRL",
"BSD",
"BWP",
"BZD",
"CAD",
"CDF",
"CHF",
"CLP",
"CNY",
"COP",
"CRC",
"CVE",
"CZK",
"DJF",
"DKK",
"DOP",
"DZD",
"EEK",
"EGP",
"ETB",
"EUR",
"FJD",
"FKP",
"GBP",
"GEL",
"GIP",
"GMD",
"GNF",
"GTQ",
"GYD",
"HKD",
"HNL",
"HRK",
"HTG",
"HUF",
"IDR",
"ILS",
"INR",
"ISK",
"JMD",
"JPY",
"KES",
"KGS",
"KHR",
"KMF",
"KRW",
"KYD",
"KZT",
"LAK",
"LBP",
"LKR",
"LRD",
"LSL",
"LTL",
"LVL",
"MAD",
"MDL",
"MGA",
"MKD",
"MNT",
"MOP",
"MRO",
"MUR",
"MVR",
"MWK",
"MXN",
"MYR",
"MZN",
"NAD",
"NGN",
"NIO",
"NOK",
"NPR",
"NZD",
"PAB",
"PEN",
"PGK",
"PHP",
"PKR",
"PLN",
"PYG",
"QAR",
"RON",
"RSD",
"RUB",
"RWF",
"SAR",
"SBD",
"SCR",
"SEK",
"SGD",
"SHP",
"SLL",
"SOS",
"SRD",
"STD",
"SVC",
"SZL",
"THB",
"TJS",
"TOP",
"TRY",
"TTD",
"TWD",
"TZS",
"UAH",
"UGX",
"USD",
"UYU",
"UZS",
"VND",
"VUV",
"WST",
"XAF",
"XCD",
"XOF",
"XPF",
"YER",
"ZAR",
"ZMW",
]),
// The label of the payment button in the Checkout form (e.g. Subscribe,
// Pay {{amount}}, etc.). If you include {{amount}}, it will be replaced
// by the provided amount. Otherwise, the amount will be appended to the
// end of your label.
panelLabel: PropTypes.string,
};
constructor(props) {
super(props);
this.state = {
open: false,
};
}
componentDidMount() {
if (scriptLoaded) {
return;
}
if (scriptLoading) {
return;
}
scriptLoading = true;
const script = document.createElement("script");
script.src = "https://checkout.stripe.com/checkout.js";
script.async = 1;
this.loadPromise = (() => {
let canceled = false;
const promise = new Promise((resolve, reject) => {
script.onload = () => {
scriptLoaded = true;
scriptLoading = false;
resolve();
this.onScriptLoaded();
};
script.onerror = event => {
scriptDidError = true;
scriptLoading = false;
reject(event);
this.onScriptError(event);
};
});
const wrappedPromise = new Promise((accept, cancel) => {
promise.then(
() => (canceled ? cancel({ isCanceled: true }) : accept())
);
promise.catch(
error => (canceled ? cancel({ isCanceled: true }) : cancel(error))
);
});
return {
promise: wrappedPromise,
cancel() {
canceled = true;
},
};
})();
this.loadPromise.promise
.then(this.onScriptLoaded)
.catch(this.onScriptError);
document.body.appendChild(script);
}
componentDidUpdate() {
if (!scriptLoading) {
this.updateStripeHandler();
}
}
componentWillUnmount() {
if (this.loadPromise) {
this.loadPromise.cancel();
}
if (CardVerify.stripeHandler && this.state.open) {
CardVerify.stripeHandler.close();
}
}
onScriptLoaded = () => {
if (!CardVerify.stripeHandler) {
CardVerify.stripeHandler = StripeCheckout.configure({
key: this.props.stripeKey,
});
if (this.hasPendingClick) {
this.showStripeDialog();
}
}
};
onScriptError = (...args) => {
throw new Error("Unable to load credit validation script.");
};
onClosed = () => {
this.setState({ open: false });
};
getConfig = () =>
["token", "name", "description"].reduce(
(config, key) =>
Object.assign(
{},
config,
this.props.hasOwnProperty(key) && {
[key]: this.props[key],
}
),
{
allowRememberMe: false,
closed: this.onClosed,
description: __("Confirm Identity"),
email: this.props.email,
panelLabel: "Verify",
}
);
updateStripeHandler() {
if (!CardVerify.stripeHandler) {
CardVerify.stripeHandler = StripeCheckout.configure({
key: this.props.stripeKey,
});
}
}
showStripeDialog() {
this.setState({ open: true });
CardVerify.stripeHandler.open(this.getConfig());
}
onClick = () => {
if (scriptDidError) {
try {
throw new Error(
"Tried to call onClick, but StripeCheckout failed to load"
);
} catch (x) {}
} else if (CardVerify.stripeHandler) {
this.showStripeDialog();
} else {
this.hasPendingClick = true;
}
};
render() {
return (
<Link
button="primary"
label={this.props.label}
disabled={
this.props.disabled || this.state.open || this.hasPendingClick
}
onClick={this.onClick.bind(this)}
/>
);
}
}
export default CardVerify;

View file

@ -1,4 +1,5 @@
import React from "react"; import React from "react";
import { formatCredits } from "utils";
import lbry from "../lbry.js"; import lbry from "../lbry.js";
//component/icon.js //component/icon.js
@ -78,7 +79,7 @@ export class CreditAmount extends React.PureComponent {
}; };
render() { render() {
const formattedAmount = lbry.formatCredits( const formattedAmount = formatCredits(
this.props.amount, this.props.amount,
this.props.precision this.props.precision
); );
@ -140,7 +141,7 @@ export class Address extends React.PureComponent {
}} }}
style={addressStyle} style={addressStyle}
readOnly="readonly" readOnly="readonly"
value={this.props.address} value={this.props.address || ""}
/> />
); );
} }

View file

@ -181,13 +181,6 @@ class FileActions extends React.PureComponent {
</strong>{" "} </strong>{" "}
{__("credits")}. {__("credits")}.
</Modal> </Modal>
<Modal
isOpen={modal == "notEnoughCredits"}
contentLabel={__("Not enough credits")}
onConfirmed={closeModal}
>
{__("You don't have enough LBRY credits to pay for this stream.")}
</Modal>
<Modal <Modal
isOpen={modal == "timedOut"} isOpen={modal == "timedOut"}
contentLabel={__("Download failed")} contentLabel={__("Download failed")}

View file

@ -184,6 +184,10 @@ export class FormRow extends React.PureComponent {
React.PropTypes.string, React.PropTypes.string,
React.PropTypes.element, React.PropTypes.element,
]), ]),
errorMessage: React.PropTypes.oneOfType([
React.PropTypes.string,
React.PropTypes.object,
]),
// helper: React.PropTypes.html, // helper: React.PropTypes.html,
}; };
@ -204,6 +208,8 @@ export class FormRow extends React.PureComponent {
isError: !!props.errorMessage, isError: !!props.errorMessage,
errorMessage: typeof props.errorMessage === "string" errorMessage: typeof props.errorMessage === "string"
? props.errorMessage ? props.errorMessage
: props.errorMessage instanceof Error
? props.errorMessage.toString()
: "", : "",
}; };
} }

View file

@ -1,12 +1,12 @@
import React from "react"; import React from "react";
import lbry from "lbry"; import { formatCredits } from "utils";
import { connect } from "react-redux"; import { connect } from "react-redux";
import { selectBalance } from "selectors/wallet"; import { selectBalance } from "selectors/wallet";
import { doNavigate, doHistoryBack } from "actions/app"; import { doNavigate, doHistoryBack } from "actions/app";
import Header from "./view"; import Header from "./view";
const select = state => ({ const select = state => ({
balance: lbry.formatCredits(selectBalance(state), 1), balance: formatCredits(selectBalance(state), 1),
publish: __("Publish"), publish: __("Publish"),
}); });

View file

@ -11,7 +11,6 @@ const Link = props => {
icon, icon,
badge, badge,
button, button,
hidden,
disabled, disabled,
children, children,
} = props; } = props;

View file

@ -1,21 +0,0 @@
import React from "react";
import ReactModal from "react-modal";
export class ModalPage extends React.PureComponent {
render() {
return (
<ReactModal
onCloseRequested={this.props.onAborted || this.props.onConfirmed}
{...this.props}
className={(this.props.className || "") + " modal-page"}
overlayClassName="modal-overlay"
>
<div className="modal-page__content">
{this.props.children}
</div>
</ReactModal>
);
}
}
export default ModalPage;

View file

@ -0,0 +1,20 @@
import React from "react";
import rewards from "rewards";
import { connect } from "react-redux";
import { doCloseModal } from "actions/app";
import { makeSelectRewardByType } from "selectors/rewards";
import ModalFirstReward from "./view";
const select = (state, props) => {
const selectReward = makeSelectRewardByType();
return {
reward: selectReward(state, { reward_type: rewards.TYPE_NEW_USER }),
};
};
const perform = dispatch => ({
closeModal: () => dispatch(doCloseModal()),
});
export default connect(select, perform)(ModalFirstReward);

View file

@ -0,0 +1,50 @@
import React from "react";
import { Modal } from "component/modal";
import { CreditAmount } from "component/common";
class ModalFirstReward extends React.PureComponent {
render() {
const { closeModal, reward } = this.props;
return (
<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, shown in the top right, 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, pleaseh know that LBRY is an early beta and that it earns the name."
)}
</p>
</section>
</Modal>
);
}
}
export default ModalFirstReward;

View file

@ -0,0 +1,16 @@
import React from "react";
import { connect } from "react-redux";
import { doCloseModal, doNavigate } from "actions/app";
import ModalInsufficientCredits from "./view";
const select = state => ({});
const perform = dispatch => ({
addFunds: () => {
dispatch(doNavigate("/rewards"));
dispatch(doCloseModal());
},
closeModal: () => dispatch(doCloseModal()),
});
export default connect(select, perform)(ModalInsufficientCredits);

View file

@ -0,0 +1,24 @@
import React from "react";
import { Modal } from "component/modal";
class ModalInsufficientCredits extends React.PureComponent {
render() {
const { addFunds, closeModal } = this.props;
return (
<Modal
isOpen={true}
type="confirm"
contentLabel={__("Not enough credits")}
confirmButtonLabel={__("Get Credits")}
abortButtonLabel={__("Cancel")}
onAborted={closeModal}
onConfirmed={addFunds}
>
{__("More LBRY credits are required to purchase this.")}
</Modal>
);
}
}
export default ModalInsufficientCredits;

View file

@ -1,6 +1,5 @@
import React from "react"; import React from "react";
import { Modal } from "component/modal"; import { Modal } from "component/modal";
import { downloadUpgrade, skipUpgrade } from "actions/app";
class ModalUpgrade extends React.PureComponent { class ModalUpgrade extends React.PureComponent {
render() { render() {

View file

@ -1,11 +1,11 @@
import React from "react"; import React from "react";
import rewards from "rewards"; import rewards from "rewards";
import { connect } from "react-redux"; import { connect } from "react-redux";
import { doCloseModal } from "actions/app"; import { doCloseModal, doAuthNavigate } from "actions/app";
import { doSetClientSetting } from "actions/settings";
import { selectUserIsRewardApproved } from "selectors/user"; import { selectUserIsRewardApproved } from "selectors/user";
import { import {
makeSelectHasClaimedReward, makeSelectHasClaimedReward,
makeSelectClaimRewardError,
makeSelectRewardByType, makeSelectRewardByType,
} from "selectors/rewards"; } from "selectors/rewards";
import ModalWelcome from "./view"; import ModalWelcome from "./view";
@ -15,14 +15,24 @@ const select = (state, props) => {
selectReward = makeSelectRewardByType(); selectReward = makeSelectRewardByType();
return { return {
hasClaimed: selectHasClaimed(state, { reward_type: rewards.TYPE_NEW_USER }),
isRewardApproved: selectUserIsRewardApproved(state), isRewardApproved: selectUserIsRewardApproved(state),
reward: selectReward(state, { reward_type: rewards.TYPE_NEW_USER }), reward: selectReward(state, { reward_type: rewards.TYPE_NEW_USER }),
}; };
}; };
const perform = dispatch => ({ const perform = dispatch => () => {
closeModal: () => dispatch(doCloseModal()), const closeModal = () => {
}); dispatch(doSetClientSetting("welcome_acknowledged", true));
dispatch(doCloseModal());
};
return {
verifyAccount: () => {
closeModal();
dispatch(doAuthNavigate("/rewards"));
},
closeModal: closeModal,
};
};
export default connect(select, perform)(ModalWelcome); export default connect(select, perform)(ModalWelcome);

View file

@ -6,10 +6,10 @@ import RewardLink from "component/rewardLink";
class ModalWelcome extends React.PureComponent { class ModalWelcome extends React.PureComponent {
render() { render() {
const { closeModal, hasClaimed, isRewardApproved, reward } = this.props; const { closeModal, isRewardApproved, reward, verifyAccount } = this.props;
return !hasClaimed return (
? <Modal type="custom" isOpen={true} contentLabel="Welcome to LBRY"> <Modal type="custom" isOpen={true} contentLabel="Welcome to LBRY">
<section> <section>
<h3 className="modal__header">{__("Welcome to LBRY.")}</h3> <h3 className="modal__header">{__("Welcome to LBRY.")}</h3>
<p> <p>
@ -25,56 +25,26 @@ class ModalWelcome extends React.PureComponent {
)} )}
</p> </p>
<p> <p>
{__("Thank you for making content freedom possible!")} {__("Please have")} {" "}
{" "}{isRewardApproved ? __("Here's a nickel, kid.") : ""} {reward &&
<CreditAmount amount={parseFloat(reward.reward_amount)} />}
{!reward && <span className="credit-amount">{__("??")}</span>}
{" "} {__("as a thank you for building content freedom.")}
</p> </p>
<div className="text-center"> <div className="text-center">
{isRewardApproved {isRewardApproved &&
? <RewardLink reward_type="new_user" button="primary" /> <RewardLink reward_type="new_user" button="primary" />}
: <Link {!isRewardApproved &&
<Link
button="primary" button="primary"
onClick={closeModal} onClick={verifyAccount}
label={__("Continue")} label={__("Get Welcome Credits")}
/>} />}
<Link button="alt" onClick={closeModal} label={__("Skip")} />
</div> </div>
</section> </section>
</Modal> </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>;
} }
} }

View file

@ -1,7 +1,6 @@
import React from "react"; import React from "react";
import { connect } from "react-redux"; import { connect } from "react-redux";
import { import {
makeSelectHasClaimedReward,
makeSelectClaimRewardError, makeSelectClaimRewardError,
makeSelectRewardByType, makeSelectRewardByType,
makeSelectIsRewardClaimPending, makeSelectIsRewardClaimPending,
@ -11,13 +10,11 @@ import { doClaimReward, doClaimRewardClearError } from "actions/rewards";
import RewardLink from "./view"; import RewardLink from "./view";
const makeSelect = () => { const makeSelect = () => {
const selectHasClaimedReward = makeSelectHasClaimedReward();
const selectIsPending = makeSelectIsRewardClaimPending(); const selectIsPending = makeSelectIsRewardClaimPending();
const selectReward = makeSelectRewardByType(); const selectReward = makeSelectRewardByType();
const selectError = makeSelectClaimRewardError(); const selectError = makeSelectClaimRewardError();
const select = (state, props) => ({ const select = (state, props) => ({
isClaimed: selectHasClaimedReward(state, props),
errorMessage: selectError(state, props), errorMessage: selectError(state, props),
isPending: selectIsPending(state, props), isPending: selectIsPending(state, props),
reward: selectReward(state, props), reward: selectReward(state, props),

View file

@ -1,5 +1,4 @@
import React from "react"; import React from "react";
import { Icon } from "component/common";
import Modal from "component/modal"; import Modal from "component/modal";
import Link from "component/link"; import Link from "component/link";
@ -10,22 +9,19 @@ const RewardLink = props => {
claimReward, claimReward,
clearError, clearError,
errorMessage, errorMessage,
isClaimed,
isPending, isPending,
} = props; } = props;
return ( return (
<div className="reward-link"> <div className="reward-link">
{isClaimed <Link
? <span><Icon icon="icon-check" /> Reward claimed.</span>
: <Link
button={button ? button : "alt"} button={button ? button : "alt"}
disabled={isPending} disabled={isPending}
label={isPending ? __("Claiming...") : __("Claim Reward")} label={isPending ? __("Claiming...") : __("Claim Reward")}
onClick={() => { onClick={() => {
claimReward(reward); claimReward(reward);
}} }}
/>} />
{errorMessage {errorMessage
? <Modal ? <Modal
isOpen={true} isOpen={true}

View file

@ -13,6 +13,7 @@ import FileListDownloaded from "page/fileListDownloaded";
import FileListPublished from "page/fileListPublished"; import FileListPublished from "page/fileListPublished";
import ChannelPage from "page/channel"; import ChannelPage from "page/channel";
import SearchPage from "page/search"; import SearchPage from "page/search";
import AuthPage from "page/auth";
const route = (page, routesMap) => { const route = (page, routesMap) => {
const component = routesMap[page]; const component = routesMap[page];
@ -24,22 +25,23 @@ const Router = props => {
const { currentPage, params } = props; const { currentPage, params } = props;
return route(currentPage, { return route(currentPage, {
settings: <SettingsPage {...params} />, auth: <AuthPage {...params} />,
help: <HelpPage {...params} />,
report: <ReportPage {...params} />,
downloaded: <FileListDownloaded {...params} />,
published: <FileListPublished {...params} />,
start: <StartPage {...params} />,
wallet: <WalletPage {...params} />,
send: <WalletPage {...params} />,
receive: <WalletPage {...params} />,
show: <ShowPage {...params} />,
channel: <ChannelPage {...params} />, channel: <ChannelPage {...params} />,
publish: <PublishPage {...params} />,
developer: <DeveloperPage {...params} />, developer: <DeveloperPage {...params} />,
discover: <DiscoverPage {...params} />, discover: <DiscoverPage {...params} />,
downloaded: <FileListDownloaded {...params} />,
help: <HelpPage {...params} />,
publish: <PublishPage {...params} />,
published: <FileListPublished {...params} />,
receive: <WalletPage {...params} />,
report: <ReportPage {...params} />,
rewards: <RewardsPage {...params} />, rewards: <RewardsPage {...params} />,
search: <SearchPage {...params} />, search: <SearchPage {...params} />,
send: <WalletPage {...params} />,
settings: <SettingsPage {...params} />,
show: <ShowPage {...params} />,
start: <StartPage {...params} />,
wallet: <WalletPage {...params} />,
}); });
}; };

View file

@ -27,7 +27,6 @@ class UserEmailNew extends React.PureComponent {
return ( return (
<form <form
className="form-input-width"
onSubmit={event => { onSubmit={event => {
this.handleSubmit(event); this.handleSubmit(event);
}} }}

View file

@ -13,7 +13,7 @@ class UserEmailVerify extends React.PureComponent {
handleCodeChanged(event) { handleCodeChanged(event) {
this.setState({ this.setState({
code: event.target.value, code: String(event.target.value).trim(),
}); });
} }
@ -24,14 +24,13 @@ class UserEmailVerify extends React.PureComponent {
render() { render() {
const { errorMessage, isPending } = this.props; const { errorMessage, isPending } = this.props;
return ( return (
<form <form
className="form-input-width"
onSubmit={event => { onSubmit={event => {
this.handleSubmit(event); this.handleSubmit(event);
}} }}
> >
<p>{__("Please enter the verification code emailed to you.")}</p>
<FormRow <FormRow
type="text" type="text"
label={__("Verification Code")} label={__("Verification Code")}

View file

@ -0,0 +1,26 @@
import React from "react";
import { connect } from "react-redux";
import { doUserIdentityVerify } from "actions/user";
import rewards from "rewards";
import { makeSelectRewardByType } from "selectors/rewards";
import {
selectIdentityVerifyIsPending,
selectIdentityVerifyErrorMessage,
} from "selectors/user";
import UserVerify from "./view";
const select = (state, props) => {
const selectReward = makeSelectRewardByType();
return {
isPending: selectIdentityVerifyIsPending(state),
errorMessage: selectIdentityVerifyErrorMessage(state),
reward: selectReward(state, { reward_type: rewards.TYPE_NEW_USER }),
};
};
const perform = dispatch => ({
verifyUserIdentity: token => dispatch(doUserIdentityVerify(token)),
});
export default connect(select, perform)(UserVerify);

View file

@ -0,0 +1,48 @@
import React from "react";
import { CreditAmount } from "component/common";
import CardVerify from "component/cardVerify";
class UserVerify extends React.PureComponent {
constructor(props) {
super(props);
this.state = {
code: "",
};
}
handleCodeChanged(event) {
this.setState({
code: event.target.value,
});
}
onToken(data) {
this.props.verifyUserIdentity(data.id);
}
render() {
const { errorMessage, isPending, reward } = this.props;
return (
<div>
{(!reward || !reward.transaction_id) &&
<p>
Please link a credit card to confirm your identity and receive{" "}
{reward
? <CreditAmount amount={parseFloat(reward.reward_amount)} />
: <span>your reward</span>}
</p>}
<p>{__("This is to prevent abuse. You will not be charged.")}</p>
{errorMessage && <p className="form-field__error">{errorMessage}</p>}
<CardVerify
label={__("Link Card and Finish")}
disabled={isPending}
token={this.onToken.bind(this)}
stripeKey="pk_live_e8M4dRNnCCbmpZzduEUZBgJO"
/>
</div>
);
}
}
export default UserVerify;

View file

@ -78,13 +78,6 @@ class VideoPlayButton extends React.PureComponent {
icon={icon} icon={icon}
onClick={this.onWatchClick.bind(this)} onClick={this.onWatchClick.bind(this)}
/> />
<Modal
contentLabel={__("Not enough credits")}
isOpen={modal == "notEnoughCredits"}
onConfirmed={closeModal}
>
{__("You don't have enough LBRY credits to pay for this stream.")}
</Modal>
<Modal <Modal
type="confirm" type="confirm"
isOpen={modal == "affirmPurchaseAndPlay"} isOpen={modal == "affirmPurchaseAndPlay"}

View file

@ -5,7 +5,7 @@ export const HISTORY_BACK = "HISTORY_BACK";
export const SHOW_SNACKBAR = "SHOW_SNACKBAR"; export const SHOW_SNACKBAR = "SHOW_SNACKBAR";
export const REMOVE_SNACKBAR_SNACK = "REMOVE_SNACKBAR_SNACK"; export const REMOVE_SNACKBAR_SNACK = "REMOVE_SNACKBAR_SNACK";
export const WINDOW_FOCUSED = "WINDOW_FOCUSED"; export const WINDOW_FOCUSED = "WINDOW_FOCUSED";
export const CHANGE_AFTER_AUTH_PATH = "CHANGE_AFTER_AUTH_PATH";
export const DAEMON_READY = "DAEMON_READY"; export const DAEMON_READY = "DAEMON_READY";
export const DAEMON_VERSION_MATCH = "DAEMON_VERSION_MATCH"; export const DAEMON_VERSION_MATCH = "DAEMON_VERSION_MATCH";
export const DAEMON_VERSION_MISMATCH = "DAEMON_VERSION_MISMATCH"; export const DAEMON_VERSION_MISMATCH = "DAEMON_VERSION_MISMATCH";
@ -97,6 +97,9 @@ 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_STARTED = "USER_EMAIL_VERIFY_STARTED";
export const USER_EMAIL_VERIFY_SUCCESS = "USER_EMAIL_VERIFY_SUCCESS"; export const USER_EMAIL_VERIFY_SUCCESS = "USER_EMAIL_VERIFY_SUCCESS";
export const USER_EMAIL_VERIFY_FAILURE = "USER_EMAIL_VERIFY_FAILURE"; export const USER_EMAIL_VERIFY_FAILURE = "USER_EMAIL_VERIFY_FAILURE";
export const USER_IDENTITY_VERIFY_STARTED = "USER_IDENTITY_VERIFY_STARTED";
export const USER_IDENTITY_VERIFY_SUCCESS = "USER_IDENTITY_VERIFY_SUCCESS";
export const USER_IDENTITY_VERIFY_FAILURE = "USER_IDENTITY_VERIFY_FAILURE";
export const USER_FETCH_STARTED = "USER_FETCH_STARTED"; export const USER_FETCH_STARTED = "USER_FETCH_STARTED";
export const USER_FETCH_SUCCESS = "USER_FETCH_SUCCESS"; export const USER_FETCH_SUCCESS = "USER_FETCH_SUCCESS";
export const USER_FETCH_FAILURE = "USER_FETCH_FAILURE"; export const USER_FETCH_FAILURE = "USER_FETCH_FAILURE";

View file

@ -1,3 +1,8 @@
export const WELCOME = "welcome";
export const CONFIRM_FILE_REMOVE = "confirmFileRemove"; export const CONFIRM_FILE_REMOVE = "confirmFileRemove";
export const INCOMPATIBLE_DAEMON = "incompatibleDaemon"; export const INCOMPATIBLE_DAEMON = "incompatibleDaemon";
export const DOWNLOADING = "downloading";
export const ERROR = "error";
export const INSUFFICIENT_CREDITS = "insufficient_credits";
export const UPGRADE = "upgrade";
export const WELCOME = "welcome";
export const FIRST_REWARD = "first_reward";

View file

@ -288,11 +288,6 @@ lbry.setClientSetting = function(setting, value) {
return localStorage.setItem("setting_" + setting, JSON.stringify(value)); return localStorage.setItem("setting_" + setting, JSON.stringify(value));
}; };
//utilities
lbry.formatCredits = function(amount, precision) {
return amount.toFixed(precision || 1).replace(/\.?0+$/, "");
};
lbry.formatName = function(name) { lbry.formatName = function(name) {
// Converts LBRY name to standard format (all lower case, no special characters, spaces replaced by dashes) // Converts LBRY name to standard format (all lower case, no special characters, spaces replaced by dashes)
name = name.replace("/s+/g", "-"); name = name.replace("/s+/g", "-");

View file

@ -136,8 +136,9 @@ lbryio.authenticate = function() {
resolve({ resolve({
id: 1, id: 1,
language: "en", language: "en",
has_email: true, primary_email: "disabled@lbry.io",
has_verified_email: true, has_verified_email: true,
is_identity_verified: true,
is_reward_approved: false, is_reward_approved: false,
is_reward_eligible: false, is_reward_eligible: false,
}); });

View file

@ -6,10 +6,8 @@ import SnackBar from "component/snackBar";
import { Provider } from "react-redux"; import { Provider } from "react-redux";
import store from "store.js"; import store from "store.js";
import SplashScreen from "component/splash"; import SplashScreen from "component/splash";
import AuthOverlay from "component/authOverlay";
import { doChangePath, doNavigate, doDaemonReady } from "actions/app"; import { doChangePath, doNavigate, doDaemonReady } from "actions/app";
import { toQueryString } from "util/query_params"; import { toQueryString } from "util/query_params";
import { selectBadgeNumber } from "selectors/app";
import * as types from "constants/action_types"; import * as types from "constants/action_types";
const env = ENV; const env = ENV;
@ -97,19 +95,6 @@ const updateProgress = () => {
const initialState = app.store.getState(); const initialState = app.store.getState();
// 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() { var init = function() {
function onDaemonReady() { function onDaemonReady() {
window.sessionStorage.setItem("loaded", "y"); //once we've made it here once per session, we don't need to show splash again window.sessionStorage.setItem("loaded", "y"); //once we've made it here once per session, we don't need to show splash again
@ -117,7 +102,7 @@ var init = function() {
ReactDOM.render( ReactDOM.render(
<Provider store={store}> <Provider store={store}>
<div><AuthOverlay /><App /><SnackBar /></div> <div><App /><SnackBar /></div>
</Provider>, </Provider>,
canvas canvas
); );

30
ui/js/page/auth/index.js Normal file
View file

@ -0,0 +1,30 @@
import React from "react";
import { doNavigate } from "actions/app";
import { connect } from "react-redux";
import { selectPathAfterAuth } from "selectors/app";
import {
selectAuthenticationIsPending,
selectEmailToVerify,
selectUserIsVerificationCandidate,
selectUser,
selectUserIsPending,
selectIdentityVerifyIsPending,
} from "selectors/user";
import AuthPage from "./view";
const select = state => ({
isPending:
selectAuthenticationIsPending(state) ||
selectUserIsPending(state) ||
selectIdentityVerifyIsPending(state),
email: selectEmailToVerify(state),
pathAfterAuth: selectPathAfterAuth(state),
user: selectUser(state),
isVerificationCandidate: selectUserIsVerificationCandidate(state),
});
const perform = dispatch => ({
navigate: path => dispatch(doNavigate(path)),
});
export default connect(select, perform)(AuthPage);

89
ui/js/page/auth/view.jsx Normal file
View file

@ -0,0 +1,89 @@
import React from "react";
import { BusyMessage } from "component/common";
import UserEmailNew from "component/userEmailNew";
import UserEmailVerify from "component/userEmailVerify";
import UserVerify from "component/userVerify";
export class AuthPage extends React.PureComponent {
componentWillMount() {
this.navigateIfAuthenticated(this.props);
}
componentWillReceiveProps(nextProps) {
this.navigateIfAuthenticated(nextProps);
}
navigateIfAuthenticated(props) {
const { isPending, user } = props;
if (
!isPending &&
user &&
user.has_verified_email &&
user.is_identity_verified
) {
props.navigate(props.pathAfterAuth);
}
}
getTitle() {
const { email, isPending, isVerificationCandidate, user } = this.props;
if (isPending || (user && !user.has_verified_email && !email)) {
return __("Welcome to LBRY");
} else if (user && !user.has_verified_email) {
return __("Confirm Email");
} else if (user && !user.is_identity_verified) {
return __("Confirm Identity");
} else {
return __("Welcome to LBRY");
}
}
renderMain() {
const { email, isPending, isVerificationCandidate, user } = this.props;
if (isPending) {
return <BusyMessage message={__("Authenticating")} />;
} else if (user && !user.has_verified_email && !email) {
return <UserEmailNew />;
} else if (user && !user.has_verified_email) {
return <UserEmailVerify />;
} else if (user && !user.is_identity_verified) {
return <UserVerify />;
} else {
return <span className="empty">{__("No further steps.")}</span>;
}
}
render() {
const { email, user, isPending } = this.props;
return (
<main className="">
<section className="card card--form">
<div className="card__title-primary">
<h1>{this.getTitle()}</h1>
</div>
<div className="card__content">
{!isPending &&
!email &&
!user.has_verified_email &&
<p>
{__("Create a verified identity and receive LBC rewards.")}
</p>}
{this.renderMain()}
</div>
<div className="card__content">
<div className="help">
{__(
"This information is disclosed only to LBRY, Inc. and not to the LBRY network. It is optional and only collected to provide communication and prevent abuse. You may use LBRY without providing this information."
)}
</div>
</div>
</section>
</main>
);
}
}
export default AuthPage;

View file

@ -2,10 +2,11 @@ import React from "react";
import { doNavigate } from "actions/app"; import { doNavigate } from "actions/app";
import { connect } from "react-redux"; import { connect } from "react-redux";
import { doFetchAccessToken } from "actions/user"; import { doFetchAccessToken } from "actions/user";
import { selectAccessToken } from "selectors/user"; import { selectAccessToken, selectUser } from "selectors/user";
import HelpPage from "./view"; import HelpPage from "./view";
const select = state => ({ const select = state => ({
user: selectUser(state),
accessToken: selectAccessToken(state), accessToken: selectAccessToken(state),
}); });

View file

@ -50,7 +50,7 @@ class HelpPage extends React.PureComponent {
render() { render() {
let ver, osName, platform, newVerLink; let ver, osName, platform, newVerLink;
const { navigate } = this.props; const { navigate, user } = this.props;
if (this.state.versionInfo) { if (this.state.versionInfo) {
ver = this.state.versionInfo; ver = this.state.versionInfo;
@ -146,16 +146,24 @@ class HelpPage extends React.PureComponent {
? <table className="table-standard"> ? <table className="table-standard">
<tbody> <tbody>
<tr> <tr>
<th>{__("daemon (lbrynet)")}</th> <th>{__("App")}</th>
<td>{this.state.uiVersion}</td>
</tr>
<tr>
<th>{__("Daemon (lbrynet)")}</th>
<td>{ver.lbrynet_version}</td> <td>{ver.lbrynet_version}</td>
</tr> </tr>
<tr> <tr>
<th>{__("wallet (lbryum)")}</th> <th>{__("Wallet (lbryum)")}</th>
<td>{ver.lbryum_version}</td> <td>{ver.lbryum_version}</td>
</tr> </tr>
<tr> <tr>
<th>{__("interface")}</th> <th>{__("Connected Email")}</th>
<td>{this.state.uiVersion}</td> <td>
{user && user.primary_email
? user.primary_email
: <span className="empty">{__("none")}</span>}
</td>
</tr> </tr>
<tr> <tr>
<th>{__("Platform")}</th> <th>{__("Platform")}</th>

View file

@ -1,25 +1,33 @@
import React from "react"; import React from "react";
import { connect } from "react-redux"; import { connect } from "react-redux";
import { doNavigate } from "actions/app";
import { selectFetchingRewards, selectRewards } from "selectors/rewards";
import { import {
selectUserIsRewardEligible, makeSelectRewardByType,
selectUserHasEmail, selectFetchingRewards,
selectUserIsVerificationCandidate, selectRewards,
} from "selectors/user"; } from "selectors/rewards";
import { selectUser } from "selectors/user";
import { doAuthNavigate, doNavigate } from "actions/app";
import { doRewardList } from "actions/rewards"; import { doRewardList } from "actions/rewards";
import rewards from "rewards";
import RewardsPage from "./view"; import RewardsPage from "./view";
const select = state => ({ const select = (state, props) => {
const selectReward = makeSelectRewardByType();
return {
fetching: selectFetchingRewards(state), fetching: selectFetchingRewards(state),
rewards: selectRewards(state), rewards: selectRewards(state),
hasEmail: selectUserHasEmail(state), newUserReward: selectReward(state, { reward_type: rewards.TYPE_NEW_USER }),
isEligible: selectUserIsRewardEligible(state), user: selectUser(state),
isVerificationCandidate: selectUserIsVerificationCandidate(state), };
}); };
const perform = dispatch => ({ const perform = dispatch => ({
fetchRewards: () => dispatch(doRewardList()), fetchRewards: () => dispatch(doRewardList()),
navigate: path => dispatch(doNavigate(path)),
doAuth: () => {
dispatch(doAuthNavigate("/rewards"));
},
}); });
export default connect(select, perform)(RewardsPage); export default connect(select, perform)(RewardsPage);

View file

@ -1,8 +1,6 @@
import React from "react"; import React from "react";
import lbryio from "lbryio";
import { BusyMessage, CreditAmount, Icon } from "component/common"; import { BusyMessage, CreditAmount, Icon } from "component/common";
import SubHeader from "component/subHeader"; import SubHeader from "component/subHeader";
import Auth from "component/auth";
import Link from "component/link"; import Link from "component/link";
import RewardLink from "component/rewardLink"; import RewardLink from "component/rewardLink";
@ -41,60 +39,91 @@ class RewardsPage extends React.PureComponent {
fetchRewards(props) { fetchRewards(props) {
const { fetching, rewards, fetchRewards } = props; const { fetching, rewards, fetchRewards } = props;
if (!fetching && Object.keys(rewards).length < 1) fetchRewards(); if (!fetching && (!rewards || !rewards.length)) {
fetchRewards();
}
} }
render() { render() {
const { const { doAuth, fetching, navigate, rewards, user } = this.props;
fetching,
isEligible,
isVerificationCandidate,
hasEmail,
rewards,
} = this.props;
let content, let content, cardHeader;
isCard = false;
if (!hasEmail || isVerificationCandidate) { if (fetching) {
content = (
<div className="card__content">
<BusyMessage message={__("Fetching rewards")} />
</div>
);
} else if (rewards.length > 0) {
content = ( content = (
<div> <div>
<p> {rewards.map(reward =>
{__(
"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>
</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} /> <RewardTile key={reward.reward_type} reward={reward} />
)}
</div>
); );
} else { } else {
content = <div className="empty">{__("Failed to load rewards.")}</div>; content = (
<div className="card__content empty">
{__("Failed to load rewards.")}
</div>
);
}
if (
user &&
(!user.primary_email ||
!user.has_verified_email ||
!user.is_identity_verified)
) {
cardHeader = (
<div>
<div className="card__content empty">
<p>{__("Only verified accounts are eligible to earn rewards.")}</p>
</div>
<div className="card__content">
<Link onClick={doAuth} button="primary" label="Become Verified" />
</div>
</div>
);
} else if (user && !user.is_reward_approved) {
cardHeader = (
<div className="card__content">
<p>
{__(
"This account must undergo review before you can participate in the rewards program."
)}
{" "}
{__("This can take anywhere from several minutes to several days.")}
</p>
<p>
{__(
"We apologize for this inconvenience, but have added this additional step to prevent fraud."
)}
</p>
<p>
{__("You will receive an email when this process is complete.") +
" " +
__("Please enjoy free content in the meantime!")}
</p>
<p>
<Link
onClick={() => navigate("/discover")}
button="primary"
label="Return Home"
/>
</p>
</div>
);
} }
return ( return (
<main className="main--single-column"> <main className="main--single-column">
<SubHeader /> <SubHeader />
{isCard {cardHeader && <section className="card">{cardHeader}</section>}
? <section className="card">
<div className="card__content">
{content} {content}
</div>
</section>
: content}
</main> </main>
); );
} }

View file

@ -16,6 +16,7 @@ const reducers = {};
const defaultState = { const defaultState = {
isLoaded: false, isLoaded: false,
currentPath: currentPath(), currentPath: currentPath(),
pathAfterAuth: "/discover",
platform: process.platform, platform: process.platform,
upgradeSkipped: sessionStorage.getItem("upgradeSkipped"), upgradeSkipped: sessionStorage.getItem("upgradeSkipped"),
daemonVersionMatched: null, daemonVersionMatched: null,
@ -49,6 +50,12 @@ reducers[types.CHANGE_PATH] = function(state, action) {
}); });
}; };
reducers[types.CHANGE_AFTER_AUTH_PATH] = function(state, action) {
return Object.assign({}, state, {
pathAfterAuth: action.data.path,
});
};
reducers[types.UPGRADE_CANCELLED] = function(state, action) { reducers[types.UPGRADE_CANCELLED] = function(state, action) {
return Object.assign({}, state, { return Object.assign({}, state, {
downloadProgress: null, downloadProgress: null,

View file

@ -73,7 +73,7 @@ reducers[types.USER_EMAIL_NEW_STARTED] = function(state, action) {
reducers[types.USER_EMAIL_NEW_SUCCESS] = function(state, action) { reducers[types.USER_EMAIL_NEW_SUCCESS] = function(state, action) {
let user = Object.assign({}, state.user); let user = Object.assign({}, state.user);
user.has_email = true; user.primary_email = action.data.email;
return Object.assign({}, state, { return Object.assign({}, state, {
emailToVerify: action.data.email, emailToVerify: action.data.email,
emailNewIsPending: false, emailNewIsPending: false,
@ -105,7 +105,7 @@ reducers[types.USER_EMAIL_VERIFY_STARTED] = function(state, action) {
reducers[types.USER_EMAIL_VERIFY_SUCCESS] = function(state, action) { reducers[types.USER_EMAIL_VERIFY_SUCCESS] = function(state, action) {
let user = Object.assign({}, state.user); let user = Object.assign({}, state.user);
user.has_email = true; user.primary_email = action.data.email;
return Object.assign({}, state, { return Object.assign({}, state, {
emailToVerify: "", emailToVerify: "",
emailVerifyIsPending: false, emailVerifyIsPending: false,
@ -120,6 +120,28 @@ reducers[types.USER_EMAIL_VERIFY_FAILURE] = function(state, action) {
}); });
}; };
reducers[types.USER_IDENTITY_VERIFY_STARTED] = function(state, action) {
return Object.assign({}, state, {
identityVerifyIsPending: true,
identityVerifyErrorMessage: "",
});
};
reducers[types.USER_IDENTITY_VERIFY_SUCCESS] = function(state, action) {
return Object.assign({}, state, {
identityVerifyIsPending: false,
identityVerifyErrorMessage: "",
user: action.data.user,
});
};
reducers[types.USER_IDENTITY_VERIFY_FAILURE] = function(state, action) {
return Object.assign({}, state, {
identityVerifyIsPending: false,
identityVerifyErrorMessage: action.data.error,
});
};
reducers[types.FETCH_ACCESS_TOKEN_SUCCESS] = function(state, action) { reducers[types.FETCH_ACCESS_TOKEN_SUCCESS] = function(state, action) {
const { token } = action.data; const { token } = action.data;

View file

@ -30,6 +30,10 @@ function rewardMessage(type, amount) {
"You earned %s LBC for making your first publication.", "You earned %s LBC for making your first publication.",
amount amount
), ),
featured_download: __(
"You earned %s LBC for watching a featured download.",
amount
),
}[type]; }[type];
} }

View file

@ -192,7 +192,17 @@ export const selectSnackBarSnacks = createSelector(
snackBar => snackBar.snacks || [] snackBar => snackBar.snacks || []
); );
export const selectWelcomeModalAcknowledged = createSelector(
_selectState,
state => lbry.getClientSetting("welcome_acknowledged")
);
export const selectBadgeNumber = createSelector( export const selectBadgeNumber = createSelector(
_selectState, _selectState,
state => state.badgeNumber state => state.badgeNumber
); );
export const selectPathAfterAuth = createSelector(
_selectState,
state => state.pathAfterAuth
);

View file

@ -12,25 +12,16 @@ export const selectUserIsPending = createSelector(
state => state.userIsPending state => state.userIsPending
); );
export const selectUser = createSelector( export const selectUser = createSelector(_selectState, state => state.user);
_selectState,
state => state.user || {}
);
export const selectEmailToVerify = createSelector( export const selectEmailToVerify = createSelector(
_selectState, _selectState,
state => state.emailToVerify state => state.emailToVerify
); );
export const selectUserHasEmail = createSelector( export const selectUserEmail = createSelector(
selectUser, selectUser,
selectEmailToVerify, user => (user ? user.primary_email : null)
(user, email) => (user && user.has_email) || !!email
);
export const selectUserIsRewardEligible = createSelector(
selectUser,
user => user && user.is_reward_eligible
); );
export const selectUserIsRewardApproved = createSelector( export const selectUserIsRewardApproved = createSelector(
@ -63,18 +54,19 @@ export const selectEmailVerifyErrorMessage = createSelector(
state => state.emailVerifyErrorMessage state => state.emailVerifyErrorMessage
); );
export const selectUserIsVerificationCandidate = createSelector( export const selectIdentityVerifyIsPending = createSelector(
selectUser, _selectState,
user => user && !user.has_verified_email state => state.identityVerifyIsPending
); );
export const selectUserIsAuthRequested = createSelector( export const selectIdentityVerifyErrorMessage = createSelector(
selectEmailNewDeclined, _selectState,
selectAuthenticationIsPending, state => state.identityVerifyErrorMessage
selectUserIsVerificationCandidate, );
selectUserHasEmail,
(isEmailDeclined, isPending, isVerificationCandidate, hasEmail) => export const selectUserIsVerificationCandidate = createSelector(
!isEmailDeclined && (isPending || !hasEmail || isVerificationCandidate) selectUser,
user => user && (!user.has_verified_email || !user.is_identity_verified)
); );
export const selectAccessToken = createSelector( export const selectAccessToken = createSelector(

View file

@ -31,3 +31,7 @@ export function getSession(key, fallback = undefined) {
export function setSession(key, value) { export function setSession(key, value) {
sessionStorage.setItem(key, JSON.stringify(value)); sessionStorage.setItem(key, JSON.stringify(value));
} }
export function formatCredits(amount, precision) {
return amount.toFixed(precision || 1).replace(/\.?0+$/, "");
}

View file

@ -72,8 +72,7 @@
"webpack": "^2.6.1", "webpack": "^2.6.1",
"webpack-dev-server": "^2.4.4", "webpack-dev-server": "^2.4.4",
"webpack-notifier": "^1.5.0", "webpack-notifier": "^1.5.0",
"webpack-target-electron-renderer": "^0.4.0", "webpack-target-electron-renderer": "^0.4.0"
"why-did-you-update": "0.0.8"
}, },
"lint-staged": { "lint-staged": {
"gitDir": "../", "gitDir": "../",

View file

@ -29,6 +29,7 @@ $max-content-width: 1000px;
$max-text-width: 660px; $max-text-width: 660px;
$width-page-constrained: 800px; $width-page-constrained: 800px;
$width-input-text: 330px;
$height-header: $spacing-vertical * 2.5; $height-header: $spacing-vertical * 2.5;
$height-button: $spacing-vertical * 1.5; $height-button: $spacing-vertical * 1.5;

View file

@ -15,7 +15,6 @@
@import "component/_channel-indicator.scss"; @import "component/_channel-indicator.scss";
@import "component/_notice.scss"; @import "component/_notice.scss";
@import "component/_modal.scss"; @import "component/_modal.scss";
@import "component/_modal-page.scss";
@import "component/_snack-bar.scss"; @import "component/_snack-bar.scss";
@import "component/_video.scss"; @import "component/_video.scss";
@import "page/_developer.scss"; @import "page/_developer.scss";

View file

@ -164,6 +164,10 @@ $height-card-small: $spacing-vertical * 15;
height: $width-card-small * 9 / 16; height: $width-card-small * 9 / 16;
} }
.card--form {
width: $width-input-text + $padding-card-horizontal * 2;
}
.card__subtitle { .card__subtitle {
color: $color-help; color: $color-help;
font-size: 0.85em; font-size: 0.85em;

View file

@ -1,11 +1,6 @@
@import "../global"; @import "../global";
$width-input-border: 2px; $width-input-border: 2px;
$width-input-text: 330px;
.form-input-width {
width: $width-input-text
}
.form-row-submit .form-row-submit
{ {

View file

@ -1,54 +0,0 @@
@import "../global";
.modal-page {
position: fixed;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
border: 1px solid rgb(204, 204, 204);
background: rgb(255, 255, 255);
overflow: auto;
}
.modal-page--full {
left: 0;
right: 0;
top: 0;
bottom: 0;
.modal-page__content {
max-width: 500px;
}
}
/*
.modal-page {
position: fixed;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
border: 1px solid rgb(204, 204, 204);
background: rgb(255, 255, 255);
overflow: auto;
border-radius: 4px;
outline: none;
padding: 36px;
top: 25px;
left: 25px;
right: 25px;
bottom: 25px;
}
*/
.modal-page__content {
h1, h2 {
margin-bottom: $spacing-vertical / 2;
}
h3, h4 {
margin-bottom: $spacing-vertical / 4;
}
}