# CookieMonster

NC in-house cookie banner that doesn't suck.

[![npm](https://img.shields.io/npm/v/@naturalcycles/cookie-monster/latest.svg)](https://www.npmjs.com/package/@naturalcycles/cookie-monster)
[![code style: prettier](https://img.shields.io/badge/code_style-prettier-ff69b4.svg?style=flat-square)](https://github.com/prettier/prettier)

## Installation

```bash
pnpm add @naturalcycles/cookie-monster
```

### Script tag (CDN)

For 3rd-party sites that don't bundle this package as an npm dependency, CookieMonster is also
served as a standalone browser bundle via jsDelivr (backed by the same npm publish, so versions
always match):

```html
<head>
  <script
    async
    src="https://cdn.jsdelivr.net/npm/@naturalcycles/cookie-monster@2.18/dist/cookiemonster.js"
  ></script>
  <script>
    document.addEventListener('CookieMonsterReady', () => {
      globalThis.cookieMonsterService.register({ language: 'en-US' })
    })
  </script>
</head>
```

See the [jsDelivr package page](https://www.jsdelivr.com/package/npm/@naturalcycles/cookie-monster)
for version pinning options.

## Usage

### Basic Setup

1. Register the custom element:

```ts
import { cookieMonsterService } from '@naturalcycles/cookie-monster'

cookieMonsterService.register()
```

2. Add the element to your HTML:

```html
<cookie-monster display></cookie-monster>
```

Skip step 2 entirely if you'd rather let CookieMonster manage the element itself — `register()`
already does, see below.

`register()` owns the full "should the banner show" decision, before the banner ever renders:

- Reads the existing `CookieConsent` cookie, if any.
- If there's no existing consent and the browser signals
  [Global Privacy Control](https://globalprivacycontrol.org/), automatically opts the user out of
  everything but necessary cookies and persists that as the consent — the banner is simply never
  shown to them.
- Otherwise, if there's still no consent on file, it opens the banner itself — creating and
  appending the `<cookie-monster>` element if one isn't already in the DOM. No
  `getConsent()`/`open()` check is needed on your side for the initial "first-time visitor" case.
- Wires up the consent audit log (see below), which every future consent decision appends to.

```ts
cookieMonsterService.register({
  domain: '.example.com', // optional Cookie `Domain` attribute, e.g. to share consent across
  // subdomains. Unset by default (host-only cookie).
  language: 'en-US',
})
```

Call `cookieMonsterService.open()`/`openSettings()` yourself only when you want to _re_-open it
later — e.g. a "Manage cookie settings" link on a Cookie Policy page (see "Opening and closing
programmatically" below).

### Reading consent

```ts
import { cookieMonsterService } from '@naturalcycles/cookie-monster'

cookieMonsterService.getConsent() // CookieMonsterConsent | undefined
cookieMonsterService.getConsentLog() // ConsentLogEntry[] | undefined - compliance/audit trail
```

`getConsent()` reads the `CookieConsent` cookie directly, so it reflects the latest decision whether
it came from the banner, settings, or GPC auto opt-out. `getConsentLog()` returns the append-only
history of consent decisions (capped at the most recent 20), stored in `localStorage` separately
from the cookie — each entry records the consent, a timestamp, which CookieMonster version the user
saw, and a snapshot of the rendered banner copy. Like `getConsent()`, it returns `undefined` rather
than an empty array when there's no log yet (or under SSR).

### Opening and closing programmatically

```ts
import { CookieMonsterView, cookieMonsterService } from '@naturalcycles/cookie-monster'

cookieMonsterService.open() // shows the banner (or whichever view was last shown)
cookieMonsterService.openSettings() // shows the settings view directly
cookieMonsterService.open({ view: CookieMonsterView.banner }) // explicitly show the banner view
cookieMonsterService.close() // hides it
```

`openSettings()` (shorthand for `open({ view: CookieMonsterView.settings })`) is the call a Cookie
Policy page's "Manage cookie settings" link should use, once `register()` has run somewhere on that
page (typically once, at bootstrap — `register()` defines the custom elements `open()` needs, in
addition to its own consent/auto-open logic). It works whether or not `register()` already
auto-opened the banner for this visitor: it creates the `<cookie-monster>` element if none is
mounted yet, or just switches an already-open one to the settings view.

### Attributes

| Attribute               | Type      | Default | Description                                |
| ----------------------- | --------- | ------- | ------------------------------------------ |
| `display`               | `boolean` | `false` | Controls visibility of the cookie banner   |
| `language` (deprecated) | `string`  | `en-US` | UI language/locale (falls back to `en-US`) |

### Language

- Default: `en-US`
- Supported locales: `en-US`, `en-GB`, `sv-SE`, `de-DE`, `fr-FR`, `es-US`, `it-IT`, `pt-BR`,
  `fi-FI`, `da-DK`, `no-NO`
- Any unsupported/invalid value falls back to `en-US`

Set it via `register({ language })` / `open({ language })` (see [Usage](#usage)).

> **Deprecated:** setting a `language` attribute directly on `<cookie-monster>` still works, but
> we're moving away from configuring the element via attributes — prefer the JS API above.

## Events

All events bubble and are composed (accessible outside Shadow DOM).

```ts
import { CookieMonsterEvent } from '@naturalcycles/cookie-monster'

const cm = document.querySelector('cookie-monster')
cm?.addEventListener(CookieMonsterEvent.cookieMonsterOnClose, (e: Event) => {
  const { consent, reason } = (e as CustomEvent<CookieMonsterOnCloseDetails>).detail
  console.log({ consent, reason })
})
```

### Available Events

| Event                           | Trigger                      | Detail                         |
| ------------------------------- | ---------------------------- | ------------------------------ |
| `cookieMonsterOnMount`          | Element mounted to DOM       | -                              |
| `cookieMonsterOnDisplay`        | `display` attribute changed  | `CookieMonsterOnDisplayDetail` |
| `cookieMonsterOnClose`          | Final consent decision made  | `CookieMonsterOnCloseDetails`  |
| `cookieMonsterOnSettingsViewed` | Settings view opened         | `CookieMonsterReasonDetail`    |
| `cookieMonsterOnBackToBanner`   | Back from settings to banner | `CookieMonsterReasonDetail`    |

### Event Details

```ts
interface CookieMonsterReasonDetail {
  reason: CookieMonsterEventReason
}

interface CookieMonsterOnCloseDetails {
  reason: CookieMonsterEventReason
  consent: CookieMonsterConsent
}

interface CookieMonsterOnDisplayDetail {
  display: boolean
}

enum ConsentSource {
  explicit = 'explicit', // the visitor made a choice in the banner/settings UI
  gpc = 'gpc', // auto opt-out, derived from the browser's Global Privacy Control signal
}

interface CookieMonsterConsent {
  necessary: boolean // Always true
  preferences: boolean
  statistics: boolean
  marketing: boolean
  source: ConsentSource
}

enum CookieMonsterDropdownDetail {
  DropdownOpen = 'DropdownOpen', // internal, used between settings dropdowns — not dispatched on <cookie-monster>
}

interface ConsentLogEntry {
  date: string
  consent: CookieMonsterConsent
  version?: string // CookieMonster's own package version
  text?: string // snapshot of the rendered banner copy
}
```

### Event Reasons

- `AcceptAll` / `SaveSettings` - User consent actions
- `BackButton` - Back button in settings
- `OpenSettings` - Settings button clicked

## TypeScript Support

The package includes full TypeScript definitions. Import types as needed:

```ts
import type {
  CookieMonsterEvent,
  CookieMonsterEventReason,
  CookieMonsterConsent,
  ConsentSource,
  CookieMonsterOnCloseDetails,
  CookieMonsterOnDisplayDetail,
  CookieMonsterReasonDetail,
  CookieMonsterDropdownDetail,
  CookieMonsterView,
  OpenCookieMonsterOptions,
  RegisterCookieMonsterOptions,
  ConsentLogEntry,
} from '@naturalcycles/cookie-monster'
```

## Development

```bash
# Run tests
pnpm test

# Run E2E tests
pnpm e2e
```
