npm package: @redacto.io/consent-sdk-react
Description: A comprehensive consent management SDK that provides seamless integration with Redacto's Consent Management Platform (CMP) for React and Next.js applications.
Latest version: 4.0.0

# @redacto.io/consent-sdk-react

A comprehensive consent management SDK that provides seamless integration with Redacto's Consent Management Platform (CMP) for React and Next.js applications.

## Overview

The Redacto Consent SDK enables developers to easily integrate consent management functionality into their React applications. It provides a consistent user experience for collecting and managing user consent while maintaining compliance with privacy regulations.

## Architecture

The SDK follows a client-server architecture pattern:

```
┌─────────────┐         ┌──────────────┐         ┌─────────────────┐
│   Client    │────────▶│ Your Backend │────────▶│  Redacto API    │
│  (React)    │◀────────│    Server    │◀────────│   (CMP API)     │
└─────────────┘         └──────────────┘         └─────────────────┘
     │                        │
     │                        │
     ▼                        ▼
┌─────────────────────────────────────────────┐
│     @redacto.io/consent-sdk-react           │
│  (RedactoNoticeConsent /                   │
│   RedactoNoticeConsentInline)               │
└─────────────────────────────────────────────┘
```

### How It Works

1. **Your Backend Server**: Your server acts as a proxy between your React application and Redacto's API. It handles:
   - Generating access tokens
   - Token refresh operations

2. **React Application**: Your React app uses the SDK components to:
   - Display consent notices
   - Collect user consent choices
   - Submit consent records to Redacto API

3. **SDK Components**: The SDK components handle:
   - Fetching consent content from Redacto API
   - Rendering consent UI
   - Submitting consent records
   - Managing user interactions

## Supported Platforms

| Platform     | Description                                      | SDK | Demo App |
| ------------ | ------------------------------------------------ | --- | -------- |
| React        | React/Next.js applications                       | ✅  | ✅       |
| JavaScript   | Iframe & PHP applications (No jQuery dependency) | 🚧  | 🚧       |
| Android      | Native Android (Kotlin)                          | 🚧  | 🚧       |
| iOS          | Native iOS (Swift)                               | 🚧  | 🚧       |
| Flutter      | Flutter applications                             | 🚧  | 🚧       |
| React Native | React Native applications                        | 🚧  | 🚧       |

## Features

- **Consent Notice Display**
  - Customizable consent notices
  - Multiple display formats (modal popup, inline form)
  - Responsive design

- **Consent Collection**
  - User consent choices (accept, reject, manage preferences)
  - Granular consent management
  - Persistent consent storage

- **API Integration**
  - Seamless communication with Redacto CMP API
  - Automatic error handling and retries
  - Token-based authentication

- **Configuration**
  - Customizable UI settings
  - Language preferences
  - API endpoint configuration

- **Localization**
  - Multi-language support
  - RTL language support
  - Customizable translations

## Getting Started

### Installation

```bash
# For React applications
npm install @redacto.io/consent-sdk-react
```

### Backend Server Setup

Before using the SDK components, you need to set up backend server endpoints that generate access tokens by calling Redacto's API. Your server should implement the following endpoints:

**Important:** You need to obtain your **CMS-API-Key** from the Redacto Dashboard. This key is required to authenticate your requests to Redacto's API. Store this key securely in your environment variables and never expose it to the frontend.

#### Example Backend Implementation

