# Checkbox Elements

> A drop-in web component (and React wrapper) that shows the locked, compliant automatic-renewal disclosure above your pay button with an unchecked consent checkbox, and records each consent as evidence.

`<checkbox-disclosure>` renders the disclosure for your terms — price, interval, trial, that it renews until cancelled,
how to cancel — in a bordered box at body size or larger, directly above your pay button, with an **unchecked,
required** consent checkbox. It keeps the pay button disabled until the shopper ticks the box, and records each consent
with Checkbox, so you get a `cons_…` id to store with the subscription. The wording is the same locked text the rules
engine verifies everywhere else.

## Install {#install}

1. Get the pinned script tag with Subresource Integrity from the manifest (or from `createDisclosure`, below):

```curl
curl https://app.checkbox.site/v1/elements/manifest.json
```
```node
import Checkbox from '@checkbox-site/cli';

const checkbox = new Checkbox(process.env.CHECKBOX_SECRET_KEY);

const manifest = await checkbox.elements.retrieveManifest();
console.log(manifest.script_tag);
```
```python
import os
from checkbox import Checkbox

checkbox = Checkbox(os.environ["CHECKBOX_SECRET_KEY"])

manifest = checkbox.elements.retrieve_manifest()
print(manifest["script_tag"])
```

2. Put the script tag in `<head>` and the element inside your sign-up form, directly above the pay button:

```html
<script src="https://app.checkbox.site/v1/elements/1.0.0/elements.js"
        integrity="sha384-…" crossorigin="anonymous" defer></script>

<form action="/subscribe" method="post">
  <checkbox-disclosure
    publishable-key="pk_test_..."
    merchant-name="Joybox" amount="1999" currency="usd" interval="month" trial-days="14"
    cancel-url="https://joybox.example/account/cancel"
    customer-reference="cs_test_a1b2c3">
  </checkbox-disclosure>
  <button type="submit">Start free trial</button>
</form>
```

3. Your server receives `auto_renew_consent` (the label text) and `checkbox_consent_id` (`cons_…`) with the form.
   Store the id with the subscription — for example `metadata: { checkbox_consent: 'cons_…' }` on the Stripe Checkout
   Session — or set `customer_reference` later with `updateConsent`.

`/v1/elements.js` always serves the current version (cached for 5 minutes), handy while prototyping. In production pin
`/v1/elements/<version>/elements.js` with its `integrity`; released versions never change.

## React {#react}

```tsx
import { CheckboxDisclosure } from '@subcomply/elements/react';

export function Consent({ sessionId, onConsent }: { sessionId: string; onConsent: (id: string) => void }) {
  return (
    <CheckboxDisclosure
      publishableKey={process.env.NEXT_PUBLIC_CHECKBOX_PUBLISHABLE_KEY!}
      terms={{ merchant_name: 'Joybox', amount: 1999, currency: 'usd', interval: 'month', trial_days: 14,
               cancel_url: 'https://joybox.example/account/cancel' }}
      customerReference={sessionId}
      onConsent={(consent) => onConsent(consent.id)}
    />
  );
}
```

