diff --git a/.flowconfig b/.flowconfig index ffc632dcf..c2bb89028 100644 --- a/.flowconfig +++ b/.flowconfig @@ -17,10 +17,10 @@ module.name_mapper='^types\(.*\)$' -> '/src/renderer/types\1' module.name_mapper='^component\(.*\)$' -> '/src/renderer/component\1' module.name_mapper='^page\(.*\)$' -> '/src/renderer/page\1' module.name_mapper='^lbry\(.*\)$' -> '/src/renderer/lbry\1' +module.name_mapper='^rewards\(.*\)$' -> '/src/renderer/rewards\1' module.name_mapper='^modal\(.*\)$' -> '/src/renderer/modal\1' module.name_mapper='^app\(.*\)$' -> '/src/renderer/app\1' module.name_mapper='^native\(.*\)$' -> '/src/renderer/native\1' module.name_mapper='^analytics\(.*\)$' -> '/src/renderer/analytics\1' - [strict] diff --git a/package.json b/package.json index 914d5066c..00a4053ac 100644 --- a/package.json +++ b/package.json @@ -50,8 +50,7 @@ "formik": "^0.10.4", "hast-util-sanitize": "^1.1.2", "keytar": "^4.2.1", - "lbry-redux": "lbryio/lbry-redux#fa141b9175500b874bb77da0258e18d902e069b9", - "lbryinc": "lbryio/lbryinc#c09aa2645ecccb33c83e9a9545ff119232910f6f", + "lbry-redux": "lbryio/lbry-redux#d1cee82af119c0c5f98ec27f94b2e7f61e34b54c", "localforage": "^1.7.1", "mammoth": "^1.4.6", "mime": "^2.3.1", diff --git a/src/renderer/analytics.js b/src/renderer/analytics.js index 68454249c..7ab84e448 100644 --- a/src/renderer/analytics.js +++ b/src/renderer/analytics.js @@ -1,6 +1,6 @@ // @flow import mixpanel from 'mixpanel-browser'; -import { Lbryio } from 'lbryinc'; +import Lbryio from 'lbryio'; import isDev from 'electron-is-dev'; if (isDev) { diff --git a/src/renderer/app.js b/src/renderer/app.js index 6d93deb25..9933b47eb 100644 --- a/src/renderer/app.js +++ b/src/renderer/app.js @@ -38,7 +38,4 @@ global.__ = i18n.__; global.__n = i18n.__n; global.app = app; -// Lbryinc needs access to the redux store for dispatching auth-releated actions -global.store = app.store; - export default app; diff --git a/src/renderer/component/app/index.js b/src/renderer/component/app/index.js index e68f66443..d78487976 100644 --- a/src/renderer/component/app/index.js +++ b/src/renderer/component/app/index.js @@ -1,7 +1,7 @@ import { connect } from 'react-redux'; import { selectPageTitle, selectHistoryIndex, selectActiveHistoryEntry } from 'lbry-redux'; import { doRecordScroll } from 'redux/actions/navigation'; -import { selectUser } from 'lbryinc'; +import { selectUser } from 'redux/selectors/user'; import { doAlertError } from 'redux/actions/app'; import App from './view'; @@ -17,7 +17,4 @@ const perform = dispatch => ({ recordScroll: scrollPosition => dispatch(doRecordScroll(scrollPosition)), }); -export default connect( - select, - perform -)(App); +export default connect(select, perform)(App); diff --git a/src/renderer/component/cardVerify/index.js b/src/renderer/component/cardVerify/index.js index 5a770a0e1..390622f3d 100644 --- a/src/renderer/component/cardVerify/index.js +++ b/src/renderer/component/cardVerify/index.js @@ -1,14 +1,12 @@ +import React from 'react'; import { connect } from 'react-redux'; -import { selectUserEmail } from 'lbryinc'; +import { selectUserEmail } from 'redux/selectors/user'; import CardVerify from './view'; const select = state => ({ email: selectUserEmail(state), }); -const perform = () => ({}); +const perform = dispatch => ({}); -export default connect( - select, - perform -)(CardVerify); +export default connect(select, perform)(CardVerify); diff --git a/src/renderer/component/channelTile/view.jsx b/src/renderer/component/channelTile/view.jsx index 4b3c0f744..304bdd663 100644 --- a/src/renderer/component/channelTile/view.jsx +++ b/src/renderer/component/channelTile/view.jsx @@ -79,7 +79,7 @@ class ChannelTile extends React.PureComponent { 'card__title--large': size === 'large', })} > - {channelName || uri} +
( - - {props.children} + + {props.text} ); diff --git a/src/renderer/component/fileCard/view.jsx b/src/renderer/component/fileCard/view.jsx index f061d0b29..1ca015b89 100644 --- a/src/renderer/component/fileCard/view.jsx +++ b/src/renderer/component/fileCard/view.jsx @@ -93,7 +93,7 @@ class FileCard extends React.PureComponent {
- {title} +
{pending ?
Pending...
: } diff --git a/src/renderer/component/fileTile/view.jsx b/src/renderer/component/fileTile/view.jsx index 100d90f55..bfc650d71 100644 --- a/src/renderer/component/fileTile/view.jsx +++ b/src/renderer/component/fileTile/view.jsx @@ -127,7 +127,7 @@ class FileTile extends React.PureComponent { 'card__title--large': size === 'large', })} > - {title || name} +
{ 'card__subtext--large': size === 'large', })} > - {description} +
)} {!name && ( diff --git a/src/renderer/component/fileViewer/index.js b/src/renderer/component/fileViewer/index.js index 56e7fb69b..3dca7eb02 100644 --- a/src/renderer/component/fileViewer/index.js +++ b/src/renderer/component/fileViewer/index.js @@ -3,7 +3,6 @@ import * as settings from 'constants/settings'; import { doChangeVolume } from 'redux/actions/app'; import { selectVolume } from 'redux/selectors/app'; import { doPlayUri, doSetPlayingUri, savePosition } from 'redux/actions/content'; -import { doClaimEligiblePurchaseRewards } from 'lbryinc'; import { makeSelectMetadataForUri, makeSelectContentTypeForUri, @@ -14,6 +13,7 @@ import { makeSelectDownloadingForUri, selectSearchBarFocused, } from 'lbry-redux'; +import { doClaimEligiblePurchaseRewards } from 'redux/actions/rewards'; import { makeSelectClientSetting, selectShowNsfw } from 'redux/selectors/settings'; import { selectPlayingUri, makeSelectContentPositionForUri } from 'redux/selectors/content'; import { selectFileInfoErrors } from 'redux/selectors/file_info'; diff --git a/src/renderer/component/inviteList/index.js b/src/renderer/component/inviteList/index.js index d8df4a4b3..7dca63af5 100644 --- a/src/renderer/component/inviteList/index.js +++ b/src/renderer/component/inviteList/index.js @@ -1,5 +1,6 @@ +import React from 'react'; import { connect } from 'react-redux'; -import { selectUserInvitees, selectUserInviteStatusIsPending } from 'lbryinc'; +import { selectUserInvitees, selectUserInviteStatusIsPending } from 'redux/selectors/user'; import InviteList from './view'; const select = state => ({ @@ -7,9 +8,6 @@ const select = state => ({ isPending: selectUserInviteStatusIsPending(state), }); -const perform = () => ({}); +const perform = dispatch => ({}); -export default connect( - select, - perform -)(InviteList); +export default connect(select, perform)(InviteList); diff --git a/src/renderer/component/inviteList/view.jsx b/src/renderer/component/inviteList/view.jsx index 8d98f94d9..7874e9251 100644 --- a/src/renderer/component/inviteList/view.jsx +++ b/src/renderer/component/inviteList/view.jsx @@ -1,20 +1,10 @@ -// @flow import React from 'react'; import Icon from 'component/common/icon'; import RewardLink from 'component/rewardLink'; -import { rewards } from 'lbryinc'; +import rewards from 'rewards.js'; import * as icons from 'constants/icons'; -type Props = { - invitees: ?Array<{ - email: string, - invite_accepted: boolean, - invite_reward_claimed: boolean, - invite_reward_claimable: boolean, - }>, -}; - -class InviteList extends React.PureComponent { +class InviteList extends React.PureComponent { render() { const { invitees } = this.props; @@ -41,8 +31,8 @@ class InviteList extends React.PureComponent { - {invitees.map(invitee => ( - + {invitees.map((invitee, index) => ( + {invitee.email} {invitee.invite_accepted ? ( diff --git a/src/renderer/component/inviteNew/index.js b/src/renderer/component/inviteNew/index.js index f8f527b93..bfb65ab39 100644 --- a/src/renderer/component/inviteNew/index.js +++ b/src/renderer/component/inviteNew/index.js @@ -3,8 +3,8 @@ import { selectUserInvitesRemaining, selectUserInviteNewIsPending, selectUserInviteNewErrorMessage, - doUserInviteNew, -} from 'lbryinc'; +} from 'redux/selectors/user'; +import { doUserInviteNew } from 'redux/actions/user'; import InviteNew from './view'; const select = state => ({ @@ -17,7 +17,4 @@ const perform = dispatch => ({ inviteNew: email => dispatch(doUserInviteNew(email)), }); -export default connect( - select, - perform -)(InviteNew); +export default connect(select, perform)(InviteNew); diff --git a/src/renderer/component/rewardLink/index.js b/src/renderer/component/rewardLink/index.js index 37006bfec..f7e3561ef 100644 --- a/src/renderer/component/rewardLink/index.js +++ b/src/renderer/component/rewardLink/index.js @@ -3,10 +3,9 @@ import { makeSelectClaimRewardError, makeSelectRewardByType, makeSelectIsRewardClaimPending, - doClaimRewardType, - doClaimRewardClearError, -} from 'lbryinc'; +} from 'redux/selectors/rewards'; import { doNavigate } from 'redux/actions/navigation'; +import { doClaimRewardType, doClaimRewardClearError } from 'redux/actions/rewards'; import RewardLink from './view'; const makeSelect = () => { @@ -29,7 +28,4 @@ const perform = dispatch => ({ navigate: path => dispatch(doNavigate(path)), }); -export default connect( - makeSelect, - perform -)(RewardLink); +export default connect(makeSelect, perform)(RewardLink); diff --git a/src/renderer/component/rewardListClaimed/index.js b/src/renderer/component/rewardListClaimed/index.js index 1bab4ea82..212341cda 100644 --- a/src/renderer/component/rewardListClaimed/index.js +++ b/src/renderer/component/rewardListClaimed/index.js @@ -1,12 +1,10 @@ +import React from 'react'; import { connect } from 'react-redux'; -import { selectClaimedRewards } from 'lbryinc'; +import { selectClaimedRewards } from 'redux/selectors/rewards'; import RewardListClaimed from './view'; const select = state => ({ rewards: selectClaimedRewards(state), }); -export default connect( - select, - null -)(RewardListClaimed); +export default connect(select, null)(RewardListClaimed); diff --git a/src/renderer/component/rewardSummary/index.js b/src/renderer/component/rewardSummary/index.js index 8aaea1384..331825385 100644 --- a/src/renderer/component/rewardSummary/index.js +++ b/src/renderer/component/rewardSummary/index.js @@ -1,5 +1,6 @@ import { connect } from 'react-redux'; -import { selectUnclaimedRewardValue, selectFetchingRewards, doRewardList } from 'lbryinc'; +import { selectUnclaimedRewardValue, selectFetchingRewards } from 'redux/selectors/rewards'; +import { doRewardList } from 'redux/actions/rewards'; import { doFetchRewardedContent } from 'redux/actions/content'; import RewardSummary from './view'; diff --git a/src/renderer/component/rewardTile/view.jsx b/src/renderer/component/rewardTile/view.jsx index 9bfcfc507..743d5161e 100644 --- a/src/renderer/component/rewardTile/view.jsx +++ b/src/renderer/component/rewardTile/view.jsx @@ -3,7 +3,7 @@ import React from 'react'; import Icon from 'component/common/icon'; import RewardLink from 'component/rewardLink'; import Button from 'component/button'; -import { rewards } from 'lbryinc'; +import rewards from 'rewards'; import * as icons from 'constants/icons'; type Props = { diff --git a/src/renderer/component/transactionList/index.js b/src/renderer/component/transactionList/index.js index fde907969..f87371f4d 100644 --- a/src/renderer/component/transactionList/index.js +++ b/src/renderer/component/transactionList/index.js @@ -1,7 +1,8 @@ import { connect } from 'react-redux'; -import { selectClaimedRewardsByTransactionId } from 'lbryinc'; +import { selectClaimedRewardsByTransactionId } from 'redux/selectors/rewards'; import { doNavigate } from 'redux/actions/navigation'; -import { selectAllMyClaimsByOutpoint, doNotify } from 'lbry-redux'; +import { doNotify } from 'lbry-redux'; +import { selectAllMyClaimsByOutpoint } from 'lbry-redux'; import TransactionList from './view'; const select = state => ({ @@ -14,7 +15,4 @@ const perform = dispatch => ({ openModal: (modal, props) => dispatch(doNotify(modal, props)), }); -export default connect( - select, - perform -)(TransactionList); +export default connect(select, perform)(TransactionList); diff --git a/src/renderer/component/userEmailNew/index.js b/src/renderer/component/userEmailNew/index.js index b15793b6b..0228a4aaa 100644 --- a/src/renderer/component/userEmailNew/index.js +++ b/src/renderer/component/userEmailNew/index.js @@ -1,5 +1,7 @@ +import React from 'react'; import { connect } from 'react-redux'; -import { selectEmailNewIsPending, selectEmailNewErrorMessage, doUserEmailNew } from 'lbryinc'; +import { doUserEmailNew, doUserInviteNew } from 'redux/actions/user'; +import { selectEmailNewIsPending, selectEmailNewErrorMessage } from 'redux/selectors/user'; import UserEmailNew from './view'; const select = state => ({ @@ -11,7 +13,4 @@ const perform = dispatch => ({ addUserEmail: email => dispatch(doUserEmailNew(email)), }); -export default connect( - select, - perform -)(UserEmailNew); +export default connect(select, perform)(UserEmailNew); diff --git a/src/renderer/component/userEmailVerify/index.js b/src/renderer/component/userEmailVerify/index.js index 6ac91cd13..886a0318a 100644 --- a/src/renderer/component/userEmailVerify/index.js +++ b/src/renderer/component/userEmailVerify/index.js @@ -1,12 +1,14 @@ import { connect } from 'react-redux'; +import { + doUserEmailVerify, + doUserEmailVerifyFailure, + doUserResendVerificationEmail, +} from 'redux/actions/user'; import { selectEmailVerifyIsPending, selectEmailToVerify, selectEmailVerifyErrorMessage, - doUserEmailVerify, - doUserEmailVerifyFailure, - doUserResendVerificationEmail, -} from 'lbryinc'; +} from 'redux/selectors/user'; import UserEmailVerify from './view'; const select = state => ({ @@ -21,7 +23,4 @@ const perform = dispatch => ({ resendVerificationEmail: email => dispatch(doUserResendVerificationEmail(email)), }); -export default connect( - select, - perform -)(UserEmailVerify); +export default connect(select, perform)(UserEmailVerify); diff --git a/src/renderer/component/userPhoneNew/index.js b/src/renderer/component/userPhoneNew/index.js index 12b2fdf2d..3033a3c2c 100644 --- a/src/renderer/component/userPhoneNew/index.js +++ b/src/renderer/component/userPhoneNew/index.js @@ -1,5 +1,7 @@ +import React from 'react'; import { connect } from 'react-redux'; -import { selectPhoneNewErrorMessage, doUserPhoneNew } from 'lbryinc'; +import { doUserPhoneNew } from 'redux/actions/user'; +import { selectPhoneNewErrorMessage } from 'redux/selectors/user'; import UserPhoneNew from './view'; const select = state => ({ @@ -7,10 +9,7 @@ const select = state => ({ }); const perform = dispatch => ({ - addUserPhone: (phone, countryCode) => dispatch(doUserPhoneNew(phone, countryCode)), + addUserPhone: (phone, country_code) => dispatch(doUserPhoneNew(phone, country_code)), }); -export default connect( - select, - perform -)(UserPhoneNew); +export default connect(select, perform)(UserPhoneNew); diff --git a/src/renderer/component/userPhoneVerify/index.js b/src/renderer/component/userPhoneVerify/index.js index a916182f2..fea5e2893 100644 --- a/src/renderer/component/userPhoneVerify/index.js +++ b/src/renderer/component/userPhoneVerify/index.js @@ -1,11 +1,11 @@ +import React from 'react'; import { connect } from 'react-redux'; +import { doUserPhoneVerify, doUserPhoneReset } from 'redux/actions/user'; import { - doUserPhoneVerify, - doUserPhoneReset, selectPhoneToVerify, selectPhoneVerifyErrorMessage, selectUserCountryCode, -} from 'lbryinc'; +} from 'redux/selectors/user'; import UserPhoneVerify from './view'; const select = state => ({ @@ -19,7 +19,4 @@ const perform = dispatch => ({ verifyUserPhone: code => dispatch(doUserPhoneVerify(code)), }); -export default connect( - select, - perform -)(UserPhoneVerify); +export default connect(select, perform)(UserPhoneVerify); diff --git a/src/renderer/component/userVerify/index.js b/src/renderer/component/userVerify/index.js index 8998450b9..b139c96c1 100644 --- a/src/renderer/component/userVerify/index.js +++ b/src/renderer/component/userVerify/index.js @@ -1,13 +1,13 @@ import { connect } from 'react-redux'; import { doNotify, MODALS } from 'lbry-redux'; import { doNavigate } from 'redux/actions/navigation'; +import { doUserIdentityVerify } from 'redux/actions/user'; +import rewards from 'rewards'; +import { makeSelectRewardByType } from 'redux/selectors/rewards'; import { - doUserIdentityVerify, - rewards, - makeSelectRewardByType, selectIdentityVerifyIsPending, selectIdentityVerifyErrorMessage, -} from 'lbryinc'; +} from 'redux/selectors/user'; import UserVerify from './view'; const select = state => { @@ -26,7 +26,4 @@ const perform = dispatch => ({ verifyPhone: () => dispatch(doNotify({ id: MODALS.PHONE_COLLECTION })), }); -export default connect( - select, - perform -)(UserVerify); +export default connect(select, perform)(UserVerify); diff --git a/src/renderer/component/userVerify/view.jsx b/src/renderer/component/userVerify/view.jsx index da09e15c3..70c9afd7e 100644 --- a/src/renderer/component/userVerify/view.jsx +++ b/src/renderer/component/userVerify/view.jsx @@ -2,7 +2,7 @@ import * as React from 'react'; import Button from 'component/button'; import CardVerify from 'component/cardVerify'; -import Lbryio from 'lbryinc'; +import lbryio from 'lbryio'; import * as icons from 'constants/icons'; type Props = { @@ -51,7 +51,7 @@ class UserVerify extends React.PureComponent { label={__('Perform Card Verification')} disabled={isPending} token={this.onToken} - stripeKey={Lbryio.getStripeToken()} + stripeKey={lbryio.getStripeToken()} />
diff --git a/src/renderer/index.js b/src/renderer/index.js index 9647865f3..d4b979091 100644 --- a/src/renderer/index.js +++ b/src/renderer/index.js @@ -11,10 +11,9 @@ import { doConditionalAuthNavigate, doDaemonReady, doAutoUpdate } from 'redux/ac import { doNotify, doBlackListedOutpointsSubscribe, isURIValid } from 'lbry-redux'; import { doNavigate } from 'redux/actions/navigation'; import { doDownloadLanguages, doUpdateIsNightAsync } from 'redux/actions/settings'; -import { doUserEmailVerify, doAuthenticate, Lbryio } from 'lbryinc'; +import { doUserEmailVerify, doAuthenticate } from 'redux/actions/user'; import 'scss/all.scss'; import store from 'store'; -import pjson from 'package.json'; import app from './app'; import analytics from './analytics'; import doLogWarningConsoleMessage from './logWarningConsoleMessage'; @@ -24,40 +23,6 @@ const APPPAGEURL = 'lbry://?'; autoUpdater.logger = remote.require('electron-log'); -// We need to override Lbryio for getting/setting the authToken -// We interect with ipcRenderer to get the auth key from a users keyring -Lbryio.setOverride('setAuthToken', status => { - Lbryio.call( - 'user', - 'new', - { - auth_token: '', - language: 'en', - app_id: status.installation_id, - }, - 'post' - ).then(response => { - if (!response.auth_token) { - throw new Error(__('auth_token is missing from response')); - } - - ipcRenderer.send('set-auth-token', response.auth_token); - }); -}); - -Lbryio.setOverride( - 'getAuthToken', - () => - new Promise(resolve => { - ipcRenderer.once('auth-token-response', (event, token) => { - Lbryio.authToken = token; - resolve(token); - }); - - ipcRenderer.send('get-auth-token'); - }) -); - ipcRenderer.on('open-uri-requested', (event, uri, newSession) => { if (uri && uri.startsWith('lbry://')) { if (uri.startsWith('lbry://?verify=')) { @@ -191,7 +156,7 @@ const init = () => { ReactDOM.render( app.store.dispatch(doAuthenticate(pjson.version))} + authenticate={() => app.store.dispatch(doAuthenticate())} onReadyToLaunch={onDaemonReady} /> , diff --git a/src/renderer/lbryio.js b/src/renderer/lbryio.js index e69de29bb..a80ab27f1 100644 --- a/src/renderer/lbryio.js +++ b/src/renderer/lbryio.js @@ -0,0 +1,155 @@ +import { ipcRenderer } from 'electron'; +import { Lbry } from 'lbry-redux'; +import querystring from 'querystring'; + +const Lbryio = { + enabled: true, + authenticationPromise: null, +}; + +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/'; + +Lbryio.call = (resource, action, params = {}, method = 'get') => { + if (!Lbryio.enabled) { + console.log(__('Internal API disabled')); + return Promise.reject(new Error(__('LBRY internal API is disabled'))); + } + + if (!(method === 'get' || method === 'post')) { + return Promise.reject(new Error(__('Invalid method'))); + } + + function checkAndParse(response) { + if (response.status >= 200 && response.status < 300) { + return response.json(); + } + return response.json().then(json => { + let error; + if (json.error) { + error = new Error(json.error); + } else { + error = new Error('Unknown API error signature'); + } + error.response = response; // This is primarily a hack used in actions/user.js + return Promise.reject(error); + }); + } + + function makeRequest(url, options) { + return fetch(url, options).then(checkAndParse); + } + + return Lbryio.getAuthToken().then(token => { + const fullParams = { auth_token: token, ...params }; + const qs = querystring.stringify(fullParams); + let url = `${CONNECTION_STRING}${resource}/${action}?${qs}`; + + let options = { + method: 'GET', + }; + + if (method === 'post') { + options = { + method: 'POST', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + }, + body: qs, + }; + url = `${CONNECTION_STRING}${resource}/${action}`; + } + + return makeRequest(url, options).then(response => response.data); + }); +}; + +Lbryio.authToken = null; + +Lbryio.getAuthToken = () => + new Promise(resolve => { + if (Lbryio.authToken) { + resolve(Lbryio.authToken); + } else { + ipcRenderer.once('auth-token-response', (event, token) => { + Lbryio.authToken = token; + return resolve(token); + }); + ipcRenderer.send('get-auth-token'); + } + }); + +Lbryio.setAuthToken = token => { + Lbryio.authToken = token ? token.toString().trim() : null; + ipcRenderer.send('set-auth-token', token); +}; + +Lbryio.getCurrentUser = () => Lbryio.call('user', 'me'); + +Lbryio.authenticate = () => { + if (!Lbryio.enabled) { + return new Promise(resolve => { + resolve({ + id: 1, + language: 'en', + primary_email: 'disabled@lbry.io', + has_verified_email: true, + is_identity_verified: true, + is_reward_approved: false, + }); + }); + } + + if (Lbryio.authenticationPromise === null) { + Lbryio.authenticationPromise = new Promise((resolve, reject) => { + Lbryio.getAuthToken() + .then(token => { + if (!token || token.length > 60) { + return false; + } + + // check that token works + return Lbryio.getCurrentUser() + .then(() => true) + .catch(() => false); + }) + .then(isTokenValid => { + if (isTokenValid) { + return reject; + } + + return Lbry.status() + .then(status => + Lbryio.call( + 'user', + 'new', + { + auth_token: '', + language: 'en', + app_id: status.installation_id, + }, + 'post' + ) + ) + .then(response => { + if (!response.auth_token) { + throw new Error(__('auth_token is missing from response')); + } + return Lbryio.setAuthToken(response.auth_token); + }); + }) + .then(Lbryio.getCurrentUser) + .then(resolve, reject); + }); + } + + return Lbryio.authenticationPromise; +}; + +Lbryio.getStripeToken = () => + CONNECTION_STRING.startsWith('http://localhost:') + ? 'pk_test_NoL1JWL7i1ipfhVId5KfDZgo' + : 'pk_live_e8M4dRNnCCbmpZzduEUZBgJO'; + +export default Lbryio; diff --git a/src/renderer/modal/modalCreditIntro/index.js b/src/renderer/modal/modalCreditIntro/index.js index b2d17af94..da724d2fa 100644 --- a/src/renderer/modal/modalCreditIntro/index.js +++ b/src/renderer/modal/modalCreditIntro/index.js @@ -1,8 +1,9 @@ import { connect } from 'react-redux'; import { doNavigate } from 'redux/actions/navigation'; import { doSetClientSetting } from 'redux/actions/settings'; -import { selectUserIsRewardApproved, selectUnclaimedRewardValue } from 'lbryinc'; +import { selectUserIsRewardApproved } from 'redux/selectors/user'; import { selectBalance, doHideNotification } from 'lbry-redux'; +import { selectUnclaimedRewardValue } from 'redux/selectors/rewards'; import * as settings from 'constants/settings'; import ModalCreditIntro from './view'; @@ -24,7 +25,4 @@ const perform = dispatch => () => ({ }, }); -export default connect( - select, - perform -)(ModalCreditIntro); +export default connect(select, perform)(ModalCreditIntro); diff --git a/src/renderer/modal/modalEmailCollection/index.js b/src/renderer/modal/modalEmailCollection/index.js index eba4ab760..311f1c274 100644 --- a/src/renderer/modal/modalEmailCollection/index.js +++ b/src/renderer/modal/modalEmailCollection/index.js @@ -2,7 +2,7 @@ import * as settings from 'constants/settings'; import { connect } from 'react-redux'; import { doHideNotification } from 'lbry-redux'; import { doSetClientSetting } from 'redux/actions/settings'; -import { selectEmailToVerify, selectUser } from 'lbryinc'; +import { selectEmailToVerify, selectUser } from 'redux/selectors/user'; import ModalEmailCollection from './view'; const select = state => ({ @@ -17,7 +17,4 @@ const perform = dispatch => () => ({ }, }); -export default connect( - select, - perform -)(ModalEmailCollection); +export default connect(select, perform)(ModalEmailCollection); diff --git a/src/renderer/modal/modalFirstReward/index.js b/src/renderer/modal/modalFirstReward/index.js index aab42baf6..c38b9d7e0 100644 --- a/src/renderer/modal/modalFirstReward/index.js +++ b/src/renderer/modal/modalFirstReward/index.js @@ -1,6 +1,7 @@ -import { rewards, makeSelectRewardByType } from 'lbryinc'; +import rewards from 'rewards'; import { connect } from 'react-redux'; import { doHideNotification } from 'lbry-redux'; +import { makeSelectRewardByType } from 'redux/selectors/rewards'; import ModalFirstReward from './view'; const select = state => { @@ -15,7 +16,4 @@ const perform = dispatch => ({ closeModal: () => dispatch(doHideNotification()), }); -export default connect( - select, - perform -)(ModalFirstReward); +export default connect(select, perform)(ModalFirstReward); diff --git a/src/renderer/modal/modalPhoneCollection/index.js b/src/renderer/modal/modalPhoneCollection/index.js index af7d379b4..98f60212b 100644 --- a/src/renderer/modal/modalPhoneCollection/index.js +++ b/src/renderer/modal/modalPhoneCollection/index.js @@ -1,6 +1,9 @@ +import React from 'react'; +import * as settings from 'constants/settings'; import { connect } from 'react-redux'; import { doHideNotification } from 'lbry-redux'; -import { selectPhoneToVerify, selectUser } from 'lbryinc'; +import { doSetClientSetting } from 'redux/actions/settings'; +import { selectPhoneToVerify, selectUser } from 'redux/selectors/user'; import { doNavigate } from 'redux/actions/navigation'; import ModalPhoneCollection from './view'; @@ -16,7 +19,4 @@ const perform = dispatch => () => ({ }, }); -export default connect( - select, - perform -)(ModalPhoneCollection); +export default connect(select, perform)(ModalPhoneCollection); diff --git a/src/renderer/modal/modalRouter/index.js b/src/renderer/modal/modalRouter/index.js index f88bce9dd..6e794d9b5 100644 --- a/src/renderer/modal/modalRouter/index.js +++ b/src/renderer/modal/modalRouter/index.js @@ -9,7 +9,7 @@ import { selectNotificationProps, } from 'lbry-redux'; import { makeSelectClientSetting } from 'redux/selectors/settings'; -import { selectUser, selectUserIsVerificationCandidate } from 'lbryinc'; +import { selectUser, selectUserIsVerificationCandidate } from 'redux/selectors/user'; import ModalRouter from './view'; @@ -32,7 +32,4 @@ const perform = dispatch => ({ openModal: notification => dispatch(doNotify(notification)), }); -export default connect( - select, - perform -)(ModalRouter); +export default connect(select, perform)(ModalRouter); diff --git a/src/renderer/page/auth/index.js b/src/renderer/page/auth/index.js index 4fbef0886..7964ef405 100644 --- a/src/renderer/page/auth/index.js +++ b/src/renderer/page/auth/index.js @@ -8,7 +8,7 @@ import { selectUser, selectUserIsPending, selectIdentityVerifyIsPending, -} from 'lbryinc'; +} from 'redux/selectors/user'; import AuthPage from './view'; const select = state => ({ @@ -26,7 +26,4 @@ const perform = dispatch => ({ navigate: path => dispatch(doNavigate(path)), }); -export default connect( - select, - perform -)(AuthPage); +export default connect(select, perform)(AuthPage); diff --git a/src/renderer/page/auth/view.jsx b/src/renderer/page/auth/view.jsx index 50a7c20ad..f7e045241 100644 --- a/src/renderer/page/auth/view.jsx +++ b/src/renderer/page/auth/view.jsx @@ -21,7 +21,7 @@ type Props = { navigate: (string, ?{}) => void, }; -class AuthPage extends React.PureComponent { +export class AuthPage extends React.PureComponent { componentWillMount() { this.navigateIfAuthenticated(this.props); } diff --git a/src/renderer/page/help/index.js b/src/renderer/page/help/index.js index d00bda044..cfabb784b 100644 --- a/src/renderer/page/help/index.js +++ b/src/renderer/page/help/index.js @@ -1,7 +1,8 @@ import { connect } from 'react-redux'; import { doAuthNavigate } from 'redux/actions/navigation'; -import { doFetchAccessToken, selectAccessToken, selectUser } from 'lbryinc'; +import { doFetchAccessToken } from 'redux/actions/user'; import { selectDaemonSettings } from 'redux/selectors/settings'; +import { selectAccessToken, selectUser } from 'redux/selectors/user'; import HelpPage from './view'; const select = state => ({ diff --git a/src/renderer/page/invite/index.js b/src/renderer/page/invite/index.js index c69a52697..65360a1f2 100644 --- a/src/renderer/page/invite/index.js +++ b/src/renderer/page/invite/index.js @@ -1,10 +1,11 @@ +import React from 'react'; import { connect } from 'react-redux'; +import InvitePage from './view'; +import { doFetchInviteStatus } from 'redux/actions/user'; import { - doFetchInviteStatus, selectUserInviteStatusFailed, selectUserInviteStatusIsPending, -} from 'lbryinc'; -import InvitePage from './view'; +} from 'redux/selectors/user'; const select = state => ({ isFailed: selectUserInviteStatusFailed(state), @@ -15,7 +16,4 @@ const perform = dispatch => ({ fetchInviteStatus: () => dispatch(doFetchInviteStatus()), }); -export default connect( - select, - perform -)(InvitePage); +export default connect(select, perform)(InvitePage); diff --git a/src/renderer/page/rewards/index.js b/src/renderer/page/rewards/index.js index 636f57752..3a8bfb3c4 100644 --- a/src/renderer/page/rewards/index.js +++ b/src/renderer/page/rewards/index.js @@ -3,10 +3,10 @@ import { selectFetchingRewards, selectUnclaimedRewards, selectClaimedRewards, - selectUser, - doRewardList, -} from 'lbryinc'; +} from 'redux/selectors/rewards'; +import { selectUser } from 'redux/selectors/user'; import { doAuthNavigate, doNavigate } from 'redux/actions/navigation'; +import { doRewardList } from 'redux/actions/rewards'; import { selectDaemonSettings } from 'redux/selectors/settings'; import RewardsPage from './view'; diff --git a/src/renderer/redux/actions/app.js b/src/renderer/redux/actions/app.js index c41702b4e..afb7bf13e 100644 --- a/src/renderer/redux/actions/app.js +++ b/src/renderer/redux/actions/app.js @@ -16,6 +16,7 @@ import Native from 'native'; import { doFetchRewardedContent } from 'redux/actions/content'; import { doFetchDaemonSettings } from 'redux/actions/settings'; import { doAuthNavigate } from 'redux/actions/navigation'; +import { doAuthenticate } from 'redux/actions/user'; import { doCheckSubscriptionsInit } from 'redux/actions/subscriptions'; import { selectIsUpgradeSkipped, @@ -27,8 +28,7 @@ import { selectRemoteVersion, selectUpgradeTimer, } from 'redux/selectors/app'; -import { doAuthenticate } from 'lbryinc'; -import { lbrySettings as config, version as appVersion } from 'package.json'; +import { lbrySettings as config } from 'package.json'; const { autoUpdater } = remote.require('electron-updater'); const { download } = remote.require('electron-dl'); @@ -333,7 +333,7 @@ export function doDaemonReady() { return (dispatch, getState) => { const state = getState(); - dispatch(doAuthenticate(appVersion)); + dispatch(doAuthenticate()); dispatch({ type: ACTIONS.DAEMON_READY }); dispatch(doFetchDaemonSettings()); dispatch(doBalanceSubscribe()); diff --git a/src/renderer/redux/actions/content.js b/src/renderer/redux/actions/content.js index 208fc3879..e39508808 100644 --- a/src/renderer/redux/actions/content.js +++ b/src/renderer/redux/actions/content.js @@ -1,6 +1,7 @@ // @flow import * as NOTIFICATION_TYPES from 'constants/notification_types'; import { ipcRenderer } from 'electron'; +import Lbryio from 'lbryio'; import { doAlertError } from 'redux/actions/app'; import { doNavigate } from 'redux/actions/navigation'; import { @@ -31,7 +32,6 @@ import { makeSelectClientSetting, selectosNotificationsEnabled } from 'redux/sel import setBadge from 'util/setBadge'; import setProgressBar from 'util/setProgressBar'; import analytics from 'analytics'; -import { Lbryio } from 'lbryinc'; const DOWNLOAD_POLL_INTERVAL = 250; diff --git a/src/renderer/redux/actions/rewards.js b/src/renderer/redux/actions/rewards.js new file mode 100644 index 000000000..18823dcf3 --- /dev/null +++ b/src/renderer/redux/actions/rewards.js @@ -0,0 +1,117 @@ +import * as ACTIONS from 'constants/action_types'; +import Lbryio from 'lbryio'; +import { doNotify, MODALS } from 'lbry-redux'; +import { selectUnclaimedRewards } from 'redux/selectors/rewards'; +import { selectUserIsRewardApproved } from 'redux/selectors/user'; +import rewards from 'rewards'; + +export function doRewardList() { + return dispatch => { + dispatch({ + type: ACTIONS.FETCH_REWARDS_STARTED, + }); + + Lbryio.call('reward', 'list', { multiple_rewards_per_type: true }) + .then(userRewards => { + dispatch({ + type: ACTIONS.FETCH_REWARDS_COMPLETED, + data: { userRewards }, + }); + }) + .catch(() => { + dispatch({ + type: ACTIONS.FETCH_REWARDS_COMPLETED, + data: { userRewards: [] }, + }); + }); + }; +} + +export function doClaimRewardType(rewardType, options) { + return (dispatch, getState) => { + const state = getState(); + const unclaimedRewards = selectUnclaimedRewards(state); + const reward = unclaimedRewards.find(ur => ur.reward_type === rewardType); + const userIsRewardApproved = selectUserIsRewardApproved(state); + + if (!reward || reward.transaction_id) { + // already claimed or doesn't exist, do nothing + return; + } + + if (!userIsRewardApproved && rewardType !== rewards.TYPE_CONFIRM_EMAIL) { + if (!options || !options.failSilently) { + const action = doNotify({ + id: MODALS.REWARD_APPROVAL_REQUIRED, + isError: false, + }); + dispatch(action); + } + + return; + } + + dispatch({ + type: ACTIONS.CLAIM_REWARD_STARTED, + data: { reward }, + }); + + const success = successReward => { + dispatch(doRewardList()); + dispatch({ + type: ACTIONS.CLAIM_REWARD_SUCCESS, + data: { + reward: successReward, + }, + }); + if (successReward.reward_type === rewards.TYPE_NEW_USER) { + const action = doNotify({ + id: MODALS.FIRST_REWARD, + isError: false, + }); + dispatch(action); + } + }; + + const failure = error => { + dispatch({ + type: ACTIONS.CLAIM_REWARD_FAILURE, + data: { + reward, + error: !options || !options.failSilently ? error : undefined, + }, + }); + }; + + rewards.claimReward(rewardType).then(success, failure); + }; +} + +export function doClaimEligiblePurchaseRewards() { + return (dispatch, getState) => { + const state = getState(); + const unclaimedRewards = selectUnclaimedRewards(state); + const userIsRewardApproved = selectUserIsRewardApproved(state); + + if (!userIsRewardApproved || !Lbryio.enabled) { + return; + } + + if (unclaimedRewards.find(ur => ur.reward_type === rewards.TYPE_FIRST_STREAM)) { + dispatch(doClaimRewardType(rewards.TYPE_FIRST_STREAM)); + } else { + [rewards.TYPE_MANY_DOWNLOADS, rewards.TYPE_FEATURED_DOWNLOAD].forEach(type => { + dispatch(doClaimRewardType(type, { failSilently: true })); + }); + } + }; +} + +export function doClaimRewardClearError(reward) { + return dispatch => { + dispatch({ + type: ACTIONS.CLAIM_REWARD_CLEAR_ERROR, + data: { reward }, + }); + }; +} diff --git a/src/renderer/redux/actions/subscriptions.js b/src/renderer/redux/actions/subscriptions.js index 13cce467d..1f4f320aa 100644 --- a/src/renderer/redux/actions/subscriptions.js +++ b/src/renderer/redux/actions/subscriptions.js @@ -2,14 +2,16 @@ import * as ACTIONS from 'constants/action_types'; import * as NOTIFICATION_TYPES from 'constants/notification_types'; import * as SETTINGS from 'constants/settings'; -import { Lbryio, rewards, doClaimRewardType } from 'lbryinc'; +import rewards from 'rewards'; import type { Dispatch, SubscriptionNotifications } from 'redux/reducers/subscriptions'; import type { Subscription } from 'types/subscription'; import { selectSubscriptions } from 'redux/selectors/subscriptions'; import { makeSelectClientSetting } from 'redux/selectors/settings'; import { Lbry, buildURI, parseURI, selectCurrentPage } from 'lbry-redux'; import { doPurchaseUri, doFetchClaimsByChannel } from 'redux/actions/content'; +import { doClaimRewardType } from 'redux/actions/rewards'; import Promise from 'bluebird'; +import Lbryio from 'lbryio'; const CHECK_SUBSCRIPTIONS_INTERVAL = 15 * 60 * 1000; const SUBSCRIPTION_DOWNLOAD_LIMIT = 1; @@ -244,7 +246,7 @@ export const doChannelSubscribe = (subscription: Subscription) => ( claim_id: claimId, }); - dispatch(doClaimRewardType(rewards.TYPE_SUBSCRIPTION, { failSilently: true })); + dispatch(doClaimRewardType(rewards.SUBSCRIPTION, { failSilently: true })); } dispatch(doCheckSubscription(subscription.uri, true)); diff --git a/src/renderer/redux/actions/user.js b/src/renderer/redux/actions/user.js new file mode 100644 index 000000000..963e503c9 --- /dev/null +++ b/src/renderer/redux/actions/user.js @@ -0,0 +1,360 @@ +import * as ACTIONS from 'constants/action_types'; +import Lbryio from 'lbryio'; +import { Lbry, doNotify, MODALS, doHideNotification } from 'lbry-redux'; +import { doClaimRewardType, doRewardList } from 'redux/actions/rewards'; +import { + selectEmailToVerify, + selectPhoneToVerify, + selectUserCountryCode, +} from 'redux/selectors/user'; +import rewards from 'rewards'; +import analytics from 'analytics'; +import pjson from 'package.json'; + +export function doFetchInviteStatus() { + return dispatch => { + dispatch({ + type: ACTIONS.USER_INVITE_STATUS_FETCH_STARTED, + }); + + Lbryio.call('user', 'invite_status') + .then(status => { + dispatch({ + type: ACTIONS.USER_INVITE_STATUS_FETCH_SUCCESS, + data: { + invitesRemaining: status.invites_remaining ? status.invites_remaining : 0, + invitees: status.invitees, + }, + }); + }) + .catch(error => { + dispatch({ + type: ACTIONS.USER_INVITE_STATUS_FETCH_FAILURE, + data: { error }, + }); + }); + }; +} + +export function doInstallNew() { + const payload = { app_version: pjson.version }; + Lbry.status().then(status => { + payload.app_id = status.installation_id; + if (status.dht) payload.node_id = status.dht.node_id; + Lbry.version().then(version => { + payload.daemon_version = version.lbrynet_version; + payload.operating_system = version.os_system; + payload.platform = version.platform; + Lbryio.call('install', 'new', payload); + }); + }); +} + +export function doAuthenticate() { + return dispatch => { + dispatch({ + type: ACTIONS.AUTHENTICATION_STARTED, + }); + Lbryio.authenticate() + .then(user => { + analytics.setUser(user); + dispatch({ + type: ACTIONS.AUTHENTICATION_SUCCESS, + data: { user }, + }); + dispatch(doRewardList()); + dispatch(doFetchInviteStatus()); + doInstallNew(); + }) + .catch(error => { + dispatch(doNotify({ id: MODALS.AUTHENTICATION_FAILURE })); + dispatch({ + type: ACTIONS.AUTHENTICATION_FAILURE, + data: { error }, + }); + }); + }; +} + +export function doUserFetch() { + return dispatch => { + dispatch({ + type: ACTIONS.USER_FETCH_STARTED, + }); + Lbryio.getCurrentUser() + .then(user => { + analytics.setUser(user); + dispatch(doRewardList()); + + dispatch({ + type: ACTIONS.USER_FETCH_SUCCESS, + data: { user }, + }); + }) + .catch(error => { + dispatch({ + type: ACTIONS.USER_FETCH_FAILURE, + data: { error }, + }); + }); + }; +} + +export function doUserPhoneReset() { + return { + type: ACTIONS.USER_PHONE_RESET, + }; +} + +export function doUserPhoneNew(phone, countryCode) { + return dispatch => { + dispatch({ + type: ACTIONS.USER_PHONE_NEW_STARTED, + data: { phone, country_code: countryCode }, + }); + + const success = () => { + dispatch({ + type: ACTIONS.USER_PHONE_NEW_SUCCESS, + data: { phone }, + }); + }; + + const failure = error => { + dispatch({ + type: ACTIONS.USER_PHONE_NEW_FAILURE, + data: { error }, + }); + }; + + Lbryio.call( + 'user', + 'phone_number_new', + { phone_number: phone, country_code: countryCode }, + 'post' + ).then(success, failure); + }; +} + +export function doUserPhoneVerifyFailure(error) { + return { + type: ACTIONS.USER_PHONE_VERIFY_FAILURE, + data: { error }, + }; +} + +export function doUserPhoneVerify(verificationCode) { + return (dispatch, getState) => { + const phoneNumber = selectPhoneToVerify(getState()); + const countryCode = selectUserCountryCode(getState()); + + dispatch({ + type: ACTIONS.USER_PHONE_VERIFY_STARTED, + code: verificationCode, + }); + + Lbryio.call( + 'user', + 'phone_number_confirm', + { + verification_code: verificationCode, + phone_number: phoneNumber, + country_code: countryCode, + }, + 'post' + ) + .then(user => { + if (user.is_identity_verified) { + dispatch({ + type: ACTIONS.USER_PHONE_VERIFY_SUCCESS, + data: { user }, + }); + dispatch(doHideNotification()); + dispatch(doClaimRewardType(rewards.TYPE_NEW_USER)); + } + }) + .catch(error => dispatch(doUserPhoneVerifyFailure(error))); + }; +} + +export function doUserEmailNew(email) { + return dispatch => { + dispatch({ + type: ACTIONS.USER_EMAIL_NEW_STARTED, + email, + }); + + const success = () => { + dispatch({ + type: ACTIONS.USER_EMAIL_NEW_SUCCESS, + data: { email }, + }); + dispatch(doUserFetch()); + }; + + const failure = error => { + dispatch({ + type: ACTIONS.USER_EMAIL_NEW_FAILURE, + data: { error }, + }); + }; + + Lbryio.call('user_email', 'new', { email, send_verification_email: true }, 'post') + .catch(error => { + if (error.response && error.response.status === 409) { + return Lbryio.call( + 'user_email', + 'resend_token', + { email, only_if_expired: true }, + 'post' + ).then(success, failure); + } + throw error; + }) + .then(success, failure); + }; +} + +export function doUserResendVerificationEmail(email) { + return dispatch => { + dispatch({ + type: ACTIONS.USER_EMAIL_VERIFY_RETRY, + email, + }); + + const success = () => { + dispatch({ + type: ACTIONS.USER_EMAIL_NEW_SUCCESS, + data: { email }, + }); + dispatch(doUserFetch()); + }; + + const failure = error => { + dispatch({ + type: ACTIONS.USER_EMAIL_NEW_FAILURE, + data: { error }, + }); + }; + + Lbryio.call('user_email', 'resend_token', { email }, 'post') + .catch(error => { + if (error.response && error.response.status === 409) { + throw error; + } + }) + .then(success, failure); + }; +} + +export function doUserEmailVerifyFailure(error) { + return { + type: ACTIONS.USER_EMAIL_VERIFY_FAILURE, + data: { error }, + }; +} + +export function doUserEmailVerify(verificationToken, recaptcha) { + return (dispatch, getState) => { + const email = selectEmailToVerify(getState()); + + dispatch({ + type: ACTIONS.USER_EMAIL_VERIFY_STARTED, + code: verificationToken, + recaptcha, + }); + + Lbryio.call( + 'user_email', + 'confirm', + { + verification_token: verificationToken, + email, + recaptcha, + }, + 'post' + ) + .then(userEmail => { + if (userEmail.is_verified) { + dispatch({ + type: ACTIONS.USER_EMAIL_VERIFY_SUCCESS, + data: { email }, + }); + dispatch(doUserFetch()); + } else { + throw new Error('Your email is still not verified.'); // shouldn't happen + } + }) + .catch(error => dispatch(doUserEmailVerifyFailure(error))); + }; +} + +export function doUserIdentityVerify(stripeToken) { + return dispatch => { + dispatch({ + type: ACTIONS.USER_IDENTITY_VERIFY_STARTED, + token: stripeToken, + }); + + Lbryio.call('user', 'verify_identity', { stripe_token: stripeToken }, 'post') + .then(user => { + if (user.is_identity_verified) { + dispatch({ + type: ACTIONS.USER_IDENTITY_VERIFY_SUCCESS, + data: { user }, + }); + dispatch(doClaimRewardType(rewards.TYPE_NEW_USER)); + } else { + throw new Error('Your identity is still not verified. This should not happen.'); // shouldn't happen + } + }) + .catch(error => { + dispatch({ + type: ACTIONS.USER_IDENTITY_VERIFY_FAILURE, + data: { error: error.toString() }, + }); + }); + }; +} + +export function doFetchAccessToken() { + return dispatch => { + const success = token => + dispatch({ + type: ACTIONS.FETCH_ACCESS_TOKEN_SUCCESS, + data: { token }, + }); + Lbryio.getAuthToken().then(success); + }; +} + +export function doUserInviteNew(email) { + return dispatch => { + dispatch({ + type: ACTIONS.USER_INVITE_NEW_STARTED, + }); + + Lbryio.call('user', 'invite', { email }, 'post') + .then(() => { + dispatch({ + type: ACTIONS.USER_INVITE_NEW_SUCCESS, + data: { email }, + }); + + dispatch( + doNotify({ + displayType: ['snackbar'], + message: __('Invite sent to %s', email), + }) + ); + + dispatch(doFetchInviteStatus()); + }) + .catch(error => { + dispatch({ + type: ACTIONS.USER_INVITE_NEW_FAILURE, + data: { error }, + }); + }); + }; +} diff --git a/src/renderer/redux/reducers/rewards.js b/src/renderer/redux/reducers/rewards.js new file mode 100644 index 000000000..67f861e6c --- /dev/null +++ b/src/renderer/redux/reducers/rewards.js @@ -0,0 +1,98 @@ +import * as ACTIONS from 'constants/action_types'; + +const reducers = {}; +const defaultState = { + fetching: false, + claimedRewardsById: {}, // id => reward + unclaimedRewards: [], + claimPendingByType: {}, + claimErrorsByType: {}, +}; + +reducers[ACTIONS.FETCH_REWARDS_STARTED] = state => + Object.assign({}, state, { + fetching: true, + }); + +reducers[ACTIONS.FETCH_REWARDS_COMPLETED] = (state, action) => { + const { userRewards } = action.data; + + const unclaimedRewards = []; + const claimedRewards = {}; + userRewards.forEach(reward => { + if (reward.transaction_id) { + claimedRewards[reward.id] = reward; + } else { + unclaimedRewards.push(reward); + } + }); + + return Object.assign({}, state, { + claimedRewardsById: claimedRewards, + unclaimedRewards, + fetching: false, + }); +}; + +function setClaimRewardState(state, reward, isClaiming, errorMessage = '') { + const newClaimPendingByType = Object.assign({}, state.claimPendingByType); + const newClaimErrorsByType = Object.assign({}, state.claimErrorsByType); + if (isClaiming) { + newClaimPendingByType[reward.reward_type] = isClaiming; + } else { + delete newClaimPendingByType[reward.reward_type]; + } + if (errorMessage) { + newClaimErrorsByType[reward.reward_type] = errorMessage; + } else { + delete newClaimErrorsByType[reward.reward_type]; + } + + return Object.assign({}, state, { + claimPendingByType: newClaimPendingByType, + claimErrorsByType: newClaimErrorsByType, + }); +} + +reducers[ACTIONS.CLAIM_REWARD_STARTED] = (state, action) => { + const { reward } = action.data; + + return setClaimRewardState(state, reward, true, ''); +}; + +reducers[ACTIONS.CLAIM_REWARD_SUCCESS] = (state, action) => { + const { reward } = action.data; + const { unclaimedRewards } = state; + + const index = unclaimedRewards.findIndex(ur => ur.reward_type === reward.reward_type); + unclaimedRewards.splice(index, 1); + + const { claimedRewardsById } = state; + claimedRewardsById[reward.id] = reward; + + const newState = { + ...state, + unclaimedRewards: [...unclaimedRewards], + claimedRewardsById: { ...claimedRewardsById }, + }; + + return setClaimRewardState(newState, reward, false, ''); +}; + +reducers[ACTIONS.CLAIM_REWARD_FAILURE] = (state, action) => { + const { reward, error } = action.data; + + return setClaimRewardState(state, reward, false, error ? error.message : ''); +}; + +reducers[ACTIONS.CLAIM_REWARD_CLEAR_ERROR] = (state, action) => { + const { reward } = action.data; + + return setClaimRewardState(state, reward, state.claimPendingByType[reward.reward_type], ''); +}; + +export default function reducer(state = defaultState, action) { + const handler = reducers[action.type]; + if (handler) return handler(state, action); + return state; +} diff --git a/src/renderer/redux/reducers/user.js b/src/renderer/redux/reducers/user.js new file mode 100644 index 000000000..aade82c69 --- /dev/null +++ b/src/renderer/redux/reducers/user.js @@ -0,0 +1,222 @@ +import * as ACTIONS from 'constants/action_types'; + +const reducers = {}; + +const defaultState = { + authenticationIsPending: false, + userIsPending: false, + emailNewIsPending: false, + emailNewErrorMessage: '', + emailToVerify: '', + inviteNewErrorMessage: '', + inviteNewIsPending: false, + inviteStatusIsPending: false, + invitesRemaining: undefined, + invitees: undefined, + user: undefined, +}; + +reducers[ACTIONS.AUTHENTICATION_STARTED] = state => + Object.assign({}, state, { + authenticationIsPending: true, + userIsPending: true, + user: defaultState.user, + }); + +reducers[ACTIONS.AUTHENTICATION_SUCCESS] = (state, action) => + Object.assign({}, state, { + authenticationIsPending: false, + userIsPending: false, + user: action.data.user, + }); + +reducers[ACTIONS.AUTHENTICATION_FAILURE] = state => + Object.assign({}, state, { + authenticationIsPending: false, + userIsPending: false, + user: null, + }); + +reducers[ACTIONS.USER_FETCH_STARTED] = state => + Object.assign({}, state, { + userIsPending: true, + user: defaultState.user, + }); + +reducers[ACTIONS.USER_FETCH_SUCCESS] = (state, action) => + Object.assign({}, state, { + userIsPending: false, + user: action.data.user, + }); + +reducers[ACTIONS.USER_FETCH_FAILURE] = state => + Object.assign({}, state, { + userIsPending: true, + user: null, + }); + +reducers[ACTIONS.USER_PHONE_NEW_STARTED] = (state, action) => { + const user = Object.assign({}, state.user); + user.country_code = action.data.country_code; + return Object.assign({}, state, { + phoneNewIsPending: true, + phoneNewErrorMessage: '', + user, + }); +}; + +reducers[ACTIONS.USER_PHONE_NEW_SUCCESS] = (state, action) => + Object.assign({}, state, { + phoneToVerify: action.data.phone, + phoneNewIsPending: false, + }); + +reducers[ACTIONS.USER_PHONE_RESET] = state => + Object.assign({}, state, { + phoneToVerify: null, + }); + +reducers[ACTIONS.USER_PHONE_NEW_FAILURE] = (state, action) => + Object.assign({}, state, { + phoneNewIsPending: false, + phoneNewErrorMessage: action.data.error, + }); + +reducers[ACTIONS.USER_PHONE_VERIFY_STARTED] = state => + Object.assign({}, state, { + phoneVerifyIsPending: true, + phoneVerifyErrorMessage: '', + }); + +reducers[ACTIONS.USER_PHONE_VERIFY_SUCCESS] = (state, action) => + Object.assign({}, state, { + phoneToVerify: '', + phoneVerifyIsPending: false, + user: action.data.user, + }); + +reducers[ACTIONS.USER_PHONE_VERIFY_FAILURE] = (state, action) => + Object.assign({}, state, { + phoneVerifyIsPending: false, + phoneVerifyErrorMessage: action.data.error, + }); + +reducers[ACTIONS.USER_EMAIL_NEW_STARTED] = state => + Object.assign({}, state, { + emailNewIsPending: true, + emailNewErrorMessage: '', + }); + +reducers[ACTIONS.USER_EMAIL_NEW_SUCCESS] = (state, action) => { + const user = Object.assign({}, state.user); + user.primary_email = action.data.email; + return Object.assign({}, state, { + emailToVerify: action.data.email, + emailNewIsPending: false, + user, + }); +}; + +reducers[ACTIONS.USER_EMAIL_NEW_EXISTS] = (state, action) => + Object.assign({}, state, { + emailToVerify: action.data.email, + emailNewIsPending: false, + }); + +reducers[ACTIONS.USER_EMAIL_NEW_FAILURE] = (state, action) => + Object.assign({}, state, { + emailNewIsPending: false, + emailNewErrorMessage: action.data.error, + }); + +reducers[ACTIONS.USER_EMAIL_VERIFY_STARTED] = state => + Object.assign({}, state, { + emailVerifyIsPending: true, + emailVerifyErrorMessage: '', + }); + +reducers[ACTIONS.USER_EMAIL_VERIFY_SUCCESS] = (state, action) => { + const user = Object.assign({}, state.user); + user.primary_email = action.data.email; + return Object.assign({}, state, { + emailToVerify: '', + emailVerifyIsPending: false, + user, + }); +}; + +reducers[ACTIONS.USER_EMAIL_VERIFY_FAILURE] = (state, action) => + Object.assign({}, state, { + emailVerifyIsPending: false, + emailVerifyErrorMessage: action.data.error, + }); + +reducers[ACTIONS.USER_IDENTITY_VERIFY_STARTED] = state => + Object.assign({}, state, { + identityVerifyIsPending: true, + identityVerifyErrorMessage: '', + }); + +reducers[ACTIONS.USER_IDENTITY_VERIFY_SUCCESS] = (state, action) => + Object.assign({}, state, { + identityVerifyIsPending: false, + identityVerifyErrorMessage: '', + user: action.data.user, + }); + +reducers[ACTIONS.USER_IDENTITY_VERIFY_FAILURE] = (state, action) => + Object.assign({}, state, { + identityVerifyIsPending: false, + identityVerifyErrorMessage: action.data.error, + }); + +reducers[ACTIONS.FETCH_ACCESS_TOKEN_SUCCESS] = (state, action) => { + const { token } = action.data; + + return Object.assign({}, state, { + accessToken: token, + }); +}; + +reducers[ACTIONS.USER_INVITE_STATUS_FETCH_STARTED] = state => + Object.assign({}, state, { + inviteStatusIsPending: true, + }); + +reducers[ACTIONS.USER_INVITE_STATUS_FETCH_SUCCESS] = (state, action) => + Object.assign({}, state, { + inviteStatusIsPending: false, + invitesRemaining: action.data.invitesRemaining, + invitees: action.data.invitees, + }); + +reducers[ACTIONS.USER_INVITE_NEW_STARTED] = state => + Object.assign({}, state, { + inviteNewIsPending: true, + inviteNewErrorMessage: '', + }); + +reducers[ACTIONS.USER_INVITE_NEW_SUCCESS] = state => + Object.assign({}, state, { + inviteNewIsPending: false, + inviteNewErrorMessage: '', + }); + +reducers[ACTIONS.USER_INVITE_NEW_FAILURE] = (state, action) => + Object.assign({}, state, { + inviteNewIsPending: false, + inviteNewErrorMessage: action.data.error.message, + }); + +reducers[ACTIONS.USER_INVITE_STATUS_FETCH_FAILURE] = state => + Object.assign({}, state, { + inviteStatusIsPending: false, + invitesRemaining: null, + invitees: null, + }); + +export default function reducer(state = defaultState, action) { + const handler = reducers[action.type]; + if (handler) return handler(state, action); + return state; +} diff --git a/src/renderer/redux/selectors/rewards.js b/src/renderer/redux/selectors/rewards.js new file mode 100644 index 000000000..c7fd3ef3d --- /dev/null +++ b/src/renderer/redux/selectors/rewards.js @@ -0,0 +1,64 @@ +import { createSelector } from 'reselect'; + +const selectState = state => state.rewards || {}; + +export const selectUnclaimedRewardsByType = createSelector( + selectState, + state => state.unclaimedRewardsByType +); + +export const selectClaimedRewardsById = createSelector( + selectState, + state => state.claimedRewardsById +); + +export const selectClaimedRewards = createSelector( + selectClaimedRewardsById, + byId => Object.values(byId) || [] +); + +export const selectClaimedRewardsByTransactionId = createSelector(selectClaimedRewards, rewards => + rewards.reduce((mapParam, reward) => { + const map = mapParam; + map[reward.transaction_id] = reward; + return map; + }, {}) +); + +export const selectUnclaimedRewards = createSelector(selectState, state => state.unclaimedRewards); + +export const selectFetchingRewards = createSelector(selectState, state => !!state.fetching); + +export const selectUnclaimedRewardValue = createSelector(selectUnclaimedRewards, rewards => + rewards.reduce((sum, reward) => sum + reward.reward_amount, 0) +); + +export const selectClaimsPendingByType = createSelector( + selectState, + state => state.claimPendingByType +); + +const selectIsClaimRewardPending = (state, props) => + selectClaimsPendingByType(state, props)[props.reward_type]; + +export const makeSelectIsRewardClaimPending = () => + createSelector(selectIsClaimRewardPending, isClaiming => isClaiming); + +export const selectClaimErrorsByType = createSelector( + selectState, + state => state.claimErrorsByType +); + +const selectClaimRewardError = (state, props) => + selectClaimErrorsByType(state, props)[props.reward_type]; + +export const makeSelectClaimRewardError = () => + createSelector(selectClaimRewardError, errorMessage => errorMessage); + +const selectRewardByType = (state, rewardType) => + selectUnclaimedRewards(state).find(reward => reward.reward_type === rewardType); + +export const makeSelectRewardByType = () => createSelector(selectRewardByType, reward => reward); + +export const makeSelectRewardAmountByType = () => + createSelector(selectRewardByType, reward => (reward ? reward.reward_amount : 0)); diff --git a/src/renderer/redux/selectors/user.js b/src/renderer/redux/selectors/user.js new file mode 100644 index 000000000..a1b0df18e --- /dev/null +++ b/src/renderer/redux/selectors/user.js @@ -0,0 +1,118 @@ +import { createSelector } from 'reselect'; + +export const selectState = state => state.user || {}; + +export const selectAuthenticationIsPending = createSelector( + selectState, + state => state.authenticationIsPending +); + +export const selectUserIsPending = createSelector(selectState, state => state.userIsPending); + +export const selectUser = createSelector(selectState, state => state.user); + +export const selectUserEmail = createSelector( + selectUser, + user => (user ? user.primary_email : null) +); + +export const selectUserPhone = createSelector( + selectUser, + user => (user ? user.phone_number : null) +); + +export const selectUserCountryCode = createSelector( + selectUser, + user => (user ? user.country_code : null) +); + +export const selectEmailToVerify = createSelector( + selectState, + selectUserEmail, + (state, userEmail) => state.emailToVerify || userEmail +); + +export const selectPhoneToVerify = createSelector( + selectState, + selectUserPhone, + (state, userPhone) => state.phoneToVerify || userPhone +); + +export const selectUserIsRewardApproved = createSelector( + selectUser, + user => user && user.is_reward_approved +); + +export const selectEmailNewIsPending = createSelector( + selectState, + state => state.emailNewIsPending +); + +export const selectEmailNewErrorMessage = createSelector( + selectState, + state => state.emailNewErrorMessage +); + +export const selectPhoneNewErrorMessage = createSelector( + selectState, + state => state.phoneNewErrorMessage +); + +export const selectEmailVerifyIsPending = createSelector( + selectState, + state => state.emailVerifyIsPending +); + +export const selectEmailVerifyErrorMessage = createSelector( + selectState, + state => state.emailVerifyErrorMessage +); + +export const selectPhoneVerifyErrorMessage = createSelector( + selectState, + state => state.phoneVerifyErrorMessage +); + +export const selectIdentityVerifyIsPending = createSelector( + selectState, + state => state.identityVerifyIsPending +); + +export const selectIdentityVerifyErrorMessage = createSelector( + selectState, + state => state.identityVerifyErrorMessage +); + +export const selectUserIsVerificationCandidate = createSelector( + selectUser, + user => user && (!user.has_verified_email || !user.is_identity_verified) +); + +export const selectAccessToken = createSelector(selectState, state => state.accessToken); + +export const selectUserInviteStatusIsPending = createSelector( + selectState, + state => state.inviteStatusIsPending +); + +export const selectUserInvitesRemaining = createSelector( + selectState, + state => state.invitesRemaining +); + +export const selectUserInvitees = createSelector(selectState, state => state.invitees); + +export const selectUserInviteStatusFailed = createSelector( + selectUserInvitesRemaining, + () => selectUserInvitesRemaining === null +); + +export const selectUserInviteNewIsPending = createSelector( + selectState, + state => state.inviteNewIsPending +); + +export const selectUserInviteNewErrorMessage = createSelector( + selectState, + state => state.inviteNewErrorMessage +); diff --git a/src/renderer/rewards.js b/src/renderer/rewards.js new file mode 100644 index 000000000..da3833e7b --- /dev/null +++ b/src/renderer/rewards.js @@ -0,0 +1,113 @@ +import { Lbry, doNotify } from 'lbry-redux'; +import Lbryio from 'lbryio'; + +const rewards = {}; + +rewards.TYPE_NEW_DEVELOPER = 'new_developer'; +rewards.TYPE_NEW_USER = 'new_user'; +rewards.TYPE_CONFIRM_EMAIL = 'verified_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_REFERRAL = 'referral'; +rewards.YOUTUBE_CREATOR = 'youtube_creator'; +rewards.SUBSCRIPTION = 'subscription'; + +rewards.claimReward = type => { + function requestReward(resolve, reject, params) { + if (!Lbryio.enabled) { + reject(new Error(__('Rewards are not enabled.'))); + return; + } + Lbryio.call('reward', 'new', params, 'post').then(reward => { + const message = + reward.reward_notification || `You have claimed a ${reward.reward_amount} LBC reward.`; + + // Display global notice + const action = doNotify({ + message, + linkText: __('Show All'), + linkTarget: '/rewards', + isError: false, + displayType: ['snackbar'], + }); + window.app.store.dispatch(action); + + // Add more events here to display other places + + resolve(reward); + }, 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(claims => { + const claim = claims + .reverse() + .find( + foundClaim => + foundClaim.name.length && + foundClaim.name[0] === '@' && + foundClaim.txid.length && + foundClaim.category === '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 => { + const claim = claims + .reverse() + .find( + foundClaim => + foundClaim.name.length && + foundClaim.name[0] !== '@' && + foundClaim.txid.length && + foundClaim.category === '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/src/renderer/store.js b/src/renderer/store.js index fdb1be5c9..37cbd6cf0 100644 --- a/src/renderer/store.js +++ b/src/renderer/store.js @@ -12,8 +12,9 @@ import { blacklistReducer, } from 'lbry-redux'; import navigationReducer from 'redux/reducers/navigation'; +import rewardsReducer from 'redux/reducers/rewards'; import settingsReducer from 'redux/reducers/settings'; -import { userReducer, rewardsReducer } from 'lbryinc'; +import userReducer from 'redux/reducers/user'; import shapeShiftReducer from 'redux/reducers/shape_shift'; import subscriptionsReducer from 'redux/reducers/subscriptions'; import publishReducer from 'redux/reducers/publish'; diff --git a/yarn.lock b/yarn.lock index 2b2961765..149175414 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5655,27 +5655,13 @@ lazy-val@^1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/lazy-val/-/lazy-val-1.0.3.tgz#bb97b200ef00801d94c317e29dc6ed39e31c5edc" -lbry-redux@lbryio/lbry-redux: +lbry-redux@lbryio/lbry-redux#d1cee82af119c0c5f98ec27f94b2e7f61e34b54c: version "0.0.1" - resolved "https://codeload.github.com/lbryio/lbry-redux/tar.gz/a8e81949837171e94e649fce6f7c7a8b7faadd51" + resolved "https://codeload.github.com/lbryio/lbry-redux/tar.gz/d1cee82af119c0c5f98ec27f94b2e7f61e34b54c" dependencies: proxy-polyfill "0.1.6" reselect "^3.0.0" -lbry-redux@lbryio/lbry-redux#fa141b9175500b874bb77da0258e18d902e069b9: - version "0.0.1" - resolved "https://codeload.github.com/lbryio/lbry-redux/tar.gz/fa141b9175500b874bb77da0258e18d902e069b9" - dependencies: - proxy-polyfill "0.1.6" - reselect "^3.0.0" - -lbryinc@lbryio/lbryinc#c09aa2645ecccb33c83e9a9545ff119232910f6f: - version "0.0.1" - resolved "https://codeload.github.com/lbryio/lbryinc/tar.gz/c09aa2645ecccb33c83e9a9545ff119232910f6f" - dependencies: - lbry-redux lbryio/lbry-redux - reselect "^3.0.0" - lcid@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/lcid/-/lcid-1.0.0.tgz#308accafa0bc483a3867b4b6f2b9506251d1b835"