npm package: @sudeep7353/grantly-react-consent
Description: React implementation of the Grantly consent widget
Author: Gfox
Latest version: 0.1.7

# @gfox/grantly-react-consent

[![npm version](https://img.shields.io/npm/v/@gfox/grantly-react-consent.svg)](https://www.npmjs.com/package/@gfox/grantly-react-consent)
[![npm license](https://img.shields.io/npm/l/@gfox/grantly-react-consent.svg)](https://www.npmjs.com/package/@gfox/grantly-react-consent)
[![npm downloads](https://img.shields.io/npm/dm/@gfox/grantly-react-consent.svg)](https://www.npmjs.com/package/@gfox/grantly-react-consent)

A React component library that simplifies user consent management, offering components for both consent collection and revocation. Built with TypeScript support and fully customizable styling.

## Table of Contents

- [Installation](#installation)
- [Quick Start](#quick-start)
- [Requirements](#requirements)
- [TypeScript Support](#typescript-support)
- [ConsentWidget](#consentwidget)
  - [Props](#consentwidget-props)
  - [Display Modes](#display-modes)
  - [Ref Methods](#ref-methods)
  - [Status Callbacks](#status-callbacks)
  - [Styling](#consentwidget-styling)
- [WithdrawConsentWidget](#withdrawconsentwidget)
  - [Props](#withdrawconsentwidget-props)
  - [Display Modes](#withdrawconsentwidget-display-modes)
  - [Status Callbacks](#withdrawconsentwidget-status-callbacks)
  - [Styling](#withdrawconsentwidget-styling)
- [Examples](#examples)
- [Troubleshooting](#troubleshooting)
- [License](#license)

## Installation

Install the package using your preferred package manager:

```bash
npm install @gfox/grantly-react-consent
```

```bash
yarn add @gfox/grantly-react-consent
```

```bash
pnpm add @gfox/grantly-react-consent
```

## Quick Start

Get started with the Consent Widget in just a few steps:

1. **Install the package** (see installation above)
2. **Import the component** in your React application
3. **Configure the required props**

### Basic Example

```jsx
import React from 'react';
import { ConsentWidget } from '@gfox/grantly-react-consent';

function App() {
  const handleStatusChange = (data) => {
    console.log('Consent status changed:', data.status, data);
    
    if (data.status === 'accepted') {
      console.log('User accepted purposes:', data.acceptedPurposes);
    } else if (data.status === 'already-consented') {
      console.log('User already consented');
    }
  };

  return (
    <div className="App">
      <ConsentWidget
        clientId="your-client-id"
        xUserId="user-123"
        onStatusChange={handleStatusChange}
      />
    </div>
  );
}

export default App;
```

## Requirements

- **React** >= 17.0.0
- **React-DOM** >= 17.0.0

These are peer dependencies and must be installed in your project.

## TypeScript Support

This package includes full TypeScript definitions. No additional `@types` package is required.

```tsx
import { ConsentWidget, WithdrawConsentWidget, ConsentStatusDetail, WithdrawConsentStatusDetail } from '@gfox/grantly-react-consent';

function App() {
  const handleStatusChange = (data: ConsentStatusDetail) => {
    // TypeScript will provide full type checking and autocomplete
    if (data.status === 'accepted') {
      console.log(data.acceptedPurposes); // TypeScript knows this exists
    }
  };

  return (
    <ConsentWidget
      clientId="your-client-id"
      xUserId="user-123"
      onStatusChange={handleStatusChange}
    />
  );
}
```

## ConsentWidget

A React component for managing user consent and data agreements with multiple display modes and customizable styling.

### Description

The Consent Widget is a flexible React component that handles user consent management for data processing purposes. It supports three display modes: popup (default), inline, and plain, making it suitable for various integration scenarios. The widget automatically fetches consent agreements from an API and provides an intuitive interface for users to select their consent preferences.

**Key Features:**
- Automatic scroll locking in popup mode
- Consent modification mode with smart button enabling
- Programmatic consent submission via ref
- Comprehensive status callbacks
- Multi-language support via `acceptLanguage` prop
- Markdown support for agreement text
- XSS protection via DOMPurify
- Authentication token support (via prop or URL query parameter) - when provided, creates active consent directly

### ConsentWidget Props

| Prop | Type | Required | Default | Description |
|------|------|----------|---------|-------------|
| `clientId` | `string` | Required | - | Client identifier for the consent request |
| `xUserId` | `string` | Required | - | User identifier |
| `authToken` | `string` | Optional | - | Authentication token for consent submission. Can also be provided via URL query parameter `?token=...`. When provided, an active consent is created directly. Otherwise, a draft consent is created |
| `acceptLanguage` | `string` | Optional | `"en"` | Language code for content localization (e.g., "en", "en-US", "hi") |
| `displayMode` | `string` | Optional | `""` | Display mode flags (see Display Modes below) |
| `modifyConsent` | `boolean` | Optional | `false` | Enable consent modification mode. When enabled, the submit button is only enabled when consent has been modified |
| `onStatusChange` | `function` | Optional | - | Callback for consent status updates (see Status Callbacks below) |

### Display Modes

The `displayMode` prop accepts space-separated flags:

| Flag | Description |
|------|-------------|
| **Default (popup)** | Modal overlay with full consent interface. Automatically locks background scroll when open |
| `plain` | Inline display without modal styling. No scroll locking |
| `inline` | Compact inline display (hides title, agreement text, and action buttons) |
| `no-agreement-title` | Hide the agreement title |
| `no-agreement-text` | Hide the agreement description text |
| `no-change-summary` | Hide purpose change summaries (shown by default in all modes except `inline`) |

**Note:** In default (popup) mode, the widget automatically locks background scrolling when the modal is open and restores it when closed. This ensures a clean user experience where users can only interact with the consent modal when it's open.

**Examples:**
```jsx
// Modal popup (default)
<ConsentWidget displayMode="" />

// Plain inline mode
<ConsentWidget displayMode="plain" />

// Compact inline with hidden title
<ConsentWidget displayMode="inline no-agreement-title" />

// Plain mode with hidden change summary
<ConsentWidget displayMode="plain no-change-summary" />
```

### Ref Methods

The component supports ref forwarding and exposes the following method:

| Method | Description |
|--------|-------------|
| `submitConsent()` | Programmatically submit the consent. Returns a Promise that resolves with the consent result |

**Example:**
```jsx
import { useRef } from 'react';
import { ConsentWidget } from '@gfox/grantly-react-consent';

const consentRef = useRef();

const handleSubmit = async () => {
  try {
    const result = await consentRef.current.submitConsent();
    console.log('Consent submitted:', result);
  } catch (error) {
    console.error('Failed to submit:', error);
  }
};

<ConsentWidget
  ref={consentRef}
  clientId="your-client-id"
  xUserId="user-123"
  // ... other props
/>
```

### Status Callbacks

The `onStatusChange` callback receives status updates with the following structure:

```tsx
onStatusChange({
  status: 'in-progress' | 'already-consented' | 'accepted' | 'declined' | 'error',
  clientId: string,
  userId: string, // Note: This is 'userId' in the callback, not 'xUserId'
  // Conditional fields based on status:
  acceptedPurposes?: string[], // present when status is 'accepted' or 'declined'
  agreementVersionId?: string, // present when status is 'accepted' or 'declined'
  actionId?: string, // present when status is 'accepted' (ID to use for completing the consent)
  consentState?: string, // present when status is 'accepted' (backend consent state: 'draft' or 'active')
  expiresAt?: string | null, // present when status is 'accepted' (consent expiration date if applicable)
  error?: string // ONLY present when status is 'error' (contains error message)
})
```

| Status | Description | Additional Fields |
|--------|-------------|-------------------|
| `"in-progress"` | Widget is fetching consent agreement data | None |
| `"already-consented"` | User has already consented (204 response). Widget won't open in popup mode | None |
| `"accepted"` | User has successfully submitted consent | `acceptedPurposes`, `agreementVersionId`, `actionId`, `consentState`, `expiresAt` |
| `"declined"` | User closed the modal without submitting consent | `acceptedPurposes: []`, `agreementVersionId` |
| `"error"` | An error occurred during fetch or submission | `error` (contains error message string) |

### ConsentWidget Styling

The widget exposes CSS custom properties for complete customization:

#### Base Properties
```css
--consent-widget-width: 100%;
--consent-widget-font-family: inherit;
--consent-widget-font-size: inherit;
--consent-widget-text-color: #f9fafb;
```

#### Modal Properties
```css
--consent-widget-modal-bg: rgba(0, 0, 0, 0.8);
--consent-widget-modal-content-bg: #1f2937;
--consent-widget-modal-z-index: 9999;
```

#### Container Properties
```css
--consent-widget-container-bg: #1f2937;
--consent-widget-container-padding: 2rem;
--consent-widget-container-border-radius: 16px;
--consent-widget-container-max-width: 800px;
```

#### Button Properties
```css
--consent-widget-primary-bg: #8b5cf6;
--consent-widget-secondary-bg: transparent;
--consent-widget-button-padding: 0.75rem 1.5rem;
--consent-widget-button-border-radius: 6px;
```

#### Spacing Properties
```css
--consent-widget-spacing-xs: 0.25rem;
--consent-widget-spacing-sm: 0.5rem;
--consent-widget-spacing-md: 1rem;
--consent-widget-spacing-lg: 1.5rem;
--consent-widget-spacing-xl: 2rem;
```

Override these variables in your CSS to customize the widget appearance:

```css
.consent-widget {
  --consent-widget-primary-bg: #007bff;
  --consent-widget-container-border-radius: 8px;
  --consent-widget-text-color: #333;
}
```

## WithdrawConsentWidget

A React component that allows users to view and withdraw their existing consents. The widget displays consent information in a clean, customizable interface with support for both modal and inline display modes.

**Key Features:**
- Automatic modal opening when consents are loaded (default mode)
- ESC key support to close modal
- Automatic scroll locking in modal mode
- Status callback deduplication to prevent duplicate events
- Auto-detection of display mode from consent data
- Multi-language support via `acceptLanguage` prop
- Markdown support for agreement text
- XSS protection via DOMPurify

**Note:** The widget returns `null` (doesn't render) when loading or when there are no consents. Use the `onStatusChange` callback to handle these states.

### WithdrawConsentWidget Props

| Prop | Type | Required | Default | Description |
|------|------|----------|---------|-------------|
| `clientId` | `string` | Required | - | Client identifier for the application |
| `userId` | `string` | Required | - | User identifier |
| `displayMode` | `string` | Optional | `""` | Display mode flags (see Display Modes) |
| `acceptLanguage` | `string` | Optional | `"en"` | Language code for content localization (e.g., "en", "en-US", "hi") |
| `onStatusChange` | `function` | Optional | - | Callback for widget status changes (see Status Callbacks below) |
| `onModifyConsent` | `function` | Optional | - | Callback when user clicks modify consent. Receives the consent object as parameter |

### WithdrawConsentWidget Display Modes

The `displayMode` prop accepts space-separated flags:

| Flag | Description |
|------|-------------|
| **Default (modal)** | Modal overlay with full interface. Automatically opens when consents are loaded. Locks background scroll and supports ESC key to close |
| `plain` | Inline display without modal. No scroll locking |
| `no-agreement-title` | Hide agreement titles |
| `no-agreement-text` | Hide agreement descriptions |
| `no-consent-timestamps` | Hide consent timestamps (consented_at, expires_at, withdrawn_at) |
| `no-modify-button` | Hide modify consent buttons |

**Note:** 
- In default (modal) mode, the widget automatically opens the modal when consents are successfully loaded
- The modal locks background scrolling when open and restores it when closed
- Press ESC key to close the modal in default mode
- The widget automatically determines the display mode from the first consent's `display_mode` if not explicitly provided via props

**Examples:**
```jsx
// Modal mode (default)
<WithdrawConsentWidget displayMode="" />

// Plain inline mode
<WithdrawConsentWidget displayMode="plain" />

// Plain mode with hidden titles and timestamps
<WithdrawConsentWidget displayMode="plain no-agreement-title no-consent-timestamps" />

// Modal mode without modify buttons
<WithdrawConsentWidget displayMode="no-modify-button" />
```

### WithdrawConsentWidget Styling

The widget exposes CSS variables for complete customization. All variables are prefixed with `--wcw-`:

#### Layout & Spacing
```css
--wcw-max-width: 800px;
--wcw-padding: 20px;
--wcw-margin: 0 auto;
--wcw-border-radius: 8px;
--wcw-spacing-xs: 4px;
--wcw-spacing-sm: 8px;
--wcw-spacing-md: 12px;
--wcw-spacing-lg: 16px;
--wcw-spacing-xl: 20px;
--wcw-spacing-2xl: 24px;
--wcw-spacing-3xl: 40px;
```

#### Colors
```css
--wcw-bg-primary: #ffffff;
--wcw-bg-secondary: #f9fafb;
--wcw-text-primary: #1f2937;
--wcw-text-secondary: #4b5563;
--wcw-primary: #8b5cf6;
--wcw-danger: #8b5cf6;
--wcw-success: #059669;
```

#### Typography
```css
--wcw-font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
--wcw-font-size-base: 14px;
--wcw-font-size-small: 12px;
--wcw-font-size-large: 18px;
--wcw-font-size-xl: 24px;
--wcw-font-weight-normal: 400;
--wcw-font-weight-medium: 500;
--wcw-font-weight-semibold: 600;
```

#### Shadows & Transitions
```css
--wcw-shadow-sm: 0 2px 8px rgba(0, 0, 0, 0.1);
--wcw-shadow-md: 0 4px 12px rgba(0, 0, 0, 0.1);
--wcw-shadow-lg: 0 8px 25px rgba(0, 0, 0, 0.15);
--wcw-transition-fast: 0.15s ease;
--wcw-transition-normal: 0.2s ease;
--wcw-transition-slow: 0.3s ease;
```

#### Plain Mode Variables
```css
--wcw-plain-bg: transparent;
--wcw-plain-shadow: none;
--wcw-plain-border: none;
--wcw-plain-padding: 0;
--wcw-plain-margin: 0;
--wcw-plain-max-width: none;
--wcw-plain-min-height: auto;
```

### WithdrawConsentWidget Status Callbacks

The `onStatusChange` callback receives status updates with the following structure:

```tsx
onStatusChange({
  status: 'in-progress' | 'consents-loaded' | 'no-consents' | 'withdrawing' | 'withdrawn' | 'error',
  clientId: string,
  // Additional fields based on status:
  consents?: Consent[], // present when status is 'consents-loaded' or 'withdrawn'
  totalCount?: number, // present when status is 'consents-loaded' or 'no-consents'
  withdrawnConsent?: Consent, // present when status is 'withdrawing' or 'withdrawn'
  actionId?: string, // present when status is 'withdrawn' (ID to use for completing the consent withdrawal)
  error?: string // present when status is 'error'
})
```

| Status | Description |
|--------|-------------|
| `"in-progress"` | Widget is fetching consents from the API |
| `"consents-loaded"` | Consents successfully loaded. Includes `consents` array and `totalCount` |
| `"no-consents"` | User has no active consents. Includes `totalCount: 0` |
| `"withdrawing"` | Consent withdrawal in progress. Includes `withdrawnConsent` |
| `"withdrawn"` | Consent successfully withdrawn. Includes updated `consents` array, `withdrawnConsent`, and `actionId` |
| `"error"` | Error occurred during operation. Includes `error` message |

**Note:** The callback uses deduplication to avoid firing duplicate status updates for the same state.

**Example:**
```jsx
<WithdrawConsentWidget
  onStatusChange={(data) => {
    switch (data.status) {
      case 'consents-loaded':
        console.log('Loaded consents:', data.consents);
        console.log('Total count:', data.totalCount);
        break;
      case 'withdrawn':
        console.log('Consent withdrawn:', data.withdrawnConsent);
        console.log('Remaining consents:', data.consents);
        break;
      case 'no-consents':
        console.log('User has no active consents');
        break;
      case 'error':
        console.error('Error:', data.error);
        break;
    }
  }}
/>
```

## Examples

### Advanced ConsentWidget Usage

#### Inline Mode with Custom Styling

```jsx
import React from 'react';
import { ConsentWidget } from '@gfox/grantly-react-consent';
import './ConsentWidget.css';

function InlineConsent() {
  return (
    <div className="consent-section">
      <h2>Data Processing Consent</h2>
      <ConsentWidget
        clientId="your-client-id"
        xUserId="user-123"
        displayMode="inline no-agreement-title"
        acceptLanguage="en"
        onStatusChange={(data) => {
          if (data.status === 'accepted') {
            console.log('User has given consent');
          }
        }}
      />
    </div>
  );
}
```

#### Plain Mode for Embedded Use

```jsx
import React from 'react';
import { ConsentWidget } from '@gfox/grantly-react-consent';

function EmbeddedConsent() {
  return (
    <div className="embedded-consent">
      <ConsentWidget
        clientId="your-client-id"
        xUserId="user-123"
        displayMode="plain no-change-summary"
        modifyConsent={true}
        onStatusChange={(data) => {
          // Handle consent modifications
          if (data.status === 'accepted') {
            console.log('Consent modified:', data.acceptedPurposes);
          }
        }}
      />
    </div>
  );
}
```

**Note:** When `modifyConsent={true}`, the submit button is only enabled when the user has actually changed their consent selection from the initial state. This prevents unnecessary API calls when no changes are made.

### Advanced WithdrawConsentWidget Usage

#### Modal Mode with Custom Callbacks

```jsx
import React, { useState } from 'react';
import { WithdrawConsentWidget } from '@gfox/grantly-react-consent';

function ConsentManagement() {
  const [consentData, setConsentData] = useState(null);

  const handleStatusChange = (data) => {
    switch (data.status) {
      case 'consents-loaded':
        setConsentData(data.consents);
        break;
      case 'withdrawn':
        alert('Consent successfully withdrawn');
        setConsentData(data.consents); // Update with remaining consents
        break;
      case 'error':
        console.error('Error:', data.error);
        break;
    }
  };

  const handleModifyConsent = (consent) => {
    // Redirect to consent modification page
    window.location.href = `/modify-consent/${consent.id}`;
  };

  return (
    <div>
      <h1>Manage Your Consents</h1>
      <WithdrawConsentWidget
        clientId="your-client-id"
        userId="user-123"
        onStatusChange={handleStatusChange}
        onModifyConsent={handleModifyConsent}
      />
    </div>
  );
}
```

#### Plain Mode for Settings Page

```jsx
import React from 'react';
import { WithdrawConsentWidget } from '@gfox/grantly-react-consent';

function SettingsPage() {
  return (
    <div className="settings-page">
      <h2>Privacy Settings</h2>
      <div className="consent-management">
        <WithdrawConsentWidget
          clientId="your-client-id"
          userId="user-123"
          displayMode="plain no-modify-button"
          acceptLanguage="en"
          onStatusChange={(data) => {
            if (data.status === 'no-consents') {
              console.log('User has no active consents');
            }
          }}
        />
      </div>
    </div>
  );
}
```

### Custom Styling Examples

#### Dark Theme

```css
.consent-widget {
  --consent-widget-container-bg: #1a1a1a;
  --consent-widget-text-color: #ffffff;
  --consent-widget-primary-bg: #6366f1;
  --consent-widget-modal-bg: rgba(0, 0, 0, 0.9);
  --consent-widget-modal-content-bg: #2d2d2d;
}
```

#### Brand Colors

```css
.consent-widget {
  --consent-widget-primary-bg: #007bff;
  --consent-widget-secondary-bg: #6c757d;
  --consent-widget-container-border-radius: 12px;
  --consent-widget-button-padding: 1rem 2rem;
}
```

#### Minimal Design

```css
.consent-widget {
  --consent-widget-container-bg: transparent;
  --consent-widget-container-padding: 1rem;
  --consent-widget-container-border-radius: 4px;
  --consent-widget-button-border-radius: 4px;
  --consent-widget-spacing-md: 0.5rem;
}
```


## License

This project is licensed under the Apache License 2.0.

## Links

- [NPM Package](https://www.npmjs.com/package/@gfox/grantly-react-consent)