Skip to content
Commerce block

Return or Exchange Picker

Refund, exchange or credit decided per line, with live stock on the variants and the price difference shown before the goods are posted rather than after.

331 lineslucide-reactAdded 13 Sept 2026
  • returns
  • exchange
  • after-sale
  • commerce
  • store credit

What's included

  • components/return-exchange-picker.tsx
  • Needs lucide-react

Works with

  • React
  • Next.js
  • Tailwind CSS
  • TypeScript

npx hoverlab add return-exchange-picker

Or over MCP, from your editor's agent — no account needed.

License

Free to read, copy and install, for personal and non-commercial projects. Shipping it in client work or a paid product needs Pro ($79 once). The source lands in your repo and stops being ours — no attribution, nothing to upgrade.

Was this useful?

Start a page with this section — add more, order them, and leave with the page source.

Preview

Return or exchange

Order ORD-88410. Each line is decided on its own — one can be refunded while another is swapped.

Graphite mesh, standard height · £420.00 · 1 in the order

Outcome for Meridian task chair

Swap for

Brushed steel · £145.00 · 2 in the order

Summary

Lines selected
1
Coming back to you
£0.00

Rendered live in your current theme — this is the same component whose source is below, not a screenshot of it.

Source

components/return-exchange-picker.tsx
'use client'

/**
 * <ReturnExchangePicker> — refund, exchange, or credit, decided per line.
 *
 * The fork every returns flow has to offer and most of them get wrong by
 * asking too early: a single radio at the top — "Refund or exchange?" — before
 * the customer has said what they are sending back. Then the exchange path
 * discovers the replacement is out of stock and the whole flow restarts.
 *
 * THE ORDER OF THE QUESTIONS IS THE DESIGN
 *
 *   1. Which line, and how many.
 *   2. What is the outcome for *that* line.
 *   3. If exchange: what for — with live stock, in this step, not at checkout.
 *
 * Doing it per line matters because the answer genuinely differs: the chair
 * that arrived scratched is a refund, the shirt in the wrong size is an
 * exchange, and forcing one answer onto both is what makes people give up and
 * email.
 *
 * EXCHANGE IS PRICED HONESTLY AND IMMEDIATELY
 *
 * Swapping for a more expensive variant shows the difference to pay before the
 * customer commits, and a cheaper one shows the amount coming back. A flow
 * that reveals "£40 to pay" after the goods have been posted is a flow with a
 * complaint at the end of it.
 *
 * STORE CREDIT IS OFFERED, NOT PRE-SELECTED. It is usually the retailer's
 * preferred outcome and often carries a bonus, so it gets a badge saying what
 * the bonus is — but the default stays refund, because a pre-ticked option
 * that favours the seller is a dark pattern with a business case.
 *
 * OUT-OF-STOCK VARIANTS ARE SHOWN DISABLED, NOT HIDDEN. Same argument as
 * `search-facet-panel` makes about zero counts: a list that reshapes itself is
 * disorienting, and "my size is not there" is a different feeling from "my
 * size is out of stock until the 30th".
 *
 * ACCESSIBILITY: the outcome is a real radio group per line, named by the
 * product; variant buttons carry `aria-pressed` and their stock state in text;
 * the running summary is a polite live region.
 */

import * as React from 'react'
import { ArrowLeftRight, BadgePercent, Banknote, Check } from 'lucide-react'

export type Outcome = 'refund' | 'exchange' | 'credit'

export interface ExchangeVariant {
  id: string
  label: string
  priceDelta: number
  /** Zero renders the variant disabled with the restock note. */
  stock: number
  restock?: string
}

export interface ReturnLine {
  id: string
  name: string
  variant: string
  price: number
  quantity: number
  variants: ExchangeVariant[]
}

export interface ReturnExchangePickerProps {
  orderRef?: string
  lines?: ReturnLine[]
  /** Percentage bonus added when store credit is chosen. */
  creditBonusPct?: number
  currency?: string
  className?: string
}

