- Admin -> Privacy (POPIA): edit the privacy notice (built-in text as the
start, placeholders for org/IO/operators), drafts, preview, numbered
versions; earlier versions readable on /legal/privacy?version=N
- "Significant change" versions are accepted once after login
(NoticePrompt); sign-up records the version accepted
- Operator register (seeded with the services the app uses), listed in
the notice via {operators}
- Consent log (news opt-ins, notice acceptances, parental consent) and
access log (report exports, registers, certificates, group lists, form
answers, consent/medical lists, backups, impersonation)
- Information Officer on the contact page
- Unit tests (privacyNotice.test.js), e2e (privacy-notice.spec.ts); docs
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012mt6TCv85T9VKRq3cV7rXq
74 lines
3.2 KiB
TypeScript
74 lines
3.2 KiB
TypeScript
"use client";
|
|
|
|
import React, { useEffect, useState } from "react";
|
|
import Link from "next/link";
|
|
import { ScrollText } from "lucide-react";
|
|
import { useAuth } from "@/hooks/useAuth";
|
|
import { apiFetch } from "@/lib/api";
|
|
import { errorMessage } from "@/lib/utils";
|
|
import type { PublicNotice } from "@/components/shared/PrivacyNoticeSection";
|
|
|
|
/**
|
|
* POPIA: a privacy notice version marked as a significant change has been published, and
|
|
* this person accepted an older one. Asked once after login; it can't be waved away, but the
|
|
* notice and the Privacy page (to delete the account instead) stay reachable.
|
|
*/
|
|
export function NoticePrompt({ onDone }: { onDone: () => void }) {
|
|
const { token } = useAuth();
|
|
const [notice, setNotice] = useState<PublicNotice | null>(null);
|
|
const [saving, setSaving] = useState(false);
|
|
const [error, setError] = useState<string | null>(null);
|
|
|
|
useEffect(() => {
|
|
apiFetch<PublicNotice>("/api/privacy/notice").then(setNotice).catch(() => onDone());
|
|
}, [onDone]);
|
|
|
|
if (!notice) return null;
|
|
|
|
const accept = async () => {
|
|
if (!token) return;
|
|
setSaving(true);
|
|
setError(null);
|
|
try {
|
|
await apiFetch("/api/privacy/me/notice/accept", { method: "POST", authToken: token });
|
|
onDone();
|
|
} catch (e) {
|
|
setError(errorMessage(e) || "Could not save. Please try again.");
|
|
} finally {
|
|
setSaving(false);
|
|
}
|
|
};
|
|
|
|
// The newest version that needed accepting, for "what changed".
|
|
const changes = notice.versions.find(v => v.requiresAcceptance)?.changeSummary || notice.changeSummary;
|
|
|
|
return (
|
|
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/40 p-4" role="dialog" aria-modal="true" aria-labelledby="notice-prompt-title">
|
|
<div className="w-full max-w-md rounded-xl bg-white p-6 shadow-xl">
|
|
<div className="flex items-center gap-2 mb-3">
|
|
<div className="w-9 h-9 rounded-lg flex items-center justify-center bg-brand-50 text-brand-600 shrink-0">
|
|
<ScrollText className="w-5 h-5" />
|
|
</div>
|
|
<h2 id="notice-prompt-title" className="text-lg font-semibold text-gray-900">We've updated our privacy notice</h2>
|
|
</div>
|
|
{changes && (
|
|
<div className="mb-3 p-3 rounded-lg bg-gray-50 border text-sm text-gray-700 whitespace-pre-line">
|
|
<span className="font-medium">What changed: </span>{changes}
|
|
</div>
|
|
)}
|
|
<p className="text-sm text-gray-600 mb-5">
|
|
Please read the <Link href="/legal/privacy" target="_blank" className="text-brand-600 hover:underline">privacy notice</Link> and
|
|
accept it to carry on using your account. If you don't agree, you can{" "}
|
|
<Link href="/dashboard/user/privacy" className="text-brand-600 hover:underline">delete your account</Link> instead.
|
|
</p>
|
|
{error && <p className="text-sm text-red-600 mb-3">{error}</p>}
|
|
<div className="flex justify-end">
|
|
<button onClick={accept} disabled={saving} className="px-4 py-2 rounded-lg bg-brand-600 text-white text-sm font-medium hover:bg-brand-700 disabled:opacity-50">
|
|
{saving ? "Saving…" : "I accept"}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|