Menu
Guides
Storefront Development

Storefront Development

Displaying an order summary for the signed-in shopper

Extend the FastStore API with a query that summarizes the signed-in shopper's orders, authenticated with the shopper's own session.

9 min read
This guide describes how to extend your storefront with a section that shows signed-in shoppers how many orders they have placed and when the most recent one was. You can then place that section on any page managed in the Headless CMS, such as a landing page.
Since this data belongs to one specific shopper, the resolver must authenticate as that shopper rather than as the store, so each shopper only ever sees their own orders and never another shopper's.
For instructions on declaring type definitions and resolvers, see Extending API schemas. For the reasoning behind the credentials used here, see Authentication in API extensions.

Context

  • You want to show a compact summary of the shopper's purchase history, without loading the full order list.
  • The data comes from the Orders API, through the /api/oms/user/orders endpoint.
  • That endpoint returns only the orders belonging to whoever is signed in, so it requires no entity, schema, or app configuration of your own.
  • The summary is personal data, so no shopper can access another shopper's orders.
This guide covers the standard B2C flow, where the signed-in shopper is the owner of the orders. B2B and telesales sessions carry additional identity, such as an organization unit, a contract, or an operator acting on a shopper's behalf, and the endpoint's scoping in those cases is outside the scope of this guide. Check the Orders API reference before reusing this pattern in a B2B or telesales storefront.
The complete implementation is available in the playground.store repository, in src/graphql/vtex and src/components/OrderSummary.

Before you begin

Make sure you have:
  • A FastStore project running locally. See Getting started.
  • A shopper account you can sign in with on your store, ideally with at least one placed order, so the summary shows a value other than zero.
No configuration in the VTEX Admin is required: the endpoint scopes the response by the shopper's own session.

Step 1: Creating the type definitions

Before the API can return the summary, your store's GraphQL schema needs to know that this data exists and what shape it takes. This step declares a new orderSummary query and the OrderSummary type it returns. The resolver you will create in Step 2 is what actually fetches the data to fulfill this query.
  1. Open your store's repository in a code editor of your preference.
  2. In the src/graphql/vtex/typeDefs directory, create a file named orderSummary.graphql.
  3. In the orderSummary.graphql file, add the following content:
src/graphql/vtex/typeDefs/orderSummary.graphql

_20
type OrderSummary {
_20
"""
_20
How many orders the signed-in shopper has placed.
_20
"""
_20
totalOrders: Int!
_20
_20
"""
_20
Creation date of the most recent order, in ISO 8601 format.
_20
Null when the shopper has not placed any order yet.
_20
"""
_20
lastOrderDate: String
_20
}
_20
_20
extend type Query {
_20
"""
_20
Retrieve a summary of the signed-in shopper's orders.
_20
The query takes no arguments: the shopper is identified by the request itself.
_20
"""
_20
orderSummary: OrderSummary @cacheControl(scope: "private", sMaxAge: 0)
_20
}

In this file, the following is declared:
DeclarationDescription
type OrderSummaryDefines a new GraphQL object type with the two fields the section needs: totalOrders (required, hence Int!) and lastOrderDate (optional, since a shopper with no orders has no date to report). The triple-quoted comments above each field become that field's description in your API's schema.
extend type QueryAdds a new root-level query, orderSummary, to your store's existing GraphQL schema without modifying any existing query.
No query argumentsThe shopper's identity comes from the request, not from the caller, so there is no email or user ID to pass. Such an argument would let any caller ask for someone else's summary.
@cacheControl(scope: "private", sMaxAge: 0)Keeps the response out of shared caches, so one shopper's summary is never served to another visitor.

Step 2: Creating the resolver

The type definitions tell your API that an orderSummary query exists, but GraphQL still needs a function that produces the data when that query runs. This step writes that function: a resolver that checks the shopper is signed in, then forwards the shopper's own cookies to the Orders API so the response is scoped to that one shopper.
This step uses validateUserAuthentication, exported by @faststore/api from version 4.8.0. Update @faststore/core before continuing if your store is on an earlier version. See Updating your @faststore/core package version.
  1. In the src/graphql/vtex/resolvers directory, create a file named orderSummary.ts. If the resolvers directory doesn't exist yet, create it first.
  2. In the orderSummary.ts file, add the following content:
src/graphql/vtex/resolvers/orderSummary.ts

_48
import { validateUserAuthentication } from "@faststore/api";
_48
_48
type UserOrdersResponse = {
_48
list?: Array<{ creationDate?: string }>;
_48
paging?: { total?: number };
_48
};
_48
_48
const orderSummaryResolver = {
_48
Query: {
_48
orderSummary: async (_: never, __: never, ctx: any) => {
_48
await validateUserAuthentication(ctx);
_48
_48
// Only the most recent order is needed: `paging.total` carries the count.
_48
const searchParams = new URLSearchParams({
_48
per_page: "1",
_48
orderBy: "creationDate,desc",
_48
});
_48
_48
const response = await fetch(
_48
`https://${ctx.account}.vtexcommercestable.com.br/api/oms/user/orders?${searchParams}`,
_48
{
_48
headers: {
_48
"content-type": "application/json",
_48
// Forwarding the shopper's cookies is what scopes the response.
_48
cookie: ctx.headers?.cookie ?? "",
_48
},
_48
}
_48
);
_48
_48
if (!response.ok) {
_48
console.error("Order summary request failed", {
_48
status: response.status,
_48
});
_48
_48
throw new Error("Could not load the order summary");
_48
}
_48
_48
const orders: UserOrdersResponse = await response.json();
_48
_48
return {
_48
totalOrders: orders.paging?.total ?? 0,
_48
lastOrderDate: orders.list?.[0]?.creationDate ?? null,
_48
};
_48
},
_48
},
_48
};
_48
_48
export default orderSummaryResolver;

In this file, the following is declared:
  • orderSummaryResolver.Query.orderSummary is the function GraphQL calls whenever an orderSummary query runs. Its third parameter, ctx, is the request context, which carries the incoming headers, the account name, and the platform clients.
  • validateUserAuthentication(ctx) runs first and throws immediately for a signed-out visitor, so the rest of the function only ever runs for an authenticated shopper. It is exported by @faststore/api from version 4.8.0, and the caller receives a 401 when the token is missing, expired, or invalid.
  • cookie: ctx.headers?.cookie forwards the shopper's own cookies to the Orders API. This is what makes the endpoint answer as that shopper. Sending an appKey and appToken pair instead would authenticate the request as the store, and the endpoint would no longer be scoped to one person.
  • per_page: "1" keeps the response small. The count comes from paging.total, so there is no reason to download the whole list.
  • If the response isn't successful, the resolver logs the status and throws. The upstream body is never returned, since an error body may carry headers or internal URLs.

Step 3: Registering the resolver

  1. Open the src/graphql/vtex/resolvers/index.ts file. If your store doesn't have this file yet, create it in the src/graphql/vtex/resolvers directory.
  2. Import the resolver you created in Step 2 and merge its Query root into the exported object:
    src/graphql/vtex/resolvers/index.ts

    _10
    import { default as OrderSummaryResolver } from "./orderSummary";
    _10
    _10
    const resolvers = {
    _10
    Query: {
    _10
    ...OrderSummaryResolver.Query,
    _10
    },
    _10
    };
    _10
    _10
    export default resolvers;

  3. Save the file.
Merge the Query (and Mutation, if applicable) keys from each resolver file, as above. Spreading two resolver objects that both define Query directly into resolvers overwrites the root instead of merging it, and only the last one takes effect.

