- Privacy page: news opt-in per channel with history, download my data (JSON + PDF), correction and deletion requests - Bulk messages are an Event update or News & announcements; news only reaches people who opted in, with unsubscribe links, one-click List-Unsubscribe headers and "Reply STOP" on WhatsApp - WhatsApp STOP via WAWP's message webhook, checked against a secret in the webhook URL - Sign-up opt-in box; existing accounts kept opted in and asked once - Admin -> Data requests with 30-day due dates and a daily reminder job - Deleting an account is a request; anonymising now covers attendees, form answers, consent details, payer details in provider payloads, etc. - Unit tests (privacy.test.js) and e2e (privacy.spec.ts); docs Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012mt6TCv85T9VKRq3cV7rXq
45 lines
1.9 KiB
TypeScript
45 lines
1.9 KiB
TypeScript
"use client";
|
|
|
|
import React from "react";
|
|
|
|
export type MessageKind = "update" | "news";
|
|
|
|
const OPTIONS: { value: MessageKind; label: string; description: string }[] = [
|
|
{
|
|
value: "update",
|
|
label: "Event update",
|
|
description: "About an event the recipients registered for — a reminder, venue or time change, what to bring. Goes to everyone selected.",
|
|
},
|
|
{
|
|
value: "news",
|
|
label: "News & announcements",
|
|
description: "Promoting events or sharing news. Only goes to people who opted in, and includes an unsubscribe link.",
|
|
},
|
|
];
|
|
|
|
/** POPIA: what kind of bulk message this is — decides who it may go to (see utils/privacy.js). */
|
|
export function MessageKindPicker({ value, onChange, name }: { value: MessageKind; onChange: (kind: MessageKind) => void; name: string }) {
|
|
return (
|
|
<fieldset>
|
|
<legend className="block text-xs text-gray-600 mb-1">Message type</legend>
|
|
<div className="grid sm:grid-cols-2 gap-2">
|
|
{OPTIONS.map(o => (
|
|
<label key={o.value} className={`flex items-start gap-2 border rounded-lg p-2.5 cursor-pointer ${value === o.value ? "border-brand-500 bg-brand-50" : "border-gray-200 bg-white hover:bg-gray-50"}`}>
|
|
<input type="radio" name={name} className="mt-0.5 accent-brand-600" checked={value === o.value} onChange={() => onChange(o.value)} />
|
|
<span>
|
|
<span className="block text-sm font-medium text-gray-800">{o.label}</span>
|
|
<span className="block text-[11px] text-gray-500">{o.description}</span>
|
|
</span>
|
|
</label>
|
|
))}
|
|
</div>
|
|
</fieldset>
|
|
);
|
|
}
|
|
|
|
/** "Matched 12 recipient(s)." plus how many news skipped. */
|
|
export function matchedText(res: { matched?: number; skipped?: number } | null | undefined) {
|
|
const skipped = res?.skipped ?? 0;
|
|
return `Matched ${res?.matched ?? 0} recipient(s).${skipped ? ` ${skipped} skipped — not opted in to news or unsubscribed.` : ""}`;
|
|
}
|