@cdr-kit/react-ui/Components/Unlockable

<UnlockablePill>

Inline "free to read, pay to unlock" paywall. Wrap any anchor text in a pill; the click opens a floating card that runs the full CDR subscribe → threshold-read → decrypt flow.
import { UnlockablePill } from "@cdr-kit/react-ui"
live showcaseSee <UnlockablePill> in a real blog post →Five live unlocks across text, image, and prose attachments. Mock CDR — no wallet, no chain.

Live preview

mock kit

Trouble at the Lake House

Arlo Vance told the press he was alone in Tahoe to write. But disagrees — and the timeline in the official record doesn't add up.

The first inconsistency was the photograph itself. shows a side of the boat-house that no one ever admitted existed.

What follows is the part the estate fought to suppress — — written in his own hand the morning after.

import { CdrProvider } from "@cdr-kit/react";
import { createMockCdrKit } from "@cdr-kit/core";
import { UnlockablePill } from "@cdr-kit/react-ui";

<CdrProvider mockKit={createMockCdrKit()}>
<p>
  Arlo Vance told the press he was alone, but{" "}
  <UnlockablePill uuid={4242} priceLabel="3 $IP">
    the woman beside him on the dock
  </UnlockablePill>{" "}
  disagrees.
</p>
</CdrProvider>

Headless variant

Prefer to render your own UI? useUnlockable exposes the entire state machine — status, data, error, isOpen, open/close, request, reset. The styled UnlockablePill is a 60-line wrapper on top of it.

Use the hook for full control. Snippet on the Code tab.
import { useUnlockable } from "@cdr-kit/react";

function CustomPill({ uuid }) {
const { status, data, isOpen, open, close, request } =
  useUnlockable({ uuid, mode: "subscribe" });

return (
  <span onClick={open}>
    [click to unlock]
    {isOpen && (
      <div role="dialog">
        {status === "ready" ? render(data) : (
          <button onClick={() => request()}>Pay 5 $IP</button>
        )}
      </div>
    )}
  </span>
);
}

Props

PropTypeDefaultDescription
uuidrequirednumberThe CDR vault holding the encrypted attachment.
childrenrequiredReactNodeThe anchor text (stays plaintext / SEO-visible — it's the teaser, not the secret).
priceLabelstring"5 $IP"Human-readable price shown in the unlock card.
mode"subscribe" | "access""subscribe"Subscribe runs the paywall flow; access skips payment (open/tier-gate vaults where the reader already has rights).
titleReactNode"Unlock to reveal"Card header title.
subtitleReactNodeCard header subtitle, e.g. "1 photo · attached".
unlockedRenderer(bytes: Uint8Array) => ReactNodeunlockedAutoRender the decrypted bytes. Default auto-detects PNG/JPEG/WebP/GIF, falls back to UTF-8 text, then to a download link.
subscriptionConditionHexOverride the default deployed SubscriptionCondition address.

Mobile fallback

Below 560px the floating popover collapses to a bottom-sheet — the card pins to the bottom of the viewport with a safe-area-aware inset. No additional code; the same UnlockablePill handles both.

The anchor is plaintext — that's intentional

The text inside <UnlockablePill> is a public teaser. Search engines and inspect-element will see it. The encrypted payload is what the vault holds — an image, a file, a hidden paragraph — and that only exists in the DOM after a successful read.

This matches the onscroll.app pattern: highlighted phrases anchor the unlock UX, attached content is the secret. If you want the anchor itself to be the secret, use <VaultGate> instead.