Add cookie_policy_url field to cookie banners
Introduce a required cookie_policy_url alongside the existing
privacy_policy_url (now optional) so banners can link directly to a
dedicated cookie policy — a compliance best practice recommended by
CNIL, ICO, and the EDPB. Existing rows are seeded from their current
privacy_policy_url value.
Both {{cookie_policy_link}} and {{privacy_policy_link}} placeholders
are supported independently in banner description translations.
Signed-off-by: Émile Ré <emile@getprobo.com>
This commit is contained in:
@@ -60,6 +60,7 @@ export default function NewCookieBannerPage() {
|
||||
|
||||
const [name, setName] = useState("");
|
||||
const [origin, setOrigin] = useState("");
|
||||
const [cookiePolicyUrl, setCookiePolicyUrl] = useState("");
|
||||
const [privacyPolicyUrl, setPrivacyPolicyUrl] = useState("");
|
||||
const [consentExpiryDays, setConsentExpiryDays] = useState("365");
|
||||
const [consentMode, setConsentMode] = useState("OPT_IN");
|
||||
@@ -73,7 +74,8 @@ export default function NewCookieBannerPage() {
|
||||
organizationId,
|
||||
name,
|
||||
origin,
|
||||
privacyPolicyUrl,
|
||||
cookiePolicyUrl,
|
||||
privacyPolicyUrl: privacyPolicyUrl || undefined,
|
||||
consentExpiryDays: parseInt(consentExpiryDays, 10),
|
||||
consentMode: consentMode as "OPT_IN" | "OPT_OUT",
|
||||
},
|
||||
@@ -136,12 +138,20 @@ export default function NewCookieBannerPage() {
|
||||
/>
|
||||
</Field>
|
||||
|
||||
<Field label={__("Cookie Policy URL")}>
|
||||
<Input
|
||||
value={cookiePolicyUrl}
|
||||
onChange={e => setCookiePolicyUrl(e.target.value)}
|
||||
placeholder="https://example.com/cookies"
|
||||
required
|
||||
/>
|
||||
</Field>
|
||||
|
||||
<Field label={__("Privacy Policy URL")}>
|
||||
<Input
|
||||
value={privacyPolicyUrl}
|
||||
onChange={e => setPrivacyPolicyUrl(e.target.value)}
|
||||
placeholder="https://example.com/privacy"
|
||||
required
|
||||
/>
|
||||
</Field>
|
||||
|
||||
|
||||
@@ -27,6 +27,7 @@ const bannerSettingsFormFragment = graphql`
|
||||
id
|
||||
name
|
||||
origin
|
||||
cookiePolicyUrl
|
||||
privacyPolicyUrl
|
||||
consentExpiryDays
|
||||
consentMode
|
||||
@@ -40,6 +41,7 @@ const updateBannerMutation = graphql`
|
||||
cookieBanner {
|
||||
id
|
||||
name
|
||||
cookiePolicyUrl
|
||||
privacyPolicyUrl
|
||||
consentExpiryDays
|
||||
consentMode
|
||||
@@ -67,7 +69,8 @@ export function BannerSettingsForm({ cookieBannerKey }: BannerSettingsFormProps)
|
||||
const [updateBanner, isUpdating] = useMutation<BannerSettingsFormMutation>(updateBannerMutation);
|
||||
|
||||
const [name, setName] = useState(banner.name);
|
||||
const [privacyPolicyUrl, setPrivacyPolicyUrl] = useState(banner.privacyPolicyUrl);
|
||||
const [cookiePolicyUrl, setCookiePolicyUrl] = useState(banner.cookiePolicyUrl);
|
||||
const [privacyPolicyUrl, setPrivacyPolicyUrl] = useState(banner.privacyPolicyUrl ?? "");
|
||||
const [consentExpiryDays, setConsentExpiryDays] = useState(String(banner.consentExpiryDays));
|
||||
const [consentMode, setConsentMode] = useState(banner.consentMode);
|
||||
const [defaultLanguage, setDefaultLanguage] = useState(banner.defaultLanguage);
|
||||
@@ -80,7 +83,8 @@ export function BannerSettingsForm({ cookieBannerKey }: BannerSettingsFormProps)
|
||||
input: {
|
||||
cookieBannerId: banner.id,
|
||||
name,
|
||||
privacyPolicyUrl,
|
||||
cookiePolicyUrl,
|
||||
privacyPolicyUrl: privacyPolicyUrl || undefined,
|
||||
consentExpiryDays: parseInt(consentExpiryDays, 10),
|
||||
consentMode: consentMode,
|
||||
defaultLanguage,
|
||||
@@ -108,8 +112,12 @@ export function BannerSettingsForm({ cookieBannerKey }: BannerSettingsFormProps)
|
||||
<Input value={banner.origin} disabled />
|
||||
</Field>
|
||||
|
||||
<Field label={__("Cookie Policy URL")}>
|
||||
<Input value={cookiePolicyUrl} onChange={e => setCookiePolicyUrl(e.target.value)} required />
|
||||
</Field>
|
||||
|
||||
<Field label={__("Privacy Policy URL")}>
|
||||
<Input value={privacyPolicyUrl} onChange={e => setPrivacyPolicyUrl(e.target.value)} required />
|
||||
<Input value={privacyPolicyUrl} onChange={e => setPrivacyPolicyUrl(e.target.value)} />
|
||||
</Field>
|
||||
|
||||
<div className="grid grid-cols-3 gap-4">
|
||||
|
||||
@@ -21,19 +21,10 @@ interface BannerPreviewProps {
|
||||
buttonRejectAll: string;
|
||||
buttonCustomize: string;
|
||||
privacyPolicyLinkText: string;
|
||||
cookiePolicyLinkText: string;
|
||||
showBranding: boolean;
|
||||
}
|
||||
|
||||
function interpolateDescription(
|
||||
description: string,
|
||||
linkText: string,
|
||||
): string {
|
||||
return description.replaceAll(
|
||||
"{{privacy_policy_link}}",
|
||||
linkText,
|
||||
);
|
||||
}
|
||||
|
||||
export function BannerPreview({
|
||||
bannerTitle,
|
||||
bannerDescription,
|
||||
@@ -41,10 +32,11 @@ export function BannerPreview({
|
||||
buttonRejectAll,
|
||||
buttonCustomize,
|
||||
privacyPolicyLinkText,
|
||||
cookiePolicyLinkText,
|
||||
showBranding,
|
||||
}: BannerPreviewProps) {
|
||||
const descriptionParts = bannerDescription.split("{{privacy_policy_link}}");
|
||||
const hasPlaceholder = descriptionParts.length > 1;
|
||||
const cookieParts = bannerDescription.split("{{cookie_policy_link}}");
|
||||
const hasAnyPlaceholder = cookieParts.length > 1 || bannerDescription.includes("{{privacy_policy_link}}");
|
||||
|
||||
return (
|
||||
<div
|
||||
@@ -78,27 +70,27 @@ export function BannerPreview({
|
||||
margin: "0 0 20px",
|
||||
}}
|
||||
>
|
||||
{hasPlaceholder
|
||||
? descriptionParts.map((part, i) => (
|
||||
<span key={i}>
|
||||
{part}
|
||||
{i < descriptionParts.length - 1 && (
|
||||
<a
|
||||
href="#"
|
||||
onClick={e => e.preventDefault()}
|
||||
style={{
|
||||
color: "var(--probo-accent, #1a1a1a)",
|
||||
textDecoration: "underline",
|
||||
}}
|
||||
>
|
||||
{privacyPolicyLinkText}
|
||||
{hasAnyPlaceholder
|
||||
? cookieParts.map((cookiePart, ci) => (
|
||||
<span key={`c${ci}`}>
|
||||
{cookiePart.split("{{privacy_policy_link}}").map((privPart, pi, arr) => (
|
||||
<span key={`p${pi}`}>
|
||||
{privPart}
|
||||
{pi < arr.length - 1 && (
|
||||
<a href="#" onClick={e => e.preventDefault()} style={{ color: "var(--probo-accent, #1a1a1a)", textDecoration: "underline" }}>
|
||||
{privacyPolicyLinkText}
|
||||
</a>
|
||||
)}
|
||||
</span>
|
||||
))}
|
||||
{ci < cookieParts.length - 1 && (
|
||||
<a href="#" onClick={e => e.preventDefault()} style={{ color: "var(--probo-accent, #1a1a1a)", textDecoration: "underline" }}>
|
||||
{cookiePolicyLinkText}
|
||||
</a>
|
||||
)}
|
||||
</span>
|
||||
))
|
||||
: (
|
||||
interpolateDescription(bannerDescription, privacyPolicyLinkText)
|
||||
)}
|
||||
: bannerDescription}
|
||||
</p>
|
||||
<div
|
||||
style={{
|
||||
|
||||
@@ -35,6 +35,10 @@ export function BannerTranslationSection({
|
||||
const buttonAcceptAll = useWatch({ control, name: "button_accept_all" });
|
||||
const buttonRejectAll = useWatch({ control, name: "button_reject_all" });
|
||||
const buttonCustomize = useWatch({ control, name: "button_customize" });
|
||||
const cookiePolicyLinkText = useWatch({
|
||||
control,
|
||||
name: "cookie_policy_link_text",
|
||||
});
|
||||
const privacyPolicyLinkText = useWatch({
|
||||
control,
|
||||
name: "privacy_policy_link_text",
|
||||
@@ -62,7 +66,7 @@ export function BannerTranslationSection({
|
||||
<Field
|
||||
label={__(TRANSLATION_LABELS.banner_description)}
|
||||
>
|
||||
<p className="text-xs text-txt-secondary mb-2">{"Use {{privacy_policy_link}} to insert the privacy policy link."}</p>
|
||||
<p className="text-xs text-txt-secondary mb-2">{"Use {{cookie_policy_link}} and {{privacy_policy_link}} to insert policy links."}</p>
|
||||
<Textarea {...field} rows={3} />
|
||||
</Field>
|
||||
)}
|
||||
@@ -87,12 +91,23 @@ export function BannerTranslationSection({
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<Controller
|
||||
control={control}
|
||||
name="button_customize"
|
||||
render={({ field }) => (
|
||||
<Field label={__(TRANSLATION_LABELS.button_customize)}>
|
||||
<Input {...field} />
|
||||
</Field>
|
||||
)}
|
||||
/>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<Controller
|
||||
control={control}
|
||||
name="button_customize"
|
||||
name="cookie_policy_link_text"
|
||||
render={({ field }) => (
|
||||
<Field label={__(TRANSLATION_LABELS.button_customize)}>
|
||||
<Field
|
||||
label={__(TRANSLATION_LABELS.cookie_policy_link_text)}
|
||||
>
|
||||
<Input {...field} />
|
||||
</Field>
|
||||
)}
|
||||
@@ -119,6 +134,7 @@ export function BannerTranslationSection({
|
||||
buttonAcceptAll={buttonAcceptAll}
|
||||
buttonRejectAll={buttonRejectAll}
|
||||
buttonCustomize={buttonCustomize}
|
||||
cookiePolicyLinkText={cookiePolicyLinkText}
|
||||
privacyPolicyLinkText={privacyPolicyLinkText}
|
||||
showBranding={showBranding}
|
||||
/>
|
||||
|
||||
@@ -25,6 +25,7 @@ export const BANNER_KEYS = [
|
||||
"button_accept_all",
|
||||
"button_reject_all",
|
||||
"button_customize",
|
||||
"cookie_policy_link_text",
|
||||
"privacy_policy_link_text",
|
||||
] as const;
|
||||
|
||||
@@ -60,6 +61,7 @@ export const TRANSLATION_LABELS: Record<string, string> = {
|
||||
button_accept_all: "Accept all button",
|
||||
button_reject_all: "Reject all button",
|
||||
button_customize: "Customize button",
|
||||
cookie_policy_link_text: "Cookie policy link text",
|
||||
privacy_policy_link_text: "Privacy policy link text",
|
||||
panel_title: "Panel title",
|
||||
panel_description: "Panel description",
|
||||
|
||||
Reference in New Issue
Block a user