various cookie banner fixes (#5027)

# Description of Changes

<!--
Please provide a summary of the changes, including:

- What was changed
- Why the change was made
- Any challenges encountered

Closes #(issue_number)
-->

---

## Checklist

### General

- [ ] I have read the [Contribution
Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md)
- [ ] I have read the [Stirling-PDF Developer
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md)
(if applicable)
- [ ] I have read the [How to add new languages to
Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md)
(if applicable)
- [ ] I have performed a self-review of my own code
- [ ] My changes generate no new warnings

### Documentation

- [ ] I have updated relevant docs on [Stirling-PDF's doc
repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/)
(if functionality has heavily changed)
- [ ] I have read the section [Add New Translation
Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags)
(for new translation tags only)

### Translations (if applicable)

- [ ] I ran
[`scripts/counter_translation.py`](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/docs/counter_translation.md)

### UI Changes (if applicable)

- [ ] Screenshots or videos demonstrating the UI changes are attached
(e.g., as comments or direct attachments in the PR)

### Testing (if applicable)

- [ ] I have tested my changes locally. Refer to the [Testing
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md#6-testing)
for more details.
This commit is contained in:
Reece Browne
2025-11-26 15:57:04 +00:00
committed by GitHub
parent d4765938a8
commit 1e34038b1e
7 changed files with 38 additions and 169 deletions
@@ -17,7 +17,6 @@ import { TourOrchestrationProvider } from "@app/contexts/TourOrchestrationContex
import { AdminTourOrchestrationProvider } from "@app/contexts/AdminTourOrchestrationContext"; import { AdminTourOrchestrationProvider } from "@app/contexts/AdminTourOrchestrationContext";
import { PageEditorProvider } from "@app/contexts/PageEditorContext"; import { PageEditorProvider } from "@app/contexts/PageEditorContext";
import { BannerProvider } from "@app/contexts/BannerContext"; import { BannerProvider } from "@app/contexts/BannerContext";
import { CookieConsentProvider } from "@app/contexts/CookieConsentContext";
import ErrorBoundary from "@app/components/shared/ErrorBoundary"; import ErrorBoundary from "@app/components/shared/ErrorBoundary";
import { useScarfTracking } from "@app/hooks/useScarfTracking"; import { useScarfTracking } from "@app/hooks/useScarfTracking";
import { useAppInitialization } from "@app/hooks/useAppInitialization"; import { useAppInitialization } from "@app/hooks/useAppInitialization";
@@ -83,7 +82,6 @@ export function AppProviders({ children, appConfigRetryOptions, appConfigProvide
retryOptions={appConfigRetryOptions} retryOptions={appConfigRetryOptions}
{...appConfigProviderProps} {...appConfigProviderProps}
> >
<CookieConsentProvider>
<ScarfTrackingInitializer /> <ScarfTrackingInitializer />
<FileContextProvider enableUrlSync={true} enablePersistence={true}> <FileContextProvider enableUrlSync={true} enablePersistence={true}>
<AppInitializer /> <AppInitializer />
@@ -114,7 +112,6 @@ export function AppProviders({ children, appConfigRetryOptions, appConfigProvide
</NavigationProvider> </NavigationProvider>
</ToolRegistryProvider> </ToolRegistryProvider>
</FileContextProvider> </FileContextProvider>
</CookieConsentProvider>
</AppConfigProvider> </AppConfigProvider>
</OnboardingProvider> </OnboardingProvider>
</BannerProvider> </BannerProvider>
@@ -1,7 +1,6 @@
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { usePreferences } from '@app/contexts/PreferencesContext'; import { usePreferences } from '@app/contexts/PreferencesContext';
import { useAppConfig } from '@app/contexts/AppConfigContext'; import { useAppConfig } from '@app/contexts/AppConfigContext';
import { useCookieConsentContext } from '@app/contexts/CookieConsentContext';
import { useOnboarding } from '@app/contexts/OnboardingContext'; import { useOnboarding } from '@app/contexts/OnboardingContext';
import type { LicenseNotice } from '@app/types/types'; import type { LicenseNotice } from '@app/types/types';
import { useNavigate, useLocation } from 'react-router-dom'; import { useNavigate, useLocation } from 'react-router-dom';
@@ -39,7 +38,6 @@ interface ServerLicenseModalHandlers {
export function useOnboardingFlow() { export function useOnboardingFlow() {
const { preferences, updatePreference } = usePreferences(); const { preferences, updatePreference } = usePreferences();
const { config, loading: configLoading } = useAppConfig(); const { config, loading: configLoading } = useAppConfig();
const { showCookieConsent, isReady: isCookieConsentReady } = useCookieConsentContext();
const { completeTour, tourType, isOpen } = useOnboarding(); const { completeTour, tourType, isOpen } = useOnboarding();
const location = useLocation(); const location = useLocation();
@@ -71,7 +69,6 @@ export function useOnboardingFlow() {
isOverLimit: false, isOverLimit: false,
requiresLicense: false, requiresLicense: false,
}); });
const [cookieBannerPending, setCookieBannerPending] = useState(false);
const [serverLicenseIntent, setServerLicenseIntent] = useState<'idle' | 'pending' | 'deferred'>('idle'); const [serverLicenseIntent, setServerLicenseIntent] = useState<'idle' | 'pending' | 'deferred'>('idle');
const [serverLicenseSource, setServerLicenseSource] = useState<'config' | 'self-reported' | null>(null); const [serverLicenseSource, setServerLicenseSource] = useState<'config' | 'self-reported' | null>(null);
const [isServerLicenseOpen, setIsServerLicenseOpen] = useState(false); const [isServerLicenseOpen, setIsServerLicenseOpen] = useState(false);
@@ -97,29 +94,6 @@ export function useOnboardingFlow() {
setToolPromptCompleted(true); setToolPromptCompleted(true);
}, []); }, []);
const maybeShowCookieBanner = useCallback(() => {
if (preferences.hasSeenCookieBanner) {
return;
}
if (!isCookieConsentReady || isServerLicenseOpen || serverLicenseIntent !== 'idle' || !toolPromptCompleted) {
setCookieBannerPending(true);
return;
}
setCookieBannerPending(false);
showCookieConsent();
updatePreference('hasSeenCookieBanner', true);
}, [
isCookieConsentReady,
isServerLicenseOpen,
preferences.hasSeenCookieBanner,
serverLicenseIntent,
showCookieConsent,
toolPromptCompleted,
updatePreference,
]);
const requestServerLicense = useCallback( const requestServerLicense = useCallback(
({ ({
deferUntilTourComplete = false, deferUntilTourComplete = false,
@@ -191,25 +165,6 @@ export function useOnboardingFlow() {
}; };
}, [requestServerLicense]); }, [requestServerLicense]);
useEffect(() => {
if (
cookieBannerPending &&
isCookieConsentReady &&
serverLicenseIntent === 'idle' &&
!isServerLicenseOpen &&
toolPromptCompleted
) {
maybeShowCookieBanner();
}
}, [
cookieBannerPending,
isCookieConsentReady,
isServerLicenseOpen,
serverLicenseIntent,
toolPromptCompleted,
maybeShowCookieBanner,
]);
useEffect(() => { useEffect(() => {
const isEligibleAdmin = const isEligibleAdmin =
isAdminUser || serverLicenseSource === 'self-reported' || licenseNotice.requiresLicense; isAdminUser || serverLicenseSource === 'self-reported' || licenseNotice.requiresLicense;
@@ -269,8 +224,7 @@ export function useOnboardingFlow() {
setHasShownServerLicense(true); setHasShownServerLicense(true);
setServerLicenseIntent('idle'); setServerLicenseIntent('idle');
setServerLicenseSource(null); setServerLicenseSource(null);
maybeShowCookieBanner(); }, []);
}, [maybeShowCookieBanner]);
useEffect(() => { useEffect(() => {
if (onboardingSessionMarkedRef.current) { if (onboardingSessionMarkedRef.current) {
@@ -309,11 +263,9 @@ export function useOnboardingFlow() {
setServerLicenseSource((prev) => prev ?? (isAdminUser ? 'config' : 'self-reported')); setServerLicenseSource((prev) => prev ?? (isAdminUser ? 'config' : 'self-reported'));
setServerLicenseIntent((prev) => (prev === 'pending' ? prev : 'pending')); setServerLicenseIntent((prev) => (prev === 'pending' ? prev : 'pending'));
} }
maybeShowCookieBanner();
}, [ }, [
completeTour, completeTour,
isAdminUser, isAdminUser,
maybeShowCookieBanner,
serverLicenseIntent, serverLicenseIntent,
serverLicenseSource, serverLicenseSource,
tourType, tourType,
@@ -1,6 +1,6 @@
import { Flex } from '@mantine/core'; import { Flex } from '@mantine/core';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { useCookieConsentContext } from '@app/contexts/CookieConsentContext'; import { useCookieConsent } from '@app/hooks/useCookieConsent';
interface FooterProps { interface FooterProps {
privacyPolicy?: string; privacyPolicy?: string;
@@ -20,7 +20,7 @@ export default function Footer({
analyticsEnabled = false analyticsEnabled = false
}: FooterProps) { }: FooterProps) {
const { t } = useTranslation(); const { t } = useTranslation();
const { showCookiePreferences } = useCookieConsentContext(); const { showCookiePreferences } = useCookieConsent({ analyticsEnabled });
// Helper to check if a value is valid (not null/undefined/empty string) // Helper to check if a value is valid (not null/undefined/empty string)
const isValidLink = (link?: string) => link && link.trim().length > 0; const isValidLink = (link?: string) => link && link.trim().length > 0;
@@ -1,45 +0,0 @@
import React, { createContext, useContext, useMemo } from 'react';
import { useCookieConsent } from '@app/hooks/useCookieConsent';
import { useAppConfig } from '@app/contexts/AppConfigContext';
interface CookieConsentContextValue {
isReady: boolean;
showCookieConsent: () => void;
showCookiePreferences: () => void;
hasResponded: boolean;
}
const CookieConsentContext = createContext<CookieConsentContextValue | undefined>(undefined);
export const CookieConsentProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
const { config } = useAppConfig();
const analyticsEnabled = config ? config.enableAnalytics !== false : false;
const {
showCookieConsent,
showCookiePreferences,
isInitialized,
hasResponded,
} = useCookieConsent({ analyticsEnabled });
const value = useMemo<CookieConsentContextValue>(() => ({
isReady: analyticsEnabled && isInitialized,
showCookieConsent,
showCookiePreferences,
hasResponded,
}), [analyticsEnabled, hasResponded, isInitialized, showCookieConsent, showCookiePreferences]);
return (
<CookieConsentContext.Provider value={value}>
{children}
</CookieConsentContext.Provider>
);
};
export const useCookieConsentContext = (): CookieConsentContextValue => {
const context = useContext(CookieConsentContext);
if (!context) {
throw new Error('useCookieConsentContext must be used within a CookieConsentProvider');
}
return context;
};
+27 -56
View File
@@ -10,7 +10,6 @@ declare global {
show: (show?: boolean) => void; show: (show?: boolean) => void;
acceptedCategory: (category: string) => boolean; acceptedCategory: (category: string) => boolean;
acceptedService: (serviceName: string, category: string) => boolean; acceptedService: (serviceName: string, category: string) => boolean;
validConsent?: () => boolean;
}; };
} }
} }
@@ -20,66 +19,46 @@ interface CookieConsentConfig {
} }
export const useCookieConsent = ({ export const useCookieConsent = ({
analyticsEnabled = false, analyticsEnabled = false
}: CookieConsentConfig = {}) => { }: CookieConsentConfig = {}) => {
const { t } = useTranslation(); const { t } = useTranslation();
const { config } = useAppConfig(); const { config } = useAppConfig();
const [isInitialized, setIsInitialized] = useState(false); const [isInitialized, setIsInitialized] = useState(false);
const [hasRespondedInternal, setHasRespondedInternal] = useState(false);
useEffect(() => { useEffect(() => {
if (typeof window === 'undefined') { if (!analyticsEnabled) {
console.log('Cookie consent not enabled - analyticsEnabled is false');
return; return;
} }
const markResponded = () => setHasRespondedInternal(true); // Load the cookie consent CSS files first (always needed)
const removeConsentListeners = () => { const mainCSS = document.createElement('link');
window.removeEventListener('cc:onFirstConsent', markResponded); mainCSS.rel = 'stylesheet';
window.removeEventListener('cc:onConsent', markResponded); mainCSS.href = `${BASE_PATH}css/cookieconsent.css`;
window.removeEventListener('cc:onChange', markResponded); if (!document.querySelector(`link[href="${mainCSS.href}"]`)) {
}; document.head.appendChild(mainCSS);
window.addEventListener('cc:onFirstConsent', markResponded);
window.addEventListener('cc:onConsent', markResponded);
window.addEventListener('cc:onChange', markResponded);
if (analyticsEnabled) {
setHasRespondedInternal(window.CookieConsent?.validConsent?.() ?? false);
} }
if (!analyticsEnabled) { const customCSS = document.createElement('link');
console.log('Cookie consent not enabled - analyticsEnabled is false'); customCSS.rel = 'stylesheet';
setHasRespondedInternal(false); customCSS.href = `${BASE_PATH}css/cookieconsentCustomisation.css`;
return () => { if (!document.querySelector(`link[href="${customCSS.href}"]`)) {
removeConsentListeners(); document.head.appendChild(customCSS);
};
} }
// Prevent double initialization // Prevent double initialization
if (window.CookieConsent) { if (window.CookieConsent) {
setIsInitialized(true); setIsInitialized(true);
if (window.CookieConsent.validConsent?.()) { // Force show the modal if it exists but isn't visible
markResponded(); setTimeout(() => {
window.CookieConsent?.show();
}, 100);
return;
} }
return () => {
removeConsentListeners();
};
}
// Load the cookie consent CSS files first
const mainCSS = document.createElement('link');
mainCSS.rel = 'stylesheet';
mainCSS.href = `${BASE_PATH}/css/cookieconsent.css`;
document.head.appendChild(mainCSS);
const customCSS = document.createElement('link');
customCSS.rel = 'stylesheet';
customCSS.href = `${BASE_PATH}/css/cookieconsentCustomisation.css`;
document.head.appendChild(customCSS);
// Load the cookie consent library // Load the cookie consent library
const script = document.createElement('script'); const script = document.createElement('script');
script.src = `${BASE_PATH}/js/thirdParty/cookieconsent.umd.js`; script.src = `${BASE_PATH}js/thirdParty/cookieconsent.umd.js`;
script.onload = () => { script.onload = () => {
// Small delay to ensure DOM is ready // Small delay to ensure DOM is ready
setTimeout(() => { setTimeout(() => {
@@ -141,7 +120,7 @@ export const useCookieConsent = ({
// Initialize cookie consent with full configuration // Initialize cookie consent with full configuration
try { try {
window.CookieConsent.run({ window.CookieConsent.run({
autoShow: false, autoShow: true,
hideFromBots: false, hideFromBots: false,
guiOptions: { guiOptions: {
consentModal: { consentModal: {
@@ -227,20 +206,17 @@ export const useCookieConsent = ({
} }
} }
} }
}, }
onFirstConsent: markResponded,
onConsent: markResponded,
onChange: markResponded,
}); });
// Force show after initialization
setTimeout(() => {
window.CookieConsent?.show();
}, 200);
} catch (error) { } catch (error) {
console.error('Error initializing CookieConsent:', error); console.error('Error initializing CookieConsent:', error);
} }
if (window.CookieConsent?.validConsent?.()) {
markResponded();
} else {
setHasRespondedInternal(false);
}
setIsInitialized(true); setIsInitialized(true);
}, 100); // Small delay to ensure DOM is ready }, 100); // Small delay to ensure DOM is ready
}; };
@@ -252,8 +228,6 @@ export const useCookieConsent = ({
document.head.appendChild(script); document.head.appendChild(script);
return () => { return () => {
// Cleanup event listeners
removeConsentListeners();
// Cleanup script and CSS when component unmounts // Cleanup script and CSS when component unmounts
if (document.head.contains(script)) { if (document.head.contains(script)) {
document.head.removeChild(script); document.head.removeChild(script);
@@ -286,13 +260,10 @@ export const useCookieConsent = ({
return window.CookieConsent.acceptedService(service, category); return window.CookieConsent.acceptedService(service, category);
}, []); }, []);
const effectiveHasResponded = analyticsEnabled ? hasRespondedInternal : true;
return { return {
showCookieConsent, showCookieConsent,
showCookiePreferences, showCookiePreferences,
isServiceAccepted, isServiceAccepted,
isInitialized, isInitialized,
hasResponded: effectiveHasResponded,
}; };
}; };
+2 -4
View File
@@ -9,7 +9,6 @@ import { useIsMobile } from "@app/hooks/useIsMobile";
import { useAppConfig } from "@app/contexts/AppConfigContext"; import { useAppConfig } from "@app/contexts/AppConfigContext";
import { useLogoPath } from "@app/hooks/useLogoPath"; import { useLogoPath } from "@app/hooks/useLogoPath";
import { useLogoAssets } from '@app/hooks/useLogoAssets'; import { useLogoAssets } from '@app/hooks/useLogoAssets';
import { useCookieConsentContext } from "@app/contexts/CookieConsentContext";
import { useFileContext } from "@app/contexts/file/fileHooks"; import { useFileContext } from "@app/contexts/file/fileHooks";
import { useNavigationActions } from "@app/contexts/NavigationContext"; import { useNavigationActions } from "@app/contexts/NavigationContext";
import { useViewer } from "@app/contexts/ViewerContext"; import { useViewer } from "@app/contexts/ViewerContext";
@@ -49,7 +48,6 @@ export default function HomePage() {
const { openFilesModal } = useFilesModalContext(); const { openFilesModal } = useFilesModalContext();
const { colorScheme } = useMantineColorScheme(); const { colorScheme } = useMantineColorScheme();
const { config } = useAppConfig(); const { config } = useAppConfig();
const { hasResponded: cookieConsentResponded } = useCookieConsentContext();
const isMobile = useIsMobile(); const isMobile = useIsMobile();
const sliderRef = useRef<HTMLDivElement | null>(null); const sliderRef = useRef<HTMLDivElement | null>(null);
const [activeMobileView, setActiveMobileView] = useState<MobileView>("tools"); const [activeMobileView, setActiveMobileView] = useState<MobileView>("tools");
@@ -64,10 +62,10 @@ export default function HomePage() {
// Show admin analytics choice modal if analytics settings not configured // Show admin analytics choice modal if analytics settings not configured
useEffect(() => { useEffect(() => {
if (config && config.enableAnalytics === null && cookieConsentResponded) { if (config && config.enableAnalytics === null) {
setShowAnalyticsModal(true); setShowAnalyticsModal(true);
} }
}, [config, cookieConsentResponded]); }, [config]);
// Auto-switch to viewer when going from 0 to 1 file // Auto-switch to viewer when going from 0 to 1 file
useEffect(() => { useEffect(() => {
@@ -1,7 +1,6 @@
import React, { useState, useEffect, useCallback } from 'react'; import React, { useState, useEffect, useCallback } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import { useCookieConsentContext } from '@app/contexts/CookieConsentContext';
import { useOnboarding } from '@app/contexts/OnboardingContext'; import { useOnboarding } from '@app/contexts/OnboardingContext';
import { useCheckout } from '@app/contexts/CheckoutContext'; import { useCheckout } from '@app/contexts/CheckoutContext';
import { InfoBanner } from '@app/components/shared/InfoBanner'; import { InfoBanner } from '@app/components/shared/InfoBanner';
@@ -22,7 +21,6 @@ const WEEK_IN_MS = 7 * 24 * 60 * 60 * 1000;
const UpgradeBanner: React.FC = () => { const UpgradeBanner: React.FC = () => {
const { t } = useTranslation(); const { t } = useTranslation();
const navigate = useNavigate(); const navigate = useNavigate();
const { hasResponded: cookieChoiceMade } = useCookieConsentContext();
const { isOpen: tourOpen } = useOnboarding(); const { isOpen: tourOpen } = useOnboarding();
const { openCheckout } = useCheckout(); const { openCheckout } = useCheckout();
const { const {
@@ -159,7 +157,6 @@ const UpgradeBanner: React.FC = () => {
shouldShowFriendlyBase && shouldShowFriendlyBase &&
!licenseLoading && !licenseLoading &&
effectiveTotalUsersLoaded && effectiveTotalUsersLoaded &&
cookieChoiceMade &&
!tourOpen && !tourOpen &&
!sessionBlocked, !sessionBlocked,
); );
@@ -168,7 +165,6 @@ const UpgradeBanner: React.FC = () => {
: Boolean( : Boolean(
shouldShowUrgentBase && shouldShowUrgentBase &&
!licenseLoading && !licenseLoading &&
cookieChoiceMade &&
!tourOpen && !tourOpen &&
!sessionBlocked, !sessionBlocked,
); );