diff --git a/src/components/ActivityFeed/ActivityFeed.js b/src/components/ActivityFeed/ActivityFeed.js
index 7df809c3..5ce36a93 100644
--- a/src/components/ActivityFeed/ActivityFeed.js
+++ b/src/components/ActivityFeed/ActivityFeed.js
@@ -5,7 +5,7 @@ import { dropWhile } from 'lodash';
import classNames from 'classnames';
import { Avatar, InlineTextButton, ReviewRating } from '../../components';
import { formatDate } from '../../util/dates';
-import { ensureTransaction, ensureUser, ensureListing } from '../../util/data';
+import { ensureTransaction, ensureUser, ensureListing, userDisplayName } from '../../util/data';
import * as propTypes from '../../util/propTypes';
import * as log from '../../util/log';
@@ -212,7 +212,13 @@ const Transition = props => {
const currentTransaction = ensureTransaction(transaction);
const customer = currentTransaction.customer;
const provider = currentTransaction.provider;
- const listingTitle = currentTransaction.listing.attributes.title;
+
+ const deletedListing = intl.formatMessage({
+ id: 'ActivityFeed.deletedListing',
+ });
+ const listingTitle = currentTransaction.listing.attributes.deleted
+ ? deletedListing
+ : currentTransaction.listing.attributes.title;
const lastTransition = currentTransaction.attributes.lastTransition;
const ownRole =
@@ -220,10 +226,13 @@ const Transition = props => {
? propTypes.TX_TRANSITION_ACTOR_CUSTOMER
: propTypes.TX_TRANSITION_ACTOR_PROVIDER;
+ const bannedUserDisplayName = intl.formatMessage({
+ id: 'ActivityFeed.bannedUserDisplayName',
+ });
const otherUsersName =
ownRole === propTypes.TX_TRANSITION_ACTOR_CUSTOMER
- ? provider.attributes.profile.displayName
- : customer.attributes.profile.displayName;
+ ? userDisplayName(provider, bannedUserDisplayName)
+ : userDisplayName(customer, bannedUserDisplayName);
const transitionMessage = resolveTransitionMessage(
transition,
diff --git a/src/components/OrderDetailsPanel/OrderDetailsPanel.css b/src/components/OrderDetailsPanel/OrderDetailsPanel.css
deleted file mode 100644
index 8b7e84a2..00000000
--- a/src/components/OrderDetailsPanel/OrderDetailsPanel.css
+++ /dev/null
@@ -1,288 +0,0 @@
-@import '../../marketplace.css';
-
-.root {
- position: relative;
-}
-
-.error {
- color: var(--failColor);
- margin: 13px 0 22px 0;
-
- @media (--viewportMedium) {
- margin: 13px 0 23px 0;
- }
- @media (--viewportLarge) {
- margin: 12px 0 23px 0;
- }
-}
-
-.container {
- display: flex;
- flex-direction: column;
-
- @media (--viewportLarge) {
- flex-direction: row;
- justify-content: center;
- max-width: 1156px; /* 1060 + (paddingLeft + paddingRight) */
- margin: 0 auto 57px auto;
- padding: 0 48px;
- }
-}
-
-.imageWrapperMobile {
- /* Layout */
- display: block;
- width: 100%;
- position: relative;
-
- @media (--viewportLarge) {
- display: none;
- }
-}
-
-/* Firefox doesn't support image aspect ratio inside flexbox */
-.aspectWrapper {
- padding-bottom: 66.6667%; /* 3:2 Aspect Ratio */
- background-color: var(--matterColorNegative); /* Loading BG color */
-}
-
-.rootForImage {
- /* Layout - image will take space defined by aspect ratio wrapper */
- position: absolute;
- top: 0;
- bottom: 0;
- left: 0;
- right: 0;
- width: 100%;
-
- @media (--viewportLarge) {
- border-radius: 2px 2px 0 0;
- }
-}
-
-.avatarWrapper {
- /* Position (over the listing image)*/
- margin-left: 24px;
- margin-top: -31px;
-
- /* Rendering context to the same lavel as listing image */
- position: relative;
-
- /* Layout */
- display: block;
-
- @media (--viewportLarge) {
- margin-left: 48px;
- }
-}
-
-.avatarMobile {
- @media (--viewportLarge) {
- display: none;
- }
-}
-
-.orderInfo {
- margin-bottom: 210px;
-
- @media (--viewportLarge) {
- max-width: 538px;
- margin-right: 108px;
- margin-bottom: 0;
- }
-}
-
-.heading {
- margin: 29px 24px 0 24px;
-
- @media (--viewportMedium) {
- margin: 25px 24px 0 24px;
- max-width: 80%;
- }
-
- @media (--viewportLarge) {
- max-width: 100%;
- margin: 177px 0 0 0;
- }
-}
-
-.mainTitle {
- display: block;
-}
-
-.orderInfoText {
- margin: 18px 24px 0 24px;
-
- @media (--viewportMedium) {
- margin: 23px 24px 0 24px;
- }
- @media (--viewportLarge) {
- margin: 23px 0 0 0;
- }
-}
-
-.infoTextDivider {
- display: none;
-
- height: 1px;
- background-color: var(--matterColorNegative);
- border: none;
- margin: 37px 0 0 0;
-
- @media (--viewportLarge) {
- display: block;
- }
-}
-
-.transitionDate {
- white-space: nowrap;
-}
-
-.breakdownMobile {
- margin-top: 47px;
-
- @media (--viewportMedium) {
- margin-top: 43px;
- }
- @media (--viewportLarge) {
- display: none;
- }
-}
-
-.breakdownDesktop {
- display: none;
-
- position: sticky;
- top: 24px;
- width: 409px;
- padding-bottom: 55px;
- background-color: var(--matterColorLight);
- border: 1px solid var(--matterColorNegative);
- border-radius: 2px;
-
- @media (--viewportLarge) {
- display: block;
- }
-}
-
-.breakdownImageWrapper {
- /* Layout */
- display: block;
- width: 100%;
- position: relative;
-}
-
-.bookingBreakdownContainer {
- margin: 1px 0 0 0;
-
- @media (--viewportLarge) {
- margin-top: 123px;
- margin-left: 0;
- margin-right: 0;
- }
-}
-
-.breakdownHeadings {
- display: none;
-
- @media (--viewportLarge) {
- display: block;
- margin-left: 48px;
- margin-right: 48px;
- }
-}
-
-.breakdownTitle {
- margin-bottom: 10px;
-
- @media (--viewportLarge) {
- margin-top: 14px;
- margin-bottom: 0;
- }
-}
-
-.breakdownSubtitle {
- @apply --marketplaceH5FontStyles;
- color: var(--matterColorAnti);
-
- margin-top: 0;
- margin-bottom: 0;
-
- @media (--viewportLarge) {
- margin-top: 1px;
- margin-bottom: 0;
- }
-}
-
-.bookingBreakdownTitle {
- /* Font */
- color: var(--matterColorAnti);
-
- margin: 0 24px 0 24px;
-
- @media (--viewportLarge) {
- margin: 37px 48px 26px 48px;
- }
-}
-
-.breakdown {
- margin: 14px 24px 0 24px;
-
- @media (--viewportMedium) {
- margin: 18px 24px 0 24px;
- }
- @media (--viewportLarge) {
- margin: 14px 48px 0 48px;
- }
-}
-
-.feedContainer {
- margin: 46px 24px 0 24px;
-
- @media (--viewportMedium) {
- margin: 46px 24px 0 24px;
- }
- @media (--viewportLarge) {
- margin: 43px 0 0 0;
- }
-}
-
-.feedContainerWithInfoAbove {
- @media (--viewportLarge) {
- margin: 38px 0 0 0;
- }
-}
-
-.feedHeading {
- color: var(--matterColorAnti);
- margin: 0;
-
- @media (--viewportMedium) {
- margin: 0;
- }
-}
-
-.feed {
- margin-top: 20px;
-}
-
-.sendMessageForm {
- position: fixed;
- bottom: 0;
- width: 100%;
- box-shadow: var(--boxShadowBottomForm);
-
- @media (--viewportLarge) {
- margin-top: 46px;
- position: relative;
- box-shadow: none;
- }
-}
-
-.sendMessageFormFocusedInMobileSafari {
- position: absolute;
-
- @media (--viewportLarge) {
- position: relative;
- }
-}
diff --git a/src/components/OrderDetailsPanel/OrderDetailsPanel.js b/src/components/OrderDetailsPanel/OrderDetailsPanel.js
deleted file mode 100644
index 47f0bf4b..00000000
--- a/src/components/OrderDetailsPanel/OrderDetailsPanel.js
+++ /dev/null
@@ -1,439 +0,0 @@
-import React, { Component } from 'react';
-import PropTypes from 'prop-types';
-import { injectIntl, intlShape, FormattedMessage } from 'react-intl';
-import classNames from 'classnames';
-import * as propTypes from '../../util/propTypes';
-import { createSlug } from '../../util/urlHelpers';
-import { ensureListing, ensureTransaction, ensureUser, userDisplayName } from '../../util/data';
-import { isMobileSafari } from '../../util/userAgent';
-import {
- ActivityFeed,
- AvatarMedium,
- BookingBreakdown,
- NamedLink,
- ResponsiveImage,
- ReviewModal,
-} from '../../components';
-import { SendMessageForm } from '../../containers';
-
-import css from './OrderDetailsPanel.css';
-
-const breakdown = transaction => {
- const loaded = transaction && transaction.id && transaction.booking && transaction.booking.id;
-
- return loaded ? (
-
- ) : null;
-};
-
-const orderTitle = (transaction, listingLink, customerName) => {
- if (propTypes.txIsPreauthorized(transaction)) {
- return (
-
-
-
-
-
-
- );
- } else if (propTypes.txIsAccepted(transaction)) {
- return (
-
-
-
-
-
-
- );
- } else if (propTypes.txIsDeclined(transaction)) {
- return (
-
- );
- } else if (propTypes.txIsAutodeclined(transaction)) {
- return (
-
- );
- } else if (propTypes.txIsCanceled(transaction)) {
- return (
-
- );
- } else if (
- propTypes.txIsDelivered(transaction) ||
- propTypes.txHasFirstReview(transaction) ||
- propTypes.txIsReviewed(transaction)
- ) {
- return (
-
- );
- } else {
- return null;
- }
-};
-
-const orderMessage = (transaction, providerName) => {
- if (propTypes.txIsPreauthorized(transaction)) {
- return (
-
- );
- }
- return null;
-};
-
-export class OrderDetailsPanelComponent extends Component {
- constructor(props) {
- super(props);
- this.state = {
- sendMessageFormFocused: false,
- isReviewModalOpen: false,
- reviewSubmitted: false,
- };
- this.onOpenReviewModal = this.onOpenReviewModal.bind(this);
- this.onSubmitReview = this.onSubmitReview.bind(this);
- }
-
- onOpenReviewModal() {
- this.setState({ isReviewModalOpen: true });
- }
-
- onSubmitReview(values) {
- const { onSendReview, transaction } = this.props;
- const currentTransaction = ensureTransaction(transaction);
- const { reviewRating, reviewContent } = values;
- const rating = Number.parseInt(reviewRating, 10);
- onSendReview(currentTransaction, rating, reviewContent)
- .then(r => this.setState({ isReviewModalOpen: false, reviewSubmitted: true }))
- .catch(e => {
- // Do nothing.
- });
- }
-
- render() {
- const {
- rootClassName,
- className,
- currentUser,
- transaction,
- totalMessagePages,
- oldestMessagePageFetched,
- messages,
- initialMessageFailed,
- fetchMessagesInProgress,
- fetchMessagesError,
- sendMessageInProgress,
- sendMessageError,
- sendReviewInProgress,
- sendReviewError,
- onManageDisableScrolling,
- onShowMoreMessages,
- onSendMessage,
- onResetForm,
- intl,
- } = this.props;
-
- const currentTransaction = ensureTransaction(transaction);
- const currentListing = ensureListing(currentTransaction.listing);
- const currentProvider = ensureUser(currentTransaction.provider);
- const currentCustomer = ensureUser(currentTransaction.customer);
-
- const listingLoaded = !!currentListing.id;
- const listingDeleted = listingLoaded && currentListing.attributes.deleted;
-
- const bannedUserDisplayName = intl.formatMessage({
- id: 'OrderDetailsPanel.bannedUserDisplayName',
- });
- const deletedListingTitle = intl.formatMessage({
- id: 'OrderDetailsPanel.deletedListingTitle',
- });
- const deletedListingOrderTitle = intl.formatMessage({
- id: 'OrderDetailsPanel.deletedListingOrderTitle',
- });
- const orderMessageDeletedListing = intl.formatMessage({
- id: 'OrderDetailsPanel.messageDeletedListing',
- });
-
- const authorDisplayName = userDisplayName(currentProvider, bannedUserDisplayName);
- const customerDisplayName = userDisplayName(currentCustomer, bannedUserDisplayName);
-
- let otherUserDisplayName = '';
- const currentUserIsCustomer =
- currentUser.id && currentCustomer.id && currentUser.id.uuid === currentCustomer.id.uuid;
- const currentUserIsProvider =
- currentUser.id && currentProvider.id && currentUser.id.uuid === currentProvider.id.uuid;
-
- if (currentUserIsCustomer) {
- otherUserDisplayName = authorDisplayName;
- } else if (currentUserIsProvider) {
- otherUserDisplayName = customerDisplayName;
- }
-
- let listingLink = null;
-
- if (listingLoaded && currentListing.attributes.title) {
- const title = currentListing.attributes.title;
- const params = { id: currentListing.id.uuid, slug: createSlug(title) };
- listingLink = (
-
- {title}
-
- );
- } else {
- listingLink = deletedListingOrderTitle;
- }
-
- const listingTitle = currentListing.attributes.deleted
- ? deletedListingTitle
- : currentListing.attributes.title;
-
- const bookingInfo = breakdown(currentTransaction);
- const orderHeading = orderTitle(currentTransaction, listingLink, customerDisplayName);
- const orderInfoText = listingDeleted
- ? orderMessageDeletedListing
- : orderMessage(currentTransaction, authorDisplayName);
- const showInfoMessage = !!orderInfoText;
-
- const firstImage =
- currentListing.images && currentListing.images.length > 0 ? currentListing.images[0] : null;
-
- const feedContainerClasses = classNames(css.feedContainer, {
- [css.feedContainerWithInfoAbove]: showInfoMessage,
- });
- const txTransitions = currentTransaction.attributes.transitions
- ? currentTransaction.attributes.transitions
- : [];
- const hasOlderMessages = totalMessagePages > oldestMessagePageFetched;
- const handleShowOlderMessages = () => {
- onShowMoreMessages(currentTransaction.id);
- };
- const showFeed =
- messages.length > 0 || txTransitions.length > 0 || initialMessageFailed || fetchMessagesError;
- const feedContainer = showFeed ? (
-
-
-
-
- {initialMessageFailed ? (
-
-
-
- ) : null}
- {fetchMessagesError ? (
-
-
-
- ) : null}
-
-
- ) : null;
-
- const sendMessagePlaceholder = intl.formatMessage(
- { id: 'OrderDetailsPanel.sendMessagePlaceholder' },
- { name: otherUserDisplayName }
- );
-
- const sendMessageFormName = 'OrderDetailsPanel.SendMessageForm';
- const scrollToMessage = messageId => {
- const selector = `#msg-${messageId.uuid}`;
- const el = document.querySelector(selector);
- if (el) {
- el.scrollIntoView({
- block: 'start',
- behavior: 'smooth',
- });
- }
- };
- const isMobSaf = isMobileSafari();
- const handleSendMessageFormFocus = () => {
- this.setState({ sendMessageFormFocused: true });
- if (isMobSaf) {
- // Scroll to bottom
- window.scroll({ top: document.body.scrollHeight, left: 0, behavior: 'smooth' });
- }
- };
- const handleSendMessageFormBlur = () => {
- this.setState({ sendMessageFormFocused: false });
- };
- const handleMessageSubmit = values => {
- const message = values.message ? values.message.trim() : null;
- if (!message) {
- return;
- }
- onSendMessage(currentTransaction.id, message)
- .then(messageId => {
- onResetForm(sendMessageFormName);
- scrollToMessage(messageId);
- })
- .catch(e => {
- // Ignore, Redux handles the error
- });
- };
- const sendMessageFormClasses = classNames(css.sendMessageForm, {
- [css.sendMessageFormFocusedInMobileSafari]: isMobSaf && this.state.sendMessageFormFocused,
- });
-
- const classes = classNames(rootClassName || css.root, className);
-
- return (
-
-
-
-
-
-
{orderHeading}
- {showInfoMessage ?
{orderInfoText}
: null}
- {showInfoMessage ?
: null}
-
-
-
-
- {bookingInfo}
-
- {feedContainer}
-
-
-
-
-
-
-
-
-
-
- {bookingInfo}
-
-
-
-
this.setState({ isReviewModalOpen: false })}
- onManageDisableScrolling={onManageDisableScrolling}
- onSubmitReview={this.onSubmitReview}
- revieweeName={authorDisplayName}
- reviewSent={this.state.reviewSubmitted}
- sendReviewInProgress={sendReviewInProgress}
- sendReviewError={sendReviewError}
- />
-
- );
- }
-}
-
-OrderDetailsPanelComponent.defaultProps = {
- rootClassName: null,
- className: null,
- currentUser: null,
- fetchMessagesError: null,
- sendMessageError: null,
- sendReviewError: null,
-};
-
-const { string, arrayOf, bool, func, number } = PropTypes;
-
-OrderDetailsPanelComponent.propTypes = {
- rootClassName: string,
- className: string,
-
- currentUser: propTypes.currentUser,
- transaction: propTypes.transaction.isRequired,
- totalMessagePages: number.isRequired,
- oldestMessagePageFetched: number.isRequired,
- messages: arrayOf(propTypes.message).isRequired,
- initialMessageFailed: bool.isRequired,
- fetchMessagesInProgress: bool.isRequired,
- fetchMessagesError: propTypes.error,
- sendMessageInProgress: bool.isRequired,
- sendMessageError: propTypes.error,
- sendReviewInProgress: bool.isRequired,
- sendReviewError: propTypes.error,
- onManageDisableScrolling: func.isRequired,
- onShowMoreMessages: func.isRequired,
- onSendMessage: func.isRequired,
- onSendReview: func.isRequired,
- onResetForm: func.isRequired,
-
- // from injectIntl
- intl: intlShape,
-};
-
-const OrderDetailsPanel = injectIntl(OrderDetailsPanelComponent);
-
-export default OrderDetailsPanel;
diff --git a/src/components/OrderDetailsPanel/OrderDetailsPanel.test.js b/src/components/OrderDetailsPanel/OrderDetailsPanel.test.js
deleted file mode 100644
index 54f7f87d..00000000
--- a/src/components/OrderDetailsPanel/OrderDetailsPanel.test.js
+++ /dev/null
@@ -1,146 +0,0 @@
-import React from 'react';
-import { shallow } from 'enzyme';
-import { types } from '../../util/sdkLoader';
-import {
- createBooking,
- createListing,
- createUser,
- createTransaction,
- createCurrentUser,
- createMessage,
-} from '../../util/test-data';
-import { renderShallow } from '../../util/test-helpers';
-import { fakeIntl } from '../../util/test-data';
-import * as propTypes from '../../util/propTypes';
-import { BookingBreakdown } from '../../components';
-import { OrderDetailsPanelComponent } from './OrderDetailsPanel.js';
-
-const { Money } = types;
-
-const baseTxAttrs = {
- total: new Money(16500, 'USD'),
- booking: createBooking('booking1', {
- start: new Date(Date.UTC(2017, 5, 10)),
- end: new Date(Date.UTC(2017, 5, 13)),
- }),
- listing: createListing('listing1'),
- provider: createUser('provider'),
- customer: createUser('customer'),
-};
-
-const txPreauthorized = createTransaction({
- id: 'order-preauthorized',
- lastTransition: propTypes.TX_TRANSITION_PREAUTHORIZE,
- ...baseTxAttrs,
-});
-
-const txAccepted = createTransaction({
- id: 'order-accepted',
- lastTransition: propTypes.TX_TRANSITION_ACCEPT,
- ...baseTxAttrs,
-});
-
-const txDeclined = createTransaction({
- id: 'order-declined',
- lastTransition: propTypes.TX_TRANSITION_DECLINE,
- ...baseTxAttrs,
-});
-
-const txAutoDeclined = createTransaction({
- id: 'order-autodeclined',
- lastTransition: propTypes.TX_TRANSITION_AUTO_DECLINE,
- ...baseTxAttrs,
-});
-
-const txCanceled = createTransaction({
- id: 'order-canceled',
- lastTransition: propTypes.TX_TRANSITION_CANCELED,
- ...baseTxAttrs,
-});
-
-const txDelivered = createTransaction({
- id: 'order-delivered',
- lastTransition: propTypes.TX_TRANSITION_MARK_DELIVERED,
- ...baseTxAttrs,
-});
-
-const noop = () => null;
-
-describe('OrderDetailsPanel', () => {
- const panelBaseProps = {
- intl: fakeIntl,
- currentUser: createCurrentUser('user2'),
- totalMessages: 2,
- totalMessagePages: 1,
- oldestMessagePageFetched: 1,
- messages: [
- createMessage('msg1', {}, { sender: createUser('user1') }),
- createMessage('msg2', {}, { sender: createUser('user2') }),
- ],
- initialMessageFailed: false,
- fetchMessagesInProgress: false,
- sendMessageInProgress: false,
- sendReviewInProgress: false,
- onManageDisableScrolling: noop,
- onOpenReviewModal: noop,
- onShowMoreMessages: noop,
- onSendMessage: noop,
- onSendReview: noop,
- onResetForm: noop,
- };
-
- it('preauthorized matches snapshot', () => {
- const tree = renderShallow(
-
- );
- expect(tree).toMatchSnapshot();
- });
- it('accepted matches snapshot', () => {
- const tree = renderShallow(
-
- );
- expect(tree).toMatchSnapshot();
- });
- it('declined matches snapshot', () => {
- const tree = renderShallow(
-
- );
- expect(tree).toMatchSnapshot();
- });
- it('autodeclined matches snapshot', () => {
- const tree = renderShallow(
-
- );
- expect(tree).toMatchSnapshot();
- });
- it('canceled matches snapshot', () => {
- const tree = renderShallow(
-
- );
- expect(tree).toMatchSnapshot();
- });
- it('delivered matches snapshot', () => {
- const tree = renderShallow(
-
- );
- expect(tree).toMatchSnapshot();
- });
- it('renders correct total price', () => {
- const { Money } = types;
- const tx = createTransaction({
- id: 'order-tx',
- lastTransition: propTypes.TX_TRANSITION_PREAUTHORIZE,
- total: new Money(16500, 'USD'),
- booking: createBooking('booking1', {
- start: new Date(Date.UTC(2017, 5, 10)),
- end: new Date(Date.UTC(2017, 5, 13)),
- }),
- listing: createListing('listing1'),
- provider: createUser('provider'),
- customer: createUser('customer'),
- });
- const panel = shallow();
- const breakdownProps = panel.find(BookingBreakdown).props();
- expect(breakdownProps.transaction.attributes.payinTotal).toEqual(new Money(16500, 'USD'));
- });
-});
diff --git a/src/components/OrderDetailsPanel/__snapshots__/OrderDetailsPanel.test.js.snap b/src/components/OrderDetailsPanel/__snapshots__/OrderDetailsPanel.test.js.snap
deleted file mode 100644
index ba14c0f9..00000000
--- a/src/components/OrderDetailsPanel/__snapshots__/OrderDetailsPanel.test.js.snap
+++ /dev/null
@@ -1,3933 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`OrderDetailsPanel accepted matches snapshot 1`] = `
-
-
-
-
-
-
-
-
-
-
-
- listing1 title
- ,
- }
- }
- />
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- listing1 title
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-`;
-
-exports[`OrderDetailsPanel autodeclined matches snapshot 1`] = `
-
-
-
-
-
-
-
- listing1 title
- ,
- }
- }
- />
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- listing1 title
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-`;
-
-exports[`OrderDetailsPanel canceled matches snapshot 1`] = `
-
-
-
-
-
-
-
-
-
-
-
- listing1 title
- ,
- }
- }
- />
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- listing1 title
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-`;
-
-exports[`OrderDetailsPanel declined matches snapshot 1`] = `
-
-
-
-
-
-
-
- listing1 title
- ,
- }
- }
- />
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- listing1 title
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-`;
-
-exports[`OrderDetailsPanel delivered matches snapshot 1`] = `
-
-
-
-
-
-
-
- listing1 title
- ,
- }
- }
- />
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- listing1 title
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-`;
-
-exports[`OrderDetailsPanel preauthorized matches snapshot 1`] = `
-
-
-
-
-
-
-
-
-
-
-
- listing1 title
- ,
- }
- }
- />
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- listing1 title
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-`;
diff --git a/src/components/SaleDetailsPanel/SaleDetailsPanel.js b/src/components/SaleDetailsPanel/SaleDetailsPanel.js
deleted file mode 100644
index a3434e6a..00000000
--- a/src/components/SaleDetailsPanel/SaleDetailsPanel.js
+++ /dev/null
@@ -1,459 +0,0 @@
-import React, { Component } from 'react';
-import PropTypes from 'prop-types';
-import { injectIntl, intlShape, FormattedMessage } from 'react-intl';
-import classNames from 'classnames';
-import * as propTypes from '../../util/propTypes';
-import { createSlug } from '../../util/urlHelpers';
-import { ensureListing, ensureTransaction, ensureUser, userDisplayName } from '../../util/data';
-import { isMobileSafari } from '../../util/userAgent';
-import {
- ActivityFeed,
- AvatarLarge,
- AvatarMedium,
- BookingBreakdown,
- NamedLink,
- PrimaryButton,
- ResponsiveImage,
- ReviewModal,
- SecondaryButton,
-} from '../../components';
-import { SendMessageForm } from '../../containers';
-
-import css from './SaleDetailsPanel.css';
-
-const breakdown = transaction => {
- const loaded = transaction && transaction.id && transaction.booking && transaction.booking.id;
-
- return loaded ? (
-
- ) : null;
-};
-
-const saleTitle = (transaction, listingLink, customerName) => {
- if (propTypes.txIsPreauthorized(transaction)) {
- return (
-
- );
- } else if (propTypes.txIsAccepted(transaction)) {
- return (
-
- );
- } else if (propTypes.txIsDeclined(transaction)) {
- return (
-
- );
- } else if (propTypes.txIsAutodeclined(transaction)) {
- return (
-
- );
- } else if (propTypes.txIsCanceled(transaction)) {
- return (
-
- );
- } else if (
- propTypes.txIsDelivered(transaction) ||
- propTypes.txHasFirstReview(transaction) ||
- propTypes.txIsReviewed(transaction)
- ) {
- return (
-
- );
- } else {
- return null;
- }
-};
-
-const saleInfoText = (transaction, customerName) => {
- if (propTypes.txIsPreauthorized(transaction)) {
- return ;
- }
- return null;
-};
-
-export class SaleDetailsPanelComponent extends Component {
- constructor(props) {
- super(props);
- this.state = {
- sendMessageFormFocused: false,
- isReviewModalOpen: false,
- reviewSubmitted: false,
- };
- this.onOpenReviewModal = this.onOpenReviewModal.bind(this);
- this.onSubmitReview = this.onSubmitReview.bind(this);
- }
-
- onOpenReviewModal() {
- this.setState({ isReviewModalOpen: true });
- }
-
- onSubmitReview(values) {
- const { onSendReview, transaction } = this.props;
- const currentTransaction = ensureTransaction(transaction);
- const { reviewRating, reviewContent } = values;
- const rating = Number.parseInt(reviewRating, 10);
- onSendReview(currentTransaction, rating, reviewContent)
- .then(r => this.setState({ isReviewModalOpen: false, reviewSubmitted: true }))
- .catch(e => {
- // Do nothing.
- });
- }
-
- render() {
- const {
- rootClassName,
- className,
- currentUser,
- transaction,
- onAcceptSale,
- onDeclineSale,
- acceptInProgress,
- declineInProgress,
- acceptSaleError,
- declineSaleError,
- fetchMessagesInProgress,
- fetchMessagesError,
- totalMessagePages,
- oldestMessagePageFetched,
- messages,
- sendMessageInProgress,
- sendMessageError,
- sendReviewInProgress,
- sendReviewError,
- onManageDisableScrolling,
- onShowMoreMessages,
- onSendMessage,
- onResetForm,
- intl,
- } = this.props;
-
- const currentTransaction = ensureTransaction(transaction);
- const currentListing = ensureListing(currentTransaction.listing);
- const currentCustomer = ensureUser(currentTransaction.customer);
- const currentProvider = ensureUser(currentTransaction.provider);
- const customerLoaded = !!currentCustomer.id;
- const isCustomerBanned = customerLoaded && currentCustomer.attributes.banned;
-
- const bannedUserDisplayName = intl.formatMessage({
- id: 'SaleDetailsPanel.bannedUserDisplayName',
- });
-
- const authorDisplayName = userDisplayName(currentProvider, bannedUserDisplayName);
- const customerDisplayName = userDisplayName(currentCustomer, bannedUserDisplayName);
-
- let otherUserDisplayName = '';
- const currentUserIsCustomer =
- currentUser.id && currentCustomer.id && currentUser.id.uuid === currentCustomer.id.uuid;
- const currentUserIsProvider =
- currentUser.id && currentProvider.id && currentUser.id.uuid === currentProvider.id.uuid;
-
- if (currentUserIsCustomer) {
- otherUserDisplayName = authorDisplayName;
- } else if (currentUserIsProvider) {
- otherUserDisplayName = customerDisplayName;
- }
-
- let listingLink = null;
-
- if (currentListing.id && currentListing.attributes.title) {
- const title = currentListing.attributes.title;
- const params = { id: currentListing.id.uuid, slug: createSlug(title) };
- listingLink = (
-
- {title}
-
- );
- }
-
- const bookingInfo = breakdown(currentTransaction);
-
- const title = saleTitle(currentTransaction, listingLink, customerDisplayName);
- const infoText = isCustomerBanned
- ? intl.formatMessage({
- id: 'SaleDetailsPanel.customerBannedStatus',
- })
- : saleInfoText(currentTransaction, customerDisplayName);
- const showInfoText = !!infoText;
-
- const listingTitle = currentListing.attributes.title;
- const firstImage =
- currentListing.images && currentListing.images.length > 0 ? currentListing.images[0] : null;
-
- const feedContainerClasses = classNames(css.feedContainer, {
- [css.feedContainerWithInfoAbove]: showInfoText,
- });
- const txTransitions = currentTransaction.attributes.transitions
- ? currentTransaction.attributes.transitions
- : [];
- const hasOlderMessages = totalMessagePages > oldestMessagePageFetched;
- const handleShowOlderMessages = () => {
- onShowMoreMessages(currentTransaction.id);
- };
- const showFeed = messages.length > 0 || txTransitions.length > 0 || fetchMessagesError;
- const feedContainer = showFeed ? (
-
-
-
-
- {fetchMessagesError ? (
-
-
-
- ) : null}
-
-
- ) : null;
-
- const canShowButtons = propTypes.txIsPreauthorized(currentTransaction) && !isCustomerBanned;
- const buttonsDisabled = acceptInProgress || declineInProgress;
-
- const acceptErrorMessage = acceptSaleError ? (
-
-
-
- ) : null;
- const declineErrorMessage = declineSaleError ? (
-
-
-
- ) : null;
-
- const actionButtonClasses = classNames(css.actionButtons, {
- [css.actionButtonsWithFormFocused]: this.state.sendMessageFormFocused,
- });
- const actionButtons = canShowButtons ? (
-
-
- {acceptErrorMessage}
- {declineErrorMessage}
-
-
-
onDeclineSale(currentTransaction.id)}
- >
-
-
-
onAcceptSale(currentTransaction.id)}
- >
-
-
-
-
- ) : null;
-
- const sendMessagePlaceholder = intl.formatMessage(
- { id: 'SaleDetailsPanel.sendMessagePlaceholder' },
- { name: otherUserDisplayName }
- );
-
- const sendMessageFormName = 'SaleDetailsPanel.SendMessageForm';
- const scrollToMessage = messageId => {
- const selector = `#msg-${messageId.uuid}`;
- const el = document.querySelector(selector);
- if (el) {
- el.scrollIntoView({
- block: 'start',
- behavior: 'smooth',
- });
- }
- };
- const isMobSaf = isMobileSafari();
- const handleSendMessageFormFocus = () => {
- this.setState({ sendMessageFormFocused: true });
- if (isMobSaf) {
- // Scroll to bottom
- window.scroll({ top: document.body.scrollHeight, left: 0, behavior: 'smooth' });
- }
- };
- const handleSendMessageFormBlur = () => {
- this.setState({ sendMessageFormFocused: false });
- };
- const handleMessageSubmit = values => {
- const message = values.message ? values.message.trim() : null;
- if (!message) {
- return;
- }
- onSendMessage(currentTransaction.id, message)
- .then(messageId => {
- onResetForm(sendMessageFormName);
- scrollToMessage(messageId);
- })
- .catch(e => {
- // Ignore, Redux handles the error
- });
- };
- const sendMessageFormClasses = classNames(css.sendMessageForm, {
- [css.sendMessageFormFixed]: this.state.sendMessageFormFocused || !canShowButtons,
- [css.sendMessageFormFocusedInMobileSafari]: isMobSaf && this.state.sendMessageFormFocused,
- });
-
- const classes = classNames(rootClassName || css.root, className);
-
- return (
-
-
-
-
-
-
-
{title}
- {showInfoText ?
{infoText}
: null}
-
-
-
-
- {bookingInfo}
-
- {feedContainer}
-
-
{actionButtons}
-
-
-
-
-
-
-
-
{bookingInfo}
-
{actionButtons}
-
-
-
-
this.setState({ isReviewModalOpen: false })}
- onManageDisableScrolling={onManageDisableScrolling}
- onSubmitReview={this.onSubmitReview}
- revieweeName={customerDisplayName}
- reviewSent={this.state.reviewSubmitted}
- sendReviewInProgress={sendReviewInProgress}
- sendReviewError={sendReviewError}
- />
-
- );
- }
-}
-
-SaleDetailsPanelComponent.defaultProps = {
- rootClassName: null,
- className: null,
- currentUser: null,
- acceptSaleError: null,
- declineSaleError: null,
- fetchMessagesError: null,
- sendMessageError: null,
- sendReviewError: null,
-};
-
-const { bool, func, string, arrayOf, number } = PropTypes;
-
-SaleDetailsPanelComponent.propTypes = {
- rootClassName: string,
- className: string,
-
- currentUser: propTypes.currentUser,
- transaction: propTypes.transaction.isRequired,
- onAcceptSale: func.isRequired,
- onDeclineSale: func.isRequired,
- acceptInProgress: bool.isRequired,
- declineInProgress: bool.isRequired,
- acceptSaleError: propTypes.error,
- declineSaleError: propTypes.error,
- fetchMessagesInProgress: bool.isRequired,
- fetchMessagesError: propTypes.error,
- totalMessagePages: number.isRequired,
- oldestMessagePageFetched: number.isRequired,
- messages: arrayOf(propTypes.message).isRequired,
- sendMessageInProgress: bool.isRequired,
- sendMessageError: propTypes.error,
- sendReviewInProgress: bool.isRequired,
- sendReviewError: propTypes.error,
- onManageDisableScrolling: func.isRequired,
- onShowMoreMessages: func.isRequired,
- onSendMessage: func.isRequired,
- onSendReview: func.isRequired,
- onResetForm: func.isRequired,
-
- // from injectIntl
- intl: intlShape.isRequired,
-};
-
-const SaleDetailsPanel = injectIntl(SaleDetailsPanelComponent);
-
-export default SaleDetailsPanel;
diff --git a/src/components/SaleDetailsPanel/SaleDetailsPanel.test.js b/src/components/SaleDetailsPanel/SaleDetailsPanel.test.js
deleted file mode 100644
index aa4384ea..00000000
--- a/src/components/SaleDetailsPanel/SaleDetailsPanel.test.js
+++ /dev/null
@@ -1,167 +0,0 @@
-import React from 'react';
-import { shallow } from 'enzyme';
-import { types } from '../../util/sdkLoader';
-import {
- createTransaction,
- createBooking,
- createListing,
- createUser,
- createCurrentUser,
- createMessage,
-} from '../../util/test-data';
-import { renderShallow } from '../../util/test-helpers';
-import { fakeIntl } from '../../util/test-data';
-import * as propTypes from '../../util/propTypes';
-import { BookingBreakdown } from '../../components';
-import { SaleDetailsPanelComponent } from './SaleDetailsPanel.js';
-
-const noop = () => null;
-
-const { Money } = types;
-
-const baseTxAttrs = {
- total: new Money(16500, 'USD'),
- commission: new Money(1000, 'USD'),
- booking: createBooking('booking1', {
- start: new Date(Date.UTC(2017, 5, 10)),
- end: new Date(Date.UTC(2017, 5, 13)),
- }),
- listing: createListing('listing1'),
- customer: createUser('customer1'),
- lastTransitionedAt: new Date(Date.UTC(2017, 5, 10)),
-};
-const txPreauthorized = createTransaction({
- id: 'sale-preauthorized',
- lastTransition: propTypes.TX_TRANSITION_PREAUTHORIZE,
- ...baseTxAttrs,
-});
-
-const txAccepted = createTransaction({
- id: 'sale-accepted',
- lastTransition: propTypes.TX_TRANSITION_ACCEPT,
- ...baseTxAttrs,
-});
-
-const txDeclined = createTransaction({
- id: 'sale-declined',
- lastTransition: propTypes.TX_TRANSITION_DECLINE,
- ...baseTxAttrs,
-});
-
-const txAutoDeclined = createTransaction({
- id: 'sale-autodeclined',
- lastTransition: propTypes.TX_TRANSITION_AUTO_DECLINE,
- ...baseTxAttrs,
-});
-
-const txCanceled = createTransaction({
- id: 'sale-canceled',
- lastTransition: propTypes.TX_TRANSITION_CANCELED,
- ...baseTxAttrs,
-});
-
-const txDelivered = createTransaction({
- id: 'sale-delivered',
- lastTransition: propTypes.TX_TRANSITION_MARK_DELIVERED,
- ...baseTxAttrs,
-});
-
-describe('SaleDetailsPanel', () => {
- const panelBaseProps = {
- onAcceptSale: noop,
- onDeclineSale: noop,
- acceptInProgress: false,
- declineInProgress: false,
- currentUser: createCurrentUser('user1'),
- totalMessages: 2,
- totalMessagePages: 1,
- oldestMessagePageFetched: 1,
- messages: [
- createMessage('msg1', {}, { sender: createUser('user1') }),
- createMessage('msg2', {}, { sender: createUser('user2') }),
- ],
- fetchMessagesInProgress: false,
- sendMessageInProgress: false,
- sendReviewInProgress: false,
- onManageDisableScrolling: noop,
- onOpenReviewModal: noop,
- onShowMoreMessages: noop,
- onSendMessage: noop,
- onSendReview: noop,
- onResetForm: noop,
- intl: fakeIntl,
- };
-
- it('preauthorized matches snapshot', () => {
- const props = {
- ...panelBaseProps,
- transaction: txPreauthorized,
- };
- const tree = renderShallow();
- expect(tree).toMatchSnapshot();
- });
- it('accepted matches snapshot', () => {
- const props = {
- ...panelBaseProps,
- transaction: txAccepted,
- };
- const tree = renderShallow();
- expect(tree).toMatchSnapshot();
- });
- it('declined matches snapshot', () => {
- const props = {
- ...panelBaseProps,
- transaction: txDeclined,
- };
- const tree = renderShallow();
- expect(tree).toMatchSnapshot();
- });
- it('autodeclined matches snapshot', () => {
- const props = {
- ...panelBaseProps,
- transaction: txAutoDeclined,
- };
- const tree = renderShallow();
- expect(tree).toMatchSnapshot();
- });
- it('canceled matches snapshot', () => {
- const props = {
- ...panelBaseProps,
- transaction: txCanceled,
- };
- const tree = renderShallow();
- expect(tree).toMatchSnapshot();
- });
- it('delivered matches snapshot', () => {
- const props = {
- ...panelBaseProps,
- transaction: txDelivered,
- };
- const tree = renderShallow();
- expect(tree).toMatchSnapshot();
- });
- it('renders correct total price', () => {
- const transaction = createTransaction({
- id: 'sale-tx',
- lastTransition: propTypes.TX_TRANSITION_PREAUTHORIZE,
- total: new Money(16500, 'USD'),
- commission: new Money(1000, 'USD'),
- booking: createBooking('booking1', {
- start: new Date(Date.UTC(2017, 5, 10)),
- end: new Date(Date.UTC(2017, 5, 13)),
- }),
- listing: createListing('listing1'),
- customer: createUser('customer1'),
- lastTransitionedAt: new Date(Date.UTC(2017, 5, 10)),
- });
- const props = {
- ...panelBaseProps,
- transaction,
- };
- const panel = shallow();
- const breakdownProps = panel.find(BookingBreakdown).props();
-
- // Total price for the provider should be transaction total minus the commission.
- expect(breakdownProps.transaction.attributes.payoutTotal).toEqual(new Money(15500, 'USD'));
- });
-});
diff --git a/src/components/SaleDetailsPanel/__snapshots__/SaleDetailsPanel.test.js.snap b/src/components/SaleDetailsPanel/__snapshots__/SaleDetailsPanel.test.js.snap
deleted file mode 100644
index bc3882e1..00000000
--- a/src/components/SaleDetailsPanel/__snapshots__/SaleDetailsPanel.test.js.snap
+++ /dev/null
@@ -1,3661 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`SaleDetailsPanel accepted matches snapshot 1`] = `
-
-
-
-
-
-
-
-
- listing1 title
- ,
- }
- }
- />
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-`;
-
-exports[`SaleDetailsPanel autodeclined matches snapshot 1`] = `
-
-
-
-
-
-
-
-
- listing1 title
- ,
- }
- }
- />
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-`;
-
-exports[`SaleDetailsPanel canceled matches snapshot 1`] = `
-
-
-
-
-
-
-
-
- listing1 title
- ,
- }
- }
- />
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-`;
-
-exports[`SaleDetailsPanel declined matches snapshot 1`] = `
-
-
-
-
-
-
-
-
- listing1 title
- ,
- }
- }
- />
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-`;
-
-exports[`SaleDetailsPanel delivered matches snapshot 1`] = `
-
-
-
-
-
-
-
-
- listing1 title
- ,
- }
- }
- />
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-`;
-
-exports[`SaleDetailsPanel preauthorized matches snapshot 1`] = `
-
-
-
-
-
-
-
-
- listing1 title
- ,
- }
- }
- />
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-`;
diff --git a/src/components/SaleDetailsPanel/SaleDetailsPanel.css b/src/components/TransactionPanel/TransactionPanel.css
similarity index 73%
rename from src/components/SaleDetailsPanel/SaleDetailsPanel.css
rename to src/components/TransactionPanel/TransactionPanel.css
index 0aa98539..350189d4 100644
--- a/src/components/SaleDetailsPanel/SaleDetailsPanel.css
+++ b/src/components/TransactionPanel/TransactionPanel.css
@@ -17,19 +17,18 @@
}
}
-.saleInfo {
- margin-bottom: 104px;
+.txInfo {
+ margin-bottom: 210px;
@media (--viewportLarge) {
- max-width: 542px;
+ flex-basis: 538px;
+ flex-grow: 0;
+ flex-shrink: 1;
margin-right: 56px;
+ margin-bottom: 0;
}
}
-.nowrap {
- white-space: nowrap;
-}
-
.imageWrapperMobile {
/* Layout */
display: block;
@@ -41,25 +40,12 @@
}
}
+/* Firefox doesn't support image aspect ratio inside flexbox */
.aspectWrapper {
padding-bottom: 66.6667%; /* 3:2 Aspect Ratio */
background-color: var(--matterColorNegative); /* Loading BG color */
}
-.aspectWrapperMobile {
- @media (--viewportLarge) {
- display: none;
- }
-}
-
-.aspectWrapperDesktop {
- display: none;
-
- @media (--viewportLarge) {
- display: block;
- }
-}
-
.rootForImage {
/* Layout - image will take space defined by aspect ratio wrapper */
position: absolute;
@@ -68,68 +54,33 @@
left: 0;
right: 0;
width: 100%;
-}
-
-.title {
- margin: 22px 24px 0 24px;
-
- @media (--viewportMedium) {
- max-width: 80%;
- }
@media (--viewportLarge) {
- max-width: 100%;
- margin: 42px 0 0 0;
+ border-radius: 2px 2px 0 0;
}
}
-.infoText {
- margin: 18px 24px 0 24px;
+.avatarWrapperMobile {
+ /* Position (over the listing image)*/
+ margin-left: 24px;
+ margin-top: -31px;
- @media (--viewportMedium) {
- margin: 23px 24px 0 24px;
- }
- @media (--viewportLarge) {
- margin: 22px 0 -2px 0;
- }
+ /* Rendering context to the same lavel as listing image */
+ position: relative;
+
+ /* Layout */
+ display: block;
}
-.messagesError {
- color: var(--failColor);
- margin: 13px 0 0 0;
-}
-
-.actionError {
- @apply --marketplaceH5FontStyles;
- color: var(--failColor);
- margin: 0 0 11px 0;
-
- @media (--viewportMedium) {
- margin: 0 0 10px 0;
- }
- @media (--viewportLarge) {
- margin: 0;
- }
-}
-
-.actionErrors {
- width: 100%;
- text-align: center;
+.avatarWrapperCustomerDesktop {
+ composes: avatarWrapperMobile;
@media (--viewportLarge) {
- position: absolute;
- top: 151px;
+ margin-left: 48px;
}
}
.avatarMobile {
- /* Position (over the listing image)*/
- margin-top: -31px;
- margin-left: 24px;
-
- /* Bring on top of image */
- position: relative;
-
@media (--viewportLarge) {
display: none;
}
@@ -144,22 +95,57 @@
}
}
-.breakdown {
- margin: 14px 0 0 0;
+.headingOrder {
+ margin: 29px 24px 0 24px;
@media (--viewportMedium) {
- margin: 18px 0 0 0;
+ margin: 25px 24px 0 24px;
+ max-width: 80%;
}
+
@media (--viewportLarge) {
- margin: 14px 0 0 0;
+ max-width: 100%;
+ margin: 177px 0 0 0;
}
}
-.breakdownContainerMobile {
- margin: 47px 24px 0 24px;
+.headingSale {
+ margin: 22px 24px 0 24px;
@media (--viewportMedium) {
- margin-top: 44px;
+ max-width: 80%;
+ }
+
+ @media (--viewportLarge) {
+ max-width: 100%;
+ margin: 42px 0 0 0;
+ }
+}
+
+.mainTitle {
+ display: block;
+}
+
+.transactionInfoMessage {
+ margin: 18px 24px 0 24px;
+
+ @media (--viewportMedium) {
+ margin: 23px 24px 0 24px;
+ }
+ @media (--viewportLarge) {
+ margin: 23px 0 0 0;
+ }
+}
+
+.transitionDate {
+ white-space: nowrap;
+}
+
+.breakdownMobile {
+ margin-top: 47px;
+
+ @media (--viewportMedium) {
+ margin-top: 43px;
}
@media (--viewportLarge) {
display: none;
@@ -167,21 +153,28 @@
}
.desktopAside {
- display: none;
+ margin: 1px 0 0 0;
@media (--viewportLarge) {
- display: block;
- margin: 120px 0 0 0;
+ margin-top: 123px;
+ margin-left: 0;
+ margin-right: 0;
}
}
-.breakdownContainerDesktop {
+.breakdownDesktop {
+ display: none;
+
position: sticky;
top: -272px; /* This is a hack to showcase how the component would look when the image isn't sticky */
+ width: 409px;
background-color: var(--matterColorLight);
border: 1px solid var(--matterColorNegative);
border-radius: 2px;
- width: 409px;
+
+ @media (--viewportLarge) {
+ display: block;
+ }
}
.breakdownImageWrapper {
@@ -191,27 +184,62 @@
position: relative;
}
-.breakdownTitleMobile {
- /* Font */
- color: var(--matterColorAnti);
- margin: 0;
+.breakdownHeadings {
+ display: none;
- @media (--viewportMedium) {
- margin: 0;
+ @media (--viewportLarge) {
+ display: block;
+ margin-left: 48px;
+ margin-right: 48px;
}
}
-.breakdownTitleDesktop {
- margin: 44px 48px 18px 48px;
- color: var(--matterColorAnti);
+.breakdownTitle {
+ margin-bottom: 10px;
+
+ @media (--viewportLarge) {
+ margin-top: 14px;
+ margin-bottom: 0;
+ }
}
-.breakdownDesktop {
- margin: 0 48px 47px 48px;
+.breakdownSubtitle {
+ @apply --marketplaceH5FontStyles;
+ color: var(--matterColorAnti);
+
+ margin-top: 0;
+ margin-bottom: 0;
+
+ @media (--viewportLarge) {
+ margin-top: 1px;
+ margin-bottom: 0;
+ }
+}
+
+.bookingBreakdownTitle {
+ /* Font */
+ color: var(--matterColorAnti);
+
+ margin: 0 24px 0 24px;
+
+ @media (--viewportLarge) {
+ margin: 37px 48px 26px 48px;
+ }
+}
+
+.breakdown {
+ margin: 14px 24px 0 24px;
+
+ @media (--viewportMedium) {
+ margin: 18px 24px 0 24px;
+ }
+ @media (--viewportLarge) {
+ margin: 14px 48px 47px 48px;
+ }
}
.feedContainer {
- margin: 46px 24px 36px 24px;
+ margin: 46px 24px 0 24px;
@media (--viewportMedium) {
margin: 46px 24px 0 24px;
@@ -292,7 +320,7 @@
position: static;
box-shadow: none;
width: auto;
- margin: 0 48px 48px 48px;
+ margin: 0 48px 0 48px;
padding: 0;
}
}
@@ -310,6 +338,7 @@
@media (--viewportLarge) {
display: block;
+ margin-bottom: 48px;
}
}
@@ -341,3 +370,39 @@
}
}
}
+
+.messageError {
+ color: var(--failColor);
+ margin: 13px 0 22px 0;
+
+ @media (--viewportMedium) {
+ margin: 13px 0 23px 0;
+ }
+
+ @media (--viewportLarge) {
+ margin: 12px 0 23px 0;
+ }
+}
+
+.actionError {
+ @apply --marketplaceH5FontStyles;
+ color: var(--failColor);
+ margin: 0 0 11px 0;
+
+ @media (--viewportMedium) {
+ margin: 0 0 10px 0;
+ }
+ @media (--viewportLarge) {
+ margin: 0;
+ }
+}
+
+.actionErrors {
+ width: 100%;
+ text-align: center;
+
+ @media (--viewportLarge) {
+ position: absolute;
+ top: 151px;
+ }
+}
diff --git a/src/components/TransactionPanel/TransactionPanel.helpers.js b/src/components/TransactionPanel/TransactionPanel.helpers.js
new file mode 100644
index 00000000..21abcbc8
--- /dev/null
+++ b/src/components/TransactionPanel/TransactionPanel.helpers.js
@@ -0,0 +1,426 @@
+import React from 'react';
+import { FormattedMessage } from 'react-intl';
+import classNames from 'classnames';
+import * as propTypes from '../../util/propTypes';
+import { userDisplayName } from '../../util/data';
+import { createSlug } from '../../util/urlHelpers';
+import {
+ ActivityFeed,
+ BookingBreakdown,
+ NamedLink,
+ PrimaryButton,
+ SecondaryButton,
+} from '../../components';
+import css from './TransactionPanel.css';
+
+// Functional component as a helper to build ActivityFeed section
+export const FeedSection = props => {
+ const {
+ className,
+ rootClassName,
+ currentTransaction,
+ currentUser,
+ fetchMessagesError,
+ fetchMessagesInProgress,
+ initialMessageFailed,
+ messages,
+ oldestMessagePageFetched,
+ onShowMoreMessages,
+ onOpenReviewModal,
+ totalMessagePages,
+ } = props;
+
+ const txTransitions = currentTransaction.attributes.transitions
+ ? currentTransaction.attributes.transitions
+ : [];
+ const hasOlderMessages = totalMessagePages > oldestMessagePageFetched;
+
+ const showFeed =
+ messages.length > 0 || txTransitions.length > 0 || initialMessageFailed || fetchMessagesError;
+
+ const classes = classNames(rootClassName || css.feedContainer, className);
+
+ return showFeed ? (
+
+
+
+
+ {initialMessageFailed ? (
+
+
+
+ ) : null}
+ {fetchMessagesError ? (
+
+
+
+ ) : null}
+
{
+ onShowMoreMessages(currentTransaction.id);
+ }}
+ fetchMessagesInProgress={fetchMessagesInProgress}
+ />
+
+ ) : null;
+};
+
+// Functional component as a helper to build BookingBreakdown
+export const BreakdownMaybe = props => {
+ const { className, rootClassName, transaction, transactionRole } = props;
+ const loaded = transaction && transaction.id && transaction.booking && transaction.booking.id;
+
+ const classes = classNames(rootClassName || css.breakdown, className);
+ return loaded ? (
+
+
+
+
+
+
+ ) : null;
+};
+
+// Functional component as a helper to build ActionButtons
+// Provider only when state is preauthorized
+export const ActionButtonsMaybe = props => {
+ const {
+ className,
+ rootClassName,
+ canShowButtons,
+ transaction,
+ acceptInProgress,
+ declineInProgress,
+ acceptSaleError,
+ declineSaleError,
+ onAcceptSale,
+ onDeclineSale,
+ } = props;
+
+ const buttonsDisabled = acceptInProgress || declineInProgress;
+
+ const acceptErrorMessage = acceptSaleError ? (
+
+
+
+ ) : null;
+ const declineErrorMessage = declineSaleError ? (
+
+
+
+ ) : null;
+
+ const classes = classNames(rootClassName || css.actionButtons, className);
+
+ return canShowButtons ? (
+
+
+ {acceptErrorMessage}
+ {declineErrorMessage}
+
+
+
onDeclineSale(transaction.id)}
+ >
+
+
+
onAcceptSale(transaction.id)}
+ >
+
+
+
+
+ ) : null;
+};
+
+const createListingLink = (listingLoaded, title, id) => {
+ if (listingLoaded && title) {
+ const params = { id: id.uuid, slug: createSlug(title) };
+ return (
+
+ {title}
+
+ );
+ } else {
+ return ;
+ }
+};
+
+// Functional component as a helper to build order title
+export const OrderTitle = props => {
+ const {
+ className,
+ rootClassName,
+ transaction,
+ customerDisplayName: customerName,
+ currentListing,
+ listingTitle,
+ } = props;
+ const listingLoaded = !!currentListing.id;
+ const listingLink = createListingLink(listingLoaded, listingTitle, currentListing.id);
+
+ const classes = classNames(rootClassName || css.headingOrder, className);
+
+ if (propTypes.txIsPreauthorized(transaction)) {
+ return (
+
+
+
+
+
+
+ );
+ } else if (propTypes.txIsAccepted(transaction)) {
+ return (
+
+
+
+
+
+
+ );
+ } else if (propTypes.txIsDeclined(transaction)) {
+ return (
+
+
+
+ );
+ } else if (propTypes.txIsAutodeclined(transaction)) {
+ return (
+
+
+
+ );
+ } else if (propTypes.txIsCanceled(transaction)) {
+ return (
+
+
+
+ );
+ } else if (
+ propTypes.txIsDelivered(transaction) ||
+ propTypes.txHasFirstReview(transaction) ||
+ propTypes.txIsReviewed(transaction)
+ ) {
+ return (
+
+
+
+ );
+ } else {
+ return null;
+ }
+};
+
+// Functional component as a helper to build order message below title
+export const OrderMessage = props => {
+ const {
+ className,
+ rootClassName,
+ transaction,
+ authorDisplayName: providerName,
+ listingDeleted,
+ } = props;
+ const classes = classNames(rootClassName || css.transactionInfoMessage, className);
+
+ if (!listingDeleted && propTypes.txIsPreauthorized(transaction)) {
+ return (
+
+
+
+ );
+ } else if (listingDeleted) {
+ return (
+
+
+
+ );
+ }
+ return null;
+};
+
+// Functional component as a helper to build sale title
+export const SaleTitle = props => {
+ const {
+ className,
+ rootClassName,
+ transaction,
+ customerDisplayName: customerName,
+ currentListing,
+ listingTitle,
+ } = props;
+ const listingLoaded = !!currentListing.id;
+ const listingLink = createListingLink(listingLoaded, listingTitle, currentListing.id);
+
+ const classes = classNames(rootClassName || css.headingSale, className);
+
+ if (propTypes.txIsPreauthorized(transaction)) {
+ return (
+
+
+
+ );
+ } else if (propTypes.txIsAccepted(transaction)) {
+ return (
+
+
+
+ );
+ } else if (propTypes.txIsDeclined(transaction)) {
+ return (
+
+
+
+ );
+ } else if (propTypes.txIsAutodeclined(transaction)) {
+ return (
+
+
+
+ );
+ } else if (propTypes.txIsCanceled(transaction)) {
+ return (
+
+
+
+ );
+ } else if (
+ propTypes.txIsDelivered(transaction) ||
+ propTypes.txHasFirstReview(transaction) ||
+ propTypes.txIsReviewed(transaction)
+ ) {
+ return (
+
+
+
+ );
+ } else {
+ return null;
+ }
+};
+
+// Functional component as a helper to build sale message below title
+export const SaleMessage = props => {
+ const {
+ className,
+ rootClassName,
+ transaction,
+ customerDisplayName: customerName,
+ isCustomerBanned,
+ } = props;
+ const classes = classNames(rootClassName || css.transactionInfoMessage, className);
+
+ if (!isCustomerBanned && propTypes.txIsPreauthorized(transaction)) {
+ return (
+
+
+
+ );
+ } else if (isCustomerBanned) {
+ return (
+
+
+
+ );
+ }
+ return null;
+};
+
+// Functional component as a helper to choose and show Order or Sale title
+export const TransactionPageTitle = props => {
+ return props.transactionRole === 'customer' ? (
+
+ ) : (
+
+ );
+};
+
+// Functional component as a helper to choose and show Order or Sale message
+export const TransactionPageMessage = props => {
+ return props.transactionRole === 'customer' ? (
+
+ ) : (
+
+ );
+};
+
+// Helper function to get display names for different roles
+export const displayNames = (
+ currentUser,
+ currentProvider,
+ currentCustomer,
+ bannedUserDisplayName
+) => {
+ const authorDisplayName = userDisplayName(currentProvider, bannedUserDisplayName);
+ const customerDisplayName = userDisplayName(currentCustomer, bannedUserDisplayName);
+
+ let otherUserDisplayName = '';
+ const currentUserIsCustomer =
+ currentUser.id && currentCustomer.id && currentUser.id.uuid === currentCustomer.id.uuid;
+ const currentUserIsProvider =
+ currentUser.id && currentProvider.id && currentUser.id.uuid === currentProvider.id.uuid;
+
+ if (currentUserIsCustomer) {
+ otherUserDisplayName = authorDisplayName;
+ } else if (currentUserIsProvider) {
+ otherUserDisplayName = customerDisplayName;
+ }
+
+ return {
+ authorDisplayName,
+ customerDisplayName,
+ otherUserDisplayName,
+ };
+};
diff --git a/src/components/TransactionPanel/TransactionPanel.js b/src/components/TransactionPanel/TransactionPanel.js
new file mode 100644
index 00000000..9a0600c9
--- /dev/null
+++ b/src/components/TransactionPanel/TransactionPanel.js
@@ -0,0 +1,387 @@
+import React, { Component } from 'react';
+import PropTypes from 'prop-types';
+import { injectIntl, intlShape, FormattedMessage } from 'react-intl';
+import classNames from 'classnames';
+import * as propTypes from '../../util/propTypes';
+import { ensureListing, ensureTransaction, ensureUser } from '../../util/data';
+import { isMobileSafari } from '../../util/userAgent';
+import { AvatarMedium, AvatarLarge, ResponsiveImage, ReviewModal } from '../../components';
+import { SendMessageForm } from '../../containers';
+
+// These are internal components that make this file more readable.
+import {
+ ActionButtonsMaybe,
+ BreakdownMaybe,
+ FeedSection,
+ TransactionPageTitle,
+ TransactionPageMessage,
+ displayNames,
+} from './TransactionPanel.helpers';
+import css from './TransactionPanel.css';
+
+export class TransactionPanelComponent extends Component {
+ constructor(props) {
+ super(props);
+ this.state = {
+ sendMessageFormFocused: false,
+ isReviewModalOpen: false,
+ reviewSubmitted: false,
+ };
+ this.isMobSaf = false;
+ this.sendMessageFormName = 'TransactionPanel.SendMessageForm';
+
+ this.onOpenReviewModal = this.onOpenReviewModal.bind(this);
+ this.onSubmitReview = this.onSubmitReview.bind(this);
+ this.onSendMessageFormFocus = this.onSendMessageFormFocus.bind(this);
+ this.onSendMessageFormBlur = this.onSendMessageFormBlur.bind(this);
+ this.onMessageSubmit = this.onMessageSubmit.bind(this);
+ this.scrollToMessage = this.scrollToMessage.bind(this);
+ }
+
+ componentWillMount() {
+ this.isMobSaf = isMobileSafari();
+ }
+
+ onOpenReviewModal() {
+ this.setState({ isReviewModalOpen: true });
+ }
+
+ onSubmitReview(values) {
+ const { onSendReview, transaction } = this.props;
+ const currentTransaction = ensureTransaction(transaction);
+ const { reviewRating, reviewContent } = values;
+ const rating = Number.parseInt(reviewRating, 10);
+ onSendReview(currentTransaction, rating, reviewContent)
+ .then(r => this.setState({ isReviewModalOpen: false, reviewSubmitted: true }))
+ .catch(e => {
+ // Do nothing.
+ });
+ }
+
+ onSendMessageFormFocus() {
+ this.setState({ sendMessageFormFocused: true });
+ if (this.isMobSaf) {
+ // Scroll to bottom
+ window.scroll({ top: document.body.scrollHeight, left: 0, behavior: 'smooth' });
+ }
+ }
+
+ onSendMessageFormBlur() {
+ this.setState({ sendMessageFormFocused: false });
+ }
+
+ onMessageSubmit(values) {
+ const message = values.message ? values.message.trim() : null;
+ const { transaction, onResetForm, onSendMessage } = this.props;
+ const ensuredTransaction = ensureTransaction(transaction);
+
+ if (!message) {
+ return;
+ }
+ onSendMessage(ensuredTransaction.id, message)
+ .then(messageId => {
+ onResetForm(this.sendMessageFormName);
+ this.scrollToMessage(messageId);
+ })
+ .catch(e => {
+ // Ignore, Redux handles the error
+ });
+ }
+
+ scrollToMessage(messageId) {
+ const selector = `#msg-${messageId.uuid}`;
+ const el = document.querySelector(selector);
+ if (el) {
+ el.scrollIntoView({
+ block: 'start',
+ behavior: 'smooth',
+ });
+ }
+ }
+
+ render() {
+ const {
+ rootClassName,
+ className,
+ currentUser,
+ transaction,
+ totalMessagePages,
+ oldestMessagePageFetched,
+ messages,
+ initialMessageFailed,
+ fetchMessagesInProgress,
+ fetchMessagesError,
+ sendMessageInProgress,
+ sendMessageError,
+ sendReviewInProgress,
+ sendReviewError,
+ onManageDisableScrolling,
+ onShowMoreMessages,
+ transactionRole,
+ intl,
+ onAcceptSale,
+ onDeclineSale,
+ acceptInProgress,
+ declineInProgress,
+ acceptSaleError,
+ declineSaleError,
+ } = this.props;
+
+ const currentTransaction = ensureTransaction(transaction);
+ const currentListing = ensureListing(currentTransaction.listing);
+ const currentProvider = ensureUser(currentTransaction.provider);
+ const currentCustomer = ensureUser(currentTransaction.customer);
+ const isCustomer = transactionRole === 'customer';
+ const isProvider = transactionRole === 'provider';
+
+ const listingLoaded = !!currentListing.id;
+ const listingDeleted = listingLoaded && currentListing.attributes.deleted;
+ const customerLoaded = !!currentCustomer.id;
+ const isCustomerBanned = customerLoaded && currentCustomer.attributes.banned;
+ const canShowButtons =
+ isProvider && propTypes.txIsPreauthorized(currentTransaction) && !isCustomerBanned;
+
+ const bannedUserDisplayName = intl.formatMessage({
+ id: 'TransactionPanel.bannedUserDisplayName',
+ });
+ const deletedListingTitle = intl.formatMessage({
+ id: 'TransactionPanel.deletedListingTitle',
+ });
+
+ const { authorDisplayName, customerDisplayName, otherUserDisplayName } = displayNames(
+ currentUser,
+ currentProvider,
+ currentCustomer,
+ bannedUserDisplayName
+ );
+
+ const listingTitle = currentListing.attributes.deleted
+ ? deletedListingTitle
+ : currentListing.attributes.title;
+
+ const firstImage =
+ currentListing.images && currentListing.images.length > 0 ? currentListing.images[0] : null;
+
+ const actionButtonClasses = classNames(css.actionButtons, {
+ [css.actionButtonsWithFormFocused]: this.state.sendMessageFormFocused,
+ });
+
+ const actionButtons = (
+
+ );
+
+ const sendMessagePlaceholder = intl.formatMessage(
+ { id: 'TransactionPanel.sendMessagePlaceholder' },
+ { name: otherUserDisplayName }
+ );
+
+ const sendMessageFormClasses = classNames(css.sendMessageForm, {
+ [css.sendMessageFormFixed]: !canShowButtons || this.state.sendMessageFormFocused,
+ [css.sendMessageFormFocusedInMobileSafari]:
+ this.isMobSaf && this.state.sendMessageFormFocused,
+ });
+
+ const showInfoMessage =
+ listingDeleted || (!listingDeleted && propTypes.txIsPreauthorized(transaction)); // !!orderInfoMessage;
+
+ const feedContainerClasses = classNames(css.feedContainer, {
+ [css.feedContainerWithInfoAbove]: showInfoMessage,
+ });
+
+ const classes = classNames(rootClassName || css.root, className);
+
+ return (
+
+
+
+
+
+ {isProvider ? (
+
+ ) : null}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {isProvider && canShowButtons ? (
+
{actionButtons}
+ ) : null}
+
+
+
+
+
+ {isCustomer ? (
+
+ ) : null}
+ {isCustomer ? (
+
+ ) : null}
+
+ {isProvider && canShowButtons ? (
+
{actionButtons}
+ ) : null}
+
+
+
+
this.setState({ isReviewModalOpen: false })}
+ onManageDisableScrolling={onManageDisableScrolling}
+ onSubmitReview={this.onSubmitReview}
+ revieweeName={authorDisplayName}
+ reviewSent={this.state.reviewSubmitted}
+ sendReviewInProgress={sendReviewInProgress}
+ sendReviewError={sendReviewError}
+ />
+
+ );
+ }
+}
+
+TransactionPanelComponent.defaultProps = {
+ rootClassName: null,
+ className: null,
+ currentUser: null,
+ acceptSaleError: null,
+ declineSaleError: null,
+ fetchMessagesError: null,
+ initialMessageFailed: null,
+ sendMessageError: null,
+ sendReviewError: null,
+};
+
+const { arrayOf, bool, func, number, string } = PropTypes;
+
+TransactionPanelComponent.propTypes = {
+ rootClassName: string,
+ className: string,
+
+ currentUser: propTypes.currentUser,
+ transaction: propTypes.transaction.isRequired,
+ totalMessagePages: number.isRequired,
+ oldestMessagePageFetched: number.isRequired,
+ messages: arrayOf(propTypes.message).isRequired,
+ initialMessageFailed: bool,
+ fetchMessagesInProgress: bool.isRequired,
+ fetchMessagesError: propTypes.error,
+ sendMessageInProgress: bool.isRequired,
+ sendMessageError: propTypes.error,
+ sendReviewInProgress: bool.isRequired,
+ sendReviewError: propTypes.error,
+ onManageDisableScrolling: func.isRequired,
+ onShowMoreMessages: func.isRequired,
+ onSendMessage: func.isRequired,
+ onSendReview: func.isRequired,
+ onResetForm: func.isRequired,
+
+ // Sale related props
+ onAcceptSale: func.isRequired,
+ onDeclineSale: func.isRequired,
+ acceptInProgress: bool.isRequired,
+ declineInProgress: bool.isRequired,
+ acceptSaleError: propTypes.error,
+ declineSaleError: propTypes.error,
+
+ // from injectIntl
+ intl: intlShape,
+};
+
+const TransactionPanel = injectIntl(TransactionPanelComponent);
+
+export default TransactionPanel;
diff --git a/src/components/TransactionPanel/TransactionPanel.test.js b/src/components/TransactionPanel/TransactionPanel.test.js
new file mode 100644
index 00000000..d4b7633c
--- /dev/null
+++ b/src/components/TransactionPanel/TransactionPanel.test.js
@@ -0,0 +1,321 @@
+import React from 'react';
+import { shallow } from 'enzyme';
+import { types } from '../../util/sdkLoader';
+import {
+ createTransaction,
+ createBooking,
+ createListing,
+ createUser,
+ createCurrentUser,
+ createMessage,
+} from '../../util/test-data';
+import { renderShallow } from '../../util/test-helpers';
+import { fakeIntl } from '../../util/test-data';
+import * as propTypes from '../../util/propTypes';
+import { BreakdownMaybe } from './TransactionPanel.helpers';
+import { TransactionPanelComponent } from './TransactionPanel';
+
+const noop = () => null;
+
+const { Money } = types;
+
+describe('TransactionPanel - Sale', () => {
+ const baseTxAttrs = {
+ total: new Money(16500, 'USD'),
+ commission: new Money(1000, 'USD'),
+ booking: createBooking('booking1', {
+ start: new Date(Date.UTC(2017, 5, 10)),
+ end: new Date(Date.UTC(2017, 5, 13)),
+ }),
+ listing: createListing('listing1'),
+ customer: createUser('customer1'),
+ lastTransitionedAt: new Date(Date.UTC(2017, 5, 10)),
+ };
+ const txPreauthorized = createTransaction({
+ id: 'sale-preauthorized',
+ lastTransition: propTypes.TX_TRANSITION_PREAUTHORIZE,
+ ...baseTxAttrs,
+ });
+
+ const txAccepted = createTransaction({
+ id: 'sale-accepted',
+ lastTransition: propTypes.TX_TRANSITION_ACCEPT,
+ ...baseTxAttrs,
+ });
+
+ const txDeclined = createTransaction({
+ id: 'sale-declined',
+ lastTransition: propTypes.TX_TRANSITION_DECLINE,
+ ...baseTxAttrs,
+ });
+
+ const txAutoDeclined = createTransaction({
+ id: 'sale-autodeclined',
+ lastTransition: propTypes.TX_TRANSITION_AUTO_DECLINE,
+ ...baseTxAttrs,
+ });
+
+ const txCanceled = createTransaction({
+ id: 'sale-canceled',
+ lastTransition: propTypes.TX_TRANSITION_CANCELED,
+ ...baseTxAttrs,
+ });
+
+ const txDelivered = createTransaction({
+ id: 'sale-delivered',
+ lastTransition: propTypes.TX_TRANSITION_MARK_DELIVERED,
+ ...baseTxAttrs,
+ });
+
+ const panelBaseProps = {
+ onAcceptSale: noop,
+ onDeclineSale: noop,
+ acceptInProgress: false,
+ declineInProgress: false,
+ currentUser: createCurrentUser('user1'),
+ totalMessages: 2,
+ totalMessagePages: 1,
+ oldestMessagePageFetched: 1,
+ messages: [
+ createMessage('msg1', {}, { sender: createUser('user1') }),
+ createMessage('msg2', {}, { sender: createUser('user2') }),
+ ],
+ initialMessageFailed: false,
+ fetchMessagesInProgress: false,
+ sendMessageInProgress: false,
+ sendReviewInProgress: false,
+ onManageDisableScrolling: noop,
+ onOpenReviewModal: noop,
+ onShowMoreMessages: noop,
+ onSendMessage: noop,
+ onSendReview: noop,
+ onResetForm: noop,
+ intl: fakeIntl,
+ };
+
+ it('preauthorized matches snapshot', () => {
+ const props = {
+ ...panelBaseProps,
+ transaction: txPreauthorized,
+ };
+ const tree = renderShallow();
+ expect(tree).toMatchSnapshot();
+ });
+ it('accepted matches snapshot', () => {
+ const props = {
+ ...panelBaseProps,
+ transaction: txAccepted,
+ };
+ const tree = renderShallow();
+ expect(tree).toMatchSnapshot();
+ });
+ it('declined matches snapshot', () => {
+ const props = {
+ ...panelBaseProps,
+ transaction: txDeclined,
+ };
+ const tree = renderShallow();
+ expect(tree).toMatchSnapshot();
+ });
+ it('autodeclined matches snapshot', () => {
+ const props = {
+ ...panelBaseProps,
+ transaction: txAutoDeclined,
+ };
+ const tree = renderShallow();
+ expect(tree).toMatchSnapshot();
+ });
+ it('canceled matches snapshot', () => {
+ const props = {
+ ...panelBaseProps,
+ transaction: txCanceled,
+ };
+ const tree = renderShallow();
+ expect(tree).toMatchSnapshot();
+ });
+ it('delivered matches snapshot', () => {
+ const props = {
+ ...panelBaseProps,
+ transaction: txDelivered,
+ };
+ const tree = renderShallow();
+ expect(tree).toMatchSnapshot();
+ });
+ it('renders correct total price', () => {
+ const transaction = createTransaction({
+ id: 'sale-tx',
+ lastTransition: propTypes.TX_TRANSITION_PREAUTHORIZE,
+ total: new Money(16500, 'USD'),
+ commission: new Money(1000, 'USD'),
+ booking: createBooking('booking1', {
+ start: new Date(Date.UTC(2017, 5, 10)),
+ end: new Date(Date.UTC(2017, 5, 13)),
+ }),
+ listing: createListing('listing1'),
+ customer: createUser('customer1'),
+ lastTransitionedAt: new Date(Date.UTC(2017, 5, 10)),
+ });
+ const props = {
+ ...panelBaseProps,
+ transaction,
+ };
+ const panel = shallow();
+ const breakdownProps = panel
+ .find(BreakdownMaybe)
+ .first()
+ .props();
+
+ // Total price for the provider should be transaction total minus the commission.
+ expect(breakdownProps.transaction.attributes.payoutTotal).toEqual(new Money(15500, 'USD'));
+ });
+});
+
+describe('TransactionPanel - Order', () => {
+ const baseTxAttrs = {
+ total: new Money(16500, 'USD'),
+ booking: createBooking('booking1', {
+ start: new Date(Date.UTC(2017, 5, 10)),
+ end: new Date(Date.UTC(2017, 5, 13)),
+ }),
+ listing: createListing('listing1'),
+ provider: createUser('provider'),
+ customer: createUser('customer'),
+ };
+
+ const txPreauthorized = createTransaction({
+ id: 'order-preauthorized',
+ lastTransition: propTypes.TX_TRANSITION_PREAUTHORIZE,
+ ...baseTxAttrs,
+ });
+
+ const txAccepted = createTransaction({
+ id: 'order-accepted',
+ lastTransition: propTypes.TX_TRANSITION_ACCEPT,
+ ...baseTxAttrs,
+ });
+
+ const txDeclined = createTransaction({
+ id: 'order-declined',
+ lastTransition: propTypes.TX_TRANSITION_DECLINE,
+ ...baseTxAttrs,
+ });
+
+ const txAutoDeclined = createTransaction({
+ id: 'order-autodeclined',
+ lastTransition: propTypes.TX_TRANSITION_AUTO_DECLINE,
+ ...baseTxAttrs,
+ });
+
+ const txCanceled = createTransaction({
+ id: 'order-canceled',
+ lastTransition: propTypes.TX_TRANSITION_CANCELED,
+ ...baseTxAttrs,
+ });
+
+ const txDelivered = createTransaction({
+ id: 'order-delivered',
+ lastTransition: propTypes.TX_TRANSITION_MARK_DELIVERED,
+ ...baseTxAttrs,
+ });
+
+ const panelBaseProps = {
+ intl: fakeIntl,
+ currentUser: createCurrentUser('user2'),
+ totalMessages: 2,
+ totalMessagePages: 1,
+ oldestMessagePageFetched: 1,
+ messages: [
+ createMessage('msg1', {}, { sender: createUser('user1') }),
+ createMessage('msg2', {}, { sender: createUser('user2') }),
+ ],
+ initialMessageFailed: false,
+ fetchMessagesInProgress: false,
+ sendMessageInProgress: false,
+ sendReviewInProgress: false,
+ onManageDisableScrolling: noop,
+ onOpenReviewModal: noop,
+ onShowMoreMessages: noop,
+ onSendMessage: noop,
+ onSendReview: noop,
+ onResetForm: noop,
+ onAcceptSale: noop,
+ onDeclineSale: noop,
+ acceptInProgress: false,
+ declineInProgress: false,
+ };
+
+ it('preauthorized matches snapshot', () => {
+ const props = {
+ ...panelBaseProps,
+ transaction: txPreauthorized,
+ };
+
+ const tree = renderShallow();
+ expect(tree).toMatchSnapshot();
+ });
+ it('accepted matches snapshot', () => {
+ const props = {
+ ...panelBaseProps,
+ transaction: txAccepted,
+ };
+ const tree = renderShallow();
+ expect(tree).toMatchSnapshot();
+ });
+ it('declined matches snapshot', () => {
+ const props = {
+ ...panelBaseProps,
+ transaction: txDeclined,
+ };
+ const tree = renderShallow();
+ expect(tree).toMatchSnapshot();
+ });
+ it('autodeclined matches snapshot', () => {
+ const props = {
+ ...panelBaseProps,
+ transaction: txAutoDeclined,
+ };
+ const tree = renderShallow();
+ expect(tree).toMatchSnapshot();
+ });
+ it('canceled matches snapshot', () => {
+ const props = {
+ ...panelBaseProps,
+ transaction: txCanceled,
+ };
+ const tree = renderShallow();
+ expect(tree).toMatchSnapshot();
+ });
+ it('delivered matches snapshot', () => {
+ const props = {
+ ...panelBaseProps,
+ transaction: txDelivered,
+ };
+ const tree = renderShallow();
+ expect(tree).toMatchSnapshot();
+ });
+ it('renders correct total price', () => {
+ const { Money } = types;
+ const tx = createTransaction({
+ id: 'order-tx',
+ lastTransition: propTypes.TX_TRANSITION_PREAUTHORIZE,
+ total: new Money(16500, 'USD'),
+ booking: createBooking('booking1', {
+ start: new Date(Date.UTC(2017, 5, 10)),
+ end: new Date(Date.UTC(2017, 5, 13)),
+ }),
+ listing: createListing('listing1'),
+ provider: createUser('provider'),
+ customer: createUser('customer'),
+ });
+ const props = {
+ ...panelBaseProps,
+ transaction: tx,
+ };
+ const panel = shallow();
+ const breakdownProps = panel
+ .find(BreakdownMaybe)
+ .first()
+ .props();
+ expect(breakdownProps.transaction.attributes.payinTotal).toEqual(new Money(16500, 'USD'));
+ });
+});
diff --git a/src/components/TransactionPanel/__snapshots__/TransactionPanel.test.js.snap b/src/components/TransactionPanel/__snapshots__/TransactionPanel.test.js.snap
new file mode 100644
index 00000000..790a8a41
--- /dev/null
+++ b/src/components/TransactionPanel/__snapshots__/TransactionPanel.test.js.snap
@@ -0,0 +1,9493 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`TransactionPanel - Order accepted matches snapshot 1`] = `
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+`;
+
+exports[`TransactionPanel - Order autodeclined matches snapshot 1`] = `
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+`;
+
+exports[`TransactionPanel - Order canceled matches snapshot 1`] = `
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+`;
+
+exports[`TransactionPanel - Order declined matches snapshot 1`] = `
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+`;
+
+exports[`TransactionPanel - Order delivered matches snapshot 1`] = `
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+`;
+
+exports[`TransactionPanel - Order preauthorized matches snapshot 1`] = `
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+`;
+
+exports[`TransactionPanel - Sale accepted matches snapshot 1`] = `
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+`;
+
+exports[`TransactionPanel - Sale autodeclined matches snapshot 1`] = `
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+`;
+
+exports[`TransactionPanel - Sale canceled matches snapshot 1`] = `
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+`;
+
+exports[`TransactionPanel - Sale declined matches snapshot 1`] = `
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+`;
+
+exports[`TransactionPanel - Sale delivered matches snapshot 1`] = `
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+`;
+
+exports[`TransactionPanel - Sale preauthorized matches snapshot 1`] = `
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+`;
diff --git a/src/components/index.js b/src/components/index.js
index 8cd12c2b..a88c5cad 100644
--- a/src/components/index.js
+++ b/src/components/index.js
@@ -76,7 +76,6 @@ export { default as ModalInMobile } from './ModalInMobile/ModalInMobile';
export { default as NamedLink } from './NamedLink/NamedLink';
export { default as NamedRedirect } from './NamedRedirect/NamedRedirect';
export { default as NotificationBadge } from './NotificationBadge/NotificationBadge';
-export { default as OrderDetailsPanel } from './OrderDetailsPanel/OrderDetailsPanel';
export { default as OrderDiscussionPanel } from './OrderDiscussionPanel/OrderDiscussionPanel';
export { default as Page } from './Page/Page';
export { default as PaginationLinks } from './PaginationLinks/PaginationLinks';
@@ -86,7 +85,6 @@ export { default as ResponsiveImage } from './ResponsiveImage/ResponsiveImage';
export { default as ReviewModal } from './ReviewModal/ReviewModal';
export { default as ReviewRating } from './ReviewRating/ReviewRating';
export { default as Reviews } from './Reviews/Reviews';
-export { default as SaleDetailsPanel } from './SaleDetailsPanel/SaleDetailsPanel';
export { default as SearchMap } from './SearchMap/SearchMap';
export { default as SearchMapGroupLabel } from './SearchMapGroupLabel/SearchMapGroupLabel';
export { default as SearchMapInfoCard } from './SearchMapInfoCard/SearchMapInfoCard';
@@ -107,6 +105,7 @@ export { default as TextInputField } from './TextInputField/TextInputField';
export { default as Topbar } from './Topbar/Topbar';
export { default as TopbarDesktop } from './TopbarDesktop/TopbarDesktop';
export { default as TopbarMobileMenu } from './TopbarMobileMenu/TopbarMobileMenu';
+export { default as TransactionPanel } from './TransactionPanel/TransactionPanel';
export { default as UserCard } from './UserCard/UserCard';
export { default as UserNav } from './UserNav/UserNav';
export { default as ValidationError } from './ValidationError/ValidationError';
diff --git a/src/containers/SendMessageForm/SendMessageForm.css b/src/containers/SendMessageForm/SendMessageForm.css
index ae0bf2ea..b775125d 100644
--- a/src/containers/SendMessageForm/SendMessageForm.css
+++ b/src/containers/SendMessageForm/SendMessageForm.css
@@ -23,6 +23,10 @@
margin: 0;
max-height: 183px;
+ /* We need to remove horizontal paddings,
+ since textarea uses content-box (autosize library fix). */
+ width: calc(100vw - 100px);
+
background-color: var(--matterColorLight);
@media (--viewportMedium) {
@@ -32,6 +36,7 @@
padding: 0 0 5px 0;
margin: 0;
max-height: initial;
+ width: 100%;
border-bottom-width: 2px;
border-bottom-color: var(--attentionColor);
background-color: transparent;
diff --git a/src/containers/TransactionPage/TransactionPage.js b/src/containers/TransactionPage/TransactionPage.js
index b1e9db68..460ade0a 100644
--- a/src/containers/TransactionPage/TransactionPage.js
+++ b/src/containers/TransactionPage/TransactionPage.js
@@ -11,8 +11,7 @@ import { getMarketplaceEntities } from '../../ducks/marketplaceData.duck';
import { isScrollingDisabled, manageDisableScrolling } from '../../ducks/UI.duck';
import {
NamedRedirect,
- OrderDetailsPanel,
- SaleDetailsPanel,
+ TransactionPanel,
Page,
LayoutSingleColumn,
LayoutWrapperTopbar,
@@ -71,7 +70,12 @@ export const TransactionPageComponent = props => {
const currentTransaction = ensureTransaction(transaction);
const currentListing = ensureListing(currentTransaction.listing);
- const listingTitle = currentListing.attributes.title;
+ const deletedListingTitle = intl.formatMessage({
+ id: 'TransactionPage.deletedListing',
+ });
+ const listingTitle = currentListing.attributes.deleted
+ ? deletedListingTitle
+ : currentListing.attributes.title;
// Redirect users with someone else's direct link to their own inbox/sales or inbox/orders page.
const isDataAvailable =
@@ -122,42 +126,16 @@ export const TransactionPageComponent = props => {
);
- const salePanel = isDataAvailable ? (
-
- ) : null;
-
const initialMessageFailed = !!(
initialMessageFailedToTransaction &&
currentTransaction.id &&
initialMessageFailedToTransaction.uuid === currentTransaction.id.uuid
);
- const orderPanel = isDataAvailable ? (
- {
onSendMessage={onSendMessage}
onSendReview={onSendReview}
onResetForm={onResetForm}
+ transactionRole={transactionRole}
+ onAcceptSale={onAcceptSale}
+ onDeclineSale={onDeclineSale}
+ acceptInProgress={acceptInProgress}
+ declineInProgress={declineInProgress}
+ acceptSaleError={acceptSaleError}
+ declineSaleError={declineSaleError}
/>
- ) : null;
-
- const saleOrOrderPanel = isDataAvailable && isOwnSale ? salePanel : orderPanel;
- const panel = isDataAvailable ? saleOrOrderPanel : loadingOrFailedFetching;
+ ) : (
+ loadingOrFailedFetching
+ );
return (
-
@@ -204,7 +211,7 @@ exports[`TransactionPage - Sale matches snapshot 1`] = `
rootClassName={null}
>
-
diff --git a/src/translations/en.json b/src/translations/en.json
index 7c5cf3cf..a4540ac1 100644
--- a/src/translations/en.json
+++ b/src/translations/en.json
@@ -1,4 +1,6 @@
{
+ "ActivityFeed.bannedUserDisplayName": "Banned user",
+ "ActivityFeed.deletedListing": "deleted listing",
"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.",
@@ -284,24 +286,6 @@
"NotFoundPage.description": "We can't find the page or sauna you're looking for. Make sure you've typed in the URL correctly or try searching Saunatime.",
"NotFoundPage.heading": "Sorry, we couldn't find that page.",
"NotFoundPage.title": "Page not found",
- "OrderDetailsPanel.activityHeading": "Activity",
- "OrderDetailsPanel.bannedUserDisplayName": "Banned user",
- "OrderDetailsPanel.bookingBreakdownTitle": "Booking breakdown",
- "OrderDetailsPanel.deletedListingOrderTitle": "a listing",
- "OrderDetailsPanel.deletedListingTitle": "Deleted listing",
- "OrderDetailsPanel.hostedBy": "Hosted by {name}",
- "OrderDetailsPanel.initialMessageFailed": "Whoops, failed to send message from checkout.",
- "OrderDetailsPanel.messageDeletedListing": "However, the listing is deleted and cannot be viewed anymore.",
- "OrderDetailsPanel.messageLoadingFailed": "Something went wrong when loading messages. Please refresh the page and try again.",
- "OrderDetailsPanel.orderAcceptedSubtitle": "Your booking for {listingLink} has been accepted.",
- "OrderDetailsPanel.orderAcceptedTitle": "Woohoo {customerName}!",
- "OrderDetailsPanel.orderCancelledTitle": "{customerName}, your booking for {listingLink} has been cancelled.",
- "OrderDetailsPanel.orderDeclinedTitle": "{customerName}, your booking for {listingLink} has been declined.",
- "OrderDetailsPanel.orderDeliveredTitle": "{customerName}, your booking for {listingLink} has been completed.",
- "OrderDetailsPanel.orderPreauthorizedInfo": "{providerName} has been notified about the booking request. Sit back and relax.",
- "OrderDetailsPanel.orderPreauthorizedSubtitle": "You have requested to book {listingLink}.",
- "OrderDetailsPanel.orderPreauthorizedTitle": "Great success, {customerName}!",
- "OrderDetailsPanel.sendMessagePlaceholder": "Send a message to {name}…",
"Page.schemaDescription": "Book a sauna using Saunatime or earn some income by sharing your sauna",
"Page.schemaTitle": "Book saunas everywhere | {siteTitle}",
"PaginationLinks.next": "Next page",
@@ -458,22 +442,6 @@
"ReviewModal.description": "Reviews are an important part of the Saunatime community. Please share what went well and what could have been improved.",
"ReviewModal.later": "Later",
"ReviewModal.title": "Leave a review for {revieweeName}",
- "SaleDetailsPanel.acceptButton": "Accept",
- "SaleDetailsPanel.acceptSaleFailed": "Oops, accepting failed. Please try again.",
- "SaleDetailsPanel.activityHeading": "Activity",
- "SaleDetailsPanel.bannedUserDisplayName": "Banned user",
- "SaleDetailsPanel.bookingBreakdownTitle": "Booking breakdown",
- "SaleDetailsPanel.customerBannedStatus": "The user made the request, but was later banned.",
- "SaleDetailsPanel.declineButton": "Decline",
- "SaleDetailsPanel.declineSaleFailed": "Oops, declining failed. Please try again.",
- "SaleDetailsPanel.messageLoadingFailed": "Something went wrong when loading messages. Please refresh the page and try again.",
- "SaleDetailsPanel.saleAcceptedTitle": "You accepted a request from {customerName} to book {listingLink}.",
- "SaleDetailsPanel.saleCancelledTitle": "The booking from {customerName} for {listingLink} has been cancelled.",
- "SaleDetailsPanel.saleDeclinedTitle": "The request from {customerName} to book {listingLink} has been declined.",
- "SaleDetailsPanel.saleDeliveredTitle": "The booking from {customerName} for {listingLink} has been completed.",
- "SaleDetailsPanel.saleRequestedInfo": "{customerName} is waiting for your response.",
- "SaleDetailsPanel.saleRequestedTitle": "{customerName} has requested to book {listingLink}.",
- "SaleDetailsPanel.sendMessagePlaceholder": "Send a message to {name}…",
"SearchMapInfoCard.noImage": "No image",
"SearchPage.foundResults": "{count, number} {count, plural, one {sauna} other {saunas}} found",
"SearchPage.foundResultsMobile": "{count, number} {count, plural, one {result} other {results}}",
@@ -621,11 +589,41 @@
"TopbarMobileMenu.yourListingsLink": "Your listings",
"TopbarSearchForm.placeholder": "Search saunas…",
"TopbarSearchForm.searchHelp": "Tip: You can also search saunas by zip code, for example \"00500\" or city district \"Sörnäinen\".",
+ "TransactionPage.deletedListing": "deleted listing",
"TransactionPage.fetchOrderFailed": "Fetching order data failed.",
"TransactionPage.fetchSaleFailed": "Fetching sale data failed.",
"TransactionPage.loadingOrderData": "Loading order data.",
"TransactionPage.loadingSaleData": "Loading sale data.",
"TransactionPage.title": "Sale details: {title}",
+ "TransactionPanel.acceptButton": "Accept",
+ "TransactionPanel.acceptSaleFailed": "Oops, accepting failed. Please try again.",
+ "TransactionPanel.activityHeading": "Activity",
+ "TransactionPanel.bannedUserDisplayName": "Banned user",
+ "TransactionPanel.bookingBreakdownTitle": "Booking breakdown",
+ "TransactionPanel.customerBannedStatus": "The user made the request, but was later banned.",
+ "TransactionPanel.declineButton": "Decline",
+ "TransactionPanel.declineSaleFailed": "Oops, declining failed. Please try again.",
+ "TransactionPanel.deletedListingOrderTitle": "a listing",
+ "TransactionPanel.deletedListingTitle": "Deleted listing",
+ "TransactionPanel.hostedBy": "Hosted by {name}",
+ "TransactionPanel.initialMessageFailed": "Whoops, failed to send message from checkout.",
+ "TransactionPanel.messageDeletedListing": "However, the listing is deleted and cannot be viewed anymore.",
+ "TransactionPanel.messageLoadingFailed": "Something went wrong when loading messages. Please refresh the page and try again.",
+ "TransactionPanel.orderAcceptedSubtitle": "Your booking for {listingLink} has been accepted.",
+ "TransactionPanel.orderAcceptedTitle": "Woohoo {customerName}!",
+ "TransactionPanel.orderCancelledTitle": "{customerName}, your booking for {listingLink} has been cancelled.",
+ "TransactionPanel.orderDeclinedTitle": "{customerName}, your booking for {listingLink} has been declined.",
+ "TransactionPanel.orderDeliveredTitle": "{customerName}, your booking for {listingLink} has been completed.",
+ "TransactionPanel.orderPreauthorizedInfo": "{providerName} has been notified about the booking request. Sit back and relax.",
+ "TransactionPanel.orderPreauthorizedSubtitle": "You have requested to book {listingLink}.",
+ "TransactionPanel.orderPreauthorizedTitle": "Great success, {customerName}!",
+ "TransactionPanel.saleAcceptedTitle": "You accepted a request from {customerName} to book {listingLink}.",
+ "TransactionPanel.saleCancelledTitle": "The booking from {customerName} for {listingLink} has been cancelled.",
+ "TransactionPanel.saleDeclinedTitle": "The request from {customerName} to book {listingLink} has been declined.",
+ "TransactionPanel.saleDeliveredTitle": "The booking from {customerName} for {listingLink} has been completed.",
+ "TransactionPanel.saleRequestedInfo": "{customerName} is waiting for your response.",
+ "TransactionPanel.saleRequestedTitle": "{customerName} has requested to book {listingLink}.",
+ "TransactionPanel.sendMessagePlaceholder": "Send a message to {name}…",
"UserCard.contactUser": "Contact",
"UserCard.heading": "Hello, I'm {name}.",
"UserCard.showFullBioLink": "more",
diff --git a/src/util/propTypes.js b/src/util/propTypes.js
index 748de3dc..f2c63d2e 100644
--- a/src/util/propTypes.js
+++ b/src/util/propTypes.js
@@ -17,13 +17,23 @@
* defined. This way we get the validation errors only in the most
* specific place and avoid duplicate errros.
*/
-import PropTypes from 'prop-types';
+import {
+ arrayOf,
+ bool,
+ func,
+ instanceOf,
+ number,
+ object,
+ oneOf,
+ oneOfType,
+ shape,
+ string,
+} from 'prop-types';
import Decimal from 'decimal.js';
import { types as sdkTypes } from './sdkLoader';
import { ensureTransaction } from './data';
const { UUID, LatLng, LatLngBounds, Money } = sdkTypes;
-const { arrayOf, bool, func, instanceOf, number, object, oneOf, shape, string } = PropTypes;
// Fixed value
export const value = val => oneOf([val]);
@@ -113,19 +123,26 @@ export const user = shape({
profileImage: image,
});
+const listingAttributes = shape({
+ title: string.isRequired,
+ description: string.isRequired,
+ address: string.isRequired,
+ geolocation: latlng.isRequired,
+ closed: bool.isRequired,
+ deleted: value(false).isRequired,
+ price: money,
+});
+
+const deletedListingAttributes = shape({
+ closed: bool.isRequired,
+ deleted: value(true).isRequired,
+});
+
// Denormalised listing object
export const listing = shape({
id: uuid.isRequired,
type: value('listing').isRequired,
- attributes: shape({
- title: string.isRequired,
- description: string.isRequired,
- address: string.isRequired,
- geolocation: latlng.isRequired,
- closed: bool.isRequired,
- deleted: bool.isRequired,
- price: money,
- }).isRequired,
+ attributes: oneOfType([listingAttributes, deletedListingAttributes]).isRequired,
author: user,
images: arrayOf(image),
});