```typescript
// Express.js example
import express, { Application, Request, Response } from "express";
import axios from "axios";

const app: Application = express();
app.use(express.json());

// Get CMS-API-Key from environment variables (obtained from Redacto Dashboard)
const CMS_API_KEY = process.env.CMS_API_KEY;

// Endpoint to generate access token
app.post(
  "/api/consent/token",
  async (req: Request, res: Response): Promise<void> => {
    try {
      const { email } = req.body;
      const { organisation_uuid, workspace_uuid, base_url } = req.query;

      if (!CMS_API_KEY) {
        return res.status(500).json({ error: "CMS API Key not configured" });
      }

      // Call Redacto API to generate access token
      const response = await axios.post(
        `${base_url}/public/organisations/${organisation_uuid}/workspaces/${workspace_uuid}/tokens/generate-access-token`,
        {
          email,
          // expires_in_days: 1, // Optional: set token expiration
        },
        {
          headers: {
            "X-CMS-API-Key": CMS_API_KEY,
          },
        }
      );

      // Return token data to frontend
      res.json({
        token: response.data.detail.token,
        refresh_token: response.data.detail.refresh_token,
        expires_at: response.data.detail.expires_at,
      });
    } catch (error) {
      console.error("Error generating token:", error);
      res.status(500).json({ error: "Failed to generate token" });
    }
  }
);

// Endpoint to refresh access token
app.post(
  "/api/consent/refresh-token",
  async (req: Request, res: Response): Promise<void> => {
    try {
      const { refresh_token, organisation_uuid, workspace_uuid, base_url } =
        req.body;

      if (!CMS_API_KEY) {
        return res.status(500).json({ error: "CMS API Key not configured" });
      }

      // Call Redacto API to refresh access token
      const response = await axios.post(
        `${base_url}/consent/public/organisations/${organisation_uuid}/workspaces/${workspace_uuid}/tokens/refresh-token`,
        { refresh_token: refresh_token },
        {
          headers: {
            "X-CMS-API-Key": CMS_API_KEY,
          },
        }
      );

      res.json({
        token: response.data.detail.token,
        refresh_token: response.data.detail.refresh_token,
        expires_at: response.data.detail.expires_at,
      });
    } catch (error) {
      console.error("Error refreshing token:", error);
      res.status(500).json({ error: "Failed to refresh token" });
    }
  }
);
```

#### Redacto API Endpoints Used

Your backend needs to call the following Redacto API endpoints:

1. **Generate Access Token**
   - **Endpoint**: `POST /public/organisations/{organisation_uuid}/workspaces/{workspace_uuid}/tokens/generate-access-token`
   - **Headers**: `X-CMS-API-Key: {your-cms-api-key}`
   - **Body**: `{ email, expires_in_days? }`
   - **Response**: `{ detail: { token, refresh_token, expires_at } }`

2. **Refresh Access Token**
   - **Endpoint**: `POST /consent/public/organisations/{organisation_uuid}/workspaces/{workspace_uuid}/tokens/refresh-token`
   - **Headers**: `X-CMS-API-Key: {your-cms-api-key}`
   - **Body**: `{ refresh_token }`
   - **Response**: `{ detail: { token, refresh_token, expires_at } }`

**Note:** Replace `{your-cms-api-key}` with the CMS-API-Key you obtained from the Redacto Dashboard. Store this key securely in your environment variables.

## Components

### `RedactoNoticeConsent`

A modal popup component for consent collection. Displays a full-screen modal with consent options. This component is ideal for initial consent collection flows where you want to show consent before users can proceed.

**Key Characteristics:**

- Requires tokens upfront (must be provided before rendering)
- Displays as a modal overlay that blocks UI interaction
- Best for: Initial consent collection, standalone consent flows

**Props:**

| Prop              | Type                     | Required | Default | Description                         |
| ----------------- | ------------------------ | -------- | ------- | ----------------------------------- |
| `noticeId`        | `string`                 | Yes      | -       | UUID of the consent notice          |
| `accessToken`     | `string`                 | Yes      | -       | Authentication token for API calls  |
| `refreshToken`    | `string`                 | Yes      | -       | Refresh token for token renewal     |
| `baseUrl`         | `string`                 | No       | -       | Base URL for the consent API        |
| `blockUI`         | `boolean`                | No       | `true`  | Whether to block UI interaction     |
| `language`        | `string`                 | No       | `"en"`  | Language code                       |
| `settings`        | `object`                 | No       | -       | Custom styling options              |
| `applicationId`   | `string`                 | No       | -       | Application-specific UUID           |
| `validateAgainst` | `"all" \| "required"`    | No       | `"all"` | Consent validation mode (see below) |
| `includeFullyConsentedData` | `boolean` | No | `false` | When true, API returns 200 with pre-selected purposes instead of 409 when user has already consented |
| `reviewModeButtonText` | `string` | No | `"Continue"` | Button text when in fully consented review mode (magic link flow) |
| `onAccept`        | `() => void`             | Yes      | -       | Callback when consent is accepted   |
| `onDecline`       | `() => void`             | Yes      | -       | Callback when consent is declined   |
| `onError`         | `(error: Error) => void` | No       | -       | Error handler callback              |

**`validateAgainst` Parameter:**