const DEFAULT_LINES: ReturnLine[] = [
  {
    id: 'l1',
    name: 'Meridian task chair',
    variant: 'Graphite mesh, standard height',
    price: 420,
    quantity: 1,
    variants: [
      { id: 'v1', label: 'Graphite mesh, tall', priceDelta: 0, stock: 6 },
      { id: 'v2', label: 'Slate mesh, standard', priceDelta: 0, stock: 0, restock: 'back on 30 September' },
      { id: 'v3', label: 'Graphite mesh, headrest', priceDelta: 65, stock: 3 },
      { id: 'v4', label: 'Graphite fabric, standard', priceDelta: -40, stock: 11 },
    ],
  },
  {
    id: 'l2',
    name: 'Halo task lamp',
    variant: 'Brushed steel',
    price: 145,
    quantity: 2,
    variants: [
      { id: 'v5', label: 'Matte black', priceDelta: 0, stock: 14 },
      { id: 'v6', label: 'Brass', priceDelta: 25, stock: 0, restock: 'back on 12 October' },
    ],
  },
]

const OUTCOMES: { id: Outcome; label: string; note: string; icon: typeof Banknote }[] = [
  { id: 'refund', label: 'Refund', note: 'Back to the original payment method', icon: Banknote },
  { id: 'exchange', label: 'Exchange', note: 'Swap for another variant', icon: ArrowLeftRight },
  { id: 'credit', label: 'Store credit', note: 'Issued instantly, never expires', icon: BadgePercent },
]

interface LineChoice {
  on: boolean
  quantity: number
  outcome: Outcome
  variantId: string
}

