mirror of
https://github.com/Frooodle/Stirling-PDF.git
synced 2025-12-18 20:04:17 +01:00
Cookie changes
This commit is contained in:
parent
af57ae02dd
commit
32c0fc0f1c
@ -320,6 +320,8 @@ public class ApplicationProperties {
|
||||
private String tessdataDir;
|
||||
private Boolean enableAlphaFunctionality;
|
||||
private Boolean enableAnalytics;
|
||||
private Boolean enablePosthog;
|
||||
private Boolean enableScarf;
|
||||
private Datasource datasource;
|
||||
private Boolean disableSanitize;
|
||||
private int maxDPI;
|
||||
@ -332,6 +334,18 @@ public class ApplicationProperties {
|
||||
public boolean isAnalyticsEnabled() {
|
||||
return this.getEnableAnalytics() != null && this.getEnableAnalytics();
|
||||
}
|
||||
|
||||
public boolean isPosthogEnabled() {
|
||||
// Treat null as enabled when analytics is enabled
|
||||
return this.isAnalyticsEnabled()
|
||||
&& (this.getEnablePosthog() == null || this.getEnablePosthog());
|
||||
}
|
||||
|
||||
public boolean isScarfEnabled() {
|
||||
// Treat null as enabled when analytics is enabled
|
||||
return this.isAnalyticsEnabled()
|
||||
&& (this.getEnableScarf() == null || this.getEnableScarf());
|
||||
}
|
||||
}
|
||||
|
||||
@Data
|
||||
|
||||
@ -56,7 +56,7 @@ public class PostHogService {
|
||||
}
|
||||
|
||||
private void captureSystemInfo() {
|
||||
if (!applicationProperties.getSystem().isAnalyticsEnabled()) {
|
||||
if (!applicationProperties.getSystem().isPosthogEnabled()) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
@ -67,7 +67,7 @@ public class PostHogService {
|
||||
}
|
||||
|
||||
public void captureEvent(String eventName, Map<String, Object> properties) {
|
||||
if (!applicationProperties.getSystem().isAnalyticsEnabled()) {
|
||||
if (!applicationProperties.getSystem().isPosthogEnabled()) {
|
||||
return;
|
||||
}
|
||||
|
||||
@ -325,6 +325,14 @@ public class PostHogService {
|
||||
properties,
|
||||
"system_enableAnalytics",
|
||||
applicationProperties.getSystem().isAnalyticsEnabled());
|
||||
addIfNotEmpty(
|
||||
properties,
|
||||
"system_enablePosthog",
|
||||
applicationProperties.getSystem().isPosthogEnabled());
|
||||
addIfNotEmpty(
|
||||
properties,
|
||||
"system_enableScarf",
|
||||
applicationProperties.getSystem().isScarfEnabled());
|
||||
|
||||
// Capture UI properties
|
||||
addIfNotEmpty(properties, "ui_appName", applicationProperties.getUi().getAppName());
|
||||
|
||||
@ -6,7 +6,7 @@ import java.util.Map;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
|
||||
import io.swagger.v3.oas.annotations.Hidden;
|
||||
|
||||
@ -29,7 +29,7 @@ public class SettingsController {
|
||||
|
||||
@AutoJobPostMapping("/update-enable-analytics")
|
||||
@Hidden
|
||||
public ResponseEntity<String> updateApiKey(@RequestBody Boolean enabled) throws IOException {
|
||||
public ResponseEntity<String> updateApiKey(@RequestParam Boolean enabled) throws IOException {
|
||||
if (applicationProperties.getSystem().getEnableAnalytics() != null) {
|
||||
return ResponseEntity.status(HttpStatus.ALREADY_REPORTED)
|
||||
.body(
|
||||
|
||||
@ -65,6 +65,8 @@ public class ConfigController {
|
||||
applicationProperties.getSystem().getEnableAlphaFunctionality());
|
||||
configData.put(
|
||||
"enableAnalytics", applicationProperties.getSystem().getEnableAnalytics());
|
||||
configData.put("enablePosthog", applicationProperties.getSystem().getEnablePosthog());
|
||||
configData.put("enableScarf", applicationProperties.getSystem().getEnableScarf());
|
||||
|
||||
// Premium/Enterprise settings
|
||||
configData.put("premiumEnabled", applicationProperties.getPremium().isEnabled());
|
||||
|
||||
@ -105,7 +105,9 @@ system:
|
||||
showUpdateOnlyAdmin: false # only admins can see when a new update is available, depending on showUpdate it must be set to 'true'
|
||||
customHTMLFiles: false # enable to have files placed in /customFiles/templates override the existing template HTML files
|
||||
tessdataDir: /usr/share/tessdata # path to the directory containing the Tessdata files. This setting is relevant for Windows systems. For Windows users, this path should be adjusted to point to the appropriate directory where the Tessdata files are stored.
|
||||
enableAnalytics: null # set to 'true' to enable analytics, set to 'false' to disable analytics; for enterprise users, this is set to true
|
||||
enableAnalytics: null # Master toggle for analytics: set to 'true' to enable all analytics, 'false' to disable all analytics, or leave as 'null' to prompt admin on first launch
|
||||
enablePosthog: null # Enable PostHog analytics (open-source product analytics): set to 'true' to enable, 'false' to disable, or 'null' to enable by default when analytics is enabled
|
||||
enableScarf: null # Enable Scarf tracking pixel: set to 'true' to enable, 'false' to disable, or 'null' to enable by default when analytics is enabled
|
||||
enableUrlToPDF: false # Set to 'true' to enable URL to PDF, INTERNAL ONLY, known security issues, should not be used externally
|
||||
disableSanitize: false # set to true to disable Sanitize HTML; (can lead to injections in HTML)
|
||||
maxDPI: 500 # Maximum allowed DPI for PDF to image conversion
|
||||
|
||||
@ -3092,6 +3092,10 @@
|
||||
"title": "Analytics",
|
||||
"description": "These cookies help us understand how our tools are being used, so we can focus on building the features our community values most. Rest assured—Stirling PDF cannot and will never track the content of the documents you work with."
|
||||
}
|
||||
},
|
||||
"services": {
|
||||
"posthog": "PostHog Analytics",
|
||||
"scarf": "Scarf Pixel"
|
||||
}
|
||||
},
|
||||
"removeMetadata": {
|
||||
@ -3617,6 +3621,29 @@
|
||||
"title": "Attachment Results"
|
||||
}
|
||||
},
|
||||
"analytics": {
|
||||
"modal": {
|
||||
"title": "Configure Analytics",
|
||||
"description": "Choose whether to enable analytics for Stirling PDF. If enabled, users can control individual services (PostHog and Scarf) through the cookie preferences.",
|
||||
"whatWeCollect": "What we collect:",
|
||||
"collect": {
|
||||
"system": "Operating system and Java version",
|
||||
"config": "CPU/memory configuration and deployment type",
|
||||
"features": "Aggregate feature usage counts",
|
||||
"pages": "Page visits (via tracking pixel)"
|
||||
},
|
||||
"whatWeDoNotCollect": "What we do NOT collect:",
|
||||
"notCollect": {
|
||||
"documents": "Document content or file data",
|
||||
"pii": "Personally identifiable information (PII)",
|
||||
"ip": "IP addresses"
|
||||
},
|
||||
"privacy": "All analytics data is hosted on EU servers and respects your privacy.",
|
||||
"enable": "Enable Analytics",
|
||||
"disable": "Disable Analytics",
|
||||
"note": "This choice can be changed later by editing the settings.yml file."
|
||||
}
|
||||
},
|
||||
"termsAndConditions": "Terms & Conditions",
|
||||
"logOut": "Log out"
|
||||
}
|
||||
|
||||
@ -9,6 +9,7 @@ import { SidebarProvider } from "./contexts/SidebarContext";
|
||||
import { PreferencesProvider } from "./contexts/PreferencesContext";
|
||||
import ErrorBoundary from "./components/shared/ErrorBoundary";
|
||||
import HomePage from "./pages/HomePage";
|
||||
import { useScarfTracking } from "./hooks/useScarfTracking";
|
||||
|
||||
// Import global styles
|
||||
import "./styles/tailwind.css";
|
||||
@ -38,6 +39,9 @@ const LoadingFallback = () => (
|
||||
);
|
||||
|
||||
export default function App() {
|
||||
// Initialize scarf tracking (mounts once at app startup)
|
||||
useScarfTracking();
|
||||
|
||||
return (
|
||||
<Suspense fallback={<LoadingFallback />}>
|
||||
<RainbowThemeProvider>
|
||||
|
||||
@ -6,6 +6,7 @@ import { useFileHandler } from '../../hooks/useFileHandler';
|
||||
import { useFileState } from '../../contexts/FileContext';
|
||||
import { useNavigationState, useNavigationActions } from '../../contexts/NavigationContext';
|
||||
import { useViewer } from '../../contexts/ViewerContext';
|
||||
import { useAppConfig } from '../../hooks/useAppConfig';
|
||||
import './Workbench.css';
|
||||
|
||||
import TopControls from '../shared/TopControls';
|
||||
@ -20,6 +21,7 @@ import DismissAllErrorsButton from '../shared/DismissAllErrorsButton';
|
||||
// No props needed - component uses contexts directly
|
||||
export default function Workbench() {
|
||||
const { isRainbowMode } = useRainbowThemeContext();
|
||||
const { config } = useAppConfig();
|
||||
|
||||
// Use context-based hooks to eliminate all prop drilling
|
||||
const { selectors } = useFileState();
|
||||
@ -180,7 +182,7 @@ export default function Workbench() {
|
||||
{renderMainContent()}
|
||||
</Box>
|
||||
|
||||
<Footer analyticsEnabled />
|
||||
<Footer analyticsEnabled={config?.enableAnalytics === true} />
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
127
frontend/src/components/shared/AdminAnalyticsChoiceModal.tsx
Normal file
127
frontend/src/components/shared/AdminAnalyticsChoiceModal.tsx
Normal file
@ -0,0 +1,127 @@
|
||||
import { Modal, Stack, Button, Text, Title, Paper, List } from '@mantine/core';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useState } from 'react';
|
||||
import { Z_ANALYTICS_MODAL } from '../../styles/zIndex';
|
||||
import apiClient from '../../services/apiClient';
|
||||
|
||||
interface AdminAnalyticsChoiceModalProps {
|
||||
opened: boolean;
|
||||
onClose?: () => void;
|
||||
}
|
||||
|
||||
export default function AdminAnalyticsChoiceModal({ opened }: AdminAnalyticsChoiceModalProps) {
|
||||
const { t } = useTranslation();
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const handleChoice = async (enableAnalytics: boolean) => {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
|
||||
try {
|
||||
const formData = new FormData();
|
||||
formData.append('enabled', enableAnalytics.toString());
|
||||
|
||||
await apiClient.post('/api/v1/settings/update-enable-analytics', formData);
|
||||
|
||||
|
||||
// Reload the page to apply new settings
|
||||
window.location.reload();
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : 'Unknown error occurred');
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleEnable = () => {
|
||||
handleChoice(true);
|
||||
};
|
||||
|
||||
const handleDisable = () => {
|
||||
handleChoice(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal
|
||||
opened={opened}
|
||||
onClose={() => {}} // Prevent closing
|
||||
closeOnClickOutside={false}
|
||||
closeOnEscape={false}
|
||||
withCloseButton={false}
|
||||
size="lg"
|
||||
centered
|
||||
zIndex={Z_ANALYTICS_MODAL}
|
||||
>
|
||||
<Stack gap="md">
|
||||
<Title order={2}>{t('analytics.modal.title', 'Configure Analytics')}</Title>
|
||||
|
||||
<Text size="sm" c="dimmed">
|
||||
{t('analytics.modal.description', 'Choose whether to enable analytics for Stirling PDF. If enabled, users can control individual services (PostHog and Scarf) through the cookie preferences.')}
|
||||
</Text>
|
||||
|
||||
<Paper p="md" withBorder>
|
||||
<Stack gap="xs">
|
||||
<Text fw={600} size="sm">
|
||||
{t('analytics.modal.whatWeCollect', 'What we collect:')}
|
||||
</Text>
|
||||
<List size="sm" spacing="xs">
|
||||
<List.Item>{t('analytics.modal.collect.system', 'Operating system and Java version')}</List.Item>
|
||||
<List.Item>{t('analytics.modal.collect.config', 'CPU/memory configuration and deployment type')}</List.Item>
|
||||
<List.Item>{t('analytics.modal.collect.features', 'Aggregate feature usage counts')}</List.Item>
|
||||
<List.Item>{t('analytics.modal.collect.pages', 'Page visits (via tracking pixel)')}</List.Item>
|
||||
</List>
|
||||
</Stack>
|
||||
</Paper>
|
||||
|
||||
<Paper p="md" withBorder>
|
||||
<Stack gap="xs">
|
||||
<Text fw={600} size="sm">
|
||||
{t('analytics.modal.whatWeDoNotCollect', 'What we do NOT collect:')}
|
||||
</Text>
|
||||
<List size="sm" spacing="xs">
|
||||
<List.Item>{t('analytics.modal.notCollect.documents', 'Document content or file data')}</List.Item>
|
||||
<List.Item>{t('analytics.modal.notCollect.pii', 'Personally identifiable information (PII)')}</List.Item>
|
||||
<List.Item>{t('analytics.modal.notCollect.ip', 'IP addresses')}</List.Item>
|
||||
</List>
|
||||
</Stack>
|
||||
</Paper>
|
||||
|
||||
<Text size="sm" fs="italic">
|
||||
{t('analytics.modal.privacy', 'All analytics data is hosted on EU servers and respects your privacy.')}
|
||||
</Text>
|
||||
|
||||
{error && (
|
||||
<Text c="red" size="sm">
|
||||
{error}
|
||||
</Text>
|
||||
)}
|
||||
|
||||
<Stack gap="sm">
|
||||
<Button
|
||||
onClick={handleEnable}
|
||||
loading={loading}
|
||||
fullWidth
|
||||
size="md"
|
||||
>
|
||||
{t('analytics.modal.enable', 'Enable Analytics')}
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
onClick={handleDisable}
|
||||
loading={loading}
|
||||
fullWidth
|
||||
size="md"
|
||||
variant="subtle"
|
||||
c="gray"
|
||||
>
|
||||
{t('analytics.modal.disable', 'Disable Analytics')}
|
||||
</Button>
|
||||
</Stack>
|
||||
|
||||
<Text size="xs" c="dimmed" ta="center">
|
||||
{t('analytics.modal.note', 'This choice can be changed later by editing the settings.yml file.')}
|
||||
</Text>
|
||||
</Stack>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
@ -10,7 +10,9 @@ export interface AppConfig {
|
||||
languages?: string[];
|
||||
enableLogin?: boolean;
|
||||
enableAlphaFunctionality?: boolean;
|
||||
enableAnalytics?: boolean;
|
||||
enableAnalytics?: boolean | null;
|
||||
enablePosthog?: boolean | null;
|
||||
enableScarf?: boolean | null;
|
||||
premiumEnabled?: boolean;
|
||||
premiumKey?: string;
|
||||
termsAndConditions?: string;
|
||||
@ -45,15 +47,16 @@ export function useAppConfig(): UseAppConfigReturn {
|
||||
try {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
|
||||
|
||||
const response = await fetch('/api/v1/config/app-config');
|
||||
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to fetch config: ${response.status} ${response.statusText}`);
|
||||
}
|
||||
|
||||
|
||||
const data: AppConfig = await response.json();
|
||||
setConfig(data);
|
||||
console.warn('Fetched app config:', data);
|
||||
} catch (err) {
|
||||
const errorMessage = err instanceof Error ? err.message : 'Unknown error occurred';
|
||||
setError(errorMessage);
|
||||
|
||||
@ -1,12 +1,15 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useEffect, useState, useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { BASE_PATH } from '../constants/app';
|
||||
import { useAppConfig } from './useAppConfig';
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
CookieConsent: {
|
||||
CookieConsent?: {
|
||||
run: (config: any) => void;
|
||||
show: (show?: boolean) => void;
|
||||
acceptedCategory: (category: string) => boolean;
|
||||
acceptedService: (serviceName: string, category: string) => boolean;
|
||||
};
|
||||
}
|
||||
}
|
||||
@ -15,8 +18,11 @@ interface CookieConsentConfig {
|
||||
analyticsEnabled?: boolean;
|
||||
}
|
||||
|
||||
export const useCookieConsent = ({ analyticsEnabled = false }: CookieConsentConfig = {}) => {
|
||||
export const useCookieConsent = ({
|
||||
analyticsEnabled = false
|
||||
}: CookieConsentConfig = {}) => {
|
||||
const { t } = useTranslation();
|
||||
const { config } = useAppConfig();
|
||||
const [isInitialized, setIsInitialized] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
@ -30,7 +36,7 @@ export const useCookieConsent = ({ analyticsEnabled = false }: CookieConsentConf
|
||||
setIsInitialized(true);
|
||||
// Force show the modal if it exists but isn't visible
|
||||
setTimeout(() => {
|
||||
window.CookieConsent.show();
|
||||
window.CookieConsent?.show();
|
||||
}, 100);
|
||||
return;
|
||||
}
|
||||
@ -130,7 +136,24 @@ export const useCookieConsent = ({ analyticsEnabled = false }: CookieConsentConf
|
||||
necessary: {
|
||||
readOnly: true
|
||||
},
|
||||
analytics: {}
|
||||
analytics: {
|
||||
services: {
|
||||
...(config?.enablePosthog !== false && {
|
||||
posthog: {
|
||||
label: t('cookieBanner.services.posthog', 'PostHog Analytics'),
|
||||
onAccept: () => console.log('PostHog service accepted'),
|
||||
onReject: () => console.log('PostHog service rejected')
|
||||
}
|
||||
}),
|
||||
...(config?.enableScarf !== false && {
|
||||
scarf: {
|
||||
label: t('cookieBanner.services.scarf', 'Scarf Pixel'),
|
||||
onAccept: () => console.log('Scarf service accepted'),
|
||||
onReject: () => console.log('Scarf service rejected')
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
},
|
||||
language: {
|
||||
default: "en",
|
||||
@ -184,7 +207,7 @@ export const useCookieConsent = ({ analyticsEnabled = false }: CookieConsentConf
|
||||
|
||||
// Force show after initialization
|
||||
setTimeout(() => {
|
||||
window.CookieConsent.show();
|
||||
window.CookieConsent?.show();
|
||||
}, 200);
|
||||
|
||||
} catch (error) {
|
||||
@ -212,15 +235,23 @@ export const useCookieConsent = ({ analyticsEnabled = false }: CookieConsentConf
|
||||
document.head.removeChild(customCSS);
|
||||
}
|
||||
};
|
||||
}, [analyticsEnabled, t]);
|
||||
}, [analyticsEnabled, config?.enablePosthog, config?.enableScarf, t]);
|
||||
|
||||
const showCookiePreferences = () => {
|
||||
if (isInitialized && window.CookieConsent) {
|
||||
window.CookieConsent.show(true);
|
||||
window.CookieConsent?.show(true);
|
||||
}
|
||||
};
|
||||
|
||||
const isServiceAccepted = useCallback((service: string, category: string): boolean => {
|
||||
if (typeof window === 'undefined' || !window.CookieConsent) {
|
||||
return false;
|
||||
}
|
||||
return window.CookieConsent.acceptedService(service, category);
|
||||
}, []);
|
||||
|
||||
return {
|
||||
showCookiePreferences
|
||||
showCookiePreferences,
|
||||
isServiceAccepted
|
||||
};
|
||||
};
|
||||
|
||||
45
frontend/src/hooks/useScarfTracking.ts
Normal file
45
frontend/src/hooks/useScarfTracking.ts
Normal file
@ -0,0 +1,45 @@
|
||||
import { useEffect } from 'react';
|
||||
import { useAppConfig } from './useAppConfig';
|
||||
import { useCookieConsent } from './useCookieConsent';
|
||||
import { setScarfConfig, firePixel } from '../utils/scarfTracking';
|
||||
|
||||
/**
|
||||
* Hook for initializing Scarf tracking
|
||||
*
|
||||
* This hook should be mounted once during app initialization (e.g., in index.tsx).
|
||||
* It configures the scarf tracking utility with current config and consent state,
|
||||
* and sets up event listeners to auto-fire pixels when consent is granted.
|
||||
*
|
||||
* After initialization, firePixel() can be called from anywhere in the app,
|
||||
* including non-React utility functions like urlRouting.ts.
|
||||
*/
|
||||
export function useScarfTracking() {
|
||||
const { config } = useAppConfig();
|
||||
const { isServiceAccepted } = useCookieConsent({ analyticsEnabled: config?.enableAnalytics === true });
|
||||
|
||||
// Update scarf config whenever config or consent changes
|
||||
useEffect(() => {
|
||||
console.warn('[useScarfTracking] Updating scarf config:', { enableScarf: config?.enableScarf, isServiceAccepted });
|
||||
if (config && config.enableScarf !== undefined) {
|
||||
setScarfConfig(config.enableScarf, isServiceAccepted);
|
||||
}
|
||||
}, [config?.enableScarf, isServiceAccepted]);
|
||||
|
||||
// Listen to cookie consent changes and auto-fire pixel when consent is granted
|
||||
useEffect(() => {
|
||||
const handleConsentChange = () => {
|
||||
console.warn('[useScarfTracking] Consent changed, checking scarf service acceptance');
|
||||
if (isServiceAccepted('scarf', 'analytics')) {
|
||||
firePixel(window.location.pathname);
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener('cc:onConsent', handleConsentChange);
|
||||
window.addEventListener('cc:onChange', handleConsentChange);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('cc:onConsent', handleConsentChange);
|
||||
window.removeEventListener('cc:onChange', handleConsentChange);
|
||||
};
|
||||
}, [isServiceAccepted]);
|
||||
}
|
||||
@ -29,22 +29,21 @@ posthog.init(import.meta.env.VITE_PUBLIC_POSTHOG_KEY, {
|
||||
defaults: '2025-05-24',
|
||||
capture_exceptions: true, // This enables capturing exceptions using Error Tracking, set to false if you don't want this
|
||||
debug: false,
|
||||
opt_out_capturing_by_default: false, // We handle opt-out via cookie consent
|
||||
opt_out_capturing_by_default: true, // Opt-out by default, controlled by cookie consent
|
||||
});
|
||||
|
||||
function updatePosthogConsent(){
|
||||
if(typeof(posthog) == "undefined") {
|
||||
return;
|
||||
}
|
||||
const optIn = (window.CookieConsent as any).acceptedCategory('analytics');
|
||||
if (optIn) {
|
||||
posthog.opt_in_capturing();
|
||||
} else {
|
||||
posthog.opt_out_capturing();
|
||||
}
|
||||
|
||||
console.log("Updated analytics consent: ", optIn? "opted in" : "opted out");
|
||||
if(typeof(posthog) == "undefined" || !posthog.__loaded) {
|
||||
return;
|
||||
}
|
||||
const optIn = (window.CookieConsent as any)?.acceptedService?.('posthog', 'analytics') || false;
|
||||
if (optIn) {
|
||||
posthog.opt_in_capturing();
|
||||
} else {
|
||||
posthog.opt_out_capturing();
|
||||
}
|
||||
console.log("Updated PostHog consent: ", optIn ? "opted in" : "opted out");
|
||||
}
|
||||
|
||||
window.addEventListener("cc:onConsent", updatePosthogConsent);
|
||||
window.addEventListener("cc:onChange", updatePosthogConsent);
|
||||
|
||||
@ -6,6 +6,7 @@ import { useSidebarContext } from "../contexts/SidebarContext";
|
||||
import { useDocumentMeta } from "../hooks/useDocumentMeta";
|
||||
import { BASE_PATH, getBaseUrl } from "../constants/app";
|
||||
import { useMediaQuery } from "@mantine/hooks";
|
||||
import { useAppConfig } from "../hooks/useAppConfig";
|
||||
import AppsIcon from '@mui/icons-material/AppsRounded';
|
||||
|
||||
import ToolPanel from "../components/tools/ToolPanel";
|
||||
@ -17,6 +18,7 @@ import LocalIcon from "../components/shared/LocalIcon";
|
||||
import { useFilesModalContext } from "../contexts/FilesModalContext";
|
||||
import AppConfigModal from "../components/shared/AppConfigModal";
|
||||
import ToolPanelModePrompt from "../components/tools/ToolPanelModePrompt";
|
||||
import AdminAnalyticsChoiceModal from "../components/shared/AdminAnalyticsChoiceModal";
|
||||
|
||||
import "./HomePage.css";
|
||||
|
||||
@ -42,11 +44,20 @@ export default function HomePage() {
|
||||
|
||||
const { openFilesModal } = useFilesModalContext();
|
||||
const { colorScheme } = useMantineColorScheme();
|
||||
const { config } = useAppConfig();
|
||||
const isMobile = useMediaQuery("(max-width: 1024px)");
|
||||
const sliderRef = useRef<HTMLDivElement | null>(null);
|
||||
const [activeMobileView, setActiveMobileView] = useState<MobileView>("tools");
|
||||
const isProgrammaticScroll = useRef(false);
|
||||
const [configModalOpen, setConfigModalOpen] = useState(false);
|
||||
const [showAnalyticsModal, setShowAnalyticsModal] = useState(false);
|
||||
|
||||
// Show admin analytics choice modal if analytics settings not configured
|
||||
useEffect(() => {
|
||||
if (config && config.enableAnalytics === null) {
|
||||
setShowAnalyticsModal(true);
|
||||
}
|
||||
}, [config]);
|
||||
|
||||
const brandAltText = t("home.mobile.brandAlt", "Stirling PDF logo");
|
||||
const brandIconSrc = `${BASE_PATH}/branding/StirlingPDFLogoNoText${
|
||||
@ -151,6 +162,7 @@ export default function HomePage() {
|
||||
|
||||
return (
|
||||
<div className="h-screen overflow-hidden">
|
||||
<AdminAnalyticsChoiceModal opened={showAnalyticsModal} />
|
||||
<ToolPanelModePrompt />
|
||||
{isMobile ? (
|
||||
<div className="mobile-layout">
|
||||
|
||||
@ -3,7 +3,8 @@
|
||||
|
||||
export const Z_INDEX_FULLSCREEN_SURFACE = 1000;
|
||||
export const Z_INDEX_AUTOMATE_MODAL = 1100;
|
||||
export const Z_INDEX_FILE_MANAGER_MODAL = 1200;
|
||||
export const Z_INDEX_FILE_MANAGER_MODAL = 1200;
|
||||
export const Z_INDEX_OVER_FULLSCREEN_SURFACE = 1300;
|
||||
export const Z_ANALYTICS_MODAL = 1301;
|
||||
|
||||
|
||||
|
||||
@ -1,14 +1,74 @@
|
||||
/**
|
||||
* Scarf analytics pixel tracking utility
|
||||
*
|
||||
* This module provides a firePixel function that can be called from anywhere,
|
||||
* including non-React utility functions. Configuration and consent state are
|
||||
* injected via setScarfConfig() which should be called from a React hook
|
||||
* during app initialization.
|
||||
*
|
||||
* IMPORTANT: setScarfConfig() must be called before firePixel() will work.
|
||||
* The initialization hook (useScarfTracking) is mounted in App.tsx.
|
||||
*
|
||||
* For testing: Use resetScarfConfig() to clear module state between tests.
|
||||
*/
|
||||
|
||||
// Module-level state
|
||||
let configured: boolean = false;
|
||||
let enableScarf: boolean | null = null;
|
||||
let isServiceAccepted: ((service: string, category: string) => boolean) | null = null;
|
||||
let lastFiredPathname: string | null = null;
|
||||
let lastFiredTime = 0;
|
||||
|
||||
/**
|
||||
* Configure scarf tracking with app config and consent checker
|
||||
* Should be called from a React hook during app initialization (see useScarfTracking)
|
||||
*
|
||||
* @param scarfEnabled - Whether scarf tracking is enabled globally
|
||||
* @param consentChecker - Function to check if user has accepted scarf service
|
||||
*/
|
||||
export function setScarfConfig(
|
||||
scarfEnabled: boolean | null,
|
||||
consentChecker: (service: string, category: string) => boolean
|
||||
): void {
|
||||
configured = true;
|
||||
enableScarf = scarfEnabled;
|
||||
isServiceAccepted = consentChecker;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fire scarf pixel for analytics tracking
|
||||
* Only fires if pathname is different from last call or enough time has passed
|
||||
* Only fires if:
|
||||
* - Scarf tracking has been initialized via setScarfConfig()
|
||||
* - Scarf is globally enabled in config
|
||||
* - User has accepted scarf service via cookie consent
|
||||
* - Pathname has changed or enough time has passed since last fire
|
||||
*
|
||||
* @param pathname - The pathname to track (usually window.location.pathname)
|
||||
*/
|
||||
export function firePixel(pathname: string): void {
|
||||
console.warn('[scarfTracking]Firing with Current config:', { enableScarf, isServiceAccepted });
|
||||
// Dev-mode warning if called before initialization
|
||||
if (!configured) {
|
||||
console.warn(
|
||||
'[scarfTracking] firePixel() called before setScarfConfig(). ' +
|
||||
'Ensure useScarfTracking() hook is mounted in App.tsx.'
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if Scarf is globally disabled
|
||||
if (enableScarf === false) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if consent checker is available and scarf service is accepted
|
||||
if (!isServiceAccepted || !isServiceAccepted('scarf', 'analytics')) {
|
||||
return;
|
||||
}
|
||||
|
||||
const now = Date.now();
|
||||
|
||||
// Only fire if pathname changed or it's been at least 1 second since last fire
|
||||
// Only fire if pathname changed or it's been at least 250ms since last fire
|
||||
if (pathname === lastFiredPathname && now - lastFiredTime < 250) {
|
||||
return;
|
||||
}
|
||||
@ -22,5 +82,16 @@ export function firePixel(pathname: string): void {
|
||||
const img = new Image();
|
||||
img.referrerPolicy = "no-referrer-when-downgrade";
|
||||
img.src = url;
|
||||
console.warn('[scarfTracking] Firing pixel for', pathname);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset scarf tracking configuration and state
|
||||
* Useful for testing to ensure clean state between test runs
|
||||
*/
|
||||
export function resetScarfConfig(): void {
|
||||
enableScarf = null;
|
||||
isServiceAccepted = null;
|
||||
lastFiredPathname = null;
|
||||
lastFiredTime = 0;
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user