The `validateAgainst` prop controls how the backend validates consent status when fetching consent content. It accepts two values:

- **`"all"`** (default): Validates all purposes and data elements, regardless of whether they are required or optional. This is the standard mode for initial consent collection and provides comprehensive validation. Responses are cached for better performance.

- **`"required"`**: Only validates required purposes and data elements. This mode is useful for reconsent flows or scenarios where you only need to verify that mandatory consent elements are checked. Responses are not cached to ensure fresh validation data.

**Why use it:**

- Use `"all"` for standard consent flows where you need to validate all consent elements
- Use `"required"` for reconsent scenarios or when you only need to verify mandatory consent requirements
- The mode affects API caching behavior: `"all"` responses are cached for performance, while `"required"` responses bypass cache to ensure up-to-date validation

**`includeFullyConsentedData` Parameter:**

When users arrive via a magic link and have already consented, the API normally returns 409 and the SDK bypasses to "Already Consented". Set `includeFullyConsentedData={true}` to instead show a **read-only review** of what they consented to, then a single Continue button.

- **Purposes**: Green tick for each selected purpose
- **Data elements**: Green tick = consented, empty circle = not consented (all read-only, no editing)
- **Partial selection**: If a purpose is selected but some data elements within it are not, the review correctly shows green ticks for consented elements and empty circles for non-consented ones
- Use `reviewModeButtonText` to customize the button label (default: "Continue")

### `RedactoNoticeConsentInline`

An inline component for embedding consent directly in forms. The component can load consent content without tokens and automatically submits consent when tokens are provided and all required conditions are met.

**Key Characteristics:**

- Can load content without tokens (tokens optional initially)
- Renders inline within your form
- Auto-submits consent when `accessToken` is provided and all required elements are checked
- Best for: Registration forms, checkout flows, embedded consent in multi-step processes

**Requirements to Display the Component:**

To see and display the `RedactoNoticeConsentInline` component, you must provide the following **required props**:

- `org_uuid` (string, required): Your organization UUID
- `workspace_uuid` (string, required): Your workspace UUID
- `notice_uuid` (string, required): The consent notice UUID

**Note:** The component can fetch and display consent content using only these three UUIDs. No `accessToken` is needed to display the component - users can see and interact with the consent form immediately.

**Requirements to Submit Consent:**

To actually submit/give consent, the component requires **all** of the following conditions to be met:

1. **`accessToken` must be provided**: The authentication token obtained from your backend server (see Backend Server Setup section)
2. **Consent content must be loaded**: The component must have successfully fetched the consent notice content
3. **All required data elements must be checked**: The user must check all

When all these conditions are met, the component **automatically submits** the consent without requiring any additional user action.

**Auto-Submit Behavior:**

The component automatically submits consent when all of the following conditions are met:

- `accessToken` is provided
- Consent content has been loaded
- All required data elements are checked
- The component is not already submitting

**Important:** The `hasSubmittedRef` only prevents duplicate submissions with the **same token**. To allow users to re-submit consent, simply update the `accessToken` prop with a new token from your backend, and the component will automatically attempt submission again when all conditions are met.

**Props:**

| Prop                 | Type                         | Required | Default | Description                            |
| -------------------- | ---------------------------- | -------- | ------- | -------------------------------------- |
| `org_uuid`           | `string`                     | Yes      | -       | Organization UUID                      |
| `workspace_uuid`     | `string`                     | Yes      | -       | Workspace UUID                         |
| `notice_uuid`        | `string`                     | Yes      | -       | Notice UUID                            |
| `accessToken`        | `string`                     | No       | -       | Authentication token                   |
| `refreshToken`       | `string`                     | No       | -       | Refresh token                          |
| `baseUrl`            | `string`                     | No       | -       | Base URL for the consent API           |
| `language`           | `string`                     | No       | `"en"`  | Language code                          |
| `settings`           | `object`                     | No       | -       | Custom styling options                 |
| `applicationId`      | `string`                     | No       | -       | Application-specific UUID              |
| `onAccept`           | `() => void`                 | No       | -       | Callback when consent is accepted      |
| `onDecline`          | `() => void`                 | No       | -       | Callback when consent is declined      |
| `onError`            | `(error: Error) => void`     | No       | -       | Error handler callback                 |
| `onValidationChange` | `(isValid: boolean) => void` | No       | -       | Callback when validation state changes |

