[Developer Blog](/developer-blog/)

# Showing purchased cart listings

The default behavior of Sharetribe marketplace transactions is to only involve a single listing. Therefore both the checkout page and the order page show only the main listing’s information, even if you are checking out a cart full of items. We want to modify this behavior so that cart listing information is displayed instead of the transaction’s default listing. In this blog series, we are diving into the ins and outs of building a multi-vendor shopping cart with single-vendor checkout using the Sharetribe Developer Platform.

Mar 18, 2024

![Two paper bags of groceries on a wooden floor. Visible on top are apples, bagels, cabbage, bananas, and a baguette.](https://images.prismic.io/sharetribe/d9240361-8fe7-405a-9e39-ecede4ad8a05_maria-lin-kim-8RaUEd8zD-U-unsplash.jpg?auto=compress%2Cformat&fit=max&w=3840)

[![Sari has long light-brown hair and bangs and she's wearing a striped shirt with a slightly puffy collar. She's smiling at the camera.](https://images.prismic.io/sharetribe/b5473113-1cc5-4d08-a39d-2139e4ba1861_sari.png?auto=compress%2Cformat&fit=max&w=3840)](/author/sari-saariaho/)

Sari Saariaho

Developer Advocacy Guild Lead

**Note: The implementation in this article is built on top of [Sharetribe Web Template v4.1.0](https://github.com/sharetribe/web-template/releases/tag/v4.1.0), so if you are working on a later version, you may need to make some adjustments.**

The default behavior of Sharetribe marketplace transactions is to only involve a single listing. Therefore both the checkout page and the order page show only the main listing’s information, even if you are checking out a cart full of items. We want to modify this behavior so that in relevant places, cart listing information is displayed instead of the transaction’s default listing.

To make this happen, we will need to make the following changes:

* Add a new component, _CartDetailsSideCard.js_, to _CheckoutPage_ and use it when checking out a cart instead of a single listing
* Update _InboxPage.js_ to indicate multiple listings within a transaction
* Update _TransactionPage_ to show cart listings

All the code examples used in this guide can be found in [this Gist](https://gist.github.com/SariSaar/158b3a0e4d4229515593e8f6fcdc2ecb).

In this blog series, we are diving into the ins and outs of building a multi-vendor shopping cart with single-vendor checkout using the Sharetribe Developer Platform. This is the final part of the series, so after implementing these steps, you will have a functioning first version of a multi-vendor shopping cart with single-vendor checkout. Still, it is a good idea to keep testing your implementation to find where you would like to continue making further improvements before you release the feature to your live marketplace.

The previous articles in this guide have dived into

* [adding listings to cart,](/developer-blog/adding-items-to-cart/)
* [viewing cart items,](/developer-blog/viewing-cart-items/)
* [calculating shopping cart price](/developer-blog/calculating-shopping-cart-price/),
* [designing a shopping cart transaction flow](/developer-blog/designing-a-shopping-cart-transaction-flow/), and
* [checking out the cart](/developer-blog/checking-out-a-marketplace-shopping-cart/).

If you didn't already, read those first!

## Add CartDetailsSideCard.js to CheckoutPage and use it when checking out a cart instead of a single listing

---

The default display for listing information on _CheckoutPage_ is a component called _DetailsSideCard_. It accepts details on a single listing and displays them alongside the order breakdown for the order being checked out.

For a cart, we need something similar but with the ability to show details on multiple listings. Add a new file to _src/containers/CheckoutPage_ called _CartDetailsSideCard.js._

   src
    ├── containers
            ├── CheckoutPage
                    ├── CartDetailsSideCard.js

Then, copy the contents of [this file](https://gist.github.com/SariSaar/158b3a0e4d4229515593e8f6fcdc2ecb#file-cartdetailssidecard-js) in that new file.

The _CartDetailsSideCard.js_ component is modified from the default _DetailsSideCard.js_ component, and it does the following things:

* It defines helper functions to get image configurations for each listing
* It shows the order breakdown if one has been passed to it – the default _DetailsSideCard_ shows the breakdown after the image, but since we can’t know how many listings the cart has, we will show the breakdown first
* It shows an error message if there is an issue with speculating the transaction – again, the default _DetailsSideCard_ shows this after the image, but we want the user to always see this without scrolling.
* Finally, it maps the listings prop to an array of listing image and detail cards similar to what the _DetailsSideCard_ uses.

Next, use the _CartDetailsSideCard.js_ component on _CheckoutPageWithPayment.js_.

   src
    ├── containers
            ├── CheckoutPage
                    ├── CheckoutPageWithPayment.js

First, let’s import the new component.

import CartDetailsSideCard from './CartDetailsSideCard';

Then, we will add _cartListings_ to the destructuring statement from _pageData_ at the beginning of the _CheckoutPageWithPayment_ component.

export const CheckoutPageWithPayment = props => {
  ...
  const { listing, transaction, orderData, cartListings } = pageData;

We can then conditionally use either the new _CartDetailsSideCard_ component or the default _DetailsSideCard_ one depending on whether _cartListings_ exists.

  const detailsSideCard  = cartListings ? (
    <CartDetailsSideCard
      listings={cartListings}
      author={listing?.author}
      layoutListingImageConfig={config.layout.listingImage}
      speculateTransactionErrorMessage={errorMessages.speculateTransactionErrorMessage}
      isInquiryProcess={false}
      processName={processName}
      breakdown={breakdown}
      intl={intl}
    />
  ) : (
    <DetailsSideCard
      listing={listing}
      listingTitle={listingTitle}
      author={listing?.author}
      firstImage={firstImage}
      layoutListingImageConfig={config.layout.listingImage}
      speculateTransactionErrorMessage={errorMessages.speculateTransactionErrorMessage}
      isInquiryProcess={false}
      processName={processName}
      breakdown={breakdown}
      intl={intl}
    />
  );

Finally, replace the original _DetailsSideCard_ usage with the variable we just defined.

    return (
    <Page title={title} scrollingDisabled={scrollingDisabled}>
      ...
          </div>
        {detailsSideCard}
      </div>
    </Page>

Now, you should be able to see all listings represented on the right side of the checkout page.

## Update InboxPage.js to indicate multiple listings within a transaction

---

After the transaction has been successfully initiated, it will show up on the inbox page. Here, too, the default setup only handles transactions with a single listing. Let’s modify the inbox item display to distinguish cart transactions from regular ones.

To start with, we need to determine whether the transaction has a cart in the first place, so we need to access the transaction’s protected data. By default, however, transactions on the inbox page are fetched using [sparse attributes](https://www.sharetribe.com/api-reference/#sparse-attributes), i.e. only the specified fields are included in the response. We need to add protected data to that list of specified fields.

   src
    ├── containers
            ├── InboxPage
                    ├── InboxPage.duck.js

On _InboxPage.duck.js_ _loadData_ thunk, the _apiQueryParams_ constant defines what gets included in the response. In the _fields.transaction_ array, add _protectedData_.

  const apiQueryParams = {
	...,
    'fields.transaction': [
	...
      'protectedData',
    ],

Now, you can access _transaction.protectedData_ for each transaction on _InboxPage_. Next, we can update the inbox page to use this information.

   src
    ├── containers
            ├── InboxPage
                    ├── InboxPage.js

First, we will import a helper function for getting listing item ids from the transaction’s cart.

import { getCartListingIds } from '../CartPage/CartPage.duck';

The reason why each inbox item only shows details related to one of the listings is that there is a function _getUnitLineItem_, which finds the line item where the code corresponds to a listing unit type. However, for cart transactions, we have multiple such line items, so instead of finding one, we will return an array of one or more unit line items, however many are included in the line item array.

const getUnitLineItem = lineItems => {
  const unitLineItems = lineItems?.filter(
    item => LISTING_UNIT_TYPES.includes(item.code) && !item.reversal
  );
  return unitLineItems;
};

Next, we will modify the _InboxItem_ component in the same file. We will start with determining the number of listings in the transaction’s cart, and set _itemTitle_ to either a generic message or the listing’s title depending on how many listings the transaction has.

  const listingIds = getCartListingIds(tx.attributes.protectedData.cart || {});
  const hasMultipleListings = listingIds.length > 1;
  const listingCount = listingIds.length;
  const itemTitle = hasMultipleListings
    ? intl.formatMessage({ id: 'InboxPage.cartTitle' }, { listingCount })
    : listing?.attributes?.title;

For stock items, the _InboxItem_ component also shows the number of items in the transaction, which is determined from the unit line items. Let’s update the quantity calculation next. Instead of using the _unitLineItem_ quantity directly, we need to calculate the total quantity based on all unit line items.

  const unitLineItems = getUnitLineItem(lineItems);
  const unitLineItemsQuantity = unitLineItems.reduce((sum, item) => {
    return sum + Number(item.quantity);
  }, 0);
  const quantity = hasPricingData && !isBooking ? unitLineItemsQuantity.toString() : null;
  const showStock = stockType === STOCK_MULTIPLE_ITEMS || (quantity && unitLineItemsQuantity > 1);

Then, we can replace the listing title with our new _itemTitle_ constant in the returned _InboxItem_ component.

<div className={css.itemTitle}>{itemTitle}</div>

Finally, you will need to add the new marketplace text key and value in your translations.

   src
    ├── translations
            ├── en.json

Include this in your _en.json_ file and your Marketplace texts editor in Console. That way, you can modify the actual text later, but the template has a fallback value in case the Console value is later removed.

"InboxPage.cartTitle": "{listingCount} listings",

Now, we can see cart transactions indicated differently on the Inbox page.

![Sharetribe marketplace inbox item displaying 2 listings and 2 items](https://images.prismic.io/sharetribe/cda4ee16-ceb6-4ffe-9542-868e8e5c8daf_inbox-item-cart.png?auto=compress%2Cformat&fit=max&w=1920)

If you want to show listing names or other details on _InboxPage_ as well, you would need to make, for example, these additional changes:

* After fetching the transaction information in _InboxPage.duck.js_, determine the transactions that have a cart
* Parse the cart listing ids from each transaction’s cart
* Save an object with the transaction ids as keys, and an array of their associated cart listing ids mapped to UUIDs as values, in _Inbox_ state
* Fetch all cart listings with the sdk, using the _{ ids: \[...\] }_ query parameter
* Create a selector function in _InboxPage.duck.js_ that accepts a transaction id as parameter, gets the transaction’s cart listing ids from state, and returns the listings using the _getListingsById_ helper
* Map the selector function to _InboxPage_ props with _mapDispatchToProps_
* Pass the selector function to _InboxItem_ and use it to fetch the listing details for each transaction separately

## Update TransactionPage to show cart listings

---

Finally, we want to show the transaction’s cart listings on the order page as well. To do this, we will need to make the following changes:

* On _TransactionPage.duck.js_, save the cart listing ids in _TransactionPage_ state
* Fetch all cart listings, instead of only the transaction’s primary listing, in the _fetchTransaction_ thunk
* On _TransactionPage_, use the _getListingsById_ helper in _mapStateToProps_ to fetch cart listings and set them to props
* Pass the cart listings in the _ActivityFeed_ and _TransactionPanel_ components
* In _ActivityFeed_, set _listingTitle_ based on cart listing names
* In _TransactionPanel_, show multiple listing detail cards when the transaction has cart listings.

### Save cart listing ids in state and fetch cart listings instead of the primary listing

   src
    ├── containers
            ├── TransactionPage
                    ├── TransactionPage.duck.js

First, let’s import a helper from _CartPage.duck.js_ for handling cart ids.

import { getCartListingIds } from '../CartPage/CartPage.duck';

We’ll need to prepare the _TransactionPage_ state for a new attribute, _cartListingIds_. Whenever we add a new state attribute, we need to make the following changes:

* Include a new action type (grouped under the _Action types_ comment heading)

export const SET_CART_LISTING_IDS = 'app/TransactionPage/SET_CART_LISTING_IDS';

* Add the new attribute to _initialState_

const initialState = {
  ...
  cartListingIds: [],
};

* Add a new case to the switch statement inside the _transactionPageReducer_ function

export default function transactionPageReducer(state = initialState, action = {}) {
  const { type, payload } = action;
  switch (type) {
  ...
    case SET_CART_LISTING_IDS:
      return { ...state, cartListingIds: payload };
    default:
      return state;
  }
}

* Add a new action creator for the action type we created (grouped under the _Action creators_ comment heading)

export const setCartListingIds = ids => ({
  type: SET_CART_LISTING_IDS,
  payload: ids,
});

After adding all these elements, we can set an array as _cartListingIds_ to _TransactionPage_ state by dispatching _setCartListingIds(array)_. We will use this action in the _fetchTransaction_ thunk.

After the transaction has been fetched in the _fetchTransaction_ thunk, the default behavior of the next block is to fetch the related listing. We want to update this so that if the transaction has a cart, we instead fetch all the cart listings.

To start with, we need to get the cart information from the transaction’s protected data. We will then use the cart information to determine whether to fetch the related listing or the whole cart.

    .then(response => {
	...
      const { cart } = transaction.attributes.protectedData;
	// No cart, a single non-deleted listing
      const canFetchListing = listing && listing.attributes && !listing.attributes.deleted && !cart;
      if (canFetchListing) {
        return sdk.listings.show({
          id: listingId,
          include: ['author', 'author.profileImage', 'images'],
          ...getImageVariants(config.layout.listingImage),
        });
	// a cart of 1-n listings that may or may not be deleted
      } else if (!!cart) {
        const listingIds = getCartListingIds(cart);
        dispatch(setCartListingIds(listingIds.map(id => new UUID(id))));
        return sdk.listings.query({
          ids: listingIds,
          include: ['author', 'author.profileImage', 'images'],
          ...getImageVariants(config.layout.listingImage),
        });
      } else {
        return response;
      }

It’s good to note that in the previous snippet, we saved cart listing ids to state as UUIDs. In the next _.then_ block, the response gets set to _state.marketplaceData_ when _addMarketplaceEntities_ is dispatched. Later, we will use a default helper to get that listing data to our component, and the default helper uses UUIDs instead of string ids, so we can already save them to state in the correct format.

### Get cart listings from state and pass them to components

On TransactionPage, we can now fetch the information from state and pass it to components. 

   src
    ├── containers
            ├── TransactionPage
                    ├── TransactionPage.js

First, add the necessary helper to our import from _marketplaceData.duck_.

import { getMarketplaceEntities, getListingsById } from '../../ducks/marketplaceData.duck';

Then, all the way towards the bottom of the file in mapStateToProps, we will fetch our cart listings using the ids from state and the helper we just imported.

const mapStateToProps = state => {
  const {
    ...
    cartListingIds,
  } = state.TransactionPage;
...
  const cartListings = getListingsById(state, cartListingIds);
  return {
    ...
    cartListings,
  };
};

Now we can destructure _cartListings_ from props in the component, and then pass it to the necessary subcomponents, _TransactionPanel_ and _ActivityFeed_.

export const TransactionPageComponent = props => {
  ...
  const {
    ...
    cartListings,
  } = props;
...
  const panel = isDataAvailable ? (
    <TransactionPanel
      ...
      cartListings={cartListings}
      activityFeed={
        <ActivityFeed
          ...
          cartListings={cartListings}
        />
      }
	...
    />
  ) : (
    loadingOrFailedFetching
  );

### Set listingTitle based on cart listing names in ActivityFeed

The _ActivityFeed_ component renders the different events and messages within the transaction as a coherent timeline. Here, we want to show the names of all the listings whenever the component needs a listing title.

   src
    ├── containers
            ├── TransactionPage
                    ├── ActivityFeed
                            ├── ActivityFeed.js

First, let’s add the _cartListings_ prop to the destructuring statement, and set a boolean constant for whether or not we are dealing with a cart transaction.

export const ActivityFeedComponent = props => {
  const {
    ...
    cartListings,
  } = props;
  const isCart = cartListings?.length > 0;

By default, the listing title in _const transitionListItem_ is set depending on whether the listing has been deleted. We will update this logic to a function that can take an individual listing as a parameter. This way, we can construct the actual _listingTitle_ using the same logic with either the cart listings or the main listing.

  const transitionListItem = transition => {
    ...
    if (currentUser?.id && customer?.id && provider?.id && listing?.id) {
      ...
      const getListingTitle = listing =>
        listing.attributes.deleted
          ? intl.formatMessage({ id: 'TransactionPage.ActivityFeed.deletedListing' })
          : listing.attributes.title;
      const listingTitle = isCart
        ? cartListings.map(l => getListingTitle(l)).join(', ')
        : getListingTitle(listing);

Now, you can see the cart listing names in the activity feed titles.

![Screenshot of a text that reads: "Activity. You placed an order for Great bike, Super bike. Feb 23, 2:05 PM."](https://images.prismic.io/sharetribe/301dd9ab-5abf-4251-af5d-4ab39691f669_activity-feed-cart-listings.png?auto=compress%2Cformat&fit=max&w=1920)

### Show cart listing detail cards in TransactionPanel

Finally, let’s modify the TransactionPanel to show listing details for all cart listings.

   src
    ├── containers
            ├── TransactionPage
                    ├── TransactionPanel
                            ├── TransactionPanel.js

Unlike _CheckoutPage_, the _TransactionPanel_ does not have a specified component for showing listing details. However, the logic we will use here is very similar to what we did on CheckoutPage: if the transaction has a cart, we will show the order breakdown and possible action buttons first, and then map the array of cart listings to the same listing detail display that a single listing uses by default.

First, let’s again destructure _cartListings_ from props at the beginning of the _render()_ function, and set a boolean to determine whether we are dealing with a cart transaction.

render() {
    const {
      ...
      cartListings,
    } = this.props;
    const isCart = cartListings?.length > 0;

Then, instead of defining a constant for _firstImage_, let’s convert the logic to a function that we can use to get the first image of either the main listing or each cart listing.

    const getFirstImage = listing => (listing?.images?.length > 0 ? listing?.images[0] : null);
    const firstImage = getFirstImage(listing);

Now, we can add a _listingDetails_ constant that either shows the cart version of listing details, or the default one, depending on whether we are working with a cart transaction or not.

    const listingDetails = isCart ? (
      {/* Show breakdown and action buttons followed by all cart listings */}
      <div className={css.detailCard}>
        {stateData.showOrderPanel ? orderPanel : null}
        <BreakdownMaybe
          className={css.breakdownContainer}
          orderBreakdown={orderBreakdown}
          processName={stateData.processName}
        />
        {stateData.showActionButtons ? (
          <div className={css.desktopActionButtons}>{actionButtons}</div>
        ) : null}
        {cartListings.map(l => (
          <div key={l.id?.uuid}>
            <DetailCardImage
              avatarWrapperClassName={css.avatarWrapperDesktop}
              listingTitle={l.attributes.title}
              image={getFirstImage(l)}
              provider={provider}
              isCustomer={isCustomer}
              listingImageConfig={config.layout.listingImage}
            />
            <DetailCardHeadingsMaybe
              showDetailCardHeadings={stateData.showDetailCardHeadings}
              listingTitle={
                l.attributes.deleted ? (
                  l.attributes.title
                ) : (
                  <NamedLink
                    name="ListingPage"
                    params={{ id: l.id?.uuid, slug: createSlug(l.attributes.title) }}
                  >
                    {l.attributes.title}
                  </NamedLink>
                )
              }
              showPrice
              price={l?.attributes?.price}
              intl={intl}
            />
          </div>
        ))}
      </div>
    ) : (
      // Show listing image followed by breakdown and action buttons
      <div className={css.detailCard}>
        <DetailCardImage
          avatarWrapperClassName={css.avatarWrapperDesktop}
          listingTitle={listingTitle}
          image={firstImage}
          provider={provider}
          isCustomer={isCustomer}
          listingImageConfig={config.layout.listingImage}
        />
        <DetailCardHeadingsMaybe
          showDetailCardHeadings={stateData.showDetailCardHeadings}
          listingTitle={
            listingDeleted ? (
              listingTitle
            ) : (
              <NamedLink
                name="ListingPage"
                params={{ id: listing.id?.uuid, slug: createSlug(listingTitle) }}
              >
                {listingTitle}
              </NamedLink>
            )
          }
          showPrice={showPrice}
          price={listing?.attributes?.price}
          intl={intl}
        />
        {stateData.showOrderPanel ? orderPanel : null}
        <BreakdownMaybe
          className={css.breakdownContainer}
          orderBreakdown={orderBreakdown}
          processName={stateData.processName}
        />
        {stateData.showActionButtons ? (
          <div className={css.desktopActionButtons}>{actionButtons}</div>
        ) : null}
      </div>
    );

Finally, we can replace the original listing detail element, wrapped in a div with class _css.detailCard_, with the _listingDetails_ variable.

            <div className={css.stickySection}>
              {listingDetails}
              <DiminishedActionButtonMaybe
                showDispute={stateData.showDispute}
                onOpenDisputeModal={onOpenDisputeModal}
              />
            </div>

Now, you can see the breakdown and action buttons, followed by all listing images, on the transaction page.

![Screenshot of a marketplace shopping cart page showing an activity feed, an order breakdown, and a button "I've received my order"](https://images.prismic.io/sharetribe/0e0f5483-e92b-4d56-978d-cb43220649a5_transaction-panel-cart-breakdown.png?auto=compress%2Cformat&fit=max&w=3840)

In this version, we still keep the dispute button i.e. the _DiminishedActionButtonMaybe_ component below all listing images, but you could absolutely include it in the _listingDetails_ constant as well. That way, you could show the dispute button above the cart listing images similarly to the order breakdown and other action buttons.

## Summary

---

In this guide, we made the following changes:

* We added a new component, _CartDetailsSideCard.js_, to CheckoutPage and used it when checking out a cart instead of a single listing
* We updated _InboxPage.js_ to indicate multiple listings within a transaction
* We updated TransactionPage to show cart listings

If you followed the whole series and built your own first draft implementation of a single-vendor shopping cart in your marketplace, we would love to hear from you! You can reach out to our Developer Advocate team through the chat widget in your Sharetribe Console and let us know if you learned something new or discovered something unexpected while following this series. We're happy to help you keep building your marketplace!

## You might also like...

[![Store checkout machine with a person holding the screen with one hand and a receipt with the other.](https://images.prismic.io/sharetribe/a17bd3bf-e450-4f61-9472-8decf36dad78_simon-kadula-OluDgzgCHp4-unsplash.jpg?auto=compress%2Cformat&fit=max&w=3840)Checking out a marketplace shopping cart](/developer-blog/checking-out-a-marketplace-shopping-cart/)[![A drawing of a flowchart on white paper on top of a wooden work area, with a manicured hand holding a marker and another hand holding down the paper.](https://images.prismic.io/sharetribe/013df4f4-c444-4a1b-ad83-289811834245_kelly-sikkema-lFtttcsx5Vk-unsplash.jpg?auto=compress%2Cformat&fit=max&w=3840)Designing a shopping cart transaction flow](/developer-blog/designing-a-shopping-cart-transaction-flow/)

## Liked this? Get email updates on new Sharetribe Developer Blog posts.

[Subscribe](#subscribe-dev-blog)