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

# @gfox_ai/grantly-react-consent

[![npm version](https://img.shields.io/npm/v/@gfox_ai/grantly-react-consent.svg)](https://www.npmjs.com/package/@gfox_ai/grantly-react-consent)
[![npm license](https://img.shields.io/npm/l/@gfox_ai/grantly-react-consent.svg)](https://www.npmjs.com/package/@gfox_ai/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)
- [Consent Widget](#consent-widget)
  - [Props](#consentwidget-props)
  - [Display Modes](#display-modes)
  - [Ref Methods](#ref-methods)
  - [Status Callbacks](#status-callbacks)
  - [Styling](#consentwidget-styling)
- [Consent Withdraw Widget](#consent-withdraw-widget)
  - [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_ai/grantly-react-consent
```

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

```bash
pnpm add @gfox_ai/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_ai/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);
      console.log('Action ID:', data.actionId); // Use this to complete the consent action
      console.log('Consent State:', data.consentState); // 'draft' or 'active'
      
      // If consent is in draft state, you need to complete it using the actionId
      // See the "Action ID" section in Status Callbacks for details
    } 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;
```

**Note:** When consent is accepted, you'll receive an `actionId` in the callback. If the `consentState` is `"draft"`, you must use this `actionId` to complete the consent action via the API endpoint. See the [Status Callbacks](#status-callbacks) section for complete details.

## 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_ai/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}
    />
  );
}
```

## Consent Widget

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 `?token=...`) - 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_ai/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, // userId for identification of user
  // 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' (see Action ID section below)
  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) |

#### Action ID

When the consent status is `"accepted"`, the callback includes an `actionId` field. This ID is required to complete the consent action via the API.

**Important:** 
- The `actionId` is returned when a consent is successfully submitted
- Use this ID to call the completion endpoint: `POST /api/v1/consent-actions/{action_id}/complete`
- The completion endpoint activates draft consents or finalizes withdrawal requests
- If `consentState` is `"draft"`, you must complete the action using the `actionId` to activate the consent
- If `consentState` is `"active"` (when `authToken` is provided), the consent is already active and completion may not be necessary

**Example:**
```jsx
<ConsentWidget
  clientId="your-client-id"
  xUserId="user-123"
  onStatusChange={(data) => {
    if (data.status === 'accepted') {
      console.log('Action ID:', data.actionId);
      console.log('Consent State:', data.consentState);
      
      // If consent is in draft state, complete it using the actionId
      if (data.consentState === 'draft' && data.actionId) {
        completeConsentAction(data.actionId, data.userId);
      }
    }
  }}
/>

// Function to complete the consent action
async function completeConsentAction(actionId, userId) {
  const response = await fetch(`/api/v1/consent-actions/${actionId}/complete`, {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      'X-Client-Id': 'your-client-id',
      'X-Client-Secret': 'your-client-secret',
    },
    body: JSON.stringify({
      user_id: userId,
      // ... other user information
    }),
  });
  
  if (response.ok) {
    console.log('Consent action completed successfully');
  }
}
```

### ConsentWidget Styling

The widget exposes CSS custom properties for complete customization. Override these variables in your CSS to customize the widget appearance.

#### Base Properties
```css
--cw-width: 100%;
--cw-font-family: inherit;
--cw-font-size: inherit;
--cw-text-color: #1f2937;
```

#### Modal Properties
```css
--cw-modal-bg: rgba(0, 0, 0, 0.5);
--cw-modal-content-bg: #ffffff;
--cw-modal-z-index: 9999;
--cw-modal-padding-top: var(--cw-spacing-md);
--cw-modal-padding-bottom: var(--cw-spacing-lg);
```

#### Container Properties
```css
--cw-container-bg: #ffffff;
--cw-container-padding: 1.25rem;
--cw-container-border-radius: 16px;
--cw-container-border-color: #e5e7eb;
--cw-container-border-width: 1px;
--cw-container-max-width: 800px;
--cw-container-margin-bottom: var(--cw-spacing-md);
```

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

#### Typography Properties
```css
--cw-title-font-size: 1.75rem;
--cw-title-font-weight: 600;
--cw-title-margin-bottom: var(--cw-spacing-lg);
--cw-text-font-size: inherit;
--cw-text-line-height: 1.5;
```

#### Card Properties
```css
--cw-card-bg: #f9fafb;
--cw-card-padding: 1rem;
--cw-card-border-radius: 8px;
--cw-card-border-color: #e5e7eb;
--cw-card-border-width: 1px;
```

#### Purpose Item Properties
```css
--cw-purpose-bg: #ffffff;
--cw-purpose-padding: 1rem;
--cw-purpose-border-radius: 6px;
--cw-purpose-border-color: #e5e7eb;
--cw-purpose-border-width: 1px;
--cw-purpose-gap: 0.5rem;
--cw-purpose-summary-font-size: 0.85rem;
--cw-purpose-summary-color: #6b7280;
--cw-purpose-hover-bg: #f3f4f6;
--cw-purpose-desc-margin: 0;
--cw-purpose-desc-padding: 0;
```

#### Checkbox Properties
```css
--cw-checkbox-width: 1.2rem;
--cw-checkbox-height: 1.2rem;
--cw-checkbox-container-padding: 0.2rem;
```

#### Button Properties
```css
--cw-button-padding: 0.75rem 1.5rem;
--cw-button-font-weight: 600;
--cw-button-border-radius: 6px;
--cw-button-gap: 1rem;
```

#### Primary Button Properties
```css
--cw-primary-bg: #8b5cf6;
--cw-primary-color: white;
--cw-primary-border: none;
--cw-primary-hover: #7c3aed;
```

#### Secondary Button Properties
```css
--cw-secondary-bg: transparent;
--cw-secondary-color: #1f2937;
--cw-secondary-border-color: #d1d5db;
--cw-secondary-border-width: 1px;
--cw-secondary-hover-bg: #f3f4f6;
--cw-secondary-hover-border-color: #9ca3af;
```

#### Plain Mode Overrides
```css
--cw-plain-text-color: #000;
--cw-plain-button-padding: 0.5rem 1rem;
--cw-plain-button-font-size: 0.95rem;
--cw-plain-button-border-color: #000;
```

#### Inline Mode Overrides
```css
--cw-inline-text-color: #000;
--cw-inline-purpose-padding: 0.25rem 0;
--cw-inline-purpose-margin: 0.25rem 0;
```

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

## Consent Withdraw Widget

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


### 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 |

**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 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' (see Action ID section below)
  error?: string // present when status is 'error'
})
```

| Status | Description |
|--------|-------------|
| `"in-progress"` | Widget is fetching consents from the API |
| `"consents-loaded"` | Consents successfully loaded |
| `"no-consents"` | User has no active consents |
| `"withdrawing"` | Consent withdrawal in progress |
| `"withdrawn"` | Consent successfully withdrawn |
| `"error"` | Error occurred during operation |

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

#### Action ID

When the withdrawal status is `"withdrawn"`, the callback includes an `actionId` field. This ID is required to complete the withdrawal action via the API.

**Important:**
- The `actionId` is returned when a consent withdrawal is successfully requested
- Use this ID to call the completion endpoint: `POST /api/v1/consent-actions/{action_id}/complete`
- The completion endpoint finalizes the withdrawal, updating the consent status from `'withdrawal_requested'` to `'withdrawn'`
- You must complete the withdrawal action using the `actionId` to finalize the withdrawal

**Example:**
```jsx
<WithdrawConsentWidget
  clientId="your-client-id"
  userId="user-123"
  onStatusChange={(data) => {
    if (data.status === 'withdrawn') {
      console.log('Withdrawal Action ID:', data.actionId);
      console.log('Withdrawn Consent:', data.withdrawnConsent);
      
      // Complete the withdrawal action using the actionId
      if (data.actionId) {
        completeWithdrawalAction(data.actionId, data.clientId);
      }
    }
  }}
/>

// Function to complete the withdrawal action
async function completeWithdrawalAction(actionId, clientId) {
  const response = await fetch(`/api/v1/consent-actions/${actionId}/complete`, {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      'X-Client-Id': clientId,
      'X-Client-Secret': 'your-client-secret',
    },
    body: JSON.stringify({
      user_id: 'user-123',
      // ... other user information
    }),
  });
  
  if (response.ok) {
    console.log('Withdrawal action completed successfully');
  }
}
```

**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);
        console.log('Action ID:', data.actionId); // Use this to complete the withdrawal
        break;
      case 'no-consents':
        console.log('User has no active consents');
        break;
      case 'error':
        console.error('Error:', data.error);
        break;
    }
  }}
/>
```

### 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-min-height: auto;
--wcw-width: auto;
--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;
--wcw-gap-xs: 4px;
--wcw-gap-sm: 8px;
--wcw-gap-md: 12px;
--wcw-gap-lg: 16px;
--wcw-gap-xl: 20px;
--wcw-card-gap: var(--wcw-gap-xl);
--wcw-header-gap: var(--wcw-gap-lg);
--wcw-content-gap: var(--wcw-gap-lg);
--wcw-content-padding: 0;
```

#### 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;
--wcw-line-height: 1.5;
--wcw-line-height-tight: 1.25;
```

#### Colors
```css
--wcw-bg-primary: #ffffff;
--wcw-bg-secondary: #f9fafb;
--wcw-bg-tertiary: #f3f4f6;
--wcw-bg-error: #fef2f2;
--wcw-bg-success: #d1fae5;
--wcw-bg-warning: #fef3c7;
--wcw-bg-danger: #fee2e2;
--wcw-text-primary: #1f2937;
--wcw-text-secondary: #4b5563;
--wcw-text-muted: #6b7280;
--wcw-text-error: #dc2626;
--wcw-text-success: #065f46;
--wcw-text-warning: #92400e;
--wcw-text-danger: #991b1b;
--wcw-text-disabled: #9ca3af;
--wcw-border-primary: #e5e7eb;
--wcw-border-secondary: #f3f4f6;
--wcw-border-error: #fecaca;
--wcw-primary: #8b5cf6;
--wcw-primary-hover: #7c3aed;
--wcw-danger: #8b5cf6;
--wcw-danger-hover: #7c3aed;
--wcw-success: #059669;
```

#### Checkbox Properties
```css
--wcw-checkbox-bg: var(--wcw-primary);
--wcw-checkbox-checkmark: #ffffff;
--wcw-checkbox-border: var(--wcw-primary);
--wcw-checkbox-size: 20px;
```

#### 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;
```

#### Focus Ring
```css
--wcw-focus-ring: 2px solid var(--wcw-primary);
--wcw-focus-ring-offset: 2px;
--wcw-focus-ring-radius: 4px;
```

#### 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;
```

**Example Usage:**
```css
.withdraw-consent-widget-content {
  --wcw-primary: #007bff;
  --wcw-max-width: 1000px;
  --wcw-border-radius: 12px;
}
```

## Examples

### Advanced ConsentWidget Usage

#### Inline Mode with Custom Styling

```jsx
import React from 'react';
import { ConsentWidget } from '@gfox_ai/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_ai/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>
  );
}
```

#### Using Action ID to Complete Consent

When a consent is submitted, you receive an `actionId` that must be used to complete the consent action. This is especially important when `consentState` is `"draft"`:

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

function ConsentWithCompletion() {
  const [isCompleting, setIsCompleting] = useState(false);

  const completeConsentAction = async (actionId, userId) => {
    setIsCompleting(true);
    try {
      const response = await fetch(`/api/v1/consent-actions/${actionId}/complete`, {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
          'X-Client-Id': 'your-client-id',
          'X-Client-Secret': 'your-client-secret',
        },
        body: JSON.stringify({
          user_id: userId,
          // Add other user information as required by your API
        }),
      });

      if (!response.ok) {
        throw new Error('Failed to complete consent action');
      }

      const result = await response.json();
      console.log('Consent action completed:', result);
      alert('Consent activated successfully!');
    } catch (error) {
      console.error('Error completing consent:', error);
      alert('Failed to activate consent. Please try again.');
    } finally {
      setIsCompleting(false);
    }
  };

  return (
    <div>
      {isCompleting && <p>Activating consent...</p>}
      <ConsentWidget
        clientId="your-client-id"
        xUserId="user-123"
        onStatusChange={(data) => {
          if (data.status === 'accepted') {
            console.log('Consent submitted:', {
              actionId: data.actionId,
              consentState: data.consentState,
              acceptedPurposes: data.acceptedPurposes,
            });

            // If consent is in draft state, complete it to activate
            if (data.consentState === 'draft' && data.actionId) {
              completeConsentAction(data.actionId, data.userId);
            } else if (data.consentState === 'active') {
              console.log('Consent is already active (authToken was provided)');
            }
          }
        }}
      />
    </div>
  );
}
```

### Advanced Consent Withdraw Widget Usage

#### Modal Mode with Custom Callbacks

```jsx
import React, { useState } from 'react';
import { WithdrawConsentWidget } from '@gfox_ai/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
        // Complete the withdrawal action using the actionId
        if (data.actionId) {
          completeWithdrawalAction(data.actionId, data.clientId);
        }
        break;
      case 'error':
        console.error('Error:', data.error);
        break;
    }
  };

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

  const completeWithdrawalAction = async (actionId, clientId) => {
    try {
      const response = await fetch(`/api/v1/consent-actions/${actionId}/complete`, {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
          'X-Client-Id': clientId,
          'X-Client-Secret': 'your-client-secret',
        },
        body: JSON.stringify({
          user_id: 'user-123',
          // Add other user information as required by your API
        }),
      });

      if (!response.ok) {
        throw new Error('Failed to complete withdrawal action');
      }

      const result = await response.json();
      console.log('Withdrawal action completed:', result);
    } catch (error) {
      console.error('Error completing withdrawal:', error);
    }
  };

  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_ai/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 for Consent Widget

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

#### Brand Colors for Consent Widget

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

#### Minimal Design for Consent Widget

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

#### Custom Styling for Consent Withdraw Widget

```css
.withdraw-consent-widget-content {
  --wcw-primary: #007bff;
  --wcw-max-width: 1000px;
  --wcw-border-radius: 12px;
  --wcw-bg-primary: #f8f9fa;
}
```

## License

This project is licensed under the Apache License 2.0.

## Links

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