diff --git a/CHANGELOG.md b/CHANGELOG.md
index d926f69d..9333e3d9 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -14,6 +14,10 @@ way to update this template, but currently, we follow a pattern:
## Upcoming version 2019-XX-XX
+- [change] Extracted and refactored utility functions related to transaction and refactored several
+ components that show transaction data (incl. InboxPage, TransactionPanel, ActivityFeed). Before
+ updating your customization project, you should read more about what has changed from the pull
+ request. [#1004](https://github.com/sharetribe/flex-template-web/pull/1004)
- [change] Rest of the documentation moved to Flex Docs: https://www.sharetribe.com/docs/
[#1015](https://github.com/sharetribe/flex-template-web/pull/1015)
diff --git a/src/components/ActivityFeed/ActivityFeed.example.js b/src/components/ActivityFeed/ActivityFeed.example.js
index 8d94c2e2..af9dbe80 100644
--- a/src/components/ActivityFeed/ActivityFeed.example.js
+++ b/src/components/ActivityFeed/ActivityFeed.example.js
@@ -20,7 +20,7 @@ import {
TRANSITION_REVIEW_2_BY_PROVIDER,
TX_TRANSITION_ACTOR_CUSTOMER,
TX_TRANSITION_ACTOR_PROVIDER,
-} from '../../util/types';
+} from '../../util/transaction';
import ActivityFeed from './ActivityFeed';
const noop = () => null;
diff --git a/src/components/ActivityFeed/ActivityFeed.js b/src/components/ActivityFeed/ActivityFeed.js
index aec9f100..534e79ec 100644
--- a/src/components/ActivityFeed/ActivityFeed.js
+++ b/src/components/ActivityFeed/ActivityFeed.js
@@ -18,11 +18,17 @@ import {
TRANSITION_REVIEW_1_BY_PROVIDER,
TRANSITION_REVIEW_2_BY_CUSTOMER,
TRANSITION_REVIEW_2_BY_PROVIDER,
- TX_TRANSITION_ACTOR_CUSTOMER,
- TX_TRANSITION_ACTOR_PROVIDER,
- areReviewsCompleted,
- propTypes,
-} from '../../util/types';
+ transitionIsReviewed,
+ txIsDelivered,
+ txIsInFirstReviewBy,
+ txIsReviewed,
+ isCustomerReview,
+ isProviderReview,
+ txRoleIsProvider,
+ getUserTxRole,
+ isRelevantPastTransition,
+} from '../../util/transaction';
+import { propTypes } from '../../util/types';
import * as log from '../../util/log';
import css from './ActivityFeed.css';
@@ -87,48 +93,16 @@ Review.propTypes = {
rating: number.isRequired,
};
-// Check if a transition is the kind that
-// should be rendered in he ActivityFeed
-const shouldRenderTransition = transition => {
- return [
- TRANSITION_ACCEPT,
- TRANSITION_CANCEL,
- TRANSITION_COMPLETE,
- TRANSITION_DECLINE,
- TRANSITION_EXPIRE,
- TRANSITION_REQUEST,
- TRANSITION_REQUEST_AFTER_ENQUIRY,
- TRANSITION_REVIEW_1_BY_CUSTOMER,
- TRANSITION_REVIEW_1_BY_PROVIDER,
- TRANSITION_REVIEW_2_BY_CUSTOMER,
- TRANSITION_REVIEW_2_BY_PROVIDER,
- ].includes(transition);
-};
-
-// Check if a user giving a review is related to
-// given tx transition.
-const isReviewTransition = transition => {
- return [
- TRANSITION_REVIEW_1_BY_CUSTOMER,
- TRANSITION_REVIEW_1_BY_PROVIDER,
- TRANSITION_REVIEW_2_BY_CUSTOMER,
- TRANSITION_REVIEW_2_BY_PROVIDER,
- ].includes(transition);
-};
-
-const hasUserLeftAReviewFirst = (userRole, lastTransition) => {
- return (
- (lastTransition === TRANSITION_REVIEW_1_BY_CUSTOMER &&
- userRole === TX_TRANSITION_ACTOR_CUSTOMER) ||
- (lastTransition === TRANSITION_REVIEW_1_BY_PROVIDER &&
- userRole === TX_TRANSITION_ACTOR_PROVIDER) ||
- areReviewsCompleted(lastTransition)
- );
+const hasUserLeftAReviewFirst = (userRole, transaction) => {
+ const isProvider = txRoleIsProvider(userRole);
+ return isProvider
+ ? txIsInFirstReviewBy(transaction, isProvider)
+ : txIsInFirstReviewBy(transaction, !isProvider);
};
const resolveTransitionMessage = (
+ transaction,
transition,
- lastTransition,
listingTitle,
ownRole,
otherUsersName,
@@ -138,7 +112,6 @@ const resolveTransitionMessage = (
const isOwnTransition = transition.by === ownRole;
const currentTransition = transition.transition;
const displayName = otherUsersName;
- const deliveredState = lastTransition === TRANSITION_COMPLETE;
switch (currentTransition) {
case TRANSITION_REQUEST:
@@ -164,7 +137,7 @@ const resolveTransitionMessage = (
);
case TRANSITION_EXPIRE:
- return ownRole === TX_TRANSITION_ACTOR_PROVIDER ? (
+ return txRoleIsProvider(ownRole) ? (
) : (
@@ -174,14 +147,19 @@ const resolveTransitionMessage = (
case TRANSITION_COMPLETE:
// Show the leave a review link if the state is delivered or
// if current user is not the first to leave a review
+ const reviewPeriodJustStarted = txIsDelivered(transaction);
+ const reviewPeriodIsOver = txIsReviewed(transaction);
+ const userHasLeftAReview = hasUserLeftAReviewFirst(ownRole, transaction);
+
const reviewLink =
- deliveredState || !hasUserLeftAReviewFirst(ownRole, lastTransition) ? (
+ reviewPeriodJustStarted || !(reviewPeriodIsOver || userHasLeftAReview) ? (
) : null;
return ;
+
case TRANSITION_REVIEW_1_BY_PROVIDER:
case TRANSITION_REVIEW_1_BY_CUSTOMER:
if (isOwnTransition) {
@@ -189,7 +167,9 @@ const resolveTransitionMessage = (
} else {
// show the leave a review link if current user is not the first
// one to leave a review
- const reviewLink = !hasUserLeftAReviewFirst(ownRole, lastTransition) ? (
+ const reviewPeriodIsOver = txIsReviewed(transaction);
+ const userHasLeftAReview = hasUserLeftAReviewFirst(ownRole, transaction);
+ const reviewLink = !(reviewPeriodIsOver || userHasLeftAReview) ? (
@@ -241,22 +221,18 @@ const Transition = props => {
: currentTransaction.listing.attributes.title;
const lastTransition = currentTransaction.attributes.lastTransition;
- const ownRole =
- currentUser.id.uuid === customer.id.uuid
- ? TX_TRANSITION_ACTOR_CUSTOMER
- : TX_TRANSITION_ACTOR_PROVIDER;
+ const ownRole = getUserTxRole(currentUser.id, currentTransaction);
const bannedUserDisplayName = intl.formatMessage({
id: 'ActivityFeed.bannedUserDisplayName',
});
- const otherUsersName =
- ownRole === TX_TRANSITION_ACTOR_CUSTOMER
- ? userDisplayName(provider, bannedUserDisplayName)
- : userDisplayName(customer, bannedUserDisplayName);
+ const otherUsersName = txRoleIsProvider(ownRole)
+ ? userDisplayName(customer, bannedUserDisplayName)
+ : userDisplayName(provider, bannedUserDisplayName);
const transitionMessage = resolveTransitionMessage(
+ transaction,
transition,
- lastTransition,
listingTitle,
ownRole,
otherUsersName,
@@ -267,19 +243,13 @@ const Transition = props => {
let reviewComponent = null;
- if (isReviewTransition(currentTransition) && areReviewsCompleted(lastTransition)) {
- const customerReview =
- currentTransition === TRANSITION_REVIEW_1_BY_CUSTOMER ||
- currentTransition === TRANSITION_REVIEW_2_BY_CUSTOMER;
- const providerReview =
- currentTransition === TRANSITION_REVIEW_1_BY_PROVIDER ||
- currentTransition === TRANSITION_REVIEW_2_BY_PROVIDER;
- if (customerReview) {
+ if (transitionIsReviewed(lastTransition)) {
+ if (isCustomerReview(currentTransition)) {
const review = reviewByAuthorId(currentTransaction, customer.id);
reviewComponent = (
);
- } else if (providerReview) {
+ } else if (isProviderReview(currentTransition)) {
const review = reviewByAuthorId(currentTransaction, provider.id);
reviewComponent = (
@@ -417,7 +387,7 @@ export const ActivityFeedComponent = props => {
};
const transitionListItem = transition => {
- if (shouldRenderTransition(transition.transition)) {
+ if (isRelevantPastTransition(transition.transition)) {
return (
{transitionComponent(transition)}
diff --git a/src/components/BookingBreakdown/BookingBreakdown.css b/src/components/BookingBreakdown/BookingBreakdown.css
index 7ff45a13..60801bbb 100644
--- a/src/components/BookingBreakdown/BookingBreakdown.css
+++ b/src/components/BookingBreakdown/BookingBreakdown.css
@@ -66,7 +66,7 @@
font-weight: var(--fontWeightSemiBold);
/* Move so that baseline aligns with the total price */
- padding-top: 7px;
+ padding-top: 6px;
@media (--viewportMedium) {
padding-top: 8px;
@@ -77,9 +77,11 @@
@apply --marketplaceBodyFontStyles;
font-weight: var(--fontWeightSemiBold);
margin: 0 0 0 10px;
+ padding-top: 4px;
@media (--viewportMedium) {
margin: 2px 0 0 10px;
+ padding-top: 0;
}
}
diff --git a/src/components/BookingBreakdown/BookingBreakdown.example.js b/src/components/BookingBreakdown/BookingBreakdown.example.js
index ef262952..a326183b 100644
--- a/src/components/BookingBreakdown/BookingBreakdown.example.js
+++ b/src/components/BookingBreakdown/BookingBreakdown.example.js
@@ -1,9 +1,6 @@
import Decimal from 'decimal.js';
import { types as sdkTypes } from '../../util/sdkLoader';
import {
- LINE_ITEM_DAY,
- LINE_ITEM_NIGHT,
- LINE_ITEM_UNITS,
TRANSITION_ACCEPT,
TRANSITION_CANCEL,
TRANSITION_COMPLETE,
@@ -11,7 +8,8 @@ import {
TRANSITION_EXPIRE,
TRANSITION_REQUEST,
TX_TRANSITION_ACTOR_CUSTOMER,
-} from '../../util/types';
+} from '../../util/transaction';
+import { LINE_ITEM_DAY, LINE_ITEM_NIGHT, LINE_ITEM_UNITS } from '../../util/types';
import config from '../../config';
import BookingBreakdown from './BookingBreakdown';
diff --git a/src/components/BookingBreakdown/BookingBreakdown.test.js b/src/components/BookingBreakdown/BookingBreakdown.test.js
index b4263d54..f6736293 100644
--- a/src/components/BookingBreakdown/BookingBreakdown.test.js
+++ b/src/components/BookingBreakdown/BookingBreakdown.test.js
@@ -4,11 +4,11 @@ import { fakeIntl, createBooking } from '../../util/test-data';
import { renderDeep } from '../../util/test-helpers';
import { types as sdkTypes } from '../../util/sdkLoader';
import {
- LINE_ITEM_NIGHT,
TRANSITION_CANCEL,
TRANSITION_REQUEST,
TX_TRANSITION_ACTOR_CUSTOMER,
-} from '../../util/types';
+} from '../../util/transaction';
+import { LINE_ITEM_NIGHT } from '../../util/types';
import { BookingBreakdownComponent } from './BookingBreakdown';
const { UUID, Money } = sdkTypes;
diff --git a/src/components/BookingBreakdown/LineItemTotalPrice.js b/src/components/BookingBreakdown/LineItemTotalPrice.js
index 40c4b98b..c55a036d 100644
--- a/src/components/BookingBreakdown/LineItemTotalPrice.js
+++ b/src/components/BookingBreakdown/LineItemTotalPrice.js
@@ -2,7 +2,8 @@ import React from 'react';
import { bool } from 'prop-types';
import { FormattedMessage, intlShape } from 'react-intl';
import { formatMoney } from '../../util/currency';
-import { txIsCanceled, txIsCompleted, txIsDeclinedOrExpired, propTypes } from '../../util/types';
+import { txIsCanceled, txIsDelivered, txIsDeclined } from '../../util/transaction';
+import { propTypes } from '../../util/types';
import css from './BookingBreakdown.css';
@@ -10,9 +11,9 @@ const LineItemUnitPrice = props => {
const { transaction, isProvider, intl } = props;
let providerTotalMessageId = 'BookingBreakdown.providerTotalDefault';
- if (txIsCompleted(transaction)) {
+ if (txIsDelivered(transaction)) {
providerTotalMessageId = 'BookingBreakdown.providerTotalDelivered';
- } else if (txIsDeclinedOrExpired(transaction)) {
+ } else if (txIsDeclined(transaction)) {
providerTotalMessageId = 'BookingBreakdown.providerTotalDeclined';
} else if (txIsCanceled(transaction)) {
providerTotalMessageId = 'BookingBreakdown.providerTotalCanceled';
diff --git a/src/components/TransactionPanel/AddressLinkMaybe.js b/src/components/TransactionPanel/AddressLinkMaybe.js
new file mode 100644
index 00000000..eba5470c
--- /dev/null
+++ b/src/components/TransactionPanel/AddressLinkMaybe.js
@@ -0,0 +1,29 @@
+import React from 'react';
+import classNames from 'classnames';
+import { ExternalLink } from '../../components';
+
+import css from './TransactionPanel.css';
+
+// Functional component as a helper to build AddressLinkMaybe
+const AddressLinkMaybe = props => {
+ const { className, rootClassName, location, geolocation, showAddress } = props;
+ const { address, building } = location || {};
+ const { lat, lng } = geolocation || {};
+ const hrefToGoogleMaps = geolocation
+ ? `https://maps.google.com/?q=${lat},${lng}`
+ : address
+ ? `https://maps.google.com/?q=${encodeURIComponent(address)}`
+ : null;
+
+ const fullAddress =
+ typeof building === 'string' && building.length > 0 ? `${building}, ${address}` : address;
+
+ const classes = classNames(rootClassName || css.address, className);
+ return showAddress && hrefToGoogleMaps ? (
+
+ {fullAddress}
+
+ ) : null;
+};
+
+export default AddressLinkMaybe;
diff --git a/src/components/TransactionPanel/BreakdownMaybe.js b/src/components/TransactionPanel/BreakdownMaybe.js
new file mode 100644
index 00000000..c2dc26d1
--- /dev/null
+++ b/src/components/TransactionPanel/BreakdownMaybe.js
@@ -0,0 +1,33 @@
+import React from 'react';
+import { FormattedMessage } from 'react-intl';
+import classNames from 'classnames';
+import config from '../../config';
+import { BookingBreakdown } from '../../components';
+
+import css from './TransactionPanel.css';
+
+// Functional component as a helper to build BookingBreakdown
+const BreakdownMaybe = props => {
+ const { className, rootClassName, breakdownClassName, transaction, transactionRole } = props;
+ const loaded = transaction && transaction.id && transaction.booking && transaction.booking.id;
+
+ const classes = classNames(rootClassName || css.breakdownMaybe, className);
+ const breakdownClasses = classNames(breakdownClassName || css.breakdown);
+
+ return loaded ? (
+
+
+
+
+
+
+ ) : null;
+};
+
+export default BreakdownMaybe;
diff --git a/src/components/TransactionPanel/DetailCardHeadingsMaybe.js b/src/components/TransactionPanel/DetailCardHeadingsMaybe.js
new file mode 100644
index 00000000..a58e51fa
--- /dev/null
+++ b/src/components/TransactionPanel/DetailCardHeadingsMaybe.js
@@ -0,0 +1,26 @@
+import React from 'react';
+import AddressLinkMaybe from './AddressLinkMaybe';
+
+import css from './TransactionPanel.css';
+
+// Functional component as a helper to build detail card headings
+const DetailCardHeadingsMaybe = props => {
+ const {
+ showDetailCardHeadings,
+ listingTitle,
+ subTitle,
+ location,
+ geolocation,
+ showAddress,
+ } = props;
+
+ return showDetailCardHeadings ? (
+
+
{listingTitle}
+
{subTitle}
+
+
+ ) : null;
+};
+
+export default DetailCardHeadingsMaybe;
diff --git a/src/components/TransactionPanel/DetailCardImage.js b/src/components/TransactionPanel/DetailCardImage.js
new file mode 100644
index 00000000..abe6607e
--- /dev/null
+++ b/src/components/TransactionPanel/DetailCardImage.js
@@ -0,0 +1,40 @@
+import React from 'react';
+import classNames from 'classnames';
+import { AvatarMedium, ResponsiveImage } from '../../components';
+
+import css from './TransactionPanel.css';
+
+// Functional component as a helper to build AddressLinkMaybe
+const DetailCardImage = props => {
+ const {
+ className,
+ rootClassName,
+ avatarWrapperClassName,
+ listingTitle,
+ image,
+ provider,
+ isCustomer,
+ } = props;
+ const classes = classNames(rootClassName || css.detailCardImageWrapper, className);
+ return (
+
+
+ {isCustomer ? (
+
+ ) : null}
+
+ );
+};
+
+export default DetailCardImage;
diff --git a/src/components/TransactionPanel/FeedSection.js b/src/components/TransactionPanel/FeedSection.js
new file mode 100644
index 00000000..51d7ccfb
--- /dev/null
+++ b/src/components/TransactionPanel/FeedSection.js
@@ -0,0 +1,64 @@
+import React from 'react';
+import { FormattedMessage } from 'react-intl';
+import classNames from 'classnames';
+import { ActivityFeed } from '../../components';
+
+import css from './TransactionPanel.css';
+
+// Functional component as a helper to build ActivityFeed section
+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}
+
+
+ ) : null;
+};
+
+export default FeedSection;
diff --git a/src/components/TransactionPanel/PanelHeading.js b/src/components/TransactionPanel/PanelHeading.js
new file mode 100644
index 00000000..b41275f9
--- /dev/null
+++ b/src/components/TransactionPanel/PanelHeading.js
@@ -0,0 +1,226 @@
+import React from 'react';
+import { FormattedMessage } from 'react-intl';
+import classNames from 'classnames';
+import { createSlug, stringify } from '../../util/urlHelpers';
+import { NamedLink } from '../../components';
+
+import css from './TransactionPanel.css';
+
+export const HEADING_ENQUIRED = 'enquired';
+export const HEADING_REQUESTED = 'requested';
+export const HEADING_ACCEPTED = 'accepted';
+export const HEADING_DECLINED = 'declined';
+export const HEADING_CANCELED = 'canceled';
+export const HEADING_DELIVERED = 'deliveded';
+
+const createListingLink = (listingId, label, listingDeleted, searchParams = {}, className = '') => {
+ if (!listingDeleted) {
+ const params = { id: listingId, slug: createSlug(label) };
+ const to = { search: stringify(searchParams) };
+ return (
+
+ {label}
+
+ );
+ } else {
+ return ;
+ }
+};
+
+const ListingDeletedInfoMaybe = props => {
+ return props.listingDeleted ? (
+
+
+
+ ) : null;
+};
+
+const HeadingCustomer = props => {
+ const { className, id, values, listingDeleted } = props;
+ return (
+
+
+
+
+
+
+
+
+ );
+};
+
+const HeadingCustomerWithSubtitle = props => {
+ const { className, id, values, subtitleId, subtitleValues, children, listingDeleted } = props;
+ return (
+
+
+
+
+
+
+
+ {children}
+
+
+ );
+};
+
+const CustomerBannedInfoMaybe = props => {
+ return props.isCustomerBanned ? (
+
+
+
+ ) : null;
+};
+
+const HeadingProvider = props => {
+ const { className, id, values, isCustomerBanned } = props;
+ return (
+
+
+
+
+
+
+
+
+ );
+};
+
+// Functional component as a helper to choose and show Order or Sale heading info:
+// title, subtitle, and message
+const PanelHeading = props => {
+ const {
+ className,
+ rootClassName,
+ panelHeadingState,
+ customerName,
+ providerName,
+ listingId,
+ listingTitle,
+ listingDeleted,
+ isCustomerBanned,
+ } = props;
+
+ const isCustomer = props.transactionRole === 'customer';
+
+ const defaultRootClassName = isCustomer ? css.headingOrder : css.headingSale;
+ const titleClasses = classNames(rootClassName || defaultRootClassName, className);
+ const listingLink = createListingLink(listingId, listingTitle, listingDeleted);
+
+ switch (panelHeadingState) {
+ case HEADING_ENQUIRED:
+ return isCustomer ? (
+
+ ) : (
+
+ );
+ case HEADING_REQUESTED:
+ return isCustomer ? (
+
+ {!listingDeleted ? (
+
+
+
+ ) : null}
+
+ ) : (
+
+ {!isCustomerBanned ? (
+
+
+
+ ) : null}
+
+ );
+ case HEADING_ACCEPTED:
+ return isCustomer ? (
+
+ ) : (
+
+ );
+ case HEADING_DECLINED:
+ return isCustomer ? (
+
+ ) : (
+
+ );
+ case HEADING_CANCELED:
+ return isCustomer ? (
+
+ ) : (
+
+ );
+ case HEADING_DELIVERED:
+ return isCustomer ? (
+
+ ) : (
+
+ );
+ default:
+ console.warning('Unknown state given to panel heading.');
+ return null;
+ }
+};
+
+export default PanelHeading;
diff --git a/src/components/TransactionPanel/SaleActionButtonsMaybe.js b/src/components/TransactionPanel/SaleActionButtonsMaybe.js
new file mode 100644
index 00000000..6951afd9
--- /dev/null
+++ b/src/components/TransactionPanel/SaleActionButtonsMaybe.js
@@ -0,0 +1,64 @@
+import React from 'react';
+import { FormattedMessage } from 'react-intl';
+import classNames from 'classnames';
+import { PrimaryButton, SecondaryButton } from '../../components';
+
+import css from './TransactionPanel.css';
+
+// Functional component as a helper to build ActionButtons for
+// provider when state is preauthorized
+const SaleActionButtonsMaybe = props => {
+ const {
+ className,
+ rootClassName,
+ showButtons,
+ 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 showButtons ? (
+
+
+ {acceptErrorMessage}
+ {declineErrorMessage}
+
+
+
+ ) : null;
+};
+
+export default SaleActionButtonsMaybe;
diff --git a/src/components/TransactionPanel/TransactionPanel.css b/src/components/TransactionPanel/TransactionPanel.css
index bd0bc237..6403385a 100644
--- a/src/components/TransactionPanel/TransactionPanel.css
+++ b/src/components/TransactionPanel/TransactionPanel.css
@@ -29,14 +29,17 @@
}
}
-.imageWrapperMobile {
+/* DetailCardImage subcomponent */
+.detailCardImageWrapper {
/* Layout */
- display: block;
+ display: none;
width: 100%;
position: relative;
@media (--viewportLarge) {
- display: none;
+ display: block;
+ max-height: 268px;
+ overflow-y: hidden;
}
}
@@ -60,37 +63,51 @@
}
}
-.avatarWrapperMobile {
+.avatarWrapper {
/* Position (over the listing image)*/
margin-left: 24px;
- margin-top: -31px;
+ margin-top: -30px;
/* Rendering context to the same lavel as listing image */
position: relative;
-
- /* Layout */
- display: block;
-}
-
-.avatarWrapperCustomerDesktop {
- display: none;
- composes: avatarWrapperMobile;
-
- @media (--viewportLarge) {
- display: block;
- margin-left: 48px;
- }
-}
-
-.avatarMobile {
/* Flex would give too much width by default. */
width: 60px;
+ @media (--viewportMedium) {
+ margin-top: -32px;
+ padding: 2px 0;
+ }
+ @media (--viewportLarge) {
+ margin-left: 48px;
+ width: unset;
+ padding: 2px 0;
+ }
+}
+
+/* Passed-in props for DetailCardImage subcomponent */
+.imageWrapperMobile {
@media (--viewportLarge) {
display: none;
}
}
+.avatarWrapperMobile {
+ composes: avatarWrapper;
+
+ @media (--viewportLarge) {
+ display: none;
+ }
+}
+
+.avatarWrapperDesktop {
+ composes: avatarWrapper;
+ display: none;
+
+ @media (--viewportLarge) {
+ display: block;
+ }
+}
+
.avatarWrapperProviderDesktop {
display: none;
@@ -105,30 +122,37 @@
}
}
+/* PanelHeadings subcomponent */
.headingOrder {
margin: 29px 24px 0 24px;
@media (--viewportMedium) {
- margin: 25px 24px 0 24px;
max-width: 80%;
+ margin: 24px 24px 0 24px;
+ padding: 1px 0 7px 0;
}
@media (--viewportLarge) {
max-width: 100%;
margin: 152px 0 0 0;
+ padding: 0;
}
}
.headingSale {
- margin: 22px 24px 0 24px;
+ margin: 18px 24px 0 24px;
+ padding: 5px 0 1px 0;
@media (--viewportMedium) {
max-width: 80%;
+ margin: 24px 24px 0 24px;
+ padding: 1px 0 7px 0;
}
@media (--viewportLarge) {
max-width: 100%;
margin: 42px 0 0 0;
+ padding: 0;
}
}
@@ -147,28 +171,30 @@
}
}
-.transitionDate {
- white-space: nowrap;
-}
-
+/* Container for booking details in mobile layout */
.bookingDetailsMobile {
margin-top: 47px;
@media (--viewportMedium) {
- margin-top: 43px;
+ margin-top: 40px;
+ padding: 4px 0 0px 0;
}
@media (--viewportLarge) {
display: none;
}
}
+/* "aside" section in desktop layout */
.asideDesktop {
margin: 1px 0 0 0;
+ /**
+ * Aside is visible on mobile layout too, since it includes BookingPanel component.
+ * It might get rendered as a Modal in mobile layout.
+ */
+
@media (--viewportLarge) {
- margin-top: 123px;
- margin-left: 0;
- margin-right: 0;
+ margin: 123px 0 0 0;
}
}
@@ -184,26 +210,13 @@
}
}
-.detailCardImageWrapper {
- display: none;
-
- /* Layout */
- width: 100%;
- position: relative;
-
- @media (--viewportLarge) {
- display: block;
- }
-}
-
+/* DetailCardHeadingsMaybe subcomponent */
.detailCardHeadings {
display: none;
@media (--viewportLarge) {
display: block;
- margin-left: 48px;
- margin-right: 48px;
- margin-bottom: 37px;
+ margin: 0 48px 33px 48px;
}
}
@@ -228,16 +241,25 @@
}
}
+/* AddressLinkMaybe subcomponent */
.address {
@apply --marketplaceH5FontStyles;
color: var(--matterColorAnti);
margin: 0;
}
-.addressMobileWrapper {
+/* Passed-in props for AddressLinkMaybe subcomponent */
+.addressMobile {
+ @apply --marketplaceH5FontStyles;
+ color: var(--matterColorAnti);
margin: 0 24px 24px 24px;
}
+/* BreakdownMaybe subcomponent */
+.breakdownMaybe {
+ /* default "root" class for breakdown container */
+}
+
.bookingBreakdownTitle {
/* Font */
color: var(--matterColorAnti);
@@ -246,6 +268,20 @@
@media (--viewportLarge) {
margin: 37px 48px 26px 48px;
+ margin: 32px 48px 24px 48px;
+ padding: 4px 0 4px 0;
+ }
+}
+
+.breakdown {
+ margin: 14px 24px 0 24px;
+
+ @media (--viewportMedium) {
+ margin: 18px 24px 0 24px;
+ }
+ @media (--viewportLarge) {
+ margin: 24px 48px 47px 48px;
+ padding: 6px 0 2px 0;
}
}
@@ -257,34 +293,7 @@
}
}
-.breakdown {
- margin: 14px 24px 0 24px;
-
- @media (--viewportMedium) {
- margin: 18px 24px 0 24px;
- }
- @media (--viewportLarge) {
- margin: 14px 48px 47px 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;
- }
-}
-
+/* FeedSection subcomponent */
.feedHeading {
color: var(--matterColorAnti);
margin: 0;
@@ -298,6 +307,32 @@
margin-top: 20px;
}
+.messageError {
+ color: var(--failColor);
+ margin: 13px 0 22px 0;
+
+ @media (--viewportMedium) {
+ margin: 13px 0 23px 0;
+ }
+
+ @media (--viewportLarge) {
+ margin: 12px 0 23px 0;
+ }
+}
+
+/* Passed-in props for FeedSection subcomponent */
+.feedContainer {
+ margin: 46px 24px 0 24px;
+
+ @media (--viewportMedium) {
+ margin: 40px 24px 0 24px;
+ }
+ @media (--viewportLarge) {
+ margin: 43px 0 0 0;
+ }
+}
+
+/* Prop to be passed to SendMessageForm component */
.sendMessageForm {
position: relative;
margin: 46px 24px 0 24px;
@@ -311,12 +346,7 @@
}
}
-.requestToBookButton {
- @apply --marketplaceButtonStylesPrimary;
-
- text-decoration: none;
-}
-
+/* SaleActionButtonsMaybe subcomponent */
.actionButtons {
/* Position action button row above the footer */
z-index: 9;
@@ -342,23 +372,6 @@
}
}
-.mobileActionButtons {
- display: block;
-
- @media (--viewportLarge) {
- display: none;
- }
-}
-
-.desktopActionButtons {
- display: none;
-
- @media (--viewportLarge) {
- display: block;
- margin-bottom: 48px;
- }
-}
-
.actionButtonWrapper {
width: 100%;
display: flex;
@@ -380,19 +393,6 @@
}
}
-.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);
@@ -416,6 +416,25 @@
}
}
+/* Container for SaleActionButtonsMaybe subcomponent */
+.mobileActionButtons {
+ display: block;
+
+ @media (--viewportLarge) {
+ display: none;
+ }
+}
+
+.desktopActionButtons {
+ display: none;
+
+ @media (--viewportLarge) {
+ display: block;
+ margin-bottom: 48px;
+ }
+}
+
+/* BookingPanel subcompnent */
.bookingPanel {
margin: 16px 48px 48px 48px;
}
diff --git a/src/components/TransactionPanel/TransactionPanel.helpers.js b/src/components/TransactionPanel/TransactionPanel.helpers.js
deleted file mode 100644
index 022614d6..00000000
--- a/src/components/TransactionPanel/TransactionPanel.helpers.js
+++ /dev/null
@@ -1,550 +0,0 @@
-import React from 'react';
-import { FormattedMessage } from 'react-intl';
-import classNames from 'classnames';
-import {
- txHasBeenAccepted,
- txHasFirstReview,
- txIsAccepted,
- txIsCanceled,
- txIsCompleted,
- txIsDeclined,
- txIsEnquired,
- txIsExpired,
- txIsRequested,
- txIsReviewed,
-} from '../../util/types';
-import { userDisplayName } from '../../util/data';
-import { createSlug, stringify } from '../../util/urlHelpers';
-import {
- ActivityFeed,
- BookingBreakdown,
- BookingPanel,
- ExternalLink,
- NamedLink,
- PrimaryButton,
- SecondaryButton,
-} from '../../components';
-import config from '../../config';
-
-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 AddressLinkMaybe
-export const AddressLinkMaybe = props => {
- const { transaction, transactionRole, currentListing } = props;
-
- const isCustomer = transactionRole === 'customer';
- const txIsAcceptedForCustomer = isCustomer && txHasBeenAccepted(transaction);
-
- const publicData = currentListing.attributes.publicData || {};
- const { address, building } = publicData.location || {};
- const geolocation = currentListing.attributes.geolocation;
-
- const { lat, lng } = geolocation || {};
- const hrefToGoogleMaps = geolocation
- ? `https://maps.google.com/?q=${lat},${lng}`
- : address
- ? `https://maps.google.com/?q=${encodeURIComponent(address)}`
- : null;
-
- const fullAddress =
- typeof building === 'string' && building.length > 0 ? `${building}, ${address}` : address;
-
- return txIsAcceptedForCustomer && hrefToGoogleMaps ? (
-
- {fullAddress}
-
- ) : null;
-};
-
-// Functional component as a helper to build BookingBreakdown
-export const BreakdownMaybe = props => {
- const { className, rootClassName, breakdownClassName, transaction, transactionRole } = props;
- const loaded = transaction && transaction.id && transaction.booking && transaction.booking.id;
-
- const classes = classNames(rootClassName || className);
- const breakdownClasses = classNames(css.breakdown, breakdownClassName);
-
- return loaded ? (
-
-
-
-
-
-
- ) : null;
-};
-
-const createListingLink = (listing, label, searchParams = {}, className = '') => {
- const listingLoaded = !!listing.id;
-
- if (listingLoaded && !listing.attributes.deleted) {
- const title = listing.attributes.title;
- const params = { id: listing.id.uuid, slug: createSlug(title) };
- const to = { search: stringify(searchParams) };
- return (
-
- {label}
-
- );
- } else {
- return ;
- }
-};
-
-// Functional component as a helper to build detail card headings
-export const DetailCardHeadingsMaybe = props => {
- const { transaction, transactionRole, listing, listingTitle, subTitle } = props;
-
- const isCustomer = transactionRole === 'customer';
- const canShowDetailCardHeadings = isCustomer && !txIsEnquired(transaction);
-
- return canShowDetailCardHeadings ? (
-
-
{listingTitle}
-
{subTitle}
-
-
- ) : null;
-};
-
-// Functional component as a helper to build a BookingPanel
-export const BookingPanelMaybe = props => {
- const {
- authorDisplayName,
- transaction,
- transactionRole,
- listing,
- listingTitle,
- subTitle,
- provider,
- onSubmit,
- onManageDisableScrolling,
- timeSlots,
- fetchTimeSlotsError,
- } = props;
-
- const isProviderLoaded = !!provider.id;
- const isProviderBanned = isProviderLoaded && provider.attributes.banned;
- const isCustomer = transactionRole === 'customer';
- const canShowBookingPanel = isCustomer && txIsEnquired(transaction) && !isProviderBanned;
-
- return canShowBookingPanel ? (
- console.log('submit')}
- title={listingTitle}
- subTitle={subTitle}
- authorDisplayName={authorDisplayName}
- onSubmit={onSubmit}
- onManageDisableScrolling={onManageDisableScrolling}
- timeSlots={timeSlots}
- fetchTimeSlotsError={fetchTimeSlotsError}
- />
- ) : null;
-};
-
-// Functional component as a helper to build ActionButtons for
-// provider when state is preauthorized
-export const SaleActionButtonsMaybe = 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;
-};
-
-// Functional component as a helper to build order title
-export const OrderTitle = props => {
- const {
- className,
- rootClassName,
- transaction,
- customerDisplayName: customerName,
- currentListing,
- listingTitle,
- } = props;
- const listingLink = createListingLink(currentListing, listingTitle);
-
- const classes = classNames(rootClassName || css.headingOrder, className);
-
- if (txIsEnquired(transaction)) {
- return (
-
-
-
-
-
- );
- } else if (txIsRequested(transaction)) {
- return (
-
-
-
-
-
-
- );
- } else if (txIsAccepted(transaction)) {
- return (
-
-
-
-
-
-
- );
- } else if (txIsDeclined(transaction)) {
- return (
-
-
-
- );
- } else if (txIsExpired(transaction)) {
- return (
-
-
-
- );
- } else if (txIsCanceled(transaction)) {
- return (
-
-
-
- );
- } else if (
- txIsCompleted(transaction) ||
- txHasFirstReview(transaction) ||
- 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 && txIsRequested(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 listingLink = createListingLink(currentListing, listingTitle);
-
- const classes = classNames(rootClassName || css.headingSale, className);
-
- if (txIsEnquired(transaction)) {
- return (
-
-
-
- );
- } else if (txIsRequested(transaction)) {
- return (
-
-
-
- );
- } else if (txIsAccepted(transaction)) {
- return (
-
-
-
- );
- } else if (txIsDeclined(transaction)) {
- return (
-
-
-
- );
- } else if (txIsExpired(transaction)) {
- return (
-
-
-
- );
- } else if (txIsCanceled(transaction)) {
- return (
-
-
-
- );
- } else if (
- txIsCompleted(transaction) ||
- txHasFirstReview(transaction) ||
- 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 && txIsRequested(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
index c068bb2d..b59b0ad5 100644
--- a/src/components/TransactionPanel/TransactionPanel.js
+++ b/src/components/TransactionPanel/TransactionPanel.js
@@ -2,29 +2,64 @@ import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { injectIntl, intlShape } from 'react-intl';
import classNames from 'classnames';
-import { txIsRequested, LINE_ITEM_NIGHT, LINE_ITEM_DAY, propTypes } from '../../util/types';
-import { ensureListing, ensureTransaction, ensureUser } from '../../util/data';
+import {
+ txIsAccepted,
+ txIsCanceled,
+ txIsDeclined,
+ txIsEnquired,
+ txIsRequested,
+ txHasBeenDelivered,
+} from '../../util/transaction';
+import { LINE_ITEM_NIGHT, LINE_ITEM_DAY, propTypes } from '../../util/types';
+import { ensureListing, ensureTransaction, ensureUser, userDisplayName } from '../../util/data';
import { isMobileSafari } from '../../util/userAgent';
import { formatMoney } from '../../util/currency';
-import { AvatarMedium, AvatarLarge, ResponsiveImage, ReviewModal } from '../../components';
+import { AvatarLarge, BookingPanel, ReviewModal } from '../../components';
import { SendMessageForm } from '../../forms';
import config from '../../config';
// These are internal components that make this file more readable.
-import {
- AddressLinkMaybe,
- BookingPanelMaybe,
- BreakdownMaybe,
- DetailCardHeadingsMaybe,
- FeedSection,
- SaleActionButtonsMaybe,
- TransactionPageTitle,
- TransactionPageMessage,
- displayNames,
-} from './TransactionPanel.helpers';
+import AddressLinkMaybe from './AddressLinkMaybe';
+import BreakdownMaybe from './BreakdownMaybe';
+import DetailCardHeadingsMaybe from './DetailCardHeadingsMaybe';
+import DetailCardImage from './DetailCardImage';
+import FeedSection from './FeedSection';
+import SaleActionButtonsMaybe from './SaleActionButtonsMaybe';
+import PanelHeading, {
+ HEADING_ENQUIRED,
+ HEADING_REQUESTED,
+ HEADING_ACCEPTED,
+ HEADING_DECLINED,
+ HEADING_CANCELED,
+ HEADING_DELIVERED,
+} from './PanelHeading';
import css from './TransactionPanel.css';
+// Helper function to get display names for different roles
+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,
+ };
+};
+
export class TransactionPanelComponent extends Component {
constructor(props) {
super(props);
@@ -147,7 +182,48 @@ export class TransactionPanelComponent extends Component {
const listingDeleted = listingLoaded && currentListing.attributes.deleted;
const customerLoaded = !!currentCustomer.id;
const isCustomerBanned = customerLoaded && currentCustomer.attributes.banned;
- const canShowSaleButtons = isProvider && txIsRequested(currentTransaction) && !isCustomerBanned;
+ const isProviderLoaded = !!currentProvider.id;
+ const isProviderBanned = isProviderLoaded && currentProvider.attributes.banned;
+
+ const stateDataFn = tx => {
+ if (txIsEnquired(tx)) {
+ return {
+ headingState: HEADING_ENQUIRED,
+ showBookingPanel: isCustomer && !isProviderBanned,
+ };
+ } else if (txIsRequested(tx)) {
+ return {
+ headingState: HEADING_REQUESTED,
+ showDetailCardHeadings: isCustomer,
+ showSaleButtons: isProvider && !isCustomerBanned,
+ };
+ } else if (txIsAccepted(tx)) {
+ return {
+ headingState: HEADING_ACCEPTED,
+ showDetailCardHeadings: isCustomer,
+ showAddress: isCustomer,
+ };
+ } else if (txIsDeclined(tx)) {
+ return {
+ headingState: HEADING_DECLINED,
+ showDetailCardHeadings: isCustomer,
+ };
+ } else if (txIsCanceled(tx)) {
+ return {
+ headingState: HEADING_CANCELED,
+ showDetailCardHeadings: isCustomer,
+ };
+ } else if (txHasBeenDelivered(tx)) {
+ return {
+ headingState: HEADING_DELIVERED,
+ showDetailCardHeadings: isCustomer,
+ showAddress: isCustomer,
+ };
+ } else {
+ return { headingState: 'unknown' };
+ }
+ };
+ const stateData = stateDataFn(currentTransaction);
const bannedUserDisplayName = intl.formatMessage({
id: 'TransactionPanel.bannedUserDisplayName',
@@ -163,6 +239,8 @@ export class TransactionPanelComponent extends Component {
bannedUserDisplayName
);
+ const { publicData = {}, geolocation } = currentListing.attributes;
+ const location = publicData.location || {};
const listingTitle = currentListing.attributes.deleted
? deletedListingTitle
: currentListing.attributes.title;
@@ -185,19 +263,15 @@ export class TransactionPanelComponent extends Component {
const firstImage =
currentListing.images && currentListing.images.length > 0 ? currentListing.images[0] : null;
- const actionButtonClasses = classNames(css.actionButtons);
-
const saleButtons = (
onAcceptSale(currentTransaction.id)}
+ onDeclineSale={() => onDeclineSale(currentTransaction.id)}
/>
);
@@ -206,68 +280,49 @@ export class TransactionPanelComponent extends Component {
{ name: otherUserDisplayName }
);
- const sendMessageFormClasses = classNames(css.sendMessageForm);
-
- const showInfoMessage = listingDeleted || (!listingDeleted && txIsRequested(transaction)); // !!orderInfoMessage;
-
- const feedContainerClasses = classNames(css.feedContainer, {
- [css.feedContainerWithInfoAbove]: showInfoMessage,
- });
-
const classes = classNames(rootClassName || css.root, className);
return (
-
-
+
{isProvider ? (
) : null}
-
-
onShowMoreMessages(currentTransaction.id)}
totalMessagePages={totalMessagePages}
/>
- {canShowSaleButtons ? (
+ {stateData.showSaleButtons ? (
{saleButtons}
) : null}
-
- {isCustomer ? (
-
- ) : null}
+
-
+ {stateData.showBookingPanel ? (
+
+ ) : null}
- {canShowSaleButtons ? (
+ {stateData.showSaleButtons ? (
{saleButtons}
) : null}
diff --git a/src/components/TransactionPanel/TransactionPanel.test.js b/src/components/TransactionPanel/TransactionPanel.test.js
index b3443d0a..8f6b299a 100644
--- a/src/components/TransactionPanel/TransactionPanel.test.js
+++ b/src/components/TransactionPanel/TransactionPanel.test.js
@@ -2,6 +2,7 @@ import React from 'react';
import { shallow } from 'enzyme';
import { types as sdkTypes } from '../../util/sdkLoader';
import {
+ createTxTransition,
createTransaction,
createBooking,
createListing,
@@ -19,8 +20,8 @@ import {
TRANSITION_ENQUIRE,
TRANSITION_EXPIRE,
TRANSITION_REQUEST,
-} from '../../util/types';
-import { BreakdownMaybe } from './TransactionPanel.helpers';
+} from '../../util/transaction';
+import BreakdownMaybe from './BreakdownMaybe';
import { TransactionPanelComponent } from './TransactionPanel';
const noop = () => null;
@@ -82,6 +83,23 @@ describe('TransactionPanel - Sale', () => {
const txDelivered = createTransaction({
id: 'sale-delivered',
lastTransition: TRANSITION_COMPLETE,
+ transitions: [
+ createTxTransition({
+ createdAt: new Date(Date.UTC(2017, 4, 1)),
+ by: 'customer',
+ transition: TRANSITION_REQUEST,
+ }),
+ createTxTransition({
+ createdAt: new Date(Date.UTC(2017, 5, 1)),
+ by: 'provider',
+ transition: TRANSITION_ACCEPT,
+ }),
+ createTxTransition({
+ createdAt: new Date(Date.UTC(2017, 6, 1)),
+ by: 'system',
+ transition: TRANSITION_COMPLETE,
+ }),
+ ],
...baseTxAttrs,
});
@@ -250,6 +268,23 @@ describe('TransactionPanel - Order', () => {
const txDelivered = createTransaction({
id: 'order-delivered',
lastTransition: TRANSITION_COMPLETE,
+ transitions: [
+ createTxTransition({
+ createdAt: new Date(Date.UTC(2017, 4, 1)),
+ by: 'customer',
+ transition: TRANSITION_REQUEST,
+ }),
+ createTxTransition({
+ createdAt: new Date(Date.UTC(2017, 5, 1)),
+ by: 'provider',
+ transition: TRANSITION_ACCEPT,
+ }),
+ createTxTransition({
+ createdAt: new Date(Date.UTC(2017, 6, 1)),
+ by: 'system',
+ transition: TRANSITION_COMPLETE,
+ }),
+ ],
...baseTxAttrs,
});
diff --git a/src/components/TransactionPanel/__snapshots__/TransactionPanel.test.js.snap b/src/components/TransactionPanel/__snapshots__/TransactionPanel.test.js.snap
index cac213d5..677dc392 100644
--- a/src/components/TransactionPanel/__snapshots__/TransactionPanel.test.js.snap
+++ b/src/components/TransactionPanel/__snapshots__/TransactionPanel.test.js.snap
@@ -6,483 +6,47 @@ exports[`TransactionPanel - Order accepted matches snapshot 1`] = `
>
-
-
-
+
-
-
-
-
+
-
-
-
-
+
-
-
-
-
+
-
-
-
-
+
-
-
-
-
+
-
-
-
-
+
-
-
-
-
+
-
-
-
-
+
-
-
-
-
+
-
-
-
-
+
-
-
-
-
+
-
-
-
-
+
-
-
-
-
+
-
@@ -91,6 +92,7 @@ export const loadData = (params, search) => (dispatch, getState, sdk) => {
const apiQueryParams = {
only: onlyFilter,
+ lastTransitions: TRANSITIONS,
include: ['provider', 'provider.profileImage', 'customer', 'customer.profileImage', 'booking'],
'fields.image': ['variants.square-small', 'variants.square-small2x'],
page,
diff --git a/src/containers/InboxPage/InboxPage.js b/src/containers/InboxPage/InboxPage.js
index c4c94a32..6d5544ab 100644
--- a/src/containers/InboxPage/InboxPage.js
+++ b/src/containers/InboxPage/InboxPage.js
@@ -6,18 +6,14 @@ import { FormattedMessage, injectIntl, intlShape } from 'react-intl';
import moment from 'moment';
import classNames from 'classnames';
import {
- LINE_ITEM_DAY,
- LINE_ITEM_UNITS,
- txHasFirstReview,
txIsAccepted,
txIsCanceled,
- txIsCompleted,
- txIsDeclinedOrExpired,
+ txIsDeclined,
txIsEnquired,
txIsRequested,
- txIsReviewed,
- propTypes,
-} from '../../util/types';
+ txHasBeenDelivered,
+} from '../../util/transaction';
+import { LINE_ITEM_DAY, LINE_ITEM_UNITS, propTypes } from '../../util/types';
import { formatMoney } from '../../util/currency';
import { ensureCurrentUser, userDisplayName } from '../../util/data';
import { dateFromAPIToLocalNoon, daysBetween } from '../../util/dates';
@@ -57,73 +53,85 @@ const formatDate = (intl, date) => {
};
// Translated name of the state of the given transaction
-const txState = (intl, tx, isOrder) => {
- if (txIsAccepted(tx)) {
+export const txState = (intl, tx, type) => {
+ const isOrder = type === 'order';
+
+ if (txIsEnquired(tx)) {
return {
- nameClassName: css.nameAccepted,
- bookingClassName: css.bookingAccepted,
- lastTransitionedAtClassName: css.lastTransitionedAtAccepted,
- stateClassName: css.stateAccepted,
- state: intl.formatMessage({
- id: 'InboxPage.stateAccepted',
- }),
- };
- } else if (txIsDeclinedOrExpired(tx)) {
- return {
- nameClassName: css.nameDeclined,
- bookingClassName: css.bookingDeclined,
- lastTransitionedAtClassName: css.lastTransitionedAtDeclined,
- stateClassName: css.stateDeclined,
- state: intl.formatMessage({
- id: 'InboxPage.stateDeclined',
- }),
- };
- } else if (txIsCanceled(tx)) {
- return {
- nameClassName: css.nameCanceled,
- bookingClassName: css.bookingCanceled,
- lastTransitionedAtClassName: css.lastTransitionedAtCanceled,
- stateClassName: css.stateCanceled,
- state: intl.formatMessage({
- id: 'InboxPage.stateCanceled',
- }),
- };
- } else if (txIsCompleted(tx) || txHasFirstReview(tx) || txIsReviewed(tx)) {
- return {
- nameClassName: css.nameDelivered,
- bookingClassName: css.bookingDelivered,
- lastTransitionedAtClassName: css.lastTransitionedAtDelivered,
- stateClassName: css.stateDelivered,
- state: intl.formatMessage({
- id: 'InboxPage.stateDelivered',
- }),
- };
- } else if (txIsEnquired(tx)) {
- return {
- nameClassName: isOrder ? css.nameEnquiredOrder : css.nameEnquired,
- bookingClassName: css.bookingEnquired,
- lastTransitionedAtClassName: css.lastTransitionedAtEnquired,
- stateClassName: css.stateEnquired,
+ nameClassName: isOrder ? css.nameNotEmphasized : css.nameEmphasized,
+ bookingClassName: css.bookingActionNeeded,
+ lastTransitionedAtClassName: css.lastTransitionedAtEmphasized,
+ stateClassName: css.stateActionNeeded,
state: intl.formatMessage({
id: 'InboxPage.stateEnquiry',
}),
};
+ } else if (txIsRequested(tx)) {
+ const requested = isOrder
+ ? {
+ nameClassName: css.nameNotEmphasized,
+ bookingClassName: css.bookingNoActionNeeded,
+ lastTransitionedAtClassName: css.lastTransitionedAtEmphasized,
+ stateClassName: css.stateActionNeeded,
+ state: intl.formatMessage({
+ id: 'InboxPage.stateRequested',
+ }),
+ }
+ : {
+ nameClassName: css.nameEmphasized,
+ bookingClassName: css.bookingActionNeeded,
+ lastTransitionedAtClassName: css.lastTransitionedAtEmphasized,
+ stateClassName: css.stateActionNeeded,
+ state: intl.formatMessage({
+ id: 'InboxPage.statePending',
+ }),
+ };
+
+ return requested;
+ } else if (txIsDeclined(tx)) {
+ return {
+ nameClassName: css.nameNotEmphasized,
+ bookingClassName: css.bookingNoActionNeeded,
+ lastTransitionedAtClassName: css.lastTransitionedAtNotEmphasized,
+ stateClassName: css.stateNoActionNeeded,
+ state: intl.formatMessage({
+ id: 'InboxPage.stateDeclined',
+ }),
+ };
+ } else if (txIsAccepted(tx)) {
+ return {
+ nameClassName: css.nameNotEmphasized,
+ bookingClassName: css.bookingNoActionNeeded,
+ lastTransitionedAtClassName: css.lastTransitionedAtNotEmphasized,
+ stateClassName: css.stateSucces,
+ state: intl.formatMessage({
+ id: 'InboxPage.stateAccepted',
+ }),
+ };
+ } else if (txIsCanceled(tx)) {
+ return {
+ nameClassName: css.nameNotEmphasized,
+ bookingClassName: css.bookingNoActionNeeded,
+ lastTransitionedAtClassName: css.lastTransitionedAtNotEmphasized,
+ stateClassName: css.stateNoActionNeeded,
+ state: intl.formatMessage({
+ id: 'InboxPage.stateCanceled',
+ }),
+ };
+ } else if (txHasBeenDelivered(tx)) {
+ return {
+ nameClassName: css.nameNotEmphasized,
+ bookingClassName: css.bookingNoActionNeeded,
+ lastTransitionedAtClassName: css.lastTransitionedAtNotEmphasized,
+ stateClassName: css.stateNoActionNeeded,
+ state: intl.formatMessage({
+ id: 'InboxPage.stateDelivered',
+ }),
+ };
+ } else {
+ console.warn('This transition is unknown:', tx.attributes.lastTransition);
+ return null;
}
- return {
- nameClassName: isOrder ? css.nameRequested : css.namePending,
- bookingClassName: isOrder ? css.bookingRequested : css.bookingPending,
- lastTransitionedAtClassName: isOrder
- ? css.lastTransitionedAtRequested
- : css.lastTransitionedAtPending,
- stateClassName: isOrder ? css.stateRequested : css.statePending,
- state: isOrder
- ? intl.formatMessage({
- id: 'InboxPage.stateRequested',
- })
- : intl.formatMessage({
- id: 'InboxPage.statePending',
- }),
- };
};
const bookingData = (unitType, tx, isOrder, intl) => {
@@ -176,7 +184,7 @@ BookingInfoMaybe.propTypes = {
};
export const InboxItem = props => {
- const { unitType, type, tx, intl } = props;
+ const { unitType, type, tx, intl, stateData } = props;
const { customer, provider } = tx;
const isOrder = type === 'order';
@@ -187,7 +195,6 @@ export const InboxItem = props => {
const isOtherUserBanned = otherUser.attributes.banned;
const otherUserDisplayName = userDisplayName(otherUser, bannedUserDisplayName);
- const stateData = txState(intl, tx, isOrder);
const isSaleNotification = !isOrder && txIsRequested(tx);
const rowNotificationDot = isSaleNotification ? : null;
const lastTransitionedAt = formatDate(intl, tx.attributes.lastTransitionedAt);
@@ -270,11 +277,15 @@ export const InboxPageComponent = props => {
const title = isOrders ? ordersTitle : salesTitle;
const toTxItem = tx => {
- return (
+ const type = isOrders ? 'order' : 'sale';
+ const stateData = txState(intl, tx, type);
+
+ // Render InboxItem only if the latest transition of the transaction is handled in the `txState` function.
+ return stateData ? (
-
+
- );
+ ) : null;
};
const error = fetchOrdersOrSalesError ? (
diff --git a/src/containers/InboxPage/InboxPage.test.js b/src/containers/InboxPage/InboxPage.test.js
index 484beb8a..f3eef444 100644
--- a/src/containers/InboxPage/InboxPage.test.js
+++ b/src/containers/InboxPage/InboxPage.test.js
@@ -8,9 +8,10 @@ import {
createTransaction,
createBooking,
} from '../../util/test-data';
-import { InboxPageComponent, InboxItem } from './InboxPage';
+import { InboxPageComponent, InboxItem, txState } from './InboxPage';
import routeConfiguration from '../../routeConfiguration';
-import { LINE_ITEM_NIGHT, TRANSITION_REQUEST } from '../../util/types';
+import { TRANSITION_REQUEST } from '../../util/transaction';
+import { LINE_ITEM_NIGHT } from '../../util/types';
const noop = () => null;
@@ -73,6 +74,8 @@ describe('InboxPage', () => {
const ordersTree = renderShallow();
expect(ordersTree).toMatchSnapshot();
+ const stateDataOrder = txState(fakeIntl, ordersProps.transactions[0], 'order');
+
// Deeply render one InboxItem
const orderItem = renderDeep(
{
type="order"
tx={ordersProps.transactions[0]}
intl={fakeIntl}
+ stateData={stateDataOrder}
/>
);
expect(orderItem).toMatchSnapshot();
@@ -127,6 +131,8 @@ describe('InboxPage', () => {
const salesTree = renderShallow();
expect(salesTree).toMatchSnapshot();
+ const stateDataSale = txState(fakeIntl, salesProps.transactions[0], 'sale');
+
// Deeply render one InboxItem
const saleItem = renderDeep(
{
type="sale"
tx={salesProps.transactions[0]}
intl={fakeIntl}
+ stateData={stateDataSale}
/>
);
expect(saleItem).toMatchSnapshot();
diff --git a/src/containers/InboxPage/__snapshots__/InboxPage.test.js.snap b/src/containers/InboxPage/__snapshots__/InboxPage.test.js.snap
index 914c89a7..c1b3ae9c 100644
--- a/src/containers/InboxPage/__snapshots__/InboxPage.test.js.snap
+++ b/src/containers/InboxPage/__snapshots__/InboxPage.test.js.snap
@@ -91,6 +91,15 @@ exports[`InboxPage matches snapshot 1`] = `
"now": [Function],
}
}
+ stateData={
+ Object {
+ "bookingClassName": undefined,
+ "lastTransitionedAtClassName": undefined,
+ "nameClassName": undefined,
+ "state": "InboxPage.stateRequested",
+ "stateClassName": undefined,
+ }
+ }
tx={
Object {
"attributes": Object {
@@ -216,6 +225,15 @@ exports[`InboxPage matches snapshot 1`] = `
"now": [Function],
}
}
+ stateData={
+ Object {
+ "bookingClassName": undefined,
+ "lastTransitionedAtClassName": undefined,
+ "nameClassName": undefined,
+ "state": "InboxPage.stateRequested",
+ "stateClassName": undefined,
+ }
+ }
tx={
Object {
"attributes": Object {
@@ -483,6 +501,15 @@ exports[`InboxPage matches snapshot 3`] = `
"now": [Function],
}
}
+ stateData={
+ Object {
+ "bookingClassName": undefined,
+ "lastTransitionedAtClassName": undefined,
+ "nameClassName": undefined,
+ "state": "InboxPage.statePending",
+ "stateClassName": undefined,
+ }
+ }
tx={
Object {
"attributes": Object {
@@ -608,6 +635,15 @@ exports[`InboxPage matches snapshot 3`] = `
"now": [Function],
}
}
+ stateData={
+ Object {
+ "bookingClassName": undefined,
+ "lastTransitionedAtClassName": undefined,
+ "nameClassName": undefined,
+ "state": "InboxPage.statePending",
+ "stateClassName": undefined,
+ }
+ }
tx={
Object {
"attributes": Object {
diff --git a/src/containers/ListingPage/ListingPage.duck.js b/src/containers/ListingPage/ListingPage.duck.js
index c64ee5f4..54c83262 100644
--- a/src/containers/ListingPage/ListingPage.duck.js
+++ b/src/containers/ListingPage/ListingPage.duck.js
@@ -5,7 +5,7 @@ import { types as sdkTypes } from '../../util/sdkLoader';
import { storableError } from '../../util/errors';
import { addMarketplaceEntities } from '../../ducks/marketplaceData.duck';
import { denormalisedResponseEntities } from '../../util/data';
-import { TRANSITION_ENQUIRE } from '../../util/types';
+import { TRANSITION_ENQUIRE } from '../../util/transaction';
import {
LISTING_PAGE_DRAFT_VARIANT,
LISTING_PAGE_PENDING_APPROVAL_VARIANT,
diff --git a/src/containers/TransactionPage/TransactionPage.duck.js b/src/containers/TransactionPage/TransactionPage.duck.js
index f102e14f..903777bf 100644
--- a/src/containers/TransactionPage/TransactionPage.duck.js
+++ b/src/containers/TransactionPage/TransactionPage.duck.js
@@ -7,13 +7,12 @@ import { types as sdkTypes } from '../../util/sdkLoader';
import { isTransactionsTransitionInvalidTransition, storableError } from '../../util/errors';
import {
txIsEnquired,
+ getReview1Transition,
+ getReview2Transition,
+ txIsInFirstReviewBy,
TRANSITION_ACCEPT,
TRANSITION_DECLINE,
- TRANSITION_REVIEW_1_BY_CUSTOMER,
- TRANSITION_REVIEW_1_BY_PROVIDER,
- TRANSITION_REVIEW_2_BY_CUSTOMER,
- TRANSITION_REVIEW_2_BY_PROVIDER,
-} from '../../util/types';
+} from '../../util/transaction';
import * as log from '../../util/log';
import {
updatedEntities,
@@ -443,8 +442,7 @@ const IMAGE_VARIANTS = {
// If other party has already sent a review, we need to make transition to
// TRANSITION_REVIEW_2_BY_
const sendReviewAsSecond = (id, params, role, dispatch, sdk) => {
- const transition =
- role === CUSTOMER ? TRANSITION_REVIEW_2_BY_CUSTOMER : TRANSITION_REVIEW_2_BY_PROVIDER;
+ const transition = getReview2Transition(role === CUSTOMER);
const include = REVIEW_TX_INCLUDES;
@@ -470,8 +468,7 @@ const sendReviewAsSecond = (id, params, role, dispatch, sdk) => {
// So, error is likely to happen and then we must try another state transition
// by calling sendReviewAsSecond().
const sendReviewAsFirst = (id, params, role, dispatch, sdk) => {
- const transition =
- role === CUSTOMER ? TRANSITION_REVIEW_1_BY_CUSTOMER : TRANSITION_REVIEW_1_BY_PROVIDER;
+ const transition = getReview1Transition(role === CUSTOMER);
const include = REVIEW_TX_INCLUDES;
return sdk.transactions
@@ -498,10 +495,7 @@ const sendReviewAsFirst = (id, params, role, dispatch, sdk) => {
export const sendReview = (role, tx, reviewRating, reviewContent) => (dispatch, getState, sdk) => {
const params = { reviewRating, reviewContent };
- const txStateOtherPartyFirst =
- role === CUSTOMER
- ? tx.attributes.lastTransition === TRANSITION_REVIEW_1_BY_PROVIDER
- : tx.attributes.lastTransition === TRANSITION_REVIEW_1_BY_CUSTOMER;
+ const txStateOtherPartyFirst = txIsInFirstReviewBy(tx, role !== CUSTOMER);
dispatch(sendReviewRequest());
diff --git a/src/containers/TransactionPage/TransactionPage.test.js b/src/containers/TransactionPage/TransactionPage.test.js
index ccec9181..c8a7cd8e 100644
--- a/src/containers/TransactionPage/TransactionPage.test.js
+++ b/src/containers/TransactionPage/TransactionPage.test.js
@@ -8,7 +8,7 @@ import {
fakeIntl,
} from '../../util/test-data';
import { renderShallow } from '../../util/test-helpers';
-import { TRANSITION_REQUEST } from '../../util/types';
+import { TRANSITION_REQUEST } from '../../util/transaction';
import { TransactionPageComponent } from './TransactionPage';
const noop = () => null;
diff --git a/src/ducks/user.duck.js b/src/ducks/user.duck.js
index 0f40fbd2..c37ddebc 100644
--- a/src/ducks/user.duck.js
+++ b/src/ducks/user.duck.js
@@ -3,11 +3,8 @@ import isUndefined from 'lodash/isUndefined';
import config from '../config';
import { denormalisedResponseEntities, ensureOwnListing } from '../util/data';
import { storableError } from '../util/errors';
-import {
- LISTING_STATE_DRAFT,
- TRANSITION_REQUEST,
- TRANSITION_REQUEST_AFTER_ENQUIRY,
-} from '../util/types';
+import { transitionsToRequested } from '../util/transaction';
+import { LISTING_STATE_DRAFT } from '../util/types';
import * as log from '../util/log';
import { authInfo } from './Auth.duck';
@@ -319,7 +316,7 @@ export const fetchCurrentUserNotifications = () => (dispatch, getState, sdk) =>
const apiQueryParams = {
only: 'sale',
- last_transitions: [TRANSITION_REQUEST, TRANSITION_REQUEST_AFTER_ENQUIRY],
+ last_transitions: transitionsToRequested,
page: 1,
per_page: NOTIFICATION_PAGE_SIZE,
};
diff --git a/src/forms/BookingDatesForm/BookingDatesForm.test.js b/src/forms/BookingDatesForm/BookingDatesForm.test.js
index 0ff5fbf4..bd875e77 100644
--- a/src/forms/BookingDatesForm/BookingDatesForm.test.js
+++ b/src/forms/BookingDatesForm/BookingDatesForm.test.js
@@ -4,7 +4,7 @@ import Decimal from 'decimal.js';
import { types as sdkTypes } from '../../util/sdkLoader';
import { renderShallow, renderDeep } from '../../util/test-helpers';
import { fakeIntl } from '../../util/test-data';
-import { LINE_ITEM_NIGHT, TRANSITION_REQUEST } from '../../util/types';
+import { LINE_ITEM_NIGHT } from '../../util/types';
import { dateFromAPIToLocalNoon } from '../../util/dates';
import { BookingBreakdown } from '../../components';
import { BookingDatesFormComponent } from './BookingDatesForm';
@@ -70,7 +70,6 @@ describe('EstimatedBreakdownMaybe', () => {
expect(dateFromAPIToLocalNoon(booking.attributes.start)).toEqual(startDate);
expect(dateFromAPIToLocalNoon(booking.attributes.end)).toEqual(endDate);
- expect(transaction.attributes.lastTransition).toEqual(TRANSITION_REQUEST);
expect(transaction.attributes.payinTotal).toEqual(new Money(2198, 'USD'));
expect(transaction.attributes.payoutTotal).toEqual(new Money(2198, 'USD'));
expect(transaction.attributes.lineItems).toEqual([
diff --git a/src/forms/BookingDatesForm/EstimatedBreakdownMaybe.js b/src/forms/BookingDatesForm/EstimatedBreakdownMaybe.js
index c4507e2f..8289d423 100644
--- a/src/forms/BookingDatesForm/EstimatedBreakdownMaybe.js
+++ b/src/forms/BookingDatesForm/EstimatedBreakdownMaybe.js
@@ -30,13 +30,8 @@ import moment from 'moment';
import Decimal from 'decimal.js';
import { types as sdkTypes } from '../../util/sdkLoader';
import { dateFromLocalToAPI, nightsBetween, daysBetween } from '../../util/dates';
-import {
- LINE_ITEM_DAY,
- LINE_ITEM_NIGHT,
- LINE_ITEM_UNITS,
- TRANSITION_REQUEST,
- TX_TRANSITION_ACTOR_CUSTOMER,
-} from '../../util/types';
+import { TRANSITION_REQUEST, TX_TRANSITION_ACTOR_CUSTOMER } from '../../util/transaction';
+import { LINE_ITEM_DAY, LINE_ITEM_NIGHT, LINE_ITEM_UNITS } from '../../util/types';
import { unitDivisor, convertMoneyToNumber, convertUnitToSubUnit } from '../../util/currency';
import { BookingBreakdown } from '../../components';
diff --git a/src/util/test-data.js b/src/util/test-data.js
index b47347ee..e6f20f41 100644
--- a/src/util/test-data.js
+++ b/src/util/test-data.js
@@ -7,9 +7,8 @@ import {
TRANSITION_REQUEST,
TX_TRANSITION_ACTOR_CUSTOMER,
TX_TRANSITION_ACTOR_PROVIDER,
- LISTING_STATE_PUBLISHED,
- TIME_SLOT_DAY,
-} from '../util/types';
+} from '../util/transaction';
+import { LISTING_STATE_PUBLISHED, TIME_SLOT_DAY } from '../util/types';
const { UUID, LatLng, Money } = sdkTypes;
diff --git a/src/util/transaction.js b/src/util/transaction.js
new file mode 100644
index 00000000..bf9207b0
--- /dev/null
+++ b/src/util/transaction.js
@@ -0,0 +1,314 @@
+import { ensureTransaction } from './data';
+
+/**
+ * Transitions
+ *
+ * These strings must sync with values defined in Flex API,
+ * since transaction objects given by API contain info about last transitions.
+ * All the actions in API side happen in transitions,
+ * so we need to understand what those strings mean.
+ */
+
+// When a customer makes a booking to a listing, a transaction is
+// created with the initial request transition.
+export const TRANSITION_REQUEST = 'transition/request';
+
+// A customer can also initiate a transaction with an enquiry, and
+// then transition that with a request.
+export const TRANSITION_ENQUIRE = 'transition/enquire';
+export const TRANSITION_REQUEST_AFTER_ENQUIRY = 'transition/request-after-enquiry';
+
+// When the provider accepts or declines a transaction from the
+// SalePage, it is transitioned with the accept or decline transition.
+export const TRANSITION_ACCEPT = 'transition/accept';
+export const TRANSITION_DECLINE = 'transition/decline';
+
+// The backend automatically expire the transaction.
+export const TRANSITION_EXPIRE = 'transition/expire';
+
+// Admin can also cancel the transition.
+export const TRANSITION_CANCEL = 'transition/cancel';
+
+// The backend will mark the transaction completed.
+export const TRANSITION_COMPLETE = 'transition/complete';
+
+// Reviews are given through transaction transitions. Review 1 can be
+// by provider or customer, and review 2 will be the other party of
+// the transaction.
+export const TRANSITION_REVIEW_1_BY_PROVIDER = 'transition/review-1-by-provider';
+export const TRANSITION_REVIEW_2_BY_PROVIDER = 'transition/review-2-by-provider';
+export const TRANSITION_REVIEW_1_BY_CUSTOMER = 'transition/review-1-by-customer';
+export const TRANSITION_REVIEW_2_BY_CUSTOMER = 'transition/review-2-by-customer';
+export const TRANSITION_EXPIRE_CUSTOMER_REVIEW_PERIOD = 'transition/expire-customer-review-period';
+export const TRANSITION_EXPIRE_PROVIDER_REVIEW_PERIOD = 'transition/expire-provider-review-period';
+export const TRANSITION_EXPIRE_REVIEW_PERIOD = 'transition/expire-review-period';
+
+/**
+ * Actors
+ *
+ * There are 4 different actors that might initiate transitions:
+ */
+
+// Roles of actors that perform transaction transitions
+export const TX_TRANSITION_ACTOR_CUSTOMER = 'customer';
+export const TX_TRANSITION_ACTOR_PROVIDER = 'provider';
+export const TX_TRANSITION_ACTOR_SYSTEM = 'system';
+export const TX_TRANSITION_ACTOR_OPERATOR = 'operator';
+
+export const TX_TRANSITION_ACTORS = [
+ TX_TRANSITION_ACTOR_CUSTOMER,
+ TX_TRANSITION_ACTOR_PROVIDER,
+ TX_TRANSITION_ACTOR_SYSTEM,
+ TX_TRANSITION_ACTOR_OPERATOR,
+];
+
+/**
+ * States
+ *
+ * These constants are only for making it clear how transitions work together.
+ * You should not use these constants outside of this file.
+ *
+ * Note: these states are not in sync with states used transaction process definitions
+ * in Marketplace API. Only last transitions are passed along transaction object.
+ */
+const STATE_INITIAL = 'initial';
+const STATE_ENQUIRY = 'enquiry';
+const STATE_PREAUTHORIZED = 'preauthorized';
+const STATE_DECLINED = 'declined';
+const STATE_ACCEPTED = 'accepted';
+const STATE_CANCELED = 'canceled';
+const STATE_DELIVERED = 'delivered';
+const STATE_REVIEWED = 'reviewed';
+const STATE_REVIEWED_BY_CUSTOMER = 'reviewed-by-customer';
+const STATE_REVIEWED_BY_PROVIDER = 'reviewed-by-provider';
+
+/**
+ * Description of transaction process
+ *
+ * You should keep this in sync with transaction process defined in Marketplace API
+ *
+ * Note: we don't use yet any state machine library,
+ * but this description format is following Xstate (FSM library)
+ * https://xstate.js.org/docs/
+ */
+const stateDescription = {
+ // id is defined only to support Xstate format.
+ // However if you have multiple transaction processes defined,
+ // it is best to keep them in sync with transaction process aliases.
+ id: 'preauth-with-nightly-booking/release-1',
+
+ // This 'initial' state is a starting point for new transaction
+ initial: STATE_INITIAL,
+
+ // States
+ states: {
+ [STATE_INITIAL]: {
+ on: {
+ [TRANSITION_ENQUIRE]: STATE_ENQUIRY,
+ [TRANSITION_REQUEST]: STATE_PREAUTHORIZED,
+ },
+ },
+ [STATE_ENQUIRY]: {
+ on: {
+ [TRANSITION_REQUEST_AFTER_ENQUIRY]: STATE_PREAUTHORIZED,
+ },
+ },
+
+ [STATE_PREAUTHORIZED]: {
+ on: {
+ [TRANSITION_DECLINE]: STATE_DECLINED,
+ [TRANSITION_EXPIRE]: STATE_DECLINED,
+ [TRANSITION_ACCEPT]: STATE_ACCEPTED,
+ },
+ },
+
+ [STATE_DECLINED]: {},
+ [STATE_ACCEPTED]: {
+ on: {
+ [TRANSITION_CANCEL]: STATE_CANCELED,
+ [TRANSITION_COMPLETE]: STATE_DELIVERED,
+ },
+ },
+
+ [STATE_CANCELED]: {},
+ [STATE_DELIVERED]: {
+ on: {
+ [TRANSITION_EXPIRE_REVIEW_PERIOD]: STATE_REVIEWED,
+ [TRANSITION_REVIEW_1_BY_CUSTOMER]: STATE_REVIEWED_BY_CUSTOMER,
+ [TRANSITION_REVIEW_1_BY_PROVIDER]: STATE_REVIEWED_BY_PROVIDER,
+ },
+ },
+
+ [STATE_REVIEWED_BY_CUSTOMER]: {
+ on: {
+ [TRANSITION_REVIEW_2_BY_PROVIDER]: STATE_REVIEWED,
+ [TRANSITION_EXPIRE_PROVIDER_REVIEW_PERIOD]: STATE_REVIEWED,
+ },
+ },
+ [STATE_REVIEWED_BY_PROVIDER]: {
+ on: {
+ [TRANSITION_REVIEW_2_BY_CUSTOMER]: STATE_REVIEWED,
+ [TRANSITION_EXPIRE_CUSTOMER_REVIEW_PERIOD]: STATE_REVIEWED,
+ },
+ },
+ [STATE_REVIEWED]: { type: 'final' },
+ },
+};
+
+// Note: currently we assume that state description doesn't contain nested states.
+const statesFromStateDescription = description => description.states || {};
+
+// Get all the transitions from states object in an array
+const getTransitions = states => {
+ const stateNames = Object.keys(states);
+
+ const transitionsReducer = (transitionArray, name) => {
+ const stateTransitions = states[name] && states[name].on;
+ const transitionKeys = stateTransitions ? Object.keys(stateTransitions) : [];
+ return [
+ ...transitionArray,
+ ...transitionKeys.map(key => ({ key, value: stateTransitions[key] })),
+ ];
+ };
+
+ return stateNames.reduce(transitionsReducer, []);
+};
+
+// This is a list of all the transitions that this app should be able to handle.
+export const TRANSITIONS = getTransitions(statesFromStateDescription(stateDescription)).map(
+ t => t.key
+);
+
+// This function returns a function that has given stateDesc in scope chain.
+const getTransitionsToStateFn = stateDesc => state =>
+ getTransitions(statesFromStateDescription(stateDesc))
+ .filter(t => t.value === state)
+ .map(t => t.key);
+
+// Get all the transitions that lead to specified state.
+const getTransitionsToState = getTransitionsToStateFn(stateDescription);
+
+// This is needed to fetch transactions that need response from provider.
+// I.e. transactions which provider needs to accept or decline
+export const transitionsToRequested = getTransitionsToState(STATE_PREAUTHORIZED);
+
+/**
+ * Helper functions to figure out if transaction is in a specific state.
+ * State is based on lastTransition given by transaction object and state description.
+ */
+
+const txLastTransition = tx => ensureTransaction(tx).attributes.lastTransition;
+
+// DEPRECATED: use txIsDelivered instead
+export const txIsCompleted = tx => txLastTransition(tx) === TRANSITION_COMPLETE;
+
+export const txIsEnquired = tx =>
+ getTransitionsToState(STATE_ENQUIRY).includes(txLastTransition(tx));
+
+// Note: state name used in Marketplace API docs (and here) is actually preauthorized
+// However, word "requested" is used in many places so that we decided to keep it.
+export const txIsRequested = tx =>
+ getTransitionsToState(STATE_PREAUTHORIZED).includes(txLastTransition(tx));
+
+export const txIsAccepted = tx =>
+ getTransitionsToState(STATE_ACCEPTED).includes(txLastTransition(tx));
+
+export const txIsDeclined = tx =>
+ getTransitionsToState(STATE_DECLINED).includes(txLastTransition(tx));
+
+export const txIsCanceled = tx =>
+ getTransitionsToState(STATE_CANCELED).includes(txLastTransition(tx));
+
+export const txIsDelivered = tx =>
+ getTransitionsToState(STATE_DELIVERED).includes(txLastTransition(tx));
+
+const firstReviewTransitions = [
+ ...getTransitionsToState(STATE_REVIEWED_BY_CUSTOMER),
+ ...getTransitionsToState(STATE_REVIEWED_BY_PROVIDER),
+];
+export const txIsInFirstReview = tx => firstReviewTransitions.includes(txLastTransition(tx));
+
+export const txIsInFirstReviewBy = (tx, isCustomer) =>
+ isCustomer
+ ? getTransitionsToState(STATE_REVIEWED_BY_CUSTOMER).includes(txLastTransition(tx))
+ : getTransitionsToState(STATE_REVIEWED_BY_PROVIDER).includes(txLastTransition(tx));
+
+export const txIsReviewed = tx =>
+ getTransitionsToState(STATE_REVIEWED).includes(txLastTransition(tx));
+
+/**
+ * Helper functions to figure out if transaction has passed a given state.
+ * This is based on transitions history given by transaction object.
+ */
+
+const txTransitions = tx => ensureTransaction(tx).attributes.transitions || [];
+const hasPassedTransition = (transitionName, tx) =>
+ !!txTransitions(tx).find(t => t.transition === transitionName);
+
+const hasPassedStateFn = state => tx =>
+ getTransitionsToState(state).filter(t => hasPassedTransition(t, tx)).length > 0;
+
+export const txHasBeenAccepted = hasPassedStateFn(STATE_ACCEPTED);
+export const txHasBeenDelivered = hasPassedStateFn(STATE_DELIVERED);
+
+/**
+ * Other transaction related utility functions
+ */
+
+export const transitionIsReviewed = transition =>
+ getTransitionsToState(STATE_REVIEWED).includes(transition);
+
+export const transitionIsFirstReviewedBy = (transition, isCustomer) =>
+ isCustomer
+ ? getTransitionsToState(STATE_REVIEWED_BY_CUSTOMER).includes(transition)
+ : getTransitionsToState(STATE_REVIEWED_BY_PROVIDER).includes(transition);
+
+export const getReview1Transition = isCustomer =>
+ isCustomer ? TRANSITION_REVIEW_1_BY_CUSTOMER : TRANSITION_REVIEW_1_BY_PROVIDER;
+
+export const getReview2Transition = isCustomer =>
+ isCustomer ? TRANSITION_REVIEW_2_BY_CUSTOMER : TRANSITION_REVIEW_2_BY_PROVIDER;
+
+// Check if a transition is the kind that should be rendered
+// when showing transition history (e.g. ActivityFeed)
+// The first transition and most of the expiration transitions made by system are not relevant
+export const isRelevantPastTransition = transition => {
+ return [
+ TRANSITION_ACCEPT,
+ TRANSITION_CANCEL,
+ TRANSITION_COMPLETE,
+ TRANSITION_DECLINE,
+ TRANSITION_EXPIRE,
+ TRANSITION_REQUEST,
+ TRANSITION_REQUEST_AFTER_ENQUIRY,
+ TRANSITION_REVIEW_1_BY_CUSTOMER,
+ TRANSITION_REVIEW_1_BY_PROVIDER,
+ TRANSITION_REVIEW_2_BY_CUSTOMER,
+ TRANSITION_REVIEW_2_BY_PROVIDER,
+ ].includes(transition);
+};
+
+export const isCustomerReview = transition => {
+ return [TRANSITION_REVIEW_1_BY_CUSTOMER, TRANSITION_REVIEW_2_BY_CUSTOMER].includes(transition);
+};
+
+export const isProviderReview = transition => {
+ return [TRANSITION_REVIEW_1_BY_PROVIDER, TRANSITION_REVIEW_2_BY_PROVIDER].includes(transition);
+};
+
+export const getUserTxRole = (currentUserId, transaction) => {
+ const tx = ensureTransaction(transaction);
+ const customer = tx.customer;
+ if (currentUserId && currentUserId.uuid && tx.id && customer.id) {
+ // user can be either customer or provider
+ return currentUserId.uuid === customer.id.uuid
+ ? TX_TRANSITION_ACTOR_CUSTOMER
+ : TX_TRANSITION_ACTOR_PROVIDER;
+ } else {
+ throw new Error(`Parameters for "userIsCustomer" function were wrong.
+ currentUserId: ${currentUserId}, transaction: ${transaction}`);
+ }
+};
+
+export const txRoleIsProvider = userRole => userRole === TX_TRANSITION_ACTOR_PROVIDER;
+export const txRoleIsCustomer = userRole => userRole === TX_TRANSITION_ACTOR_CUSTOMER;
diff --git a/src/util/transaction.test.js b/src/util/transaction.test.js
new file mode 100644
index 00000000..a48519c0
--- /dev/null
+++ b/src/util/transaction.test.js
@@ -0,0 +1,96 @@
+import { createUser, createTransaction, createListing, createTxTransition } from './test-data';
+
+import {
+ TX_TRANSITION_ACTOR_CUSTOMER,
+ TX_TRANSITION_ACTOR_PROVIDER,
+ TX_TRANSITION_ACTOR_SYSTEM,
+ TRANSITION_REQUEST,
+ TRANSITION_ACCEPT,
+ TRANSITION_COMPLETE,
+ TRANSITION_EXPIRE_REVIEW_PERIOD,
+ txIsAccepted,
+ txIsReviewed,
+ txHasBeenAccepted,
+ txHasBeenDelivered,
+} from './transaction';
+
+const transitionRequest = createTxTransition({
+ createdAt: new Date(Date.UTC(2017, 10, 9, 8, 10)),
+ by: TX_TRANSITION_ACTOR_CUSTOMER,
+ transition: TRANSITION_REQUEST,
+});
+
+const transitionAccept = createTxTransition({
+ createdAt: new Date(Date.UTC(2017, 10, 9, 8, 12)),
+ by: TX_TRANSITION_ACTOR_PROVIDER,
+ transition: TRANSITION_ACCEPT,
+});
+
+const transitionDelivered = createTxTransition({
+ createdAt: new Date(Date.UTC(2017, 10, 16, 8, 12)),
+ by: TX_TRANSITION_ACTOR_SYSTEM,
+ transition: TRANSITION_COMPLETE,
+});
+const transitionReviewed = createTxTransition({
+ createdAt: new Date(Date.UTC(2017, 10, 16, 8, 12)),
+ by: TX_TRANSITION_ACTOR_SYSTEM,
+ transition: TRANSITION_EXPIRE_REVIEW_PERIOD,
+});
+
+const txRequested = createTransaction({
+ lastTransition: TRANSITION_REQUEST,
+ customer: createUser('user1'),
+ provider: createUser('user2'),
+ listing: createListing('Listing'),
+ transitions: [transitionRequest],
+});
+
+const txAccepted = createTransaction({
+ lastTransition: TRANSITION_ACCEPT,
+ customer: createUser('user1'),
+ provider: createUser('user2'),
+ listing: createListing('Listing'),
+ transitions: [transitionRequest, transitionAccept],
+});
+
+const txReviewed = createTransaction({
+ lastTransition: TRANSITION_EXPIRE_REVIEW_PERIOD,
+ customer: createUser('user1'),
+ provider: createUser('user2'),
+ listing: createListing('Listing'),
+ transitions: [transitionRequest, transitionAccept, transitionDelivered, transitionReviewed],
+});
+
+describe('transaction utils', () => {
+ describe('tx is in correct state', () => {
+ it(`txIsReviewed(txReviewed) succeeds with last transaction: ${TRANSITION_EXPIRE_REVIEW_PERIOD}`, () => {
+ expect(txIsReviewed(txReviewed)).toEqual(true);
+ });
+ it(`txIsAccepted(txReviewed) fails with last transaction: ${TRANSITION_EXPIRE_REVIEW_PERIOD}`, () => {
+ expect(txIsAccepted(txReviewed)).toEqual(false);
+ });
+ });
+
+ describe('tx has passed a state X', () => {
+ it('txHasBeenAccepted(txRequested) fails', () => {
+ expect(txHasBeenAccepted(txRequested)).toEqual(false);
+ });
+ it('txHasBeenDelivered(txRequest) fails', () => {
+ expect(txHasBeenDelivered(txRequested)).toEqual(false);
+ });
+
+ it('txHasBeenAccepted(txAccepted) succeeds', () => {
+ expect(txHasBeenAccepted(txAccepted)).toEqual(true);
+ });
+ it('txHasBeenDelivered(txAccepted) fails', () => {
+ expect(txHasBeenDelivered(txAccepted)).toEqual(false);
+ });
+
+ it('txHasBeenAccepted(txReviewed) succeeds', () => {
+ expect(txHasBeenAccepted(txReviewed)).toEqual(true);
+ });
+ it('txHasBeenDelivered(txReviewed) succeeds', () => {
+ expect(txHasBeenDelivered(txReviewed)).toEqual(true);
+ });
+ });
+});
diff --git a/src/util/types.js b/src/util/types.js
index 88932196..5ab9070e 100644
--- a/src/util/types.js
+++ b/src/util/types.js
@@ -32,7 +32,7 @@ import {
} from 'prop-types';
import Decimal from 'decimal.js';
import { types as sdkTypes } from './sdkLoader';
-import { ensureTransaction } from './data';
+import { TRANSITIONS, TX_TRANSITION_ACTORS } from './transaction';
const { UUID, LatLng, LatLngBounds, Money } = sdkTypes;
@@ -231,131 +231,12 @@ propTypes.availabilityException = shape({
}),
});
-// When a customer makes a booking to a listing, a transaction is
-// created with the initial request transition.
-export const TRANSITION_REQUEST = 'transition/request';
-
-// A customer can also initiate a transaction with an enquiry, and
-// then transition that with a request.
-export const TRANSITION_ENQUIRE = 'transition/enquire';
-export const TRANSITION_REQUEST_AFTER_ENQUIRY = 'transition/request-after-enquiry';
-
-// When the provider accepts or declines a transaction from the
-// SalePage, it is transitioned with the accept or decline transition.
-export const TRANSITION_ACCEPT = 'transition/accept';
-export const TRANSITION_DECLINE = 'transition/decline';
-
-// The backend automatically expire the transaction.
-export const TRANSITION_EXPIRE = 'transition/expire';
-
-// Admin can also cancel the transition.
-export const TRANSITION_CANCEL = 'transition/cancel';
-
-// The backend will mark the transaction completed.
-export const TRANSITION_COMPLETE = 'transition/complete';
-
-// Reviews are given through transaction transitions. Review 1 can be
-// by provider or customer, and review 2 will be the other party of
-// the transaction.
-export const TRANSITION_REVIEW_1_BY_PROVIDER = 'transition/review-1-by-provider';
-export const TRANSITION_REVIEW_2_BY_PROVIDER = 'transition/review-2-by-provider';
-export const TRANSITION_REVIEW_1_BY_CUSTOMER = 'transition/review-1-by-customer';
-export const TRANSITION_REVIEW_2_BY_CUSTOMER = 'transition/review-2-by-customer';
-export const TRANSITION_EXPIRE_CUSTOMER_REVIEW_PERIOD = 'transition/expire-customer-review-period';
-export const TRANSITION_EXPIRE_PROVIDER_REVIEW_PERIOD = 'transition/expire-provider-review-period';
-export const TRANSITION_EXPIRE_REVIEW_PERIOD = 'transition/expire-review-period';
-
-export const TRANSITIONS = [
- TRANSITION_ACCEPT,
- TRANSITION_CANCEL,
- TRANSITION_COMPLETE,
- TRANSITION_DECLINE,
- TRANSITION_ENQUIRE,
- TRANSITION_EXPIRE,
- TRANSITION_EXPIRE_CUSTOMER_REVIEW_PERIOD,
- TRANSITION_EXPIRE_PROVIDER_REVIEW_PERIOD,
- TRANSITION_EXPIRE_REVIEW_PERIOD,
- TRANSITION_REQUEST,
- TRANSITION_REQUEST_AFTER_ENQUIRY,
- TRANSITION_REVIEW_1_BY_CUSTOMER,
- TRANSITION_REVIEW_1_BY_PROVIDER,
- TRANSITION_REVIEW_2_BY_CUSTOMER,
- TRANSITION_REVIEW_2_BY_PROVIDER,
-];
-
-// Roles of actors that perform transaction transitions
-export const TX_TRANSITION_ACTOR_CUSTOMER = 'customer';
-export const TX_TRANSITION_ACTOR_PROVIDER = 'provider';
-export const TX_TRANSITION_ACTOR_SYSTEM = 'system';
-export const TX_TRANSITION_ACTOR_OPERATOR = 'operator';
-
-export const TX_TRANSITION_ACTORS = [
- TX_TRANSITION_ACTOR_CUSTOMER,
- TX_TRANSITION_ACTOR_PROVIDER,
- TX_TRANSITION_ACTOR_SYSTEM,
- TX_TRANSITION_ACTOR_OPERATOR,
-];
-
-const txLastTransition = tx => ensureTransaction(tx).attributes.lastTransition;
-
-export const txIsEnquired = tx => txLastTransition(tx) === TRANSITION_ENQUIRE;
-
-export const txIsRequested = tx => {
- const transition = txLastTransition(tx);
- return transition === TRANSITION_REQUEST || transition === TRANSITION_REQUEST_AFTER_ENQUIRY;
-};
-
-export const txIsAccepted = tx => txLastTransition(tx) === TRANSITION_ACCEPT;
-
-export const txIsDeclined = tx => txLastTransition(tx) === TRANSITION_DECLINE;
-
-export const txIsExpired = tx => txLastTransition(tx) === TRANSITION_EXPIRE;
-
-export const txIsDeclinedOrExpired = tx => txIsDeclined(tx) || txIsExpired(tx);
-
-export const txIsCanceled = tx => txLastTransition(tx) === TRANSITION_CANCEL;
-
-export const txIsCompleted = tx => txLastTransition(tx) === TRANSITION_COMPLETE;
-
-export const txHasFirstReview = tx => firstReviewTransitions.includes(txLastTransition(tx));
-
-export const txIsReviewed = tx => areReviewsCompleted(txLastTransition(tx));
-
propTypes.transition = shape({
createdAt: instanceOf(Date).isRequired,
by: oneOf(TX_TRANSITION_ACTORS).isRequired,
transition: oneOf(TRANSITIONS).isRequired,
});
-const firstReviewTransitions = [TRANSITION_REVIEW_1_BY_CUSTOMER, TRANSITION_REVIEW_1_BY_PROVIDER];
-
-// Check if tx transition is followed by a state where
-// reviews are completed
-export const areReviewsCompleted = transition => {
- return [
- TRANSITION_EXPIRE_CUSTOMER_REVIEW_PERIOD,
- TRANSITION_EXPIRE_PROVIDER_REVIEW_PERIOD,
- TRANSITION_EXPIRE_REVIEW_PERIOD,
- TRANSITION_REVIEW_2_BY_CUSTOMER,
- TRANSITION_REVIEW_2_BY_PROVIDER,
- ].includes(transition);
-};
-
-export const txHasBeenAccepted = tx => {
- const transition = txLastTransition(tx);
- return [
- TRANSITION_ACCEPT,
- TRANSITION_REVIEW_1_BY_CUSTOMER,
- TRANSITION_REVIEW_1_BY_PROVIDER,
- TRANSITION_REVIEW_2_BY_CUSTOMER,
- TRANSITION_REVIEW_2_BY_PROVIDER,
- TRANSITION_EXPIRE_CUSTOMER_REVIEW_PERIOD,
- TRANSITION_EXPIRE_PROVIDER_REVIEW_PERIOD,
- TRANSITION_EXPIRE_REVIEW_PERIOD,
- TRANSITION_COMPLETE,
- ].includes(transition);
-};
-
// Possible amount of stars in a review
export const REVIEW_RATINGS = [1, 2, 3, 4, 5];