diff --git a/src/components/ActivityFeed/ActivityFeed.css b/src/components/ActivityFeed/ActivityFeed.css index 6b8a9405..fd793555 100644 --- a/src/components/ActivityFeed/ActivityFeed.css +++ b/src/components/ActivityFeed/ActivityFeed.css @@ -62,7 +62,7 @@ box-shadow: var(--boxShadow); @media (--viewportMedium) { - padding: 8px 14px 9px 14px; + padding: 6px 14px 11px 14px; margin: 0; } } @@ -137,6 +137,32 @@ margin-right: 6px; } +.reviewContent { + @apply --marketplaceH4FontStyles; + font-style: italic; + white-space: pre-line; + margin: 8px 0 0 0; + + @media (--viewportMedium) { + max-width: 500px; + margin: 7px 0 0 0; + } +} + +.reviewStars { + margin-top: 4px; + + @media (--viewportMedium) { + margin-top: 2px; + } +} + +.reviewStar { + width: 12px; + height: 12px; + font-styles: initial; +} + .showOlderWrapper { text-align: center; margin-bottom: 1px; diff --git a/src/components/ActivityFeed/ActivityFeed.example.js b/src/components/ActivityFeed/ActivityFeed.example.js index bd99f626..8740b28d 100644 --- a/src/components/ActivityFeed/ActivityFeed.example.js +++ b/src/components/ActivityFeed/ActivityFeed.example.js @@ -6,6 +6,7 @@ import { createTransaction, createListing, createTxTransition, + createReview, } from '../../util/test-data'; import * as propTypes from '../../util/propTypes'; import ActivityFeed from './ActivityFeed'; @@ -84,7 +85,7 @@ export const WithTransitions = { group: 'messages', }; -export const WithMessagesAndTransitions = { +export const WithMessagesTransitionsAndReviews = { component: ActivityFeed, props: { currentUser: createCurrentUser('user2'), @@ -92,6 +93,7 @@ export const WithMessagesAndTransitions = { customer: createUser('user1'), provider: createUser('user2'), listing: createListing('Listing'), + lastTransition: propTypes.TX_TRANSITION_REVIEW_BY_CUSTOMER_SECOND, transitions: [ createTxTransition({ at: new Date(Date.UTC(2017, 10, 9, 8, 10)), @@ -113,6 +115,28 @@ export const WithMessagesAndTransitions = { by: propTypes.TX_TRANSITION_ACTOR_PROVIDER, transition: propTypes.TX_TRANSITION_MARK_DELIVERED, }), + createTxTransition({ + at: new Date(Date.UTC(2017, 10, 9, 11, 34)), + by: propTypes.TX_TRANSITION_ACTOR_PROVIDER, + transition: propTypes.TX_TRANSITION_REVIEW_BY_PROVIDER_FIRST, + }), + createTxTransition({ + at: new Date(Date.UTC(2017, 10, 9, 12, 34)), + by: propTypes.TX_TRANSITION_ACTOR_CUSTOMER, + transition: propTypes.TX_TRANSITION_REVIEW_BY_CUSTOMER_SECOND, + }), + ], + reviews: [ + createReview( + 'review1', + { at: new Date(Date.UTC(2017, 10, 9, 11, 34)), rating: 3, type: 'ofCustomer' }, + { author: createUser('user2'), subject: createUser('user1') } + ), + createReview( + 'review2', + { at: new Date(Date.UTC(2017, 10, 9, 12, 34)), rating: 5, type: 'ofProvider' }, + { author: createUser('user1'), subject: createUser('user2') } + ), ], }), messages: [ @@ -144,6 +168,48 @@ export const WithMessagesAndTransitions = { group: 'messages', }; +export const WithAReviewFromBothUsers = { + component: ActivityFeed, + props: { + currentUser: createCurrentUser('user1'), + transaction: createTransaction({ + customer: createUser('user1'), + provider: createUser('user2'), + listing: createListing('Listing'), + reviews: [ + createReview( + 'review1', + { at: new Date(Date.UTC(2017, 10, 9, 8, 10)), rating: 3, type: 'ofProvider' }, + { author: createUser('user1'), subject: createUser('user2') } + ), + createReview( + 'review2', + { at: new Date(Date.UTC(2017, 10, 10, 8, 10)), rating: 5, type: 'ofCustomer' }, + { author: createUser('user2'), subject: createUser('user1') } + ), + ], + lastTransition: propTypes.TX_TRANSITION_REVIEW_BY_PROVIDER_SECOND, + transitions: [ + createTxTransition({ + at: new Date(Date.UTC(2017, 10, 9, 8, 10)), + by: propTypes.TX_TRANSITION_ACTOR_CUSTOMER, + transition: propTypes.TX_TRANSITION_REVIEW_BY_CUSTOMER_FIRST, + }), + createTxTransition({ + at: new Date(Date.UTC(2017, 10, 10, 8, 10)), + by: propTypes.TX_TRANSITION_ACTOR_PROVIDER, + transition: propTypes.TX_TRANSITION_REVIEW_BY_PROVIDER_SECOND, + }), + ], + }), + messages: [], + hasOlderMessages: false, + onShowOlderMessages: noop, + fetchMessagesInProgress: false, + }, + group: 'messages', +}; + class PagedFeed extends Component { constructor(props) { super(props); diff --git a/src/components/ActivityFeed/ActivityFeed.js b/src/components/ActivityFeed/ActivityFeed.js index 80780078..415f63cb 100644 --- a/src/components/ActivityFeed/ActivityFeed.js +++ b/src/components/ActivityFeed/ActivityFeed.js @@ -1,9 +1,9 @@ import React from 'react'; -import { string, arrayOf, bool, func } from 'prop-types'; +import { string, arrayOf, bool, func, number } from 'prop-types'; import { injectIntl, intlShape, FormattedMessage } from 'react-intl'; import { dropWhile } from 'lodash'; import classNames from 'classnames'; -import { Avatar, InlineTextButton } from '../../components'; +import { Avatar, InlineTextButton, ReviewRating } from '../../components'; import { formatDate } from '../../util/dates'; import { ensureTransaction, ensureUser, ensureListing } from '../../util/data'; import * as propTypes from '../../util/propTypes'; @@ -47,43 +47,144 @@ OwnMessage.propTypes = { intl: intlShape.isRequired, }; +const Review = props => { + const { content, rating } = props; + return ( +
+

{content}

+ +
+ ); +}; + +Review.propTypes = { + content: string.isRequired, + rating: number.isRequired, +}; + +const hasUserLeftAReviewFirst = (userRole, lastTransition) => { + return ( + (lastTransition === propTypes.TX_TRANSITION_REVIEW_BY_CUSTOMER_FIRST && + userRole === propTypes.TX_TRANSITION_ACTOR_CUSTOMER) || + (lastTransition === propTypes.TX_TRANSITION_REVIEW_BY_PROVIDER_FIRST && + userRole === propTypes.TX_TRANSITION_ACTOR_PROVIDER) || + propTypes.areReviewsCompleted(lastTransition) + ); +}; + +const onLeaveAReview = () => { + // TODO: Open up a review modal + + console.log('Leave a review button clicked'); +}; + +const resolveTransitionMessage = ( + transition, + lastTransition, + listingTitle, + ownRole, + otherUsersName, + intl +) => { + const isOwnTransition = transition.by === ownRole; + const currentTransition = transition.transition; + const displayName = otherUsersName; + const deliveredState = lastTransition === propTypes.TX_TRANSITION_MARK_DELIVERED; + + switch (currentTransition) { + case propTypes.TX_TRANSITION_PREAUTHORIZE: + return isOwnTransition ? ( + + ) : ( + + ); + case propTypes.TX_TRANSITION_ACCEPT: + return isOwnTransition ? ( + + ) : ( + + ); + case propTypes.TX_TRANSITION_DECLINE: + return isOwnTransition ? ( + + ) : ( + + ); + case propTypes.TX_TRANSITION_AUTO_DECLINE: + return ownRole === propTypes.TX_TRANSITION_ACTOR_PROVIDER ? ( + + ) : ( + + ); + case propTypes.TX_TRANSITION_CANCEL: + return ; + case propTypes.TX_TRANSITION_MARK_DELIVERED: + // Show the leave a review link if the state is delivered or + // if current user is not the first to leave a review + const reviewLink = + deliveredState || !hasUserLeftAReviewFirst(ownRole, lastTransition) ? ( + + + + ) : null; + + return ; + case propTypes.TX_TRANSITION_REVIEW_BY_PROVIDER_FIRST: + case propTypes.TX_TRANSITION_REVIEW_BY_CUSTOMER_FIRST: + if (isOwnTransition) { + return ; + } else { + // show the leave a review link if current user is not the first + // one to leave a review + const reviewLink = !hasUserLeftAReviewFirst(ownRole, lastTransition) ? ( + + + + ) : null; + return ( + + ); + } + case propTypes.TX_TRANSITION_REVIEW_BY_PROVIDER_SECOND: + case propTypes.TX_TRANSITION_REVIEW_BY_CUSTOMER_SECOND: + if (isOwnTransition) { + return ; + } else { + return ( + + ); + } + + default: + return ''; + } +}; + +const reviewByAuthorId = (transaction, userId) => { + return transaction.reviews.filter(r => r.author.id.uuid === userId.uuid)[0]; +}; + const Transition = props => { - const { transition, currentUser, customer, provider, listingTitle, intl } = props; + const { transition, transaction, currentUser, intl } = props; - const resolveTransitionMessage = (transition, ownRole, otherUsersName, intl) => { - const isOwnTransition = transition.by === ownRole; - const transitionType = transition.transition; - const displayName = otherUsersName; - - switch (transitionType) { - case propTypes.TX_TRANSITION_PREAUTHORIZE: - return isOwnTransition - ? intl.formatMessage({ id: 'ActivityFeed.ownTransitionRequest' }, { listingTitle }) - : intl.formatMessage( - { id: 'ActivityFeed.transitionRequest' }, - { displayName, listingTitle } - ); - case propTypes.TX_TRANSITION_ACCEPT: - return isOwnTransition - ? intl.formatMessage({ id: 'ActivityFeed.ownTransitionAccept' }) - : intl.formatMessage({ id: 'ActivityFeed.transitionAccept' }, { displayName }); - case propTypes.TX_TRANSITION_DECLINE: - return isOwnTransition - ? intl.formatMessage({ id: 'ActivityFeed.ownTransitionDecline' }) - : intl.formatMessage({ id: 'ActivityFeed.transitionDecline' }, { displayName }); - case propTypes.TX_TRANSITION_AUTO_DECLINE: - return ownRole === propTypes.TX_TRANSITION_ACTOR_PROVIDER - ? intl.formatMessage({ id: 'ActivityFeed.ownTransitionAutoDecline' }) - : intl.formatMessage({ id: 'ActivityFeed.transitionAutoDecline' }, { displayName }); - case propTypes.TX_TRANSITION_MARK_DELIVERED: - return intl.formatMessage({ id: 'ActivityFeed.transitionComplete' }); - case propTypes.TX_TRANSITION_CANCEL: - return intl.formatMessage({ id: 'ActivityFeed.transitionCancel' }); - - default: - return ''; - } - }; + const currentTransaction = ensureTransaction(transaction); + const customer = currentTransaction.customer; + const provider = currentTransaction.provider; + const listingTitle = currentTransaction.listing.attributes.title; + const lastTransition = currentTransaction.attributes.lastTransition; const ownRole = currentUser.id.uuid === customer.id.uuid @@ -95,7 +196,41 @@ const Transition = props => { ? provider.attributes.profile.displayName : customer.attributes.profile.displayName; - const transitionMessage = resolveTransitionMessage(transition, ownRole, otherUsersName, intl); + const transitionMessage = resolveTransitionMessage( + transition, + lastTransition, + listingTitle, + ownRole, + otherUsersName, + intl + ); + const currentTransition = transition.transition; + + let reviewComponent = null; + + if ( + propTypes.isReviewTransition(currentTransition) && + propTypes.areReviewsCompleted(lastTransition) + ) { + const customerReview = + currentTransition === propTypes.TX_TRANSITION_REVIEW_BY_CUSTOMER_FIRST || + currentTransition === propTypes.TX_TRANSITION_REVIEW_BY_CUSTOMER_SECOND; + const providerReview = + currentTransition === propTypes.TX_TRANSITION_REVIEW_BY_PROVIDER_FIRST || + currentTransition === propTypes.TX_TRANSITION_REVIEW_BY_PROVIDER_SECOND; + if (customerReview) { + const review = reviewByAuthorId(currentTransaction, customer.id); + reviewComponent = ( + + ); + } else if (providerReview) { + const review = reviewByAuthorId(currentTransaction, provider.id); + reviewComponent = ( + + ); + } + } + const todayString = intl.formatMessage({ id: 'ActivityFeed.today' }); return ( @@ -106,6 +241,7 @@ const Transition = props => {

{transitionMessage}

{formatDate(intl, todayString, transition.at)}

+ {reviewComponent}
); @@ -113,10 +249,8 @@ const Transition = props => { Transition.propTypes = { transition: propTypes.txTransition.isRequired, + transaction: propTypes.transaction.isRequired, currentUser: propTypes.currentUser.isRequired, - customer: propTypes.user.isRequired, - provider: propTypes.user.isRequired, - listingTitle: string.isRequired, intl: intlShape.isRequired, }; @@ -180,7 +314,8 @@ export const ActivityFeedComponent = props => { currentUser && currentUser.id && currentCustomer.id && - currentProvider.id + currentProvider.id && + currentListing.id ); // combine messages and transaction transitions @@ -191,10 +326,8 @@ export const ActivityFeedComponent = props => { return ( ); diff --git a/src/components/OrderDetailsPanel/__snapshots__/OrderDetailsPanel.test.js.snap b/src/components/OrderDetailsPanel/__snapshots__/OrderDetailsPanel.test.js.snap index 03cf25a8..71cf60de 100644 --- a/src/components/OrderDetailsPanel/__snapshots__/OrderDetailsPanel.test.js.snap +++ b/src/components/OrderDetailsPanel/__snapshots__/OrderDetailsPanel.test.js.snap @@ -217,6 +217,7 @@ exports[`OrderDetailsPanel accepted matches snapshot 1`] = ` }, "type": "user", }, + "reviews": Array [], "type": "transaction", } } @@ -424,6 +425,7 @@ exports[`OrderDetailsPanel accepted matches snapshot 1`] = ` }, "type": "user", }, + "reviews": Array [], "type": "transaction", } } @@ -633,6 +635,7 @@ exports[`OrderDetailsPanel accepted matches snapshot 1`] = ` }, "type": "user", }, + "reviews": Array [], "type": "transaction", } } @@ -849,6 +852,7 @@ exports[`OrderDetailsPanel autodeclined matches snapshot 1`] = ` }, "type": "user", }, + "reviews": Array [], "type": "transaction", } } @@ -1056,6 +1060,7 @@ exports[`OrderDetailsPanel autodeclined matches snapshot 1`] = ` }, "type": "user", }, + "reviews": Array [], "type": "transaction", } } @@ -1265,6 +1270,7 @@ exports[`OrderDetailsPanel autodeclined matches snapshot 1`] = ` }, "type": "user", }, + "reviews": Array [], "type": "transaction", } } @@ -1493,6 +1499,7 @@ exports[`OrderDetailsPanel canceled matches snapshot 1`] = ` }, "type": "user", }, + "reviews": Array [], "type": "transaction", } } @@ -1700,6 +1707,7 @@ exports[`OrderDetailsPanel canceled matches snapshot 1`] = ` }, "type": "user", }, + "reviews": Array [], "type": "transaction", } } @@ -1909,6 +1917,7 @@ exports[`OrderDetailsPanel canceled matches snapshot 1`] = ` }, "type": "user", }, + "reviews": Array [], "type": "transaction", } } @@ -2125,6 +2134,7 @@ exports[`OrderDetailsPanel declined matches snapshot 1`] = ` }, "type": "user", }, + "reviews": Array [], "type": "transaction", } } @@ -2332,6 +2342,7 @@ exports[`OrderDetailsPanel declined matches snapshot 1`] = ` }, "type": "user", }, + "reviews": Array [], "type": "transaction", } } @@ -2541,6 +2552,7 @@ exports[`OrderDetailsPanel declined matches snapshot 1`] = ` }, "type": "user", }, + "reviews": Array [], "type": "transaction", } } @@ -2757,6 +2769,7 @@ exports[`OrderDetailsPanel delivered matches snapshot 1`] = ` }, "type": "user", }, + "reviews": Array [], "type": "transaction", } } @@ -2964,6 +2977,7 @@ exports[`OrderDetailsPanel delivered matches snapshot 1`] = ` }, "type": "user", }, + "reviews": Array [], "type": "transaction", } } @@ -3173,6 +3187,7 @@ exports[`OrderDetailsPanel delivered matches snapshot 1`] = ` }, "type": "user", }, + "reviews": Array [], "type": "transaction", } } @@ -3412,6 +3427,7 @@ exports[`OrderDetailsPanel preauthorized matches snapshot 1`] = ` }, "type": "user", }, + "reviews": Array [], "type": "transaction", } } @@ -3619,6 +3635,7 @@ exports[`OrderDetailsPanel preauthorized matches snapshot 1`] = ` }, "type": "user", }, + "reviews": Array [], "type": "transaction", } } @@ -3828,6 +3845,7 @@ exports[`OrderDetailsPanel preauthorized matches snapshot 1`] = ` }, "type": "user", }, + "reviews": Array [], "type": "transaction", } } diff --git a/src/components/ReviewRating/ReviewRating.example.js b/src/components/ReviewRating/ReviewRating.example.js new file mode 100644 index 00000000..902b1e97 --- /dev/null +++ b/src/components/ReviewRating/ReviewRating.example.js @@ -0,0 +1,11 @@ +import ReviewRating from './ReviewRating'; + +export const ReviewRatingThree = { + component: ReviewRating, + props: { rating: 3 }, +}; + +export const ReviewRatingFive = { + component: ReviewRating, + props: { rating: 5 }, +}; diff --git a/src/components/ReviewRating/ReviewRating.js b/src/components/ReviewRating/ReviewRating.js new file mode 100644 index 00000000..7f61b1ed --- /dev/null +++ b/src/components/ReviewRating/ReviewRating.js @@ -0,0 +1,40 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import classNames from 'classnames'; +import { IconReviewStar } from '../../components'; +import * as propTypes from '../../util/propTypes'; + +const ReviewRating = props => { + const { className, rootClassName, reviewStarClassName, rating } = props; + const classes = classNames(rootClassName, className); + + const stars = propTypes.REVIEW_RATINGS; + return ( +
+ {stars.map(star => ( + + ))} +
+ ); +}; + +ReviewRating.defaultProps = { + rootClassName: null, + className: null, + reviewStarClassName: null, +}; + +const { string, oneOf } = PropTypes; + +ReviewRating.propTypes = { + rating: oneOf(propTypes.REVIEW_RATINGS).isRequired, + reviewStartClassName: string, + rootClassName: string, + className: string, +}; + +export default ReviewRating; diff --git a/src/components/SaleDetailsPanel/__snapshots__/SaleDetailsPanel.test.js.snap b/src/components/SaleDetailsPanel/__snapshots__/SaleDetailsPanel.test.js.snap index 260d3344..020ddbac 100644 --- a/src/components/SaleDetailsPanel/__snapshots__/SaleDetailsPanel.test.js.snap +++ b/src/components/SaleDetailsPanel/__snapshots__/SaleDetailsPanel.test.js.snap @@ -211,6 +211,7 @@ exports[`SaleDetailsPanel accepted matches snapshot 1`] = ` "type": "listing", }, "provider": null, + "reviews": Array [], "type": "transaction", } } @@ -406,6 +407,7 @@ exports[`SaleDetailsPanel accepted matches snapshot 1`] = ` "type": "listing", }, "provider": null, + "reviews": Array [], "type": "transaction", } } @@ -571,6 +573,7 @@ exports[`SaleDetailsPanel accepted matches snapshot 1`] = ` "type": "listing", }, "provider": null, + "reviews": Array [], "type": "transaction", } } @@ -795,6 +798,7 @@ exports[`SaleDetailsPanel autodeclined matches snapshot 1`] = ` "type": "listing", }, "provider": null, + "reviews": Array [], "type": "transaction", } } @@ -990,6 +994,7 @@ exports[`SaleDetailsPanel autodeclined matches snapshot 1`] = ` "type": "listing", }, "provider": null, + "reviews": Array [], "type": "transaction", } } @@ -1155,6 +1160,7 @@ exports[`SaleDetailsPanel autodeclined matches snapshot 1`] = ` "type": "listing", }, "provider": null, + "reviews": Array [], "type": "transaction", } } @@ -1379,6 +1385,7 @@ exports[`SaleDetailsPanel canceled matches snapshot 1`] = ` "type": "listing", }, "provider": null, + "reviews": Array [], "type": "transaction", } } @@ -1574,6 +1581,7 @@ exports[`SaleDetailsPanel canceled matches snapshot 1`] = ` "type": "listing", }, "provider": null, + "reviews": Array [], "type": "transaction", } } @@ -1739,6 +1747,7 @@ exports[`SaleDetailsPanel canceled matches snapshot 1`] = ` "type": "listing", }, "provider": null, + "reviews": Array [], "type": "transaction", } } @@ -1963,6 +1972,7 @@ exports[`SaleDetailsPanel declined matches snapshot 1`] = ` "type": "listing", }, "provider": null, + "reviews": Array [], "type": "transaction", } } @@ -2158,6 +2168,7 @@ exports[`SaleDetailsPanel declined matches snapshot 1`] = ` "type": "listing", }, "provider": null, + "reviews": Array [], "type": "transaction", } } @@ -2323,6 +2334,7 @@ exports[`SaleDetailsPanel declined matches snapshot 1`] = ` "type": "listing", }, "provider": null, + "reviews": Array [], "type": "transaction", } } @@ -2547,6 +2559,7 @@ exports[`SaleDetailsPanel delivered matches snapshot 1`] = ` "type": "listing", }, "provider": null, + "reviews": Array [], "type": "transaction", } } @@ -2742,6 +2755,7 @@ exports[`SaleDetailsPanel delivered matches snapshot 1`] = ` "type": "listing", }, "provider": null, + "reviews": Array [], "type": "transaction", } } @@ -2907,6 +2921,7 @@ exports[`SaleDetailsPanel delivered matches snapshot 1`] = ` "type": "listing", }, "provider": null, + "reviews": Array [], "type": "transaction", } } @@ -3141,6 +3156,7 @@ exports[`SaleDetailsPanel preauthorized matches snapshot 1`] = ` "type": "listing", }, "provider": null, + "reviews": Array [], "type": "transaction", } } @@ -3336,6 +3352,7 @@ exports[`SaleDetailsPanel preauthorized matches snapshot 1`] = ` "type": "listing", }, "provider": null, + "reviews": Array [], "type": "transaction", } } @@ -3529,6 +3546,7 @@ exports[`SaleDetailsPanel preauthorized matches snapshot 1`] = ` "type": "listing", }, "provider": null, + "reviews": Array [], "type": "transaction", } } diff --git a/src/components/index.js b/src/components/index.js index 54535659..22c61959 100644 --- a/src/components/index.js +++ b/src/components/index.js @@ -80,6 +80,7 @@ export { default as PaginationLinks } from './PaginationLinks/PaginationLinks'; export { default as PrivacyPolicy } from './PrivacyPolicy/PrivacyPolicy'; export { default as Promised } from './Promised/Promised'; export { default as ResponsiveImage } from './ResponsiveImage/ResponsiveImage'; +export { default as ReviewRating } from './ReviewRating/ReviewRating'; export { default as SaleDetailsPanel } from './SaleDetailsPanel/SaleDetailsPanel'; export { default as SearchMap } from './SearchMap/SearchMap'; export { default as SearchMapGroupLabel } from './SearchMapGroupLabel/SearchMapGroupLabel'; diff --git a/src/containers/InboxPage/__snapshots__/InboxPage.test.js.snap b/src/containers/InboxPage/__snapshots__/InboxPage.test.js.snap index 18ebeb1c..7131cc9e 100644 --- a/src/containers/InboxPage/__snapshots__/InboxPage.test.js.snap +++ b/src/containers/InboxPage/__snapshots__/InboxPage.test.js.snap @@ -183,6 +183,7 @@ exports[`InboxPage matches snapshot 1`] = ` }, "type": "user", }, + "reviews": Array [], "type": "transaction", } } @@ -297,6 +298,7 @@ exports[`InboxPage matches snapshot 1`] = ` }, "type": "user", }, + "reviews": Array [], "type": "transaction", } } @@ -569,6 +571,7 @@ exports[`InboxPage matches snapshot 3`] = ` }, "type": "user", }, + "reviews": Array [], "type": "transaction", } } @@ -683,6 +686,7 @@ exports[`InboxPage matches snapshot 3`] = ` }, "type": "user", }, + "reviews": Array [], "type": "transaction", } } diff --git a/src/containers/OrderPage/OrderPage.duck.js b/src/containers/OrderPage/OrderPage.duck.js index dc921aeb..651091f1 100644 --- a/src/containers/OrderPage/OrderPage.duck.js +++ b/src/containers/OrderPage/OrderPage.duck.js @@ -117,7 +117,21 @@ export const fetchOrder = id => (dispatch, getState, sdk) => { let txResponse = null; return sdk.transactions - .show({ id, include: ['customer', 'provider', 'listing', 'booking'] }, { expand: true }) + .show( + { + id, + include: [ + 'customer', + 'provider', + 'listing', + 'booking', + 'reviews', + 'reviews.author', + 'reviews.subject', + ], + }, + { expand: true } + ) .then(response => { txResponse = response; const listingId = listingRelationship(response).id; diff --git a/src/containers/OrderPage/__snapshots__/OrderPage.test.js.snap b/src/containers/OrderPage/__snapshots__/OrderPage.test.js.snap index fc91023a..981a359a 100644 --- a/src/containers/OrderPage/__snapshots__/OrderPage.test.js.snap +++ b/src/containers/OrderPage/__snapshots__/OrderPage.test.js.snap @@ -165,6 +165,7 @@ exports[`OrderPage matches snapshot 1`] = ` }, "type": "user", }, + "reviews": Array [], "type": "transaction", } } diff --git a/src/containers/SalePage/SalePage.duck.js b/src/containers/SalePage/SalePage.duck.js index f3b5573f..1a74484e 100644 --- a/src/containers/SalePage/SalePage.duck.js +++ b/src/containers/SalePage/SalePage.duck.js @@ -159,6 +159,9 @@ export const fetchSale = id => (dispatch, getState, sdk) => { 'provider.profileImage', 'listing', 'booking', + 'reviews', + 'reviews.author', + 'reviews.subject', ], }, { expand: true } diff --git a/src/containers/SalePage/__snapshots__/SalePage.test.js.snap b/src/containers/SalePage/__snapshots__/SalePage.test.js.snap index b2183007..b9280c89 100644 --- a/src/containers/SalePage/__snapshots__/SalePage.test.js.snap +++ b/src/containers/SalePage/__snapshots__/SalePage.test.js.snap @@ -170,6 +170,7 @@ exports[`SalePage matches snapshot 1`] = ` }, "type": "user", }, + "reviews": Array [], "type": "transaction", } } diff --git a/src/examples.js b/src/examples.js index 34c359da..3e5a37d3 100644 --- a/src/examples.js +++ b/src/examples.js @@ -36,6 +36,7 @@ import * as ModalInMobile from './components/ModalInMobile/ModalInMobile.example import * as NamedLink from './components/NamedLink/NamedLink.example'; import * as PaginationLinks from './components/PaginationLinks/PaginationLinks.example'; import * as ResponsiveImage from './components/ResponsiveImage/ResponsiveImage.example'; +import * as ReviewRating from './components/ReviewRating/ReviewRating.example'; import * as SelectField from './components/SelectField/SelectField.example'; import * as StripeBankAccountTokenInputField from './components/StripeBankAccountTokenInputField/StripeBankAccountTokenInputField.example'; import * as TabNav from './components/TabNav/TabNav.example'; @@ -110,6 +111,7 @@ export { PasswordResetForm, PayoutDetailsForm, ResponsiveImage, + ReviewRating, SelectField, SendMessageForm, SignupForm, diff --git a/src/translations/en.json b/src/translations/en.json index d82a0808..f3d08fa3 100644 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -1,16 +1,20 @@ { + "ActivityFeed.leaveAReview": "Leave a review for { displayName }.", + "ActivityFeed.leaveAReviewSecond": "Leave a review for { displayName } to see their review.", "ActivityFeed.ownTransitionAccept": "You accepted the booking request.", "ActivityFeed.ownTransitionAutoDecline": "Booking request expired. You did not reply on time.", "ActivityFeed.ownTransitionDecline": "You declined the booking request.", "ActivityFeed.ownTransitionRequest": "You requested to book { listingTitle }.", + "ActivityFeed.ownTransitionReview": "You left a review for { displayName }.", "ActivityFeed.showOlderMessages": "Show older", "ActivityFeed.today": "Today", "ActivityFeed.transitionAccept": "{ displayName } accepted the booking request.", "ActivityFeed.transitionAutoDecline": "Booking request expired. { displayName } did not reply on time.", "ActivityFeed.transitionCancel": "The booking was cancelled.", - "ActivityFeed.transitionComplete": "The booking was completed.", + "ActivityFeed.transitionComplete": "The booking was completed. { reviewLink }", "ActivityFeed.transitionDecline": "{ displayName } declined the booking request.", "ActivityFeed.transitionRequest": "{ displayName } requested to book { listingTitle }.", + "ActivityFeed.transitionReview": "{ displayName } left a review for you. { reviewLink }", "AuthenticationPage.emailAlreadyInUse": "An account with this email address already exists. Try logging in instead.", "AuthenticationPage.fixEmail": "Whoops, typo in your email? {fixEmailLink}.", "AuthenticationPage.fixEmailLinkText": "Fix it", diff --git a/src/util/propTypes.js b/src/util/propTypes.js index 104994dd..12e3cb4c 100644 --- a/src/util/propTypes.js +++ b/src/util/propTypes.js @@ -163,6 +163,16 @@ export const TX_TRANSITION_CANCEL = 'transition/cancel'; // with the mark-delivered transition. export const TX_TRANSITION_MARK_DELIVERED = 'transition/mark-delivered'; +// Review transitions +export const TX_TRANSITION_REVIEW_BY_PROVIDER_FIRST = 'transition/review-by-provider-first'; +export const TX_TRANSITION_REVIEW_BY_PROVIDER_SECOND = 'transition/review-by-provider-second'; +export const TX_TRANSITION_REVIEW_BY_CUSTOMER_FIRST = 'transition/review-by-customer-first'; +export const TX_TRANSITION_REVIEW_BY_CUSTOMER_SECOND = 'transition/review-by-customer-second'; +export const TX_TRANSITION_MARK_REVIEWED_BY_CUSTOMER = 'transition/mark-reviewed-by-customer'; +export const TX_TRANSITION_MARK_REVIEWED_BY_PROVIDER = 'transition/mark-reviewed-by-provider'; +export const TX_TRANSITION_AUTO_COMPLETE_WITHOUT_REVIEWS = + 'transition/auto-complete-without-reviews'; + export const TX_TRANSITIONS = [ TX_TRANSITION_PREAUTHORIZE, TX_TRANSITION_ACCEPT, @@ -170,6 +180,13 @@ export const TX_TRANSITIONS = [ TX_TRANSITION_AUTO_DECLINE, TX_TRANSITION_CANCEL, TX_TRANSITION_MARK_DELIVERED, + TX_TRANSITION_REVIEW_BY_PROVIDER_FIRST, + TX_TRANSITION_REVIEW_BY_PROVIDER_SECOND, + TX_TRANSITION_REVIEW_BY_CUSTOMER_FIRST, + TX_TRANSITION_REVIEW_BY_CUSTOMER_SECOND, + TX_TRANSITION_MARK_REVIEWED_BY_CUSTOMER, + TX_TRANSITION_MARK_REVIEWED_BY_PROVIDER, + TX_TRANSITION_AUTO_COMPLETE_WITHOUT_REVIEWS, ]; // Roles of actors that perform transaction transitions @@ -207,6 +224,46 @@ export const txTransition = shape({ transition: oneOf(TX_TRANSITIONS).isRequired, }); +// Check if tx transition is followed by a state where +// reviews are completed +export const areReviewsCompleted = transition => { + return [ + TX_TRANSITION_REVIEW_BY_PROVIDER_SECOND, + TX_TRANSITION_REVIEW_BY_CUSTOMER_SECOND, + TX_TRANSITION_MARK_REVIEWED_BY_CUSTOMER, + TX_TRANSITION_MARK_REVIEWED_BY_PROVIDER, + TX_TRANSITION_AUTO_COMPLETE_WITHOUT_REVIEWS, + ].includes(transition); +}; + +// Check if a user giving a review is related to +// given tx transition. +export const isReviewTransition = transition => { + return [ + TX_TRANSITION_REVIEW_BY_PROVIDER_FIRST, + TX_TRANSITION_REVIEW_BY_CUSTOMER_FIRST, + TX_TRANSITION_REVIEW_BY_PROVIDER_SECOND, + TX_TRANSITION_REVIEW_BY_CUSTOMER_SECOND, + ].includes(transition); +}; + +// Possible amount of stars in a review +export const REVIEW_RATINGS = [1, 2, 3, 4, 5]; + +// A review on a user +export const review = shape({ + id: uuid.isRequired, + attributes: shape({ + at: instanceOf(Date).isRequired, + content: string, + rating: oneOf(REVIEW_RATINGS), + state: string.isRequired, + type: string.isRequired, + }).isRequired, + author: user, + subject: user, +}); + // Denormalised transaction object export const transaction = shape({ id: uuid.isRequired, @@ -232,6 +289,7 @@ export const transaction = shape({ listing, customer: user, provider: user, + reviews: arrayOf(review), }); // Denormalised transaction message diff --git a/src/util/test-data.js b/src/util/test-data.js index d6767095..d191f3e3 100644 --- a/src/util/test-data.js +++ b/src/util/test-data.js @@ -105,6 +105,7 @@ export const createTransaction = options => { listing = null, customer = null, provider = null, + reviews = [], lastTransitionedAt = new Date(Date.UTC(2017, 5, 1)), transitions = [ createTxTransition({ @@ -150,6 +151,7 @@ export const createTransaction = options => { listing, customer, provider, + reviews, }; }; @@ -166,6 +168,22 @@ export const createMessage = (id, attributes = {}, includes = {}) => { }; }; +export const createReview = (id, attributes = {}, includes = {}) => { + return { + id: new UUID(id), + attributes: { + at: new Date(), + content: + 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.', + rating: 3, + state: 'public', + type: 'ofProvider', + ...attributes, + }, + ...includes, + }; +}; + // Default config for currency formatting in tests and examples. export const currencyConfig = { style: 'currency',