diff --git a/src/css/injected.scss b/src/css/injected.scss index 7130e166..e31230a0 100644 --- a/src/css/injected.scss +++ b/src/css/injected.scss @@ -3524,6 +3524,26 @@ div.day-empty { font-size: 1em; color: var(--text-primary); } +.whatsnewHeader.engageParentsAnnouncementHeader { + height: auto; + min-height: unset; +} +.whatsnewHeader.engageParentsAnnouncementHeader h1 { + line-height: 1.2; +} +.whatsnewHeader.engageParentsAnnouncementHeader .engageParentsSubheading { + margin-top: 0.35rem; + font-size: 1.05rem; + font-weight: 600; + opacity: 0.92; +} +.seqtaEngageAccent { + color: #ea580c; + font-weight: 700; +} +.dark .seqtaEngageAccent { + color: #fb923c; +} .whatsnewBackground { width: 100%; height: 100%; @@ -3652,6 +3672,25 @@ div.day-empty { object-fit: cover; margin-bottom: 12px; } +.whatsnewTextContainer .engageParentsPromoWrap { + width: 100%; + margin-bottom: 12px; + border-radius: 16px; + overflow: hidden; + aspect-ratio: 16 / 9; + box-shadow: 0 0 10px rgba(0, 0, 0, 0.28); + background: color-mix(in srgb, var(--background-secondary) 88%, var(--text-primary) 12%); +} +.whatsnewTextContainer .engageParentsPromoWrap .engageParentsPromoImg { + display: block; + width: 100%; + height: 100%; + margin: 0; + border-radius: 0; + aspect-ratio: unset; + object-fit: contain; + object-position: center; +} @keyframes shimmer { 0% { diff --git a/src/plugins/monofile.ts b/src/plugins/monofile.ts index 353386e3..1a65bb96 100644 --- a/src/plugins/monofile.ts +++ b/src/plugins/monofile.ts @@ -29,8 +29,7 @@ import { updateEngageHomeMenuActive, } from "@/seqta/utils/Loaders/LoadEngageHomePage"; import { loadHomePage } from "@/seqta/utils/Loaders/LoadHomePage"; -import { OpenWhatsNewPopup } from "@/seqta/utils/Openers/OpenWhatsNewPopup"; -import { showPrivacyNotification } from "@/seqta/utils/Openers/OpenPrivacyNotification"; +import { runStartupPopupQueue } from "@/seqta/utils/Openers/StartupPopupQueue"; import { updateTimetableTimes } from "@/seqta/utils/updateTimetableTimes"; @@ -106,14 +105,7 @@ export async function finishLoad() { console.error("Error during loading cleanup:", err); } - // Check and show privacy statement notification (before what's new) - if (!document.getElementById("privacy-notification")) { - await showPrivacyNotification(); - } - - if (settingsState.justupdated && !document.getElementById("whatsnewbk") && !document.getElementById("privacy-notification")) { - OpenWhatsNewPopup(); - } + runStartupPopupQueue(); } export function GetCSSElement(file: string) { diff --git a/src/resources/bq+engage.png b/src/resources/bq+engage.png new file mode 100644 index 00000000..298ce3b6 Binary files /dev/null and b/src/resources/bq+engage.png differ diff --git a/src/seqta/utils/Openers/OpenEngageParentsAnnouncement.ts b/src/seqta/utils/Openers/OpenEngageParentsAnnouncement.ts new file mode 100644 index 00000000..ca8b4747 --- /dev/null +++ b/src/seqta/utils/Openers/OpenEngageParentsAnnouncement.ts @@ -0,0 +1,59 @@ +import stringToHTML from "../stringToHTML"; +import { settingsState } from "../listeners/SettingsState"; +import { openPopup } from "./PopupManager"; + +/** Same hosting pattern as the privacy statement branding images (avoids page-relative extension URLs on Engage). */ +const ENGAGE_PROMO_IMG_URL = + "https://raw.githubusercontent.com/BetterSEQTA/BetterSEQTA-Plus/main/src/resources/bq%2Bengage.png"; + +export function shouldShowEngageParentsAnnouncement(): boolean { + return !settingsState.engageParentsAnnouncementShown; +} + +/** + * One-time announcement that BetterSEQTA Plus works on SEQTA Engage (parents). + */ +export function showEngageParentsAnnouncement(onDismissed?: () => void) { + if (document.getElementById("whatsnewbk")) { + onDismissed?.(); + return; + } + if (!shouldShowEngageParentsAnnouncement()) { + onDismissed?.(); + return; + } + + const header = stringToHTML( + /* html */ + `
+

BetterSEQTA Plus now supports SEQTA Engage

+

Buy your mom a BetterSEQTA Plus

+
`, + ).firstChild as HTMLElement; + + const text = stringToHTML(/* html */ ` +
+
+ BetterSEQTA Plus now supports SEQTA Engage +
+

+ SEQTA Engage is the portal many parents use for notices, messages, and day-to-day school info. + Before anything else: BetterSEQTA Plus now supports SEQTA Engage, so parents get the same kinds of improvements you are used to on SEQTA Learn—themes, a clearer home experience, and other Plus polish while browsing Engage. +

+

+ The title is a bit of fun; if the extension saves you time, you can always support development via Open Collective or Ko-fi from the What is New changelog or related links in settings. +

+

+ Close this dialog when you are done. We will not show this announcement again. +

+
+ `).firstChild as HTMLElement; + + settingsState.engageParentsAnnouncementShown = true; + + openPopup({ + header, + content: [text], + afterClose: onDismissed, + }); +} diff --git a/src/seqta/utils/Openers/OpenPrivacyNotification.ts b/src/seqta/utils/Openers/OpenPrivacyNotification.ts index fa573fc9..b205c641 100644 --- a/src/seqta/utils/Openers/OpenPrivacyNotification.ts +++ b/src/seqta/utils/Openers/OpenPrivacyNotification.ts @@ -2,12 +2,29 @@ import stringToHTML from "../stringToHTML"; import { settingsState } from "../listeners/SettingsState"; import { openPopup } from "./PopupManager"; -export function showPrivacyNotification() { - const lastUpdated = "2025-12-19"; +const PRIVACY_STATEMENT_VERSION = "2025-12-19"; - if (document.getElementById("whatsnewbk")) return; - if (settingsState.privacyStatementShown) return; - if (settingsState.privacyStatementLastUpdated && new Date(settingsState.privacyStatementLastUpdated) > new Date(lastUpdated)) return; +export function shouldShowPrivacyNotification(): boolean { + if (settingsState.privacyStatementShown) return false; + if ( + settingsState.privacyStatementLastUpdated && + new Date(settingsState.privacyStatementLastUpdated) > + new Date(PRIVACY_STATEMENT_VERSION) + ) { + return false; + } + return true; +} + +export function showPrivacyNotification(onDismissed?: () => void) { + if (document.getElementById("whatsnewbk")) { + onDismissed?.(); + return; + } + if (!shouldShowPrivacyNotification()) { + onDismissed?.(); + return; + } const header = stringToHTML( /* html */ @@ -48,5 +65,6 @@ export function showPrivacyNotification() { openPopup({ header, content: [text], + afterClose: onDismissed, }); } diff --git a/src/seqta/utils/Openers/OpenWhatsNewPopup.ts b/src/seqta/utils/Openers/OpenWhatsNewPopup.ts index 4a11128e..fe5a8028 100644 --- a/src/seqta/utils/Openers/OpenWhatsNewPopup.ts +++ b/src/seqta/utils/Openers/OpenWhatsNewPopup.ts @@ -3,7 +3,7 @@ import browser from "webextension-polyfill"; import kofi from "@/resources/kofi.png?base64"; import { openPopup } from "./PopupManager"; -export function OpenWhatsNewPopup() { +export function OpenWhatsNewPopup(onDismissed?: () => void) { const header = stringToHTML( /* html */ `
@@ -362,5 +362,7 @@ export function OpenWhatsNewPopup() { openPopup({ header, content: [imageContainer, text, footer], + afterClose: onDismissed, + clearJustUpdated: true, }); } diff --git a/src/seqta/utils/Openers/PopupManager.ts b/src/seqta/utils/Openers/PopupManager.ts index 57125289..ca44c68d 100644 --- a/src/seqta/utils/Openers/PopupManager.ts +++ b/src/seqta/utils/Openers/PopupManager.ts @@ -4,6 +4,13 @@ import { animate as motionAnimate, stagger } from "motion"; type AnimationTarget = string | Element | Element[] | NodeList | null; let isClosing = false; +let pendingAfterClose: (() => void) | undefined; + +function invokeAfterClose() { + const fn = pendingAfterClose; + pendingAfterClose = undefined; + fn?.(); +} export async function closePopup() { if (isClosing) return; @@ -16,12 +23,14 @@ export async function closePopup() { if (!background || !popup) { isClosing = false; + invokeAfterClose(); return; } if (!settingsState.animations) { background.remove(); isClosing = false; + invokeAfterClose(); return; } @@ -33,19 +42,28 @@ export async function closePopup() { background.remove(); isClosing = false; + invokeAfterClose(); } interface OpenPopupOptions { header?: Node | null; content?: (Node | null | undefined)[]; animateSelector?: AnimationTarget; + /** Called once after this popup is fully closed (including skip-animation path). */ + afterClose?: () => void; + /** When true, clears the post-update flag when this popup opens (What's New only). */ + clearJustUpdated?: boolean; } export function openPopup({ header, content = [], animateSelector = ".whatsnewTextContainer *", + afterClose, + clearJustUpdated = false, }: OpenPopupOptions = {}) { + pendingAfterClose = afterClose; + const background = document.createElement("div"); background.id = "whatsnewbk"; background.classList.add("whatsnewBackground"); @@ -88,7 +106,9 @@ export function openPopup({ } } - delete settingsState.justupdated; + if (clearJustUpdated) { + delete settingsState.justupdated; + } background.addEventListener("click", (event) => { if (event.target === background) void closePopup(); diff --git a/src/seqta/utils/Openers/StartupPopupQueue.ts b/src/seqta/utils/Openers/StartupPopupQueue.ts new file mode 100644 index 00000000..b29b9e6c --- /dev/null +++ b/src/seqta/utils/Openers/StartupPopupQueue.ts @@ -0,0 +1,39 @@ +import { settingsState } from "../listeners/SettingsState"; +import { OpenWhatsNewPopup } from "./OpenWhatsNewPopup"; +import { + shouldShowPrivacyNotification, + showPrivacyNotification, +} from "./OpenPrivacyNotification"; +import { + shouldShowEngageParentsAnnouncement, + showEngageParentsAnnouncement, +} from "./OpenEngageParentsAnnouncement"; + +type QueueStep = (goNext: () => void) => void; + +/** + * Runs startup modals in order: What's New (if the extension just updated), + * privacy statement (if required), then the SEQTA Engage announcement (once). + */ +export function runStartupPopupQueue() { + const steps: QueueStep[] = []; + + if (settingsState.justupdated) { + steps.push((goNext) => OpenWhatsNewPopup(goNext)); + } + + if (shouldShowPrivacyNotification()) { + steps.push((goNext) => showPrivacyNotification(goNext)); + } + + if (shouldShowEngageParentsAnnouncement()) { + steps.push((goNext) => showEngageParentsAnnouncement(goNext)); + } + + function runNext() { + const step = steps.shift(); + if (step) step(runNext); + } + + runNext(); +} diff --git a/src/types/storage.ts b/src/types/storage.ts index 232bc4c2..5e72c618 100644 --- a/src/types/storage.ts +++ b/src/types/storage.ts @@ -32,6 +32,8 @@ export interface SettingsState { justupdated?: boolean; privacyStatementShown?: boolean; privacyStatementLastUpdated?: string; + /** One-time announcement: SEQTA Engage support for parents (dismissed popup queue). */ + engageParentsAnnouncementShown?: boolean; timeFormat?: string; animations: boolean; defaultPage: string;