Merge pull request #506 from sharetribe/refactoring-error-storing

Refactoring error storing
This commit is contained in:
Vesa Luusua 2017-10-18 18:19:51 +03:00 committed by GitHub
commit 50dd6d05fc
54 changed files with 319 additions and 193 deletions

View file

@ -67,6 +67,30 @@ const template = params => {
return templateTags(templatedWithHtmlAttributes)(tags);
};
//
// Clean Error details when stringifying Error.
//
const cleanErrorValue = value => {
// This should not happen
// Pick only selected few values to be stringified if Error object is encountered.
// Other values might contain circular structures
// (SDK's Axios library might add ctx and config which has such structures)
if (value instanceof Error) {
const { name, message, status, statusText, apiErrors } = value;
return { type: 'error', name, message, status, statusText, apiErrors };
}
return value;
};
//
// JSON replacer
// This stringifies SDK types and errors.
//
const replacer = (key = null, value) => {
const cleanedValue = cleanErrorValue(value);
return types.replacer(key, cleanedValue);
};
exports.render = function(requestUrl, context, preloadedState) {
const { head, body } = renderApp(requestUrl, context, preloadedState);
@ -74,7 +98,7 @@ exports.render = function(requestUrl, context, preloadedState) {
// For security reasons we ensure that preloaded state is considered as a string
// by replacing '<' character with its unicode equivalent.
// http://redux.js.org/docs/recipes/ServerRendering.html#security-considerations
const serializedState = JSON.stringify(preloadedState, types.replacer).replace(/</g, '\\u003c');
const serializedState = JSON.stringify(preloadedState, replacer).replace(/</g, '\\u003c');
// At this point the serializedState is a string, the second
// JSON.stringify wraps it within double quotes and escapes the

View file

@ -6,6 +6,7 @@ import { FormattedMessage, injectIntl, intlShape } from 'react-intl';
import classNames from 'classnames';
import config from '../../config';
import { canonicalURL, metaTagProps } from '../../util/seo';
import * as propTypes from '../../util/propTypes';
import facebookImage from '../../assets/saunatimeFacebook-1200x630.jpg';
import twitterImage from '../../assets/saunatimeTwitter-600x314.jpg';
@ -164,7 +165,7 @@ class PageComponent extends Component {
}
}
const { any, arrayOf, bool, func, instanceOf, number, shape, string } = PropTypes;
const { any, arrayOf, bool, func, number, shape, string } = PropTypes;
PageComponent.defaultProps = {
className: null,
@ -190,8 +191,8 @@ PageComponent.propTypes = {
className: string,
rootClassName: string,
children: any,
authInfoError: instanceOf(Error),
logoutError: instanceOf(Error),
authInfoError: propTypes.error,
logoutError: propTypes.error,
scrollingDisabled: bool,
// SEO related props

View file

@ -267,7 +267,7 @@ SaleDetailsPanelComponent.defaultProps = {
declineSaleError: null,
};
const { string, func, bool, instanceOf } = PropTypes;
const { bool, func, string } = PropTypes;
SaleDetailsPanelComponent.propTypes = {
rootClassName: string,
@ -277,8 +277,8 @@ SaleDetailsPanelComponent.propTypes = {
onDeclineSale: func.isRequired,
acceptInProgress: bool.isRequired,
declineInProgress: bool.isRequired,
acceptSaleError: instanceOf(Error),
declineSaleError: instanceOf(Error),
acceptSaleError: propTypes.error,
declineSaleError: propTypes.error,
// from injectIntl
intl: intlShape.isRequired,

View file

@ -354,7 +354,7 @@ TopbarComponent.defaultProps = {
sendVerificationEmailError: null,
};
const { bool, func, instanceOf, number, shape, string } = PropTypes;
const { bool, func, number, shape, string } = PropTypes;
TopbarComponent.propTypes = {
className: string,
@ -373,7 +373,7 @@ TopbarComponent.propTypes = {
onManageDisableScrolling: func.isRequired,
onResendVerificationEmail: func.isRequired,
sendVerificationEmailInProgress: bool.isRequired,
sendVerificationEmailError: instanceOf(Error),
sendVerificationEmailError: propTypes.error,
// These are passed from Page to keep Topbar rendering aware of location changes
history: shape({

View file

@ -236,23 +236,23 @@ AuthenticationPageComponent.defaultProps = {
sendVerificationEmailError: null,
};
const { bool, func, instanceOf, object, oneOf, shape } = PropTypes;
const { bool, func, object, oneOf, shape } = PropTypes;
AuthenticationPageComponent.propTypes = {
authInfoError: instanceOf(Error),
authInfoError: propTypes.error,
authInProgress: bool.isRequired,
currentUser: propTypes.currentUser,
isAuthenticated: bool.isRequired,
loginError: instanceOf(Error),
logoutError: instanceOf(Error),
loginError: propTypes.error,
logoutError: propTypes.error,
scrollingDisabled: bool.isRequired,
signupError: instanceOf(Error),
signupError: propTypes.error,
submitLogin: func.isRequired,
submitSignup: func.isRequired,
tab: oneOf(['login', 'signup']),
sendVerificationEmailInProgress: bool.isRequired,
sendVerificationEmailError: instanceOf(Error),
sendVerificationEmailError: propTypes.error,
onResendVerificationEmail: func.isRequired,
// from withRouter

View file

@ -1,5 +1,6 @@
import { pick } from 'lodash';
import { updatedEntities, denormalisedEntities } from '../../util/data';
import { storableError } from '../../util/errors';
import * as propTypes from '../../util/propTypes';
import * as log from '../../util/log';
import { fetchCurrentUserHasOrdersSuccess } from '../../ducks/user.duck';
@ -118,7 +119,7 @@ export const initiateOrder = params => (dispatch, getState, sdk) => {
return orderId;
})
.catch(e => {
dispatch(initiateOrderError(e));
dispatch(initiateOrderError(storableError(e)));
log.error(e, 'initiate-order-failed', {
listingId: params.listingId.uuid,
bookingStart: params.bookingStart,
@ -174,6 +175,6 @@ export const speculateTransaction = (listingId, bookingStart, bookingEnd) => (
bookingStart: bookingStart,
bookingEnd: bookingEnd,
});
return dispatch(speculateTransactionError(e));
return dispatch(speculateTransactionError(storableError(e)));
});
};

View file

@ -385,7 +385,7 @@ CheckoutPageComponent.defaultProps = {
const { func, instanceOf, shape, string, bool } = PropTypes;
CheckoutPageComponent.propTypes = {
authInfoError: instanceOf(Error),
authInfoError: propTypes.error,
listing: propTypes.listing,
bookingDates: shape({
bookingStart: instanceOf(Date).isRequired,
@ -393,10 +393,10 @@ CheckoutPageComponent.propTypes = {
}),
fetchSpeculatedTransaction: func.isRequired,
speculateTransactionInProgress: bool.isRequired,
speculateTransactionError: instanceOf(Error),
speculateTransactionError: propTypes.error,
speculatedTransaction: propTypes.transaction,
initiateOrderError: instanceOf(Error),
logoutError: instanceOf(Error),
initiateOrderError: propTypes.error,
logoutError: propTypes.error,
currentUser: propTypes.currentUser,
params: shape({
id: string,

View file

@ -4,6 +4,7 @@ import { compose } from 'redux';
import { FormattedMessage, injectIntl, intlShape } from 'react-intl';
import { reduxForm, propTypes as formPropTypes } from 'redux-form';
import classNames from 'classnames';
import * as propTypes from '../../util/propTypes';
import * as validators from '../../util/validators';
import { ensureCurrentUser } from '../../util/data';
import {
@ -264,18 +265,18 @@ ContactDetailsFormComponent.defaultProps = {
sendVerificationEmailInProgress: false,
};
const { bool, func, instanceOf, string } = PropTypes;
const { bool, func, string } = PropTypes;
ContactDetailsFormComponent.propTypes = {
...formPropTypes,
rootClassName: string,
className: string,
changeEmailError: instanceOf(Error),
changeEmailError: propTypes.error,
inProgress: bool,
intl: intlShape.isRequired,
onResendVerificationEmail: func.isRequired,
ready: bool.isRequired,
sendVerificationEmailError: instanceOf(Error),
sendVerificationEmailError: propTypes.error,
sendVerificationEmailInProgress: bool,
};

View file

@ -1,3 +1,4 @@
import { storableError } from '../../util/errors';
import { currentUserShowSuccess } from '../../ducks/user.duck';
// ================ Action types ================ //
@ -64,5 +65,5 @@ export const changeEmail = params => (dispatch, getState, sdk) => {
dispatch(changeEmailSuccess());
dispatch(currentUserShowSuccess(currentUser));
})
.catch(e => dispatch(changeEmailError(e)));
.catch(e => dispatch(changeEmailError(storableError(e))));
};

View file

@ -112,20 +112,20 @@ ContactDetailsPageComponent.defaultProps = {
sendVerificationEmailError: null,
};
const { bool, func, instanceOf } = PropTypes;
const { bool, func } = PropTypes;
ContactDetailsPageComponent.propTypes = {
authInfoError: instanceOf(Error),
changeEmailError: instanceOf(Error),
authInfoError: propTypes.error,
changeEmailError: propTypes.error,
changeEmailInProgress: bool.isRequired,
currentUser: propTypes.currentUser,
emailChanged: bool.isRequired,
logoutError: instanceOf(Error),
logoutError: propTypes.error,
onChange: func.isRequired,
onSubmitChangeEmail: func.isRequired,
scrollingDisabled: bool.isRequired,
sendVerificationEmailInProgress: bool.isRequired,
sendVerificationEmailError: instanceOf(Error),
sendVerificationEmailError: propTypes.error,
onResendVerificationEmail: func.isRequired,
};

View file

@ -4,6 +4,7 @@ import { compose } from 'redux';
import { reduxForm, propTypes as formPropTypes } from 'redux-form';
import { intlShape, injectIntl, FormattedMessage } from 'react-intl';
import classNames from 'classnames';
import * as propTypes from '../../util/propTypes';
import { maxLength, required } from '../../util/validators';
import { Form, Button, TextInputField } from '../../components';
@ -99,7 +100,7 @@ const EditListingDescriptionFormComponent = props => {
EditListingDescriptionFormComponent.defaultProps = { className: null, updateError: null };
const { func, string, bool, instanceOf } = PropTypes;
const { bool, func, string } = PropTypes;
EditListingDescriptionFormComponent.propTypes = {
...formPropTypes,
@ -108,7 +109,7 @@ EditListingDescriptionFormComponent.propTypes = {
onSubmit: func.isRequired,
saveActionMsg: string.isRequired,
updated: bool.isRequired,
updateError: instanceOf(Error),
updateError: propTypes.error,
updateInProgress: bool.isRequired,
};

View file

@ -99,7 +99,7 @@ EditListingLocationFormComponent.defaultProps = {
updateError: null,
};
const { func, string, bool, instanceOf } = PropTypes;
const { func, string, bool } = PropTypes;
EditListingLocationFormComponent.propTypes = {
...formPropTypes,
@ -108,7 +108,7 @@ EditListingLocationFormComponent.propTypes = {
saveActionMsg: string.isRequired,
selectedPlace: propTypes.place,
updated: bool.isRequired,
updateError: instanceOf(Error),
updateError: propTypes.error,
updateInProgress: bool.isRequired,
};

View file

@ -1,5 +1,6 @@
import { omit, omitBy, isUndefined } from 'lodash';
import { types } from '../../util/sdkLoader';
import { storableError } from '../../util/errors';
import { addMarketplaceEntities } from '../../ducks/marketplaceData.duck';
import * as log from '../../util/log';
import { fetchCurrentUserHasListingsSuccess } from '../../ducks/user.duck';
@ -245,7 +246,7 @@ export function requestShowListing(actionPayload) {
dispatch(showListingsSuccess(response));
return response;
})
.catch(e => dispatch(showListingsError(e)));
.catch(e => dispatch(showListingsError(storableError(e))));
};
}
@ -281,7 +282,7 @@ export function requestCreateListing(data) {
})
.catch(e => {
log.error(e, 'create-listing-failed', { listingData: cleanedData });
return dispatch(createListingError(e));
return dispatch(createListingError(storableError(e)));
});
};
}
@ -294,7 +295,7 @@ export function requestImageUpload(actionPayload) {
return sdk.images
.uploadListingImage({ image: actionPayload.file })
.then(resp => dispatch(uploadImageSuccess({ data: { id, imageId: resp.data.data.id } })))
.catch(e => dispatch(uploadImageError({ id, error: e })));
.catch(e => dispatch(uploadImageError({ id, error: storableError(e) })));
};
}
@ -320,7 +321,7 @@ export function requestUpdateListing(tab, data) {
})
.catch(e => {
log.error(e, 'update-listing-failed', { listingData: cleanedData });
return dispatch(updateListingError(e));
return dispatch(updateListingError(storableError(e)));
});
};
}

View file

@ -177,15 +177,15 @@ EditListingPageComponent.defaultProps = {
sendVerificationEmailError: null,
};
const { bool, func, instanceOf, object, shape, string, oneOf } = PropTypes;
const { bool, func, object, shape, string, oneOf } = PropTypes;
EditListingPageComponent.propTypes = {
authInfoError: instanceOf(Error),
createStripeAccountError: instanceOf(Error),
authInfoError: propTypes.error,
createStripeAccountError: propTypes.error,
currentUser: propTypes.currentUser,
fetchInProgress: bool.isRequired,
getListing: func.isRequired,
logoutError: instanceOf(Error),
logoutError: propTypes.error,
onCreateListing: func.isRequired,
onCreateListingDraft: func.isRequired,
onImageUpload: func.isRequired,

View file

@ -6,6 +6,7 @@ import { FormattedMessage, intlShape, injectIntl } from 'react-intl';
import { isEqual } from 'lodash';
import { arrayMove } from 'react-sortable-hoc';
import classNames from 'classnames';
import * as propTypes from '../../util/propTypes';
import { noEmptyArray } from '../../util/validators';
import { isUploadListingImageOverLimitError } from '../../util/errors';
import { Form, AddImages, Button, ValidationError } from '../../components';
@ -31,7 +32,7 @@ const RenderAddImage = props => {
);
};
const { func, node, object, shape, string, bool, instanceOf } = PropTypes;
const { bool, func, node, object, shape, string } = PropTypes;
RenderAddImage.propTypes = {
accept: string.isRequired,
@ -231,7 +232,7 @@ EditListingPhotosFormComponent.propTypes = {
saveActionMsg: string.isRequired,
updated: bool.isRequired,
ready: bool.isRequired,
updateError: instanceOf(Error),
updateError: propTypes.error,
updateInProgress: bool.isRequired,
onRemoveImage: func.isRequired,
};

View file

@ -5,6 +5,7 @@ import { reduxForm, propTypes as formPropTypes } from 'redux-form';
import { intlShape, injectIntl, FormattedMessage } from 'react-intl';
import classNames from 'classnames';
import config from '../../config';
import * as propTypes from '../../util/propTypes';
import { required } from '../../util/validators';
import { Form, Button, CurrencyInputField } from '../../components';
@ -70,7 +71,7 @@ export const EditListingPricingFormComponent = props => {
EditListingPricingFormComponent.defaultProps = { updateError: null };
const { func, string, bool, instanceOf } = PropTypes;
const { bool, func, string } = PropTypes;
EditListingPricingFormComponent.propTypes = {
...formPropTypes,
@ -78,7 +79,7 @@ EditListingPricingFormComponent.propTypes = {
onSubmit: func.isRequired,
saveActionMsg: string.isRequired,
updated: bool.isRequired,
updateError: instanceOf(Error),
updateError: propTypes.error,
updateInProgress: bool.isRequired,
};

View file

@ -94,13 +94,13 @@ EmailVerificationFormComponent.defaultProps = {
verificationError: null,
};
const { instanceOf, bool } = PropTypes;
const { bool } = PropTypes;
EmailVerificationFormComponent.propTypes = {
...formPropTypes,
inProgress: bool,
currentUser: propTypes.currentUser.isRequired,
verificationError: instanceOf(Error),
verificationError: propTypes.error,
};
const defaultFormName = 'EmailVerificationForm';

View file

@ -87,16 +87,16 @@ EmailVerificationPageComponent.defaultProps = {
verificationError: null,
};
const { bool, func, instanceOf, shape, string } = PropTypes;
const { bool, func, shape, string } = PropTypes;
EmailVerificationPageComponent.propTypes = {
authInfoError: instanceOf(Error),
authInfoError: propTypes.error,
currentUser: propTypes.currentUser,
logoutError: instanceOf(Error),
logoutError: propTypes.error,
scrollingDisabled: bool.isRequired,
submitVerification: func.isRequired,
emailVerificationInProgress: bool.isRequired,
verificationError: instanceOf(Error),
verificationError: propTypes.error,
// from withRouter
location: shape({

View file

@ -1,4 +1,5 @@
import { reverse, sortBy } from 'lodash';
import { storableError } from '../../util/errors';
import { parse } from '../../util/urlHelpers';
import { addMarketplaceEntities } from '../../ducks/marketplaceData.duck';
@ -102,7 +103,7 @@ export const loadData = (params, search) => (dispatch, getState, sdk) => {
return response;
})
.catch(e => {
dispatch(fetchOrdersOrSalesError(e));
dispatch(fetchOrdersOrSalesError(storableError(e)));
throw e;
});
};

View file

@ -23,7 +23,7 @@ import { TopbarContainer } from '../../containers';
import { loadData } from './InboxPage.duck';
import css from './InboxPage.css';
const { arrayOf, bool, instanceOf, number, oneOf, shape, string } = PropTypes;
const { arrayOf, bool, number, oneOf, shape, string } = PropTypes;
const formatDate = (intl, date) => {
return {
@ -303,11 +303,11 @@ InboxPageComponent.propTypes = {
tab: string.isRequired,
}).isRequired,
authInfoError: instanceOf(Error),
authInfoError: propTypes.error,
currentUser: propTypes.currentUser,
fetchInProgress: bool.isRequired,
fetchOrdersOrSalesError: instanceOf(Error),
logoutError: instanceOf(Error),
fetchOrdersOrSalesError: propTypes.error,
logoutError: propTypes.error,
pagination: propTypes.pagination,
providerNotificationCount: number,
scrollingDisabled: bool.isRequired,

View file

@ -4,6 +4,7 @@ import { compose } from 'redux';
import { connect } from 'react-redux';
import { withRouter } from 'react-router-dom';
import { injectIntl, intlShape } from 'react-intl';
import * as propTypes from '../../util/propTypes';
import { isScrollingDisabled } from '../../ducks/UI.duck';
import config from '../../config';
import { Page, HeroSection } from '../../components';
@ -60,11 +61,11 @@ LandingPageComponent.defaultProps = {
logoutError: null,
};
const { bool, instanceOf, object } = PropTypes;
const { bool, object } = PropTypes;
LandingPageComponent.propTypes = {
authInfoError: instanceOf(Error),
logoutError: instanceOf(Error),
authInfoError: propTypes.error,
logoutError: propTypes.error,
scrollingDisabled: bool.isRequired,
// from withRouter

View file

@ -1,3 +1,4 @@
import { storableError } from '../../util/errors';
import { addMarketplaceEntities } from '../../ducks/marketplaceData.duck';
import { fetchCurrentUser } from '../../ducks/user.duck';
@ -50,6 +51,6 @@ export const showListing = listingId => (dispatch, getState, sdk) => {
return data;
})
.catch(e => {
dispatch(showListingError(e));
dispatch(showListingError(storableError(e)));
});
};

View file

@ -83,7 +83,7 @@ export const ActionBar = props => {
}
};
const { bool, func, instanceOf, object, oneOf, shape, string } = PropTypes;
const { bool, func, object, oneOf, shape, string } = PropTypes;
ActionBar.propTypes = {
isOwnListing: bool.isRequired,
@ -495,13 +495,13 @@ ListingPageComponent.propTypes = {
id: string.isRequired,
slug: string,
}).isRequired,
authInfoError: instanceOf(Error),
authInfoError: propTypes.error,
currentUser: propTypes.currentUser,
getListing: func.isRequired,
logoutError: instanceOf(Error),
logoutError: propTypes.error,
onManageDisableScrolling: func.isRequired,
scrollingDisabled: bool.isRequired,
showListingError: instanceOf(Error),
showListingError: propTypes.error,
tab: oneOf(['book', 'listing']),
useInitialValues: func.isRequired,
};

View file

@ -3,6 +3,7 @@ import { shallow } from 'enzyme';
import { FormattedMessage } from 'react-intl';
import { types } from '../../util/sdkLoader';
import { createUser, createCurrentUser, createListing, fakeIntl } from '../../util/test-data';
import { storableError } from '../../util/errors';
import { renderShallow } from '../../util/test-helpers';
import { addMarketplaceEntities } from '../../ducks/marketplaceData.duck';
import { showListingRequest, showListingError, showListing } from './ListingPage.duck';
@ -84,7 +85,7 @@ describe('ListingPage', () => {
expect(dispatch.mock.calls).toEqual([
[showListingRequest(id)],
[expect.anything()], // fetchCurrentUser() call
[showListingError(error)],
[showListingError(storableError(error))],
]);
});
});

View file

@ -1,4 +1,5 @@
import { updatedEntities, denormalisedEntities } from '../../util/data';
import { storableError } from '../../util/errors';
// ================ Action types ================ //
@ -246,7 +247,7 @@ export const queryOwnListings = queryParams => (dispatch, getState, sdk) => {
return response;
})
.catch(e => {
dispatch(queryListingsError(e));
dispatch(queryListingsError(storableError(e)));
throw e;
});
};
@ -261,7 +262,7 @@ export const closeListing = listingId => (dispatch, getState, sdk) => {
return response;
})
.catch(e => {
dispatch(closeListingError(e));
dispatch(closeListingError(storableError(e)));
});
};
@ -275,6 +276,6 @@ export const openListing = listingId => (dispatch, getState, sdk) => {
return response;
})
.catch(e => {
dispatch(openListingError(e));
dispatch(openListingError(storableError(e)));
});
};

View file

@ -152,27 +152,27 @@ ManageListingsPageComponent.defaultProps = {
openingListingError: null,
};
const { arrayOf, bool, func, instanceOf, object, shape, string } = PropTypes;
const { arrayOf, bool, func, object, shape, string } = PropTypes;
ManageListingsPageComponent.propTypes = {
authInfoError: instanceOf(Error),
authInfoError: propTypes.error,
closingListing: shape({ uuid: string.isRequired }),
closingListingError: shape({
listingId: propTypes.uuid.isRequired,
error: instanceOf(Error).isRequired,
error: propTypes.error.isRequired,
}),
listings: arrayOf(propTypes.listing),
logoutError: instanceOf(Error),
logoutError: propTypes.error,
onCloseListing: func.isRequired,
onOpenListing: func.isRequired,
openingListing: shape({ uuid: string.isRequired }),
openingListingError: shape({
listingId: propTypes.uuid.isRequired,
error: instanceOf(Error).isRequired,
error: propTypes.error.isRequired,
}),
pagination: propTypes.pagination,
queryInProgress: bool.isRequired,
queryListingsError: instanceOf(Error),
queryListingsError: propTypes.error,
queryParams: object,
scrollingDisabled: bool.isRequired,
};

View file

@ -5,6 +5,7 @@ import { compose } from 'redux';
import { connect } from 'react-redux';
import { withRouter } from 'react-router-dom';
import routeConfiguration from '../../routeConfiguration';
import * as propTypes from '../../util/propTypes';
import { createResourceLocatorString } from '../../util/routes';
import { isScrollingDisabled } from '../../ducks/UI.duck';
import { Page } from '../../components';
@ -68,11 +69,11 @@ NotFoundPageComponent.defaultProps = {
staticContext: {},
};
const { bool, func, instanceOf, object, shape } = PropTypes;
const { bool, func, object, shape } = PropTypes;
NotFoundPageComponent.propTypes = {
authInfoError: instanceOf(Error),
logoutError: instanceOf(Error),
authInfoError: propTypes.error,
logoutError: propTypes.error,
scrollingDisabled: bool.isRequired,
// context object from StaticRouter, injected by the withRouter wrapper

View file

@ -1,4 +1,5 @@
import { types } from '../../util/sdkLoader';
import { storableError } from '../../util/errors';
import { addMarketplaceEntities } from '../../ducks/marketplaceData.duck';
import { updatedEntities, denormalisedEntities } from '../../util/data';
@ -79,7 +80,7 @@ export const fetchOrder = id => (dispatch, getState, sdk) => {
return response;
})
.catch(e => {
dispatch(fetchOrderError(e));
dispatch(fetchOrderError(storableError(e)));
throw e;
});
};

