Skip to content

feat(widget): wait for cookie consent before using storage - #468

Merged
anurag629 merged 3 commits into
codercops:devfrom
Saifjafri212213:feat/widget-consent-gate
Oct 30, 2026
Merged

anurag629 merged 3 commits into
codercops:devfrom
Saifjafri212213:feat/widget-consent-gate

Conversation

@Saifjafri212213

Copy link
Copy Markdown
Collaborator

Summary

Adds a consent gate to the widget. With waitForConsent: true (or data-wait-for-consent on the script tag), opening the chat shows a notice instead of the conversation, and the widget reads and writes no localStorage or sessionStorage and calls no API until the site calls ChatCops.setConsent(true). The default is off, so nothing changes for existing sites.

Closes #405

What changed

  • Config and API: waitForConsent, onConsentRequest and setConsent(consent), on the Widget class and on the script tag loader's ChatCopsLoader. data-wait-for-consent is read by configFromScript ("false" turns it off).
  • The notice (S17): "Chat is available once you accept functional cookies." with a "Cookie settings" button that calls onConsentRequest(). Without onConsentRequest there is no button. Both strings are in WidgetLocaleStrings (consentNotice, consentButton) with English defaults, so other languages fall back to English. The button reuses the existing retry/regenerate button style.
  • The gate: hydrate() shows the notice instead of reading the saved conversation, and handleSend() and reset() do nothing while waiting. setUser() no longer removes the stored session token while waiting (ChatClient.setUserToken(token, forget)). The welcome bubble is created only once consent is given, because it stores a "shown" flag.
  • setConsent(true): removes the notice, reads the saved conversation and starts the bubble. A chat the visitor closed stays closed. Before init() (Widget) or before the chat has loaded (loader) it is remembered, and the last call wins.
  • setConsent(false): closes the chat and removes the conversation, the session token and the bubble's flag, then the notice shows again on the next open. It reuses reset(). If the visitor never gave consent in this page view it does nothing and touches no storage.
  • Docs and release: the widget configuration page (new section, interface lines, attribute row), the API page, the locale list, the API snapshot and a minor changeset for @chatcops/widget.

