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.
Because 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 may read another shopper's orders.
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:
  • type OrderSummary defines 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 Query adds a new root-level query, orderSummary, to your store's existing GraphQL schema, without modifying any query that already exists.
  • Notice that the query takes no arguments. The shopper's identity comes from the request, not from the caller, so there is no email or user ID to pass. An argument like that 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.
  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

_78
import { UnauthorizedError } from "@faststore/api";
_78
_78
type ResolverContext = {
_78
account: string;
_78
headers: Record<string, string | undefined>;
_78
clients: {
_78
commerce: {
_78
vtexid: { validate: () => Promise<{ authStatus?: string }> };
_78
};
_78
};
_78
};
_78
_78
type UserOrdersResponse = {
_78
list?: Array<{ creationDate?: string }>;
_78
paging?: { total?: number };
_78
};
_78
_78
// Throws unless the incoming request carries a valid shopper token.
_78
const requireAuthenticatedShopper = async (ctx: ResolverContext) => {
_78
try {
_78
const validation = await ctx.clients.commerce.vtexid.validate();
_78
_78
if (validation?.authStatus?.toLowerCase() !== "success") {
_78
throw new UnauthorizedError("Authentication required");
_78
}
_78
} catch (error) {
_78
if (error instanceof UnauthorizedError) {
_78
throw error;
_78
}
_78
_78
// For a missing or expired token, VTEX ID answers 401 and the client
_78
// throws instead of resolving. Without this catch, the caller would get
_78
// a 500 rather than a 401.
_78
throw new UnauthorizedError("Authentication required");
_78
}
_78
};
_78
_78
const orderSummaryResolver = {
_78
Query: {
_78
orderSummary: async (_: never, __: never, ctx: ResolverContext) => {
_78
await requireAuthenticatedShopper(ctx);
_78
_78
// Only the most recent order is needed: `paging.total` carries the count.
_78
const searchParams = new URLSearchParams({
_78
per_page: "1",
_78
orderBy: "creationDate,desc",
_78
});
_78
_78
const response = await fetch(
_78
`https://${ctx.account}.vtexcommercestable.com.br/api/oms/user/orders?${searchParams}`,
_78
{
_78
headers: {
_78
"content-type": "application/json",
_78
// Forwarding the shopper's cookies is what scopes the response.
_78
cookie: ctx.headers?.cookie ?? "",
_78
},
_78
}
_78
);
_78
_78
if (!response.ok) {
_78
console.error("Order summary request failed", {
_78
status: response.status,
_78
});
_78
_78
throw new Error("Could not load the order summary");
_78
}
_78
_78
const orders: UserOrdersResponse = await response.json();
_78
_78
return {
_78
totalOrders: orders.paging?.total ?? 0,
_78
lastOrderDate: orders.list?.[0]?.creationDate ?? null,
_78
};
_78
},
_78
},
_78
};
_78
_78
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.
  • requireAuthenticatedShopper(ctx) runs first and throws immediately for a signed-out visitor, so the rest of the function only ever runs for an authenticated shopper. UnauthorizedError comes from @faststore/api and results in a 401 response to the caller.
  • The try/catch around the validation matters. For a missing or expired token, VTEX ID answers 401 and the client throws rather than resolving with an unsuccessful status, so without the catch the caller would receive a 500 instead of a 401.
  • 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.
Contributors
2
Photo of the contributor Mariana Caetano
Photo of the contributor LarĂ­cia Mota
+ 2 contributors
Was this helpful?
Yes
No
Suggest Edits (GitHub)
Contributors
2
Photo of the contributor Mariana Caetano
Photo of the contributor LarĂ­cia Mota
+ 2 contributors
On this page
Was this helpful?
Suggest Edits (GitHub)
On this page