View file

@ -87,14 +87,14 @@ OrderPageComponent.defaultProps = {
transaction: null,
};
const { bool, instanceOf, oneOf, shape, string } = PropTypes;
const { bool, oneOf, shape, string } = PropTypes;
OrderPageComponent.propTypes = {
authInfoError: instanceOf(Error),
authInfoError: propTypes.error,
currentUser: propTypes.currentUser,
fetchOrderError: instanceOf(Error),
fetchOrderError: propTypes.error,
intl: intlShape.isRequired,
logoutError: instanceOf(Error),
logoutError: propTypes.error,
params: shape({ id: string }).isRequired,
scrollingDisabled: bool.isRequired,
tab: oneOf(['details', 'discussion']).isRequired,

View file

@ -4,6 +4,7 @@ import { compose } from 'redux';
import { FormattedMessage, injectIntl, intlShape } from 'react-intl';
import { reduxForm, propTypes as formPropTypes } from 'redux-form';
import classNames from 'classnames';
import * as propTypes from '../../util/propTypes';
import * as validators from '../../util/validators';
import { ensureCurrentUser } from '../../util/data';
import { isChangePasswordWrongPassword } from '../../util/errors';
@ -183,13 +184,13 @@ PasswordChangeFormComponent.defaultProps = {
inProgress: false,
};
const { bool, instanceOf, string } = PropTypes;
const { bool, string } = PropTypes;
PasswordChangeFormComponent.propTypes = {
...formPropTypes,
rootClassName: string,
className: string,
changePasswordError: instanceOf(Error),
changePasswordError: propTypes.error,
inProgress: bool,
intl: intlShape.isRequired,
ready: bool.isRequired,

View file

@ -1,3 +1,5 @@
import { storableError } from '../../util/errors';
// ================ Action types ================ //
export const CHANGE_PASSWORD_REQUEST = 'app/PasswordChangePage/CHANGE_PASSWORD_REQUEST';
@ -59,7 +61,7 @@ export const changePassword = params => (dispatch, getState, sdk) => {
.changePassword({ newPassword, currentPassword })
.then(() => dispatch(changePasswordSuccess()))
.catch(e => {
dispatch(changePasswordError(e));
dispatch(changePasswordError(storableError(storableError(e))));
// This is thrown so that form can be cleared
// after a timeout on changePassword submit handler
throw e;

View file

@ -101,14 +101,14 @@ PasswordChangePageComponent.defaultProps = {
logoutError: null,
};
const { bool, func, instanceOf } = PropTypes;
const { bool, func } = PropTypes;
PasswordChangePageComponent.propTypes = {
authInfoError: instanceOf(Error),
changePasswordError: instanceOf(Error),
authInfoError: propTypes.error,
changePasswordError: propTypes.error,
changePasswordInProgress: bool.isRequired,
currentUser: propTypes.currentUser,
logoutError: instanceOf(Error),
logoutError: propTypes.error,
onChange: func.isRequired,
onSubmitChangePassword: func.isRequired,
passwordChanged: bool.isRequired,

View file

@ -4,9 +4,10 @@ import { compose } from 'redux';
import { FormattedMessage, injectIntl, intlShape } from 'react-intl';
import { reduxForm, propTypes as formPropTypes } from 'redux-form';
import classNames from 'classnames';
import { Form, PrimaryButton, TextInputField, NamedLink } from '../../components';
import * as propTypes from '../../util/propTypes';
import * as validators from '../../util/validators';
import { isPasswordRecoveryEmailNotFoundError } from '../../util/errors';
import { Form, PrimaryButton, TextInputField, NamedLink } from '../../components';
import css from './PasswordRecoveryForm.css';
@ -96,7 +97,7 @@ PasswordRecoveryFormComponent.defaultProps = {
recoveryError: null,
};
const { instanceOf, string, bool } = PropTypes;
const { bool, string } = PropTypes;
PasswordRecoveryFormComponent.propTypes = {
...formPropTypes,
@ -104,7 +105,7 @@ PasswordRecoveryFormComponent.propTypes = {
className: string,
inProgress: bool,
recoveryError: instanceOf(Error),
recoveryError: propTypes.error,
// from injectIntl
intl: intlShape.isRequired,

View file

@ -1,3 +1,5 @@
import { storableError } from '../../util/errors';
// ================ Action types ================ //
export const RECOVERY_REQUEST = 'app/PasswordRecoveryPage/RECOVERY_REQUEST';
@ -75,5 +77,5 @@ export const recoverPassword = email => (dispatch, getState, sdk) => {
return sdk.passwordReset
.request({ email })
.then(() => dispatch(passwordRecoverySuccess(email)))
.catch(error => dispatch(passwordRecoveryError(error, email)));
.catch(e => dispatch(passwordRecoveryError(storableError(e), email)));
};

View file

@ -3,19 +3,20 @@ import PropTypes from 'prop-types';
import { compose } from 'redux';
import { connect } from 'react-redux';
import { FormattedMessage, injectIntl, intlShape } from 'react-intl';
import * as propTypes from '../../util/propTypes';
import {
isPasswordRecoveryEmailNotFoundError,
isPasswordRecoveryEmailNotVerifiedError,
} from '../../util/errors';
import { isScrollingDisabled } from '../../ducks/UI.duck';
import { Page, InlineTextButton, IconKeys } from '../../components';
import { PasswordRecoveryForm, TopbarContainer } from '../../containers';
import {
recoverPassword,
retypePasswordRecoveryEmail,
clearPasswordRecoveryError,
} from './PasswordRecoveryPage.duck';
import { Page, InlineTextButton, IconKeys } from '../../components';
import { PasswordRecoveryForm, TopbarContainer } from '../../containers';
import DoorIcon from './DoorIcon';
import css from './PasswordRecoveryPage.css';
@ -166,14 +167,14 @@ PasswordRecoveryPageComponent.defaultProps = {
recoveryError: null,
};
const { bool, func, instanceOf, string } = PropTypes;
const { bool, func, string } = PropTypes;
PasswordRecoveryPageComponent.propTypes = {
authInfoError: instanceOf(Error),
logoutError: instanceOf(Error),
authInfoError: propTypes.error,
logoutError: propTypes.error,
initialEmail: string,
submittedEmail: string,
recoveryError: instanceOf(Error),
recoveryError: propTypes.error,
recoveryInProgress: bool.isRequired,
passwordRequested: bool.isRequired,
onChange: func.isRequired,

View file

@ -1,3 +1,5 @@
import { storableError } from '../../util/errors';
// ================ Action types ================ //
export const RESET_PASSWORD_REQUEST = 'app/PasswordResetPage/RESET_PASSWORD_REQUEST';
@ -50,5 +52,5 @@ export const resetPassword = (email, passwordResetToken, newPassword) => (
return sdk.passwordReset
.reset(params)
.then(() => dispatch(resetPasswordSuccess()))
.catch(e => dispatch(resetPasswordError(e)));
.catch(e => dispatch(resetPasswordError(storableError(e))));
};

View file

@ -4,8 +4,9 @@ import { compose } from 'redux';
import { connect } from 'react-redux';
import { withRouter } from 'react-router-dom';
import { FormattedMessage, injectIntl, intlShape } from 'react-intl';
import { isScrollingDisabled } from '../../ducks/UI.duck';
import * as propTypes from '../../util/propTypes';
import { parse } from '../../util/urlHelpers';
import { isScrollingDisabled } from '../../ducks/UI.duck';
import { Page, NamedLink, IconKeys, IconKeysSuccess } from '../../components';
import { PasswordResetForm, TopbarContainer } from '../../containers';
@ -130,14 +131,14 @@ PasswordResetPageComponent.defaultProps = {
resetPasswordError: null,
};
const { bool, func, instanceOf, shape, string } = PropTypes;
const { bool, func, shape, string } = PropTypes;
PasswordResetPageComponent.propTypes = {
authInfoError: instanceOf(Error),
logoutError: instanceOf(Error),
authInfoError: propTypes.error,
logoutError: propTypes.error,
scrollingDisabled: bool.isRequired,
resetPasswordInProgress: bool.isRequired,
resetPasswordError: instanceOf(Error),
resetPasswordError: propTypes.error,
onSubmitPassword: func.isRequired,
// from withRouter

View file

@ -2,6 +2,7 @@ import React from 'react';
import PropTypes from 'prop-types';
import { compose } from 'redux';
import { connect } from 'react-redux';
import * as propTypes from '../../util/propTypes';
import { isScrollingDisabled } from '../../ducks/UI.duck';
import { Page } from '../../components';
import { TopbarContainer } from '../../containers';
@ -26,11 +27,11 @@ PayoutPreferencesPageComponent.defaultProps = {
logoutError: null,
};
const { bool, instanceOf } = PropTypes;
const { bool } = PropTypes;
PayoutPreferencesPageComponent.propTypes = {
authInfoError: instanceOf(Error),
logoutError: instanceOf(Error),
authInfoError: propTypes.error,
logoutError: propTypes.error,
scrollingDisabled: bool.isRequired,
};

View file

@ -2,6 +2,7 @@ import React from 'react';
import PropTypes from 'prop-types';
import { compose } from 'redux';
import { connect } from 'react-redux';
import * as propTypes from '../../util/propTypes';
import { isScrollingDisabled } from '../../ducks/UI.duck';
import { Page } from '../../components';
import { TopbarContainer } from '../../containers';
@ -26,11 +27,11 @@ ProfilePageComponent.defaultProps = {
logoutError: null,
};
const { bool, instanceOf, shape, string } = PropTypes;
const { bool, shape, string } = PropTypes;
ProfilePageComponent.propTypes = {
authInfoError: instanceOf(Error),
logoutError: instanceOf(Error),
authInfoError: propTypes.error,
logoutError: propTypes.error,
params: shape({ displayName: string.isRequired }).isRequired,
scrollingDisabled: bool.isRequired,
};

View file

@ -5,6 +5,7 @@ import { FormattedMessage, injectIntl, intlShape } from 'react-intl';
import { Field, reduxForm, propTypes as formPropTypes } from 'redux-form';
import classNames from 'classnames';
import { ensureCurrentUser } from '../../util/data';
import * as propTypes from '../../util/propTypes';
import * as validators from '../../util/validators';
import { isUploadProfileImageOverLimitError } from '../../util/errors';
import { Form, Avatar, Button, ImageFromFile, IconSpinner, TextInputField } from '../../components';
@ -60,7 +61,7 @@ const RenderAvatar = props => {
};
RenderAvatar.defaultProps = { uploadImageError: null };
const { bool, func, instanceOf, node, object, shape, string } = PropTypes;
const { bool, func, node, object, shape, string } = PropTypes;
RenderAvatar.propTypes = {
accept: string.isRequired,
@ -73,7 +74,7 @@ RenderAvatar.propTypes = {
}).isRequired,
label: node.isRequired,
type: string.isRequired,
uploadImageError: instanceOf(Error),
uploadImageError: propTypes.error,
};
class ProfileSettingsFormComponent extends Component {
@ -293,10 +294,10 @@ ProfileSettingsFormComponent.propTypes = {
rootClassName: string,
className: string,
uploadImageError: instanceOf(Error),
uploadImageError: propTypes.error,
uploadInProgress: bool.isRequired,
updateInProgress: bool.isRequired,
updateProfileError: instanceOf(Error),
updateProfileError: propTypes.error,
updateProfileReady: bool,
// from injectIntl

View file

@ -1,4 +1,5 @@
import { updatedEntities, denormalisedEntities } from '../../util/data';
import { storableError } from '../../util/errors';
import { currentUserShowSuccess } from '../../ducks/user.duck';
// ================ Action types ================ //
@ -120,7 +121,7 @@ export function uploadImage(actionPayload) {
const uploadedImage = resp.data.data;
dispatch(uploadImageSuccess({ data: { id, uploadedImage } }));
})
.catch(e => dispatch(uploadImageError({ id, error: e })));
.catch(e => dispatch(uploadImageError({ id, error: storableError(e) })));
};
}
@ -142,6 +143,6 @@ export const updateProfile = actionPayload => {
// Update current user in state.user.currentUser through user.duck.js
dispatch(currentUserShowSuccess(currentUser));
})
.catch(e => dispatch(updateProfileError(e)));
.catch(e => dispatch(updateProfileError(storableError(e))));
};
};

View file

@ -114,10 +114,10 @@ ProfileSettingsPageComponent.defaultProps = {
image: null,
};
const { bool, func, instanceOf, object, shape, string } = PropTypes;
const { bool, func, object, shape, string } = PropTypes;
ProfileSettingsPageComponent.propTypes = {
authInfoError: instanceOf(Error),
authInfoError: propTypes.error,
currentUser: propTypes.currentUser,
image: shape({
id: string,
@ -125,14 +125,14 @@ ProfileSettingsPageComponent.propTypes = {
file: object,
uploadedImage: propTypes.image,
}),
logoutError: instanceOf(Error),
logoutError: propTypes.error,
onChange: func.isRequired,
onImageUpload: func.isRequired,
onUpdateProfile: func.isRequired,
scrollingDisabled: bool.isRequired,
updateInProgress: bool.isRequired,
updateProfileError: instanceOf(Error),
uploadImageError: instanceOf(Error),
updateProfileError: propTypes.error,
uploadImageError: propTypes.error,
uploadInProgress: bool.isRequired,
};

View file

@ -1,4 +1,5 @@
import { types } from '../../util/sdkLoader';
import { storableError } from '../../util/errors';
import * as propTypes from '../../util/propTypes';
import * as log from '../../util/log';
import { addMarketplaceEntities } from '../../ducks/marketplaceData.duck';
@ -122,7 +123,7 @@ export const fetchSale = id => (dispatch, getState, sdk) => {
return response;
})
.catch(e => {
dispatch(fetchSaleError(e));
dispatch(fetchSaleError(storableError(e)));
throw e;
});
};
@ -142,7 +143,7 @@ export const acceptSale = id => (dispatch, getState, sdk) => {
return response;
})
.catch(e => {
dispatch(acceptSaleError(e));
dispatch(acceptSaleError(storableError(e)));
log.error(e, 'accept-sale-failed', {
txId: id,
transition: propTypes.TX_TRANSITION_ACCEPT,
@ -166,7 +167,7 @@ export const declineSale = id => (dispatch, getState, sdk) => {
return response;
})
.catch(e => {
dispatch(declineSaleError(e));
dispatch(declineSaleError(storableError(e)));
log.error(e, 'redect-sale-failed', {
txId: id,
transition: propTypes.TX_TRANSITION_DECLINE,

View file

@ -103,18 +103,18 @@ SalePageComponent.defaultProps = {
transaction: null,
};
const { bool, func, instanceOf, oneOf, shape, string } = PropTypes;
const { bool, func, oneOf, shape, string } = PropTypes;
SalePageComponent.propTypes = {
authInfoError: instanceOf(Error),
authInfoError: propTypes.error,
currentUser: propTypes.currentUser,
fetchSaleError: instanceOf(Error),
acceptSaleError: instanceOf(Error),
declineSaleError: instanceOf(Error),
fetchSaleError: propTypes.error,
acceptSaleError: propTypes.error,
declineSaleError: propTypes.error,
acceptInProgress: bool.isRequired,
declineInProgress: bool.isRequired,
intl: intlShape.isRequired,
logoutError: instanceOf(Error),
logoutError: propTypes.error,
onAcceptSale: func.isRequired,
onDeclineSale: func.isRequired,
params: shape({ id: string }).isRequired,

View file

@ -1,4 +1,5 @@
import { unionWith } from 'lodash';
import { storableError } from '../../util/errors';
import { addMarketplaceEntities } from '../../ducks/marketplaceData.duck';
// ================ Action types ================ //
@ -124,7 +125,7 @@ export const searchListings = searchParams => (dispatch, getState, sdk) => {
return response;
})
.catch(e => {
dispatch(searchListingsError(e));
dispatch(searchListingsError(storableError(e)));
throw e;
});
};
@ -146,7 +147,7 @@ export const searchMapListings = searchParams => (dispatch, getState, sdk) => {
return response;
})
.catch(e => {
dispatch(searchMapListingsError(e));
dispatch(searchMapListingsError(storableError(e)));
throw e;
});
};

View file

@ -354,19 +354,19 @@ SearchPageComponent.defaultProps = {
tab: 'listings',
};
const { array, bool, func, instanceOf, oneOf, object, shape, string } = PropTypes;
const { array, bool, func, oneOf, object, shape, string } = PropTypes;
SearchPageComponent.propTypes = {
authInfoError: instanceOf(Error),
authInfoError: propTypes.error,
listings: array,
mapListings: array,
logoutError: instanceOf(Error),
logoutError: propTypes.error,
onManageDisableScrolling: func.isRequired,
onSearchMapListings: func.isRequired,
pagination: propTypes.pagination,
scrollingDisabled: bool.isRequired,
searchInProgress: bool.isRequired,
searchListingsError: instanceOf(Error),
searchListingsError: propTypes.error,
searchParams: object,
tab: oneOf(['filters', 'listings', 'map']).isRequired,

View file

@ -2,6 +2,7 @@ import React from 'react';
import PropTypes from 'prop-types';
import { compose } from 'redux';
import { connect } from 'react-redux';
import * as propTypes from '../../util/propTypes';
import { isScrollingDisabled } from '../../ducks/UI.duck';
import { Page } from '../../components';
import { TopbarContainer } from '../../containers';
@ -26,11 +27,11 @@ SecurityPageComponent.defaultProps = {
logoutError: null,
};
const { bool, instanceOf } = PropTypes;
const { bool } = PropTypes;
SecurityPageComponent.propTypes = {
authInfoError: instanceOf(Error),
logoutError: instanceOf(Error),
authInfoError: propTypes.error,
logoutError: propTypes.error,
scrollingDisabled: bool.isRequired,
};

View file

@ -54,7 +54,7 @@ TopbarContainerComponent.defaultProps = {
sendVerificationEmailError: null,
};
const { bool, func, instanceOf, number, object, shape } = PropTypes;
const { bool, func, number, object, shape } = PropTypes;
TopbarContainerComponent.propTypes = {
authInProgress: bool.isRequired,
@ -66,7 +66,7 @@ TopbarContainerComponent.propTypes = {
onLogout: func.isRequired,
onManageDisableScrolling: func.isRequired,
sendVerificationEmailInProgress: bool.isRequired,
sendVerificationEmailError: instanceOf(Error),
sendVerificationEmailError: propTypes.error,
onResendVerificationEmail: func.isRequired,
// from withRouter

View file

@ -1,4 +1,5 @@
import { clearCurrentUser, fetchCurrentUser } from './user.duck';
import { storableError } from '../util/errors';
import * as log from '../util/log';
const authenticated = authInfo => authInfo.grantType === 'refresh_token';
@ -124,7 +125,7 @@ export const authInfo = () => (dispatch, getState, sdk) => {
return sdk
.authInfo()
.then(info => dispatch(authInfoSuccess(info)))
.catch(e => dispatch(authInfoError(e)));
.catch(e => dispatch(authInfoError(storableError(e))));
};
export const login = (username, password) => (dispatch, getState, sdk) => {
@ -139,7 +140,7 @@ export const login = (username, password) => (dispatch, getState, sdk) => {
.login({ username, password })
.then(() => dispatch(loginSuccess()))
.then(() => dispatch(fetchCurrentUser()))
.catch(e => dispatch(loginError(e)));
.catch(e => dispatch(loginError(storableError(e))));
};
export const logout = () => (dispatch, getState, sdk) => {
@ -158,7 +159,7 @@ export const logout = () => (dispatch, getState, sdk) => {
dispatch(userLogout());
log.clearUserId();
})
.catch(e => dispatch(logoutError(e)));
.catch(e => dispatch(logoutError(storableError(e))));
};
export const signup = params => (dispatch, getState, sdk) => {
@ -175,7 +176,7 @@ export const signup = params => (dispatch, getState, sdk) => {
.then(() => dispatch(signupSuccess()))
.then(() => dispatch(login(email, password)))
.catch(e => {
dispatch(signupError(e));
dispatch(signupError(storableError(e)));
log.error(e, 'signup-failed', {
email: params.email,
firstName: params.firstName,

View file

@ -1,3 +1,4 @@
import { storableError } from '../util/errors';
import { clearCurrentUser, currentUserShowRequest, currentUserShowSuccess } from './user.duck';
import reducer, {
authenticationInProgress,
@ -222,7 +223,7 @@ describe('Auth duck', () => {
return login(username, password)(dispatch, getState, sdk).then(() => {
expect(sdk.login.mock.calls).toEqual([[{ username, password }]]);
expect(dispatch.mock.calls).toEqual([[loginRequest()], [loginError(error)]]);
expect(dispatch.mock.calls).toEqual([[loginRequest()], [loginError(storableError(error))]]);
});
});
it('should reject if another login is in progress', () => {
@ -293,7 +294,10 @@ describe('Auth duck', () => {
return logout()(dispatch, getState, sdk).then(() => {
expect(sdk.logout.mock.calls.length).toEqual(1);
expect(dispatch.mock.calls).toEqual([[logoutRequest()], [logoutError(error)]]);
expect(dispatch.mock.calls).toEqual([
[logoutRequest()],
[logoutError(storableError(error))],
]);
});
});
it('should reject if another logout is in progress', () => {
@ -384,7 +388,10 @@ describe('Auth duck', () => {
return signup(params)(dispatch, getState, sdk).then(() => {
expect(sdk.currentUser.create.mock.calls).toEqual([[params]]);
expect(dispatchedActions(dispatch)).toEqual([signupRequest(), signupError(error)]);
expect(dispatchedActions(dispatch)).toEqual([
signupRequest(),
signupError(storableError(error)),
]);
});
});
});

View file

@ -1,3 +1,4 @@
import { storableError } from '../util/errors';
import { fetchCurrentUser } from './user.duck';
// ================ Action types ================ //
@ -64,5 +65,5 @@ export const verify = verificationToken => (dispatch, getState, sdk) => {
.verifyEmail({ verificationToken })
.then(() => dispatch(verificationSuccess()))
.then(() => dispatch(fetchCurrentUser()))
.catch(e => dispatch(verificationError(e)));
.catch(e => dispatch(verificationError(storableError(e))));
};

View file

@ -1,7 +1,8 @@
import { authInfo } from './Auth.duck';
import { updatedEntities, denormalisedEntities } from '../util/data';
import { storableError } from '../util/errors';
import { TX_TRANSITION_PREAUTHORIZE } from '../util/propTypes';
import * as log from '../util/log';
import { authInfo } from './Auth.duck';
// ================ Action types ================ //
@ -268,7 +269,7 @@ export const fetchCurrentUserHasListings = () => (dispatch, getState, sdk) => {
})
.catch(e => {
// TODO: dispatch flash message: "Something went wrong while retrieving user info"
dispatch(fetchCurrentUserHasListingsError(e));
dispatch(fetchCurrentUserHasListingsError(storableError(e)));
});
};
@ -294,7 +295,7 @@ export const fetchCurrentUserHasOrders = () => (dispatch, getState, sdk) => {
})
.catch(e => {
// TODO: dispatch flash message: "Something went wrong while retrieving user info"
dispatch(fetchCurrentUserHasOrdersError(e));
dispatch(fetchCurrentUserHasOrdersError(storableError(e)));
});
};
@ -319,7 +320,7 @@ export const fetchCurrentUserNotifications = () => (dispatch, getState, sdk) =>
})
.catch(e => {
// TODO: dispatch flash message: "Something went wrong while retrieving user info"
dispatch(fetchCurrentUserNotificationsError(e));
dispatch(fetchCurrentUserNotificationsError(storableError(e)));
throw e;
});
};
@ -362,7 +363,7 @@ export const fetchCurrentUser = () => (dispatch, getState, sdk) => {
dispatch(authInfo());
// TODO: dispatch flash message: "Something went wrong while retrieving user info"
dispatch(currentUserShowError(e));
dispatch(currentUserShowError(storableError(e)));
});
};
@ -381,7 +382,7 @@ export const createStripeAccount = payoutDetails => (dispatch, getState, sdk) =>
dispatch(stripeAccountCreateSuccess(accountResponse));
})
.catch(e => {
dispatch(stripeAccountCreateError(e));
dispatch(stripeAccountCreateError(storableError(e)));
log.error(e, 'create-stripe-account-failed');
throw e;
});
@ -395,5 +396,5 @@ export const sendVerificationEmail = () => (dispatch, getState, sdk) => {
return sdk.currentUser
.sendVerificationEmail()
.then(() => dispatch(sendVerificationEmailSuccess()))
.catch(e => dispatch(sendVerificationEmailError(e)));
.catch(e => dispatch(sendVerificationEmailError(storableError(e))));
};

View file

@ -9,35 +9,42 @@
* name and the docstring of the function to ensure correct usage.
*/
const ERROR_CODE_TRANSITION_PARAMETER_VALIDATION_FAILED = 'transition-parameter-validation-failed';
const ERROR_CODE_PAYMENT_FAILED = 'transaction-payment-failed';
const ERROR_CODE_EMAIL_TAKEN = 'email-taken';
const ERROR_CODE_TOO_MANY_VERIFICATION_REQUESTS = 'email-too-many-verification-requests';
const ERROR_CODE_UPLOAD_OVER_LIMIT = 'request-upload-over-limit';
import {
ERROR_CODE_TRANSITION_PARAMETER_VALIDATION_FAILED,
ERROR_CODE_PAYMENT_FAILED,
ERROR_CODE_EMAIL_TAKEN,
ERROR_CODE_TOO_MANY_VERIFICATION_REQUESTS,
ERROR_CODE_UPLOAD_OVER_LIMIT,
} from './propTypes';
const responseErrors = apiError => {
return apiError && apiError.data && apiError.data.errors ? apiError.data.errors : [];
const errorAPIErrors = error => {
return error && error.apiErrors ? error.apiErrors : [];
};
const hasErrorWithCode = (apiError, code) => {
return responseErrors(apiError).some(error => {
return error.code === code;
const hasErrorWithCode = (error, code) => {
return errorAPIErrors(error).some(apiError => {
return apiError.code === code;
});
};
/**
* return apiErrors from error response
*/
const responseAPIErrors = error => {
return error && error.data && error.data.errors ? error.data.errors : [];
};
/**
* Check if the given API error (from `sdk.currentuser.create()`) is
* due to the email address already being in use.
*/
export const isSignupEmailTakenError = apiError =>
hasErrorWithCode(apiError, ERROR_CODE_EMAIL_TAKEN);
export const isSignupEmailTakenError = error => hasErrorWithCode(error, ERROR_CODE_EMAIL_TAKEN);
/**
* Check if the given API error (from `sdk.currentuser.changeEmail()`) is
* due to the email address already being in use.
*/
export const isChangeEmailTakenError = apiError =>
hasErrorWithCode(apiError, ERROR_CODE_EMAIL_TAKEN);
export const isChangeEmailTakenError = error => hasErrorWithCode(error, ERROR_CODE_EMAIL_TAKEN);
/**
* Check if the given API error (from
@ -48,45 +55,44 @@ export const isChangeEmailTakenError = apiError =>
* allowed, and the user has to wait for them to expire to be able to
* request sending new verification emails.
*/
export const isTooManyEmailVerificationRequestsError = apiError =>
hasErrorWithCode(apiError, ERROR_CODE_TOO_MANY_VERIFICATION_REQUESTS);
export const isTooManyEmailVerificationRequestsError = error =>
hasErrorWithCode(error, ERROR_CODE_TOO_MANY_VERIFICATION_REQUESTS);
/**
* Check if the given API error (from
* `sdk.images.uploadListingImage()`) is due to the image being over
* the size limit.
*/
export const isUploadListingImageOverLimitError = apiError =>
hasErrorWithCode(apiError, ERROR_CODE_UPLOAD_OVER_LIMIT);
export const isUploadListingImageOverLimitError = error =>
hasErrorWithCode(error, ERROR_CODE_UPLOAD_OVER_LIMIT);
/**
* Check if the given API error (from
* `sdk.images.uploadProfileImage()`) is due to the image being over
* the size limit.
*/
export const isUploadProfileImageOverLimitError = apiError =>
hasErrorWithCode(apiError, ERROR_CODE_UPLOAD_OVER_LIMIT);
export const isUploadProfileImageOverLimitError = error =>
hasErrorWithCode(error, ERROR_CODE_UPLOAD_OVER_LIMIT);
/**
* Check if the given API error (from `sdk.passwordReset.request()`)
* is due to no user having the given email address.
*/
export const isPasswordRecoveryEmailNotFoundError = apiError => apiError && apiError.status === 404;
export const isPasswordRecoveryEmailNotFoundError = error => error && error.status === 404;
/**
* Check if the given API error (from `sdk.passwordReset.request()`)
* is due to the email not being verified, preventing the reset.
*/
export const isPasswordRecoveryEmailNotVerifiedError = apiError =>
apiError && apiError.status === 409;
export const isPasswordRecoveryEmailNotVerifiedError = error => error && error.status === 409;
/**
* Check if the given API error (from `sdk.transaction.initiate()` or
* `sdk.transaction.initiateSpeculative()`) is due to the listing
* being closed or deleted.
*/
export const isTransactionInitiateListingNotFoundError = apiError => {
return responseErrors(apiError).some(error => {
export const isTransactionInitiateListingNotFoundError = error => {
return responseAPIErrors(error).some(apiError => {
let notfound = false;
try {
@ -94,12 +100,12 @@ export const isTransactionInitiateListingNotFoundError = apiError => {
// the deleted information from the error and should be changed
// to a proper error code when the API supports a specific code
// for this.
notfound = error.details.data.rep[0][1].reason === 'listing-not-found';
notfound = apiError.details.data.rep[0][1].reason === 'listing-not-found';
} catch (e) {
// Ignore
}
return error.code === ERROR_CODE_TRANSITION_PARAMETER_VALIDATION_FAILED && notfound;
return apiError.code === ERROR_CODE_TRANSITION_PARAMETER_VALIDATION_FAILED && notfound;
});
};
@ -107,15 +113,16 @@ export const isTransactionInitiateListingNotFoundError = apiError => {
* Check if the given API error (from `sdk.transaction.initiate()`) is
* due to the transaction total amount being too low for Stripe.
*/
export const isTransactionInitiateAmountTooLowError = apiError => {
return responseErrors(apiError).some(error => {
const isPaymentFailedError = error.status === 402 && error.code === ERROR_CODE_PAYMENT_FAILED;
export const isTransactionInitiateAmountTooLowError = error => {
return responseAPIErrors(error).some(apiError => {
const isPaymentFailedError =
apiError.status === 402 && apiError.code === ERROR_CODE_PAYMENT_FAILED;
let isAmountTooLow = false;
try {
// TODO: This is a temporary solution until a proper error code
// for this specific error is received in the response.
const msg = error.details.msg;
const msg = apiError.details.msg;
isAmountTooLow = msg.startsWith('Amount must be at least');
} catch (e) {
// Ignore
@ -129,10 +136,26 @@ export const isTransactionInitiateAmountTooLowError = apiError => {
* Check if the given API error (from `sdk.currentUser.changeEmail(params)`)
* is due to giving wrong password.
*/
export const isChangeEmailWrongPassword = apiError => apiError && apiError.status === 403;
export const isChangeEmailWrongPassword = error => error && error.status === 403;
/**
* Check if the given API error (from `sdk.currentUser.changePassword(params)`)
* is due to giving wrong password.
*/
export const isChangePasswordWrongPassword = apiError => apiError && apiError.status === 403;
export const isChangePasswordWrongPassword = error => error && error.status === 403;
export const storableError = error => {
const { name, message, status, statusText } = error;
// Status, statusText, and data.errors are (possibly) added to the error object by SDK
const apiErrors = responseAPIErrors(error);
// Returned object is the same as prop type check in util/proptypes -> error
return {
type: 'error',
name,
message,
status,
statusText,
apiErrors,
};
};

View file

@ -23,7 +23,7 @@ import { types as sdkTypes } from './sdkLoader';
import { ensureTransaction } from './data';
const { UUID, LatLng, LatLngBounds, Money } = sdkTypes;
const { arrayOf, bool, func, instanceOf, number, oneOf, shape, string } = PropTypes;
const { arrayOf, bool, func, instanceOf, number, object, oneOf, shape, string } = PropTypes;
// Fixed value
export const value = val => oneOf([val]);
@ -217,3 +217,40 @@ export const pagination = shape({
totalItems: number.isRequired,
totalPages: number.isRequired,
});
export const ERROR_CODE_TRANSITION_PARAMETER_VALIDATION_FAILED =
'transition-parameter-validation-failed';
export const ERROR_CODE_PAYMENT_FAILED = 'transaction-payment-failed';
export const ERROR_CODE_EMAIL_TAKEN = 'email-taken';
export const ERROR_CODE_TOO_MANY_VERIFICATION_REQUESTS = 'email-too-many-verification-requests';
export const ERROR_CODE_UPLOAD_OVER_LIMIT = 'request-upload-over-limit';
const ERROR_CODES = [
ERROR_CODE_TRANSITION_PARAMETER_VALIDATION_FAILED,
ERROR_CODE_PAYMENT_FAILED,
ERROR_CODE_EMAIL_TAKEN,
ERROR_CODE_TOO_MANY_VERIFICATION_REQUESTS,
ERROR_CODE_UPLOAD_OVER_LIMIT,
];
// API error
// TODO this is likely to change soonish
export const apiError = shape({
id: uuid.isRequired,
status: number.isRequired,
code: oneOf(ERROR_CODES).isRequired,
title: string.isRequired,
details: shape({
data: object,
msg: string,
}),
});
// Storable error prop type. (Error object should not be stored as it is.)
export const error = shape({
type: value('error').isRequired,
name: string.isRequired,
message: string,
status: number,
statusText: string,
apiErrors: arrayOf(apiError),
});