feat: redirect to AI consent settings when consent is missing
- requestAiConsent now redirects to /settings/general?highlight=aiConsent instead of opening the modal when called outside the settings page - Settings page highlights the consent section and scrolls to it - Adds consent.ai.settingsBanner i18n key (fr/en) - Keeps inline modal on /settings/general so the grant button still works
This commit is contained in:
@@ -1,11 +1,11 @@
|
|||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
import { useState } from 'react'
|
import { useState, useEffect, useRef } from 'react'
|
||||||
import Link from 'next/link'
|
import Link from 'next/link'
|
||||||
import { useLanguage } from '@/lib/i18n'
|
import { useLanguage } from '@/lib/i18n'
|
||||||
import { updateAISettings } from '@/app/actions/ai-settings'
|
import { updateAISettings } from '@/app/actions/ai-settings'
|
||||||
import { toast } from 'sonner'
|
import { toast } from 'sonner'
|
||||||
import { useRouter } from 'next/navigation'
|
import { useRouter, useSearchParams } from 'next/navigation'
|
||||||
import { Globe, Bell, Shield, Brain, HelpCircle } from 'lucide-react'
|
import { Globe, Bell, Shield, Brain, HelpCircle } from 'lucide-react'
|
||||||
import { motion } from 'motion/react'
|
import { motion } from 'motion/react'
|
||||||
import { openCookiePreferences } from '@/lib/consent/cookie-consent'
|
import { openCookiePreferences } from '@/lib/consent/cookie-consent'
|
||||||
@@ -27,6 +27,20 @@ export function GeneralSettingsClient({ initialSettings }: GeneralSettingsClient
|
|||||||
const { t, setLanguage: setContextLanguage } = useLanguage()
|
const { t, setLanguage: setContextLanguage } = useLanguage()
|
||||||
const { hasAiConsent, revokeConsent, requestAiConsent } = useAiConsent()
|
const { hasAiConsent, revokeConsent, requestAiConsent } = useAiConsent()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
const searchParams = useSearchParams()
|
||||||
|
const aiConsentSectionRef = useRef<HTMLDivElement>(null)
|
||||||
|
const highlightAiConsent = searchParams?.get('highlight') === 'aiConsent'
|
||||||
|
const [showConsentHighlight, setShowConsentHighlight] = useState(highlightAiConsent)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (highlightAiConsent && aiConsentSectionRef.current) {
|
||||||
|
setShowConsentHighlight(true)
|
||||||
|
aiConsentSectionRef.current.scrollIntoView({ behavior: 'smooth', block: 'center' })
|
||||||
|
const timer = setTimeout(() => setShowConsentHighlight(false), 4000)
|
||||||
|
return () => clearTimeout(timer)
|
||||||
|
}
|
||||||
|
}, [highlightAiConsent])
|
||||||
|
|
||||||
const [language, setLanguage] = useState(initialSettings.preferredLanguage || 'auto')
|
const [language, setLanguage] = useState(initialSettings.preferredLanguage || 'auto')
|
||||||
const [emailNotifications, setEmailNotifications] = useState(initialSettings.emailNotifications ?? false)
|
const [emailNotifications, setEmailNotifications] = useState(initialSettings.emailNotifications ?? false)
|
||||||
const [desktopNotifications, setDesktopNotifications] = useState(initialSettings.desktopNotifications ?? false)
|
const [desktopNotifications, setDesktopNotifications] = useState(initialSettings.desktopNotifications ?? false)
|
||||||
@@ -76,6 +90,12 @@ export function GeneralSettingsClient({ initialSettings }: GeneralSettingsClient
|
|||||||
{t('generalSettings.description')}
|
{t('generalSettings.description')}
|
||||||
</h3>
|
</h3>
|
||||||
|
|
||||||
|
{highlightAiConsent && (
|
||||||
|
<div className="rounded-xl border border-brand-accent/30 bg-brand-accent/10 p-4 text-sm font-medium text-ink">
|
||||||
|
{t('consent.ai.settingsBanner')}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||||
<div className="bg-white/40 dark:bg-white/5 border border-border rounded-xl p-8 space-y-6">
|
<div className="bg-white/40 dark:bg-white/5 border border-border rounded-xl p-8 space-y-6">
|
||||||
<div className="flex items-center gap-5">
|
<div className="flex items-center gap-5">
|
||||||
@@ -202,7 +222,15 @@ export function GeneralSettingsClient({ initialSettings }: GeneralSettingsClient
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="bg-white/40 dark:bg-white/5 border border-border rounded-xl p-8 space-y-6 md:col-span-2">
|
<div
|
||||||
|
ref={aiConsentSectionRef}
|
||||||
|
className={cn(
|
||||||
|
'bg-white/40 dark:bg-white/5 border rounded-xl p-8 space-y-6 md:col-span-2 transition-all duration-500',
|
||||||
|
showConsentHighlight
|
||||||
|
? 'border-brand-accent ring-2 ring-brand-accent/40 shadow-lg shadow-brand-accent/10'
|
||||||
|
: 'border-border'
|
||||||
|
)}
|
||||||
|
>
|
||||||
<div className="flex items-start justify-between gap-4">
|
<div className="flex items-start justify-between gap-4">
|
||||||
<div className="flex items-center gap-5">
|
<div className="flex items-center gap-5">
|
||||||
<div className="p-3 bg-paper dark:bg-white/10 rounded-2xl text-concrete border border-border">
|
<div className="p-3 bg-paper dark:bg-white/10 rounded-2xl text-concrete border border-border">
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { useReducedMotion } from 'motion/react'
|
|||||||
import { Inbox, Send, Bell, Mail } from 'lucide-react'
|
import { Inbox, Send, Bell, Mail } from 'lucide-react'
|
||||||
import { useLanguage } from '@/lib/i18n'
|
import { useLanguage } from '@/lib/i18n'
|
||||||
import { useAiConsent } from '@/components/legal/ai-consent-provider'
|
import { useAiConsent } from '@/components/legal/ai-consent-provider'
|
||||||
|
import { redirectToAiConsentSettings } from '@/lib/consent/ai-consent-redirect'
|
||||||
import { createNote } from '@/app/actions/notes'
|
import { createNote } from '@/app/actions/notes'
|
||||||
import { emitNoteChange } from '@/lib/note-change-sync'
|
import { emitNoteChange } from '@/lib/note-change-sync'
|
||||||
import { toast } from 'sonner'
|
import { toast } from 'sonner'
|
||||||
@@ -519,7 +520,10 @@ export function DashboardView({ onNoteSelect }: DashboardViewProps) {
|
|||||||
try {
|
try {
|
||||||
const res = await fetch('/api/ai/echo')
|
const res = await fetch('/api/ai/echo')
|
||||||
const json = await res.json()
|
const json = await res.json()
|
||||||
if (res.status === 403 && json.code === 'ai_consent_required') { await requestAiConsent(); return }
|
if (res.status === 403 && json.code === 'ai_consent_required') {
|
||||||
|
redirectToAiConsentSettings(router)
|
||||||
|
return
|
||||||
|
}
|
||||||
if (!res.ok) throw new Error(json.error)
|
if (!res.ok) throw new Error(json.error)
|
||||||
if (json.insight) {
|
if (json.insight) {
|
||||||
await reloadBriefingAndPaths()
|
await reloadBriefingAndPaths()
|
||||||
@@ -533,7 +537,7 @@ export function DashboardView({ onNoteSelect }: DashboardViewProps) {
|
|||||||
} finally {
|
} finally {
|
||||||
setEchoRefreshing(false)
|
setEchoRefreshing(false)
|
||||||
}
|
}
|
||||||
}, [requestAiConsent, reloadBriefingAndPaths, t])
|
}, [requestAiConsent, reloadBriefingAndPaths, t, router])
|
||||||
|
|
||||||
const handleEnableAi = useCallback(async () => {
|
const handleEnableAi = useCallback(async () => {
|
||||||
await requestAiConsent()
|
await requestAiConsent()
|
||||||
|
|||||||
@@ -2,7 +2,9 @@
|
|||||||
|
|
||||||
import { createContext, useContext, useState, useEffect, useRef, useCallback } from 'react'
|
import { createContext, useContext, useState, useEffect, useRef, useCallback } from 'react'
|
||||||
import { useSession } from 'next-auth/react'
|
import { useSession } from 'next-auth/react'
|
||||||
|
import { useRouter } from 'next/navigation'
|
||||||
import { getLocalStorageAiConsent, setLocalStorageAiConsent, removeLocalStorageAiConsent } from '@/lib/consent/ai-consent-client'
|
import { getLocalStorageAiConsent, setLocalStorageAiConsent, removeLocalStorageAiConsent } from '@/lib/consent/ai-consent-client'
|
||||||
|
import { redirectToAiConsentSettings } from '@/lib/consent/ai-consent-redirect'
|
||||||
import { updateAISettings } from '@/app/actions/ai-settings'
|
import { updateAISettings } from '@/app/actions/ai-settings'
|
||||||
import { AiConsentModal } from './ai-consent-modal'
|
import { AiConsentModal } from './ai-consent-modal'
|
||||||
import { toast } from 'sonner'
|
import { toast } from 'sonner'
|
||||||
@@ -24,6 +26,7 @@ interface AiConsentProviderProps {
|
|||||||
export function AiConsentProvider({ children, initialPersistentConsent = false }: AiConsentProviderProps) {
|
export function AiConsentProvider({ children, initialPersistentConsent = false }: AiConsentProviderProps) {
|
||||||
const { data: session, update: updateSession } = useSession()
|
const { data: session, update: updateSession } = useSession()
|
||||||
const { t } = useLanguage()
|
const { t } = useLanguage()
|
||||||
|
const router = useRouter()
|
||||||
|
|
||||||
const [persistentConsent, setPersistentConsent] = useState(false)
|
const [persistentConsent, setPersistentConsent] = useState(false)
|
||||||
const [sessionConsent, setSessionConsent] = useState(false)
|
const [sessionConsent, setSessionConsent] = useState(false)
|
||||||
@@ -60,11 +63,22 @@ export function AiConsentProvider({ children, initialPersistentConsent = false }
|
|||||||
return Promise.resolve(true)
|
return Promise.resolve(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
setModalOpen(true)
|
// On settings page we keep the inline modal; elsewhere redirect to settings
|
||||||
|
// so the user sees exactly where to grant consent.
|
||||||
|
if (typeof window !== 'undefined' && window.location.pathname.startsWith('/settings/general')) {
|
||||||
|
setModalOpen(true)
|
||||||
|
return new Promise<boolean>((resolve) => {
|
||||||
|
pendingResolveRef.current = resolve
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
redirectToAiConsentSettings(router, { replace: false })
|
||||||
return new Promise<boolean>((resolve) => {
|
return new Promise<boolean>((resolve) => {
|
||||||
pendingResolveRef.current = resolve
|
pendingResolveRef.current = resolve
|
||||||
|
pendingResolveRef.current(false)
|
||||||
|
pendingResolveRef.current = null
|
||||||
})
|
})
|
||||||
}, [hasAiConsent])
|
}, [hasAiConsent, router])
|
||||||
|
|
||||||
const handleConfirm = async (remember: boolean) => {
|
const handleConfirm = async (remember: boolean) => {
|
||||||
setModalOpen(false)
|
setModalOpen(false)
|
||||||
|
|||||||
24
memento-note/lib/consent/ai-consent-redirect.ts
Normal file
24
memento-note/lib/consent/ai-consent-redirect.ts
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
import type { AppRouterInstance } from 'next/dist/shared/lib/app-router-context.shared-runtime'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Redirects the user to General Settings with the AI consent section highlighted.
|
||||||
|
* Call this when an AI feature is blocked because the user has not granted consent.
|
||||||
|
*/
|
||||||
|
export function redirectToAiConsentSettings(
|
||||||
|
router: AppRouterInstance | ReturnType<typeof import('next/navigation').useRouter>,
|
||||||
|
options?: { replace?: boolean }
|
||||||
|
) {
|
||||||
|
const url = '/settings/general?highlight=aiConsent'
|
||||||
|
if (options?.replace) {
|
||||||
|
router.replace(url)
|
||||||
|
} else {
|
||||||
|
router.push(url)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the settings URL that highlights the AI consent section.
|
||||||
|
*/
|
||||||
|
export function getAiConsentSettingsUrl() {
|
||||||
|
return '/settings/general?highlight=aiConsent'
|
||||||
|
}
|
||||||
@@ -3807,7 +3807,8 @@
|
|||||||
"revokedToast": "AI processing consent successfully revoked.",
|
"revokedToast": "AI processing consent successfully revoked.",
|
||||||
"complianceBadge": "GDPR compliance",
|
"complianceBadge": "GDPR compliance",
|
||||||
"auditFailed": "Could not record your consent. Please try again.",
|
"auditFailed": "Could not record your consent. Please try again.",
|
||||||
"requiredToast": "Enable AI processing in Settings → AI to use this feature."
|
"requiredToast": "Enable AI processing in Settings → AI to use this feature.",
|
||||||
|
"settingsBanner": "To use this feature, enable AI processing below and click « Grant consent »."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"account": {
|
"account": {
|
||||||
|
|||||||
@@ -3813,7 +3813,8 @@
|
|||||||
"revokedToast": "Consentement IA révoqué avec succès.",
|
"revokedToast": "Consentement IA révoqué avec succès.",
|
||||||
"complianceBadge": "Conformité RGPD",
|
"complianceBadge": "Conformité RGPD",
|
||||||
"auditFailed": "Impossible d'enregistrer votre consentement. Réessayez.",
|
"auditFailed": "Impossible d'enregistrer votre consentement. Réessayez.",
|
||||||
"requiredToast": "Activez le traitement IA dans Paramètres → IA pour utiliser cette fonctionnalité."
|
"requiredToast": "Activez le traitement IA dans Paramètres → IA pour utiliser cette fonctionnalité.",
|
||||||
|
"settingsBanner": "Pour utiliser cette fonctionnalité, activez le traitement IA ci-dessous puis cliquez sur « Accorder le consentement »."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"account": {
|
"account": {
|
||||||
|
|||||||
Reference in New Issue
Block a user