From ddd01855b0489bfc4a8ac0320ac1733d18803ed3 Mon Sep 17 00:00:00 2001 From: Jeremy Kauffman Date: Wed, 31 May 2017 19:29:10 -0400 Subject: [PATCH 01/12] slight progress --- app/main.js | 2 +- ui/js/component/auth.js | 57 ++++++++-------- ui/js/lbryio.js | 103 +++++++++++++---------------- ui/scss/component/_form-field.scss | 4 ++ 4 files changed, 77 insertions(+), 89 deletions(-) diff --git a/app/main.js b/app/main.js index 85a0dd3c7..14ead8e95 100644 --- a/app/main.js +++ b/app/main.js @@ -1,6 +1,6 @@ const {app, BrowserWindow, ipcMain} = require('electron'); const url = require('url'); -const isDebug = process.env.NODE_ENV === 'development' +const isDebug = process.env.NODE_ENV === 'development' || true if (isDebug) { require('electron-debug')({showDevTools: true}); diff --git a/ui/js/component/auth.js b/ui/js/component/auth.js index 60a9ad9d4..fa4bef0d1 100644 --- a/ui/js/component/auth.js +++ b/ui/js/component/auth.js @@ -16,8 +16,9 @@ class SubmitEmailStage extends React.Component { this.state = { rewardType: null, - email: "", - submitting: false, + email: '', + showNoEmailConfirm: false, + submitting: false }; } @@ -31,6 +32,15 @@ class SubmitEmailStage extends React.Component { this.props.setStage("confirm", { email: email }); } + onEmailSkipClick() { + this.setState({ showNoEmailConfirm: true }) + } + + onEmailSkipConfirm() { + setLocal('auth_bypassed', true); + this.props.setStage(null) + } + handleSubmit(event) { event.preventDefault(); @@ -60,34 +70,21 @@ class SubmitEmailStage extends React.Component { render() { return (
-
{ - this.handleSubmit(event); - }} - > - { - this._emailRow = ref; - }} - type="text" - label={__("Email")} - placeholder="scrwvwls@lbry.io" - name="email" - value={this.state.email} - onChange={event => { - this.handleEmailChanged(event); - }} - /> -
- { - this.handleSubmit(event); - }} - /> + { this.handleSubmit(event) }}> + { this._emailRow = ref }} type="text" label={__("Email")} placeholder="scrwvwls@lbry.io" + name="email" value={this.state.email} + onChange={(event) => { this.handleEmailChanged(event) }} /> +
+ { this.handleSubmit(event) }} />
+ { this.state.showNoEmailConfirm ? +
+

If you continue without an email, you will be ineligible to earn free LBC rewards, as well as unable to receive security related communications.

+ { this.onEmailSkipConfirm() }} label="Continue without email" /> +
+ : + { this.onEmailSkipClick() }} label="Do I have to?" /> + }
); @@ -363,7 +360,7 @@ class CodeRequiredStage extends React.Component {

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

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

diff --git a/ui/js/lbryio.js b/ui/js/lbryio.js index b23697d15..a55c3355c 100644 --- a/ui/js/lbryio.js +++ b/ui/js/lbryio.js @@ -1,4 +1,4 @@ -import { getSession, setSession } from './utils.js'; +import { getSession, setSession, setLocal } from './utils.js'; import lbry from './lbry.js'; const querystring = require('querystring'); @@ -132,67 +132,54 @@ lbryio.setAccessToken = token => { }; 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, + has_verified_email: true + }) + }) + } + if (lbryio._authenticationPromise === null) { + lbryio._authenticationPromise = new Promise((resolve, reject) => { + lbry.status().then((response) => { - 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); - } - }); - } + let installation_id = response.installation_id.substring(0, response.installation_id.length - 2) + "C"; - 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) { - /* + 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) + setLocal('auth_bypassed', false) + 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(); - }); + */ + lbryio.setAccessToken(installation_id); + setCurrentUser(); + }); } else { setCurrentUser(); } diff --git a/ui/scss/component/_form-field.scss b/ui/scss/component/_form-field.scss index 2d471f760..8fd86efef 100644 --- a/ui/scss/component/_form-field.scss +++ b/ui/scss/component/_form-field.scss @@ -3,6 +3,10 @@ $width-input-border: 2px; $width-input-text: 330px; +.form-input-width { + width: $width-input-text +} + .form-row-submit { margin-top: $spacing-vertical; From e0b8afe02827ffdec338489d88cb6e035e3c6399 Mon Sep 17 00:00:00 2001 From: Jeremy Kauffman Date: Thu, 1 Jun 2017 12:20:12 -0400 Subject: [PATCH 02/12] midway through auth rewrite --- ui/js/actions/app.js | 25 +- ui/js/actions/user.js | 21 ++ ui/js/component/authOverlay/index.jsx | 21 ++ ui/js/component/authOverlay/view.jsx | 350 ++++++++++++++++++++++++++ ui/js/constants/action_types.js | 5 + ui/js/lbryio.js | 9 +- ui/js/main.js | 10 +- ui/js/reducers/app.js | 6 - ui/js/reducers/user.js | 34 +++ ui/js/selectors/availability.js | 1 - ui/js/selectors/content.js | 1 - ui/js/selectors/user.js | 13 + ui/js/selectors/wallet.js | 14 -- ui/js/store.js | 22 +- 14 files changed, 480 insertions(+), 52 deletions(-) create mode 100644 ui/js/actions/user.js create mode 100644 ui/js/component/authOverlay/index.jsx create mode 100644 ui/js/component/authOverlay/view.jsx create mode 100644 ui/js/reducers/user.js create mode 100644 ui/js/selectors/user.js diff --git a/ui/js/actions/app.js b/ui/js/actions/app.js index 709d85ffb..1454f8d2c 100644 --- a/ui/js/actions/app.js +++ b/ui/js/actions/app.js @@ -9,7 +9,18 @@ import { selectCurrentPage, selectCurrentParams, } from "selectors/app"; -import { doSearch } from "actions/search"; +import { + doSearch, +} from 'actions/search' +import { + doFetchDaemonSettings +} from 'actions/settings' +import { + doAuthenticate +} from 'actions/user' +import { + doFileList +} from 'actions/file_info' const { remote, ipcRenderer, shell } = require("electron"); const path = require("path"); @@ -216,9 +227,15 @@ export function doAlertError(errorList) { } export function doDaemonReady() { - return { - type: types.DAEMON_READY, - }; + return function(dispatch, getState) { + dispatch({ + type: types.DAEMON_READY + }) + dispatch(doAuthenticate()) + dispatch(doChangePath('/discover')) + dispatch(doFetchDaemonSettings()) + dispatch(doFileList()) + } } export function doShowSnackBar(data) { diff --git a/ui/js/actions/user.js b/ui/js/actions/user.js new file mode 100644 index 000000000..d616c3ac4 --- /dev/null +++ b/ui/js/actions/user.js @@ -0,0 +1,21 @@ +import * as types from 'constants/action_types' +import lbryio from 'lbryio' + +export function doAuthenticate() { + return function(dispatch, getState) { + dispatch({ + type: types.AUTHENTICATION_STARTED, + }) + lbryio.authenticate().then((user) => { + dispatch({ + type: types.AUTHENTICATION_SUCCESS, + data: { user } + }) + }).catch((error) => { + dispatch({ + type: types.AUTHENTICATION_FAILURE, + data: { error } + }) + }) + } +} \ No newline at end of file diff --git a/ui/js/component/authOverlay/index.jsx b/ui/js/component/authOverlay/index.jsx new file mode 100644 index 000000000..7e08a5ef9 --- /dev/null +++ b/ui/js/component/authOverlay/index.jsx @@ -0,0 +1,21 @@ +import React from 'react' +import { + connect +} from 'react-redux' +import { + doStartUpgrade, + doCancelUpgrade, +} from 'actions/app' +import { + selectAuthenticationIsPending, +} from 'selectors/user' +import AuthOverlay from './view' + +const select = (state) => ({ + isPending: selectAuthenticationIsPending(state) +}) + +const perform = (dispatch) => ({ +}) + +export default connect(select, perform)(AuthOverlay) diff --git a/ui/js/component/authOverlay/view.jsx b/ui/js/component/authOverlay/view.jsx new file mode 100644 index 000000000..20025820a --- /dev/null +++ b/ui/js/component/authOverlay/view.jsx @@ -0,0 +1,350 @@ +import React from "react"; +import lbry from "lbry.js"; +import lbryio from "lbryio.js"; +import Modal from "component/modal.js"; +import ModalPage from "component/modal-page.js"; +import Link from "component/link" +import {BusyMessage} from "component/common" +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.Component { + constructor(props) { + super(props); + + this.state = { + rewardType: null, + email: '', + showNoEmailConfirm: false, + submitting: false + }; + } + + handleEmailChanged(event) { + this.setState({ + email: event.target.value, + }); + } + + onEmailSaved(email) { + this.props.setStage("confirm", { email: email }) + } + + onEmailSkipClick() { + this.setState({ showNoEmailConfirm: true }) + } + + onEmailSkipConfirm() { + setLocal('auth_bypassed', true); + this.props.setStage(null) + } + + handleSubmit(event) { + event.preventDefault(); + + this.setState({ + submitting: true, + }); + lbryio.call('user_email', 'new', {email: this.state.email}, 'post').then(() => { + this.onEmailSaved(this.state.email); + }, (error) => { + if (error.xhr && (error.xhr.status == 409 || error.message == "This email is already in use")) { + this.onEmailSaved(this.state.email); + return; + } else if (this._emailRow) { + this._emailRow.showError(error.message) + } + this.setState({ submitting: false }); + }); + } + + render() { + return ( +
+
{ this.handleSubmit(event) }}> + { this._emailRow = ref }} type="text" label="Email" placeholder="scrwvwls@lbry.io" + name="email" value={this.state.email} + onChange={(event) => { this.handleEmailChanged(event) }} /> +
+ { this.handleSubmit(event) }} /> +
+ { this.state.showNoEmailConfirm ? +
+

If you continue without an email, you will be ineligible to earn free LBC rewards, as well as unable to receive security related communications.

+ { this.onEmailSkipConfirm() }} label="Continue without email" /> +
+ : + { this.onEmailSkipClick() }} label="Do I have to?" /> + } + +
+ ); + } +} + +class ConfirmEmailStage extends React.Component { + constructor(props) { + super(props); + + this.state = { + rewardType: null, + code: '', + submitting: false, + errorMessage: null, + }; + } + + handleCodeChanged(event) { + this.setState({ + code: event.target.value, + }); + } + + handleSubmit(event) { + event.preventDefault(); + this.setState({ + submitting: true, + }); + + const onSubmitError = (error) => { + if (this._codeRow) { + this._codeRow.showError(error.message) + } + this.setState({ submitting: false }); + }; + + lbryio.call('user_email', 'confirm', {verification_token: this.state.code, email: this.props.email}, 'post').then((userEmail) => { + if (userEmail.is_verified) { + this.props.setStage("welcome") + } else { + onSubmitError(new Error("Your email is still not verified.")) //shouldn't happen? + } + }, onSubmitError); + } + + render() { + return ( +
+
{ this.handleSubmit(event) }}> + { this._codeRow = ref }} type="text" + name="code" placeholder="a94bXXXXXXXXXXXXXX" value={this.state.code} onChange={(event) => { this.handleCodeChanged(event) }} + helper="A verification code is required to access this version."/> +
+ { this.handleSubmit(event)}} /> +
+
+ No code? { this.props.setStage("nocode")}} label="Click here" />. +
+ +
+ ); + } +} + +class WelcomeStage extends React.Component { + static propTypes = { + endAuth: React.PropTypes.func, + } + + constructor(props) { + super(props); + + this.state = { + hasReward: false, + rewardAmount: null, + }; + } + + onRewardClaim(reward) { + this.setState({ + hasReward: true, + rewardAmount: reward.amount + }) + } + + render() { + return ( + !this.state.hasReward ? + +
+

Welcome to LBRY.

+

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

+

Up top, LBRY is similar to popular media sites.

+

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

+

Thank you for making content freedom possible! Here's a nickel, kid.

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

About Your Reward

+

You earned a reward of LBRY credits, or LBC.

+

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

+

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

+

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

+

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

+
+
+ ); + } +} + +const ErrorStage = (props) => { + return
+

An error was encountered that we cannot continue from.

+

At least we're earning the name beta.

+ { props.errorText ?

Message: {props.errorText}

: '' } + { window.location.reload() } } /> +
+} + +const PendingStage = (props) => { + return
+ +
+} + + +class CodeRequiredStage extends React.Component { + 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 ( +
+
+

Early access to LBRY is restricted as we build and scale the network.

+

There are two ways in.

+

Own LBRY Credits

+

If you own at least 1 LBC, you can get in right now.

+

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

+

Your balance is . To increase your balance, send credits to this address:

+

+

If you don't understand how to send credits, then...

+
+
+

Wait For A Code

+

If you provide your email, you'll automatically receive a notification when the system is open.

+

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

+
+
+ ); + } +} + + +export class AuthOverlay extends React.Component { + constructor(props) { + super(props); + + this._stages = { + pending: PendingStage, + error: ErrorStage, + nocode: CodeRequiredStage, + email: SubmitEmailStage, + confirm: ConfirmEmailStage, + welcome: WelcomeStage + } + } + + 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() { + let StageContent + const { + isPending, + } = this.props + + if (isPending) { + StageContent = PendingStage; + } else { + return null + StageContent = this._stages[this.state.stage]; + } + + if (!StageContent) { + return Unknown authentication step. + } +//setStage={(stage, stageProps) => { this.setStage(stage, stageProps) }} {...this.state.stageProps} + return ( + true || this.state.stage != "welcome" ? + +

LBRY Early Access

+ +
: + + ); + } +} + +export default AuthOverlay \ No newline at end of file diff --git a/ui/js/constants/action_types.js b/ui/js/constants/action_types.js index 9e6a168a4..42485e7aa 100644 --- a/ui/js/constants/action_types.js +++ b/ui/js/constants/action_types.js @@ -69,3 +69,8 @@ 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' diff --git a/ui/js/lbryio.js b/ui/js/lbryio.js index a55c3355c..19b3c9616 100644 --- a/ui/js/lbryio.js +++ b/ui/js/lbryio.js @@ -144,7 +144,7 @@ lbryio.authenticate = function() { lbryio._authenticationPromise = new Promise((resolve, reject) => { lbry.status().then((response) => { - let installation_id = response.installation_id.substring(0, response.installation_id.length - 2) + "C"; + let installation_id = response.installation_id.substring(0, response.installation_id.length - 6) + "C"; function setCurrentUser() { lbryio.call('user', 'me').then((data) => { @@ -152,12 +152,7 @@ lbryio.authenticate = function() { resolve(data) }).catch(function(err) { lbryio.setAccessToken(null); - if (!getSession('reloadedOnFailedAuth')) { - setSession('reloadedOnFailedAuth', true) - window.location.reload(); - } else { - reject(err); - } + reject(err); }) } diff --git a/ui/js/main.js b/ui/js/main.js index e0193987c..1a5d79398 100644 --- a/ui/js/main.js +++ b/ui/js/main.js @@ -2,7 +2,6 @@ import React from 'react'; import ReactDOM from 'react-dom'; 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'; @@ -10,8 +9,6 @@ import store from 'store.js'; import SplashScreen from 'component/splash.js'; import { AuthOverlay } from 'component/auth.js'; 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 { remote, ipcRenderer, shell } = require('electron'); @@ -69,12 +66,7 @@ const initialState = app.store.getState(); 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()); + app.store.dispatch(doDaemonReady()) ReactDOM.render( diff --git a/ui/js/reducers/app.js b/ui/js/reducers/app.js index 373f13c3c..d5c95bde5 100644 --- a/ui/js/reducers/app.js +++ b/ui/js/reducers/app.js @@ -81,12 +81,6 @@ reducers[types.UPGRADE_DOWNLOAD_PROGRESSED] = function(state, action) { }); }; -reducers[types.DAEMON_READY] = function(state, action) { - return Object.assign({}, state, { - daemonReady: true, - }); -}; - reducers[types.SHOW_SNACKBAR] = function(state, action) { const { message, linkText, linkTarget, isError } = action.data; const snackBar = Object.assign({}, state.snackBar); diff --git a/ui/js/reducers/user.js b/ui/js/reducers/user.js new file mode 100644 index 000000000..f5475dfcc --- /dev/null +++ b/ui/js/reducers/user.js @@ -0,0 +1,34 @@ +import * as types from 'constants/action_types' + +const reducers = {} + +const defaultState = { + authenticationIsPending: false, + user: undefined +} + +reducers[types.AUTHENTICATION_STARTED] = function(state, action) { + return Object.assign({}, state, { + authenticationIsPending: true + }) +} + +reducers[types.AUTHENTICATION_SUCCESS] = function(state, action) { + return Object.assign({}, state, { + authenticationIsPending: false, + user: action.data.user, + }) +} + +reducers[types.AUTHENTICATION_FAILURE] = function(state, action) { + return Object.assign({}, state, { + authenticationIsPending: false, + user: null, + }) +} + +export default function reducer(state = defaultState, action) { + const handler = reducers[action.type]; + if (handler) return handler(state, action); + return state; +} diff --git a/ui/js/selectors/availability.js b/ui/js/selectors/availability.js index df330fed0..1200dd904 100644 --- a/ui/js/selectors/availability.js +++ b/ui/js/selectors/availability.js @@ -1,5 +1,4 @@ import { createSelector } from "reselect"; -import { selectDaemonReady, selectCurrentPage } from "selectors/app"; const _selectState = state => state.availability; diff --git a/ui/js/selectors/content.js b/ui/js/selectors/content.js index 78d81ab99..75162a454 100644 --- a/ui/js/selectors/content.js +++ b/ui/js/selectors/content.js @@ -1,5 +1,4 @@ import { createSelector } from "reselect"; -import { selectDaemonReady, selectCurrentPage } from "selectors/app"; export const _selectState = state => state.content || {}; diff --git a/ui/js/selectors/user.js b/ui/js/selectors/user.js new file mode 100644 index 000000000..77871ab78 --- /dev/null +++ b/ui/js/selectors/user.js @@ -0,0 +1,13 @@ +import { createSelector } from 'reselect' + +export const _selectState = state => state.user || {} + +export const selectAuthenticationIsPending = createSelector( + _selectState, + (state) => state.authenticationIsPending +) + +export const selectAuthenticationIsFailed = createSelector( + _selectState, + (state) => state.user === null +) \ No newline at end of file diff --git a/ui/js/selectors/wallet.js b/ui/js/selectors/wallet.js index b74cd01da..1027bb6e7 100644 --- a/ui/js/selectors/wallet.js +++ b/ui/js/selectors/wallet.js @@ -50,20 +50,6 @@ export const selectGettingNewAddress = createSelector( state => state.gettingNewAddress ); -export const shouldCheckAddressIsMine = createSelector( - _selectState, - selectCurrentPage, - selectReceiveAddress, - selectDaemonReady, - (state, page, address, daemonReady) => { - if (!daemonReady) return false; - if (address === undefined) return false; - if (state.addressOwnershipChecked) return false; - - return true; - } -); - export const selectDraftTransaction = createSelector( _selectState, state => state.draftTransaction || {} diff --git a/ui/js/store.js b/ui/js/store.js index 112f19015..9bc70dc25 100644 --- a/ui/js/store.js +++ b/ui/js/store.js @@ -13,6 +13,7 @@ import rewardsReducer from 'reducers/rewards'; import searchReducer from 'reducers/search'; import settingsReducer from 'reducers/settings'; import walletReducer from 'reducers/wallet'; +import userReducer from 'reducers/user'; function isFunction(object) { return typeof object === 'function'; @@ -47,16 +48,17 @@ function enableBatching(reducer) { } const reducers = redux.combineReducers({ - app: appReducer, - availability: availabilityReducer, - claims: claimsReducer, - fileInfo: fileInfoReducer, - content: contentReducer, - costInfo: costInfoReducer, - rewards: rewardsReducer, - search: searchReducer, - settings: settingsReducer, - wallet: walletReducer + app: appReducer, + availability: availabilityReducer, + claims: claimsReducer, + fileInfo: fileInfoReducer, + content: contentReducer, + costInfo: costInfoReducer, + rewards: rewardsReducer, + search: searchReducer, + settings: settingsReducer, + wallet: walletReducer, + user: userReducer, }); const bulkThunk = createBulkThunkMiddleware(); From eb170b97208423d087a8514d930955246e9168c8 Mon Sep 17 00:00:00 2001 From: 6ea86b96 <6ea86b96@gmail.com> Date: Fri, 26 May 2017 12:53:32 +0400 Subject: [PATCH 03/12] Working on rewards refactor --- ui/js/actions/rewards.js | 30 ++++-- ui/js/component/auth/index.js | 27 +++++ ui/js/component/{auth.js => auth/view.jsx} | 35 +++--- ui/js/component/rewardLink/index.js | 27 +++++ ui/js/component/rewardLink/view.jsx | 120 +++++++++++++++++++++ ui/js/constants/action_types.js | 7 ++ ui/js/main.js | 2 +- ui/js/page/rewards/index.js | 19 ++++ ui/js/page/rewards/view.jsx | 54 ++++++++++ ui/js/reducers/rewards.js | 47 ++++++++ ui/js/selectors/rewards.js | 53 ++++++++- 11 files changed, 394 insertions(+), 27 deletions(-) create mode 100644 ui/js/component/auth/index.js rename ui/js/component/{auth.js => auth/view.jsx} (96%) create mode 100644 ui/js/component/rewardLink/index.js create mode 100644 ui/js/component/rewardLink/view.jsx create mode 100644 ui/js/page/rewards/index.js create mode 100644 ui/js/page/rewards/view.jsx diff --git a/ui/js/actions/rewards.js b/ui/js/actions/rewards.js index 4c12d9c20..277be8a80 100644 --- a/ui/js/actions/rewards.js +++ b/ui/js/actions/rewards.js @@ -20,16 +20,26 @@ export function doFetchRewards() { }; } -export function doClaimReward(rewardType) { +export function doClaimReward(reward) { return function(dispatch, getState) { + dispatch({ + type: types.CLAIM_REWARD_STARTED, + data: { reward } + }) try { - rewards.claimReward(rewards[rewardType]); - dispatch({ - type: types.REWARD_CLAIMED, - data: { - reward: rewards[rewardType], - }, - }); - } catch (err) {} - }; + const success = (a) => { + console.log(a) + dispatch({ + type: types.CLAIM_REWARD_COMPLETED, + data: { + a + } + }) + } + const failure = (a) => console.error(a) + rewards.claimReward(reward.reward_type).then(success, failure) + } catch(err) { + console.error(err) + } + } } diff --git a/ui/js/component/auth/index.js b/ui/js/component/auth/index.js new file mode 100644 index 000000000..5cddc68f6 --- /dev/null +++ b/ui/js/component/auth/index.js @@ -0,0 +1,27 @@ +import React from 'react' +import { + connect, +} from 'react-redux' +import { + selectFetchingRewards, + selectClaimedRewardsByType, + makeSelectRewardByType, +} from 'selectors/rewards' +import AuthOverlay from './view' + +const makeSelect = () => { + const selectRewardByType = makeSelectRewardByType() + + const select = (state) => ({ + fetchingRewards: selectFetchingRewards(state), + claimedRewardsByType: selectClaimedRewardsByType(state), + newUserReward: selectRewardByType(state, { reward_type: 'new_user' }), + }) + + return select +} + +const perform = (dispatch) => ({ +}) + +export default connect(makeSelect, perform)(AuthOverlay) diff --git a/ui/js/component/auth.js b/ui/js/component/auth/view.jsx similarity index 96% rename from ui/js/component/auth.js rename to ui/js/component/auth/view.jsx index fa4bef0d1..c8e29787a 100644 --- a/ui/js/component/auth.js +++ b/ui/js/component/auth/view.jsx @@ -8,7 +8,6 @@ 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.Component { constructor(props) { @@ -197,7 +196,7 @@ class WelcomeStage extends React.Component { super(props); this.state = { - hasReward: false, + hasReward: true, rewardAmount: null, }; } @@ -210,7 +209,18 @@ class WelcomeStage extends React.Component { } render() { - return !this.state.hasReward + const { + claimedRewardsByType, + fetchingRewards, + newUserReward, + } = this.props + + const hasReward = claimedRewardsByType.length > 0 + + if (fetchingRewards) return null + if (!newUserReward) return null + + return !hasReward ? { - userRewards.filter(function(reward) { - return ( - reward.reward_type == rewards.TYPE_NEW_USER && - reward.transaction_id - ); - }).length - ? this.setStage(null) - : this.setStage("welcome"); - }); - } - }) + const { + claimedRewardsByType, + } = this.props + claimedRewardsByType[rewards.TYPE_NEW_USER] ? this.setStage(null) : this.setStage("welcome") + }}) .catch(err => { this.setStage("error", { errorText: err.message }); document.dispatchEvent( @@ -510,3 +513,5 @@ export class AuthOverlay extends React.Component { />; } } + +export default AuthOverlay diff --git a/ui/js/component/rewardLink/index.js b/ui/js/component/rewardLink/index.js new file mode 100644 index 000000000..736fa3b30 --- /dev/null +++ b/ui/js/component/rewardLink/index.js @@ -0,0 +1,27 @@ +import React from 'react' +import { + connect, +} from 'react-redux' +import { + makeSelectHasClaimedReward, +} from 'selectors/rewards' +import { + doClaimReward, +} from 'actions/rewards' +import RewardLink from './view' + +const makeSelect = () => { + const selectHasClaimedReward = makeSelectHasClaimedReward() + + const select = (state, props) => ({ + claimed: selectHasClaimedReward(state, props) + }) + + return select +} + +const perform = (dispatch) => ({ + claimReward: (reward) => dispatch(doClaimReward(reward)), +}) + +export default connect(makeSelect, perform)(RewardLink) diff --git a/ui/js/component/rewardLink/view.jsx b/ui/js/component/rewardLink/view.jsx new file mode 100644 index 000000000..003f1758e --- /dev/null +++ b/ui/js/component/rewardLink/view.jsx @@ -0,0 +1,120 @@ +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' + +// class RewardLink extends React.Component { +// static propTypes = { +// type: React.PropTypes.string.isRequired, +// claimed: React.PropTypes.bool, +// onRewardClaim: React.PropTypes.func, +// onRewardFailure: React.PropTypes.func +// } + +// constructor(props) { +// super(props); + +// this.state = { +// claimable: true, +// pending: false, +// errorMessage: null +// }; +// } + +// refreshClaimable() { +// switch(this.props.type) { +// case 'new_user': +// this.setState({ claimable: true }); +// return; + +// case 'first_publish': +// lbry.claim_list_mine().then((list) => { +// this.setState({ +// claimable: list.length > 0 +// }) +// }); +// return; +// } +// } + +// componentWillMount() { +// this.refreshClaimable(); +// } + +// claimReward() { +// this.setState({ +// pending: true +// }) + +// rewards.claimReward(this.props.type).then((reward) => { +// this.setState({ +// pending: false, +// errorMessage: null +// }) +// if (this.props.onRewardClaim) { +// this.props.onRewardClaim(reward); +// } +// }).catch((error) => { +// this.setState({ +// errorMessage: error.message, +// pending: false +// }) +// }) +// } + +// clearError() { +// if (this.props.onRewardFailure) { +// this.props.onRewardFailure() +// } +// this.setState({ +// errorMessage: null +// }) +// } + +// render() { +// return ( +//
+// {this.props.claimed +// ? Reward claimed. +// : { this.claimReward() }} />} +// {this.state.errorMessage ? +// { this.clearError() }}> +// {this.state.errorMessage} +// +// : ''} +//
+// ); +// } +// } + + +const RewardLink = (props) => { + const { + reward, + claimed, + button, + pending, + claimable = true, + claimReward, + errorMessage, + clearError, + } = props + + return ( +
+ {claimed + ? Reward claimed. + : { claimReward(reward) }} />} + {errorMessage ? + { clearError() }}> + {errorMessage} + + : ''} +
+ ) +} +export default RewardLink \ No newline at end of file diff --git a/ui/js/constants/action_types.js b/ui/js/constants/action_types.js index 42485e7aa..460893f89 100644 --- a/ui/js/constants/action_types.js +++ b/ui/js/constants/action_types.js @@ -74,3 +74,10 @@ export const DAEMON_SETTINGS_RECEIVED = "DAEMON_SETTINGS_RECEIVED"; export const AUTHENTICATION_STARTED = 'AUTHENTICATION_STARTED' export const AUTHENTICATION_SUCCESS = 'AUTHENTICATION_SUCCESS' export const AUTHENTICATION_FAILURE = 'AUTHENTICATION_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_COMPLETED = 'CLAIM_REWARD_COMPLETED' + diff --git a/ui/js/main.js b/ui/js/main.js index 1a5d79398..178979e6b 100644 --- a/ui/js/main.js +++ b/ui/js/main.js @@ -7,7 +7,7 @@ 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 { toQueryString } from 'util/query_params'; diff --git a/ui/js/page/rewards/index.js b/ui/js/page/rewards/index.js new file mode 100644 index 000000000..2ef1e9363 --- /dev/null +++ b/ui/js/page/rewards/index.js @@ -0,0 +1,19 @@ +import React from 'react' +import { + connect, +} from 'react-redux' +import { + selectFetchingRewards, + selectRewards, +} from 'selectors/rewards' +import RewardsPage from './view' + +const select = (state) => ({ + fetching: selectFetchingRewards(state), + rewards: selectRewards(state), +}) + +const perform = (dispatch) => ({ +}) + +export default connect(select, perform)(RewardsPage) diff --git a/ui/js/page/rewards/view.jsx b/ui/js/page/rewards/view.jsx new file mode 100644 index 000000000..3f41b8fb5 --- /dev/null +++ b/ui/js/page/rewards/view.jsx @@ -0,0 +1,54 @@ +import React from 'react'; +import lbryio from 'lbryio'; +import {CreditAmount, Icon} from 'component/common'; +import SubHeader from 'component/subHeader' +import RewardLink from 'component/rewardLink'; + +const RewardTile = (props) => { + const { + reward, + } = props + + const claimed = !!reward.transaction_id + + return ( +
+
+
+ +