## Usage Examples

### Example 1: Using `RedactoNoticeConsent` (Modal Popup)

This example shows how to use the modal component for initial consent collection. The user fills out a form, and after submission, tokens are fetched from your backend, then the consent modal is displayed.

```tsx
import { RedactoNoticeConsent } from "@redacto.io/consent-sdk-react";
import { useState } from "react";

function SignUpPage() {
  const [showConsent, setShowConsent] = useState(false);
  const [tokens, setTokens] = useState({
    accessToken: "",
    refreshToken: "",
  });
  const [formData, setFormData] = useState({ email: "" });

  const handleFormSubmit = async (e: React.FormEvent) => {
    e.preventDefault();

    try {
      // Step 1: Get tokens from your backend
      const response = await fetch(
        `${BACKEND_URL}/api/consent/token?workspace_uuid=${WORKSPACE_UUID}&organisation_uuid=${ORG_UUID}&base_url=${BASE_URL}`,
        {
          method: "POST",
          headers: { "Content-Type": "application/json" },
          body: JSON.stringify({
            email: formData.email,
            organisation_uuid: ORG_UUID,
            workspace_uuid: WORKSPACE_UUID,
            base_url: BASE_URL,
          }),
        }
      );

      const data = await response.json();
      setTokens({
        accessToken: data.token,
        refreshToken: data.refresh_token,
      });

      // Step 2: Show consent modal
      setShowConsent(true);
    } catch (error) {
      console.error("Failed to get tokens:", error);
    }
  };

  const handleConsentAccept = () => {
    setShowConsent(false);
    // Proceed with your application flow
    console.log("Consent accepted, proceeding...");
  };

  const handleConsentDecline = () => {
    setShowConsent(false);
    // Handle decline case
    console.log("Consent declined");
  };

  return (
    <div>
      <form onSubmit={handleFormSubmit}>
        <input
          type="email"
          value={formData.email}
          onChange={(e) => setFormData({ email: e.target.value })}
          placeholder="Enter your email"
        />
        <button type="submit">Sign Up</button>
      </form>

      {/* Consent Modal */}
      {showConsent && tokens.accessToken && (
        <RedactoNoticeConsent
          noticeId={NOTICE_UUID}
          accessToken={tokens.accessToken}
          refreshToken={tokens.refreshToken}
          baseUrl={BASE_URL}
          blockUI={true}
          validateAgainst="all"
          onAccept={handleConsentAccept}
          onDecline={handleConsentDecline}
          onError={(error) => console.error("Consent error:", error)}
        />
      )}
    </div>
  );
}
```

### Example 2: Using `RedactoNoticeConsentInline` (Inline Form)

This example shows how to use the inline component within a registration form. The component loads consent content immediately, validates user selections, and automatically submits consent when tokens are provided.

**Key Points:**

- Component displays immediately with just `org_uuid`, `workspace_uuid`, and `notice_uuid`
- Users can interact with the consent form without tokens
- Consent is automatically submitted when `accessToken` is provided and all required elements are checked