export function ReturnExchangePicker({
  orderRef = 'ORD-88410',
  lines = DEFAULT_LINES,
  creditBonusPct = 10,
  currency = '£',
  className = '',
}: ReturnExchangePickerProps) {
  const uid = React.useId()
  const [choices, setChoices] = React.useState<Record<string, LineChoice>>(() =>
    Object.fromEntries(
      lines.map((line, i) => [
        line.id,
        { on: i === 0, quantity: 1, outcome: i === 0 ? 'exchange' : 'refund', variantId: '' },
      ]),
    ),
  )

  function money(value: number) {
    const sign = value < 0 ? '−' : ''
    return `${sign}${currency}${Math.abs(value).toFixed(2)}`
  }

  const active = lines.filter((line) => choices[line.id]?.on)

  const refundTotal = active.reduce((sum, line) => {
    const c = choices[line.id]!
    if (c.outcome === 'refund') return sum + line.price * c.quantity
    if (c.outcome === 'credit') {
      return sum + line.price * c.quantity * (1 + creditBonusPct / 100)
    }
    return sum
  }, 0)

  const exchangeDelta = active.reduce((sum, line) => {
    const c = choices[line.id]!
    if (c.outcome !== 'exchange') return sum
    const variant = line.variants.find((v) => v.id === c.variantId)
    return sum + (variant?.priceDelta ?? 0) * c.quantity
  }, 0)

  function update(lineId: string, patch: Partial<LineChoice>) {
    setChoices((c) => ({ ...c, [lineId]: { ...c[lineId]!, ...patch } }))
  }

  return (
    <section className={`bg-background px-4 py-10 sm:px-6 ${className}`}>
      <div className="mx-auto max-w-3xl">
        <h2 className="text-xl font-bold tracking-tight">Return or exchange</h2>
        <p className="mt-1 text-sm text-muted-foreground">
          Order {orderRef}. Each line is decided on its own — one can be
          refunded while another is swapped.
        </p>

        <div className="mt-6 space-y-4">
          {lines.map((line) => {
            const c = choices[line.id]!
            const chosenVariant = line.variants.find((v) => v.id === c.variantId)
            return (
              <div
                key={line.id}
                className={`rounded-xl border p-4 transition-colors ${
                  c.on ? 'border-primary bg-primary/5' : 'border-border'
                }`}
              >
                <div className="flex items-start gap-3">
                  <input
                    type="checkbox"
                    id={`${uid}-${line.id}`}
                    checked={c.on}
                    onChange={(e) => update(line.id, { on: e.target.checked })}
                    className="mt-1 h-4 w-4 shrink-0 rounded border-border accent-primary"
                  />
                  <div className="min-w-0 flex-1">
                    <label htmlFor={`${uid}-${line.id}`} className="text-sm font-medium">
                      {line.name}
                    </label>
                    <p className="text-xs text-muted-foreground">
                      {line.variant} · {money(line.price)} · {line.quantity} in the order
                    </p>

                    {c.on ? (
                      <>
                        <fieldset className="mt-3 border-0 p-0">
                          <legend className="text-xs font-semibold uppercase tracking-wide text-muted-foreground">
                            Outcome for {line.name}
                          </legend>
                          <div className="mt-2 grid gap-2 md:grid-cols-3">
                            {OUTCOMES.map((outcome) => {
                              const Icon = outcome.icon
                              const id = `${uid}-${line.id}-${outcome.id}`
                              const on = c.outcome === outcome.id
                              return (
                                <div key={outcome.id} className="min-w-0">
                                  <input
                                    type="radio"
                                    id={id}
                                    name={`${uid}-${line.id}-outcome`}
                                    checked={on}
                                    onChange={() => update(line.id, { outcome: outcome.id })}
                                    className="peer sr-only"
                                  />
                                  <label
                                    htmlFor={id}
                                    className={`block cursor-pointer rounded-lg border p-2.5 text-xs transition-colors peer-focus-visible:ring-2 peer-focus-visible:ring-ring ${
                                      on ? 'border-primary bg-card' : 'border-border hover:bg-muted/60'
                                    }`}
                                  >
                                    <span className="flex items-center gap-1.5 font-semibold">
                                      <Icon aria-hidden className="h-3.5 w-3.5 shrink-0" />
                                      {outcome.label}
                                      {outcome.id === 'credit' ? (
                                        <span className="rounded bg-emerald-500/10 px-1 text-[10px] font-bold text-emerald-700 dark:text-emerald-400">
                                          +{creditBonusPct}%
                                        </span>
                                      ) : null}
                                    </span>
                                    <span className="mt-0.5 block text-muted-foreground">
                                      {outcome.note}
                                    </span>
                                  </label>
                                </div>
                              )
                            })}
                          </div>
                        </fieldset>

                        {c.outcome === 'exchange' ? (
                          <div className="mt-3">
                            <p className="text-xs font-semibold uppercase tracking-wide text-muted-foreground">
                              Swap for
                            </p>
                            <ul className="mt-2 flex flex-wrap gap-2">
                              {line.variants.map((variant) => {
                                const out = variant.stock === 0
                                const on = c.variantId === variant.id
                                return (
                                  <li key={variant.id}>
                                    <button
                                      type="button"
                                      disabled={out}
                                      aria-pressed={on}
                                      onClick={() => update(line.id, { variantId: variant.id })}
                                      className={`rounded-lg border px-2.5 py-1.5 text-start text-xs transition-colors disabled:cursor-not-allowed disabled:opacity-50 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring ${
                                        on ? 'border-primary bg-card font-semibold' : 'border-border hover:bg-muted/60'
                                      }`}
                                    >
                                      <span className="flex items-center gap-1.5">
                                        {on ? <Check aria-hidden className="h-3 w-3" /> : null}
                                        {variant.label}
                                      </span>
                                      <span className="mt-0.5 block text-muted-foreground">
                                        {out
                                          ? `Out of stock — ${variant.restock}`
                                          : variant.priceDelta === 0
                                            ? 'Same price'
                                            : `${money(variant.priceDelta)} difference`}
                                      </span>
                                    </button>
                                  </li>
                                )
                              })}
                            </ul>
                            {chosenVariant && chosenVariant.priceDelta !== 0 ? (
                              <p className="mt-2 text-xs font-medium">
                                {chosenVariant.priceDelta > 0
                                  ? `${money(chosenVariant.priceDelta * c.quantity)} to pay before it ships.`
                                  : `${money(Math.abs(chosenVariant.priceDelta) * c.quantity)} comes back to you.`}
                              </p>
                            ) : null}
                          </div>
                        ) : null}
                      </>
                    ) : null}
                  </div>
                </div>
              </div>
            )
          })}
        </div>

        <div
          aria-live="polite"
          className="mt-6 rounded-xl border border-border bg-card p-4"
        >
          <h3 data-stress-ignore className="text-sm font-semibold">Summary</h3>
          <dl className="mt-3 space-y-2 text-sm">
            <div className="flex justify-between gap-3">
              <dt className="text-muted-foreground">Lines selected</dt>
              <dd>{active.length}</dd>
            </div>
            <div className="flex justify-between gap-3">
              <dt className="text-muted-foreground">Coming back to you</dt>
              <dd className="font-medium">{money(refundTotal)}</dd>
            </div>
            {exchangeDelta !== 0 ? (
              <div className="flex justify-between gap-3">
                <dt className="text-muted-foreground">
                  {exchangeDelta > 0 ? 'To pay for the exchange' : 'Exchange credit'}
                </dt>
                <dd className="font-medium">{money(Math.abs(exchangeDelta))}</dd>
              </div>
            ) : null}
          </dl>
          <button
            type="button"
            disabled={active.length === 0}
            className="mt-4 w-full rounded-lg bg-primary px-4 py-2 text-sm font-semibold text-primary-foreground transition-opacity hover:opacity-90 disabled:opacity-40 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-card"
          >
            Confirm and get a return label
          </button>
        </div>
      </div>
    </section>
  )
}