In single-page apps that submit with `fetch`, use `recordConsent="manual"` and call `await ref.current.recordConsent()`
before creating the subscription. (The React entry is published with the SDK packages; see the
[pre-release note](https://checkbox.site/docs/sdks/node#install).)

## Server-issued disclosures {#server}

`createDisclosure` derives the terms from the site's billing connection (or the terms you pass), generates the locked
wording, runs the rules engine on your checkout with the element installed, and returns the markup to paste:

```curl
curl https://app.checkbox.site/v1/disclosures \
  -u "$CHECKBOX_SECRET_KEY:" \
  -H "Content-Type: application/json" \
  -d '{
    "site": "site_3f9a1c2e7b4d4e0f9a8b7c6d5e4f3a2b",
    "terms": {
      "merchant_name": "Joybox",
      "amount": 1999,
      "currency": "usd",
      "interval": "month",
      "trial_days": 14,
      "cancel_url": "https://joybox.example/account/cancel"
    }
  }'
```
```node
import Checkbox from '@checkbox-site/cli';

const checkbox = new Checkbox(process.env.CHECKBOX_SECRET_KEY);

const disclosure = await checkbox.disclosures.create({
  site: 'site_3f9a1c2e7b4d4e0f9a8b7c6d5e4f3a2b',
  terms: { merchant_name: 'Joybox', amount: 1999, currency: 'usd', interval: 'month', trial_days: 14, cancel_url: 'https://joybox.example/account/cancel' },
});
console.log(disclosure.verification.passes);
console.log(disclosure.element_html);
```
```python
import os
from checkbox import Checkbox

checkbox = Checkbox(os.environ["CHECKBOX_SECRET_KEY"])

disclosure = checkbox.disclosures.create(
    site="site_3f9a1c2e7b4d4e0f9a8b7c6d5e4f3a2b",
    terms={"merchant_name": "Joybox", "amount": 1999, "currency": "usd", "interval": "month",
           "trial_days": 14, "cancel_url": "https://joybox.example/account/cancel"},
)
print(disclosure["verification"]["passes"])
print(disclosure["element_html"])
```

With `disclosure="disc_…"` on the element, it loads its text from Checkbox (publishable key, Origin-checked). Keep the
terms attributes as well: they are the fallback if Checkbox cannot be reached. Without either, the element shows
"Subscription terms unavailable" and keeps the pay button disabled.

## Attributes {#attributes}

| Attribute | |
|---|---|
| `publishable-key` | `pk_test_…` / `pk_live_…`. Without it the element still works but records nothing |
| `merchant-name`, `amount` (minor units), `currency`, `interval` (`day`, `week`, `month`, `year`), `interval-count`, `trial-days`, `cancel-url` (https), `cancel-timing` (`period_end` or `immediately`) | Your terms |
| `disclosure` | `disc_…` from `createDisclosure` |
| `site` | `site_…`; optional (resolved from the page's domain), needed on `localhost` when you have several sites |
| `submit` | CSS selector of the pay button(s) to gate; default: the form's submit buttons |
| `form` | id of the form when the element is outside it |
| `customer-reference` | Your id for the subscriber (a Stripe session or customer id). Emails are refused |
| `record-consent` | `submit` (default), `change` (when ticked) or `manual` |
| `placement` | `auto` moves the element directly above the pay button |
| `name`, `consent-id-name` | Form field names (default `auto_renew_consent`, `checkbox_consent_id`) |
| `api-base` | Defaults to the origin the script was loaded from |

Events (they bubble): `checkbox:ready`, `checkbox:change` `{ consented }`, `checkbox:consent` `{ id, consent }`,
`checkbox:guardrail` `{ violations, messages, fallbackApplied }`, `checkbox:error` `{ code, message }`. Methods:
`isConsented()`, `recordConsent({ customerReference?, metadata? })`, `consentId`, `checkGuardRails()`, `refresh()`.

A Checkbox outage never blocks a sale: if recording fails or takes over 6 seconds, the form submits without the id and
`checkbox:error` fires.

## Guard rails {#guard-rails}

Theme it with CSS custom properties (`--checkbox-font-size`, `--checkbox-text-color`, `--checkbox-border-color`,
`--checkbox-background`, `--checkbox-accent-color` and others). After rendering, and on resize, the element measures
what the shopper actually sees with the rules engine's own thresholds — font size at least the page's body size,
WCAG AA contrast, visible, set off by a border, above the pay button and within 250px of it. If your theme or page CSS
breaks size, contrast or the border, it restyles itself with safe inline styles, logs a warning and fires
`checkbox:guardrail`. A checkbox ticked by a script is not consent: the element unticks it and reports `prechecked`.

## Content-Security-Policy {#csp}

The element uses no `eval`, no inline styles or `<style>` elements and no `innerHTML`. Allow
`script-src https://app.checkbox.site` and `connect-src https://app.checkbox.site`.

## Consent records {#consents}

Each consent stores the disclosure text and label shown and their hashes, whether they match the locked wording, the
terms, the checkbox state (checked, initially checked, required), server time, the page URL, Origin, a hash of the
User-Agent, the element version and its on-screen measurement.

The publishable key is public, so a browser consent is a client claim: the `customer-reference` the page sends is kept as
`client_reference` (unverified), never as `customer_reference`. Bind the consent to the real subscription from your
server: the form carries `checkbox_consent_id`; call [`updateConsent`](https://checkbox.site/docs/api/elements#updateConsent) with a secret key
and the subscription id (or record the consent from your server with `createConsent` and a secret key). Browser consents
are limited per site and day; corroborating page captures are stored for the first 50 a day. Origins match the site's
own host and its subdomains; on shared hosts (`*.vercel.app`, `*.netlify.app`, `*.onrender.com`, `*.github.io`, …)
each customer's subdomain is its own site. The full record goes into the
site's [evidence chain](https://checkbox.site/docs/concepts/evidence), and `consent.created` is sent to your [webhooks](https://checkbox.site/docs/webhooks).
List them with [`listConsents`](https://checkbox.site/docs/api/elements#listConsents).
