Hype Discounts

UI SDK (window.HypeUI)

Updated Oct 1, 20268 min read

On this page (14)

window.HypeUI holds the controller classes that drive Hype's campaign elements. Every element on the page — the product banner, the gift banner, the progress bar and the rest — creates one controller, and the controller does the work: it finds the campaign that matches the element's identifier, computes what should be shown, and publishes that as a reactive config store. Construct a controller yourself when you want the same data for your own markup, or want to react when it changes.

Use the Storefront SDK (window.Hype) instead when you need campaigns, the cart or discount codes rather than one block's rendered state.

What loads it

The Hype: Discounts app embed assigns window.HypeUI after it imports hype-ui.js, at the same time as window.Hype and window.HypeUIBlocksSchema. Turn the embed on at Online Store → Themes → Customize → App embeds → Hype: Discounts. Without it none of the three globals exist.

window.HypeUI exposes exactly these classes:

ClassElement it drivesCampaign type
ProductBannerController<hype-product-banner>Multi Products Discount, Order Discount
ProductPriceController<hype-product-price>Multi Products Discount, Order Discount
GwpBannerController<hype-free-gift-banner>Gift with Purchase
TieredCartController<hype-tiered-cart-progress-bar>Cart Goals
VolumeDiscountController<hype-volume-selector>Volume Discount
VolumeDiscountTableController<hype-volume-table>Volume Discount
CartDrawerControllerThe Hype Cart DrawerNot campaign based; see below

The announcement bar and the toast have no controller. They take everything from attributes; see Campaign elements overview.

Constructing a controller

The six campaign controllers take the block's App block ID — the same value you pass to an element's identifier attribute — and ProductPriceController also takes your shop's money format:

const banner = new window.HypeUI.ProductBannerController('YOUR_APP_BLOCK_ID')const price = new window.HypeUI.ProductPriceController('YOUR_APP_BLOCK_ID', '${{amount}}')const gifts = new window.HypeUI.GwpBannerController('YOUR_APP_BLOCK_ID')const progress = new window.HypeUI.TieredCartController('YOUR_APP_BLOCK_ID')const selector = new window.HypeUI.VolumeDiscountController('YOUR_APP_BLOCK_ID')const table = new window.HypeUI.VolumeDiscountTableController('YOUR_APP_BLOCK_ID')

A controller initializes itself as soon as window.Hype.campaigns exists, and otherwise waits for the hype:campaigns-loaded event, so you can construct it at any point after the embed's script has run. On initialization it looks through window.HypeUIBlocksSchema for a block whose settings.identifier equals the identifier you passed, remembers that block and its campaign, and starts computing state. An identifier that matches no block leaves the controller at its default state forever; matching is exact, so trim the value.

Each controller is independent. An element on the page keeps its own controller private, so constructing yours with the same identifier gives you a second instance with its own store. Both compute the same state from the same campaign and cart, and both cost a product fetch on product pages.

Every campaign controller also exposes:

MemberTypeWhat it holds
confignanostores atomThe rendered state; see the types below
$cartnanostores atomA mirror of window.Hype.cart, updated after every cart change
campaignobject or nullThe matched campaign from window.Hype.campaigns
uiBlockobject or nullThe matched entry from window.HypeUIBlocksSchema
hypeobject or nullThe window.Hype instance, once initialized
reset()methodPuts config back to its default state

Reading and subscribing

config is a nanostores atom, so it has four methods you will use:

MethodBehavior
get()Returns the current value
subscribe(callback)Calls callback(value) immediately and again after every change. Returns a function that unsubscribes.
listen(callback)Same as subscribe but skips the immediate call
set(value)Replaces the value. The controller calls this itself; you rarely should.
const progress = new window.HypeUI.TieredCartController('YOUR_APP_BLOCK_ID')const stop = progress.config.subscribe((state) => {  if (!state.isVisible) return  document.querySelector('#my-progress').style.width = `${state.progressPercentage}%`  document.querySelector('#my-progress-text').textContent = state.message})// later, if your markup goes awaystop()

With React or Preact, use the official adapter:

import { useStore } from '@nanostores/react'function CartProgress({ identifier }) {  const controller = React.useMemo(    () => new window.HypeUI.TieredCartController(identifier),    [identifier],  )  const state = useStore(controller.config)  if (!state.isVisible) return null  return <p>{state.message} ({state.progressPercentage}%)</p>}

With Lit, wrap the atom in StoreController from @nanostores/lit, which is what Hype's own elements do.

Actions you can call

A few controllers do more than publish state.

ControllerMethodWhat it does
GwpBannerControlleraddGiftToCart(variantId)Claims a gift by variant ID. Sets isLoading and claimingVariantId while the request runs.
TieredCartControlleraddGiftToCart(variantId, selectionLimit = 1, productHandles = [], isCustomSelection = false)Claims a gift for a goal. Pass the goal's selectionLimit, giftProductHandles and isCustomSelection from the gift when the goal lets shoppers choose.
VolumeDiscountControllerhandleAddToCart(tierIndex)Adds the quantity for that tier, using the variants currently selected in tiers[tierIndex].selectedVariants
VolumeDiscountControllerhandleVariantChange(slotId, tierIndex, optionName, value)Changes one option of one variant slot in a tier and recalculates its prices. slotId is the key of that slot in tiers[tierIndex].selectedVariants.
GwpBannerController, TieredCartController, VolumeDiscountController, VolumeDiscountTableControllerupdateConfig(partial)Merges a partial object into config

Config types

The shapes below are the ones published on each controller's config. Prices are numbers in cents unless they are described as formatted, and formatted strings already carry the shop's money format.

Shared types

type Icon = {  type: 'emoji' | 'filled' | 'outlined' | 'none'  name: string}type Badge = {  icon: Icon  text: string}type MarketWiseValueProxy = {  isPrimaryMarketValue: boolean  value: number}type Gift = {  title: string  price: number  selectionLimit?: number  productHandles?: string[]  isCustomSelection?: boolean  giftSubtitle: string  description?: string  isAddedToCart: boolean  buttonText: string  variants: ProductVariant[]  options: ProductOption[]  featuredImage: string  badge: Badge}type SelectedVariant = {  variantId: number  selectedOptions: Record<string, string>  price: number  available: boolean  imageSrc: string | null}type ProductOption = {  name: string  position: number  values: string[]}

ProductVariant is Shopify's own variant object from the product JSON endpoint: id, title, options, price in cents, compare_at_price, available, sku, featured_image and the rest.

For filled and outlined icons, name must be one of Hype's built-in icon names; the list is in Announcement bar element. For emoji, name is the emoji itself. For none, leave it empty.

A MarketWiseValueProxy whose isPrimaryMarketValue is true holds an amount in the shop's primary currency. Multiply it by window.Hype.utils.getCurrencyRate() to get the shopper's currency, which is what every controller does before displaying it.

ProductBannerController

type ProductBannerConfig = {  isVisible: boolean  message: string  icon: Icon}

message is the banner text with {{discount}} and {{discount_code}} already substituted.

ProductPriceController

type ProductPriceBlock =  | { type: 'original_price'; isStrikedOff: boolean; text: string }  | { type: 'discounted_price'; text: string }  | { type: 'message'; isBadge: boolean; text: string }type ProductPriceConfig = {  blocks: ProductPriceBlock[]}

blocks holds zero to three entries in the order configured in the Hype admin, with every text already formatted and every message variable substituted. There is no visibility flag: an empty blocks array means nothing to show. Before a campaign matches, blocks carries the product's own price, so check controller.campaign if you need to know whether a discount is in play. See Product price element.

GwpBannerController

type GwpConfig = {  showGifts: boolean  isUnlocked: boolean  isLoading: boolean  claimingVariantId: number | null  selectionLimit: number  giftProductHandles: string[]  isCustomSelection: boolean  promoBarTitle: string  promoBarIcon: Icon  bannerMessage: string  bannerIcon: Icon  gifts: Gift[]  showPromoBar: boolean  showCollapsible: boolean}
FieldMeaning
showGiftsThe gift list should be visible
isUnlockedThe cart meets the campaign's condition
isLoadingA claim request is in flight; claimingVariantId says for which variant
selectionLimitHow many gifts the shopper may claim
giftProductHandlesHandles of the products offered as gifts
isCustomSelectionThe shopper chooses among the gifts rather than receiving a fixed one
promoBarTitle, promoBarIconThe collapsible header
bannerMessage, bannerIconThe message above the gift list, for the current state: pending, unlocked or added
giftsThe gifts with their variants, options, price and isAddedToCart
showPromoBar, showCollapsibleLayout settings from the block

