Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions client/src/pages/ExtensionAuth.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ export default function ExtensionAuth() {
const [error, setError] = useState<string | null>(null);

useEffect(() => {
if (!user || tokenSent) return;
if (!user || tokenSent || error) return;

(async () => {
try {
Expand All @@ -35,7 +35,7 @@ export default function ExtensionAuth() {
setError("Something went wrong. Please try again.");
}
})();
}, [user, tokenSent, error]);
}, [user, tokenSent]);

if (isLoading) {
return (
Expand Down
Binary file modified extension/fetchthechange-extension.zip
Binary file not shown.
2 changes: 1 addition & 1 deletion extension/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"manifest_version": 3,
"name": "FetchTheChange",
"description": "Track any element on any web page for changes. Get notified when prices drop, stock updates, or content changes.",
"version": "1.0.2",
"version": "1.0.3",
"permissions": ["storage", "activeTab", "scripting", "tabs"],
"host_permissions": ["https://ftc.bd73.com/*"],
"background": {
Expand Down
13 changes: 4 additions & 9 deletions extension/src/popup/popup.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { BASE_URL, MSG } from "../shared/constants";
import { getToken, clearToken, isTokenValid } from "../auth/token";
import { escapeAttr, sanitizeTier } from "./utils";

// ──────────────────────────────────────────────────────────────────
// State
Expand Down Expand Up @@ -39,6 +40,7 @@ let userInfo: UserInfo | null = null;
let selection: Selection | null = null;
let candidates: Candidate[] = [];
let currentTabUrl = "";
let currentTabTitle = "";
let currentTabId = 0;
let errorMessage = "";
let createdMonitorName = "";
Expand All @@ -57,6 +59,7 @@ async function init(): Promise<void> {
const [tab] = await chrome.tabs.query({ active: true, currentWindow: true });
if (tab) {
currentTabUrl = tab.url || "";
currentTabTitle = tab.title || "";
currentTabId = tab.id || 0;
}

Expand Down Expand Up @@ -308,7 +311,7 @@ function renderAuthenticated(): void {
selector: c.selector,
currentValue: c.text,
url: currentTabUrl,
pageTitle: document.title,
pageTitle: currentTabTitle,
};
state = "confirm";
render();
Expand Down Expand Up @@ -538,14 +541,6 @@ function escapeHtml(str: string): string {
return div.innerHTML;
}

function escapeAttr(str: string): string {
return str.replace(/"/g, "&quot;").replace(/'/g, "&#39;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
}

const KNOWN_TIERS = ["free", "pro", "power"];
function sanitizeTier(tier: string): string {
return KNOWN_TIERS.includes(tier) ? tier : "free";
}

// ──────────────────────────────────────────────────────────────────
// Start
Expand Down
53 changes: 53 additions & 0 deletions extension/src/popup/utils.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import { describe, it, expect } from "vitest";
import { escapeAttr, sanitizeTier } from "./utils";

describe("escapeAttr", () => {
it("escapes ampersand before other entities", () => {
expect(escapeAttr("A & B")).toBe("A &amp; B");
});

it("escapes double quotes", () => {
expect(escapeAttr('say "hello"')).toBe("say &quot;hello&quot;");
});

it("escapes single quotes", () => {
expect(escapeAttr("it's")).toBe("it&#39;s");
});

it("escapes angle brackets", () => {
expect(escapeAttr("<script>")).toBe("&lt;script&gt;");
});

it("does not double-escape existing entities", () => {
// Input contains a literal "&quot;" — the & should be escaped first
expect(escapeAttr("&quot;")).toBe("&amp;quot;");
});

it("handles strings with multiple special characters", () => {
expect(escapeAttr('Price &lt; $5 "deal"')).toBe(
"Price &amp;lt; $5 &quot;deal&quot;",
);
});

it("returns empty string unchanged", () => {
expect(escapeAttr("")).toBe("");
});

it("returns plain text unchanged", () => {
expect(escapeAttr("hello world")).toBe("hello world");
});
});

describe("sanitizeTier", () => {
it("passes through known tiers", () => {
expect(sanitizeTier("free")).toBe("free");
expect(sanitizeTier("pro")).toBe("pro");
expect(sanitizeTier("power")).toBe("power");
});

it("falls back to free for unknown tiers", () => {
expect(sanitizeTier("enterprise")).toBe("free");
expect(sanitizeTier("")).toBe("free");
expect(sanitizeTier("FREE")).toBe("free");
});
});
9 changes: 9 additions & 0 deletions extension/src/popup/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
export function escapeAttr(str: string): string {
return str.replace(/&/g, "&amp;").replace(/"/g, "&quot;").replace(/'/g, "&#39;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
}

// Keep in sync with shared/models/auth.ts TIER_LIMITS
const KNOWN_TIERS = ["free", "pro", "power"];
export function sanitizeTier(tier: string): string {
return KNOWN_TIERS.includes(tier) ? tier : "free";
}
Loading