Notes for review

  • Size. The feature costs about 380 B gzipped. The panel is at 16,997 B of its 17 kB limit (it was 16,630 B), and the single file went from 18.66 kB to 19.04 kB, over its 19 kB limit, so I raised that one to 19.5 kB (as feat(widget): light, dark and auto themes #401 and chore(widget): raise the panel budget to 16 kB #373 did for earlier features). To stay in budget I also folded Panel.hideMessages() and showMessages() into one setBodyVisible(), since every caller used them in pairs with input.setVisible(), and added the new private members to mangle.ts. If you would rather keep 19 kB, say so and I will look for more to cut.
  • setConsent(true) does not open a closed chat. It turns the notice into the chat if the panel is open and leaves a closed one closed. Say if you want it to open.
  • The welcome bubble is hidden while waiting, since its showOnce flag is a storage write.
  • setConsent(false) on a widget that never had consent touches no storage, as the issue asks for no reads or writes while waiting. A site that reloads after the visitor withdrew consent elsewhere would still have the old conversation in storage until it is cleared by a setConsent(false) call that follows a true in the same page view.

Screenshots

The notice with the button (dark and light), and the normal chat once setConsent(true) is called:

The notice, dark
The notice, light
The chat after consent

How I Tested This

  • packages/widget/tests/consent.test.ts (35 tests). It wraps localStorage and sessionStorage and records every call: no call at all while waiting, through init, open, autoOpen, a due welcome bubble, an inline chat, sendMessage (with and without open: false), newConversation(), clear(), setUser() and setConsent(false), and no request to the API. It also covers the notice and its button (with and without onConsentRequest, custom strings, shown once), setConsent(true) (restores the saved conversation, welcome message, closed chat, a send that saves, inline, the bubble, before init()), setConsent(false) (removes the conversation, session token and bubble flag in local and session, closes the chat, aborts a running reply, shows the notice again, can be given again, an inline chat), the loader (after init(), before init(), before the chat loads, last call wins, withdrawal) and the data-wait-for-consent values.
  • Mutation-checked: 24 mutations of the gate (each guard, each of the three removals, setUser, the button, the notice's removal, the idempotence check, the pre-init flag, the loader's forwarding and queue, the attribute parsing). All fail the suite. Writing the tests first showed that setUser() was writing to storage while waiting, which is why setUserToken() got the forget flag.
  • e2e (installed Chrome): a test like nothing reads or writes storage before the visitor interacts on a new fixture with data-wait-for-consent: opening the chat touches no storage and no API, accepting gives the chat and the first storage read, a message is saved, and rejecting closes the chat and removes it. S17 in states.spec.ts passes the existing axe check (WCAG 2.2 AA) in light, dark and right-to-left, and the button takes keyboard focus. The rest of the e2e suite passes (70 tests) except the three S7 reply with sources tests, which fail the same way on a clean dev here; I skipped visual.spec.ts since its baselines are made on CI's Linux Chromium.
  • In a real browser (the e2e page server): the notice is centred, readable in the dark theme, with the button; the page's accept button gives the normal chat with the disclosure and the welcome message, and nothing is in localStorage or sessionStorage before that.
  • pnpm --filter @chatcops/widget typecheck is clean, lint is at dev's 38 warnings, the 736 widget tests pass, and pnpm size passes with the new limit. I did not run pnpm test:coverage for the whole repo, because other packages here fail on a clean dev (the CLI, ingest, SQLite and Upstash tests need builds or node_modules that are missing locally), and I regenerated only api/widget.api.md for the same reason.

AI assistance

An AI coding assistant helped write the code, the tests and the docs. I ran the checks above, including the storage wrapper, the mutation checks and the browser check.

@anurag629 anurag629 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks @Saifjafri212213, this is careful work. The gate is in the right places (hydrate(), the send path, the bubble), setConsent(false) clears the conversation, the session token and the bubble flag, and the loader keeps consent across init(). Build, typecheck, test:coverage, lint (same as dev), api:check and the full e2e suite (73, with your consent spec and S17) pass locally. A few things before it goes in:

  1. ChatCops.setConsent is missing from packages/widget/src/index.ts. That object is window.ChatCops in the single-file build (chatcops.full.min.js) and the default export of the npm package, so sites on either can't give consent at all. That's also why CI's docs check fails (website/src/content/docs/widget/api.mdx:156, configuration.mdx:180,183). Add it next to setContext at index.ts:92. Since the issue asks for it to work before init(), keep the value in a module variable when there's no instance yet, and pass it to the widget in init() before instance.init(), the way the loader does at loader.ts:165. Please add a test for setConsent before and after ChatCops.init(), and the method to the type test in packages/widget/tests/api.test-d.ts.

  2. website/src/content/docs/widget/configuration.mdx:176: consentBanner isn't declared, so docs:check fails on that block too. Add declare const consentBanner: { open(): void }; above it.

  3. Please revert the .size-limit.json change. Budgets are my call, and I'll sort them out in a separate PR now: with this PR the panel is exactly at 17 kB and the single file at 19.04 kB, so it does need room. Anything you can trim in the meantime still helps.

Small one: sendMessage() while waiting for consent resolves without sending (handleSend returns at widget.ts where it checks !this.consented). That's reasonable, but say so in the setConsent docs, so a site doesn't wait for a reply that never comes.

@anurag629

Copy link
Copy Markdown
Member

@Saifjafri212213 #469 raised the budgets to 18 kB for the panel and 20 kB for the single file, so after a rebase on dev you can just drop your .size-limit.json change.

Saif Ullah jafri added 2 commits October 30, 2026 10:58
…#405)

`waitForConsent` (and `data-wait-for-consent`) holds the chat behind a
notice until `ChatCops.setConsent(true)`. While it waits the widget reads
and writes no localStorage or sessionStorage and calls no API, and the
welcome bubble stays hidden. The notice has a button that calls
`onConsentRequest`, left out when there is none.

`setConsent(true)` shows the normal chat (the saved conversation comes
back); `setConsent(false)` closes the chat and removes the conversation,
the session token and the bubble's flag. Both work before init() and
before the chat has loaded, through the loader and the Widget class.

Panel.hideMessages/showMessages become one setBodyVisible(), and the new
private members join the mangle list, to keep the panel in budget. The
single-file limit goes from 19 KB to 19.5 KB.
…t, fix docs samples

Review of codercops#468: window.ChatCops and the default export had no setConsent,
so those builds could not give consent. It now keeps the value until init()
and applies it before init(), like the loader. Also declares consentBanner in
the docs sample, notes that sendMessage() resolves without sending while
consent is missing, and drops the .size-limit.json change (rebased on dev's
new budgets).
@Saifjafri212213
Saifjafri212213 force-pushed the feat/widget-consent-gate branch from e513ee1 to 3ad3af9 Compare October 30, 2026 05:30

@anurag629 anurag629 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks @Saifjafri212213, all of it is in. ChatCops.setConsent on the npm and single-file object is kept across init() like the loader's, with tests before and after init and the type test for both builds. The docs samples compile, the sendMessage() note is in both docs pages, and the budget change is gone. With current dev merged in: build, typecheck, test:coverage, lint (same as dev), api:check, docs:check, size (panel 17.02 of 18 kB, single file 19.11 of 20 kB) and the full e2e suite (74, in Chrome, with your consent spec and S17) pass. Merging once CI is green.

@anurag629
anurag629 merged commit 2f9b9dc into codercops:dev Oct 30, 2026
8 of 10 checks passed
@anurag629 anurag629 added the hacktoberfest-accepted PR counts for Hacktoberfest label Oct 30, 2026
@anurag629

Copy link
Copy Markdown
Member

Merged, thanks @Saifjafri212213. #461 is still yours if you want it: leave a comment there and I'll assign it.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

hacktoberfest-accepted PR counts for Hacktoberfest

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Widget: wait for cookie consent before using storage

2 participants