{reward.title}

+
+
+ {claimed + ? Reward claimed. + : } +
+
{reward.reward_description}
+
+
+ ) +} + +const RewardsPage = (props) => { + const { + fetching, + rewards, + } = props + + let content + + if (fetching) content =
Fetching rewards
+ if (!fetching && rewards.length == 0) content =
Failed to load rewards.
+ if (!fetching && rewards.length > 0) { + content = rewards.map(reward => ) + } + + return ( +
+ + {content} +
+ ) +} + +export default RewardsPage; diff --git a/ui/js/reducers/rewards.js b/ui/js/reducers/rewards.js index c9c3efc21..dcdaa7210 100644 --- a/ui/js/reducers/rewards.js +++ b/ui/js/reducers/rewards.js @@ -3,6 +3,53 @@ import * as types from "constants/action_types"; const reducers = {}; const defaultState = {}; +reducers[types.FETCH_REWARDS_STARTED] = function(state, action) { + const newRewards = Object.assign({}, state.rewards, { + fetching: true, + }) + + return Object.assign({}, state, newRewards) +} + +reducers[types.FETCH_REWARDS_COMPLETED] = function(state, action) { + const { + userRewards, + } = action.data + + const byRewardType = {} + userRewards.forEach(reward => byRewardType[reward.reward_type] = reward) + const newRewards = Object.assign({}, state.rewards, { + byRewardType: byRewardType, + fetching: false + }) + + return Object.assign({}, state, newRewards) +} + +reducers[types.CLAIM_REWARD_STARTED] = function(state, action) { + const { + reward, + } = action.data + + const newRewards = Object.assign({}, state, { + claiming: true, + }) + + return Object.assign({}, state, newRewards) +} + +reducers[types.CLAIM_REWARD_COMPLETED] = function(state, action) { + const { + reward, + } = action.data + + const newRewards = Object.assign({}, state, { + claiming: false, + }) + + return Object.assign({}, state, newRewards) +} + export default function reducer(state = defaultState, action) { const handler = reducers[action.type]; if (handler) return handler(state, action); diff --git a/ui/js/selectors/rewards.js b/ui/js/selectors/rewards.js index 481b1f184..9d62ba7c8 100644 --- a/ui/js/selectors/rewards.js +++ b/ui/js/selectors/rewards.js @@ -1,3 +1,54 @@ import { createSelector } from "reselect"; -export const _selectState = state => state.rewards || {}; +const _selectState = state => state.rewards || {}; + +export const selectRewardsByType = createSelector( + _selectState, + (state) => state.byRewardType || {} +) + +export const selectRewards = createSelector( + selectRewardsByType, + (byType) => Object.values(byType) || [] +) + +export const selectClaimedRewards = createSelector( + selectRewards, + (rewards) => rewards.filter(reward => reward.transaction_id !== "") +) + +export const selectClaimedRewardsByType = createSelector( + selectClaimedRewards, + (claimedRewards) => { + const byType = [] + claimedRewards.forEach(reward => byType[reward.reward_type] = reward) + return byType + } +) + +export const selectFetchingRewards = createSelector( + _selectState, + (state) => !!state.fetching +) + +export const selectHasClaimedReward = (state, props) => { + return !!selectClaimedRewardsByType[props.reward.reward_type] +} + +export const makeSelectHasClaimedReward = () => { + return createSelector( + selectHasClaimedReward, + (claimed) => claimed + ) +} + +const selectRewardByType = (state, props) => { + return selectRewardsByType(state)[props.reward_type] +} + +export const makeSelectRewardByType = () => { + return createSelector( + selectRewardByType, + (reward) => reward + ) +} From a17d19038e96c87249d9bfac43593bbbb994fa2c Mon Sep 17 00:00:00 2001 From: Jeremy Kauffman Date: Thu, 1 Jun 2017 20:51:52 -0400 Subject: [PATCH 04/12] good chunk of progress towards auth and rewards refactor / degating --- ui/js/actions/rewards.js | 44 +++++++---- ui/js/actions/user.js | 41 ++++++++++ ui/js/component/authOverlay/index.jsx | 12 ++- ui/js/component/authOverlay/view.jsx | 86 +++++++------------- ui/js/component/form.js | 5 +- ui/js/component/rewardLink/index.js | 10 ++- ui/js/component/rewardLink/view.jsx | 105 ++----------------------- ui/js/component/userEmailNew/index.jsx | 23 ++++++ ui/js/component/userEmailNew/view.jsx | 43 ++++++++++ ui/js/constants/action_types.js | 10 ++- ui/js/lbryio.js | 2 +- ui/js/page/rewards/index.js | 1 + ui/js/page/rewards/view.jsx | 2 +- ui/js/reducers/rewards.js | 49 +++++++++--- ui/js/reducers/user.js | 40 ++++++++++ ui/js/rewards.js | 7 +- ui/js/selectors/rewards.js | 41 +++++++++- ui/js/selectors/user.js | 19 ++++- 18 files changed, 344 insertions(+), 196 deletions(-) create mode 100644 ui/js/component/userEmailNew/index.jsx create mode 100644 ui/js/component/userEmailNew/view.jsx diff --git a/ui/js/actions/rewards.js b/ui/js/actions/rewards.js index 277be8a80..922e7976d 100644 --- a/ui/js/actions/rewards.js +++ b/ui/js/actions/rewards.js @@ -26,20 +26,36 @@ export function doClaimReward(reward) { type: types.CLAIM_REWARD_STARTED, data: { reward } }) - try { - const success = (a) => { - console.log(a) - dispatch({ - type: types.CLAIM_REWARD_COMPLETED, - data: { - a - } - }) - } - const failure = (a) => console.error(a) - rewards.claimReward(reward.reward_type).then(success, failure) - } catch(err) { - console.error(err) + + const success = (a) => { + console.log(a) + dispatch({ + type: types.CLAIM_REWARD_SUCCESS, + data: { + a + } + }) } + + const failure = (error) => { + dispatch({ + type: types.CLAIM_REWARD_FAILURE, + data: { + reward, + error + } + }) + } + + rewards.claimReward(reward.reward_type).then(success, failure) + } +} + +export function doClaimRewardClearError(reward) { + return function(dispatch, getState) { + dispatch({ + type: types.CLAIM_REWARD_CLEAR_ERROR, + data: { reward } + }) } } diff --git a/ui/js/actions/user.js b/ui/js/actions/user.js index d616c3ac4..da8c73c61 100644 --- a/ui/js/actions/user.js +++ b/ui/js/actions/user.js @@ -1,5 +1,11 @@ import * as types from 'constants/action_types' import lbryio from 'lbryio' +import { + setLocal +} from 'utils' +import { + doFetchRewards +} from 'actions/rewards' export function doAuthenticate() { return function(dispatch, getState) { @@ -18,4 +24,39 @@ export function doAuthenticate() { }) }) } +} + +export function doUserEmailNew(email) { + return function(dispatch, getState) { + dispatch({ + type: types.USER_EMAIL_NEW_STARTED, + }) + lbryio.call('user_email', 'new', { email }, 'post').then(() => { + dispatch({ + type: types.USER_EMAIL_NEW_SUCCESS, + data: { email } + }) + }, (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, + }) + } } \ No newline at end of file diff --git a/ui/js/component/authOverlay/index.jsx b/ui/js/component/authOverlay/index.jsx index 7e08a5ef9..c350bd771 100644 --- a/ui/js/component/authOverlay/index.jsx +++ b/ui/js/component/authOverlay/index.jsx @@ -3,19 +3,23 @@ import { connect } from 'react-redux' import { - doStartUpgrade, - doCancelUpgrade, -} from 'actions/app' + doUserEmailDecline +} from 'actions/user' import { selectAuthenticationIsPending, + selectEmailNewDeclined, + selectUser, } from 'selectors/user' import AuthOverlay from './view' const select = (state) => ({ - isPending: selectAuthenticationIsPending(state) + isPending: selectAuthenticationIsPending(state), + isEmailDeclined: selectEmailNewDeclined(state), + user: selectUser(state), }) const perform = (dispatch) => ({ + userEmailDecline: () => dispatch(doUserEmailDecline()) }) export default connect(select, perform)(AuthOverlay) diff --git a/ui/js/component/authOverlay/view.jsx b/ui/js/component/authOverlay/view.jsx index 20025820a..1356892fe 100644 --- a/ui/js/component/authOverlay/view.jsx +++ b/ui/js/component/authOverlay/view.jsx @@ -5,73 +5,34 @@ import Modal from "component/modal.js"; import ModalPage from "component/modal-page.js"; import Link from "component/link" import {BusyMessage} from "component/common" -import {RewardLink} from 'component/reward-link'; +import {RewardLink} from 'component/rewardLink'; +import UserEmailNew from 'component/userEmailNew'; 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.Component { +class EmailStage extends React.Component { constructor(props) { super(props); this.state = { - rewardType: null, - email: '', showNoEmailConfirm: false, - submitting: false }; } - handleEmailChanged(event) { - this.setState({ - email: event.target.value, - }); - } - - onEmailSaved(email) { - this.props.setStage("confirm", { email: email }) - } - onEmailSkipClick() { this.setState({ showNoEmailConfirm: true }) } onEmailSkipConfirm() { - setLocal('auth_bypassed', true); - this.props.setStage(null) - } - - 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 }); - }); + this.props.userEmailDecline() } render() { return (
-
{ this.handleSubmit(event) }}> - { this._emailRow = ref }} type="text" label="Email" placeholder="scrwvwls@lbry.io" - name="email" value={this.state.email} - onChange={(event) => { this.handleEmailChanged(event) }} /> -
- { this.handleSubmit(event) }} /> -
+ +
{ this.state.showNoEmailConfirm ?

If you continue without an email, you will be ineligible to earn free LBC rewards, as well as unable to receive security related communications.

@@ -80,7 +41,7 @@ class SubmitEmailStage extends React.Component { : { this.onEmailSkipClick() }} label="Do I have to?" /> } - +
); } @@ -91,7 +52,6 @@ class ConfirmEmailStage extends React.Component { super(props); this.state = { - rewardType: null, code: '', submitting: false, errorMessage: null, @@ -275,10 +235,8 @@ export class AuthOverlay extends React.Component { super(props); this._stages = { - pending: PendingStage, error: ErrorStage, nocode: CodeRequiredStage, - email: SubmitEmailStage, confirm: ConfirmEmailStage, welcome: WelcomeStage } @@ -320,21 +278,35 @@ export class AuthOverlay extends React.Component { } render() { - let StageContent + let stageContent + const { isPending, + isEmailDeclined, + user, + userEmailDecline } = this.props - if (isPending) { - StageContent = PendingStage; - } else { + console.log('auth overlay render') + console.log(user) + + if (isEmailDeclined) { return null - StageContent = this._stages[this.state.stage]; + } else if (isPending) { + stageContent = ; + } else if (!user.has_email) { + stageContent = ; + } + else { + return null + //StageContent = this._stages[this.state.stage]; } - if (!StageContent) { - return Unknown authentication step. - } + return +

LBRY Early Access

+ {stageContent} +
; + //setStage={(stage, stageProps) => { this.setStage(stage, stageProps) }} {...this.state.stageProps} return ( true || this.state.stage != "welcome" ? diff --git a/ui/js/component/form.js b/ui/js/component/form.js index 809f79e7b..a9c1ab6bd 100644 --- a/ui/js/component/form.js +++ b/ui/js/component/form.js @@ -179,8 +179,8 @@ export class FormRow extends React.Component { this._fieldRequiredText = __("This field is required"); this.state = { - isError: false, - errorMessage: null, + isError: !!props.errorMessage, + errorMessage: typeof props.errorMessage === "string" ? props.errorMessage : '', }; } @@ -225,6 +225,7 @@ export class FormRow extends React.Component { delete fieldProps.label; } delete fieldProps.helper; + delete fieldProps.errorMessage; return (
diff --git a/ui/js/component/rewardLink/index.js b/ui/js/component/rewardLink/index.js index 736fa3b30..883d5bdde 100644 --- a/ui/js/component/rewardLink/index.js +++ b/ui/js/component/rewardLink/index.js @@ -4,17 +4,24 @@ import { } from 'react-redux' import { makeSelectHasClaimedReward, + makeSelectClaimRewardError, + makeSelectIsRewardClaimPending } from 'selectors/rewards' import { doClaimReward, + doClaimRewardClearError } from 'actions/rewards' import RewardLink from './view' const makeSelect = () => { const selectHasClaimedReward = makeSelectHasClaimedReward() + const selectIsPending = makeSelectIsRewardClaimPending() + const selectError = makeSelectClaimRewardError() const select = (state, props) => ({ - claimed: selectHasClaimedReward(state, props) + isClaimed: selectHasClaimedReward(state, props), + errorMessage: selectError(state, props), + isPending: selectIsPending(state, props) }) return select @@ -22,6 +29,7 @@ const makeSelect = () => { const perform = (dispatch) => ({ claimReward: (reward) => dispatch(doClaimReward(reward)), + clearError: (reward) => dispatch(doClaimRewardClearError(reward)) }) export default connect(makeSelect, perform)(RewardLink) diff --git a/ui/js/component/rewardLink/view.jsx b/ui/js/component/rewardLink/view.jsx index 003f1758e..f6e2d1923 100644 --- a/ui/js/component/rewardLink/view.jsx +++ b/ui/js/component/rewardLink/view.jsx @@ -1,116 +1,27 @@ 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' -// class RewardLink extends React.Component { -// static propTypes = { -// type: React.PropTypes.string.isRequired, -// claimed: React.PropTypes.bool, -// onRewardClaim: React.PropTypes.func, -// onRewardFailure: React.PropTypes.func -// } - -// constructor(props) { -// super(props); - -// this.state = { -// claimable: true, -// pending: false, -// errorMessage: null -// }; -// } - -// refreshClaimable() { -// switch(this.props.type) { -// case 'new_user': -// this.setState({ claimable: true }); -// return; - -// case 'first_publish': -// lbry.claim_list_mine().then((list) => { -// this.setState({ -// claimable: list.length > 0 -// }) -// }); -// return; -// } -// } - -// componentWillMount() { -// this.refreshClaimable(); -// } - -// claimReward() { -// this.setState({ -// pending: true -// }) - -// rewards.claimReward(this.props.type).then((reward) => { -// this.setState({ -// pending: false, -// errorMessage: null -// }) -// if (this.props.onRewardClaim) { -// this.props.onRewardClaim(reward); -// } -// }).catch((error) => { -// this.setState({ -// errorMessage: error.message, -// pending: false -// }) -// }) -// } - -// clearError() { -// if (this.props.onRewardFailure) { -// this.props.onRewardFailure() -// } -// this.setState({ -// errorMessage: null -// }) -// } - -// render() { -// return ( -//
-// {this.props.claimed -// ? Reward claimed. -// : { this.claimReward() }} />} -// {this.state.errorMessage ? -// { this.clearError() }}> -// {this.state.errorMessage} -// -// : ''} -//
-// ); -// } -// } - - const RewardLink = (props) => { const { reward, - claimed, button, - pending, - claimable = true, claimReward, - errorMessage, clearError, + errorMessage, + isClaimed, + isPending } = props - + console.log(props) return (
- {claimed + {isClaimed ? Reward claimed. - : { claimReward(reward) }} />} + : { claimReward(reward) }} />} {errorMessage ? - { clearError() }}> + { clearError(reward) }}> {errorMessage} : ''} diff --git a/ui/js/component/userEmailNew/index.jsx b/ui/js/component/userEmailNew/index.jsx new file mode 100644 index 000000000..0b5e93d3e --- /dev/null +++ b/ui/js/component/userEmailNew/index.jsx @@ -0,0 +1,23 @@ +import React from 'react' +import { + connect +} from 'react-redux' +import { + doUserEmailNew +} from 'actions/user' +import { + selectEmailNewIsPending, + selectEmailNewErrorMessage, +} from 'selectors/user' +import UserEmailNew from './view' + +const select = (state) => ({ + isPending: selectEmailNewIsPending(state), + errorMessage: selectEmailNewErrorMessage(state), +}) + +const perform = (dispatch) => ({ + addUserEmail: (email) => dispatch(doUserEmailNew(email)) +}) + +export default connect(select, perform)(UserEmailNew) diff --git a/ui/js/component/userEmailNew/view.jsx b/ui/js/component/userEmailNew/view.jsx new file mode 100644 index 000000000..502ceaa70 --- /dev/null +++ b/ui/js/component/userEmailNew/view.jsx @@ -0,0 +1,43 @@ +import React from 'react'; +import Link from 'component/link'; +import {FormRow} from 'component/form.js'; + +class UserEmailNew extends React.Component { + constructor(props) { + super(props); + + this.state = { + email: '' + }; + } + + handleEmailChanged(event) { + this.setState({ + email: event.target.value, + }); + } + + handleSubmit(event) { + event.preventDefault(); + this.props.addUserEmail(this.state.email) + } + + render() { + const { + errorMessage, + isPending + } = this.props + + return
{ this.handleSubmit(event) }}> + { this.handleEmailChanged(event) }} /> +
+ { this.handleSubmit(event) }} /> +
+ + } +} + +export default UserEmailNew \ No newline at end of file diff --git a/ui/js/constants/action_types.js b/ui/js/constants/action_types.js index 460893f89..8bcabc328 100644 --- a/ui/js/constants/action_types.js +++ b/ui/js/constants/action_types.js @@ -74,10 +74,16 @@ export const DAEMON_SETTINGS_RECEIVED = "DAEMON_SETTINGS_RECEIVED"; 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' // 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_COMPLETED = 'CLAIM_REWARD_COMPLETED' - +export const CLAIM_REWARD_SUCCESS = 'CLAIM_REWARD_SUCCESS' +export const CLAIM_REWARD_FAILURE = 'CLAIM_REWARD_FAILURE' +export const CLAIM_REWARD_CLEAR_ERROR = 'CLAIM_REWARD_CLEAR_ERROR' diff --git a/ui/js/lbryio.js b/ui/js/lbryio.js index 19b3c9616..4fa78b94a 100644 --- a/ui/js/lbryio.js +++ b/ui/js/lbryio.js @@ -144,7 +144,7 @@ lbryio.authenticate = function() { lbryio._authenticationPromise = new Promise((resolve, reject) => { lbry.status().then((response) => { - let installation_id = response.installation_id.substring(0, response.installation_id.length - 6) + "C"; + let installation_id = response.installation_id.substring(0, response.installation_id.length - 2) + "D"; function setCurrentUser() { lbryio.call('user', 'me').then((data) => { diff --git a/ui/js/page/rewards/index.js b/ui/js/page/rewards/index.js index 2ef1e9363..5e7e0aa5c 100644 --- a/ui/js/page/rewards/index.js +++ b/ui/js/page/rewards/index.js @@ -4,6 +4,7 @@ import { } from 'react-redux' import { selectFetchingRewards, + selectIsRewardEligible, selectRewards, } from 'selectors/rewards' import RewardsPage from './view' diff --git a/ui/js/page/rewards/view.jsx b/ui/js/page/rewards/view.jsx index 3f41b8fb5..1071c4aab 100644 --- a/ui/js/page/rewards/view.jsx +++ b/ui/js/page/rewards/view.jsx @@ -16,7 +16,7 @@ const RewardTile = (props) => {
-

{reward.title}

+

{reward.reward_title}

{claimed diff --git a/ui/js/reducers/rewards.js b/ui/js/reducers/rewards.js index dcdaa7210..51d2386a1 100644 --- a/ui/js/reducers/rewards.js +++ b/ui/js/reducers/rewards.js @@ -1,7 +1,11 @@ import * as types from "constants/action_types"; -const reducers = {}; -const defaultState = {}; +const reducers = {} +const defaultState = { + fetching: false, + claimPendingByType: {}, + claimErrorsByType: {} +}; reducers[types.FETCH_REWARDS_STARTED] = function(state, action) { const newRewards = Object.assign({}, state.rewards, { @@ -26,28 +30,49 @@ reducers[types.FETCH_REWARDS_COMPLETED] = function(state, action) { return Object.assign({}, state, newRewards) } +function setClaimRewardState(state, reward, isClaiming, errorMessage="") { + const newClaimPendingByType = Object.assign({}, state.claimPendingByType) + const newClaimErrorsByType = Object.assign({}, state.claimErrorsByType) + newClaimPendingByType[reward.reward_type] = isClaiming + newClaimErrorsByType[reward.reward_type] = errorMessage + + return Object.assign({}, state, { + claimPendingByType: newClaimPendingByType, + claimErrorsByType: newClaimErrorsByType, + }) +} + reducers[types.CLAIM_REWARD_STARTED] = function(state, action) { const { reward, } = action.data - const newRewards = Object.assign({}, state, { - claiming: true, - }) - - return Object.assign({}, state, newRewards) + return setClaimRewardState(state, reward, true, "") } -reducers[types.CLAIM_REWARD_COMPLETED] = function(state, action) { +reducers[types.CLAIM_REWARD_SUCCESS] = function(state, action) { const { reward, } = action.data - const newRewards = Object.assign({}, state, { - claiming: false, - }) + return setClaimRewardState(state, reward, false, "") +} - return Object.assign({}, state, newRewards) +reducers[types.CLAIM_REWARD_FAILURE] = function(state, action) { + const { + reward, + error + } = action.data + + return setClaimRewardState(state, reward, false, 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) { diff --git a/ui/js/reducers/user.js b/ui/js/reducers/user.js index f5475dfcc..40b2c1627 100644 --- a/ui/js/reducers/user.js +++ b/ui/js/reducers/user.js @@ -1,9 +1,15 @@ import * as types from 'constants/action_types' +import { + getLocal +} from 'utils' const reducers = {} const defaultState = { authenticationIsPending: false, + emailNewIsPending: false, + emailNewErrorMessage: '', + emailNewDeclined: getLocal('user_email_declined', false), user: undefined } @@ -27,6 +33,40 @@ reducers[types.AUTHENTICATION_FAILURE] = function(state, action) { }) } +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) { + return Object.assign({}, state, { + emailNewIsPending: false, + }) +} + +reducers[types.USER_EMAIL_NEW_EXISTS] = function(state, action) { + return Object.assign({}, state, { + emailNewIsPending: false, + }) +} + +reducers[types.USER_EMAIL_NEW_FAILURE] = function(state, action) { + return Object.assign({}, state, { + emailNewIsPending: false, + emailNewErrorMessage: action.data.error + }) +} + + + export default function reducer(state = defaultState, action) { const handler = reducers[action.type]; if (handler) return handler(state, action); diff --git a/ui/js/rewards.js b/ui/js/rewards.js index 25f6dbe82..33d5547bc 100644 --- a/ui/js/rewards.js +++ b/ui/js/rewards.js @@ -93,7 +93,7 @@ rewards.TYPE_FEATURED_DOWNLOAD = 'featured_download'; rewards.claimReward = function(type) { function requestReward(resolve, reject, params) { - if (!lbryio.enabled) { + if (!lbryio.enabled || !lbryio.getAccessToken()) { reject(new Error(__('Rewards are not enabled.'))); return; } @@ -193,7 +193,10 @@ rewards.claimReward = function(type) { }); }; -rewards.claimEligiblePurchaseRewards = function() { +rewards.claimEligiblePurchaseRewards = () => { + if (!lbryio.enabled || !lbryio.getAccessToken()) { + return; + } let types = {}; types[rewards.TYPE_FIRST_STREAM] = false; types[rewards.TYPE_FEATURED_DOWNLOAD] = false; diff --git a/ui/js/selectors/rewards.js b/ui/js/selectors/rewards.js index 9d62ba7c8..3b4af4e83 100644 --- a/ui/js/selectors/rewards.js +++ b/ui/js/selectors/rewards.js @@ -1,4 +1,5 @@ import { createSelector } from "reselect"; +import { selectUser } from "selectors/user"; const _selectState = state => state.rewards || {}; @@ -12,6 +13,11 @@ export const selectRewards = createSelector( (byType) => Object.values(byType) || [] ) +export const selectIsRewardEligible = createSelector( + selectUser, + (user) => user.can_claim_rewards +) + export const selectClaimedRewards = createSelector( selectRewards, (rewards) => rewards.filter(reward => reward.transaction_id !== "") @@ -20,7 +26,7 @@ export const selectClaimedRewards = createSelector( export const selectClaimedRewardsByType = createSelector( selectClaimedRewards, (claimedRewards) => { - const byType = [] + const byType = {} claimedRewards.forEach(reward => byType[reward.reward_type] = reward) return byType } @@ -42,6 +48,39 @@ export const makeSelectHasClaimedReward = () => { ) } +export const selectClaimsPendingByType = createSelector( + _selectState, + (state) => state.claimPendingByType +) + +const selectIsClaimRewardPending = (state, props) => { + return selectClaimsPendingByType(state, props)[props.reward.reward_type] +} + +export const makeSelectIsRewardClaimPending = () => { + return createSelector( + selectIsClaimRewardPending, + (isClaiming) => isClaiming + ) +} + +export const selectClaimErrorsByType = createSelector( + _selectState, + (state) => state.claimErrorsByType +) + +const selectClaimRewardError = (state, props) => { + console.log(selectClaimErrorsByType(state, props)); + return selectClaimErrorsByType(state, props)[props.reward.reward_type] +} + +export const makeSelectClaimRewardError = () => { + return createSelector( + selectClaimRewardError, + (errorMessage) => errorMessage + ) +} + const selectRewardByType = (state, props) => { return selectRewardsByType(state)[props.reward_type] } diff --git a/ui/js/selectors/user.js b/ui/js/selectors/user.js index 77871ab78..5374fa3fe 100644 --- a/ui/js/selectors/user.js +++ b/ui/js/selectors/user.js @@ -7,7 +7,22 @@ export const selectAuthenticationIsPending = createSelector( (state) => state.authenticationIsPending ) -export const selectAuthenticationIsFailed = createSelector( +export const selectUser = createSelector( _selectState, - (state) => state.user === null + (state) => state.user +) + +export const selectEmailNewIsPending = createSelector( + _selectState, + (state) => state.emailNewIsPending +) + +export const selectEmailNewErrorMessage = createSelector( + _selectState, + (state) => state.emailNewErrorMessage +) + +export const selectEmailNewDeclined = createSelector( + _selectState, + (state) => state.emailNewDeclined ) \ No newline at end of file From b6881c829ca1c90649ba7826b2b147e383d147de Mon Sep 17 00:00:00 2001 From: Jeremy Kauffman Date: Fri, 2 Jun 2017 11:45:37 -0400 Subject: [PATCH 05/12] uncommitted work from last night --- ui/js/component/rewardLink/index.js | 10 ++++++++-- ui/js/component/rewardLink/view.jsx | 3 ++- ui/js/reducers/rewards.js | 12 ++++++++++-- ui/js/selectors/rewards.js | 1 - 4 files changed, 20 insertions(+), 6 deletions(-) diff --git a/ui/js/component/rewardLink/index.js b/ui/js/component/rewardLink/index.js index 883d5bdde..17446bac6 100644 --- a/ui/js/component/rewardLink/index.js +++ b/ui/js/component/rewardLink/index.js @@ -5,8 +5,12 @@ import { import { makeSelectHasClaimedReward, makeSelectClaimRewardError, - makeSelectIsRewardClaimPending + makeSelectIsRewardClaimPending, + selectIsRewardEligible, } from 'selectors/rewards' +import { + doNavigate +} from 'actions/app' import { doClaimReward, doClaimRewardClearError @@ -20,6 +24,7 @@ const makeSelect = () => { const select = (state, props) => ({ isClaimed: selectHasClaimedReward(state, props), + isEligible: selectIsRewardEligible(state), errorMessage: selectError(state, props), isPending: selectIsPending(state, props) }) @@ -29,7 +34,8 @@ const makeSelect = () => { const perform = (dispatch) => ({ claimReward: (reward) => dispatch(doClaimReward(reward)), - clearError: (reward) => dispatch(doClaimRewardClearError(reward)) + clearError: (reward) => dispatch(doClaimRewardClearError(reward)), + navigate: (path) => dispatch(doNavigate(path)), }) export default connect(makeSelect, perform)(RewardLink) diff --git a/ui/js/component/rewardLink/view.jsx b/ui/js/component/rewardLink/view.jsx index f6e2d1923..e513ccb74 100644 --- a/ui/js/component/rewardLink/view.jsx +++ b/ui/js/component/rewardLink/view.jsx @@ -11,9 +11,10 @@ const RewardLink = (props) => { clearError, errorMessage, isClaimed, + isEligible, isPending } = props - console.log(props) + return (
{isClaimed diff --git a/ui/js/reducers/rewards.js b/ui/js/reducers/rewards.js index 51d2386a1..dd56df320 100644 --- a/ui/js/reducers/rewards.js +++ b/ui/js/reducers/rewards.js @@ -33,8 +33,16 @@ reducers[types.FETCH_REWARDS_COMPLETED] = function(state, action) { function setClaimRewardState(state, reward, isClaiming, errorMessage="") { const newClaimPendingByType = Object.assign({}, state.claimPendingByType) const newClaimErrorsByType = Object.assign({}, state.claimErrorsByType) - newClaimPendingByType[reward.reward_type] = isClaiming - newClaimErrorsByType[reward.reward_type] = errorMessage + 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, diff --git a/ui/js/selectors/rewards.js b/ui/js/selectors/rewards.js index 3b4af4e83..828be4fe0 100644 --- a/ui/js/selectors/rewards.js +++ b/ui/js/selectors/rewards.js @@ -70,7 +70,6 @@ export const selectClaimErrorsByType = createSelector( ) const selectClaimRewardError = (state, props) => { - console.log(selectClaimErrorsByType(state, props)); return selectClaimErrorsByType(state, props)[props.reward.reward_type] } From 4f659572875d0f70025ce7d6effa83814484e85e Mon Sep 17 00:00:00 2001 From: Jeremy Kauffman Date: Fri, 2 Jun 2017 19:09:52 -0400 Subject: [PATCH 06/12] not enough progress --- ui/js/actions/app.js | 3 + ui/js/actions/rewards.js | 13 +- ui/js/actions/user.js | 35 +- ui/js/component/auth/index.js | 35 +- ui/js/component/auth/view.jsx | 528 +--------------------- ui/js/component/authOverlay/view.jsx | 431 ++++++------------ ui/js/component/router/view.jsx | 2 + ui/js/component/userEmailVerify/index.jsx | 25 + ui/js/component/userEmailVerify/view.jsx | 47 ++ ui/js/constants/action_types.js | 3 + ui/js/lbryio.js | 2 +- ui/js/page/auth/index.jsx | 25 + ui/js/page/auth/view.jsx | 19 + ui/js/page/rewards/index.js | 8 + ui/js/page/rewards/view.jsx | 18 +- ui/js/reducers/user.js | 20 + ui/js/selectors/app.js | 2 + ui/js/selectors/search.js | 2 + ui/js/selectors/user.js | 22 +- 19 files changed, 414 insertions(+), 826 deletions(-) create mode 100644 ui/js/component/userEmailVerify/index.jsx create mode 100644 ui/js/component/userEmailVerify/view.jsx create mode 100644 ui/js/page/auth/index.jsx create mode 100644 ui/js/page/auth/view.jsx diff --git a/ui/js/actions/app.js b/ui/js/actions/app.js index 1454f8d2c..63c6de0f6 100644 --- a/ui/js/actions/app.js +++ b/ui/js/actions/app.js @@ -18,6 +18,9 @@ import { import { doAuthenticate } from 'actions/user' +import { + doRewardList +} from 'actions/rewards' import { doFileList } from 'actions/file_info' diff --git a/ui/js/actions/rewards.js b/ui/js/actions/rewards.js index 922e7976d..ac368483d 100644 --- a/ui/js/actions/rewards.js +++ b/ui/js/actions/rewards.js @@ -3,7 +3,7 @@ import lbry from "lbry"; import lbryio from "lbryio"; import rewards from "rewards"; -export function doFetchRewards() { +export function doRewardList() { return function(dispatch, getState) { const state = getState(); @@ -11,11 +11,16 @@ export function doFetchRewards() { type: types.FETCH_REWARDS_STARTED, }); - lbryio.call("reward", "list", {}).then(function(userRewards) { + lbryio.call('reward', 'list', {}).then((userRewards) => { dispatch({ type: types.FETCH_REWARDS_COMPLETED, - data: { userRewards }, - }); + data: { userRewards } + }) + }).catch(() => { + dispatch({ + type: types.FETCH_REWARDS_COMPLETED, + data: { userRewards: [] } + }) }); }; } diff --git a/ui/js/actions/user.js b/ui/js/actions/user.js index da8c73c61..a55fb8092 100644 --- a/ui/js/actions/user.js +++ b/ui/js/actions/user.js @@ -4,7 +4,7 @@ import { setLocal } from 'utils' import { - doFetchRewards + doRewardList } from 'actions/rewards' export function doAuthenticate() { @@ -17,7 +17,12 @@ export function doAuthenticate() { type: types.AUTHENTICATION_SUCCESS, data: { user } }) + + dispatch(doRewardList()) //FIXME - where should this happen? + }).catch((error) => { + console.log('auth error') + console.log(error) dispatch({ type: types.AUTHENTICATION_FAILURE, data: { error } @@ -30,6 +35,7 @@ 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({ @@ -59,4 +65,31 @@ export function doUserEmailDecline() { type: types.USER_EMAIL_DECLINE, }) } +} + +export function doUserEmailVerify(email, verificationToken) { + return function(dispatch, getState) { + dispatch({ + type: types.USER_EMAIL_VERIFY_STARTED, + code: code + }) + + 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 } + }) + } else { + failure(new Error("Your email is still not verified.")) //shouldn't happen? + } + }, failure); + } } \ No newline at end of file diff --git a/ui/js/component/auth/index.js b/ui/js/component/auth/index.js index 5cddc68f6..6dd09c335 100644 --- a/ui/js/component/auth/index.js +++ b/ui/js/component/auth/index.js @@ -1,27 +1,22 @@ import React from 'react' import { - connect, + connect } from 'react-redux' import { - selectFetchingRewards, - selectClaimedRewardsByType, - makeSelectRewardByType, -} from 'selectors/rewards' -import AuthOverlay from './view' + doUserEmailDecline +} from 'actions/user' +import { + selectAuthenticationIsPending, + selectEmailNewDeclined, + selectEmailNewExistingEmail, + selectUser, +} from 'selectors/user' +import Auth from './view' -const makeSelect = () => { - const selectRewardByType = makeSelectRewardByType() - - const select = (state) => ({ - fetchingRewards: selectFetchingRewards(state), - claimedRewardsByType: selectClaimedRewardsByType(state), - newUserReward: selectRewardByType(state, { reward_type: 'new_user' }), - }) - - return select -} - -const perform = (dispatch) => ({ +const select = (state) => ({ + isPending: selectAuthenticationIsPending(state), + existingEmail: selectEmailNewExistingEmail(state), + user: selectUser(state), }) -export default connect(makeSelect, perform)(AuthOverlay) +export default connect(select, null)(Auth) diff --git a/ui/js/component/auth/view.jsx b/ui/js/component/auth/view.jsx index c8e29787a..c6932bf5d 100644 --- a/ui/js/component/auth/view.jsx +++ b/ui/js/component/auth/view.jsx @@ -1,517 +1,29 @@ -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"; - -class SubmitEmailStage extends React.Component { - constructor(props) { - super(props); - - this.state = { - rewardType: null, - email: '', - showNoEmailConfirm: false, - submitting: false - }; - } - - handleEmailChanged(event) { - this.setState({ - email: event.target.value, - }); - } - - onEmailSaved(email) { - this.props.setStage("confirm", { email: email }); - } - - onEmailSkipClick() { - this.setState({ showNoEmailConfirm: true }) - } - - onEmailSkipConfirm() { - setLocal('auth_bypassed', true); - this.props.setStage(null) - } - - handleSubmit(event) { - event.preventDefault(); - - this.setState({ - submitting: true, - }); - lbryio.call("user_email", "new", { email: this.state.email }, "post").then( - () => { - this.onEmailSaved(this.state.email); - }, - error => { - if ( - error.xhr && - (error.xhr.status == 409 || - error.message == __("This email is already in use")) - ) { - this.onEmailSaved(this.state.email); - return; - } else if (this._emailRow) { - this._emailRow.showError(error.message); - } - this.setState({ submitting: false }); - } - ); - } - - render() { - return ( -
-
{ this.handleSubmit(event) }}> - { this._emailRow = ref }} type="text" label={__("Email")} placeholder="scrwvwls@lbry.io" - name="email" value={this.state.email} - onChange={(event) => { this.handleEmailChanged(event) }} /> -
- { this.handleSubmit(event) }} /> -
- { this.state.showNoEmailConfirm ? -
-

If you continue without an email, you will be ineligible to earn free LBC rewards, as well as unable to receive security related communications.

- { this.onEmailSkipConfirm() }} label="Continue without email" /> -
- : - { this.onEmailSkipClick() }} label="Do I have to?" /> - } - -
- ); - } -} - -class ConfirmEmailStage extends React.Component { - constructor(props) { - super(props); - - this.state = { - rewardType: null, - code: "", - submitting: false, - errorMessage: null, - }; - } - - handleCodeChanged(event) { - this.setState({ - code: event.target.value, - }); - } - - handleSubmit(event) { - event.preventDefault(); - this.setState({ - submitting: true, - }); - - const onSubmitError = error => { - if (this._codeRow) { - this._codeRow.showError(error.message); - } - this.setState({ submitting: false }); - }; - - lbryio - .call( - "user_email", - "confirm", - { verification_token: this.state.code, email: this.props.email }, - "post" - ) - .then(userEmail => { - if (userEmail.is_verified) { - this.props.setStage("welcome"); - } else { - onSubmitError(new Error(__("Your email is still not verified."))); //shouldn't happen? - } - }, onSubmitError); - } - - render() { - return ( -
-
{ - this.handleSubmit(event); - }} - > - { - this._codeRow = ref; - }} - type="text" - name="code" - placeholder="a94bXXXXXXXXXXXXXX" - value={this.state.code} - onChange={event => { - this.handleCodeChanged(event); - }} - helper={__( - "A verification code is required to access this version." - )} - /> -
- { - this.handleSubmit(event); - }} - /> -
-
- {__("No code?")} - {" "} - { - this.props.setStage("nocode"); - }} - label={__("Click here")} - />. -
- -
- ); - } -} - -class WelcomeStage extends React.Component { - static propTypes = { - endAuth: React.PropTypes.func, - }; - - constructor(props) { - super(props); - - this.state = { - hasReward: true, - rewardAmount: null, - }; - } - - onRewardClaim(reward) { - this.setState({ - hasReward: true, - rewardAmount: reward.amount, - }); - } +import React from 'react' +import {BusyMessage} from 'component/common' +import UserEmailNew from 'component/userEmailNew' +import UserEmailVerify from 'component/userEmailVerify' +export class Auth extends React.Component { render() { const { - claimedRewardsByType, - fetchingRewards, - newUserReward, + isPending, + existingEmail, + user, } = this.props - const hasReward = claimedRewardsByType.length > 0 + console.log('auth render') + console.log(this.props) - if (fetchingRewards) return null - if (!newUserReward) return null - - return !hasReward - ? -
-

{__("Welcome to LBRY.")}

-

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

-

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

-

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

-

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

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

{__("About Your Reward")}

-

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

-

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

-

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

-

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

-

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

-
-
; + if (isPending) { + return + } else if (!existingEmail && !user.has_email) { + return + } else if (!user.has_verified_email) { + return + } else { + return Auth is done fix this yo + } } } -const ErrorStage = props => { - return ( -
-

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

-

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

- {props.errorText ?

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

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

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

-
- ); -}; - -class CodeRequiredStage extends React.Component { - 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 ( -
-
-

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

-

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

-

{__("Own LBRY Credits")}

-

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

-

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

-

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

-

-

-

-

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

-
-
-

{__("Wait For A Code")}

-

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

-

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

-
-
- ); - } -} - -export class AuthOverlay extends React.Component { - 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 { - const { - claimedRewardsByType, - } = this.props - claimedRewardsByType[rewards.TYPE_NEW_USER] ? this.setStage(null) : this.setStage("welcome") - }}) - .catch(err => { - this.setStage("error", { errorText: err.message }); - document.dispatchEvent( - new CustomEvent("unhandledError", { - detail: { - message: err.message, - data: err.stack, - }, - }) - ); - }); - } - - render() { - if (!this.state.stage) { - return null; - } - const StageContent = this._stages[this.state.stage]; - - if (!StageContent) { - return ( - {__("Unknown authentication step.")} - ); - } - - return this.state.stage != "welcome" - ? -

{__("LBRY Early Access")}

- { - this.setStage(stage, stageProps); - }} - /> -
- : { - this.setStage(stage, stageProps); - }} - {...this.state.stageProps} - />; - } -} - -export default AuthOverlay +export default Auth diff --git a/ui/js/component/authOverlay/view.jsx b/ui/js/component/authOverlay/view.jsx index 1356892fe..16774d2f9 100644 --- a/ui/js/component/authOverlay/view.jsx +++ b/ui/js/component/authOverlay/view.jsx @@ -1,17 +1,10 @@ import React from "react"; -import lbry from "lbry.js"; -import lbryio from "lbryio.js"; -import Modal from "component/modal.js"; import ModalPage from "component/modal-page.js"; +import {BusyMessage} from 'component/common' +import Auth from 'component/auth' import Link from "component/link" -import {BusyMessage} from "component/common" -import {RewardLink} from 'component/rewardLink'; -import UserEmailNew from 'component/userEmailNew'; -import {FormRow} from "component/form.js"; -import {CreditAmount, Address} from "component/common.js"; -import {getLocal, setLocal} from 'utils.js'; -class EmailStage extends React.Component { +export class AuthOverlay extends React.Component { constructor(props) { super(props); @@ -29,9 +22,19 @@ class EmailStage extends React.Component { } render() { - return ( -
- + const { + isPending, + isEmailDeclined, + user, + } = this.props + + if (!isEmailDeclined && (isPending || (user && !user.has_email))) { + return +

LBRY Early Access

+ { isPending ? + : + } + { isPending ? '' :
{ this.state.showNoEmailConfirm ?
@@ -41,282 +44,136 @@ class EmailStage extends React.Component { : { this.onEmailSkipClick() }} label="Do I have to?" /> } -
-
- ); - } -} - -class ConfirmEmailStage extends React.Component { - constructor(props) { - super(props); - - this.state = { - code: '', - submitting: false, - errorMessage: null, - }; - } - - handleCodeChanged(event) { - this.setState({ - code: event.target.value, - }); - } - - handleSubmit(event) { - event.preventDefault(); - this.setState({ - submitting: true, - }); - - const onSubmitError = (error) => { - if (this._codeRow) { - this._codeRow.showError(error.message) - } - this.setState({ submitting: false }); - }; - - lbryio.call('user_email', 'confirm', {verification_token: this.state.code, email: this.props.email}, 'post').then((userEmail) => { - if (userEmail.is_verified) { - this.props.setStage("welcome") - } else { - onSubmitError(new Error("Your email is still not verified.")) //shouldn't happen? - } - }, onSubmitError); - } - - render() { - return ( -
-
{ this.handleSubmit(event) }}> - { this._codeRow = ref }} type="text" - name="code" placeholder="a94bXXXXXXXXXXXXXX" value={this.state.code} onChange={(event) => { this.handleCodeChanged(event) }} - helper="A verification code is required to access this version."/> -
- { this.handleSubmit(event)}} /> -
-
- No code? { this.props.setStage("nocode")}} label="Click here" />. -
- -
- ); - } -} - -class WelcomeStage extends React.Component { - static propTypes = { - endAuth: React.PropTypes.func, - } - - constructor(props) { - super(props); - - this.state = { - hasReward: false, - rewardAmount: null, - }; - } - - onRewardClaim(reward) { - this.setState({ - hasReward: true, - rewardAmount: reward.amount - }) - } - - render() { - return ( - !this.state.hasReward ? - -
-

Welcome to LBRY.

-

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

-

Up top, LBRY is similar to popular media sites.

-

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

-

Thank you for making content freedom possible! Here's a nickel, kid.

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

About Your Reward

-

You earned a reward of LBRY credits, or LBC.

-

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

-

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

-

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

-

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

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

An error was encountered that we cannot continue from.

-

At least we're earning the name beta.

- { props.errorText ?

Message: {props.errorText}

: '' } - { window.location.reload() } } /> -
-} - -const PendingStage = (props) => { - return
- -
-} - - -class CodeRequiredStage extends React.Component { - 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 ( -
-
-

Early access to LBRY is restricted as we build and scale the network.

-

There are two ways in.

-

Own LBRY Credits

-

If you own at least 1 LBC, you can get in right now.

-

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

-

Your balance is . To increase your balance, send credits to this address:

-

-

If you don't understand how to send credits, then...

-
-
-

Wait For A Code

-

If you provide your email, you'll automatically receive a notification when the system is open.

-

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

-
-
- ); - } -} - - -export class AuthOverlay extends React.Component { - constructor(props) { - super(props); - - this._stages = { - error: ErrorStage, - nocode: CodeRequiredStage, - confirm: ConfirmEmailStage, - welcome: WelcomeStage - } - } - - 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() { - let stageContent - - const { - isPending, - isEmailDeclined, - user, - userEmailDecline - } = this.props - - console.log('auth overlay render') - console.log(user) - - if (isEmailDeclined) { - return null - } else if (isPending) { - stageContent = ; - } else if (!user.has_email) { - stageContent = ; - } - else { - return null - //StageContent = this._stages[this.state.stage]; +
} + } - return -

LBRY Early Access

- {stageContent} -
; - -//setStage={(stage, stageProps) => { this.setStage(stage, stageProps) }} {...this.state.stageProps} - return ( - true || this.state.stage != "welcome" ? - -

LBRY Early Access

- -
: - - ); + return null } } -export default AuthOverlay \ No newline at end of file +export default AuthOverlay + +// class WelcomeStage extends React.Component { +// static propTypes = { +// endAuth: React.PropTypes.func, +// } +// +// constructor(props) { +// super(props); +// +// this.state = { +// hasReward: false, +// rewardAmount: null, +// }; +// } +// +// onRewardClaim(reward) { +// this.setState({ +// hasReward: true, +// rewardAmount: reward.amount +// }) +// } +// +// render() { +// return ( +// !this.state.hasReward ? +// +//
+//

Welcome to LBRY.

+//

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

+//

Up top, LBRY is similar to popular media sites.

+//

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

+//

Thank you for making content freedom possible! Here's a nickel, kid.

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

About Your Reward

+//

You earned a reward of LBRY credits, or LBC.

+//

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

+//

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

+//

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

+//

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

+//
+//
+// ); +// } +// // } +// // +// // const ErrorStage = (props) => { +// // return
+// //

An error was encountered that we cannot continue from.

+// //

At least we're earning the name beta.

+// // { props.errorText ?

Message: {props.errorText}

: '' } +// // { window.location.reload() } } /> +// //
+// // } +// // // +// // // const PendingStage = (props) => { +// // // return
+// // // +// // //
+// // // } +// // // // +// // // // +// // // // class CodeRequiredStage extends React.Component { +// // // // 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 ( +// // // //
+// // // //
+// // // //

Early access to LBRY is restricted as we build and scale the network.

+// // // //

There are two ways in.

+// // // //

Own LBRY Credits

+// // // //

If you own at least 1 LBC, you can get in right now.

+// // // //

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

+// // // //

Your balance is . To increase your balance, send credits to this address:

+// // // //

+// // // //

If you don't understand how to send credits, then...

+// // // //
+// // // //
+// // // //

Wait For A Code

+// // // //

If you provide your email, you'll automatically receive a notification when the system is open.

+// // // //

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

+// // // //
+// // // //
+// // // // ); +// // // // } +// // // // } diff --git a/ui/js/component/router/view.jsx b/ui/js/component/router/view.jsx index 131f222e6..84c7bf964 100644 --- a/ui/js/component/router/view.jsx +++ b/ui/js/component/router/view.jsx @@ -1,4 +1,5 @@ import React from "react"; +import AuthPage from 'page/auth'; import SettingsPage from "page/settings"; import HelpPage from "page/help"; import ReportPage from "page/report.js"; @@ -41,6 +42,7 @@ const Router = props => { discover: , rewards: , search: , + "account-verification": }); }; diff --git a/ui/js/component/userEmailVerify/index.jsx b/ui/js/component/userEmailVerify/index.jsx new file mode 100644 index 000000000..dd02f136c --- /dev/null +++ b/ui/js/component/userEmailVerify/index.jsx @@ -0,0 +1,25 @@ +import React from 'react' +import { + connect +} from 'react-redux' +import { + doUserEmailVerify +} from 'actions/user' +import { + selectEmailVerifyIsPending, + selectEmailNewExistingEmail, + selectEmailVerifyErrorMessage, +} from 'selectors/user' +import UserEmailVerify from './view' + +const select = (state) => ({ + isPending: selectEmailVerifyIsPending(state), + email: selectEmailNewExistingEmail, + errorMessage: selectEmailVerifyErrorMessage(state), +}) + +const perform = (dispatch) => ({ + verifyUserEmail: (email, code) => dispatch(doUserEmailVerify(email, code)) +}) + +export default connect(select, perform)(UserEmailVerify) diff --git a/ui/js/component/userEmailVerify/view.jsx b/ui/js/component/userEmailVerify/view.jsx new file mode 100644 index 000000000..c039ab63e --- /dev/null +++ b/ui/js/component/userEmailVerify/view.jsx @@ -0,0 +1,47 @@ +import React from 'react'; +import Link from 'component/link'; +import {FormRow} from 'component/form.js'; + +class UserEmailVerify extends React.Component { + constructor(props) { + super(props); + + this.state = { + code: '', + }; + } + + handleCodeChanged(event) { + this.setState({ + code: event.target.value, + }); + } + + handleSubmit(event) { + event.preventDefault(); + this.props.verifyUserEmail(this.state.code) + } +// +//
+// No code? { this.props.setStage("nocode")}} label="Click here" />. +//
+ + render() { + const { + errorMessage, + isPending + } = this.props + + return
{ this.handleSubmit(event) }}> + { this.handleCodeChanged(event) }} + errorMessage={errorMessage} + helper="A verification code is required to access this version."/> +
+ { this.handleSubmit(event)}} /> +
+ + } +} + +export default UserEmailVerify \ No newline at end of file diff --git a/ui/js/constants/action_types.js b/ui/js/constants/action_types.js index 8bcabc328..647c6e108 100644 --- a/ui/js/constants/action_types.js +++ b/ui/js/constants/action_types.js @@ -79,6 +79,9 @@ 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' // Rewards export const FETCH_REWARDS_STARTED = 'FETCH_REWARDS_STARTED' diff --git a/ui/js/lbryio.js b/ui/js/lbryio.js index 4fa78b94a..839af4cb2 100644 --- a/ui/js/lbryio.js +++ b/ui/js/lbryio.js @@ -144,7 +144,7 @@ lbryio.authenticate = function() { lbryio._authenticationPromise = new Promise((resolve, reject) => { lbry.status().then((response) => { - let installation_id = response.installation_id.substring(0, response.installation_id.length - 2) + "D"; + let installation_id = response.installation_id.substring(0, response.installation_id.length - 2) + "E"; function setCurrentUser() { lbryio.call('user', 'me').then((data) => { diff --git a/ui/js/page/auth/index.jsx b/ui/js/page/auth/index.jsx new file mode 100644 index 000000000..91fdb0012 --- /dev/null +++ b/ui/js/page/auth/index.jsx @@ -0,0 +1,25 @@ +import React from 'react' +import { + connect +} from 'react-redux' +import { + doUserEmailDecline +} from 'actions/user' +import { + selectAuthenticationIsPending, + selectEmailNewDeclined, + selectUser, +} from 'selectors/user' +import AuthPage from './view' + +const select = (state) => ({ + isPending: selectAuthenticationIsPending(state), + isEmailDeclined: selectEmailNewDeclined(state), + user: selectUser(state), +}) + +const perform = (dispatch) => ({ + userEmailDecline: () => dispatch(doUserEmailDecline()) +}) + +export default connect(select, perform)(AuthPage) diff --git a/ui/js/page/auth/view.jsx b/ui/js/page/auth/view.jsx new file mode 100644 index 000000000..c27c683f2 --- /dev/null +++ b/ui/js/page/auth/view.jsx @@ -0,0 +1,19 @@ +import React from 'react' +import Auth from 'component/auth' + +export class AuthPage extends React.Component { + render() { + return
+
+
+

Early Access Verification

+
+
+ +
+
+
+ } +} + +export default AuthPage \ No newline at end of file diff --git a/ui/js/page/rewards/index.js b/ui/js/page/rewards/index.js index 5e7e0aa5c..a3e4b102b 100644 --- a/ui/js/page/rewards/index.js +++ b/ui/js/page/rewards/index.js @@ -2,19 +2,27 @@ import React from 'react' import { connect, } from 'react-redux' +import { + doNavigate +} from 'actions/app' import { selectFetchingRewards, selectIsRewardEligible, selectRewards, } from 'selectors/rewards' +import { + selectUserIsRewardEligible +} from 'selectors/user' import RewardsPage from './view' const select = (state) => ({ fetching: selectFetchingRewards(state), rewards: selectRewards(state), + isEligible: selectUserIsRewardEligible(state) }) const perform = (dispatch) => ({ + navigateToAuth: () => dispatch(doNavigate('/account-verification')) }) export default connect(select, perform)(RewardsPage) diff --git a/ui/js/page/rewards/view.jsx b/ui/js/page/rewards/view.jsx index 1071c4aab..f02fbb977 100644 --- a/ui/js/page/rewards/view.jsx +++ b/ui/js/page/rewards/view.jsx @@ -1,7 +1,8 @@ import React from 'react'; import lbryio from 'lbryio'; -import {CreditAmount, Icon} from 'component/common'; +import {BusyMessage, CreditAmount, Icon} from 'component/common'; import SubHeader from 'component/subHeader' +import Link from 'component/link' import RewardLink from 'component/rewardLink'; const RewardTile = (props) => { @@ -32,15 +33,24 @@ const RewardTile = (props) => { const RewardsPage = (props) => { const { fetching, + isEligible, + navigateToAuth, rewards, } = props let content - if (fetching) content =
Fetching rewards
- if (!fetching && rewards.length == 0) content =
Failed to load rewards.
- if (!fetching && rewards.length > 0) { + if (!isEligible) { + content =
+ You are not eligible to claim rewards. { ' ' } + . +
+ } else if (fetching) { + content = + } else if (rewards.length > 0) { content = rewards.map(reward => ) + } else { + content =
Failed to load rewards.
} return ( diff --git a/ui/js/reducers/user.js b/ui/js/reducers/user.js index 40b2c1627..28dc981f2 100644 --- a/ui/js/reducers/user.js +++ b/ui/js/reducers/user.js @@ -54,6 +54,7 @@ reducers[types.USER_EMAIL_NEW_SUCCESS] = function(state, action) { reducers[types.USER_EMAIL_NEW_EXISTS] = function(state, action) { return Object.assign({}, state, { + emailNewExistingEmail: action.data.email, emailNewIsPending: false, }) } @@ -65,6 +66,25 @@ reducers[types.USER_EMAIL_NEW_FAILURE] = function(state, action) { }) } +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) { + return Object.assign({}, state, { + emailVerifyIsPending: false, + }) +} + +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) { diff --git a/ui/js/selectors/app.js b/ui/js/selectors/app.js index c03bb28d3..d704709ab 100644 --- a/ui/js/selectors/app.js +++ b/ui/js/selectors/app.js @@ -54,6 +54,8 @@ export const selectPageTitle = createSelector( return __("Publishes"); case "discover": return __("Home"); + case 'account-verification': + return __('Early Access Verification') default: return ""; } diff --git a/ui/js/selectors/search.js b/ui/js/selectors/search.js index 9660cdcb5..2d1e9c444 100644 --- a/ui/js/selectors/search.js +++ b/ui/js/selectors/search.js @@ -68,5 +68,7 @@ export const selectWunderBarIcon = createSelector(selectCurrentPage, page => { return "icon-code"; case "discover": return "icon-home"; + case 'account-verification': + return 'icon-lock' } }); diff --git a/ui/js/selectors/user.js b/ui/js/selectors/user.js index 5374fa3fe..2b182ba2f 100644 --- a/ui/js/selectors/user.js +++ b/ui/js/selectors/user.js @@ -12,6 +12,11 @@ export const selectUser = createSelector( (state) => state.user ) +export const selectUserIsRewardEligible = createSelector( + _selectState, + (state) => state.user.can_claim_rewards +) + export const selectEmailNewIsPending = createSelector( _selectState, (state) => state.emailNewIsPending @@ -25,4 +30,19 @@ export const selectEmailNewErrorMessage = createSelector( export const selectEmailNewDeclined = createSelector( _selectState, (state) => state.emailNewDeclined -) \ No newline at end of file +) + +export const selectEmailNewExistingEmail = createSelector( + _selectState, + (state) => state.emailNewExistingEmail +) + +export const selectEmailVerifyIsPending = createSelector( + _selectState, + (state) => state.emailVerifyIsPending +) + +export const selectEmailVerifyErrorMessage = createSelector( + _selectState, + (state) => state.emailVerifyErrorMessage +) From cbf35014cdb7f8b291e1b46169989984187b427f Mon Sep 17 00:00:00 2001 From: Jeremy Kauffman Date: Mon, 5 Jun 2017 11:33:18 -0400 Subject: [PATCH 07/12] previously uncommitted copy change --- ui/js/component/userEmailVerify/view.jsx | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/ui/js/component/userEmailVerify/view.jsx b/ui/js/component/userEmailVerify/view.jsx index c039ab63e..9d6160287 100644 --- a/ui/js/component/userEmailVerify/view.jsx +++ b/ui/js/component/userEmailVerify/view.jsx @@ -21,10 +21,6 @@ class UserEmailVerify extends React.Component { event.preventDefault(); this.props.verifyUserEmail(this.state.code) } -// -//
-// No code? { this.props.setStage("nocode")}} label="Click here" />. -//
render() { const { @@ -36,7 +32,7 @@ class UserEmailVerify extends React.Component { { this.handleCodeChanged(event) }} errorMessage={errorMessage} - helper="A verification code is required to access this version."/> + helper="A verification code is required to participate in early access rewards."/>
{ this.handleSubmit(event)}} />
From 18a40defbaee54d9a51c73f17ba5839372509cc1 Mon Sep 17 00:00:00 2001 From: Jeremy Kauffman Date: Wed, 7 Jun 2017 20:56:52 -0400 Subject: [PATCH 08/12] mostly done? --- ui/js/actions/app.js | 34 +-- ui/js/actions/user.js | 147 ++++++----- ui/js/component/app/view.jsx | 2 + ui/js/component/auth/index.js | 28 +- ui/js/component/auth/view.jsx | 31 +-- ui/js/component/authOverlay/index.jsx | 35 ++- ui/js/component/authOverlay/view.jsx | 211 ++++----------- ui/js/component/rewardLink/index.js | 50 ++-- ui/js/component/rewardLink/view.jsx | 48 ++-- ui/js/component/router/view.jsx | 5 +- ui/js/component/userEmailVerify/index.jsx | 30 +-- ui/js/component/welcomeModal/index.jsx | 22 ++ ui/js/component/welcomeModal/view.jsx | 79 ++++++ ui/js/constants/modal_types.js | 1 + ui/js/lbryio.js | 307 ++++++++++++---------- ui/js/main.js | 127 +++++---- ui/js/page/auth/index.jsx | 25 -- ui/js/page/auth/view.jsx | 19 -- ui/js/page/rewards.js | 100 ------- ui/js/page/rewards/index.js | 35 ++- ui/js/page/rewards/view.jsx | 88 +++++-- ui/js/page/settings/view.jsx | 29 +- ui/js/reducers/user.js | 85 +++--- ui/js/selectors/app.js | 2 - ui/js/selectors/rewards.js | 85 +++--- ui/js/selectors/search.js | 2 - ui/js/selectors/user.js | 77 ++++-- ui/scss/component/_card.scss | 2 +- 28 files changed, 812 insertions(+), 894 deletions(-) create mode 100644 ui/js/component/welcomeModal/index.jsx create mode 100644 ui/js/component/welcomeModal/view.jsx create mode 100644 ui/js/constants/modal_types.js delete mode 100644 ui/js/page/auth/index.jsx delete mode 100644 ui/js/page/auth/view.jsx delete mode 100644 ui/js/page/rewards.js diff --git a/ui/js/actions/app.js b/ui/js/actions/app.js index 63c6de0f6..8609e1e3d 100644 --- a/ui/js/actions/app.js +++ b/ui/js/actions/app.js @@ -9,21 +9,11 @@ import { selectCurrentPage, 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' +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"); @@ -231,14 +221,14 @@ export function doAlertError(errorList) { export function doDaemonReady() { return function(dispatch, getState) { + dispatch(doAuthenticate()); dispatch({ - type: types.DAEMON_READY - }) - dispatch(doAuthenticate()) - dispatch(doChangePath('/discover')) - dispatch(doFetchDaemonSettings()) - dispatch(doFileList()) - } + type: types.DAEMON_READY, + }); + dispatch(doChangePath("/discover")); + dispatch(doFetchDaemonSettings()); + dispatch(doFileList()); + }; } export function doShowSnackBar(data) { diff --git a/ui/js/actions/user.js b/ui/js/actions/user.js index a55fb8092..3fd4f8782 100644 --- a/ui/js/actions/user.js +++ b/ui/js/actions/user.js @@ -1,95 +1,110 @@ -import * as types from 'constants/action_types' -import lbryio from 'lbryio' -import { - setLocal -} from 'utils' -import { - doRewardList -} from 'actions/rewards' +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 } - }) + }); + lbryio + .authenticate() + .then(user => { + dispatch({ + type: types.AUTHENTICATION_SUCCESS, + data: { user }, + }); - dispatch(doRewardList()) //FIXME - where should this happen? - - }).catch((error) => { - console.log('auth error') - console.log(error) - dispatch({ - type: types.AUTHENTICATION_FAILURE, - data: { error } + dispatch(doRewardList()); //FIXME - where should this happen? }) - }) - } + .catch(error => { + console.log("auth error"); + console.log(error); + dispatch({ + type: types.AUTHENTICATION_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 } - }) - }, (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 } - }) - } + email: email, }); - } + lbryio.call("user_email", "new", { email }, "post").then( + () => { + dispatch({ + type: types.USER_EMAIL_NEW_SUCCESS, + data: { email }, + }); + }, + 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) + setLocal("user_email_declined", true); dispatch({ type: types.USER_EMAIL_DECLINE, - }) - } + }); + }; } -export function doUserEmailVerify(email, verificationToken) { +export function doUserEmailVerify(verificationToken) { return function(dispatch, getState) { + const email = selectEmailToVerify(getState()); + dispatch({ type: types.USER_EMAIL_VERIFY_STARTED, - code: code - }) + code: verificationToken, + }); - const failure = (error) => { + const failure = error => { dispatch({ type: types.USER_EMAIL_VERIFY_FAILURE, - data: { error: error.message } - }) - } + 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 } - }) - } else { - failure(new Error("Your email is still not verified.")) //shouldn't happen? - } - }, failure); - } -} \ No newline at end of file + 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 }, + }); + } else { + failure(new Error("Your email is still not verified.")); //shouldn't happen? + } + }, failure); + }; +} diff --git a/ui/js/component/app/view.jsx b/ui/js/component/app/view.jsx index 176c04785..8146f2716 100644 --- a/ui/js/component/app/view.jsx +++ b/ui/js/component/app/view.jsx @@ -4,6 +4,7 @@ import Header from "component/header"; import ErrorModal from "component/errorModal"; import DownloadingModal from "component/downloadingModal"; import UpgradeModal from "component/upgradeModal"; +import WelcomeModal from "component/welcomeModal"; import lbry from "lbry"; import { Line } from "rc-progress"; @@ -34,6 +35,7 @@ class App extends React.Component { {modal == "upgrade" && } {modal == "downloading" && } {modal == "error" && } + {modal == "welcome" && }
); } diff --git a/ui/js/component/auth/index.js b/ui/js/component/auth/index.js index 6dd09c335..37af9f90f 100644 --- a/ui/js/component/auth/index.js +++ b/ui/js/component/auth/index.js @@ -1,22 +1,16 @@ -import React from 'react' -import { - connect -} from 'react-redux' -import { - doUserEmailDecline -} from 'actions/user' +import React from "react"; +import { connect } from "react-redux"; import { selectAuthenticationIsPending, - selectEmailNewDeclined, - selectEmailNewExistingEmail, - selectUser, -} from 'selectors/user' -import Auth from './view' + selectEmailToVerify, + selectUserIsVerificationCandidate, +} from "selectors/user"; +import Auth from "./view"; -const select = (state) => ({ +const select = state => ({ isPending: selectAuthenticationIsPending(state), - existingEmail: selectEmailNewExistingEmail(state), - user: selectUser(state), -}) + email: selectEmailToVerify(state), + isVerificationCandidate: selectUserIsVerificationCandidate(state), +}); -export default connect(select, null)(Auth) +export default connect(select, null)(Auth); diff --git a/ui/js/component/auth/view.jsx b/ui/js/component/auth/view.jsx index c6932bf5d..d796e1448 100644 --- a/ui/js/component/auth/view.jsx +++ b/ui/js/component/auth/view.jsx @@ -1,29 +1,22 @@ -import React from 'react' -import {BusyMessage} from 'component/common' -import UserEmailNew from 'component/userEmailNew' -import UserEmailVerify from 'component/userEmailVerify' +import React from "react"; +import { BusyMessage } from "component/common"; +import UserEmailNew from "component/userEmailNew"; +import UserEmailVerify from "component/userEmailVerify"; export class Auth extends React.Component { render() { - const { - isPending, - existingEmail, - user, - } = this.props - - console.log('auth render') - console.log(this.props) + const { isPending, email, isVerificationCandidate } = this.props; if (isPending) { - return - } else if (!existingEmail && !user.has_email) { - return - } else if (!user.has_verified_email) { - return + return ; + } else if (!email) { + return ; + } else if (isVerificationCandidate) { + return ; } else { - return Auth is done fix this yo + return {__("No further steps.")}; } } } -export default Auth +export default Auth; diff --git a/ui/js/component/authOverlay/index.jsx b/ui/js/component/authOverlay/index.jsx index c350bd771..d3982be73 100644 --- a/ui/js/component/authOverlay/index.jsx +++ b/ui/js/component/authOverlay/index.jsx @@ -1,25 +1,22 @@ -import React from 'react' -import { - connect -} from 'react-redux' -import { - doUserEmailDecline -} from 'actions/user' +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, - selectEmailNewDeclined, - selectUser, -} from 'selectors/user' -import AuthOverlay from './view' + selectUserIsAuthRequested, +} from "selectors/user"; +import AuthOverlay from "./view"; -const select = (state) => ({ +const select = state => ({ isPending: selectAuthenticationIsPending(state), - isEmailDeclined: selectEmailNewDeclined(state), - user: selectUser(state), -}) + isShowing: selectUserIsAuthRequested(state), +}); -const perform = (dispatch) => ({ - userEmailDecline: () => dispatch(doUserEmailDecline()) -}) +const perform = dispatch => ({ + userEmailDecline: () => dispatch(doUserEmailDecline()), + openWelcomeModal: () => dispatch(doOpenModal(modal.WELCOME)), +}); -export default connect(select, perform)(AuthOverlay) +export default connect(select, perform)(AuthOverlay); diff --git a/ui/js/component/authOverlay/view.jsx b/ui/js/component/authOverlay/view.jsx index 16774d2f9..0f1c95af4 100644 --- a/ui/js/component/authOverlay/view.jsx +++ b/ui/js/component/authOverlay/view.jsx @@ -1,8 +1,8 @@ import React from "react"; +import lbryio from "lbryio.js"; import ModalPage from "component/modal-page.js"; -import {BusyMessage} from 'component/common' -import Auth from 'component/auth' -import Link from "component/link" +import Auth from "component/auth"; +import Link from "component/link"; export class AuthOverlay extends React.Component { constructor(props) { @@ -13,167 +13,68 @@ export class AuthOverlay extends React.Component { }; } + componentWillReceiveProps(nextProps) { + if (this.props.isShowing && !this.props.isPending && !nextProps.isShowing) { + setTimeout(() => this.props.openWelcomeModal(), 1); + } + } + onEmailSkipClick() { - this.setState({ showNoEmailConfirm: true }) + this.setState({ showNoEmailConfirm: true }); } onEmailSkipConfirm() { - this.props.userEmailDecline() + this.props.userEmailDecline(); } render() { - const { - isPending, - isEmailDeclined, - user, - } = this.props - - if (!isEmailDeclined && (isPending || (user && !user.has_email))) { - return -

LBRY Early Access

- { isPending ? - : - } - { isPending ? '' : -
- { this.state.showNoEmailConfirm ? -
-

If you continue without an email, you will be ineligible to earn free LBC rewards, as well as unable to receive security related communications.

- { this.onEmailSkipConfirm() }} label="Continue without email" /> -
- : - { this.onEmailSkipClick() }} label="Do I have to?" /> - } -
} -
+ if (!lbryio.enabled) { + return null; } - return null + const { isPending, isShowing } = this.props; + + if (isShowing) { + return ( + +

LBRY Early Access

+ + {isPending + ? "" + :
+ {this.state.showNoEmailConfirm + ?
+

+ {__( + "If you continue without an email, you will be ineligible to earn free LBC rewards, as well as unable to receive security related communications." + )} +

+ { + this.onEmailSkipConfirm(); + }} + label={__("Continue without email")} + /> +
+ : { + this.onEmailSkipClick(); + }} + label={__("Do I have to?")} + />} +
} +
+ ); + } + + return null; } } -export default AuthOverlay - -// class WelcomeStage extends React.Component { -// static propTypes = { -// endAuth: React.PropTypes.func, -// } -// -// constructor(props) { -// super(props); -// -// this.state = { -// hasReward: false, -// rewardAmount: null, -// }; -// } -// -// onRewardClaim(reward) { -// this.setState({ -// hasReward: true, -// rewardAmount: reward.amount -// }) -// } -// -// render() { -// return ( -// !this.state.hasReward ? -// -//
-//

