feat: queue popups and new engage popup

This commit is contained in:
2026-04-12 19:54:43 +09:30
parent 2e9a643a8c
commit 1d9b8f3747
9 changed files with 188 additions and 17 deletions
@@ -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 */
`<div class="whatsnewHeader engageParentsAnnouncementHeader">
<h1>BetterSEQTA Plus now supports <span class="seqtaEngageAccent">SEQTA Engage</span></h1>
<p class="engageParentsSubheading">Buy your mom a BetterSEQTA Plus</p>
</div>`,
).firstChild as HTMLElement;
const text = stringToHTML(/* html */ `
<div class="whatsnewTextContainer privacyStatement" style="overflow-y: auto; font-size: 1.2rem; line-height: 1.6;">
<div class="engageParentsPromoWrap">
<img class="engageParentsPromoImg" src="${ENGAGE_PROMO_IMG_URL}" width="1920" height="1080" alt="BetterSEQTA Plus now supports SEQTA Engage" />
</div>
<p>
<strong class="seqtaEngageAccent">SEQTA Engage</strong> is the portal many parents use for notices, messages, and day-to-day school info.
Before anything else: BetterSEQTA Plus now supports <strong class="seqtaEngageAccent">SEQTA Engage</strong>, 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.
</p>
<p>
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.
</p>
<p>
Close this dialog when you are done. We will not show this announcement again.
</p>
</div>
`).firstChild as HTMLElement;
settingsState.engageParentsAnnouncementShown = true;
openPopup({
header,
content: [text],
afterClose: onDismissed,
});
}
@@ -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,
});
}
+3 -1
View File
@@ -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 */
`<div class="whatsnewHeader">
@@ -362,5 +362,7 @@ export function OpenWhatsNewPopup() {
openPopup({
header,
content: [imageContainer, text, footer],
afterClose: onDismissed,
clearJustUpdated: true,
});
}
+21 -1
View File
@@ -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();
@@ -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();
}