diff --git a/src/background.ts b/src/background.ts index a0cf9411..414d823a 100644 --- a/src/background.ts +++ b/src/background.ts @@ -2,34 +2,6 @@ import browser from "webextension-polyfill"; import type { SettingsState } from "@/types/storage"; import { fetchNews } from "./background/news"; -interface PrivacyPolicyResponse { - last_updated: string; - whatsnew_html: string; -} - -async function fetchPrivacyPolicy(sendResponse: (response?: any) => void) { - try { - const response = await fetch("https://betterseqta.org/api/policy/privacy", { - method: "GET", - headers: { - "Accept": "application/json", - }, - }); - - if (!response.ok) { - console.error("[BetterSEQTA+] Failed to fetch privacy policy:", response.status); - sendResponse({ error: `HTTP ${response.status}`, data: null }); - return; - } - - const data = await response.json() as PrivacyPolicyResponse; - sendResponse({ error: null, data }); - } catch (error) { - console.error("[BetterSEQTA+] Error fetching privacy policy:", error); - sendResponse({ error: String(error), data: null }); - } -} - function reloadSeqtaPages() { const result = browser.tabs.query({}); function open(tabs: any) { @@ -84,10 +56,6 @@ browser.runtime.onMessage.addListener( fetchNews(request.source ?? "australia", sendResponse); return true; - case "fetchPrivacyPolicy": - fetchPrivacyPolicy(sendResponse); - return true; - default: console.log("Unknown request type"); } diff --git a/src/css/injected.scss b/src/css/injected.scss index 162e113a..f21b9a6a 100644 --- a/src/css/injected.scss +++ b/src/css/injected.scss @@ -3357,6 +3357,23 @@ div.day-empty { flex-direction: column; color: var(--text-primary); transform-origin: center center; + +} +.whatsnewTextContainer.privacyStatement p { + margin-bottom: 1.5ex; + + &:last-child { + margin-bottom: 0; + } +} +.whatsnewTextContainer.privacyStatement a { + background: rgba(184, 184, 184, 0.1); + border-radius: 0.5rem; + margin-left: 0.25rem; + padding: 2px 4px; +} +.dark .whatsnewTextContainer.privacyStatement a { + background: rgba(7, 7, 7, 0.1); } .whatsnewHeader { margin: 20px; diff --git a/src/interface/pages/settings/general.svelte b/src/interface/pages/settings/general.svelte index 0628898d..c9ddae18 100644 --- a/src/interface/pages/settings/general.svelte +++ b/src/interface/pages/settings/general.svelte @@ -10,7 +10,7 @@ import type { SettingsList } from "@/interface/types/SettingsProps" import { settingsState } from "@/seqta/utils/listeners/SettingsState.ts" import PickerSwatch from "@/interface/components/PickerSwatch.svelte" - import { checkAndShowPrivacyNotification } from "@/seqta/utils/Openers/OpenPrivacyNotification" + import { showPrivacyNotification } from "@/seqta/utils/Openers/OpenPrivacyNotification" import { closeExtensionPopup } from "@/seqta/utils/Closers/closeExtensionPopup" import { getAllPluginSettings } from "@/plugins" @@ -355,7 +355,7 @@ closeExtensionPopup(); // Small delay to ensure popup is closed before showing notification await new Promise(resolve => setTimeout(resolve, 100)); - await checkAndShowPrivacyNotification(); + await showPrivacyNotification(); }} text="Show Now" /> diff --git a/src/plugins/monofile.ts b/src/plugins/monofile.ts index c2006173..77ffd39b 100644 --- a/src/plugins/monofile.ts +++ b/src/plugins/monofile.ts @@ -24,7 +24,7 @@ import loading from "@/seqta/ui/Loading"; import { SendNewsPage } from "@/seqta/utils/SendNewsPage"; import { loadHomePage } from "@/seqta/utils/Loaders/LoadHomePage"; import { OpenWhatsNewPopup } from "@/seqta/utils/Openers/OpenWhatsNewPopup"; -import { checkAndShowPrivacyNotification } from "@/seqta/utils/Openers/OpenPrivacyNotification"; +import { showPrivacyNotification } from "@/seqta/utils/Openers/OpenPrivacyNotification"; import { updateTimetableTimes } from "@/seqta/utils/updateTimetableTimes"; @@ -95,9 +95,8 @@ export async function finishLoad() { } // Check and show privacy statement notification (before what's new) - // This will check the API and compare timestamps if (!document.getElementById("privacy-notification")) { - await checkAndShowPrivacyNotification(); + await showPrivacyNotification(); } if (settingsState.justupdated && !document.getElementById("whatsnewbk") && !document.getElementById("privacy-notification")) { diff --git a/src/seqta/utils/Openers/OpenPrivacyNotification.ts b/src/seqta/utils/Openers/OpenPrivacyNotification.ts index 0fc8e56d..aa2d7432 100644 --- a/src/seqta/utils/Openers/OpenPrivacyNotification.ts +++ b/src/seqta/utils/Openers/OpenPrivacyNotification.ts @@ -1,112 +1,13 @@ import stringToHTML from "../stringToHTML"; import { settingsState } from "../listeners/SettingsState"; -import { animate } from "motion"; -import { OpenWhatsNewPopup } from "./OpenWhatsNewPopup"; -import DOMPurify from "dompurify"; -import browser from "webextension-polyfill"; +import { openPopup } from "./PopupManager"; -interface PrivacyPolicyResponse { - last_updated: string; - whatsnew_html: string; -} +export function showPrivacyNotification() { + const lastUpdated = "2025-12-19"; -async function fetchPrivacyPolicy(): Promise { - try { - // Use background script to avoid CORS issues - const response = await browser.runtime.sendMessage({ type: "fetchPrivacyPolicy" }) as { error: string | null; data: PrivacyPolicyResponse | null }; - - if (response.error) { - console.error("[BetterSEQTA+] Failed to fetch privacy policy:", response.error); - return null; - } - - return response.data; - } catch (error) { - console.error("[BetterSEQTA+] Error fetching privacy policy:", error); - return null; - } -} - -export async function checkAndShowPrivacyNotification() { - if (document.getElementById("privacy-notification")) return; - - // Fetch the privacy policy from the API - const policyData = await fetchPrivacyPolicy(); - - if (!policyData) { - // If API fails, fall back to showing the notification if never shown - if (!settingsState.privacyStatementShown) { - showPrivacyNotificationWithContent(null); - settingsState.privacyStatementShown = true; - } - return; - } - - // Check if we should show the notification - const storedTimestamp = settingsState.privacyStatementLastUpdated; - const shouldShow = !storedTimestamp || - new Date(policyData.last_updated) > new Date(storedTimestamp); - - if (shouldShow) { - // Sanitize the HTML content to prevent XSS attacks - // DOMPurify will remove any dangerous scripts, event handlers, etc - // ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ - // SANITIZE CONTENT: - // ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ - // The content in sanitizedHTML is the only content that is allowed to be displayed in the notification: - const sanitizedHTML = DOMPurify.sanitize(policyData.whatsnew_html, { - ALLOWED_TAGS: ['div', 'p', 'strong', 'a', 'h1', 'h2', 'h3', 'ul', 'li', 'span', 'em', 'b', 'i'], - ALLOWED_ATTR: ['class', 'style', 'href', 'target', 'rel', 'id'], - ALLOW_DATA_ATTR: false, - ALLOW_UNKNOWN_PROTOCOLS: false, - // Ensure links are safe - allow https/http/mailto only - ALLOWED_URI_REGEXP: /^(?:(?:(?:f|ht)tps?|mailto|tel|callto|sms|cid|xmpp):|[^a-z]|[a-z+.\-]+(?:[^a-z+.\-:]|$))/i, - // Force all external links to have target="_blank" and rel="noopener noreferrer" - ADD_ATTR: ['target', 'rel'], - }); - - // Post-process to sanitize URLs and ensure all links have proper attributes - const tempDiv = document.createElement('div'); - tempDiv.innerHTML = sanitizedHTML; - - // First, remove hrefs from links that aren't to betterseqta.org - tempDiv.querySelectorAll("a").forEach(a => { - const href = a.getAttribute("href") || ""; - // Allow only links to your domain - if (!href.startsWith("https://betterseqta.org")) { - a.removeAttribute("href"); // neuter link - a.style.textDecoration = "none"; // optional visual fix - a.style.cursor = "default"; // optional - } else { - // Ensure all betterseqta.org links have proper attributes - (a as HTMLAnchorElement).target = "_blank"; - (a as HTMLAnchorElement).rel = "noopener noreferrer"; - } - }); - - const cleanHTML = tempDiv.innerHTML; - - showPrivacyNotificationWithContent(cleanHTML); - - // Update the stored timestamp - settingsState.privacyStatementLastUpdated = policyData.last_updated; - settingsState.privacyStatementShown = true; - } -} - -function showPrivacyNotificationWithContent(htmlContent: string | null) { - if (document.getElementById("privacy-notification")) return; - - const popupBackground = document.createElement("div"); - popupBackground.id = "privacy-notification"; - popupBackground.classList.add("whatsnewBackground"); - popupBackground.style.zIndex = "10000001"; - - const container = document.createElement("div"); - container.classList.add("whatsnewContainer"); - container.style.height = "auto"; - container.style.maxWidth = "800px"; - container.style.width = "90%"; + if (document.getElementById("whatsnewbk")) return; + //if (settingsState.privacyStatementShown) return; + //if (settingsState.privacyStatementLastUpdated && new Date(settingsState.privacyStatementLastUpdated) > new Date(lastUpdated)) return; const header = stringToHTML( /* html */ @@ -116,79 +17,36 @@ function showPrivacyNotificationWithContent(htmlContent: string | null) { `, ).firstChild as HTMLElement; - // Use fetched content if available, otherwise use fallback - let text: HTMLElement; - if (htmlContent) { - // Parse the sanitized HTML - text = stringToHTML(htmlContent).firstChild as HTMLElement; - } else { - // Fallback content if API fails - text = stringToHTML(/* html */ ` -
-

- It has come to our attention that several schools have expressed concerns about BetterSEQTA+. This is very disheartening, so we have decided to release a statement on the situation. -

-

- To view our privacy policy, please click the shield icon in the settings menu, or click here. -

-

- We never collect any information from you, and aim to provide the best features possible. -

-
- `).firstChild as HTMLElement; - } + const text = stringToHTML(/* html */ ` +
+ +

+ Addressing Recent Concerns About BetterSEQTA+
+ We appreciate the feedback we've received from several schools regarding BetterSEQTA+. Transparency and trust are core to our mission, and we want to address these concerns directly. +

+

+ Our Commitment to Privacy:
+ + • We do not collect, store, or share any personal information
+ • All data processing happens locally on your device
+ • Our code is open source and available for review +
+

+

+ What We're Doing:
+ We're willing to actively work with school administrators to ensure BetterSEQTA+ meets both student needs and institutional requirements. If your school has specific concerns, we encourage them to contact us at betterseqta.plus@gmail.com or via github at github.com/BetterSEQTA/BetterSEQTA-Plus. +

+

+ For complete details about our privacy practices, visit our privacy policy or click the shield icon in settings. +

+
+ `).firstChild as HTMLElement; - if (header) container.append(header); - if (text) container.append(text); + settingsState.privacyStatementLastUpdated = "2025-12-20"; + settingsState.privacyStatementShown = true; - const closeButton = document.createElement("div"); - closeButton.id = "whatsnewclosebutton"; - container.append(closeButton); - - popupBackground.append(container); - document.getElementById("container")?.append(popupBackground); - - if (settingsState.animations) { - animate([popupBackground as HTMLElement], { opacity: [0, 1] }); - } - - popupBackground.addEventListener("click", (event) => { - if (event.target === popupBackground) { - popupBackground.remove(); - // Show what's new if it was waiting - if (settingsState.justupdated && !document.getElementById("whatsnewbk")) { - OpenWhatsNewPopup(); - } - } + openPopup({ + header, + content: [text], }); - - closeButton.addEventListener("click", () => { - popupBackground.remove(); - // Show what's new if it was waiting - if (settingsState.justupdated && !document.getElementById("whatsnewbk")) { - OpenWhatsNewPopup(); - } - }); - - // Handle all privacy policy links - ensure they open in new tab - setTimeout(() => { - // Find all links that point to the privacy policy - const allLinks = container.querySelectorAll('a[href*="betterseqta.org/privacy"]'); - allLinks.forEach((link) => { - link.addEventListener("click", (e) => { - e.preventDefault(); - const href = (link as HTMLAnchorElement).href || "https://betterseqta.org/privacy"; - window.open(href, "_blank", "noopener,noreferrer"); - }); - // Ensure target and rel attributes are set - (link as HTMLAnchorElement).target = "_blank"; - (link as HTMLAnchorElement).rel = "noopener noreferrer"; - }); - }, 100); } - -// Legacy function name for backwards compatibility -export function showPrivacyNotification() { - checkAndShowPrivacyNotification(); -} -