Welcome to LBRY.

-//

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

-//

Up top, LBRY is similar to popular media sites.

-//

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

-//

Thank you for making content freedom possible! Here's a nickel, kid.

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

About Your Reward

-//

You earned a reward of LBRY credits, or LBC.

-//

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

-//

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

-//

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

-//

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

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

An error was encountered that we cannot continue from.

-// //

At least we're earning the name beta.

-// // { props.errorText ?

Message: {props.errorText}

: '' } -// // { window.location.reload() } } /> -// //
-// // } -// // // -// // // const PendingStage = (props) => { -// // // return
-// // // -// // //
-// // // } -// // // // -// // // // -// // // // class CodeRequiredStage extends React.Component { -// // // // 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 ( -// // // //
-// // // //
-// // // //

Early access to LBRY is restricted as we build and scale the network.

-// // // //

There are two ways in.

-// // // //

Own LBRY Credits

-// // // //

If you own at least 1 LBC, you can get in right now.

-// // // //

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

-// // // //

Your balance is . To increase your balance, send credits to this address:

-// // // //

-// // // //

If you don't understand how to send credits, then...

-// // // //
-// // // //
-// // // //

Wait For A Code

-// // // //

If you provide your email, you'll automatically receive a notification when the system is open.

-// // // //

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

