[WIP] lbryio internal API calls and rewards component (#1)
* lbryinc base code containing lbryio.js * added rewards and user actions, reducers and selectors
This commit is contained in:
parent
0b9493470c
commit
0cca6c1811
24 changed files with 15266 additions and 0 deletions
4
.babelrc
Normal file
4
.babelrc
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
{
|
||||||
|
"presets": ["env", "stage-2"],
|
||||||
|
"plugins": ["transform-flow-comments"]
|
||||||
|
}
|
30
.eslintrc.json
Normal file
30
.eslintrc.json
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
{
|
||||||
|
"plugins": ["flowtype"],
|
||||||
|
"extends": [
|
||||||
|
"airbnb-base",
|
||||||
|
"plugin:import/electron",
|
||||||
|
"plugin:flowtype/recommended",
|
||||||
|
"plugin:prettier/recommended"
|
||||||
|
],
|
||||||
|
"settings": {
|
||||||
|
"import/resolver": {
|
||||||
|
"webpack": {
|
||||||
|
"config": "webpack.config.js"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"parser": "babel-eslint",
|
||||||
|
"env": {
|
||||||
|
"browser": true,
|
||||||
|
"node": true
|
||||||
|
},
|
||||||
|
"globals": {
|
||||||
|
"__": true
|
||||||
|
},
|
||||||
|
"rules": {
|
||||||
|
"import/no-commonjs": "warn",
|
||||||
|
"import/no-amd": "warn",
|
||||||
|
"import/prefer-default-export": "ignore",
|
||||||
|
"func-names": ["warn", "as-needed"]
|
||||||
|
}
|
||||||
|
}
|
9
.flowconfig
Normal file
9
.flowconfig
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
[ignore]
|
||||||
|
|
||||||
|
[include]
|
||||||
|
|
||||||
|
[libs]
|
||||||
|
|
||||||
|
[options]
|
||||||
|
module.system.node.resolve_dirname=src
|
||||||
|
module.system.node.resolve_dirname=node_modules
|
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
/node_modules
|
||||||
|
yarn-error.log
|
12
.lintstagedrc.json
Normal file
12
.lintstagedrc.json
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
{
|
||||||
|
"linters": {
|
||||||
|
"src/**/*.{js,json}": [
|
||||||
|
"prettier --write",
|
||||||
|
"git add"
|
||||||
|
],
|
||||||
|
"src/**/*.js": [
|
||||||
|
"eslint --fix",
|
||||||
|
"git add"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
5
.prettierrc.json
Normal file
5
.prettierrc.json
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
{
|
||||||
|
"trailingComma": "es5",
|
||||||
|
"printWidth": 100,
|
||||||
|
"singleQuote": true
|
||||||
|
}
|
15
LICENSE
Normal file
15
LICENSE
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
The MIT License (MIT)
|
||||||
|
|
||||||
|
Copyright (c) 2017-2018 LBRY Inc
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the
|
||||||
|
"Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish,
|
||||||
|
distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the
|
||||||
|
following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||||
|
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
||||||
|
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
||||||
|
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
8257
dist/bundle.js
vendored
Normal file
8257
dist/bundle.js
vendored
Normal file
File diff suppressed because it is too large
Load diff
58
package.json
Normal file
58
package.json
Normal file
|
@ -0,0 +1,58 @@
|
||||||
|
{
|
||||||
|
"name": "lbryinc",
|
||||||
|
"version": "0.0.1",
|
||||||
|
"description": "Shared code for api.lbry.io internal APIs.",
|
||||||
|
"keywords": [
|
||||||
|
"lbry"
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"homepage": "https://lbry.io/",
|
||||||
|
"bugs": {
|
||||||
|
"url": "https://github.com/lbryio/lbryinc/issues"
|
||||||
|
},
|
||||||
|
"repository": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/lbryio/lbryinc"
|
||||||
|
},
|
||||||
|
"author": {
|
||||||
|
"name": "LBRY Inc.",
|
||||||
|
"email": "hello@lbry.io"
|
||||||
|
},
|
||||||
|
"main": "dist/bundle.js",
|
||||||
|
"scripts": {
|
||||||
|
"build": "webpack",
|
||||||
|
"precommit": "lint-staged",
|
||||||
|
"lint": "eslint 'src/**/*.js' --fix",
|
||||||
|
"format": "prettier 'src/**/*.{js,json}' --write"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"lbry-redux": "lbryio/lbry-redux",
|
||||||
|
"reselect": "^3.0.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"babel-core": "^6.26.0",
|
||||||
|
"babel-eslint": "^8.0.3",
|
||||||
|
"babel-loader": "^7.1.4",
|
||||||
|
"babel-plugin-module-resolver": "^3.0.0",
|
||||||
|
"babel-preset-env": "^1.6.1",
|
||||||
|
"babel-preset-stage-2": "^6.18.0",
|
||||||
|
"eslint": "^4.19.1",
|
||||||
|
"eslint-config-airbnb-base": "^12.1.0",
|
||||||
|
"eslint-config-prettier": "^2.9.0",
|
||||||
|
"eslint-import-resolver-webpack": "^0.9.0",
|
||||||
|
"eslint-plugin-flowtype": "^2.40.1",
|
||||||
|
"eslint-plugin-import": "^2.10.0",
|
||||||
|
"eslint-plugin-prettier": "^2.4.0",
|
||||||
|
"flow-babel-webpack-plugin": "^1.1.1",
|
||||||
|
"flow-bin": "^0.69.0",
|
||||||
|
"flow-typed": "^2.4.0",
|
||||||
|
"husky": "^0.14.3",
|
||||||
|
"lint-staged": "^7.0.4",
|
||||||
|
"prettier": "^1.4.2",
|
||||||
|
"webpack": "^4.5.0",
|
||||||
|
"webpack-cli": "^2.0.14"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"yarn": "^1.3"
|
||||||
|
}
|
||||||
|
}
|
3
src/constants/action_types.js
Normal file
3
src/constants/action_types.js
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
export const GENERATE_AUTH_TOKEN_FAILURE = 'GENERATE_AUTH_TOKEN_FAILURE';
|
||||||
|
export const GENERATE_AUTH_TOKEN_STARTED = 'GENERATE_AUTH_TOKEN_STARTED';
|
||||||
|
export const GENERATE_AUTH_TOKEN_SUCCESS = 'GENERATE_AUTH_TOKEN_SUCCESS';
|
71
src/index.js
Normal file
71
src/index.js
Normal file
|
@ -0,0 +1,71 @@
|
||||||
|
import * as LBRYINC_ACTIONS from 'constants/action_types';
|
||||||
|
import Lbryio from 'lbryio';
|
||||||
|
|
||||||
|
// constants
|
||||||
|
export { LBRYINC_ACTIONS };
|
||||||
|
|
||||||
|
// Lbryio
|
||||||
|
export { Lbryio };
|
||||||
|
|
||||||
|
// actions
|
||||||
|
export { doGenerateAuthToken } from 'redux/actions/auth';
|
||||||
|
export {
|
||||||
|
doRewardList,
|
||||||
|
doClaimRewardType,
|
||||||
|
doClaimEligiblePurchaseRewards,
|
||||||
|
doClaimRewardClearError,
|
||||||
|
} from 'redux/actions/rewards';
|
||||||
|
export { doFetchInviteStatus, doInstallNew, doAuthenticate, doUserFetch } from 'redux/actions/user';
|
||||||
|
|
||||||
|
// reducers
|
||||||
|
export { authReducer } from 'redux/reducers/auth';
|
||||||
|
export { rewardsReducer } from 'redux/reducers/rewards';
|
||||||
|
export { userReducer } from 'redux/reducers/user';
|
||||||
|
|
||||||
|
// selectors
|
||||||
|
export { selectAuthToken } from 'redux/selectors/auth';
|
||||||
|
export {
|
||||||
|
makeSelectClaimRewardError,
|
||||||
|
makeSelectIsRewardClaimPending,
|
||||||
|
makeSelectRewardAmountByType,
|
||||||
|
makeSelectRewardByType,
|
||||||
|
selectUnclaimedRewardsByType,
|
||||||
|
selectClaimedRewardsById,
|
||||||
|
selectClaimedRewards,
|
||||||
|
selectClaimedRewardsByTransactionId,
|
||||||
|
selectUnclaimedRewards,
|
||||||
|
selectFetchingRewards,
|
||||||
|
selectUnclaimedRewardValue,
|
||||||
|
selectClaimsPendingByType,
|
||||||
|
selectIsClaimRewardPending,
|
||||||
|
selectClaimErrorsByType,
|
||||||
|
selectClaimRewardError,
|
||||||
|
selectRewardByType,
|
||||||
|
} from 'redux/selectors/rewards';
|
||||||
|
export {
|
||||||
|
selectAuthenticationIsPending,
|
||||||
|
selectUserIsPending,
|
||||||
|
selectUser,
|
||||||
|
selectUserEmail,
|
||||||
|
selectUserPhone,
|
||||||
|
selectUserCountryCode,
|
||||||
|
selectEmailToVerify,
|
||||||
|
selectPhoneToVerify,
|
||||||
|
selectUserIsRewardApproved,
|
||||||
|
selectEmailNewIsPending,
|
||||||
|
selectEmailNewErrorMessage,
|
||||||
|
selectPhoneNewErrorMessage,
|
||||||
|
selectEmailVerifyIsPending,
|
||||||
|
selectEmailVerifyErrorMessage,
|
||||||
|
selectPhoneVerifyErrorMessage,
|
||||||
|
selectIdentityVerifyIsPending,
|
||||||
|
selectIdentityVerifyErrorMessage,
|
||||||
|
selectUserIsVerificationCandidate,
|
||||||
|
selectAccessToken,
|
||||||
|
selectUserInviteStatusIsPending,
|
||||||
|
selectUserInvitesRemaining,
|
||||||
|
selectUserInvitees,
|
||||||
|
selectUserInviteStatusFailed,
|
||||||
|
selectUserInviteNewIsPending,
|
||||||
|
selectUserInviteNewErrorMessage,
|
||||||
|
} from 'redux/selectors/user';
|
142
src/lbryio.js
Normal file
142
src/lbryio.js
Normal file
|
@ -0,0 +1,142 @@
|
||||||
|
import { Lbry } from 'lbry-redux';
|
||||||
|
import { doGenerateAuthToken } from 'redux/actions/auth';
|
||||||
|
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) {
|
||||||
|
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 {
|
||||||
|
const { store } = window;
|
||||||
|
if (store) {
|
||||||
|
const state = store.getState();
|
||||||
|
const token = state.auth ? state.auth.authToken : null;
|
||||||
|
Lbryio.authToken = token;
|
||||||
|
resolve(token);
|
||||||
|
}
|
||||||
|
|
||||||
|
resolve(null);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
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 => {
|
||||||
|
const { store } = window;
|
||||||
|
if (store) {
|
||||||
|
store.dispatch(doGenerateAuthToken(status.installation_id));
|
||||||
|
return resolve();
|
||||||
|
}
|
||||||
|
|
||||||
|
return reject();
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.then(resolve, reject);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return Lbryio.authenticationPromise;
|
||||||
|
};
|
||||||
|
|
||||||
|
Lbryio.getStripeToken = () =>
|
||||||
|
CONNECTION_STRING.startsWith('http://localhost:')
|
||||||
|
? 'pk_test_NoL1JWL7i1ipfhVId5KfDZgo'
|
||||||
|
: 'pk_live_e8M4dRNnCCbmpZzduEUZBgJO';
|
||||||
|
|
||||||
|
export default Lbryio;
|
38
src/redux/actions/auth.js
Normal file
38
src/redux/actions/auth.js
Normal file
|
@ -0,0 +1,38 @@
|
||||||
|
import * as ACTIONS from 'constants/action_types';
|
||||||
|
import Lbryio from 'lbryio';
|
||||||
|
|
||||||
|
export function doGenerateAuthToken(installationId) {
|
||||||
|
return dispatch => {
|
||||||
|
dispatch({
|
||||||
|
type: ACTIONS.GENERATE_AUTH_TOKEN_STARTED,
|
||||||
|
});
|
||||||
|
|
||||||
|
Lbryio.call(
|
||||||
|
'user',
|
||||||
|
'new',
|
||||||
|
{
|
||||||
|
auth_token: '',
|
||||||
|
language: 'en',
|
||||||
|
app_id: installationId,
|
||||||
|
},
|
||||||
|
'post'
|
||||||
|
)
|
||||||
|
.then(response => {
|
||||||
|
if (!response.auth_token) {
|
||||||
|
dispatch({
|
||||||
|
type: ACTIONS.GENERATE_AUTH_TOKEN_FAILURE,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
dispatch({
|
||||||
|
type: ACTIONS.GENERATE_AUTH_TOKEN_SUCCESS,
|
||||||
|
data: { authToken: response.auth_token },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
dispatch({
|
||||||
|
type: ACTIONS.GENERATE_AUTH_TOKEN_FAILURE,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
114
src/redux/actions/rewards.js
Normal file
114
src/redux/actions/rewards.js
Normal file
|
@ -0,0 +1,114 @@
|
||||||
|
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) {
|
||||||
|
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({
|
||||||
|
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 },
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
93
src/redux/actions/user.js
Normal file
93
src/redux/actions/user.js
Normal file
|
@ -0,0 +1,93 @@
|
||||||
|
import { ACTIONS, MODALS, Lbry, doNotify } from 'lbry-redux';
|
||||||
|
import { doRewardList } from 'redux/actions/rewards';
|
||||||
|
import Lbryio from 'lbryio';
|
||||||
|
|
||||||
|
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(appVersion, deviceId = null) {
|
||||||
|
const payload = { app_version: appVersion, device_id: deviceId };
|
||||||
|
Lbry.status().then(status => {
|
||||||
|
payload.app_id = status.installation_id;
|
||||||
|
payload.node_id = status.lbry_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);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Call doInstallNew separately so we don't have to pass appVersion and deviceId params?
|
||||||
|
export function doAuthenticate(appVersion, deviceId = null) {
|
||||||
|
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(appVersion, deviceId);
|
||||||
|
})
|
||||||
|
.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 },
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
29
src/redux/reducers/auth.js
Normal file
29
src/redux/reducers/auth.js
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
import * as ACTIONS from 'constants/action_types';
|
||||||
|
|
||||||
|
const reducers = {};
|
||||||
|
const defaultState = {
|
||||||
|
authenticating: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
reducers[ACTIONS.GENERATE_AUTH_TOKEN_FAILURE] = state =>
|
||||||
|
Object.assign({}, state, {
|
||||||
|
authToken: null,
|
||||||
|
authenticating: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
reducers[ACTIONS.GENERATE_AUTH_TOKEN_STARTED] = state =>
|
||||||
|
Object.assign({}, state, {
|
||||||
|
authenticating: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
reducers[ACTIONS.GENERATE_AUTH_TOKEN_SUCCESS] = (state, action) =>
|
||||||
|
Object.assign({}, state, {
|
||||||
|
authToken: action.authToken,
|
||||||
|
authenticating: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
export function authReducer(state = defaultState, action) {
|
||||||
|
const handler = reducers[action.type];
|
||||||
|
if (handler) return handler(state, action);
|
||||||
|
return state;
|
||||||
|
}
|
98
src/redux/reducers/rewards.js
Normal file
98
src/redux/reducers/rewards.js
Normal file
|
@ -0,0 +1,98 @@
|
||||||
|
import { ACTIONS } from 'lbry-redux';
|
||||||
|
|
||||||
|
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 function rewardsReducer(state = defaultState, action) {
|
||||||
|
const handler = reducers[action.type];
|
||||||
|
if (handler) return handler(state, action);
|
||||||
|
return state;
|
||||||
|
}
|
222
src/redux/reducers/user.js
Normal file
222
src/redux/reducers/user.js
Normal file
|
@ -0,0 +1,222 @@
|
||||||
|
import { ACTIONS } from 'lbry-redux';
|
||||||
|
|
||||||
|
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 function userReducer(state = defaultState, action) {
|
||||||
|
const handler = reducers[action.type];
|
||||||
|
if (handler) return handler(state, action);
|
||||||
|
return state;
|
||||||
|
}
|
5
src/redux/selectors/auth.js
Normal file
5
src/redux/selectors/auth.js
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
import { createSelector } from 'reselect';
|
||||||
|
|
||||||
|
const selectState = state => state.authToken || {};
|
||||||
|
|
||||||
|
export const selectAuthToken = createSelector(selectState, state => state.authToken);
|
64
src/redux/selectors/rewards.js
Normal file
64
src/redux/selectors/rewards.js
Normal file
|
@ -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));
|
118
src/redux/selectors/user.js
Normal file
118
src/redux/selectors/user.js
Normal file
|
@ -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
|
||||||
|
);
|
112
src/rewards.js
Normal file
112
src/rewards.js
Normal file
|
@ -0,0 +1,112 @@
|
||||||
|
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.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.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;
|
26
webpack.config.js
Normal file
26
webpack.config.js
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
/* eslint-disable import/no-commonjs */
|
||||||
|
const path = require('path');
|
||||||
|
const FlowBabelWebpackPlugin = require('flow-babel-webpack-plugin');
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
mode: 'none',
|
||||||
|
entry: './src/index.js',
|
||||||
|
output: {
|
||||||
|
filename: 'bundle.js',
|
||||||
|
path: path.resolve(__dirname, 'dist'),
|
||||||
|
libraryTarget: 'umd'
|
||||||
|
},
|
||||||
|
module: {
|
||||||
|
rules: [
|
||||||
|
{
|
||||||
|
test: /\.js$/,
|
||||||
|
exclude: /node_modules/,
|
||||||
|
loader: 'babel-loader',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
resolve: {
|
||||||
|
modules: [path.resolve(__dirname, 'src'), 'node_modules'],
|
||||||
|
},
|
||||||
|
plugins: [new FlowBabelWebpackPlugin()],
|
||||||
|
};
|
Loading…
Reference in a new issue