Before you paste

  • Styling is Tailwind utility classes on semantic tokens (bg-card, text-muted-foreground) — it inherits your theme instead of overriding it.
  • Install: npm i lucide-react
  • Every prop has a default, so it renders standalone before you wire it up.

Where it goes

Drop it at components/return-exchange-picker.tsx and import it where you need the section:

import { ReturnExchangePicker } from '@/components/return-exchange-picker'

Customize

3 of this block’s props are simple enough to drive from here. Change them and the block below re-renders — it is the same component whose source is above, not a mock of it. Everything else it accepts is in the table underneath.

Props

Read out of the component’s own type and signature, so this cannot drift from the source below. Every prop has a default — the component renders standalone before you pass it anything.

PropTypeDefault
orderRefstring'ORD-88410'
linesReturnLine[]DEFAULT_LINES
creditBonusPctPercentage bonus added when store credit is chosen.number10
currencystring'£'
classNamestring''

Not using React?

The same block rendered once to markup, wrapped as a file your framework compiles. Tailwind classes are framework-agnostic, so the design transfers intact — the behaviour does not.

This block is interactive. The markup below is its initial state with the event handlers stripped — you will need to re-wire the behaviour in your framework.

return-exchange-picker.html
<!--
  Return or Exchange Picker — markup from the Hoverlab catalog.

  This is the block rendered once to HTML and wrapped as a component
  file. It is not a port of the React source: the Tailwind classes carry
  the design, which is the part that took the work, and they are the same
  in every framework.

  This block is interactive in React and the handlers are NOT here.
  Buttons, toggles and menus render in their initial state and do
  nothing until you wire them up.
