Customer prices
A merchant can give some customers their own prices: trade accounts, wholesale buyers, members. The shopper signs in with their Throttle customer account, the storefront sends their token with each request, and the catalog answers their prices.
Customer groups
The merchant creates customer groups in Storefront › Groups and adds Throttle customers to them. A group either has its own fixed prices per variant and currency, or takes a percent off the regular price. Each customer is in at most one group. Shoppers who are signed out, or in no group, get the regular prices.
-
When a group price is below the regular price, the compare-at price carries the regular price (
compareAtMinMinorin a list's price range,compareAtPriceMinoron a variant), so you can show it struck through. - Price sorts, price filters and price facet ranges use the regular price; only the prices shown change.
- Answers never name the group.
- Carts are priced the same way: the price a signed-in shopper sees on a product page is the price their cart charges.
Send the customer token
Shoppers sign in with Throttle's customer accounts, through @usethrottle/auth (see Storefront customer auth). Give the
catalog client a customerToken function that returns the shopper's
current access token. With @usethrottle/auth 0.10.1 or later, that is auth.getAccessToken(): it refreshes the token first when it is about to
expire, and answers null when the shopper is signed out.
import { createThrottleAuth } from '@usethrottle/auth';
import { createCatalogClient } from '@usethrottle/catalog';
// Throttle customer login (@usethrottle/auth 0.10.1 or later).
export const auth = createThrottleAuth({
publishableKey: 'pk_live_your_throttle_key',
});
// The client asks for the token on every request that can use it: the
// current access token, refreshed first when it is about to expire, or null
// when the shopper is signed out.
export const catalog = createCatalogClient({
publishableKey: 'pc_pk_your_browser_key',
customerToken: () => auth.getAccessToken(),
});
-
The client calls the function on every request that can use the token, so a refreshed token is used at
once. It sends the token only in the
X-Customer-Tokenheader, never in a URL, never stores it on the client and never logs it. -
getAccessToken()can throw when a refresh fails for a moment (a network error, a 5xx). The client treats a throw, or anything that is not a string, as signed out for that request: it answers regular prices instead of failing the page. -
The token read and the request each get the full
timeoutMs(10 seconds by default), so a call that waits on a slow refresh can take up to about twice that. A token function that never answers ends inTimeoutError. -
Sign shoppers in in the browser. A Throttle
pk_is a browser key: Throttle checks the page's origin against your allowed origins, so a sign-in from server code is refused withorigin_not_allowed. On the server, take the shopper's access token from the request (a header or cookie your app sets) and return it fromcustomerToken. -
The token goes with product lists, product pages, related products, suggestions,
carts.check,carts.create, review eligibility and review submission. Categories, collections, tags, filter values, review lists andconfiganswer the same for everyone and never send it.
A verified email is required
Group prices need a verified email address. A signed-in shopper whose email is not verified gets the regular prices, on product pages and in the cart, until they verify it. Knowing a trade customer's email address is not enough to buy at trade prices.
priceContext: which prices an answer carries
An answer to a request that carried a token has priceContext. Use it to tell
the shopper why they see the prices they see:
| priceContext | Prices | Meaning and what to show |
|---|---|---|
customer | The shopper’s own | Signed in with a verified email. Their group’s prices, or the regular ones when they are in no group. |
email_unverified | Regular | Signed in, email not verified. Show a note such as “Verify your email address to see your prices”, with a way to resend the verification email. |
anonymous | Regular | The store cannot use customer tokens for prices, most often because its Product Catalog installation needs the update that adds customer prices. Nothing to show the shopper. |
anonymous_fallback | Regular | The catalog could not check the token just now. It passes: the next requests usually carry the shopper’s prices again. |
| absent | Regular | No token went with the request: the shopper is signed out, the token function threw or returned null, or the SDK retried without a token the catalog refused (below). |
import { createThrottleAuth } from '@usethrottle/auth';
import { createCatalogClient } from '@usethrottle/catalog';
const auth = createThrottleAuth({ publishableKey: 'pk_live_your_throttle_key' });
const catalog = createCatalogClient({
publishableKey: 'pc_pk_your_browser_key',
customerToken: () => auth.getAccessToken(),
});
const list = await catalog.products.list({ category: 'brakes' });
switch (list.priceContext) {
case 'customer':
// The shopper's own prices (their group's, when they are in one).
break;
case 'email_unverified':
// Signed in, email not verified yet: list prices.
console.log('Verify your email address to see your prices.');
break;
case 'anonymous':
case 'anonymous_fallback':
// The catalog could not use the token for prices: list prices.
break;
case undefined:
// No token went with the request (signed out, or the SDK's retry
// without a refused token): list prices.
break;
}
A refused token
A token the catalog refuses (expired, revoked, or for another store) answers 401 CUSTOMER_TOKEN_INVALID. The client then asks your function for the token
once more, and retries with it if it changed. If it did not, a read (or carts.check) is retried once without the token and
answers the regular prices, with no priceContext: the page still renders. A
call that cannot fall back to regular prices (reviews.eligibility, reviews.submit, and carts.create with a token)
throws CustomerTokenInvalidError instead. A call makes at most three requests.
In React
Keep one catalog client for the life of the app and pass customerToken as a
function, as above; do not make a new client when the shopper signs in. Give CatalogProvider a customerKey: the signed-in
customer's id, or null. The hooks' cache is partitioned by it, so a
change empties the cache and every mounted hook fetches again, and one shopper never sees answers fetched for
another. Change it in the same update as the token, which useAuth() does when you read the customer from it.
'use client';
import type { ReactNode } from 'react';
import { createThrottleAuth } from '@usethrottle/auth';
import { ThrottleAuthProvider, useAuth } from '@usethrottle/auth/react';
import { createCatalogClient } from '@usethrottle/catalog';
import { CatalogProvider } from '@usethrottle/catalog/react';
// One auth client and one catalog client for the life of the app.
const auth = createThrottleAuth({ publishableKey: 'pk_live_your_throttle_key' });
const catalog = createCatalogClient({
publishableKey: 'pc_pk_your_browser_key',
customerToken: () => auth.getAccessToken(),
});
function Catalog({ children }: { children: ReactNode }) {
// The signed-in customer's id, or null. It changes in the same render as
// the session, so cached answers never cross from one shopper to another.
const { customer } = useAuth();
return (
<CatalogProvider client={catalog} customerKey={customer?.id ?? null}>
{children}
</CatalogProvider>
);
}
export function Providers({ children }: { children: ReactNode }) {
return (
<ThrottleAuthProvider publishableKey="pk_live_your_throttle_key" auth={auth}>
<Catalog>{children}</Catalog>
</ThrottleAuthProvider>
);
}
X-Customer-Token, so browser caches keep them apart, and the server client
sends every request that carries a token with cache: 'no-store'. Do not put a
personal answer in a cache of your own that other shoppers read. See React and Next.js SSR and caching.