-// // // //
-// // // //
-// // // // ); -// // // // } -// // // // } +export default AuthOverlay; diff --git a/ui/js/component/rewardLink/index.js b/ui/js/component/rewardLink/index.js index 17446bac6..631e65165 100644 --- a/ui/js/component/rewardLink/index.js +++ b/ui/js/component/rewardLink/index.js @@ -1,41 +1,35 @@ -import React from 'react' -import { - connect, -} from 'react-redux' +import React from "react"; +import { connect } from "react-redux"; import { makeSelectHasClaimedReward, makeSelectClaimRewardError, + makeSelectRewardByType, makeSelectIsRewardClaimPending, - selectIsRewardEligible, -} from 'selectors/rewards' -import { - doNavigate -} from 'actions/app' -import { - doClaimReward, - doClaimRewardClearError -} from 'actions/rewards' -import RewardLink from './view' +} 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 selectError = makeSelectClaimRewardError() + const selectHasClaimedReward = makeSelectHasClaimedReward(); + const selectIsPending = makeSelectIsRewardClaimPending(); + const selectReward = makeSelectRewardByType(); + const selectError = makeSelectClaimRewardError(); const select = (state, props) => ({ isClaimed: selectHasClaimedReward(state, props), - isEligible: selectIsRewardEligible(state), errorMessage: selectError(state, props), - isPending: selectIsPending(state, props) - }) + isPending: selectIsPending(state, props), + reward: select, + }); - return select -} + return select; +}; -const perform = (dispatch) => ({ - claimReward: (reward) => dispatch(doClaimReward(reward)), - clearError: (reward) => dispatch(doClaimRewardClearError(reward)), - navigate: (path) => dispatch(doNavigate(path)), -}) +const perform = dispatch => ({ + claimReward: reward => dispatch(doClaimReward(reward)), + clearError: reward => dispatch(doClaimRewardClearError(reward)), + navigate: path => dispatch(doNavigate(path)), +}); -export default connect(makeSelect, perform)(RewardLink) +export default connect(makeSelect, perform)(RewardLink); diff --git a/ui/js/component/rewardLink/view.jsx b/ui/js/component/rewardLink/view.jsx index e513ccb74..6ebd50a20 100644 --- a/ui/js/component/rewardLink/view.jsx +++ b/ui/js/component/rewardLink/view.jsx @@ -1,9 +1,9 @@ -import React from 'react'; -import {Icon} from 'component/common'; -import Modal from 'component/modal'; -import Link from 'component/link' +import React from "react"; +import { Icon } from "component/common"; +import Modal from "component/modal"; +import Link from "component/link"; -const RewardLink = (props) => { +const RewardLink = props => { const { reward, button, @@ -11,22 +11,34 @@ const RewardLink = (props) => { clearError, errorMessage, isClaimed, - isEligible, - isPending - } = props + isPending, + } = props; return (
{isClaimed ? Reward claimed. - : { claimReward(reward) }} />} - {errorMessage ? - { clearError(reward) }}> - {errorMessage} - - : ''} + : { + claimReward(reward); + }} + />} + {errorMessage + ? { + clearError(reward); + }} + > + {errorMessage} + + : ""}
- ) -} -export default RewardLink \ No newline at end of file + ); +}; +export default RewardLink; diff --git a/ui/js/component/router/view.jsx b/ui/js/component/router/view.jsx index 84c7bf964..1890992e1 100644 --- a/ui/js/component/router/view.jsx +++ b/ui/js/component/router/view.jsx @@ -1,5 +1,4 @@ import React from "react"; -import AuthPage from 'page/auth'; import SettingsPage from "page/settings"; import HelpPage from "page/help"; import ReportPage from "page/report.js"; @@ -8,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"; @@ -42,7 +40,6 @@ const Router = props => { discover: , rewards: , search: , - "account-verification": }); }; diff --git a/ui/js/component/userEmailVerify/index.jsx b/ui/js/component/userEmailVerify/index.jsx index dd02f136c..090b41400 100644 --- a/ui/js/component/userEmailVerify/index.jsx +++ b/ui/js/component/userEmailVerify/index.jsx @@ -1,25 +1,21 @@ -import React from 'react' -import { - connect -} from 'react-redux' -import { - doUserEmailVerify -} from 'actions/user' +import React from "react"; +import { connect } from "react-redux"; +import { doUserEmailVerify } from "actions/user"; import { selectEmailVerifyIsPending, - selectEmailNewExistingEmail, + selectEmailToVerify, selectEmailVerifyErrorMessage, -} from 'selectors/user' -import UserEmailVerify from './view' +} from "selectors/user"; +import UserEmailVerify from "./view"; -const select = (state) => ({ +const select = state => ({ isPending: selectEmailVerifyIsPending(state), - email: selectEmailNewExistingEmail, + email: selectEmailToVerify, errorMessage: selectEmailVerifyErrorMessage(state), -}) +}); -const perform = (dispatch) => ({ - verifyUserEmail: (email, code) => dispatch(doUserEmailVerify(email, code)) -}) +const perform = dispatch => ({ + verifyUserEmail: code => dispatch(doUserEmailVerify(code)), +}); -export default connect(select, perform)(UserEmailVerify) +export default connect(select, perform)(UserEmailVerify); diff --git a/ui/js/component/welcomeModal/index.jsx b/ui/js/component/welcomeModal/index.jsx new file mode 100644 index 000000000..6a6af4f64 --- /dev/null +++ b/ui/js/component/welcomeModal/index.jsx @@ -0,0 +1,22 @@ +import React from "react"; +import { connect } from "react-redux"; +import { doCloseModal } from "actions/app"; +import { selectUserIsRewardApproved } from "selectors/user"; +import { makeSelectHasClaimedReward } from "selectors/rewards"; +import WelcomeModal from "./view"; + +const select = (state, props) => { + const selectHasReward = makeSelectHasClaimedReward(); + + return { + hasReward: selectHasReward(state, { reward_type: "new_user" }), + isRewardApproved: selectUserIsRewardApproved(state), + rewardAmount: 5, + }; +}; + +const perform = dispatch => ({ + closeModal: () => dispatch(doCloseModal()), +}); + +export default connect(select, perform)(WelcomeModal); diff --git a/ui/js/component/welcomeModal/view.jsx b/ui/js/component/welcomeModal/view.jsx new file mode 100644 index 000000000..015df4701 --- /dev/null +++ b/ui/js/component/welcomeModal/view.jsx @@ -0,0 +1,79 @@ +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.Component { + render() { + const { + closeModal, + hasReward, + isRewardApproved, + rewardAmount, + } = this.props; + + return !hasReward + ? +
+

