diff --git a/src/actions/auth.js b/src/actions/auth.js index b867dc761..95095eba0 100644 --- a/src/actions/auth.js +++ b/src/actions/auth.js @@ -5,11 +5,23 @@ import { } from 'apis/auth'; import { queryMeApi } from 'apis/me'; import authStatus from 'constants/authStatus'; -import rollbar from 'utils/rollbar'; -import { NOTIFICATION_TYPE } from 'constants/toastNotification'; -import { ERROR_CODE_MSG } from 'constants/errorCodeMsg'; -import { pushNotification } from 'actions/toastNotification'; +import { pushErrorNotificationAndRollbarAndThrowError } from 'actions/toastNotification'; import { GraphqlError } from 'utils/errors'; +import { + ER0001, + ER0002, + ER0003, + ER0004, + ER0006, + ER0009, + ER0010, + ER0011, + ER0012, + ER0013, + ER0014, + ER0015, + ER0016, +} from 'constants/errorCodeMsg'; export const SET_LOGIN = '@@auth/SET_LOGIN'; export const SET_USER = '@@auth/SET_USER'; @@ -42,40 +54,12 @@ const FBSDKLogin = FB => { }); }; -const composeErrMsg = (code, message, error) => { - if (error) { - return `[${code}] ${message}: ${error}`; - } else { - return `[${code}] ${message}`; - } -}; -const loginErrorToast = (code, message) => - pushNotification(NOTIFICATION_TYPE.ALERT, composeErrMsg(code, message)); - -const toastNotificationAndRollbarAndThrowError = (errorCode, error, extra) => ( - dispatch, - getState, -) => { - dispatch(loginErrorToast(errorCode, ERROR_CODE_MSG[errorCode].external)); - const internalMsg = composeErrMsg( - errorCode, - ERROR_CODE_MSG[errorCode].internal, - error, - ); - if (!extra) { - rollbar.error(internalMsg); - } else { - rollbar.error(internalMsg, extra); - } - throw new Error(internalMsg); -}; - /** * Use `hooks/login/useFacebookLogin` as possible */ export const loginWithFB = FBSDK => async (dispatch, getState) => { if (!FBSDK) { - dispatch(toastNotificationAndRollbarAndThrowError('ER0001')); + dispatch(pushErrorNotificationAndRollbarAndThrowError(ER0001)); } let fbLoginResponse = null; @@ -83,11 +67,11 @@ export const loginWithFB = FBSDK => async (dispatch, getState) => { // invoke FB SDK Login to get FB-issued access token fbLoginResponse = await FBSDKLogin(FBSDK); } catch (error) { - dispatch(toastNotificationAndRollbarAndThrowError('ER0002', error)); + dispatch(pushErrorNotificationAndRollbarAndThrowError(ER0002, error)); } if (!fbLoginResponse || !fbLoginResponse.status) { - dispatch(toastNotificationAndRollbarAndThrowError('ER0003')); + dispatch(pushErrorNotificationAndRollbarAndThrowError(ER0003)); } switch (fbLoginResponse.status) { @@ -95,7 +79,7 @@ export const loginWithFB = FBSDK => async (dispatch, getState) => { return; case authStatus.NOT_AUTHORIZED: dispatch(setLogin(authStatus.NOT_AUTHORIZED)); - dispatch(toastNotificationAndRollbarAndThrowError('ER0004')); + dispatch(pushErrorNotificationAndRollbarAndThrowError(ER0004)); break; case authStatus.CONNECTED: try { @@ -107,20 +91,20 @@ export const loginWithFB = FBSDK => async (dispatch, getState) => { } catch (error) { if (error instanceof GraphqlError && error.codes) { if (error.codes[0] === 'UNAUTHENTICATED') { - dispatch(toastNotificationAndRollbarAndThrowError('ER0014')); + dispatch(pushErrorNotificationAndRollbarAndThrowError(ER0014)); break; } else if (error.codes[0] === 'FORBIDDEN') { - dispatch(toastNotificationAndRollbarAndThrowError('ER0015')); + dispatch(pushErrorNotificationAndRollbarAndThrowError(ER0015)); break; } } - dispatch(toastNotificationAndRollbarAndThrowError('ER0016', error)); + dispatch(pushErrorNotificationAndRollbarAndThrowError(ER0016, error)); } break; default: dispatch( - toastNotificationAndRollbarAndThrowError( - 'ER0006', + pushErrorNotificationAndRollbarAndThrowError( + ER0006, null, fbLoginResponse, ), @@ -141,7 +125,7 @@ export const loginWithGoogle = credentialResponse => async ( ) => { // TODO: 當登入失敗 if (!credentialResponse || !credentialResponse.credential) { - dispatch(toastNotificationAndRollbarAndThrowError('ER0009')); + dispatch(pushErrorNotificationAndRollbarAndThrowError(ER0009)); } const idToken = credentialResponse.credential; try { @@ -149,19 +133,19 @@ export const loginWithGoogle = credentialResponse => async ( if (response && response.token) { await dispatch(loginWithToken(response.token)); } else { - dispatch(toastNotificationAndRollbarAndThrowError('ER0010')); + dispatch(pushErrorNotificationAndRollbarAndThrowError(ER0010)); } } catch (error) { if (error instanceof GraphqlError && error.codes) { if (error.codes[0] === 'UNAUTHENTICATED') { - dispatch(toastNotificationAndRollbarAndThrowError('ER0011')); + dispatch(pushErrorNotificationAndRollbarAndThrowError(ER0011)); return; } else if (error.codes[0] === 'FORBIDDEN') { - dispatch(toastNotificationAndRollbarAndThrowError('ER0012')); + dispatch(pushErrorNotificationAndRollbarAndThrowError(ER0012)); return; } } - dispatch(toastNotificationAndRollbarAndThrowError('ER0013', error)); + dispatch(pushErrorNotificationAndRollbarAndThrowError(ER0013, error)); } }; diff --git a/src/actions/toastNotification.js b/src/actions/toastNotification.js index 41e05b51c..bcd578c1a 100644 --- a/src/actions/toastNotification.js +++ b/src/actions/toastNotification.js @@ -1,3 +1,6 @@ +import rollbar from 'utils/rollbar'; +import { NOTIFICATION_TYPE } from 'constants/toastNotification'; +import { ERROR_CODE_MSG } from 'constants/errorCodeMsg'; import { generateNotification } from 'utils/toastNotification'; export const PUSH = '@@TOAST_NOTIFICATION/PUSH'; @@ -16,3 +19,51 @@ export const removeNotification = id => ({ type: REMOVE, id, }); + +const composeErrorMessage = (code, message, error) => { + if (error) { + return `[${code}] ${message}: ${error}`; + } else { + return `[${code}] ${message}`; + } +}; + +const pushErrorNotification = (code, message) => + pushNotification(NOTIFICATION_TYPE.ALERT, composeErrorMessage(code, message)); + +export const pushErrorNotificationAndRollbar = (errorCode, error, extra) => ( + dispatch, + getState, +) => { + dispatch( + pushErrorNotification(errorCode, ERROR_CODE_MSG[errorCode].external), + ); + + const internalMessage = composeErrorMessage( + errorCode, + ERROR_CODE_MSG[errorCode].internal, + error, + ); + + if (!extra) { + rollbar.error(internalMessage); + } else { + rollbar.error(internalMessage, extra); + } +}; + +export const pushErrorNotificationAndRollbarAndThrowError = ( + errorCode, + error, + extra, +) => (dispatch, getState) => { + dispatch(pushErrorNotificationAndRollbar(errorCode, error, extra)); + + const internalMessage = composeErrorMessage( + errorCode, + ERROR_CODE_MSG[errorCode].internal, + error, + ); + + throw new Error(internalMessage); +}; diff --git a/src/components/ShareExperience/InterviewForm/TypeForm/index.js b/src/components/ShareExperience/InterviewForm/TypeForm/index.js index acb0bd68e..a84973c6f 100644 --- a/src/components/ShareExperience/InterviewForm/TypeForm/index.js +++ b/src/components/ShareExperience/InterviewForm/TypeForm/index.js @@ -9,7 +9,7 @@ import Header, { CompanyJobTitleHeader } from '../../common/TypeFormHeader'; import SubmittableFormBuilder from '../../common/SubmittableFormBuilder'; import { createInterviewExperience } from 'actions/experiences'; import { GA_CATEGORY, GA_ACTION } from 'constants/gaConstants'; -import { ERROR_CODE_MSG } from 'constants/errorCodeMsg'; +import { ER0008, ERROR_CODE_MSG } from 'constants/errorCodeMsg'; import { DATA_KEY_COMPANY_NAME, DATA_KEY_JOB_TITLE, @@ -165,7 +165,7 @@ const TypeForm = ({ open, onClose }) => { category: GA_CATEGORY.SHARE_INTERVIEW_TYPE_FORM, action: GA_ACTION.UPLOAD_FAIL, }); - const errorCode = 'ER0008'; + const errorCode = ER0008; rollbar.error( `[${errorCode}] ${ERROR_CODE_MSG[errorCode].internal} ${error.message}`, error, diff --git a/src/components/ShareExperience/TimeSalaryForm/TypeForm.js b/src/components/ShareExperience/TimeSalaryForm/TypeForm.js index f69683631..4c502b717 100644 --- a/src/components/ShareExperience/TimeSalaryForm/TypeForm.js +++ b/src/components/ShareExperience/TimeSalaryForm/TypeForm.js @@ -39,7 +39,7 @@ import { DATA_KEY_HAS_OVERTIME_SALARY, DATA_KEY_HAS_COMPENSATORY_DAYOFF, } from '../constants'; -import { ERROR_CODE_MSG } from 'constants/errorCodeMsg'; +import { ER0007, ERROR_CODE_MSG } from 'constants/errorCodeMsg'; import { evolve } from '../utils'; import { generateTabURL, pageType, tabType } from 'constants/companyJobTitle'; @@ -167,7 +167,7 @@ const TypeForm = ({ open, onClose, hideProgressBar = false }) => { : GA_CATEGORY.SHARE_TIME_SALARY_TYPE_FORM, action: GA_ACTION.UPLOAD_FAIL, }); - const errorCode = 'ER0007'; + const errorCode = ER0007; rollbar.error( `[${errorCode}] ${ERROR_CODE_MSG[errorCode].internal} ${error.message}`, error, diff --git a/src/components/ShareExperience/WorkExperiencesForm/TypeForm.js b/src/components/ShareExperience/WorkExperiencesForm/TypeForm.js index 786f4e219..7c8c47879 100644 --- a/src/components/ShareExperience/WorkExperiencesForm/TypeForm.js +++ b/src/components/ShareExperience/WorkExperiencesForm/TypeForm.js @@ -37,7 +37,7 @@ import { tabType } from '../../../constants/companyJobTitle'; import { createWorkExperienceWithRating } from 'actions/experiences'; import { transferKeyToSnakecase } from 'utils/objectUtil'; import { GA_CATEGORY, GA_ACTION } from 'constants/gaConstants'; -import { ERROR_CODE_MSG } from 'constants/errorCodeMsg'; +import { ER0020, ERROR_CODE_MSG } from 'constants/errorCodeMsg'; import { sendEvent } from 'utils/hotjarUtil'; import rollbar from 'utils/rollbar'; @@ -139,7 +139,7 @@ const TypeForm = ({ open, onClose, hideProgressBar = false }) => { category: GA_CATEGORY.SHARE_WORK, action: GA_ACTION.UPLOAD_FAIL, }); - const errorCode = 'ER0020'; + const errorCode = ER0020; rollbar.error( `[${errorCode}] ${ERROR_CODE_MSG[errorCode].internal} ${error.message}`, error, diff --git a/src/components/ShareExperience/common/SubmittableFormBuilder.js b/src/components/ShareExperience/common/SubmittableFormBuilder.js index 4ceeabfe2..bd5fd59c5 100644 --- a/src/components/ShareExperience/common/SubmittableFormBuilder.js +++ b/src/components/ShareExperience/common/SubmittableFormBuilder.js @@ -10,7 +10,7 @@ import ConfirmModal from 'common/FormBuilder/Modals/ConfirmModal'; import Footer from './TypeFormFooter'; import { useExperienceCount, useSalaryWorkTimeCount } from 'hooks/useCount'; import rollbar from 'utils/rollbar'; -import { ERROR_CODE_MSG } from 'constants/errorCodeMsg'; +import { ER0018, ERROR_CODE_MSG } from 'constants/errorCodeMsg'; const SubmittableTypeForm = ({ open, @@ -38,7 +38,7 @@ const SubmittableTypeForm = ({ setSubmittedDraft(draft); setSubmitStatus('success'); } catch (error) { - const errorCode = 'ER0018'; + const errorCode = ER0018; rollbar.error( `[${errorCode}] ${ERROR_CODE_MSG[errorCode].internal} ${error}`, error, diff --git a/src/components/common/FormBuilder/index.js b/src/components/common/FormBuilder/index.js index cc82de211..7e1f34457 100644 --- a/src/components/common/FormBuilder/index.js +++ b/src/components/common/FormBuilder/index.js @@ -24,7 +24,9 @@ import AnimatedPager from './AnimatedPager'; import styles from './FormBuilder.module.css'; import { OptionPropType } from './QuestionBuilder/Checkbox/PropTypes'; import rollbar from 'utils/rollbar'; -import { ERROR_CODE_MSG } from 'constants/errorCodeMsg'; +import { ER0019, ER0021, ERROR_CODE_MSG } from 'constants/errorCodeMsg'; +import { useDispatch } from 'react-redux'; +import { pushErrorNotificationAndRollbar } from 'actions/toastNotification'; const findIfQuestionsAcceptDraft = draft => R.all( @@ -110,6 +112,7 @@ const FormBuilder = ({ const [isWarningShown, setWarningShown] = useState(false); const [showsNavigation, setShowsNavigation] = useState(true); + const dispatch = useDispatch(); const isSubmittable = useMemo( () => findIfQuestionsAcceptDraft(draft)(questions), @@ -120,10 +123,11 @@ const FormBuilder = ({ if (warning) { if (onValidateFail) onValidateFail({ dataKey, value: draft[dataKey], warning }); + else dispatch(pushErrorNotificationAndRollbar(ER0021)); } else if (isSubmittable) { onSubmit(draft); } else { - const errorCode = 'ER0019'; + const errorCode = ER0019; rollbar.error( `[${errorCode}] ${ERROR_CODE_MSG[errorCode].internal}`, null, @@ -131,7 +135,15 @@ const FormBuilder = ({ ); console.error(`Not submittable`); } - }, [warning, isSubmittable, onValidateFail, dataKey, draft, onSubmit]); + }, [ + warning, + isSubmittable, + onValidateFail, + dataKey, + draft, + onSubmit, + dispatch, + ]); useEffect(() => { if (open) { diff --git a/src/constants/errorCodeMsg.js b/src/constants/errorCodeMsg.js index a8d8fb302..d66a8ddf5 100644 --- a/src/constants/errorCodeMsg.js +++ b/src/constants/errorCodeMsg.js @@ -4,81 +4,105 @@ const LOGIN_ERROR_MSG = const LOGIN_ERROR_MSG_CLEAN_BROWSER_DATA = '登入時發生錯誤,請清除瀏覽器資料,再重試一次。若錯誤持續發生,請聯繫 findyourgoodjob@gmail.com'; +export const ER0001 = 'ER0001'; +export const ER0002 = 'ER0002'; +export const ER0003 = 'ER0003'; +export const ER0004 = 'ER0004'; +export const ER0005 = 'ER0005'; +export const ER0006 = 'ER0006'; +export const ER0007 = 'ER0007'; +export const ER0008 = 'ER0008'; +export const ER0009 = 'ER0009'; +export const ER0010 = 'ER0010'; +export const ER0011 = 'ER0011'; +export const ER0012 = 'ER0012'; +export const ER0013 = 'ER0013'; +export const ER0014 = 'ER0014'; +export const ER0015 = 'ER0015'; +export const ER0016 = 'ER0016'; +export const ER0018 = 'ER0018'; +export const ER0019 = 'ER0019'; +export const ER0020 = 'ER0020'; +export const ER0021 = 'ER0021'; + // maintain a list of global error code export const ERROR_CODE_MSG = { - ER0001: { + [ER0001]: { external: LOGIN_ERROR_MSG, internal: 'FB SDK is not ready', }, - ER0002: { + [ER0002]: { external: LOGIN_ERROR_MSG, internal: 'FB SDK login failed', }, - ER0003: { + [ER0003]: { external: LOGIN_ERROR_MSG, internal: 'FB login response is empty or does not have status field', }, - ER0004: { + [ER0004]: { external: LOGIN_ERROR_MSG, internal: 'FB login failed: unauthorized', }, - ER0005: { + [ER0005]: { external: LOGIN_ERROR_MSG, internal: 'Graphql mutation facebookLogin failed', }, - ER0006: { + [ER0006]: { external: LOGIN_ERROR_MSG_CLEAN_BROWSER_DATA, internal: 'FB login failed: unknown auth status', }, - ER0007: { + [ER0007]: { internal: 'Submit salary failed', }, - ER0008: { + [ER0008]: { internal: 'Submit interview failed', }, - ER0009: { + [ER0009]: { external: LOGIN_ERROR_MSG, internal: 'Cannot get id_token from Google auth API', }, - ER0010: { + [ER0010]: { external: LOGIN_ERROR_MSG, internal: 'Cannot get token from Graphql googleLogin mutation response', }, - ER0011: { + [ER0011]: { external: LOGIN_ERROR_MSG, internal: 'Graphql googleLogin mutation API failed with codes=UNAUTHENTICATED', }, - ER0012: { + [ER0012]: { external: LOGIN_ERROR_MSG, internal: 'Graphql googleLogin mutation API failed with codes=FORBIDDEN, probably this user is deactivated', }, - ER0013: { + [ER0013]: { external: LOGIN_ERROR_MSG, internal: 'Unknown error during Graphql googleLogin mutation', }, - ER0014: { + [ER0014]: { external: LOGIN_ERROR_MSG, internal: 'Graphql facebookLogin mutation API failed with codes=UNAUTHENTICATED', }, - ER0015: { + [ER0015]: { external: LOGIN_ERROR_MSG, internal: 'Graphql facebookLogin mutation API failed with codes=FORBIDDEN, probably this user is deactivated', }, - ER0016: { + [ER0016]: { external: LOGIN_ERROR_MSG, internal: 'Unknown error during Graphql facebookLogin mutation', }, - ER0018: { + [ER0018]: { internal: 'Unexpected error during submitting form data', }, - ER0019: { + [ER0019]: { internal: 'Not submittable', }, - ER0020: { + [ER0020]: { internal: 'Submit work experience failed', }, + [ER0021]: { + internal: 'Warning without validation failure', + }, };