New features available! Check the changelog
Subscribfy
StorefrontCustomer Portal

VIP Savings Block

Show members how much they've saved through their membership: total order discounts since their VIP start date, plus the count of discounted orders

Give members a running total of what their membership has saved them. The VIP Savings block adds up every order discount since the customer's VIP start date and shows it as a single figure, alongside the number of discounted orders.

This is a copy-paste Custom Liquid snippet, not a built-in section. It reads straight from Shopify Liquid, so it makes no Subscribfy API calls and renders entirely on the storefront.


What Customers See

Total Saved

The sum of order.total_discounts across every order placed on or after the member's VIP start date, formatted with the shop's money format.

Discounted Orders

A count of how many of those orders actually carried a discount. "3 Orders" means three discounted orders, not three orders overall.

Auto Branding

Colors, radius, and spacing come from the portal's own theme variables, so the card matches the merchant's portal styling automatically.

Auto-Hide When Empty

If the customer has no VIP start date, the card is not rendered at all.


How It Works

The VIP start date is written once

When a membership contract is created, Subscribfy writes the date of the member's first VIP/Elite contract to the exison.vip_start_date customer metafield in Shopify. This happens one time per customer. For members who joined before the feature existed, a backfill command populates the metafield from their oldest membership contract.

The snippet reads that metafield in Liquid

The snippet pulls customer.metafields.exison.vip_start_date directly. No Subscribfy API is involved. It converts the start date and each order's created_at to unix timestamps first, because ISO date strings do not compare reliably in Liquid.

It totals the savings

The snippet loops customer.orders and, for every order placed on or after the start date, adds order.total_discounts to the running total. The order counter only increments for orders that actually had a discount.

It renders (or hides)

If the metafield is missing, the start timestamp is 0 and the whole card is skipped.


Install

Open the Portal editor

Go to SubscribfyCustomizationsCustomer PortalEdit page, then switch the left sidebar to the Sections tab.

Add a Custom Liquid block

In the container where you want the card (typically Main Content), click Add Block and pick Raw Liquid.

Paste the snippet

Paste the snippet below into the block's content field, then Save and Publish.

VIP Savings block
{%- assign vip_start_raw = customer.metafields.exison.vip_start_date -%}
{%- assign vip_start_ts = vip_start_raw | date: '%s' | plus: 0 -%}
{%- assign member_savings = 0 -%}
{%- assign member_orders = 0 -%}
{%- for order in customer.orders -%}
  {%- assign order_ts = order.created_at | date: '%s' | plus: 0 -%}
  {%- if vip_start_ts > 0 and order_ts >= vip_start_ts -%}
    {%- assign member_savings = member_savings | plus: order.total_discounts -%}
    {%- if order.total_discounts > 0 -%}
      {%- assign member_orders = member_orders | plus: 1 -%}
    {%- endif -%}
  {%- endif -%}
{%- endfor -%}
{%- if vip_start_ts > 0 -%}
<div class="exm-vip-savings">
  <span class="exm-vip-savings__label">VIP Savings</span>
  <span class="exm-vip-savings__value">{{ member_savings | money }}</span>
  <span class="exm-vip-savings__meta">{{ member_orders }} Orders</span>
</div>
<style>
.exm-vip-savings {
  display: flex;
  flex-direction: column;
  align-items: center;
  text-align: center;
  gap: calc(8px * var(--exison-spacing, 1));
  background: oklch(var(--exison-primary, 0.216 0.006 56.043));
  color: oklch(var(--exison-primary-foreground, 0.985 0.001 106.423));
  border: 0;
  border-radius: calc(var(--exison-card-radius, var(--exison-radius, 10px)) + 2px);
  padding: calc(12px * var(--exison-spacing, 1));
  box-shadow: 0 1px 2px 0 rgb(0 0 0 / 0.05);
}
.exm-vip-savings__label {
  font-weight: 600;
  color: oklch(var(--exison-muted, 0.97 0.001 106.424));
}
.exm-vip-savings__value {
  font-size: 2.25rem;
  line-height: 2.5rem;
  font-weight: 700;
}
.exm-vip-savings__meta {
  color: oklch(var(--exison-muted, 0.97 0.001 106.424));
}
@media (min-width: 750px) {
  .exm-vip-savings {
    display: grid;
    grid-template-columns: 1fr auto;
    grid-template-areas:
      'label value'
      'meta  value';
    align-items: center;
    column-gap: calc(24px * var(--exison-spacing, 1));
    row-gap: calc(2px * var(--exison-spacing, 1));
    text-align: left;
  }
  .exm-vip-savings__label { grid-area: label; }
  .exm-vip-savings__meta  { grid-area: meta; }
  .exm-vip-savings__value { grid-area: value; }
}
</style>
{%- else -%}
<p>No customer session / no VIP start date</p>
{%- endif -%}

Production tip

The {%- else -%} branch with the <p>No customer session / no VIP start date</p> line is a diagnostic. For production, delete that line and the {%- else -%} above it so customers without a session or metafield simply see no block.


Why It Only Shows on the Live Storefront

The portal page is served through the Shopify App Proxy with a Content-Type: application/liquid response. The Custom Liquid block's content is embedded server-side into that response inside a <template data-liquid-block="..."> tag. Shopify evaluates the Liquid with the logged-in customer's context, and the portal's React app then moves the rendered HTML into the block's position on the page.

Two consequences follow from this:

WhereWhat you see
Editor previewNothing. Liquid only runs on the storefront, so the block is blank in the editor.
Magic-link or admin-preview visitThe fallback line. These have no Liquid customer object, so the {%- else -%} branch renders instead.
Real storefront customer sessionThe full savings card.

The card needs a real storefront customer session to render its data. Always verify by opening the live portal as a logged-in member, not from the editor preview.


Styling and Branding

All colors, radius, and spacing come from the portal's --exison-* CSS variables, so the card follows the merchant's portal branding automatically. It uses the same recipe as the Welcome block: primary background, primary-foreground text, rounded card radius, and the portal spacing scale.

Keep the oklch() wrapper

The --exison-* variables hold bare OKLCH components (for example 0.216 0.006 56.043), so they must stay wrapped in oklch(...). Using a variable directly as a color value produces invalid CSS that browsers silently drop, and the card renders white. The var() fallbacks in the snippet are the portal defaults for contexts where the variables do not exist.

Desktop screens (750px and up) switch to a label-left / amount-right stat row via the @media block. Remove that block if you want the card centered on every screen size.


Troubleshooting


Was this page helpful?

On this page

AI Chat

Ask a question about Subscribfy