TieredCartController

type TieredCartProgressUIState = {  isVisible: boolean  cartTotal: number  progressPercentage: number  nextTier?: {    index: number    remainingAmount: number    label: string    isPrimaryMarketValue: boolean  }  allTiers: TierUI[]  settings: TieredCartSettings  message: string  successMessages: string[]  isLoading: boolean  claimingVariantId: number | null}type TierUI = {  index: number  minAmount: number  isPrimaryMarketValue: boolean  isUnlocked: boolean  label: string  icon: Icon  positionPercent: number  successMessage: string  showOffers: string  isDisabled?: boolean  gifts?: Gift[]}type TieredCartSettings = {  identifier: string  useCustomCode: boolean  showIcon: boolean  title: string  successMessage: string  showOffers: string  goals: Array<FreeGiftGoal | NonGiftGoal>  header: Badge}type BaseGoal = {  text: string  icon: Icon  successMessage: string  goalName: string}type FreeGiftGoal = BaseGoal & {  type: 'freeGift'  giftBlock: {    showGiftBlock: boolean    badge: Badge    giftSubtitle: string    description: string    claimButtonText: string    claimedButtonText: string  }}type NonGiftGoal = BaseGoal & {  type: 'freeShipping' | 'amountOff'}

cartTotal and minAmount are in cents. progressPercentage runs from 0 to 100. positionPercent is where the tier's marker sits on the bar. nextTier is absent once every goal is reached, and message then carries the campaign's overall success message. showOffers is one of currentGoal, nextGoal or hideGoal, the block's Show offers setting.

VolumeDiscountController

type VolumeDiscountConfig = {  isVisible: boolean  isLoading: boolean  tiers: VolumeDiscountTier[]  header: VolumeDiscountHeader  hasVariants: boolean  variantPickerOptions: Record<string, string[]>  variants?: ProductVariant[]  options?: ProductOption[]}type VolumeDiscountTier = {  cartEligibility: {    type: 'quantity' | 'amount'    value: MarketWiseValueProxy  }  customerGets: {    appliesToEachItem: boolean    type: 'percentage' | 'amount'    value: MarketWiseValueProxy  }  popularBadge?: { icon: Icon; label: string }  title: string  discountBadgeText: string  selectedVariants: Record<string, SelectedVariant>  strikeOriginalPrice: boolean  showAmountOffOrPercentageBadges: boolean}type VolumeDiscountHeader = {  icon: Icon  title: string}

variantPickerOptions maps each option name to its values, for building pickers. selectedVariants holds one slot per unit in the tier, keyed by a generated id; iterate its entries rather than assuming the keys. variants and options are the full product data and are absent in the theme editor's sample state.

VolumeDiscountTableController

type VolumeDiscountTableConfig = {  isVisible: boolean  isLoading: boolean  tiers: Array<{ eligibility: string; discount: string }>  header: VolumeDiscountHeader}

Each tier is two formatted strings, for example Buy 2 and Save 10%, with the block's variables already substituted. This VolumeDiscountTier shape is different from the selector's, even though the two share a name in the source.

CartDrawerController

CartDrawerController belongs to Hype Cart Drawer, not to campaigns. It is constructed with an options object rather than an identifier, keeps its state in a store named state rather than config, and offers cart actions such as open(), close(), setQuantity(key, quantity), addItem(), removeLine(key), applyDiscount(code) and goToCheckout(). It listens for the hype:cart:open window event. Its documentation lives with that app.

Related articles

Still stuck?

Ask a follow-up and we'll see it came from “UI SDK (window.HypeUI)”.

Opens chat with your question and a link to this article.