This guide will walk you through the creation of a consent management system. This system will allow users to control their consent preferences for your messages or notifications. By leveraging XMTP, this tutorial offers tools to build a consent management system that respects user preferences and protects their privacy.
Start by importing the necessary libraries. This includes React for the UI, XMTP for messaging, and ethers for Ethereum blockchain interaction.
import React, { useState } from "react";
import { Client } from "@xmtp/xmtp-js"; // XMTP client for messaging
import { ethers } from "ethers"; // Ethers for Ethereum interaction
This function handles the connection to the user's Ethereum wallet using MetaMask or a similar wallet provider.
const connectWallet = async () => {
if (typeof window.ethereum !== "undefined") {
try {
await window.ethereum.request({ method: "eth_requestAccounts" });
const provider = new ethers.providers.Web3Provider(window.ethereum);
return provider.getSigner();
} catch (error) {
console.error("User rejected request", error);
}
} else {
console.error("Metamask not found");
}
};
This function is triggered when the user wants to view their subscriptions. It connects to the wallet and fetches the consent list. The consent list is a log with all consent actions ordered by date. To get the list, we need to filter unique actions by address.
// Function to refresh the consent list
const refreshConsentList = async (client) => {
// Fetch the consent list from the client
let consentList = await client.contacts.refreshConsentList();
// Create a unique consent list by removing duplicates
let uniqueConsentList = consentList
.slice() // Create a copy of the consent list
.reverse() // Reverse the list to keep the latest consent
.filter(
// Filter out duplicates by checking if the current index is the first occurrence of the consent value
(consent, index, self) =>
index === self.findIndex((t) => t.value === consent.value),
)
.reverse(); // Reverse the list back to the original order
// Sort the unique consent list based on the permission type
uniqueConsentList = uniqueConsentList.sort((a, b) => {
// If 'a' is allowed and 'b' is not, 'a' should come first
if (a.permissionType === "allowed" && b.permissionType !== "allowed")
return -1;
// If 'a' is unknown and 'b' is not, 'b' should come first
if (a.permissionType === "unknown" && b.permissionType !== "unknown")
return 1;
// If 'a' is denied and 'b' is not, 'b' should come first
if (a.permissionType === "denied" && b.permissionType !== "denied")
return 1;
// If none of the above conditions are met, keep the original order
return 0;
});
// Update the state with the unique and sorted consent list.
setConsentList(uniqueConsentList);
// Return the unique and sorted consent list
return uniqueConsentList;
};
Render the consent list in a table format, allowing users to see their current consent allowlist and denylist.
// Container for displaying the consent list
<div style={styles.ConsentContainer}>
// Table for displaying allowed consents
<div style={styles.ConsentTable}>
// Conditionally render the 'Allowed' header if there are any allowed
consents
{consentList.length > 0 && <h2>Allowed</h2>}
// Filter and map through the consentList to display allowed consents
{consentList
.filter((consent) => consent.permissionType === "allowed")
.map((consent, index) => (
// Display each allowed consent in a row with a deny option
<div
key={index}
style={{ display: "flex", justifyContent: "space-between" }}>
// Index of the consent in the list
<span style={{ textAlign: "left" }}>{index + 1}.</span>
// Value of the consent
<span style={{ textAlign: "left" }}>{consent.value}</span>
// Button to deny the consent
<span
style={{ color: "red", cursor: "pointer" }}
onClick={() => handleDeny(consent.value)}>
Deny
</span>
</div>
))}
</div>
// Table for displaying denied consents
<div style={styles.ConsentTable}>
// Conditionally render the 'Denied' header if there are any denied consents
{consentList.length > 0 && <h2>Denied</h2>}
// Filter and map through the consentList to display denied consents
{consentList
.filter((consent) => consent.permissionType === "denied")
.map((consent, index) => (
// Display each denied consent in a row with an allow option
<div
key={index}
style={{ display: "flex", justifyContent: "space-between" }}>
// Index of the consent in the list
<span style={{ textAlign: "left" }}>{index + 1}.</span>
// Value of the consent
<span style={{ textAlign: "left" }}>{consent.value}</span>
// Button to allow the consent
<span
style={{ color: "green", cursor: "pointer" }}
onClick={() => handleAllow(consent.value)}>
Allow
</span>
</div>
))}
</div>
</div>
These functions manage the consent states by allowing or denying addresses.
// Function to handle allowing an address
const handleAllow = async (address) => {
// Check if the client object is available
if (client) {
// Confirm with the user if they want to allow the address
if (window.confirm("Are you sure you want to allow this address?")) {
// Refresh the consent list before performing the allow action
await refreshConsentList(client);
// Perform the allow action on the address
await client.contacts.allow([address]);
// Refresh the consent list after performing the allow action
await refreshConsentList(client);
// Trigger the onSubscribe callback with the address and state
onSubscribe(client.address, "allowed");
}
} else {
// Log an error if the client object is not available
console.error("Client is not set");
}
};
// Function to handle denying an address
const handleDeny = async (address) => {
// Check if the client object is available
if (client) {
// Confirm with the user if they want to deny the address
if (window.confirm("Are you sure you want to deny this address?")) {
// Refresh the consent list before performing the deny action
await refreshConsentList(client);
// Perform the deny action on the address
await client.contacts.deny([address]);
// Refresh the consent list after performing the deny action
await refreshConsentList(client);
// Trigger the onUnsubscribe callback with the address and state
onUnsubscribe(client.address, "denied");
}
} else {
// Log an error if the client object is not available
console.error("Client is not set");
}
};
This function generates and downloads the consent list as a CSV file.
const downloadCSV = async () => {
const csvRows = ["Address,State"];
consentList.forEach((consent) => {
csvRows.push(`${consent.address},${consent.state}`);
});
const blob = new Blob([csvRows.join("\n")], { type: "text/csv" });
const url = URL.createObjectURL(blob);
const link = document.createElement("a");
link.href = url;
link.download = "consent_list.csv";
link.click();
};
Finally, combine all the parts to render the ConsentManagement
component.
export function ConsentManagement({ env, onError }) {
// State and other functions go here
return (
<div>
<button onClick={handleClick}>Show Subscriptions</button>
// Render the consent list table // Render Allow/Deny buttons
<button onClick={downloadCSV}>Download CSV</button>
</div>
);
}
Always synchronize consent states: Before updating consent preferences on the network, ensure you refresh the consent list with refreshConsentList
. Update the network's consent list only in these scenarios:
- User Denies Contact: Set to
denied
if a user blocks or unsubscribes. - User Allows Contact: Set to
allowed
if a user subscribes or enables notifications. - Legacy Preferences: Align the network with any existing local preferences.
- User Response: Set to
allowed
if the user has engaged in conversation.
Neglecting these guidelines can result in consent state conflicts and compromise user privacy.