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