-->
<section class="bg-background px-4 py-10 sm:px-6 ">
  <div class="mx-auto max-w-3xl">
    <h2 class="text-xl font-bold tracking-tight">Return or exchange</h2>
    <p class="mt-1 text-sm text-muted-foreground">Order ORD-88410. Each line is decided on its own — one can be refunded while another is swapped.</p>
    <div class="mt-6 space-y-4">
      <div class="rounded-xl border p-4 transition-colors border-primary bg-primary/5">
        <div class="flex items-start gap-3">
          <input type="checkbox" id="_R_0_-l1" class="mt-1 h-4 w-4 shrink-0 rounded border-border accent-primary" checked="" />
          <div class="min-w-0 flex-1">
            <label for="_R_0_-l1" class="text-sm font-medium">Meridian task chair</label>
            <p class="text-xs text-muted-foreground">Graphite mesh, standard height · £420.00 · 1 in the order</p>
            <fieldset class="mt-3 border-0 p-0">
              <legend class="text-xs font-semibold uppercase tracking-wide text-muted-foreground">Outcome for Meridian task chair</legend>
              <div class="mt-2 grid gap-2 md:grid-cols-3">
                <div class="min-w-0">
                  <input type="radio" id="_R_0_-l1-refund" class="peer sr-only" name="_R_0_-l1-outcome" />
                  <label for="_R_0_-l1-refund" class="block cursor-pointer rounded-lg border p-2.5 text-xs transition-colors peer-focus-visible:ring-2 peer-focus-visible:ring-ring border-border hover:bg-muted/60">
                    <span class="flex items-center gap-1.5 font-semibold"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-banknote h-3.5 w-3.5 shrink-0" aria-hidden="true"><rect width="20" height="12" x="2" y="6" rx="2"></rect><circle cx="12" cy="12" r="2"></circle><path d="M6 12h.01M18 12h.01"></path></svg>Refund</span>
                    <span class="mt-0.5 block text-muted-foreground">Back to the original payment method</span>
                  </label>
                </div>
                <div class="min-w-0">
                  <input type="radio" id="_R_0_-l1-exchange" class="peer sr-only" name="_R_0_-l1-outcome" checked="" />
                  <label for="_R_0_-l1-exchange" class="block cursor-pointer rounded-lg border p-2.5 text-xs transition-colors peer-focus-visible:ring-2 peer-focus-visible:ring-ring border-primary bg-card">
                    <span class="flex items-center gap-1.5 font-semibold"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-arrow-left-right h-3.5 w-3.5 shrink-0" aria-hidden="true"><path d="M8 3 4 7l4 4"></path><path d="M4 7h16"></path><path d="m16 21 4-4-4-4"></path><path d="M20 17H4"></path></svg>Exchange</span>
                    <span class="mt-0.5 block text-muted-foreground">Swap for another variant</span>
                  </label>
                </div>
                <div class="min-w-0">
                  <input type="radio" id="_R_0_-l1-credit" class="peer sr-only" name="_R_0_-l1-outcome" />
                  <label for="_R_0_-l1-credit" class="block cursor-pointer rounded-lg border p-2.5 text-xs transition-colors peer-focus-visible:ring-2 peer-focus-visible:ring-ring border-border hover:bg-muted/60">
                    <span class="flex items-center gap-1.5 font-semibold"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-badge-percent h-3.5 w-3.5 shrink-0" aria-hidden="true"><path d="M3.85 8.62a4 4 0 0 1 4.78-4.77 4 4 0 0 1 6.74 0 4 4 0 0 1 4.78 4.78 4 4 0 0 1 0 6.74 4 4 0 0 1-4.77 4.78 4 4 0 0 1-6.75 0 4 4 0 0 1-4.78-4.77 4 4 0 0 1 0-6.76Z"></path><path d="m15 9-6 6"></path><path d="M9 9h.01"></path><path d="M15 15h.01"></path></svg>Store credit<span class="rounded bg-emerald-500/10 px-1 text-[10px] font-bold text-emerald-700 dark:text-emerald-400">+10%</span></span>
                    <span class="mt-0.5 block text-muted-foreground">Issued instantly, never expires</span>
                  </label>
                </div>
              </div>
            </fieldset>
            <div class="mt-3">
              <p class="text-xs font-semibold uppercase tracking-wide text-muted-foreground">Swap for</p>
              <ul class="mt-2 flex flex-wrap gap-2">
                <li>
                  <button type="button" aria-pressed="false" class="rounded-lg border px-2.5 py-1.5 text-start text-xs transition-colors disabled:cursor-not-allowed disabled:opacity-50 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring border-border hover:bg-muted/60">
                    <span class="flex items-center gap-1.5">Graphite mesh, tall</span>
                    <span class="mt-0.5 block text-muted-foreground">Same price</span>
                  </button>
                </li>
                <li>
                  <button type="button" disabled="" aria-pressed="false" class="rounded-lg border px-2.5 py-1.5 text-start text-xs transition-colors disabled:cursor-not-allowed disabled:opacity-50 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring border-border hover:bg-muted/60">
                    <span class="flex items-center gap-1.5">Slate mesh, standard</span>
                    <span class="mt-0.5 block text-muted-foreground">Out of stock — back on 30 September</span>
                  </button>
                </li>
                <li>
                  <button type="button" aria-pressed="false" class="rounded-lg border px-2.5 py-1.5 text-start text-xs transition-colors disabled:cursor-not-allowed disabled:opacity-50 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring border-border hover:bg-muted/60">
                    <span class="flex items-center gap-1.5">Graphite mesh, headrest</span>
                    <span class="mt-0.5 block text-muted-foreground">£65.00 difference</span>
                  </button>
                </li>
                <li>
                  <button type="button" aria-pressed="false" class="rounded-lg border px-2.5 py-1.5 text-start text-xs transition-colors disabled:cursor-not-allowed disabled:opacity-50 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring border-border hover:bg-muted/60">
                    <span class="flex items-center gap-1.5">Graphite fabric, standard</span>
                    <span class="mt-0.5 block text-muted-foreground">−£40.00 difference</span>
                  </button>
                </li>
              </ul>
            </div>
          </div>
        </div>
      </div>
      <div class="rounded-xl border p-4 transition-colors border-border">
        <div class="flex items-start gap-3">
          <input type="checkbox" id="_R_0_-l2" class="mt-1 h-4 w-4 shrink-0 rounded border-border accent-primary" />
          <div class="min-w-0 flex-1">
            <label for="_R_0_-l2" class="text-sm font-medium">Halo task lamp</label>
            <p class="text-xs text-muted-foreground">Brushed steel · £145.00 · 2 in the order</p>
          </div>
        </div>
      </div>
    </div>
    <div aria-live="polite" class="mt-6 rounded-xl border border-border bg-card p-4">
      <h3 data-stress-ignore="true" class="text-sm font-semibold">Summary</h3>
      <dl class="mt-3 space-y-2 text-sm">
        <div class="flex justify-between gap-3">
          <dt class="text-muted-foreground">Lines selected</dt>
          <dd>1</dd>
        </div>
        <div class="flex justify-between gap-3">
          <dt class="text-muted-foreground">Coming back to you</dt>
          <dd class="font-medium">£0.00</dd>
        </div>
      </dl>
      <button type="button" class="mt-4 w-full rounded-lg bg-primary px-4 py-2 text-sm font-semibold text-primary-foreground transition-opacity hover:opacity-90 disabled:opacity-40 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-card">Confirm and get a return label</button>
    </div>
  </div>
