Display product approvals in the account area
On this page
When you sell restricted products, each approval is a record against a customer’s Account. The account page can show those approvals back to the customer, so they know what they are cleared to buy, how much of their allowance is left, and when it runs out.
The base theme ships this section already. Use this process when you want to change what it shows, or add the approvals link to your account menu.
Before you start
- Set up at least one restricted product and grant an approval to a test Account. See Restricted products.
- Understand how the account page routes its sections. See Theme layouts and pages.
The section already exists
The account page reads current_request.params.section and renders one snippet per section. Product approvals is one of the sections the base theme registers:
| Section | URL | Snippet |
|---|---|---|
product_approvals |
/account?section=product_approvals |
account/product_approvals |
The router clause looks like this:
```liquid
{%- when “product_approvals” %} {% render “account/product_approvals”, identifier: identifier %} ```
snippets/account/product_approvals is only a dispatcher: it renders product_approvals/index for the list and product_approvals/show for a single approval.
Override snippets/account/product_approvals/index, not the dispatcher. Replacing the dispatcher removes the detail page along with the list, and you do not need to touch the router either way.
Where the data comes from
Approvals hang off the Account, not the Contact. The account page provides both current_customer (the signed-in Contact) and current_account (their Account), so read approvals from current_account.
| Liquid | Type | Notes |
|---|---|---|
current_account.product_approvals |
List[ProductApproval] | The approvals belonging to this Account |
Each entry is a ProductApproval drop, which renders the s_c__Permitted_Restricted_Product__c record. These are the attributes worth surfacing:
| Attribute | Type | Notes |
|---|---|---|
approval_status |
String | One of approved, completed, expired, pending, or none. Prints as text, but reaches Liquid as a symbol, so cast it to a string before comparing it |
product |
Product | The approved product, when the approval is for a single product |
product_category |
ProductCategory | The approved category, when the approval covers a whole category |
approved_quantity |
Number | The maximum quantity this approval allows |
purchased_quantity |
Number | The quantity already bought on completed orders |
pending_quantity |
Number | The quantity already bought on submitted orders that have not completed |
remaining_quantity |
Number | The unused quantity still available |
unlimited? |
Boolean | True when the approval has no quantity limit |
approved_from |
Timestamp | When the approval starts, as a UTC timestamp |
approved_until |
Timestamp | When the approval ends, as a UTC timestamp |
order_items |
List[OrderItem] | The order items bought against this approval |
:::note
An approval is granted at product level or at category level, so one of product and product_category is blank on any given record. Check which one is populated rather than assuming product is always there, or the row renders with an empty name.
:::
Build the section
This snippet lists the customer’s approvals, handles both product and category approvals, and shows remaining quantity only where the approval is limited.
```liquid
Product approvals
{%- assign approvals = current_account.product_approvals %}
{%- if approvals.size > 0 %}
| Approved for | Status | Remaining | Expires |
|---|---|---|---|
| {%- if approval.product != blank %} {{ approval.product.name }} {%- elsif approval.product_category != blank %} {{ approval.product_category.name }} {%- else %} Approval {%- endif %} | {{ approval.approval_status }} | {%- if approval.unlimited? %} Unlimited {%- else %} {{ approval.remaining_quantity }} of {{ approval.approved_quantity }} {%- endif %} | {%- if approval.approved_until != blank %} {{ approval.approved_until | date: "%b %-d, %Y", timezone: current_store.timezone }} {%- else %} No expiry {%- endif %} |
{%- else %}
You have no product approvals.
{%- endif %} ```
Pass a timezone when you format these dates. approved_from and approved_until are UTC, so formatting them without timezone: can render a date a day out for customers whose local date differs from UTC, which matters most on the expiry date a customer is reading to decide whether to order today.
The markup above uses a plain table so it inherits your own styles. For how theme assets and stylesheets are loaded, see Theme assets and styling.
Add the section to the account menu
Both snippets/account/menu and snippets/header/dropdown/account already show this link when the account has approvals, so there is usually nothing to add. Override one of them when you want to change the wording or where the entry sits, not to create it.
The existing entries are conditional for a reason, so keep the guard when you change one, or retail customers see a link to an empty page:
```liquid
{%- if current_account.product_approvals.size > 0 %} Product approvals {%- endif %} ```
For the full account menu structure and how to add your own sections, see Add a custom page to the account page menu.
Show approval status on the product page
The same data is useful on the product page itself, where a customer is deciding whether to buy. Do not loop the account’s approvals to find it: the product drop already carries the answer, and it accounts for approvals granted at category level, which a loop matching on product id would miss.
| Liquid | Returns |
|---|---|
product.approval_status |
This customer’s status for this product: approved, completed, expired, pending, or none. Cast it to a string before comparing it. |
product.current_approved_quantity |
The maximum quantity this customer may buy. Returns Infinity when the approval is unlimited, and when the product is not restricted at all. |
product.pending_approval_date |
When a pending approval starts, or blank |
:::warning Two things here fail silently rather than raising an error.
approval_status reaches Liquid as a symbol, not a string. Comparing it directly, as product.approval_status == "approved", is always false, so the message never renders and nothing tells you why. Cast it first. Printing it with {{ }} is unaffected.
current_approved_quantity returns Infinity for an unlimited approval. Printing it unguarded shows the customer the word “Infinity” where a number should be.
:::
```liquid
| {%- assign approval_state = product.approval_status | cast: “string” %} |
| {%- assign approved_quantity = product.current_approved_quantity | cast: “string” %} |
{%- if approval_state == “approved” %} {%- if approved_quantity == “Infinity” %} <p class="SC-Notice">You are approved to buy this item.</p> {%- else %} <p class="SC-Notice">You can buy up to {{ approved_quantity }} of this item under your current approval.</p> {%- endif %} {%- elsif approval_state == “pending” and product.pending_approval_date != blank %}
Your approval for this item starts {{ product.pending_approval_date | date: "%b %-d, %Y", timezone: current_store.timezone }}.
{%- endif %} ```
Both attributes aggregate a product-level and a category-level approval, so this is correct where a loop over product_approvals would silently show nothing to a customer approved by category. See the cast filter.
:::warning Do not treat what you render here as an access control. Whether a customer can actually buy a restricted product is enforced by StoreConnect when the order is placed, and the approval fields are for informing the customer, not for gating the purchase. Rendering an encouraging message does not grant permission, and hiding one does not remove it. :::
Once the section is in place, an approved customer visiting /account?section=product_approvals sees each approval with its status, its remaining quantity, and its expiry date.
Was this article helpful?
Thanks for your feedback! It helps us improve our docs.