Express checkout

Three wallet buttons as mono text marks, an or divider and a short trust row.

componentexpressanycheckoutexpress-paytrust
$npx shadcn@latest add @storekit/checkout-express
checkout-express

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/checkout-express
$pnpm dlx shadcn@latest add @storekit/checkout-express
$yarn dlx shadcn@latest add @storekit/checkout-express
$bunx --bun shadcn@latest add @storekit/checkout-express

Source

components/storekit/checkout/checkout-express.tsx
90 lines
"use client"

import * as React from "react"
import { RotateCcw, ShieldCheck, Truck } from "lucide-react"
import { cn } from "@/components/storekit/lib/cn"
import { useSkBlock } from "@/components/storekit/lib/sk-analytics"

/* Express pay row. The first wallet is the filled primary, the rest are
   bordered text marks (swap in each provider's official button in
   production), then an "or" divider and a three-up trust row. */

type Wallet = { id: string; label: string }

const WALLETS: Wallet[] = [
  { id: "express", label: "Express pay" },
  { id: "shop", label: "Shop Pay" },
  { id: "paypal", label: "PayPal" },
]

const TRUST = [
  { icon: ShieldCheck, label: "Encrypted checkout" },
  { icon: Truck, label: "Ships in 24 hours" },
  { icon: RotateCcw, label: "30-day returns" },
]

export function CheckoutExpress({
  wallets = WALLETS,
  onSelect,
  className,
}: {
  wallets?: Wallet[]
  onSelect?: (id: string) => void
  className?: string
}) {
  const sk = useSkBlock("checkout", "checkout-express")
  const [pending, setPending] = React.useState<string | null>(null)
  const pick = (id: string) => {
    sk.emit("convert", `express_${id}`)
    setPending(id)
    window.setTimeout(() => {
      setPending(null)
      onSelect?.(id)
    }, 900)
  }
  return (
    <div ref={sk.ref as React.Ref<HTMLDivElement>} {...sk.attrs} className={cn("w-full", className)}>
      <p className="text-center font-sk-mono text-[11px] uppercase tracking-[0.14em] text-sk-muted">Express checkout</p>
      <div className="mt-3 grid gap-2 sm:grid-cols-3">
        {wallets.map((w, i) => (
          <button
            key={w.id}
            type="button"
            onClick={() => pick(w.id)}
            disabled={pending !== null}
            aria-busy={pending === w.id || undefined}
            className={cn(
              "relative h-12 rounded-sk-btn border text-sm font-semibold tracking-[-0.01em]",
              "transition-[transform,background-color,opacity] duration-200 active:scale-[0.97] disabled:opacity-60",
              i === 0 ? "border-sk-ink bg-sk-ink text-sk-paper hover:bg-sk-ink/90" : "border-sk-line bg-sk-card text-sk-ink hover:bg-sk-ink/[0.04]",
              "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-sk-accent focus-visible:ring-offset-2 focus-visible:ring-offset-sk-paper"
            )}
          >
            <span className={cn(pending === w.id && "opacity-0")}>{w.label}</span>
            {pending === w.id ? (
              <span className="absolute inset-0 grid place-items-center">
                <span className="h-4 w-4 animate-spin rounded-full border-2 border-current border-t-transparent" />
              </span>
            ) : null}
          </button>
        ))}
      </div>

      <div className="my-6 flex items-center gap-4" role="separator" aria-label="or">
        <span className="h-px flex-1 bg-sk-line" />
        <span className="text-[13px] text-sk-muted">or</span>
        <span className="h-px flex-1 bg-sk-line" />
      </div>

      <ul className="grid grid-cols-3 gap-2">
        {TRUST.map(({ icon: Icon, label }) => (
          <li key={label} className="flex flex-col items-center gap-1.5 text-center text-xs text-sk-muted sm:flex-row sm:justify-center sm:text-left">
            <Icon size={16} className="shrink-0 text-sk-ink" strokeWidth={1.75} />
            {label}
          </li>
        ))}
      </ul>
    </div>
  )
}

Usage

Text marks are placeholders. Use each provider's official button in production.

Express checkout, StoreKit