Automatic best offer

Prices every eligible offer against the cart, applies the best one without a code and explains why in a disclosure.

componentsummaryanydiscount
$npx shadcn@latest add @storekit/cart-auto-discount
cart-auto-discount

Live and interactive. Switch the brand in the frame header to see the same source under another token file.

Installation

$npx shadcn@latest add @storekit/cart-auto-discount
$pnpm dlx shadcn@latest add @storekit/cart-auto-discount
$yarn dlx shadcn@latest add @storekit/cart-auto-discount
$bunx --bun shadcn@latest add @storekit/cart-auto-discount

Source

components/storekit/cart/cart-auto-discount.tsx
140 lines
"use client"

import * as React from "react"
import { AnimatePresence, motion, useReducedMotion } from "motion/react"
import { BadgePercent, Check, ChevronDown } from "lucide-react"
import { cn, money } from "@/components/storekit/lib/cn"
import { useSkBlock } from "@/components/storekit/lib/sk-analytics"

/* Automatic best discount. Every eligible offer is priced against the
   current cart, the best one is applied without a code, and a disclosure
   explains why it won and what the alternatives were worth. Offers do not
   stack unless your backend says so. */

export type AutoOffer = {
  id: string
  label: string
  /** Percent off the subtotal. */
  percentOff?: number
  /** Fixed cents off. */
  amountOff?: number
  /** Minimum subtotal in cents. */
  minSubtotal?: number
  /** Minimum item count. */
  minItems?: number
}

const SAMPLE_OFFERS: AutoOffer[] = [
  { id: "ten-over-150", label: "10% off orders over $150", percentOff: 10, minSubtotal: 15000 },
  { id: "twenty-three-items", label: "$20 off 3 or more items", amountOff: 2000, minItems: 3 },
  { id: "fifteen-over-250", label: "15% off orders over $250", percentOff: 15, minSubtotal: 25000 },
]

export function CartAutoDiscount({
  subtotal = 20700,
  itemCount = 3,
  offers = SAMPLE_OFFERS,
  shipping,
  className,
}: {
  subtotal?: number
  itemCount?: number
  offers?: AutoOffer[]
  /** Shipping in cents. Omit to show "At checkout". */
  shipping?: number
  className?: string
}) {
  const reduce = useReducedMotion()
  const sk = useSkBlock("cart", "cart-auto-discount")
  const [open, setOpen] = React.useState(false)
  const regionId = React.useId()

  const priced = offers.map((o) => {
    const eligible = subtotal >= (o.minSubtotal ?? 0) && itemCount >= (o.minItems ?? 0)
    const value = Math.min(subtotal, (o.percentOff ? Math.round((subtotal * o.percentOff) / 100) : 0) + (o.amountOff ?? 0))
    const gap = o.minSubtotal && subtotal < o.minSubtotal ? o.minSubtotal - subtotal : 0
    const itemsGap = o.minItems && itemCount < o.minItems ? o.minItems - itemCount : 0
    return { ...o, eligible, value, gap, itemsGap }
  })
  const best = priced.filter((o) => o.eligible).sort((a, b) => b.value - a.value)[0]
  const total = subtotal - (best?.value ?? 0) + (shipping ?? 0)

  return (
    <div ref={sk.ref as React.Ref<HTMLDivElement>} {...sk.attrs} className={cn("w-full rounded-sk-card border border-sk-line bg-sk-card p-5", className)}>
      <dl className="grid grid-cols-1 gap-2 text-sm">
        <div className="flex justify-between gap-4">
          <dt className="text-sk-muted">Subtotal</dt>
          <dd className="tabular-nums">{money(subtotal)}</dd>
        </div>
        {best ? (
          <div className="flex justify-between gap-4 text-sk-savings">
            <dt className="flex min-w-0 items-center gap-1.5">
              <BadgePercent size={14} className="shrink-0" />
              <span className="truncate">{best.label}</span>
            </dt>
            <dd className="shrink-0 tabular-nums">-{money(best.value)}</dd>
          </div>
        ) : null}
        <div className="flex justify-between gap-4">
          <dt className="text-sk-muted">Shipping</dt>
          <dd className={cn("tabular-nums", shipping === undefined && "text-sk-muted")}>{shipping === undefined ? "At checkout" : shipping === 0 ? "Free" : money(shipping)}</dd>
        </div>
        <div className="mt-1 flex items-baseline justify-between gap-4 border-t border-sk-line pt-3">
          <dt className="font-medium">Total</dt>
          <dd className="font-sk-display text-lg font-semibold tabular-nums text-sk-price">{money(total)}</dd>
        </div>
      </dl>

      <div className="mt-3 rounded-sk-btn bg-sk-savings/[0.08]">
        <button
          type="button"
          aria-expanded={open}
          aria-controls={regionId}
          onClick={() => {
            setOpen((o) => !o)
            if (!open) sk.emit("interact", "why_this_offer")
          }}
          className="flex min-h-11 w-full items-center gap-2 rounded-sk-btn px-3 text-left text-[13px] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-sk-accent active:scale-[0.99]"
        >
          <Check size={14} className="shrink-0 text-sk-savings" />
          <span className="min-w-0 flex-1 text-sk-ink">
            {best ? "Best offer applied. No code needed." : "No offer applies yet."}
          </span>
          <span className="shrink-0 text-sk-muted">Why</span>
          <ChevronDown size={14} className={cn("shrink-0 text-sk-muted transition-transform duration-200", open && "rotate-180")} />
        </button>
        <AnimatePresence initial={false}>
          {open ? (
            <motion.div
              id={regionId}
              initial={{ height: 0, opacity: 0 }}
              animate={{ height: "auto", opacity: 1 }}
              exit={{ height: 0, opacity: 0 }}
              transition={{ duration: reduce ? 0 : 0.28, ease: [0.16, 1, 0.3, 1] }}
              className="overflow-hidden"
            >
              <div className="px-3 pb-3">
                <p className="text-xs leading-[1.5] text-sk-muted">Offers do not combine. We compared each one against this cart and kept the largest saving.</p>
                <ul className="mt-2 grid gap-1.5">
                  {priced.map((o) => (
                    <li key={o.id} className="flex items-start justify-between gap-3 text-xs">
                      <span className={cn("min-w-0", o.id === best?.id ? "font-medium text-sk-ink" : "text-sk-muted")}>{o.label}</span>
                      <span className={cn("shrink-0 text-right tabular-nums", o.id === best?.id ? "font-medium text-sk-savings" : "text-sk-muted")}>
                        {o.eligible
                          ? `${o.id === best?.id ? "Applied " : ""}-${money(o.value)}`
                          : o.gap
                            ? `${money(o.gap)} away`
                            : `${o.itemsGap} more ${o.itemsGap === 1 ? "item" : "items"}`}
                      </span>
                    </li>
                  ))}
                </ul>
              </div>
            </motion.div>
          ) : null}
        </AnimatePresence>
      </div>
    </div>
  )
}

Usage

Import from "@/components/storekit/cart/cart-auto-discount" and pass your product data. The demo above exercises the full prop surface with sample values.

Automatic best offer, StoreKit