```tsx
import { RedactoNoticeConsentInline } from "@redacto.io/consent-sdk-react";
import { useState, useCallback } from "react";

function RegistrationForm() {
  const [formData, setFormData] = useState({
    email: "",
    mobile: "",
  });
  const [tokens, setTokens] = useState({
    accessToken: undefined as string | undefined,
    refreshToken: undefined as string | undefined,
  });
  const [consentValid, setConsentValid] = useState(false);
  const [isSubmitting, setIsSubmitting] = useState(false);

  // Track when all required consent elements are checked
  const handleValidationChange = useCallback((isValid: boolean) => {
    setConsentValid(isValid);
  }, []);

  const handleFormSubmit = async (e: React.FormEvent) => {
    e.preventDefault();

    // Validate form data
    if (!formData.email && !formData.mobile) {
      alert("Please provide email or mobile number");
      return;
    }

    if (!consentValid) {
      alert("Please check all required consent elements");
      return;
    }

    setIsSubmitting(true);

    try {
      // Step 1: Get tokens from your backend
      const response = await fetch(
        `${BACKEND_URL}/api/consent/token?workspace_uuid=${WORKSPACE_UUID}&organisation_uuid=${ORG_UUID}&base_url=${BASE_URL}`,
        {
          method: "POST",
          headers: { "Content-Type": "application/json" },
          body: JSON.stringify({
            email: formData.email || undefined,
            mobile: formData.mobile || undefined,
          }),
        }
      );

      const data = await response.json();

      // Step 2: Set tokens - component will auto-submit consent
      setTokens({
        accessToken: data.token,
        refreshToken: data.refresh_token,
      });

      // Store user info for consent submission
      if (formData.email) {
        localStorage.setItem("userEmail", formData.email);
      }
      if (formData.mobile) {
        localStorage.setItem("userMobile", formData.mobile);
      }

      // Component will automatically submit consent when:
      // - accessToken is set
      // - all required elements are checked
      // - content is loaded
    } catch (error) {
      console.error("Failed to complete registration:", error);
      setIsSubmitting(false);
    }
  };

  const handleConsentAccept = useCallback(() => {
    console.log("Consent accepted and submitted");
    setIsSubmitting(false);
    // Proceed with your registration flow
  }, []);

  const handleConsentError = useCallback((error: Error) => {
    console.error("Consent error:", error);

    // Handle 409 CONFLICT (user already consented)
    const errorWithStatus = error as Error & { status?: number; code?: number };
    if (errorWithStatus.status === 409 || errorWithStatus.code === 409) {
      // Treat as success
      handleConsentAccept();
      return;
    }

    setIsSubmitting(false);
  }, []);

  return (
    <form onSubmit={handleFormSubmit}>
      <input
        type="email"
        value={formData.email}
        onChange={(e) => setFormData({ ...formData, email: e.target.value })}
        placeholder="Email (optional)"
      />

      <input
        type="tel"
        value={formData.mobile}
        onChange={(e) => setFormData({ ...formData, mobile: e.target.value })}
        placeholder="Mobile (optional)"
      />

      {/* 
        Inline consent component
        
        TO DISPLAY: Only requires org_uuid, workspace_uuid, notice_uuid
        - Component will fetch and display consent content immediately
        - Users can see and interact with consent form without tokens
        
        TO SUBMIT CONSENT: Requires all of the following:
        1. accessToken must be provided (set after form submission)
        2. Consent content must be loaded (automatic)
        3. All required data elements must be checked (tracked via onValidationChange)
        4. Component will auto-submit when all conditions are met
      */}
      <RedactoNoticeConsentInline
        org_uuid={ORG_UUID}
        workspace_uuid={WORKSPACE_UUID}
        notice_uuid={NOTICE_UUID}
        accessToken={tokens.accessToken} // Initially undefined, set after form submit
        refreshToken={tokens.refreshToken}
        baseUrl={BASE_URL}
        onValidationChange={handleValidationChange}
        onAccept={handleConsentAccept}
        onError={handleConsentError}
      />

      <button type="submit" disabled={isSubmitting || !consentValid}>
        {isSubmitting ? "Processing..." : "Submit Registration"}
      </button>
    </form>
  );
}
```

### Custom Styling

Both components support extensive styling customization:

```tsx
const customSettings = {
  button: {
    accept: {
      backgroundColor: "#9810fa",
      textColor: "white",
    },
    decline: {
      backgroundColor: "#0a0a14",
      textColor: "white",
    },
    language: {
      backgroundColor: "#0a0a14",
      textColor: "white",
      selectedBackgroundColor: "#9810fa",
      selectedTextColor: "white",
    },
  },
  borderRadius: "8px",
  backgroundColor: "#1a1a2e",
  headingColor: "#ffffff",
  textColor: "#e6e6e6",
  borderColor: "white",
  link: "#9810fa",
  font: "Arial, sans-serif", // Optional: custom font family
};

// Use in component
<RedactoNoticeConsent
  // ... other props
  settings={customSettings}
/>;
```

### Available Style Properties

- `backgroundColor`: Background color of the modal/form
- `headingColor`: Color for headings
- `textColor`: Color for body text
- `borderColor`: Color for borders
- `borderRadius`: Border radius for rounded corners
- `link`: Color for links
- `font`: Font family (optional)
- `button.accept`: Styles for accept button
- `button.decline`: Styles for decline button
- `button.language`: Styles for language selector

## Component Comparison

