mirror of
https://github.com/BetterSEQTA/BetterSEQTA-Plus.git
synced 2026-06-06 03:34:40 +00:00
feat: updated privacy statement
This commit is contained in:
@@ -2,34 +2,6 @@ import browser from "webextension-polyfill";
|
|||||||
import type { SettingsState } from "@/types/storage";
|
import type { SettingsState } from "@/types/storage";
|
||||||
import { fetchNews } from "./background/news";
|
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() {
|
function reloadSeqtaPages() {
|
||||||
const result = browser.tabs.query({});
|
const result = browser.tabs.query({});
|
||||||
function open(tabs: any) {
|
function open(tabs: any) {
|
||||||
@@ -84,10 +56,6 @@ browser.runtime.onMessage.addListener(
|
|||||||
fetchNews(request.source ?? "australia", sendResponse);
|
fetchNews(request.source ?? "australia", sendResponse);
|
||||||
return true;
|
return true;
|
||||||
|
|
||||||
case "fetchPrivacyPolicy":
|
|
||||||
fetchPrivacyPolicy(sendResponse);
|
|
||||||
return true;
|
|
||||||
|
|
||||||
default:
|
default:
|
||||||
console.log("Unknown request type");
|
console.log("Unknown request type");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3357,6 +3357,23 @@ div.day-empty {
|
|||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
color: var(--text-primary);
|
color: var(--text-primary);
|
||||||
transform-origin: center center;
|
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 {
|
.whatsnewHeader {
|
||||||
margin: 20px;
|
margin: 20px;
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
import type { SettingsList } from "@/interface/types/SettingsProps"
|
import type { SettingsList } from "@/interface/types/SettingsProps"
|
||||||
import { settingsState } from "@/seqta/utils/listeners/SettingsState.ts"
|
import { settingsState } from "@/seqta/utils/listeners/SettingsState.ts"
|
||||||
import PickerSwatch from "@/interface/components/PickerSwatch.svelte"
|
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 { closeExtensionPopup } from "@/seqta/utils/Closers/closeExtensionPopup"
|
||||||
|
|
||||||
import { getAllPluginSettings } from "@/plugins"
|
import { getAllPluginSettings } from "@/plugins"
|
||||||
@@ -355,7 +355,7 @@
|
|||||||
closeExtensionPopup();
|
closeExtensionPopup();
|
||||||
// Small delay to ensure popup is closed before showing notification
|
// Small delay to ensure popup is closed before showing notification
|
||||||
await new Promise(resolve => setTimeout(resolve, 100));
|
await new Promise(resolve => setTimeout(resolve, 100));
|
||||||
await checkAndShowPrivacyNotification();
|
await showPrivacyNotification();
|
||||||
}}
|
}}
|
||||||
text="Show Now"
|
text="Show Now"
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ import loading from "@/seqta/ui/Loading";
|
|||||||
import { SendNewsPage } from "@/seqta/utils/SendNewsPage";
|
import { SendNewsPage } from "@/seqta/utils/SendNewsPage";
|
||||||
import { loadHomePage } from "@/seqta/utils/Loaders/LoadHomePage";
|
import { loadHomePage } from "@/seqta/utils/Loaders/LoadHomePage";
|
||||||
import { OpenWhatsNewPopup } from "@/seqta/utils/Openers/OpenWhatsNewPopup";
|
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";
|
import { updateTimetableTimes } from "@/seqta/utils/updateTimetableTimes";
|
||||||
|
|
||||||
@@ -95,9 +95,8 @@ export async function finishLoad() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Check and show privacy statement notification (before what's new)
|
// Check and show privacy statement notification (before what's new)
|
||||||
// This will check the API and compare timestamps
|
|
||||||
if (!document.getElementById("privacy-notification")) {
|
if (!document.getElementById("privacy-notification")) {
|
||||||
await checkAndShowPrivacyNotification();
|
await showPrivacyNotification();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (settingsState.justupdated && !document.getElementById("whatsnewbk") && !document.getElementById("privacy-notification")) {
|
if (settingsState.justupdated && !document.getElementById("whatsnewbk") && !document.getElementById("privacy-notification")) {
|
||||||
|
|||||||
@@ -1,112 +1,13 @@
|
|||||||
import stringToHTML from "../stringToHTML";
|
import stringToHTML from "../stringToHTML";
|
||||||
import { settingsState } from "../listeners/SettingsState";
|
import { settingsState } from "../listeners/SettingsState";
|
||||||
import { animate } from "motion";
|
import { openPopup } from "./PopupManager";
|
||||||
import { OpenWhatsNewPopup } from "./OpenWhatsNewPopup";
|
|
||||||
import DOMPurify from "dompurify";
|
|
||||||
import browser from "webextension-polyfill";
|
|
||||||
|
|
||||||
interface PrivacyPolicyResponse {
|
export function showPrivacyNotification() {
|
||||||
last_updated: string;
|
const lastUpdated = "2025-12-19";
|
||||||
whatsnew_html: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function fetchPrivacyPolicy(): Promise<PrivacyPolicyResponse | null> {
|
if (document.getElementById("whatsnewbk")) return;
|
||||||
try {
|
//if (settingsState.privacyStatementShown) return;
|
||||||
// Use background script to avoid CORS issues
|
//if (settingsState.privacyStatementLastUpdated && new Date(settingsState.privacyStatementLastUpdated) > new Date(lastUpdated)) return;
|
||||||
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%";
|
|
||||||
|
|
||||||
const header = stringToHTML(
|
const header = stringToHTML(
|
||||||
/* html */
|
/* html */
|
||||||
@@ -116,79 +17,36 @@ function showPrivacyNotificationWithContent(htmlContent: string | null) {
|
|||||||
</div>`,
|
</div>`,
|
||||||
).firstChild as HTMLElement;
|
).firstChild as HTMLElement;
|
||||||
|
|
||||||
// Use fetched content if available, otherwise use fallback
|
const text = stringToHTML(/* html */ `
|
||||||
let text: HTMLElement;
|
<div class="whatsnewTextContainer privacyStatement" style="overflow-y: auto; font-size: 1.2rem; line-height: 1.6;">
|
||||||
if (htmlContent) {
|
<img style="aspect-ratio: 16/5.8;" src="${settingsState.DarkMode ? "https://raw.githubusercontent.com/BetterSEQTA/BetterSEQTA-Plus/main/src/resources/branding/dark.jpg" : "https://raw.githubusercontent.com/BetterSEQTA/BetterSEQTA-Plus/main/src/resources/branding/light.jpg"}" class="aboutImg" />
|
||||||
// Parse the sanitized HTML
|
|
||||||
text = stringToHTML(htmlContent).firstChild as HTMLElement;
|
|
||||||
} else {
|
|
||||||
// Fallback content if API fails
|
|
||||||
text = stringToHTML(/* html */ `
|
|
||||||
<div class="whatsnewTextContainer" style="overflow-y: auto; font-size: 1.3rem; line-height: 1.6;">
|
|
||||||
<p>
|
<p>
|
||||||
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.
|
<strong>Addressing Recent Concerns About BetterSEQTA+</strong><br>
|
||||||
|
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.
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
To view our privacy policy, please click the <strong>shield icon</strong> in the settings menu, or<a href="https://betterseqta.org/privacy" target="_blank" rel="noopener noreferrer" id="privacy-link" style="color: inherit; text-decoration: underline; cursor: pointer; white-space: nowrap;"> click here</a>.
|
<strong>Our Commitment to Privacy:</strong><br>
|
||||||
|
<span style="display: block; margin-left: 1em;">
|
||||||
|
• We do not collect, store, or share any personal information<br>
|
||||||
|
• All data processing happens locally on your device<br>
|
||||||
|
• Our code is open source and available for review
|
||||||
|
</span>
|
||||||
</p>
|
</p>
|
||||||
<p style="font-weight: bold; margin-top: 15px;">
|
<p>
|
||||||
We never collect any information from you, and aim to provide the best features possible.
|
<strong>What We're Doing:</strong><br>
|
||||||
|
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 <a href="mailto:betterseqta.plus@gmail.com" style="color: inherit; text-decoration: underline;">betterseqta.plus@gmail.com</a> or via github at <a href="https://github.com/BetterSEQTA/BetterSEQTA-Plus" target="_blank" rel="noopener noreferrer" style="color: inherit; text-decoration: underline;">github.com/BetterSEQTA/BetterSEQTA-Plus</a>.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
For complete details about our privacy practices, visit our <a href="https://betterseqta.org/privacy" target="_blank" rel="noopener noreferrer" style="color: inherit; text-decoration: underline;">privacy policy</a> or click the shield icon in settings.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
`).firstChild as HTMLElement;
|
`).firstChild as HTMLElement;
|
||||||
}
|
|
||||||
|
|
||||||
if (header) container.append(header);
|
settingsState.privacyStatementLastUpdated = "2025-12-20";
|
||||||
if (text) container.append(text);
|
settingsState.privacyStatementShown = true;
|
||||||
|
|
||||||
const closeButton = document.createElement("div");
|
openPopup({
|
||||||
closeButton.id = "whatsnewclosebutton";
|
header,
|
||||||
container.append(closeButton);
|
content: [text],
|
||||||
|
|
||||||
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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
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();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user