Step 4: Displaying the summary in a section

  1. In the src/components directory, create a directory named OrderSummary with a file named OrderSummary.tsx.
  2. In the OrderSummary.tsx file, add the following content:
    src/components/OrderSummary/OrderSummary.tsx

    _64
    import { gql } from "@faststore/core/api";
    _64
    _64
    import {
    _64
    useQuery_unstable as useQuery,
    _64
    useSession_unstable as useSession,
    _64
    } from "@faststore/core/experimental";
    _64
    _64
    export const query = gql(`
    _64
    query OrderSummaryQuery {
    _64
    orderSummary {
    _64
    totalOrders
    _64
    lastOrderDate
    _64
    }
    _64
    }
    _64
    `);
    _64
    _64
    type OrderSummaryData = {
    _64
    orderSummary: {
    _64
    totalOrders: number;
    _64
    lastOrderDate: string | null;
    _64
    } | null;
    _64
    };
    _64
    _64
    export const OrderSummary = () => {
    _64
    // `person` is only populated for a signed-in shopper.
    _64
    const { person } = useSession();
    _64
    const isSignedIn = Boolean(person?.id);
    _64
    _64
    // The resolver answers with a 401 for a signed-out visitor, so the query
    _64
    // only runs once there is a session to authenticate.
    _64
    const { data, error } = useQuery<OrderSummaryData>(
    _64
    query,
    _64
    {},
    _64
    { doNotRun: !isSignedIn }
    _64
    );
    _64
    _64
    if (!isSignedIn) {
    _64
    return <p>Sign in to see your order summary.</p>;
    _64
    }
    _64
    _64
    if (error) {
    _64
    return <p>We could not load your order summary. Please try again.</p>;
    _64
    }
    _64
    _64
    if (!data?.orderSummary) {
    _64
    return null;
    _64
    }
    _64
    _64
    const { totalOrders, lastOrderDate } = data.orderSummary;
    _64
    _64
    return (
    _64
    <section>
    _64
    <h2>Your orders</h2>
    _64
    _64
    <p>{totalOrders === 1 ? "1 order" : `${totalOrders} orders`}</p>
    _64
    _64
    {lastOrderDate && (
    _64
    <p>Last order on {new Date(lastOrderDate).toLocaleDateString()}</p>
    _64
    )}
    _64
    </section>
    _64
    );
    _64
    };
    _64
    _64
    export default OrderSummary;

  3. Export the component from src/components/index.tsx, so the storefront can render it:
    src/components/index.tsx

    _10
    import OrderSummary from "./OrderSummary/OrderSummary";
    _10
    _10
    const sections = {
    _10
    OrderSummary,
    _10
    };
    _10
    _10
    export default sections;

  4. Declare the section in cms/faststore/sections.json, so it becomes available in the CMS:
    cms/faststore/sections.json

    _10
    {
    _10
    "name": "OrderSummary",
    _10
    "schema": {
    _10
    "title": "OrderSummary",
    _10
    "description": "Shows the signed-in shopper a summary of their own orders",
    _10
    "type": "object",
    _10
    "properties": {}
    _10
    }
    _10
    }

  5. Run yarn cms-sync to send the section to the CMS, then add OrderSummary to a page in the VTEX Admin. See Creating a new section.
The useQuery_unstable and useSession_unstable hooks are exported from @faststore/core/experimental and their APIs may change between releases. See Experimental exports - Hooks and Components for more information.
The doNotRun option matters here: without it, the query runs for signed-out visitors too, and every one of them receives a 401 the component has to discard.
The component reads person from useSession_unstable to tell whether someone is signed in. useAuth_unstable answers the same question, but the property it returns was renamed between core versions, while person is the same in both.

Expected behavior

The section reaches a "nothing to show" state through three different paths, and they mean different things:
SituationWhat happens
Visitor is not signed inThe query never runs, and the section invites the visitor to sign in.
Shopper is signed in with no orderspaging.total is 0, and the section shows 0 orders with no date. This is the expected, non-error case.
The request failsThe resolver throws, and the section shows the error message. An expired session is the most common cause.

Result

On the page where you placed the section, a signed-in shopper sees how many orders they have placed and the date of the most recent one:
{"base64":"  ","img":{"width":1606,"height":783,"type":"png","mime":"image/png","wUnits":"px","hUnits":"px","length":130984,"url":"https://vtexhelp.vtexassets.com/assets/docs/src/order-summary-for-signed-in-shopper___3304d34c5d379692453ab84c2177bd02.png"}}
A signed-out visitor sees the sign-in message instead, and no request can return another shopper's summary, because the Orders API scopes the response to the session that made the request.