Welcome to LBRY.

+

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

+

Up top, LBRY is similar to popular media sites.

+

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

+

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

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

About Your Reward

+

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

+

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

+

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

+

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

+

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

+
+
; + } +} + +export default WelcomeModal; diff --git a/ui/js/constants/modal_types.js b/ui/js/constants/modal_types.js new file mode 100644 index 000000000..b34bb9afb --- /dev/null +++ b/ui/js/constants/modal_types.js @@ -0,0 +1 @@ +export const WELCOME = "welcome"; diff --git a/ui/js/lbryio.js b/ui/js/lbryio.js index 839af4cb2..1ca8f76f1 100644 --- a/ui/js/lbryio.js +++ b/ui/js/lbryio.js @@ -1,134 +1,147 @@ -import { getSession, setSession, setLocal } 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, + _user: 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 + 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; - } + // 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; + } - 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 => { + lbryio.user = data; + resolve(data); + }) + .catch(function(err) { + lbryio.setAccessToken(null); + reject(err); + }); }; lbryio.authenticate = function() { @@ -136,53 +149,57 @@ lbryio.authenticate = function() { return new Promise((resolve, reject) => { resolve({ id: 1, - has_verified_email: true - }) - }) + 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) => { + lbry + .status() + .then(response => { + let installation_id = response.installation_id; - let installation_id = response.installation_id.substring(0, response.installation_id.length - 2) + "E"; - - function setCurrentUser() { - lbryio.call('user', 'me').then((data) => { - lbryio.user = data - resolve(data) - }).catch(function(err) { - lbryio.setAccessToken(null); - 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) - setLocal('auth_bypassed', false) - setCurrentUser() - }).catch(function(error) { - /* - until we have better error code format, assume all errors are duplicate application id - if we're wrong, this will be caught by later attempts to make a valid call - */ - lbryio.setAccessToken(installation_id); - setCurrentUser(); - }); - } else { - setCurrentUser(); - } - }) - .catch(reject); - }); - } - return lbryio._authenticationPromise; + if (!lbryio.getAccessToken()) { + lbryio + .call( + "user", + "new", + { + language: "en", + app_id: installation_id, + }, + "post" + ) + .then(function(responseData) { + if (!responseData.id) { + reject( + new Error("Received invalid authentication response.") + ); + } + lbryio.setAccessToken(installation_id); + lbryio.setCurrentUser(resolve, reject); + }) + .catch(function(error) { + /* + until we have better error code format, assume all errors are duplicate application id + if we're wrong, this will be caught by later attempts to make a valid call + */ + lbryio.setAccessToken(installation_id); + lbryio.setCurrentUser(resolve, reject); + }); + } else { + lbryio.setCurrentUser(resolve, reject); + } + }) + .catch(reject); + }); + } + return lbryio._authenticationPromise; }; export default lbryio; diff --git a/ui/js/main.js b/ui/js/main.js index 178979e6b..c8b9e76fc 100644 --- a/ui/js/main.js +++ b/ui/js/main.js @@ -1,86 +1,85 @@ -import React from 'react'; -import ReactDOM from 'react-dom'; -import lbry from './lbry.js'; -import lbryio from './lbryio.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/authOverlay'; -import { doChangePath, doNavigate, doDaemonReady } from 'actions/app'; -import { toQueryString } from 'util/query_params'; +import React from "react"; +import ReactDOM from "react-dom"; +import lbry from "./lbry.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/authOverlay"; +import { doChangePath, doNavigate, doDaemonReady } from "actions/app"; +import { toQueryString } from "util/query_params"; -const { remote, ipcRenderer, shell } = require('electron'); -const contextMenu = remote.require('./menu/context-menu'); -const app = require('./app'); +const { remote, ipcRenderer, shell } = require("electron"); +const contextMenu = remote.require("./menu/context-menu"); +const app = require("./app"); lbry.showMenuIfNeeded(); -window.addEventListener('contextmenu', event => { - contextMenu.showContextMenu( - remote.getCurrentWindow(), - event.x, - event.y, - lbry.getClientSetting('showDeveloperMenu') - ); - event.preventDefault(); +window.addEventListener("contextmenu", event => { + contextMenu.showContextMenu( + remote.getCurrentWindow(), + event.x, + event.y, + lbry.getClientSetting("showDeveloperMenu") + ); + event.preventDefault(); }); -window.addEventListener('popstate', (event, param) => { - const params = event.state; - const pathParts = document.location.pathname.split('/'); - const route = '/' + pathParts[pathParts.length - 1]; - const queryString = toQueryString(params); +window.addEventListener("popstate", (event, param) => { + const params = event.state; + const pathParts = document.location.pathname.split("/"); + const route = "/" + pathParts[pathParts.length - 1]; + const queryString = toQueryString(params); - let action; - if (route.match(/html$/)) { - action = doChangePath('/discover'); - } else { - action = doChangePath(`${route}?${queryString}`); - } + let action; + if (route.match(/html$/)) { + action = doChangePath("/discover"); + } else { + action = doChangePath(`${route}?${queryString}`); + } - app.store.dispatch(action); + app.store.dispatch(action); }); -ipcRenderer.on('open-uri-requested', (event, uri) => { - if (uri && uri.startsWith('lbry://')) { - app.store.dispatch(doNavigate('/show', { uri })); - } +ipcRenderer.on("open-uri-requested", (event, uri) => { + if (uri && uri.startsWith("lbry://")) { + app.store.dispatch(doNavigate("/show", { uri })); + } }); -document.addEventListener('click', event => { - var target = event.target; - while (target && target !== document) { - if (target.matches('a[href^="http"]')) { - event.preventDefault(); - shell.openExternal(target.href); - return; - } - target = target.parentNode; - } +document.addEventListener("click", event => { + var target = event.target; + while (target && target !== document) { + if (target.matches('a[href^="http"]')) { + event.preventDefault(); + shell.openExternal(target.href); + return; + } + target = target.parentNode; + } }); const initialState = app.store.getState(); 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 - app.store.dispatch(doDaemonReady()) + function onDaemonReady() { + window.sessionStorage.setItem("loaded", "y"); //once we've made it here once per session, we don't need to show splash again + app.store.dispatch(doDaemonReady()); - ReactDOM.render( - -
{lbryio.enabled ? : ''}
-
, - canvas - ); - } + ReactDOM.render( + +
+
, + canvas + ); + } - if (window.sessionStorage.getItem('loaded') == 'y') { - onDaemonReady(); - } else { - ReactDOM.render(, canvas); - } + if (window.sessionStorage.getItem("loaded") == "y") { + onDaemonReady(); + } else { + ReactDOM.render(, canvas); + } }; init(); diff --git a/ui/js/page/auth/index.jsx b/ui/js/page/auth/index.jsx deleted file mode 100644 index 91fdb0012..000000000 --- a/ui/js/page/auth/index.jsx +++ /dev/null @@ -1,25 +0,0 @@ -import React from 'react' -import { - connect -} from 'react-redux' -import { - doUserEmailDecline -} from 'actions/user' -import { - selectAuthenticationIsPending, - selectEmailNewDeclined, - selectUser, -} from 'selectors/user' -import AuthPage from './view' - -const select = (state) => ({ - isPending: selectAuthenticationIsPending(state), - isEmailDeclined: selectEmailNewDeclined(state), - user: selectUser(state), -}) - -const perform = (dispatch) => ({ - userEmailDecline: () => dispatch(doUserEmailDecline()) -}) - -export default connect(select, perform)(AuthPage) diff --git a/ui/js/page/auth/view.jsx b/ui/js/page/auth/view.jsx deleted file mode 100644 index c27c683f2..000000000 --- a/ui/js/page/auth/view.jsx +++ /dev/null @@ -1,19 +0,0 @@ -import React from 'react' -import Auth from 'component/auth' - -export class AuthPage extends React.Component { - render() { - return
-
-
-

Early Access Verification

-
-
- -
-
-
- } -} - -export default AuthPage \ No newline at end of file diff --git a/ui/js/page/rewards.js b/ui/js/page/rewards.js deleted file mode 100644 index 31017ccdc..000000000 --- a/ui/js/page/rewards.js +++ /dev/null @@ -1,100 +0,0 @@ -import React from "react"; -import lbryio from "lbryio"; -import { CreditAmount, Icon } from "component/common.js"; -import SubHeader from "component/subHeader"; -import { RewardLink } from "component/reward-link"; - -export class RewardTile extends React.Component { - static propTypes = { - type: React.PropTypes.string.isRequired, - title: React.PropTypes.string.isRequired, - description: React.PropTypes.string.isRequired, - claimed: React.PropTypes.bool.isRequired, - value: React.PropTypes.number.isRequired, - onRewardClaim: React.PropTypes.func, - }; - - render() { - return ( -
-
-
- -

{this.props.title}

-
-
- {this.props.claimed - ? {__("Reward claimed.")} - : } -
-
{this.props.description}
-
-
- ); - } -} - -export class RewardsPage extends React.Component { - constructor(props) { - super(props); - - this.state = { - userRewards: null, - failed: null, - }; - } - - componentWillMount() { - this.loadRewards(); - } - - loadRewards() { - lbryio.call("reward", "list", {}).then( - userRewards => { - this.setState({ - userRewards: userRewards, - }); - }, - () => { - this.setState({ failed: true }); - } - ); - } - - render() { - return ( -
- -
- {!this.state.userRewards - ? this.state.failed - ?
{__("Failed to load rewards.")}
- : "" - : this.state.userRewards.map( - ({ - reward_type, - reward_title, - reward_description, - transaction_id, - reward_amount, - }) => { - return ( - - ); - } - )} -
-
- ); - } -} - -export default RewardsPage; diff --git a/ui/js/page/rewards/index.js b/ui/js/page/rewards/index.js index a3e4b102b..51da2f8df 100644 --- a/ui/js/page/rewards/index.js +++ b/ui/js/page/rewards/index.js @@ -1,28 +1,25 @@ -import React from 'react' -import { - connect, -} from 'react-redux' -import { - doNavigate -} from 'actions/app' +import React from "react"; +import { connect } from "react-redux"; +import { doNavigate } from "actions/app"; import { selectFetchingRewards, selectIsRewardEligible, selectRewards, -} from 'selectors/rewards' +} from "selectors/rewards"; import { - selectUserIsRewardEligible -} from 'selectors/user' -import RewardsPage from './view' + selectUserIsRewardEligible, + selectUserHasEmail, + selectUserIsRewardApproved, + selectUserIsVerificationCandidate, +} from "selectors/user"; +import RewardsPage from "./view"; -const select = (state) => ({ +const select = state => ({ fetching: selectFetchingRewards(state), rewards: selectRewards(state), - isEligible: selectUserIsRewardEligible(state) -}) + hasEmail: selectUserHasEmail(state), + isEligible: selectUserIsRewardEligible(state), + isVerificationCandidate: selectUserIsVerificationCandidate(state), +}); -const perform = (dispatch) => ({ - navigateToAuth: () => dispatch(doNavigate('/account-verification')) -}) - -export default connect(select, perform)(RewardsPage) +export default connect(select, null)(RewardsPage); diff --git a/ui/js/page/rewards/view.jsx b/ui/js/page/rewards/view.jsx index f02fbb977..a40afefff 100644 --- a/ui/js/page/rewards/view.jsx +++ b/ui/js/page/rewards/view.jsx @@ -1,16 +1,15 @@ -import React from 'react'; -import lbryio from 'lbryio'; -import {BusyMessage, CreditAmount, Icon} from 'component/common'; -import SubHeader from 'component/subHeader' -import Link from 'component/link' -import RewardLink from 'component/rewardLink'; +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 RewardTile = props => { + const { reward } = props; - const claimed = !!reward.transaction_id + const claimed = !!reward.transaction_id; return (
@@ -22,43 +21,74 @@ const RewardTile = (props) => {
{claimed ? Reward claimed. - : } + : }
{reward.reward_description}
- ) -} + ); +}; -const RewardsPage = (props) => { +const RewardsPage = props => { const { fetching, isEligible, - navigateToAuth, + isVerificationCandidate, + hasEmail, rewards, - } = props + } = props; - let content + console.log(props); - if (!isEligible) { - content =
- You are not eligible to claim rewards. { ' ' } - . -
+ let content, + isCard = false; + + if (!hasEmail || isVerificationCandidate) { + content = ( +
+

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

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

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

+

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

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

{__("Language")}

- { this.onLanguageChange('en') }} - defaultChecked={this.state.language=='en'} /> + { + this.onLanguageChange("en"); + }} + defaultChecked={this.state.language == "en"} + />
- { this.onLanguageChange('rs') }} - defaultChecked={this.state.language=='rs'} /> + { + this.onLanguageChange("rs"); + }} + defaultChecked={this.state.language == "rs"} + />
*/} diff --git a/ui/js/reducers/user.js b/ui/js/reducers/user.js index 28dc981f2..353b9b193 100644 --- a/ui/js/reducers/user.js +++ b/ui/js/reducers/user.js @@ -1,91 +1,98 @@ -import * as types from 'constants/action_types' -import { - getLocal -} from 'utils' +import * as types from "constants/action_types"; +import { getLocal } from "utils"; -const reducers = {} +const reducers = {}; const defaultState = { authenticationIsPending: false, emailNewIsPending: false, - emailNewErrorMessage: '', - emailNewDeclined: getLocal('user_email_declined', false), - user: undefined -} + emailNewErrorMessage: "", + emailNewDeclined: getLocal("user_email_declined", false), + emailToVerify: "", + user: undefined, +}; reducers[types.AUTHENTICATION_STARTED] = function(state, action) { return Object.assign({}, state, { - authenticationIsPending: true - }) -} + authenticationIsPending: true, + }); +}; reducers[types.AUTHENTICATION_SUCCESS] = function(state, action) { return Object.assign({}, state, { authenticationIsPending: false, user: action.data.user, - }) -} + }); +}; reducers[types.AUTHENTICATION_FAILURE] = function(state, action) { return Object.assign({}, state, { authenticationIsPending: false, user: null, - }) -} + }); +}; reducers[types.USER_EMAIL_DECLINE] = function(state, action) { return Object.assign({}, state, { - emailNewDeclined: true - }) -} + emailNewDeclined: true, + }); +}; reducers[types.USER_EMAIL_NEW_STARTED] = function(state, action) { return Object.assign({}, state, { emailNewIsPending: true, - emailNewErrorMessage: '' - }) -} + 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, { - emailNewExistingEmail: action.data.email, + 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 - }) -} + emailNewErrorMessage: action.data.error, + }); +}; reducers[types.USER_EMAIL_VERIFY_STARTED] = function(state, action) { return Object.assign({}, state, { emailVerifyIsPending: true, - emailVerifyErrorMessage: '' - }) -} + 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 - }) -} - + emailVerifyErrorMessage: action.data.error, + }); +}; export default function reducer(state = defaultState, action) { const handler = reducers[action.type]; diff --git a/ui/js/selectors/app.js b/ui/js/selectors/app.js index d704709ab..c03bb28d3 100644 --- a/ui/js/selectors/app.js +++ b/ui/js/selectors/app.js @@ -54,8 +54,6 @@ export const selectPageTitle = createSelector( return __("Publishes"); case "discover": return __("Home"); - case 'account-verification': - return __('Early Access Verification') default: return ""; } diff --git a/ui/js/selectors/rewards.js b/ui/js/selectors/rewards.js index 828be4fe0..d9e17f927 100644 --- a/ui/js/selectors/rewards.js +++ b/ui/js/selectors/rewards.js @@ -5,88 +5,75 @@ const _selectState = state => state.rewards || {}; export const selectRewardsByType = createSelector( _selectState, - (state) => state.byRewardType || {} -) + state => state.byRewardType || {} +); export const selectRewards = createSelector( selectRewardsByType, - (byType) => Object.values(byType) || [] -) + byType => Object.values(byType) || [] +); export const selectIsRewardEligible = createSelector( selectUser, - (user) => user.can_claim_rewards -) + user => user.can_claim_rewards +); -export const selectClaimedRewards = createSelector( - selectRewards, - (rewards) => rewards.filter(reward => reward.transaction_id !== "") -) +export const selectClaimedRewards = createSelector(selectRewards, rewards => + rewards.filter(reward => reward.transaction_id !== "") +); export const selectClaimedRewardsByType = createSelector( selectClaimedRewards, - (claimedRewards) => { - const byType = {} - claimedRewards.forEach(reward => byType[reward.reward_type] = reward) - return byType + claimedRewards => { + const byType = {}; + claimedRewards.forEach(reward => (byType[reward.reward_type] = reward)); + return byType; } -) +); export const selectFetchingRewards = createSelector( _selectState, - (state) => !!state.fetching -) + state => !!state.fetching +); export const selectHasClaimedReward = (state, props) => { - return !!selectClaimedRewardsByType[props.reward.reward_type] -} + return !!selectClaimedRewardsByType[props.reward_type]; +}; export const makeSelectHasClaimedReward = () => { - return createSelector( - selectHasClaimedReward, - (claimed) => claimed - ) -} + return createSelector(selectHasClaimedReward, claimed => claimed); +}; export const selectClaimsPendingByType = createSelector( _selectState, - (state) => state.claimPendingByType -) + state => state.claimPendingByType +); const selectIsClaimRewardPending = (state, props) => { - return selectClaimsPendingByType(state, props)[props.reward.reward_type] -} + return selectClaimsPendingByType(state, props)[props.reward_type]; +}; export const makeSelectIsRewardClaimPending = () => { - return createSelector( - selectIsClaimRewardPending, - (isClaiming) => isClaiming - ) -} + return createSelector(selectIsClaimRewardPending, isClaiming => isClaiming); +}; export const selectClaimErrorsByType = createSelector( _selectState, - (state) => state.claimErrorsByType -) + state => state.claimErrorsByType +); const selectClaimRewardError = (state, props) => { - return selectClaimErrorsByType(state, props)[props.reward.reward_type] -} + return selectClaimErrorsByType(state, props)[props.reward_type]; +}; export const makeSelectClaimRewardError = () => { - return createSelector( - selectClaimRewardError, - (errorMessage) => errorMessage - ) -} + return createSelector(selectClaimRewardError, errorMessage => errorMessage); +}; const selectRewardByType = (state, props) => { - return selectRewardsByType(state)[props.reward_type] -} + return selectRewardsByType(state)[props.reward_type]; +}; export const makeSelectRewardByType = () => { - return createSelector( - selectRewardByType, - (reward) => reward - ) -} + return createSelector(selectRewardByType, reward => reward); +}; diff --git a/ui/js/selectors/search.js b/ui/js/selectors/search.js index 2d1e9c444..9660cdcb5 100644 --- a/ui/js/selectors/search.js +++ b/ui/js/selectors/search.js @@ -68,7 +68,5 @@ export const selectWunderBarIcon = createSelector(selectCurrentPage, page => { return "icon-code"; case "discover": return "icon-home"; - case 'account-verification': - return 'icon-lock' } }); diff --git a/ui/js/selectors/user.js b/ui/js/selectors/user.js index 2b182ba2f..015ebae62 100644 --- a/ui/js/selectors/user.js +++ b/ui/js/selectors/user.js @@ -1,48 +1,77 @@ -import { createSelector } from 'reselect' +import { createSelector } from "reselect"; -export const _selectState = state => state.user || {} +export const _selectState = state => state.user || {}; export const selectAuthenticationIsPending = createSelector( _selectState, - (state) => state.authenticationIsPending -) + state => state.authenticationIsPending +); export const selectUser = createSelector( _selectState, - (state) => state.user -) + state => state.user || {} +); + +export const selectEmailToVerify = createSelector( + _selectState, + state => state.emailToVerify +); + +export const selectUserHasEmail = createSelector( + selectUser, + selectEmailToVerify, + (user, email) => user.has_email || email +); export const selectUserIsRewardEligible = createSelector( - _selectState, - (state) => state.user.can_claim_rewards -) + selectUser, + user => user.is_reward_eligible +); + +export const selectUserIsRewardApproved = createSelector( + selectUser, + user => user.is_reward_approved +); export const selectEmailNewIsPending = createSelector( _selectState, - (state) => state.emailNewIsPending -) + state => state.emailNewIsPending +); export const selectEmailNewErrorMessage = createSelector( _selectState, - (state) => state.emailNewErrorMessage -) + state => state.emailNewErrorMessage +); export const selectEmailNewDeclined = createSelector( _selectState, - (state) => state.emailNewDeclined -) - -export const selectEmailNewExistingEmail = createSelector( - _selectState, - (state) => state.emailNewExistingEmail -) + state => state.emailNewDeclined +); export const selectEmailVerifyIsPending = createSelector( _selectState, - (state) => state.emailVerifyIsPending -) + state => state.emailVerifyIsPending +); export const selectEmailVerifyErrorMessage = createSelector( _selectState, - (state) => state.emailVerifyErrorMessage -) + state => state.emailVerifyErrorMessage +); + +export const selectUserIsVerificationCandidate = createSelector( + selectUserIsRewardEligible, + selectUserIsRewardApproved, + selectEmailToVerify, + selectUser, + (isEligible, isApproved, emailToVerify, user) => + (isEligible && !isApproved) || (emailToVerify && !user.has_email) +); + +export const selectUserIsAuthRequested = createSelector( + selectEmailNewDeclined, + selectAuthenticationIsPending, + selectUserIsVerificationCandidate, + selectUserHasEmail, + (isEmailDeclined, isPending, isVerificationCandidate, hasEmail) => + !isEmailDeclined && (isPending || !hasEmail || isVerificationCandidate) +); diff --git a/ui/scss/component/_card.scss b/ui/scss/component/_card.scss index 2e325d827..4dd88a91e 100644 --- a/ui/scss/component/_card.scss +++ b/ui/scss/component/_card.scss @@ -25,7 +25,7 @@ $padding-card-horizontal: $spacing-vertical * 2/3; } .card__title-primary { padding: 0 $padding-card-horizontal; - margin-top: $spacing-vertical; + margin-top: $spacing-vertical * 2/3; } .card__title-identity { padding: 0 $padding-card-horizontal; From 09ecae7e0dc8ad8c2611e1ffdb518be98167bd88 Mon Sep 17 00:00:00 2001 From: Jeremy Kauffman Date: Thu, 8 Jun 2017 17:15:34 -0400 Subject: [PATCH 09/12] reviewable --- ui/js/actions/content.js | 5 +- ui/js/actions/rewards.js | 106 +++++-- ui/js/actions/user.js | 26 +- ui/js/component/authOverlay/index.jsx | 2 + ui/js/component/authOverlay/view.jsx | 19 +- ui/js/component/form.js | 14 +- ui/js/component/reward-link.js | 109 ------- ui/js/component/rewardLink/index.js | 4 +- ui/js/component/userEmailNew/view.jsx | 56 ++-- ui/js/component/userEmailVerify/index.jsx | 2 +- ui/js/component/userEmailVerify/view.jsx | 63 ++-- ui/js/component/welcomeModal/index.jsx | 14 +- ui/js/component/welcomeModal/view.jsx | 17 +- ui/js/constants/action_types.js | 37 +-- ui/js/lbryio.js | 17 +- ui/js/main.js | 5 +- ui/js/page/fileListPublished/index.js | 4 + ui/js/page/fileListPublished/view.jsx | 19 +- ui/js/page/publish/index.js | 4 + ui/js/page/publish/view.jsx | 4 +- ui/js/page/rewards/index.js | 7 +- ui/js/reducers/rewards.js | 97 +++--- ui/js/reducers/user.js | 26 ++ ui/js/rewards.js | 354 ++++++++++------------ ui/js/selectors/rewards.js | 18 +- ui/js/selectors/user.js | 13 +- ui/scss/_gui.scss | 5 + 27 files changed, 522 insertions(+), 525 deletions(-) delete mode 100644 ui/js/component/reward-link.js diff --git a/ui/js/actions/content.js b/ui/js/actions/content.js index f9fe0b02b..ca174b261 100644 --- a/ui/js/actions/content.js +++ b/ui/js/actions/content.js @@ -2,7 +2,6 @@ import * as types from "constants/action_types"; import lbry from "lbry"; import lbryio from "lbryio"; import lbryuri from "lbryuri"; -import rewards from "rewards"; import { selectBalance } from "selectors/wallet"; import { selectFileInfoForUri, @@ -10,8 +9,8 @@ import { } from "selectors/file_info"; import { selectResolvingUris } from "selectors/content"; import { selectCostInfoForUri } from "selectors/cost_info"; -import { selectClaimsByUri } from "selectors/claims"; import { doOpenModal } from "actions/app"; +import { doClaimEligiblePurchaseRewards } from "actions/rewards"; export function doResolveUri(uri) { return function(dispatch, getState) { @@ -171,7 +170,7 @@ export function doDownloadFile(uri, streamInfo) { }) .catch(() => {}); - rewards.claimEligiblePurchaseRewards(); + dispatch(doClaimEligiblePurchaseRewards()); }; } diff --git a/ui/js/actions/rewards.js b/ui/js/actions/rewards.js index ac368483d..14a2a8253 100644 --- a/ui/js/actions/rewards.js +++ b/ui/js/actions/rewards.js @@ -2,6 +2,7 @@ import * as types from "constants/action_types"; import lbry from "lbry"; import lbryio from "lbryio"; import rewards from "rewards"; +import { selectRewards, selectRewardsByType } from "selectors/rewards"; export function doRewardList() { return function(dispatch, getState) { @@ -11,56 +12,105 @@ export function doRewardList() { type: types.FETCH_REWARDS_STARTED, }); - lbryio.call('reward', 'list', {}).then((userRewards) => { - dispatch({ - type: types.FETCH_REWARDS_COMPLETED, - data: { userRewards } + lbryio + .call("reward", "list", {}) + .then(userRewards => { + dispatch({ + type: types.FETCH_REWARDS_COMPLETED, + data: { userRewards }, + }); }) - }).catch(() => { - dispatch({ - type: types.FETCH_REWARDS_COMPLETED, - data: { userRewards: [] } - }) - }); + .catch(() => { + dispatch({ + type: types.FETCH_REWARDS_COMPLETED, + data: { userRewards: [] }, + }); + }); }; } -export function doClaimReward(reward) { +export function doClaimRewardType(rewardType) { return function(dispatch, getState) { + const rewardsByType = selectRewardsByType(getState()), + reward = rewardsByType[rewardType]; + + if (reward) { + dispatch(doClaimReward(reward)); + } + }; +} + +export function doClaimReward(reward, saveError = false) { + return function(dispatch, getState) { + if (reward.transaction_id) { + //already claimed, do nothing + return; + } + dispatch({ type: types.CLAIM_REWARD_STARTED, - data: { reward } - }) + data: { reward }, + }); - const success = (a) => { - console.log(a) + const success = reward => { dispatch({ type: types.CLAIM_REWARD_SUCCESS, data: { - a - } - }) - } + reward, + }, + }); + }; - const failure = (error) => { + const failure = error => { dispatch({ type: types.CLAIM_REWARD_FAILURE, data: { reward, - error - } - }) + error: saveError ? error : null, + }, + }); + }; + + rewards.claimReward(reward.reward_type).then(success, failure); + }; +} + +export function doClaimEligiblePurchaseRewards() { + return function(dispatch, getState) { + if (!lbryio.enabled || !lbryio.getAccessToken()) { + return; } - rewards.claimReward(reward.reward_type).then(success, failure) - } + const rewardsByType = selectRewardsByType(getState()); + + let types = {}; + + types[rewards.TYPE_FIRST_STREAM] = false; + types[rewards.TYPE_FEATURED_DOWNLOAD] = false; + types[rewards.TYPE_MANY_DOWNLOADS] = false; + Object.values(rewardsByType).forEach(reward => { + if (types[reward.reward_type] === false && reward.transaction_id) { + types[reward.reward_type] = true; + } + }); + + let unclaimedType = Object.keys(types).find(type => { + return types[type] === false && type !== rewards.TYPE_FEATURED_DOWNLOAD; //handled below + }); + if (unclaimedType) { + dispatch(doClaimRewardType(unclaimedType)); + } + if (types[rewards.TYPE_FEATURED_DOWNLOAD] === false) { + dispatch(doClaimRewardType(rewards.TYPE_FEATURED_DOWNLOAD)); + } + }; } export function doClaimRewardClearError(reward) { return function(dispatch, getState) { dispatch({ type: types.CLAIM_REWARD_CLEAR_ERROR, - data: { reward } - }) - } + data: { reward }, + }); + }; } diff --git a/ui/js/actions/user.js b/ui/js/actions/user.js index 3fd4f8782..972e167eb 100644 --- a/ui/js/actions/user.js +++ b/ui/js/actions/user.js @@ -20,8 +20,6 @@ export function doAuthenticate() { dispatch(doRewardList()); //FIXME - where should this happen? }) .catch(error => { - console.log("auth error"); - console.log(error); dispatch({ type: types.AUTHENTICATION_FAILURE, data: { error }, @@ -30,6 +28,28 @@ export function doAuthenticate() { }; } +export function doUserFetch() { + return function(dispatch, getState) { + dispatch({ + type: types.USER_FETCH_STARTED, + }); + lbryio.setCurrentUser( + user => { + dispatch({ + type: types.USER_FETCH_SUCCESS, + data: { user }, + }); + }, + error => { + dispatch({ + type: types.USER_FETCH_FAILURE, + data: { error }, + }); + } + ); + }; +} + export function doUserEmailNew(email) { return function(dispatch, getState) { dispatch({ @@ -42,6 +62,7 @@ export function doUserEmailNew(email) { type: types.USER_EMAIL_NEW_SUCCESS, data: { email }, }); + dispatch(doUserFetch()); }, error => { if ( @@ -102,6 +123,7 @@ export function doUserEmailVerify(verificationToken) { type: types.USER_EMAIL_VERIFY_SUCCESS, data: { email }, }); + dispatch(doUserFetch()); } else { failure(new Error("Your email is still not verified.")); //shouldn't happen? } diff --git a/ui/js/component/authOverlay/index.jsx b/ui/js/component/authOverlay/index.jsx index d3982be73..ec7d7ee5a 100644 --- a/ui/js/component/authOverlay/index.jsx +++ b/ui/js/component/authOverlay/index.jsx @@ -5,11 +5,13 @@ import { doUserEmailDecline } from "actions/user"; import { doOpenModal } from "actions/app"; import { selectAuthenticationIsPending, + selectUserHasEmail, selectUserIsAuthRequested, } from "selectors/user"; import AuthOverlay from "./view"; const select = state => ({ + hasEmail: selectUserHasEmail(state), isPending: selectAuthenticationIsPending(state), isShowing: selectUserIsAuthRequested(state), }); diff --git a/ui/js/component/authOverlay/view.jsx b/ui/js/component/authOverlay/view.jsx index 0f1c95af4..fcb7e77d4 100644 --- a/ui/js/component/authOverlay/view.jsx +++ b/ui/js/component/authOverlay/view.jsx @@ -32,7 +32,7 @@ export class AuthOverlay extends React.Component { return null; } - const { isPending, isShowing } = this.props; + const { isPending, isShowing, hasEmail } = this.props; if (isShowing) { return ( @@ -46,15 +46,14 @@ export class AuthOverlay extends React.Component { {isPending ? "" :
- {this.state.showNoEmailConfirm - ?
-

+ {!hasEmail && this.state.showNoEmailConfirm + ?

+

{__( "If you continue without an email, you will be ineligible to earn free LBC rewards, as well as unable to receive security related communications." )}

{ this.onEmailSkipConfirm(); }} @@ -62,11 +61,15 @@ export class AuthOverlay extends React.Component { />
: { - this.onEmailSkipClick(); + hasEmail + ? this.onEmailSkipConfirm() + : this.onEmailSkipClick(); }} - label={__("Do I have to?")} + label={ + hasEmail ? __("Skip for now") : __("Do I have to?") + } />}
} diff --git a/ui/js/component/form.js b/ui/js/component/form.js index a9c1ab6bd..d0f192fec 100644 --- a/ui/js/component/form.js +++ b/ui/js/component/form.js @@ -178,9 +178,19 @@ export class FormRow extends React.Component { this._fieldRequiredText = __("This field is required"); - this.state = { + this.state = this.getStateFromProps(props); + } + + componentWillReceiveProps(nextProps) { + this.setState(this.getStateFromProps(nextProps)); + } + + getStateFromProps(props) { + return { isError: !!props.errorMessage, - errorMessage: typeof props.errorMessage === "string" ? props.errorMessage : '', + errorMessage: typeof props.errorMessage === "string" + ? props.errorMessage + : "", }; } diff --git a/ui/js/component/reward-link.js b/ui/js/component/reward-link.js deleted file mode 100644 index baf9df753..000000000 --- a/ui/js/component/reward-link.js +++ /dev/null @@ -1,109 +0,0 @@ -import React from "react"; -import lbry from "lbry"; -import { Icon } from "component/common"; -import Modal from "component/modal"; -import rewards from "rewards"; -import Link from "component/link"; - -export class RewardLink extends React.Component { - static propTypes = { - type: React.PropTypes.string.isRequired, - claimed: React.PropTypes.bool, - onRewardClaim: React.PropTypes.func, - onRewardFailure: React.PropTypes.func, - }; - - constructor(props) { - super(props); - - this.state = { - claimable: true, - pending: false, - errorMessage: null, - }; - } - - refreshClaimable() { - switch (this.props.type) { - case "new_user": - this.setState({ claimable: true }); - return; - - case "first_publish": - lbry.claim_list_mine().then(list => { - this.setState({ - claimable: list.length > 0, - }); - }); - return; - } - } - - componentWillMount() { - this.refreshClaimable(); - } - - claimReward() { - this.setState({ - pending: true, - }); - - rewards - .claimReward(this.props.type) - .then(reward => { - this.setState({ - pending: false, - errorMessage: null, - }); - if (this.props.onRewardClaim) { - this.props.onRewardClaim(reward); - } - }) - .catch(error => { - this.setState({ - errorMessage: error.message, - pending: false, - }); - }); - } - - clearError() { - if (this.props.onRewardFailure) { - this.props.onRewardFailure(); - } - this.setState({ - errorMessage: null, - }); - } - - render() { - return ( -
- {this.props.claimed - ? {__("Reward claimed.")} - : { - this.claimReward(); - }} - />} - {this.state.errorMessage - ? { - this.clearError(); - }} - > - {this.state.errorMessage} - - : ""} -
- ); - } -} diff --git a/ui/js/component/rewardLink/index.js b/ui/js/component/rewardLink/index.js index 631e65165..81b01488b 100644 --- a/ui/js/component/rewardLink/index.js +++ b/ui/js/component/rewardLink/index.js @@ -20,14 +20,14 @@ const makeSelect = () => { isClaimed: selectHasClaimedReward(state, props), errorMessage: selectError(state, props), isPending: selectIsPending(state, props), - reward: select, + reward: selectReward(state, props), }); return select; }; const perform = dispatch => ({ - claimReward: reward => dispatch(doClaimReward(reward)), + claimReward: reward => dispatch(doClaimReward(reward, true)), clearError: reward => dispatch(doClaimRewardClearError(reward)), navigate: path => dispatch(doNavigate(path)), }); diff --git a/ui/js/component/userEmailNew/view.jsx b/ui/js/component/userEmailNew/view.jsx index 502ceaa70..7e0f83b47 100644 --- a/ui/js/component/userEmailNew/view.jsx +++ b/ui/js/component/userEmailNew/view.jsx @@ -1,13 +1,13 @@ -import React from 'react'; -import Link from 'component/link'; -import {FormRow} from 'component/form.js'; +import React from "react"; +import Link from "component/link"; +import { FormRow } from "component/form.js"; class UserEmailNew extends React.Component { constructor(props) { super(props); this.state = { - email: '' + email: "", }; } @@ -19,25 +19,43 @@ class UserEmailNew extends React.Component { handleSubmit(event) { event.preventDefault(); - this.props.addUserEmail(this.state.email) + this.props.addUserEmail(this.state.email); } render() { - const { - errorMessage, - isPending - } = this.props + const { errorMessage, isPending } = this.props; - return
{ this.handleSubmit(event) }}> - { this.handleEmailChanged(event) }} /> -
- { this.handleSubmit(event) }} /> -
- + return ( +
{ + this.handleSubmit(event); + }} + > + { + this.handleEmailChanged(event); + }} + /> +
+ { + this.handleSubmit(event); + }} + /> +
+ + ); } } -export default UserEmailNew \ No newline at end of file +export default UserEmailNew; diff --git a/ui/js/component/userEmailVerify/index.jsx b/ui/js/component/userEmailVerify/index.jsx index 090b41400..f2ae56c86 100644 --- a/ui/js/component/userEmailVerify/index.jsx +++ b/ui/js/component/userEmailVerify/index.jsx @@ -10,7 +10,7 @@ import UserEmailVerify from "./view"; const select = state => ({ isPending: selectEmailVerifyIsPending(state), - email: selectEmailToVerify, + email: selectEmailToVerify(state), errorMessage: selectEmailVerifyErrorMessage(state), }); diff --git a/ui/js/component/userEmailVerify/view.jsx b/ui/js/component/userEmailVerify/view.jsx index 9d6160287..4b9272394 100644 --- a/ui/js/component/userEmailVerify/view.jsx +++ b/ui/js/component/userEmailVerify/view.jsx @@ -1,13 +1,13 @@ -import React from 'react'; -import Link from 'component/link'; -import {FormRow} from 'component/form.js'; +import React from "react"; +import Link from "component/link"; +import { FormRow } from "component/form.js"; class UserEmailVerify extends React.Component { constructor(props) { super(props); this.state = { - code: '', + code: "", }; } @@ -19,25 +19,50 @@ class UserEmailVerify extends React.Component { handleSubmit(event) { event.preventDefault(); - this.props.verifyUserEmail(this.state.code) + this.props.verifyUserEmail(this.state.code); } render() { - const { - errorMessage, - isPending - } = this.props + const { errorMessage, isPending } = this.props; - return
{ this.handleSubmit(event) }}> - { this.handleCodeChanged(event) }} - errorMessage={errorMessage} - helper="A verification code is required to participate in early access rewards."/> -
- { this.handleSubmit(event)}} /> -
- + return ( +
{ + this.handleSubmit(event); + }} + > + { + this.handleCodeChanged(event); + }} + errorMessage={errorMessage} + /> + {/* render help separately so it always shows */} +
+

