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 (
+
+ );
+};
+
+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',