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'; 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 }; 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); // 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); 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; 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, title: 'Front End Development Libraries Certification', certSlug: 'front-end-development-libraries' }, { show: is2018DataVisCert, title: 'Data Visualization Certification', certSlug: 'data-visualization' }, { show: isApisMicroservicesCert, 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; 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, user: { ...state.user, [username]: { ...user, sessionUser: true } }, 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: { ...defaultFetchState, pending: false, complete: true } }; }, [actionTypes.fetchProfileForUserError]: (state, { payload }) => ({ ...state, 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: { ...defaultFetchState, pending: false, complete: true } }), [actionTypes.showCertError]: (state, { payload }) => ({ ...state, showCert: {}, showCertFetchState: { pending: false, complete: false, errored: true, error: payload } }), [actionTypes.submitComplete]: (state, { payload }) => { let submittedchallenges = [{ ...payload, completedDate: Date.now() }]; if (payload.challArray) { submittedchallenges = payload.challArray; } const { appUsername } = state; return { ...state, completionCount: state.completionCount + 1, user: { ...state.user, [appUsername]: { ...state.user[appUsername], completedChallenges: uniqBy( [ ...submittedchallenges, ...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( [...state.user[appUsername].completedChallenges, payload], 'id' ) } } }; }, [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 }) => 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 );