+ Email if + you did not receive or are having trouble with your code. +

+
+
+ { + this.handleSubmit(event); + }} + /> +
+ + ); } } -export default UserEmailVerify \ No newline at end of file +export default UserEmailVerify; diff --git a/ui/js/component/welcomeModal/index.jsx b/ui/js/component/welcomeModal/index.jsx index 6a6af4f64..bbfc5b6a7 100644 --- a/ui/js/component/welcomeModal/index.jsx +++ b/ui/js/component/welcomeModal/index.jsx @@ -1,17 +1,23 @@ import React from "react"; +import rewards from "rewards"; import { connect } from "react-redux"; import { doCloseModal } from "actions/app"; import { selectUserIsRewardApproved } from "selectors/user"; -import { makeSelectHasClaimedReward } from "selectors/rewards"; +import { + makeSelectHasClaimedReward, + makeSelectClaimRewardError, + makeSelectRewardByType, +} from "selectors/rewards"; import WelcomeModal from "./view"; const select = (state, props) => { - const selectHasReward = makeSelectHasClaimedReward(); + const selectHasClaimed = makeSelectHasClaimedReward(), + selectReward = makeSelectRewardByType(); return { - hasReward: selectHasReward(state, { reward_type: "new_user" }), + hasClaimed: selectHasClaimed(state, { reward_type: rewards.TYPE_NEW_USER }), isRewardApproved: selectUserIsRewardApproved(state), - rewardAmount: 5, + reward: selectReward(state, { reward_type: rewards.TYPE_NEW_USER }), }; }; diff --git a/ui/js/component/welcomeModal/view.jsx b/ui/js/component/welcomeModal/view.jsx index 015df4701..5b0f635c5 100644 --- a/ui/js/component/welcomeModal/view.jsx +++ b/ui/js/component/welcomeModal/view.jsx @@ -6,14 +6,12 @@ import RewardLink from "component/rewardLink"; class WelcomeModal extends React.Component { render() { - const { - closeModal, - hasReward, - isRewardApproved, - rewardAmount, - } = this.props; + const { closeModal, hasClaimed, isRewardApproved, reward } = this.props; - return !hasReward + console.log("welcome"); + console.log(this.props); + + return !hasClaimed ?

Welcome to LBRY.

@@ -30,7 +28,7 @@ class WelcomeModal extends React.Component { Thank you for making content freedom possible! {" "}{isRewardApproved ? __("Here's a nickel, kid.") : ""}

-
+
{isRewardApproved ? : About Your Reward

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

diff --git a/ui/js/constants/action_types.js b/ui/js/constants/action_types.js index 647c6e108..3450c1c1a 100644 --- a/ui/js/constants/action_types.js +++ b/ui/js/constants/action_types.js @@ -71,22 +71,25 @@ export const SEARCH_CANCELLED = "SEARCH_CANCELLED"; export const DAEMON_SETTINGS_RECEIVED = "DAEMON_SETTINGS_RECEIVED"; // User -export const AUTHENTICATION_STARTED = 'AUTHENTICATION_STARTED' -export const AUTHENTICATION_SUCCESS = 'AUTHENTICATION_SUCCESS' -export const AUTHENTICATION_FAILURE = 'AUTHENTICATION_FAILURE' -export const USER_EMAIL_DECLINE = 'USER_EMAIL_DECLINE' -export const USER_EMAIL_NEW_STARTED = 'USER_EMAIL_NEW_STARTED' -export const USER_EMAIL_NEW_SUCCESS = 'USER_EMAIL_NEW_SUCCESS' -export const USER_EMAIL_NEW_EXISTS = 'USER_EMAIL_NEW_EXISTS' -export const USER_EMAIL_NEW_FAILURE = 'USER_EMAIL_NEW_FAILURE' -export const USER_EMAIL_VERIFY_STARTED = 'USER_EMAIL_VERIFY_STARTED' -export const USER_EMAIL_VERIFY_SUCCESS = 'USER_EMAIL_VERIFY_SUCCESS' -export const USER_EMAIL_VERIFY_FAILURE = 'USER_EMAIL_VERIFY_FAILURE' +export const AUTHENTICATION_STARTED = "AUTHENTICATION_STARTED"; +export const AUTHENTICATION_SUCCESS = "AUTHENTICATION_SUCCESS"; +export const AUTHENTICATION_FAILURE = "AUTHENTICATION_FAILURE"; +export const USER_EMAIL_DECLINE = "USER_EMAIL_DECLINE"; +export const USER_EMAIL_NEW_STARTED = "USER_EMAIL_NEW_STARTED"; +export const USER_EMAIL_NEW_SUCCESS = "USER_EMAIL_NEW_SUCCESS"; +export const USER_EMAIL_NEW_EXISTS = "USER_EMAIL_NEW_EXISTS"; +export const USER_EMAIL_NEW_FAILURE = "USER_EMAIL_NEW_FAILURE"; +export const USER_EMAIL_VERIFY_STARTED = "USER_EMAIL_VERIFY_STARTED"; +export const USER_EMAIL_VERIFY_SUCCESS = "USER_EMAIL_VERIFY_SUCCESS"; +export const USER_EMAIL_VERIFY_FAILURE = "USER_EMAIL_VERIFY_FAILURE"; +export const USER_FETCH_STARTED = "USER_FETCH_STARTED"; +export const USER_FETCH_SUCCESS = "USER_FETCH_SUCCESS"; +export const USER_FETCH_FAILURE = "USER_FETCH_FAILURE"; // Rewards -export const FETCH_REWARDS_STARTED = 'FETCH_REWARDS_STARTED' -export const FETCH_REWARDS_COMPLETED = 'FETCH_REWARDS_COMPLETED' -export const CLAIM_REWARD_STARTED = 'CLAIM_REWARD_STARTED' -export const CLAIM_REWARD_SUCCESS = 'CLAIM_REWARD_SUCCESS' -export const CLAIM_REWARD_FAILURE = 'CLAIM_REWARD_FAILURE' -export const CLAIM_REWARD_CLEAR_ERROR = 'CLAIM_REWARD_CLEAR_ERROR' +export const FETCH_REWARDS_STARTED = "FETCH_REWARDS_STARTED"; +export const FETCH_REWARDS_COMPLETED = "FETCH_REWARDS_COMPLETED"; +export const CLAIM_REWARD_STARTED = "CLAIM_REWARD_STARTED"; +export const CLAIM_REWARD_SUCCESS = "CLAIM_REWARD_SUCCESS"; +export const CLAIM_REWARD_FAILURE = "CLAIM_REWARD_FAILURE"; +export const CLAIM_REWARD_CLEAR_ERROR = "CLAIM_REWARD_CLEAR_ERROR"; diff --git a/ui/js/lbryio.js b/ui/js/lbryio.js index 1ca8f76f1..11416fbd6 100644 --- a/ui/js/lbryio.js +++ b/ui/js/lbryio.js @@ -6,7 +6,6 @@ const querystring = require("querystring"); const lbryio = { _accessToken: getSession("accessToken"), _authenticationPromise: null, - _user: null, enabled: true, }; @@ -36,20 +35,9 @@ lbryio.getExchangeRates = function() { return lbryio._exchangePromise; }; -lbryio.call = function( - resource, - action, - params = {}, - method = "get", - evenIfDisabled = false -) { - // evenIfDisabled is just for development, when we may have some calls working and some not +lbryio.call = function(resource, action, params = {}, method = "get") { return new Promise((resolve, reject) => { - if ( - !lbryio.enabled && - !evenIfDisabled && - (resource != "discover" || action != "list") - ) { + if (!lbryio.enabled && (resource != "discover" || action != "list")) { console.log(__("Internal API disabled")); reject(new Error(__("LBRY internal API is disabled"))); return; @@ -135,7 +123,6 @@ lbryio.setCurrentUser = (resolve, reject) => { lbryio .call("user", "me") .then(data => { - lbryio.user = data; resolve(data); }) .catch(function(err) { diff --git a/ui/js/main.js b/ui/js/main.js index c8b9e76fc..f600843bf 100644 --- a/ui/js/main.js +++ b/ui/js/main.js @@ -51,7 +51,10 @@ ipcRenderer.on("open-uri-requested", (event, uri) => { document.addEventListener("click", event => { var target = event.target; while (target && target !== document) { - if (target.matches('a[href^="http"]')) { + if ( + target.matches('a[href^="http"]') || + target.matches('a[href^="mailto"]') + ) { event.preventDefault(); shell.openExternal(target.href); return; diff --git a/ui/js/page/fileListPublished/index.js b/ui/js/page/fileListPublished/index.js index 41ef8a73f..948b81c7e 100644 --- a/ui/js/page/fileListPublished/index.js +++ b/ui/js/page/fileListPublished/index.js @@ -1,10 +1,12 @@ import React from "react"; +import rewards from "rewards"; import { connect } from "react-redux"; import { doFetchFileInfosAndPublishedClaims } from "actions/file_info"; import { selectFileInfosPublished, selectFileListDownloadedOrPublishedIsPending, } from "selectors/file_info"; +import { doClaimRewardType } from "actions/rewards"; import { doNavigate } from "actions/app"; import FileListPublished from "./view"; @@ -16,6 +18,8 @@ const select = state => ({ const perform = dispatch => ({ navigate: path => dispatch(doNavigate(path)), fetchFileListPublished: () => dispatch(doFetchFileInfosAndPublishedClaims()), + claimFirstPublishReward: () => + dispatch(doClaimRewardType(rewards.TYPE_FIRST_PUBLISH)), }); export default connect(select, perform)(FileListPublished); diff --git a/ui/js/page/fileListPublished/view.jsx b/ui/js/page/fileListPublished/view.jsx index e4dc5f3e9..5bf334cf3 100644 --- a/ui/js/page/fileListPublished/view.jsx +++ b/ui/js/page/fileListPublished/view.jsx @@ -16,24 +16,7 @@ class FileListPublished extends React.Component { } componentDidUpdate() { - if (this.props.fileInfos.length > 0) this._requestPublishReward(); - } - - _requestPublishReward() { - // TODO this is throwing an error now - // Error: LBRY internal API is disabled - // - // lbryio.call('reward', 'list', {}).then(function(userRewards) { - // //already rewarded - // if (userRewards.filter(function (reward) { - // return reward.reward_type == rewards.TYPE_FIRST_PUBLISH && reward.transaction_id - // }).length) { - // return - // } - // else { - // rewards.claimReward(rewards.TYPE_FIRST_PUBLISH).catch(() => {}) - // } - // }) + if (this.props.fileInfos.length > 0) this.props.claimFirstPublishReward(); } render() { diff --git a/ui/js/page/publish/index.js b/ui/js/page/publish/index.js index 4b20b9032..abe8137df 100644 --- a/ui/js/page/publish/index.js +++ b/ui/js/page/publish/index.js @@ -1,7 +1,9 @@ import React from "react"; import { connect } from "react-redux"; import { doNavigate, doHistoryBack } from "actions/app"; +import { doClaimRewardType } from "actions/rewards"; import { selectMyClaims } from "selectors/claims"; +import rewards from "rewards"; import PublishPage from "./view"; const select = state => ({ @@ -11,6 +13,8 @@ const select = state => ({ const perform = dispatch => ({ back: () => dispatch(doHistoryBack()), navigate: path => dispatch(doNavigate(path)), + claimFirstChannelReward: () => + dispatch(doClaimRewardType(rewards.TYPE_FIRST_CHANNEL)), }); export default connect(select, perform)(PublishPage); diff --git a/ui/js/page/publish/view.jsx b/ui/js/page/publish/view.jsx index 19e1e2f25..80fac5ec9 100644 --- a/ui/js/page/publish/view.jsx +++ b/ui/js/page/publish/view.jsx @@ -44,7 +44,7 @@ class PublishPage extends React.Component { // Calls API to update displayed list of channels. If a channel name is provided, will select // that channel at the same time (used immediately after creating a channel) lbry.channel_list_mine().then(channels => { - rewards.claimReward(rewards.TYPE_FIRST_CHANNEL).then(() => {}, () => {}); + this.props.claimFirstChannelReward(); this.setState({ channels: channels, ...(channel ? { channel } : {}), @@ -373,7 +373,7 @@ class PublishPage extends React.Component { lbry .channel_new({ channel_name: newChannelName, - amount: parseInt(this.state.newChannelBid), + amount: parseFloat(this.state.newChannelBid), }) .then( () => { diff --git a/ui/js/page/rewards/index.js b/ui/js/page/rewards/index.js index 51da2f8df..61d0515e9 100644 --- a/ui/js/page/rewards/index.js +++ b/ui/js/page/rewards/index.js @@ -1,15 +1,10 @@ import React from "react"; import { connect } from "react-redux"; import { doNavigate } from "actions/app"; -import { - selectFetchingRewards, - selectIsRewardEligible, - selectRewards, -} from "selectors/rewards"; +import { selectFetchingRewards, selectRewards } from "selectors/rewards"; import { selectUserIsRewardEligible, selectUserHasEmail, - selectUserIsRewardApproved, selectUserIsVerificationCandidate, } from "selectors/user"; import RewardsPage from "./view"; diff --git a/ui/js/reducers/rewards.js b/ui/js/reducers/rewards.js index dd56df320..0994d730b 100644 --- a/ui/js/reducers/rewards.js +++ b/ui/js/reducers/rewards.js @@ -1,87 +1,90 @@ import * as types from "constants/action_types"; -const reducers = {} +const reducers = {}; const defaultState = { fetching: false, + rewardsByType: {}, claimPendingByType: {}, - claimErrorsByType: {} + claimErrorsByType: {}, }; reducers[types.FETCH_REWARDS_STARTED] = function(state, action) { - const newRewards = Object.assign({}, state.rewards, { + return Object.assign({}, state, { fetching: true, - }) - - return Object.assign({}, state, newRewards) -} + }); +}; reducers[types.FETCH_REWARDS_COMPLETED] = function(state, action) { - const { - userRewards, - } = action.data + const { userRewards } = action.data; - const byRewardType = {} - userRewards.forEach(reward => byRewardType[reward.reward_type] = reward) - const newRewards = Object.assign({}, state.rewards, { - byRewardType: byRewardType, - fetching: false - }) + const rewardsByType = {}; + userRewards.forEach(reward => (rewardsByType[reward.reward_type] = reward)); - return Object.assign({}, state, newRewards) -} + return Object.assign({}, state, { + rewardsByType: rewardsByType, + fetching: false, + }); +}; -function setClaimRewardState(state, reward, isClaiming, errorMessage="") { - const newClaimPendingByType = Object.assign({}, state.claimPendingByType) - const newClaimErrorsByType = Object.assign({}, state.claimErrorsByType) +function setClaimRewardState(state, reward, isClaiming, errorMessage = "") { + const newClaimPendingByType = Object.assign({}, state.claimPendingByType); + const newClaimErrorsByType = Object.assign({}, state.claimErrorsByType); if (isClaiming) { - newClaimPendingByType[reward.reward_type] = isClaiming + newClaimPendingByType[reward.reward_type] = isClaiming; } else { - delete newClaimPendingByType[reward.reward_type] + delete newClaimPendingByType[reward.reward_type]; } if (errorMessage) { - newClaimErrorsByType[reward.reward_type] = errorMessage + newClaimErrorsByType[reward.reward_type] = errorMessage; } else { - delete newClaimErrorsByType[reward.reward_type] + delete newClaimErrorsByType[reward.reward_type]; } return Object.assign({}, state, { claimPendingByType: newClaimPendingByType, claimErrorsByType: newClaimErrorsByType, - }) + }); } reducers[types.CLAIM_REWARD_STARTED] = function(state, action) { - const { - reward, - } = action.data + const { reward } = action.data; - return setClaimRewardState(state, reward, true, "") -} + return setClaimRewardState(state, reward, true, ""); +}; reducers[types.CLAIM_REWARD_SUCCESS] = function(state, action) { - const { - reward, - } = action.data + const { reward } = action.data; - return setClaimRewardState(state, reward, false, "") -} + const existingReward = state.rewardsByType[reward.reward_type]; + const newReward = Object.assign({}, reward, { + reward_title: existingReward.reward_title, + reward_description: existingReward.reward_description, + }); + const rewardsByType = Object.assign({}, state.rewardsByType); + + rewardsByType[reward.reward_type] = newReward; + + const newState = Object.assign({}, state, { rewardsByType }); + + return setClaimRewardState(newState, newReward, false, ""); +}; reducers[types.CLAIM_REWARD_FAILURE] = function(state, action) { - const { - reward, - error - } = action.data + const { reward, error } = action.data; - return setClaimRewardState(state, reward, false, error.message) -} + return setClaimRewardState(state, reward, false, error ? error.message : ""); +}; reducers[types.CLAIM_REWARD_CLEAR_ERROR] = function(state, action) { - const { - reward - } = action.data + const { reward } = action.data; - return setClaimRewardState(state, reward, state.claimPendingByType[reward.reward_type], "") -} + return setClaimRewardState( + state, + reward, + state.claimPendingByType[reward.reward_type], + "" + ); +}; export default function reducer(state = defaultState, action) { const handler = reducers[action.type]; diff --git a/ui/js/reducers/user.js b/ui/js/reducers/user.js index 353b9b193..4e4ffda40 100644 --- a/ui/js/reducers/user.js +++ b/ui/js/reducers/user.js @@ -5,6 +5,7 @@ const reducers = {}; const defaultState = { authenticationIsPending: false, + userIsPending: false, emailNewIsPending: false, emailNewErrorMessage: "", emailNewDeclined: getLocal("user_email_declined", false), @@ -15,12 +16,15 @@ const defaultState = { reducers[types.AUTHENTICATION_STARTED] = function(state, action) { return Object.assign({}, state, { authenticationIsPending: true, + userIsPending: true, + user: defaultState.user, }); }; reducers[types.AUTHENTICATION_SUCCESS] = function(state, action) { return Object.assign({}, state, { authenticationIsPending: false, + userIsPending: false, user: action.data.user, }); }; @@ -28,6 +32,28 @@ reducers[types.AUTHENTICATION_SUCCESS] = function(state, action) { reducers[types.AUTHENTICATION_FAILURE] = function(state, action) { return Object.assign({}, state, { authenticationIsPending: false, + userIsPending: false, + user: null, + }); +}; + +reducers[types.USER_FETCH_STARTED] = function(state, action) { + return Object.assign({}, state, { + userIsPending: true, + user: defaultState.user, + }); +}; + +reducers[types.USER_FETCH_SUCCESS] = function(state, action) { + return Object.assign({}, state, { + userIsPending: false, + user: action.data.user, + }); +}; + +reducers[types.USER_FETCH_FAILURE] = function(state, action) { + return Object.assign({}, state, { + userIsPending: true, user: null, }); }; diff --git a/ui/js/rewards.js b/ui/js/rewards.js index 33d5547bc..08d82fd9b 100644 --- a/ui/js/rewards.js +++ b/ui/js/rewards.js @@ -1,225 +1,191 @@ -const hashes = require('jshashes'); -import lbry from 'lbry'; -import lbryio from 'lbryio'; -import { doShowSnackBar } from 'actions/app'; +const hashes = require("jshashes"); +import lbry from "lbry"; +import lbryio from "lbryio"; +import { doShowSnackBar } from "actions/app"; function rewardMessage(type, amount) { - return { - new_developer: __( - 'You earned %s for registering as a new developer.', - amount - ), - new_user: __('You earned %s LBC new user reward.', amount), - confirm_email: __( - 'You earned %s LBC for verifying your email address.', - amount - ), - new_channel: __( - 'You earned %s LBC for creating a publisher identity.', - amount - ), - first_stream: __( - 'You earned %s LBC for streaming your first video.', - amount - ), - many_downloads: __( - 'You earned %s LBC for downloading some of the things.', - amount - ), - first_publish: __( - 'You earned %s LBC for making your first publication.', - amount - ) - }[type]; + return { + new_developer: __( + "You earned %s for registering as a new developer.", + amount + ), + new_user: __("You earned %s LBC new user reward.", amount), + confirm_email: __( + "You earned %s LBC for verifying your email address.", + amount + ), + new_channel: __( + "You earned %s LBC for creating a publisher identity.", + amount + ), + first_stream: __( + "You earned %s LBC for streaming your first video.", + amount + ), + many_downloads: __( + "You earned %s LBC for downloading some of the things.", + amount + ), + first_publish: __( + "You earned %s LBC for making your first publication.", + amount + ), + }[type]; } function toHex(s) { - let h = ''; - for (var i = 0; i < s.length; i++) { - let c = s.charCodeAt(i).toString(16); - if (c.length < 2) { - c = '0'.concat(c); - } - h += c; - } - return h; + let h = ""; + for (var i = 0; i < s.length; i++) { + let c = s.charCodeAt(i).toString(16); + if (c.length < 2) { + c = "0".concat(c); + } + h += c; + } + return h; } function fromHex(h) { - let s = ''; - for (let i = 0; i < h.length; i += 2) { - s += String.fromCharCode(parseInt(h.substr(i, 2), 16)); - } - return s; + let s = ""; + for (let i = 0; i < h.length; i += 2) { + s += String.fromCharCode(parseInt(h.substr(i, 2), 16)); + } + return s; } function reverseString(s) { - let o = ''; - for (let i = s.length - 1; i >= 0; i--) { - o += s[i]; - } - return o; + let o = ""; + for (let i = s.length - 1; i >= 0; i--) { + o += s[i]; + } + return o; } function pack(num) { - return ( - '' + - String.fromCharCode(num & 0xff) + - String.fromCharCode((num >> 8) & 0xff) + - String.fromCharCode((num >> 16) & 0xff) + - String.fromCharCode((num >> 24) & 0xff) - ); + return ( + "" + + String.fromCharCode(num & 0xff) + + String.fromCharCode((num >> 8) & 0xff) + + String.fromCharCode((num >> 16) & 0xff) + + String.fromCharCode((num >> 24) & 0xff) + ); } // Returns true if claim is an initial claim, false if it's an update to an existing claim function isInitialClaim(claim) { - const reversed = reverseString(fromHex(claim.txid)); - const concat = reversed.concat(pack(claim.nout)); - const sha256 = new hashes.SHA256({ utf8: false }).raw(concat); - const ripemd160 = new hashes.RMD160({ utf8: false }).raw(sha256); - const hash = toHex(reverseString(ripemd160)); - return hash == claim.claim_id; + const reversed = reverseString(fromHex(claim.txid)); + const concat = reversed.concat(pack(claim.nout)); + const sha256 = new hashes.SHA256({ utf8: false }).raw(concat); + const ripemd160 = new hashes.RMD160({ utf8: false }).raw(sha256); + const hash = toHex(reverseString(ripemd160)); + return hash == claim.claim_id; } const rewards = {}; -(rewards.TYPE_NEW_DEVELOPER = 'new_developer'), (rewards.TYPE_NEW_USER = - 'new_user'), (rewards.TYPE_CONFIRM_EMAIL = - 'confirm_email'), (rewards.TYPE_FIRST_CHANNEL = - 'new_channel'), (rewards.TYPE_FIRST_STREAM = - 'first_stream'), (rewards.TYPE_MANY_DOWNLOADS = - 'many_downloads'), (rewards.TYPE_FIRST_PUBLISH = 'first_publish'); -rewards.TYPE_FEATURED_DOWNLOAD = 'featured_download'; +(rewards.TYPE_NEW_DEVELOPER = "new_developer"), (rewards.TYPE_NEW_USER = + "new_user"), (rewards.TYPE_CONFIRM_EMAIL = + "confirm_email"), (rewards.TYPE_FIRST_CHANNEL = + "new_channel"), (rewards.TYPE_FIRST_STREAM = + "first_stream"), (rewards.TYPE_MANY_DOWNLOADS = + "many_downloads"), (rewards.TYPE_FIRST_PUBLISH = "first_publish"); +rewards.TYPE_FEATURED_DOWNLOAD = "featured_download"; rewards.claimReward = function(type) { - function requestReward(resolve, reject, params) { - if (!lbryio.enabled || !lbryio.getAccessToken()) { - reject(new Error(__('Rewards are not enabled.'))); - return; - } - lbryio.call('reward', 'new', params, 'post').then(({ reward_amount }) => { - const message = rewardMessage(type, reward_amount), - result = { - type: type, - amount: reward_amount, - message: message - }; + function requestReward(resolve, reject, params) { + if (!lbryio.enabled || !lbryio.getAccessToken()) { + reject(new Error(__("Rewards are not enabled."))); + return; + } + lbryio.call("reward", "new", params, "post").then(reward => { + const message = rewardMessage(type, reward.reward_amount); - // Display global notice - const action = doShowSnackBar({ - message, - linkText: __('Show All'), - linkTarget: '/rewards', - isError: false - }); - window.app.store.dispatch(action); + // Display global notice + const action = doShowSnackBar({ + message, + linkText: __("Show All"), + linkTarget: "/rewards", + isError: false, + }); + window.app.store.dispatch(action); - // Add more events here to display other places + // Add more events here to display other places - resolve(result); - }, reject); - } - - return new Promise((resolve, reject) => { - lbry.wallet_unused_address().then(address => { - const params = { - reward_type: type, - wallet_address: address - }; - - switch (type) { - case rewards.TYPE_FIRST_CHANNEL: - lbry - .claim_list_mine() - .then(function(claims) { - let claim = claims.find(function(claim) { - return ( - claim.name.length && - claim.name[0] == '@' && - claim.txid.length && - isInitialClaim(claim) - ); - }); - if (claim) { - params.transaction_id = claim.txid; - requestReward(resolve, reject, params); - } else { - reject( - new Error(__('Please create a channel identity first.')) - ); - } - }) - .catch(reject); - break; - - case rewards.TYPE_FIRST_PUBLISH: - lbry - .claim_list_mine() - .then(claims => { - let claim = claims.find(function(claim) { - return ( - claim.name.length && - claim.name[0] != '@' && - claim.txid.length && - isInitialClaim(claim) - ); - }); - if (claim) { - params.transaction_id = claim.txid; - requestReward(resolve, reject, params); - } else { - reject( - claims.length - ? new Error( - __( - 'Please publish something and wait for confirmation by the network to claim this reward.' - ) - ) - : new Error( - __('Please publish something to claim this reward.') - ) - ); - } - }) - .catch(reject); - break; - - case rewards.TYPE_FIRST_STREAM: - case rewards.TYPE_NEW_USER: - default: - requestReward(resolve, reject, params); - } - }); - }); -}; - -rewards.claimEligiblePurchaseRewards = () => { - if (!lbryio.enabled || !lbryio.getAccessToken()) { - return; + resolve(reward); + }, reject); } - let types = {}; - types[rewards.TYPE_FIRST_STREAM] = false; - types[rewards.TYPE_FEATURED_DOWNLOAD] = false; - types[rewards.TYPE_MANY_DOWNLOADS] = false; - lbryio.call('reward', 'list', {}).then( - userRewards => { - userRewards.forEach(reward => { - if (types[reward.reward_type] === false && reward.transaction_id) { - types[reward.reward_type] = true; - } - }); - let unclaimedType = Object.keys(types).find(type => { - return types[type] === false && type !== rewards.TYPE_FEATURED_DOWNLOAD; //handled below - }); - if (unclaimedType) { - rewards.claimReward(unclaimedType); - } - if (types[rewards.TYPE_FEATURED_DOWNLOAD] === false) { - rewards.claimReward(rewards.TYPE_FEATURED_DOWNLOAD); - } - }, - () => {} - ); + + return new Promise((resolve, reject) => { + lbry.wallet_unused_address().then(address => { + const params = { + reward_type: type, + wallet_address: address, + }; + + switch (type) { + case rewards.TYPE_FIRST_CHANNEL: + lbry + .claim_list_mine() + .then(function(claims) { + let claim = claims.reverse().find(function(claim) { + return ( + claim.name.length && + claim.name[0] == "@" && + claim.txid.length && + isInitialClaim(claim) + ); + }); + if (claim) { + params.transaction_id = claim.txid; + requestReward(resolve, reject, params); + } else { + reject( + new Error(__("Please create a channel identity first.")) + ); + } + }) + .catch(reject); + break; + + case rewards.TYPE_FIRST_PUBLISH: + lbry + .claim_list_mine() + .then(claims => { + let claim = claims.reverse().find(function(claim) { + return ( + claim.name.length && + claim.name[0] != "@" && + claim.txid.length && + isInitialClaim(claim) + ); + }); + if (claim) { + params.transaction_id = claim.txid; + requestReward(resolve, reject, params); + } else { + reject( + claims.length + ? new Error( + __( + "Please publish something and wait for confirmation by the network to claim this reward." + ) + ) + : new Error( + __("Please publish something to claim this reward.") + ) + ); + } + }) + .catch(reject); + break; + + case rewards.TYPE_FIRST_STREAM: + case rewards.TYPE_NEW_USER: + default: + requestReward(resolve, reject, params); + } + }); + }); }; export default rewards; diff --git a/ui/js/selectors/rewards.js b/ui/js/selectors/rewards.js index d9e17f927..7a6a20429 100644 --- a/ui/js/selectors/rewards.js +++ b/ui/js/selectors/rewards.js @@ -5,7 +5,7 @@ const _selectState = state => state.rewards || {}; export const selectRewardsByType = createSelector( _selectState, - state => state.byRewardType || {} + state => state.rewardsByType || {} ); export const selectRewards = createSelector( @@ -18,26 +18,14 @@ export const selectIsRewardEligible = createSelector( user => user.can_claim_rewards ); -export const selectClaimedRewards = createSelector(selectRewards, rewards => - rewards.filter(reward => reward.transaction_id !== "") -); - -export const selectClaimedRewardsByType = createSelector( - selectClaimedRewards, - claimedRewards => { - const byType = {}; - claimedRewards.forEach(reward => (byType[reward.reward_type] = reward)); - return byType; - } -); - export const selectFetchingRewards = createSelector( _selectState, state => !!state.fetching ); export const selectHasClaimedReward = (state, props) => { - return !!selectClaimedRewardsByType[props.reward_type]; + const reward = selectRewardsByType(state)[props.reward_type]; + return reward && reward.transaction_id !== ""; }; export const makeSelectHasClaimedReward = () => { diff --git a/ui/js/selectors/user.js b/ui/js/selectors/user.js index 015ebae62..f7104d3d4 100644 --- a/ui/js/selectors/user.js +++ b/ui/js/selectors/user.js @@ -7,6 +7,11 @@ export const selectAuthenticationIsPending = createSelector( state => state.authenticationIsPending ); +export const selectUserIsPending = createSelector( + _selectState, + state => state.userIsPending +); + export const selectUser = createSelector( _selectState, state => state.user || {} @@ -20,17 +25,17 @@ export const selectEmailToVerify = createSelector( export const selectUserHasEmail = createSelector( selectUser, selectEmailToVerify, - (user, email) => user.has_email || email + (user, email) => (user && user.has_email) || email ); export const selectUserIsRewardEligible = createSelector( selectUser, - user => user.is_reward_eligible + user => user && user.is_reward_eligible ); export const selectUserIsRewardApproved = createSelector( selectUser, - user => user.is_reward_approved + user => user && user.is_reward_approved ); export const selectEmailNewIsPending = createSelector( @@ -64,7 +69,7 @@ export const selectUserIsVerificationCandidate = createSelector( selectEmailToVerify, selectUser, (isEligible, isApproved, emailToVerify, user) => - (isEligible && !isApproved) || (emailToVerify && !user.has_email) + (isEligible && !isApproved) || (emailToVerify && user && !user.has_email) ); export const selectUserIsAuthRequested = createSelector( diff --git a/ui/scss/_gui.scss b/ui/scss/_gui.scss index f4ededba0..a902fe2da 100644 --- a/ui/scss/_gui.scss +++ b/ui/scss/_gui.scss @@ -165,3 +165,8 @@ p section.section-spaced { margin-bottom: $spacing-vertical; } + +.text-center +{ + text-align: center; +} From 1a6a69914c548f5ac9f4cccdab158248d2f6bb3e Mon Sep 17 00:00:00 2001 From: Jeremy Kauffman Date: Thu, 8 Jun 2017 17:19:02 -0400 Subject: [PATCH 10/12] changelog and bugfix --- CHANGELOG.md | 3 ++- app/main.js | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c5c1bb1dc..8197e7035 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,7 +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. ### Changed * All UI strings are now rendered according to gettext standard, in prep for i18n @@ -20,6 +20,7 @@ Web UI version numbers should always match the corresponding version of LBRY App * Updated deprecated LBRY API call signatures * App scrolls to the top of the page on navigation * Download progress works properly for purchased but deleted files + * Publish channels for less than 1 LBC ### Deprecated * diff --git a/app/main.js b/app/main.js index 14ead8e95..85a0dd3c7 100644 --- a/app/main.js +++ b/app/main.js @@ -1,6 +1,6 @@ const {app, BrowserWindow, ipcMain} = require('electron'); const url = require('url'); -const isDebug = process.env.NODE_ENV === 'development' || true +const isDebug = process.env.NODE_ENV === 'development' if (isDebug) { require('electron-debug')({showDevTools: true}); From 9418e780461b6d0036d72e9e3ab10dbaa91382fc Mon Sep 17 00:00:00 2001 From: Jeremy Kauffman Date: Thu, 8 Jun 2017 20:10:53 -0400 Subject: [PATCH 11/12] restore purecomponent, remove console.log --- ui/js/component/auth/view.jsx | 2 +- ui/js/component/authOverlay/view.jsx | 2 +- ui/js/component/userEmailNew/view.jsx | 2 +- ui/js/component/userEmailVerify/view.jsx | 2 +- ui/js/component/welcomeModal/view.jsx | 5 +---- ui/js/page/rewards/view.jsx | 2 -- 6 files changed, 5 insertions(+), 10 deletions(-) diff --git a/ui/js/component/auth/view.jsx b/ui/js/component/auth/view.jsx index d796e1448..551113ffa 100644 --- a/ui/js/component/auth/view.jsx +++ b/ui/js/component/auth/view.jsx @@ -3,7 +3,7 @@ import { BusyMessage } from "component/common"; import UserEmailNew from "component/userEmailNew"; import UserEmailVerify from "component/userEmailVerify"; -export class Auth extends React.Component { +export class Auth extends React.PureComponent { render() { const { isPending, email, isVerificationCandidate } = this.props; diff --git a/ui/js/component/authOverlay/view.jsx b/ui/js/component/authOverlay/view.jsx index fcb7e77d4..0cce29f9b 100644 --- a/ui/js/component/authOverlay/view.jsx +++ b/ui/js/component/authOverlay/view.jsx @@ -4,7 +4,7 @@ import ModalPage from "component/modal-page.js"; import Auth from "component/auth"; import Link from "component/link"; -export class AuthOverlay extends React.Component { +export class AuthOverlay extends React.PureComponent { constructor(props) { super(props); diff --git a/ui/js/component/userEmailNew/view.jsx b/ui/js/component/userEmailNew/view.jsx index 7e0f83b47..5391bdb3f 100644 --- a/ui/js/component/userEmailNew/view.jsx +++ b/ui/js/component/userEmailNew/view.jsx @@ -2,7 +2,7 @@ import React from "react"; import Link from "component/link"; import { FormRow } from "component/form.js"; -class UserEmailNew extends React.Component { +class UserEmailNew extends React.PureComponent { constructor(props) { super(props); diff --git a/ui/js/component/userEmailVerify/view.jsx b/ui/js/component/userEmailVerify/view.jsx index 4b9272394..c6cc65f34 100644 --- a/ui/js/component/userEmailVerify/view.jsx +++ b/ui/js/component/userEmailVerify/view.jsx @@ -2,7 +2,7 @@ import React from "react"; import Link from "component/link"; import { FormRow } from "component/form.js"; -class UserEmailVerify extends React.Component { +class UserEmailVerify extends React.PureComponent { constructor(props) { super(props); diff --git a/ui/js/component/welcomeModal/view.jsx b/ui/js/component/welcomeModal/view.jsx index 5b0f635c5..13e0cd5f1 100644 --- a/ui/js/component/welcomeModal/view.jsx +++ b/ui/js/component/welcomeModal/view.jsx @@ -4,13 +4,10 @@ import { CreditAmount } from "component/common"; import Link from "component/link"; import RewardLink from "component/rewardLink"; -class WelcomeModal extends React.Component { +class WelcomeModal extends React.PureComponent { render() { const { closeModal, hasClaimed, isRewardApproved, reward } = this.props; - console.log("welcome"); - console.log(this.props); - return !hasClaimed ?

diff --git a/ui/js/page/rewards/view.jsx b/ui/js/page/rewards/view.jsx index a40afefff..6f78a6490 100644 --- a/ui/js/page/rewards/view.jsx +++ b/ui/js/page/rewards/view.jsx @@ -38,8 +38,6 @@ const RewardsPage = props => { rewards, } = props; - console.log(props); - let content, isCard = false; From 20fa2bb0e528eb2ffa98d0d56adebbedd24cf047 Mon Sep 17 00:00:00 2001 From: Jeremy Kauffman Date: Thu, 8 Jun 2017 20:11:51 -0400 Subject: [PATCH 12/12] bump daemon version --- build/DAEMON_URL | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build/DAEMON_URL b/build/DAEMON_URL index efb0b928e..188944dec 100644 --- a/build/DAEMON_URL +++ b/build/DAEMON_URL @@ -1 +1 @@ -https://github.com/lbryio/lbry/releases/download/v0.11.0rc1/lbrynet-daemon-v0.11.0rc1-OSNAME.zip +https://github.com/lbryio/lbry/releases/download/v0.11.0rc3/lbrynet-daemon-v0.11.0rc3-OSNAME.zip