Skip to content

feat: cookie consent management and accessibility fixes (Iubenda findings) - #528

Draft
victhorbi wants to merge 1 commit into
mainfrom
claude/iubenda-compliance-accessibility-xi69ok
Draft

feat: cookie consent management and accessibility fixes (Iubenda findings)#528
victhorbi wants to merge 1 commit into
mainfrom
claude/iubenda-compliance-accessibility-xi69ok

Conversation

@victhorbi

Copy link
Copy Markdown
Collaborator

Addresses the Iubenda compliance report. I checked each finding against the code first — three of the seven are real but cannot be closed in this repository, and one is only partly as reported. docs/Compliance.md records the disposition of every finding plus the service inventory the policy pages need.

Findings — what turned out to be real

# Finding Verdict
1 Privacy Policy misses services Real, not fixable here — policy is hosted at vevote.vechain.org/privacy, outside this repo
2a Cookie Policy misses trackers Real, not fixable here — same
2b Missing cookie banner Real, fixed
3 Images missing alt text Partly real, fixed — only 1 image; the actual gap was unlabelled icon buttons
4 European Accessibility Act Real as a direction, fixed what's concrete — scope is a legal call
5 Accept/Reject equal prominence Real, fixed
6 Data-collection notice + "Do Not Sell" Real, fixed mechanically — applicability is a legal call
7 Consent must be recorded and producible Real, partly fixed — server-side archival still missing

Consent (2b, 5, 6, 7)

The underlying problem was concrete: Mixpanel initialised at module import with persistence: "localStorage" and ignore_dnt: true, so it collected before any consent existed and deliberately overrode Do Not Track. Under ePrivacy Art. 5(3) the consent requirement covers any device storage, not just HTTP cookies — so this applied even though VeVote sets no cookies of its own.

  • CookieConsentProvider owns the decision, persists it, and re-asks when the policy version changes or the consent is over a year old.
  • Mixpanel is now lazily initialised and only after the analytics category is granted. Withdrawal opts out, clears its persistence, and removes the analytics-only localStorage keys (mixpanel_user_id, vevote_last_tracked_wallet).
  • Accept all and Reject all use identical variant, size and width — refusing is no harder than accepting (EDPB dark-pattern guidance). Nothing is pre-ticked.
  • Footer carries a persistent Cookie Preferences entry and the CCPA/CPRA Do Not Sell or Share My Personal Information opt-out.
  • Global Privacy Control and Do Not Track are honoured without asking.
  • Every decision is stored with id, timestamp, policy version, granted categories, method and context; the user can download that record from the preference centre.

Accessibility (3, 4)

WCAG 2.1 level A failures found and fixed — the whole app had 3 aria-labels and zero landmarks before this:

SC Issue
1.1.1 footer-image.png had no alt — decorative, so alt=""
4.1.2 Icon-only buttons with no accessible name: table pagination prev/next, search clear, number increment/decrement, delete voting option
3.3.2 Search field labelled only by its placeholder
2.4.1 No way past the fixed navbar + hero repeated on every route — added a skip link and a <main> target
2.4.2 <title> was the constant "VeVote" on every route
1.3.1 nav, main and footer all rendered as div

Checked and already correct, no change needed: proposal cards are keyboard-operable, Chakra IconButton usages already carry aria-label, modals get focus trapping from Chakra, and <html lang="en"> is right because en is the only locale shipped.

Still open — needs a decision or a backend

  • Policy texts (1, 2a). Only closable by editing the published policies. The inventory in docs/Compliance.md is the input: Mixpanel and Google Fonts are the notable gaps, and the connected wallet address sent to Mixpanel is personal data.
  • Google Fonts is hot-linked from index.html, sending every visitor's IP to Google. Self-hosting removes the issue rather than merely disclosing it — left out as it touches build config.
  • Server-side consent proof (7). Records live only in the user's browser today. A full fix needs Iubenda's Consent Database or a first-party endpoint.
  • CCPA/CPRA (6) and EAA scope (4) are legal determinations, not code ones.
  • Not audited: colour contrast, and the drag-and-drop voting-option reorder has no keyboard alternative (SC 2.1.1).

Testing

  • test/cookieConsent.test.tsx — 9 tests covering the consent state machine (default-deny, accept/reject/granular, proof record, version + age expiry, GPC). All pass.
  • tsc --noEmit and eslint clean on all changed files.
  • Not run: the full vite build and the two pre-existing test files. They need packages/contracts artifacts, and hardhat compile cannot download the solc list through this sandbox's TLS proxy. This is a pre-existing environment limit, not a regression — those tests fail on main here too. CI should be treated as the real check.

🤖 Generated with Claude Code

https://claude.ai/code/session_01Q3MnNPUUFzQWGMX46S472o


Generated by Claude Code

Addresses the Iubenda compliance report. Only the findings that were
verified against the code are changed here; docs/Compliance.md records
the disposition of each one, including the three that cannot be closed
in this repository.

Consent (findings 2b, 5, 6, 7)

Mixpanel previously initialised at module import with persistence in
localStorage and ignore_dnt: true, so it collected before any consent
existed and overrode Do Not Track. Under ePrivacy Art. 5(3) the consent
requirement covers any device storage, not only HTTP cookies, so this
applied even though VeVote sets no cookies of its own.

- CookieConsentProvider owns the decision, persists it, and re-asks when
  the policy version changes or the consent is over a year old.
- Mixpanel is now lazily initialised and only after the analytics
  category is granted; withdrawal opts out and clears its persistence.
  The analytics-only localStorage keys are removed on withdrawal too.
- Banner gives Accept all and Reject all identical variant, size and
  width so refusing is no harder than accepting.
- Footer carries a persistent Cookie Preferences entry and the CCPA/CPRA
  Do Not Sell or Share My Personal Information opt-out.
- Global Privacy Control and Do Not Track are honoured without asking.
- Every decision is stored with id, timestamp, policy version, granted
  categories, method and context, and the user can download that record.

Accessibility (findings 3, 4)

WCAG 2.1 level A failures found in the audit:

- 1.1.1: footer-image.png had no alt; it is decorative, so alt="".
- 4.1.2: icon-only buttons had no accessible name (table pagination,
  search clear, number increment/decrement, delete voting option).
- 3.3.2: the search field was labelled only by its placeholder.
- 2.4.1: no way to bypass the fixed navbar and hero repeated on every
  route; added a skip link and a <main> target.
- 2.4.2: <title> was the constant "VeVote" on every route.
- 1.3.1: no landmarks anywhere; nav, main and footer were all divs.

Proposal cards, Chakra IconButton usages, modal focus handling and
<html lang> were checked and already correct.

Adds test/cookieConsent.test.tsx covering the consent state machine.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q3MnNPUUFzQWGMX46S472o
@sonarqubecloud

Copy link
Copy Markdown

@github-actions

Copy link
Copy Markdown

🚀 Preview environment deployed!

Preview URL: https://preview.vevote-dev.vechain.org/claudeiubenda-compliance-accessibility-xi69ok/

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants