Files
freeCodeCamp/client/src/redux/index.js

683 lines
21 KiB
JavaScript
Raw Normal View History

import { uniqBy } from 'lodash-es';
import { createAction, handleActions } from 'redux-actions';
import store from 'store';
import { actionTypes as challengeTypes } from '../templates/Challenges/redux/action-types';
import { CURRENT_CHALLENGE_KEY } from '../templates/Challenges/redux/current-challenge-saga';
import { createAcceptTermsSaga } from './accept-terms-saga';
import { actionTypes, ns } from './action-types';
2018-08-30 15:27:53 +01:00
import { createAppMountSaga } from './app-mount-saga';
import { createDonationSaga } from './donation-saga';
import failedUpdatesEpic from './failed-updates-epic';
import { createFetchUserSaga } from './fetch-user-saga';
import { createGaSaga } from './ga-saga';
import hardGoToEpic from './hard-go-to-epic';
import { createNightModeSaga } from './night-mode-saga';
import { createReportUserSaga } from './report-user-saga';
import { actionTypes as settingsTypes } from './settings/action-types';
import { createShowCertSaga } from './show-cert-saga';
import updateCompleteEpic from './update-complete-epic';
export { ns };
2018-11-29 12:12:15 +00:00
export const defaultFetchState = {
pending: true,
complete: false,
errored: false,
error: null
};
export const defaultDonationFormState = {
redirecting: false,
processing: false,
success: false,
error: '',
loading: {
stripe: true,
paypal: true
}
};
const initialState = {
appUsername: '',
recentlyClaimedBlock: null,
canRequestProgressDonation: true,
completionCount: 0,
currentChallengeId: store.get(CURRENT_CHALLENGE_KEY),
showCert: {},
showCertFetchState: {
...defaultFetchState
},
user: {},
userFetchState: {
...defaultFetchState
},
userProfileFetchState: {
...defaultFetchState
},
sessionMeta: { activeDonations: 0 },
showDonationModal: false,
isOnline: true,
donationFormState: {
...defaultDonationFormState
}
};
export const epics = [hardGoToEpic, failedUpdatesEpic, updateCompleteEpic];
export const sagas = [
...createAcceptTermsSaga(actionTypes),
...createAppMountSaga(actionTypes),
...createDonationSaga(actionTypes),
...createGaSaga(actionTypes),
...createFetchUserSaga(actionTypes),
...createShowCertSaga(actionTypes),
...createReportUserSaga(actionTypes),
...createNightModeSaga({ ...actionTypes, ...settingsTypes })
];
export const appMount = createAction(actionTypes.appMount);
export const tryToShowDonationModal = createAction(
actionTypes.tryToShowDonationModal
);
export const executeGA = createAction(actionTypes.executeGA);
export const allowBlockDonationRequests = createAction(
actionTypes.allowBlockDonationRequests
);
export const closeDonationModal = createAction(actionTypes.closeDonationModal);
export const openDonationModal = createAction(actionTypes.openDonationModal);
export const preventBlockDonationRequests = createAction(
actionTypes.preventBlockDonationRequests
);
export const preventProgressDonationRequests = createAction(
actionTypes.preventProgressDonationRequests
);
export const updateDonationFormState = createAction(
actionTypes.updateDonationFormState
);
export const onlineStatusChange = createAction(actionTypes.onlineStatusChange);
2020-03-06 17:51:58 +01:00
// TODO: re-evaluate this since /internal is no longer used.
// `hardGoTo` is used to hit the API server directly
// without going through /internal
// used for things like /signin and /signout
export const hardGoTo = createAction(actionTypes.hardGoTo);
export const submitComplete = createAction(actionTypes.submitComplete);
export const updateComplete = createAction(actionTypes.updateComplete);
export const updateFailed = createAction(actionTypes.updateFailed);
export const acceptTerms = createAction(actionTypes.acceptTerms);
export const acceptTermsComplete = createAction(
actionTypes.acceptTermsComplete
);
export const acceptTermsError = createAction(actionTypes.acceptTermsError);
export const fetchUser = createAction(actionTypes.fetchUser);
export const fetchUserComplete = createAction(actionTypes.fetchUserComplete);
export const fetchUserError = createAction(actionTypes.fetchUserError);
2018-08-30 15:27:53 +01:00
export const addDonation = createAction(actionTypes.addDonation);
export const addDonationComplete = createAction(
actionTypes.addDonationComplete
);
export const addDonationError = createAction(actionTypes.addDonationError);
export const postChargeStripe = createAction(actionTypes.postChargeStripe);
export const postChargeStripeComplete = createAction(
actionTypes.postChargeStripeComplete
);
export const postChargeStripeError = createAction(
actionTypes.postChargeStripeError
);
export const postChargeStripeCard = createAction(
actionTypes.postChargeStripeCard
);
export const postChargeStripeCardComplete = createAction(
actionTypes.postChargeStripeCardComplete
);
export const postChargeStripeCardError = createAction(
actionTypes.postChargeStripeCardError
);
export const fetchProfileForUser = createAction(
actionTypes.fetchProfileForUser
);
export const fetchProfileForUserComplete = createAction(
actionTypes.fetchProfileForUserComplete
);
export const fetchProfileForUserError = createAction(
actionTypes.fetchProfileForUserError
);
export const reportUser = createAction(actionTypes.reportUser);
export const reportUserComplete = createAction(actionTypes.reportUserComplete);
export const reportUserError = createAction(actionTypes.reportUserError);
export const resetUserData = createAction(actionTypes.resetUserData);
export const showCert = createAction(actionTypes.showCert);
export const showCertComplete = createAction(actionTypes.showCertComplete);
export const showCertError = createAction(actionTypes.showCertError);
export const updateCurrentChallengeId = createAction(
actionTypes.updateCurrentChallengeId
);
export const completedChallengesSelector = state =>
userSelector(state).completedChallenges || [];
export const completionCountSelector = state => state[ns].completionCount;
export const currentChallengeIdSelector = state => state[ns].currentChallengeId;
export const stepsToClaimSelector = state => {
const user = userSelector(state);
const currentCerts = certificatesByNameSelector(user.username)(
state
).currentCerts;
return {
currentCerts: currentCerts,
isHonest: user?.isHonest,
isShowName: user?.profileUI?.showName,
isShowCerts: user?.profileUI?.showCerts,
isShowProfile: !user?.profileUI?.isLocked
};
};
export const isDonatingSelector = state => userSelector(state).isDonating;
export const isOnlineSelector = state => state[ns].isOnline;
export const isSignedInSelector = state => !!state[ns].appUsername;
export const isDonationModalOpenSelector = state => state[ns].showDonationModal;
export const recentlyClaimedBlockSelector = state =>
state[ns].recentlyClaimedBlock;
export const donationFormStateSelector = state => state[ns].donationFormState;
2018-09-13 18:28:23 +01:00
export const signInLoadingSelector = state =>
userFetchStateSelector(state).pending;
export const showCertSelector = state => state[ns].showCert;
export const showCertFetchStateSelector = state => state[ns].showCertFetchState;
export const shouldRequestDonationSelector = state => {
const completedChallenges = completedChallengesSelector(state);
const completionCount = completionCountSelector(state);
const canRequestProgressDonation = state[ns].canRequestProgressDonation;
const isDonating = isDonatingSelector(state);
const recentlyClaimedBlock = recentlyClaimedBlockSelector(state);
// don't request donation if already donating
if (isDonating) return false;
// a block has been completed
if (recentlyClaimedBlock) return true;
// a donation has already been requested
if (!canRequestProgressDonation) return false;
// donations only appear after the user has completed ten challenges (i.e.
// not before the 11th challenge has mounted)
if (completedChallenges.length < 10) {
return false;
}
// this will mean we have completed 3 or more challenges this browser session
// and enough challenges overall to not be new
return completionCount >= 3;
};
export const userByNameSelector = username => state => {
const { user } = state[ns];
// TODO: Why return a string or empty objet literal?
return username in user ? user[username] : {};
};
export const certificatesByNameSelector = username => state => {
const {
isRespWebDesignCert,
is2018DataVisCert,
isFrontEndLibsCert,
isJsAlgoDataStructCert,
isApisMicroservicesCert,
isInfosecQaCert,
isQaCertV7,
isInfosecCertV7,
isFrontEndCert,
isBackEndCert,
isDataVisCert,
isFullStackCert,
isSciCompPyCertV7,
isDataAnalysisPyCertV7,
isMachineLearningPyCertV7,
isRelationalDatabasesCertV8
} = userByNameSelector(username)(state);
return {
hasModernCert:
isRespWebDesignCert ||
is2018DataVisCert ||
isFrontEndLibsCert ||
isJsAlgoDataStructCert ||
isApisMicroservicesCert ||
isQaCertV7 ||
isInfosecCertV7 ||
isFullStackCert ||
isSciCompPyCertV7 ||
isDataAnalysisPyCertV7 ||
isMachineLearningPyCertV7 ||
isRelationalDatabasesCertV8,
hasLegacyCert:
isFrontEndCert || isBackEndCert || isDataVisCert || isInfosecQaCert,
isFullStackCert,
currentCerts: [
{
show: isRespWebDesignCert,
title: 'Responsive Web Design Certification',
certSlug: 'responsive-web-design'
},
{
show: isJsAlgoDataStructCert,
title: 'JavaScript Algorithms and Data Structures Certification',
certSlug: 'javascript-algorithms-and-data-structures'
},
{
show: isFrontEndLibsCert,
feat: add 'back/front end' in curriculum (#42596) * chore: rename APIs and Microservices to include "Backend" (#42515) * fix typo * fix typo * undo change * Corrected grammar mistake Corrected a grammar mistake by removing a comma. * change APIs and Microservices cert title * update title * Change APIs and Microservices certi title * Update translations.json * update title * feat(curriculum): rename apis and microservices cert * rename folder structure * rename certificate * rename learn Markdown * apis-and-microservices -> back-end-development-and-apis * update backend meta * update i18n langs and cypress test Co-authored-by: Shaun Hamilton <shauhami020@gmail.com> * fix: add development to front-end libraries (#42512) * fix: added-the-word-Development-to-front-end-libraries * fix/added-the-word-Development-to-front-end-libraries * fix/added-word-development-to-front-end-libraries-in-other-related-files * fix/added-the-word-Development-to-front-end-and-all-related-files * fix/removed-typos-from-last-commit-in-index.md * fix/reverted-changes-that-i-made-to-dependecies * fix/removed xvfg * fix/reverted changes that i made to package.json * remove unwanted changes * front-end-development-libraries changes * rename backend certSlug and README * update i18n folder names and keys * test: add legacy path redirect tests This uses serve.json from the client-config repo, since we currently use that in production * fix: create public dir before moving serve.json * fix: add missing script * refactor: collect redirect tests * test: convert to cy.location for stricter tests * rename certificate folder to 00-certificates * change crowdin config to recognise new certificates location * allow translations to be used Co-authored-by: Nicholas Carrigan (he/him) <nhcarrigan@gmail.com> * add forwards slashes to path redirects * fix cypress path tests again * plese cypress * fix: test different challenge Okay so I literally have no idea why this one particular challenge fails in Cypress Firefox ONLY. Tom and I paired and spun a full build instance and confirmed in Firefox the page loads and redirects as expected. Changing to another bootstrap challenge passes Cypress firefox locally. Absolutely boggled by this. AAAAAAAAAAAAAAA * fix: separate the test Okay apparently the test does not work unless we separate it into a different `it` statement. >:( >:( >:( >:( Co-authored-by: Sujal Gupta <55016909+heysujal@users.noreply.github.com> Co-authored-by: Noor Fakhry <65724923+NoorFakhry@users.noreply.github.com> Co-authored-by: Oliver Eyton-Williams <ojeytonwilliams@gmail.com> Co-authored-by: Nicholas Carrigan (he/him) <nhcarrigan@gmail.com>
2021-08-14 03:57:13 +01:00
title: 'Front End Development Libraries Certification',
certSlug: 'front-end-development-libraries'
},
{
show: is2018DataVisCert,
title: 'Data Visualization Certification',
certSlug: 'data-visualization'
},
{
show: isApisMicroservicesCert,
feat: add 'back/front end' in curriculum (#42596) * chore: rename APIs and Microservices to include "Backend" (#42515) * fix typo * fix typo * undo change * Corrected grammar mistake Corrected a grammar mistake by removing a comma. * change APIs and Microservices cert title * update title * Change APIs and Microservices certi title * Update translations.json * update title * feat(curriculum): rename apis and microservices cert * rename folder structure * rename certificate * rename learn Markdown * apis-and-microservices -> back-end-development-and-apis * update backend meta * update i18n langs and cypress test Co-authored-by: Shaun Hamilton <shauhami020@gmail.com> * fix: add development to front-end libraries (#42512) * fix: added-the-word-Development-to-front-end-libraries * fix/added-the-word-Development-to-front-end-libraries * fix/added-word-development-to-front-end-libraries-in-other-related-files * fix/added-the-word-Development-to-front-end-and-all-related-files * fix/removed-typos-from-last-commit-in-index.md * fix/reverted-changes-that-i-made-to-dependecies * fix/removed xvfg * fix/reverted changes that i made to package.json * remove unwanted changes * front-end-development-libraries changes * rename backend certSlug and README * update i18n folder names and keys * test: add legacy path redirect tests This uses serve.json from the client-config repo, since we currently use that in production * fix: create public dir before moving serve.json * fix: add missing script * refactor: collect redirect tests * test: convert to cy.location for stricter tests * rename certificate folder to 00-certificates * change crowdin config to recognise new certificates location * allow translations to be used Co-authored-by: Nicholas Carrigan (he/him) <nhcarrigan@gmail.com> * add forwards slashes to path redirects * fix cypress path tests again * plese cypress * fix: test different challenge Okay so I literally have no idea why this one particular challenge fails in Cypress Firefox ONLY. Tom and I paired and spun a full build instance and confirmed in Firefox the page loads and redirects as expected. Changing to another bootstrap challenge passes Cypress firefox locally. Absolutely boggled by this. AAAAAAAAAAAAAAA * fix: separate the test Okay apparently the test does not work unless we separate it into a different `it` statement. >:( >:( >:( >:( Co-authored-by: Sujal Gupta <55016909+heysujal@users.noreply.github.com> Co-authored-by: Noor Fakhry <65724923+NoorFakhry@users.noreply.github.com> Co-authored-by: Oliver Eyton-Williams <ojeytonwilliams@gmail.com> Co-authored-by: Nicholas Carrigan (he/him) <nhcarrigan@gmail.com>
2021-08-14 03:57:13 +01:00
title: 'Back End Development and APIs Certification',
certSlug: 'back-end-development-and-apis'
},
{
show: isQaCertV7,
title: ' Quality Assurance Certification',
certSlug: 'quality-assurance-v7'
},
{
show: isInfosecCertV7,
title: 'Information Security Certification',
certSlug: 'information-security-v7'
},
{
show: isSciCompPyCertV7,
title: 'Scientific Computing with Python Certification',
certSlug: 'scientific-computing-with-python-v7'
},
{
show: isDataAnalysisPyCertV7,
title: 'Data Analysis with Python Certification',
certSlug: 'data-analysis-with-python-v7'
},
{
show: isMachineLearningPyCertV7,
title: 'Machine Learning with Python Certification',
certSlug: 'machine-learning-with-python-v7'
},
{
show: isRelationalDatabasesCertV8,
title: 'Relational Databases Certification',
certSlug: 'relational-databases-v8'
}
],
legacyCerts: [
{
show: isFrontEndCert,
title: 'Front End Certification',
certSlug: 'legacy-front-end'
},
{
show: isBackEndCert,
title: 'Back End Certification',
certSlug: 'legacy-back-end'
},
{
show: isDataVisCert,
title: 'Data Visualization Certification',
certSlug: 'legacy-data-visualization'
},
{
show: isInfosecQaCert,
title: 'Information Security and Quality Assurance Certification',
// Keep the current public profile cert slug
certSlug: 'information-security-and-quality-assurance'
},
{
show: isFullStackCert,
title: 'Full Stack Certification',
// Keep the current public profile cert slug
certSlug: 'full-stack'
}
]
};
};
export const userFetchStateSelector = state => state[ns].userFetchState;
export const userProfileFetchStateSelector = state =>
state[ns].userProfileFetchState;
export const usernameSelector = state => state[ns].appUsername;
2018-09-13 18:28:23 +01:00
export const userSelector = state => {
const username = usernameSelector(state);
return state[ns].user[username] || {};
};
export const sessionMetaSelector = state => state[ns].sessionMeta;
function spreadThePayloadOnUser(state, payload) {
return {
...state,
user: {
...state.user,
[state.appUsername]: {
...state.user[state.appUsername],
...payload
}
}
};
}
export const reducer = handleActions(
{
[actionTypes.acceptTermsComplete]: (state, { payload }) => {
const { appUsername } = state;
return {
...state,
user: {
...state.user,
[appUsername]: {
...state.user[appUsername],
// TODO: the user accepts the privacy terms in practice during auth
// however, it's currently being used to track if they've accepted
// or rejected the newsletter. Ideally this should be migrated,
// since they can't sign up without accepting the terms.
acceptedPrivacyTerms: true,
sendQuincyEmail:
payload === null
? state.user[appUsername].sendQuincyEmail
: payload
}
}
};
},
[actionTypes.allowBlockDonationRequests]: (state, { payload }) => {
return {
...state,
recentlyClaimedBlock: payload
};
},
[actionTypes.updateDonationFormState]: (state, { payload }) => ({
...state,
donationFormState: { ...state.donationFormState, ...payload }
}),
[actionTypes.addDonation]: state => ({
...state,
donationFormState: { ...defaultDonationFormState, processing: true }
}),
[actionTypes.addDonationComplete]: state => {
const { appUsername } = state;
return {
...state,
user: {
...state.user,
[appUsername]: {
...state.user[appUsername],
isDonating: true
}
},
donationFormState: { ...defaultDonationFormState, success: true }
};
},
[actionTypes.addDonationError]: (state, { payload }) => ({
...state,
donationFormState: { ...defaultDonationFormState, error: payload }
}),
[actionTypes.postChargeStripe]: state => ({
...state,
donationFormState: { ...defaultDonationFormState, processing: true }
}),
[actionTypes.postChargeStripeComplete]: state => {
const { appUsername } = state;
return {
...state,
user: {
...state.user,
[appUsername]: {
...state.user[appUsername],
isDonating: true
}
},
donationFormState: { ...defaultDonationFormState, success: true }
};
},
[actionTypes.postChargeStripeError]: (state, { payload }) => ({
...state,
donationFormState: { ...defaultDonationFormState, error: payload }
}),
[actionTypes.postChargeStripeCard]: state => ({
...state,
donationFormState: { ...defaultDonationFormState, processing: true }
}),
[actionTypes.postChargeStripeCardComplete]: state => {
const { appUsername } = state;
return {
...state,
user: {
...state.user,
[appUsername]: {
...state.user[appUsername],
isDonating: true
}
},
donationFormState: { ...defaultDonationFormState, success: true }
};
},
[actionTypes.postChargeStripeCardError]: (state, { payload }) => ({
...state,
donationFormState: { ...defaultDonationFormState, error: payload }
}),
[actionTypes.fetchUser]: state => ({
...state,
userFetchState: { ...defaultFetchState }
}),
[actionTypes.fetchProfileForUser]: state => ({
...state,
userProfileFetchState: { ...defaultFetchState }
}),
[actionTypes.fetchUserComplete]: (
state,
{ payload: { user, username, sessionMeta } }
) => ({
...state,
2018-09-13 18:28:23 +01:00
user: {
...state.user,
[username]: { ...user, sessionUser: true }
2018-09-13 18:28:23 +01:00
},
appUsername: username,
currentChallengeId: user.currentChallengeId,
userFetchState: {
pending: false,
complete: true,
errored: false,
error: null
},
sessionMeta: {
...state.sessionMeta,
...sessionMeta
}
}),
[actionTypes.fetchUserError]: (state, { payload }) => ({
...state,
userFetchState: {
pending: false,
complete: false,
errored: true,
error: payload
}
}),
[actionTypes.fetchProfileForUserComplete]: (
state,
{ payload: { user, username } }
) => {
const previousUserObject =
username in state.user ? state.user[username] : {};
return {
...state,
user: {
...state.user,
[username]: { ...previousUserObject, ...user }
},
userProfileFetchState: {
2018-11-29 12:12:15 +00:00
...defaultFetchState,
pending: false,
2018-11-29 12:12:15 +00:00
complete: true
}
};
},
[actionTypes.fetchProfileForUserError]: (state, { payload }) => ({
...state,
2018-11-29 12:12:15 +00:00
userProfileFetchState: {
pending: false,
complete: false,
errored: true,
error: payload
}
}),
[actionTypes.onlineStatusChange]: (state, { payload: isOnline }) => ({
...state,
isOnline
}),
[actionTypes.closeDonationModal]: state => ({
...state,
showDonationModal: false
}),
[actionTypes.openDonationModal]: state => ({
...state,
showDonationModal: true
}),
[actionTypes.preventBlockDonationRequests]: state => ({
...state,
recentlyClaimedBlock: null
}),
[actionTypes.preventProgressDonationRequests]: state => ({
...state,
canRequestProgressDonation: false
}),
[actionTypes.resetUserData]: state => ({
...state,
appUsername: '',
user: {}
}),
[actionTypes.showCert]: state => ({
...state,
showCert: {},
showCertFetchState: { ...defaultFetchState }
}),
[actionTypes.showCertComplete]: (state, { payload }) => ({
...state,
showCert: payload,
showCertFetchState: {
2018-11-29 12:12:15 +00:00
...defaultFetchState,
pending: false,
2018-11-29 12:12:15 +00:00
complete: true
}
}),
[actionTypes.showCertError]: (state, { payload }) => ({
...state,
showCert: {},
showCertFetchState: {
pending: false,
complete: false,
errored: true,
error: payload
}
2018-09-13 18:28:23 +01:00
}),
[actionTypes.submitComplete]: (state, { payload }) => {
let submittedchallenges = [{ ...payload, completedDate: Date.now() }];
if (payload.challArray) {
submittedchallenges = payload.challArray;
2019-03-26 17:54:18 +03:00
}
const { appUsername } = state;
return {
...state,
completionCount: state.completionCount + 1,
user: {
...state.user,
[appUsername]: {
...state.user[appUsername],
completedChallenges: uniqBy(
[
...submittedchallenges,
2019-03-26 17:54:18 +03:00
...state.user[appUsername].completedChallenges
],
'id'
)
}
}
};
},
[challengeTypes.challengeMounted]: (state, { payload }) => ({
...state,
currentChallengeId: payload
}),
[settingsTypes.updateLegacyCertComplete]: (state, { payload }) => {
const { appUsername } = state;
return {
...state,
completionCount: state.completionCount + 1,
user: {
...state.user,
[appUsername]: {
...state.user[appUsername],
completedChallenges: uniqBy(
2019-03-26 17:54:18 +03:00
[...state.user[appUsername].completedChallenges, payload],
'id'
)
}
}
};
},
2018-09-13 18:28:23 +01:00
[settingsTypes.submitNewUsernameComplete]: (state, { payload }) =>
payload
? {
...state,
user: {
...state.user,
[state.appUsername]: {
...state.user[state.appUsername],
username: payload
}
}
}
: state,
[settingsTypes.submitNewAboutComplete]: (state, { payload }) =>
payload ? spreadThePayloadOnUser(state, payload) : state,
[settingsTypes.updateMyEmailComplete]: (state, { payload }) =>
payload ? spreadThePayloadOnUser(state, payload) : state,
[settingsTypes.updateUserFlagComplete]: (state, { payload }) =>
2018-09-25 12:51:17 +01:00
payload ? spreadThePayloadOnUser(state, payload) : state,
[settingsTypes.verifyCertComplete]: (state, { payload }) =>
payload ? spreadThePayloadOnUser(state, payload) : state,
[settingsTypes.submitProfileUIComplete]: (state, { payload }) =>
payload
? {
...state,
user: {
...state.user,
[state.appUsername]: {
...state.user[state.appUsername],
profileUI: { ...payload }
}
}
}
: state
},
initialState
);