</section>
  • This is rendered HTML, not a translation of the React source. The Tailwind classes carry the design and work in any framework.
  • It is one frame: the component in its initial state, with no props applied beyond the defaults.
  • This block is interactive in React — toggles, menus or form state. None of that survives here; the markup is the closed/default state and the handlers are gone. Re-wire them in your own framework.
  • Requires Tailwind, and the design tokens the classes reference (bg-card, text-muted-foreground, and so on). The template ZIPs ship a globals.css that defines them.

What each framework gets across the whole catalog — effects convert properly; this rung is markup.

For AI

The component, its props, the design tokens it expects and the command that installs it — as one prompt. Paste it into Claude, Cursor, v0 or ChatGPT and what they build around it will match the rest of the catalog instead of inventing its own system.

See the prompt

8 more blocks in After-Sale Service

All of them free to read, copy and install — no account, no locked tiles, no watermarked preview. The whole catalog is open, and so are the API and the CLI.

Browse After-Sale Service

Shipping one commercially

Copying the code is free. Putting it in client work or a paid product is what Pro is for — the licence, not the access.

  • A commercial licence for everything in the catalog
  • Unlimited bundle exports, in Vue, Svelte and Tailwind
  • One payment — no subscription, nothing to renew
Pro — $79 once

More After-Sale Service blocks

View category
Open the full page for this block

Warranty Coverage Panel

A verdict for this serial number today, with exclusions given equal weight to cover and an extension offered only while it can still be bought.

After-Sale Service259 lines1 dep
Open the full page for this block

Refund Request Form

Refunds are per line, not per order: a reason on each line that drives what is asked for next, and a running total itemising the restocking fee and the delivery rule.

After-Sale Service348 lines1 dep
Open the full page for this block

Refund Status Tracker

Where the money is and who is holding it - four stages, each naming the responsible party, including the bank stage every where-is-my-refund contact comes from.

After-Sale Service241 lines1 dep
Open the full page for this block

Refunds Overview Table

The operator queue, sorted oldest-open-first because recency is the wrong default, with blocked as a first-class status that names its blocker.

After-Sale Service277 lines1 dep
Open the full page for this block

Return Label and Instructions

The screen between approved and posted: three drop-off methods with the thing that actually blocks each one, the packing rules that get returns refused, and a date rather than a duration.

After-Sale Service285 lines1 dep
Open the full page for this block

Service Repair Request

A repair is not a return - a symptom picker that filters the service routes, warranty resolved inside the form, and an estimate with its uncertainty stated.

After-Sale Service310 lines1 dep
Open the full page for this block

Review History

The reviews you wrote and the ones you owe, with editing that is honest about being marked as edited and moderation that names the rule it applied.

After-Sale Service291 lines1 dep
Open the full page for this block

Gift Card Balance Check

A balance checker built for the two ways it goes wrong - an empty card and an expired one - answering with a transaction history rather than a bare zero.

After-Sale Service254 lines1 dep