| Feature                 | `RedactoNoticeConsent`                    | `RedactoNoticeConsentInline`                |
| ----------------------- | ----------------------------------------- | ------------------------------------------- |
| **Display Type**        | Modal popup overlay                       | Inline form component                       |
| **Token Requirement**   | Required upfront                          | Optional (can load without)                 |
| **Use Case**            | Initial consent, standalone flows         | Embedded in forms, multi-step processes     |
| **Auto-Submit**         | No (user clicks accept/decline)           | Yes (when token provided + validated)       |
| **Validation Callback** | No                                        | Yes (`onValidationChange`)                  |
| **UI Blocking**         | Yes (by default)                          | No (inline)                                 |
| **Props Required**      | `noticeId`, `accessToken`, `refreshToken` | `org_uuid`, `workspace_uuid`, `notice_uuid` |

## Best Practices

1. **Backend Token Generation**: Always generate tokens on your backend server, never expose API keys or authentication tokens to the frontend

2. **Token Management**: Store tokens securely (consider using httpOnly cookies or secure storage) and handle token expiration/refresh

3. **Error Handling**: Always provide `onError` callbacks to handle API errors gracefully. For inline component, handle 409 CONFLICT errors (already consented) as success cases

4. **Validation Flow**: For inline component, use `onValidationChange` to track when all required elements are checked before allowing form submission

5. **Auto-Submit Pattern**: For inline component, provide `accessToken` after obtaining it from your backend - the component will automatically submit consent when all conditions are met

6. **User Feedback**: Show loading states and error messages to users during token generation and consent submission

7. **Accessibility**: The components include ARIA labels and keyboard navigation - ensure your forms maintain accessibility standards

8. **Re-submission**: Users can view and modify consent they've already given. To allow re-submission, update the `accessToken` prop with a new token from your backend. The component will automatically submit when all conditions are met. The internal ref only prevents duplicate submissions with the same token, not re-submission with a new token.

## Troubleshooting

### Component not rendering

- Check that all required props are provided
- For `RedactoNoticeConsent`: Verify that tokens are valid and not expired
- For `RedactoNoticeConsentInline`: Verify that `org_uuid`, `workspace_uuid`, and `notice_uuid` are correct
- Check browser console for error messages

### Consent not submitting

- Ensure `accessToken` is provided and valid (not expired)
- Verify that all required elements are checked (check `onValidationChange` callback for inline component)
- Ensure consent content has loaded (check for loading state)
- Verify network requests in browser DevTools
- For inline component: Check that tokens are set after form validation passes

### Backend token generation failing

- Verify authentication setup is correct for your environment
- Check that API key generation is working
- Ensure organisation and workspace UUIDs are correct
- Verify base URL matches your environment (staging vs production)

### Validation not working

- Ensure `onValidationChange` callback is provided for inline component
- Check that required data elements are properly configured in your consent notice
- Verify that user has checked all required elements

## API Documentation

### Redacto CMP API Endpoints

The SDK communicates with Redacto's CMP API. Key endpoints used:

- **Get Consent Notice**: `GET /public/organisations/{org_uuid}/workspaces/{workspace_uuid}/notices/{notice_uuid}`
- **Submit Consent**: `POST /public/organisations/{org_uuid}/workspaces/{workspace_uuid}/submit-consent`
- **Generate Access Token**: `POST /public/organisations/{org_uuid}/workspaces/{workspace_uuid}/tokens/generate-access-token`

For complete API documentation, visit: [Redacto API Documentation](https://staging.api.redacto.io/consent/docs)

## Development

### Prerequisites

- Node.js 16+
- npm or yarn
- Platform-specific development tools for native SDKs

### Building

```bash
# Install dependencies
npm install

# Build all packages
npm run build

# Build specific package
npm run build --workspace=@redacto.io/consent-sdk-react
```

### Testing

```bash
# Run all tests
npm test

# Run tests for specific package
npm test --workspace=@redacto.io/consent-sdk-react
```

## Additional Information

### Requirements

- React 16.8.0 or higher (including React 17, 18, and 19)
- React DOM 16.8.0 or higher (including React DOM 17, 18, and 19)

### Additional Features

- ✅ TypeScript support with full type definitions
- ✅ ESM and CommonJS module formats
- ✅ Accessibility features (ARIA labels, keyboard navigation)
- ✅ Text-to-speech support (English only)
- ✅ Age verification and guardian consent for minors
- ✅ Reconsent flow support

## License

Apache-2.0