diff --git a/.cursor/commands/commit.md b/.cursor/commands/commit.md new file mode 100644 index 0000000..944bdb8 --- /dev/null +++ b/.cursor/commands/commit.md @@ -0,0 +1,39 @@ +--- +description: AGENT STEPS TO COMMIT CHANGES +--- + +Commit the current changes following Conventional Commits specification. + +## Actions + +1. Check git status to see what files have changed +2. Analyze the changes to determine: + - Type: feat, fix, docs, style, refactor, perf, test, chore, ci, build + - Scope: ui, storage, services, notifications, background, content, popup, hooks, managers, docs, build, workflows + - Subject: concise description of the change +3. Check if changes contain breaking changes (API changes, signature changes, etc.) +4. Generate commit message in format: `(): ` +5. Add `!` after scope if breaking change detected +6. Include body if change needs explanation (ensure each body line is ≤100 characters) +7. Execute: `git add -A` +8. Execute: `git commit -m ""` + +## Rules + +- Use lowercase for type and scope +- Subject should be imperative mood, no period at end +- Maximum 72 characters for subject line +- Maximum 100 characters per line in commit body (wrap long lines with proper indentation) +- If breaking change, add `!` after scope or include `BREAKING CHANGE:` in footer +- Scope is optional but recommended + +## Linting, testing and formatting + +- Before committing execute linting, testing and formatting checks: + - `npm run lint` or `npm run lint:fix` + - `npm run format:check` or `npm run format` + - `npm run test:e2e` + +## CRITICAL + +- DO NOT EXCEED THE CHARACTER LIMIT diff --git a/.cursor/commands/create-pr.md b/.cursor/commands/create-pr.md new file mode 100644 index 0000000..32992e0 --- /dev/null +++ b/.cursor/commands/create-pr.md @@ -0,0 +1,40 @@ +--- +description: CREATE PULL REQUEST +--- + +Analyze current changes and create a GitHub PR with appropriate description and labels. + +## Actions + +1. Check current branch name +2. Get git diff to analyze changes +3. Determine PR type from changes: + - `type:feature` - New functionality + - `type:bugfix` - Bug fixes + - `type:breaking` - Breaking changes + - `type:documentation` - Docs only + - `type:refactor` - Code refactoring + - `type:performance` - Performance improvements + - `type:chore` - Maintenance +4. Determine area labels from changed files: + - `area:ui`, `area:storage`, `area:services`, `area:notifications`, `area:background`, `area:content`, `area:popup`, `area:hooks`, `area:managers`, `area:docs`, `area:build`, `area:workflows` +5. Generate PR description with: + - Summary of changes + - List of modified files + - Breaking changes (if any) + - Testing notes +6. Create PR using GitHub CLI or API: + - Title: Follow conventional commit format + - Body: Generated description + - Labels: Type and area labels + - Base branch: master (or development for ongoing work) +7. Output PR URL + +## Rules + +- PR title should follow conventional commit format +- Include clear description of what changed and why +- List breaking changes prominently if present +- Add appropriate labels for changelog generation +- Set base branch to `master` for releases, `development` for ongoing work +- Ensure CODEOWNERS (@vertefra, @emypeeler) are notified for review diff --git a/components/ui/Button.tsx b/components/ui/Button.tsx index bc8940d..f76544b 100644 --- a/components/ui/Button.tsx +++ b/components/ui/Button.tsx @@ -1,4 +1,4 @@ -import { commonSpacing, spacing, textSize } from "../../design-system" +import { commonSpacing, iconSize, spacing, textSize } from "../../design-system" type ButtonProps = { children: React.ReactNode @@ -72,7 +72,11 @@ const Button = ({ onClick={handleClick} disabled={disabled}> {icon && ( - icon + icon )} {children} diff --git a/components/ui/Header.tsx b/components/ui/Header.tsx index 3d089d9..3d1cdb0 100644 --- a/components/ui/Header.tsx +++ b/components/ui/Header.tsx @@ -1,6 +1,6 @@ import React from "react" -import { commonSpacing, spacing } from "../../design-system" +import { commonSpacing, iconSize, layout, spacing } from "../../design-system" type HeaderProps = { onBack?: () => void @@ -11,9 +11,7 @@ type HeaderProps = { } const headerStyle: React.CSSProperties = { - display: "grid", - gridTemplateColumns: "1fr 1fr 1fr", - alignItems: "center", + ...layout.gridThreeColumn, marginBottom: commonSpacing.itemMargin } @@ -123,7 +121,7 @@ const Header = ({ {centerIconAlt ) : ( centerIcon diff --git a/components/ui/PauseMenu.tsx b/components/ui/PauseMenu.tsx index c129258..42fd78e 100644 --- a/components/ui/PauseMenu.tsx +++ b/components/ui/PauseMenu.tsx @@ -1,6 +1,6 @@ import React from "react" -import { spacing, textSize } from "../../design-system" +import { layout, spacing, textSize, typography } from "../../design-system" export type PauseDuration = "close" | "30s" | "1hour" | "1day" @@ -28,17 +28,14 @@ const menuStyle: React.CSSProperties = { padding: spacing.lg, minWidth: "280px", boxShadow: "0 4px 24px rgba(0, 0, 0, 0.15)", - display: "flex", - flexDirection: "column", - gap: spacing.md + ...layout.actionsContainer } const titleStyle: React.CSSProperties = { - fontSize: textSize.xl, + ...typography.title, fontWeight: "600", margin: 0, - color: "#1a1a1a", - textAlign: "center" + color: "#1a1a1a" } const optionsStyle: React.CSSProperties = { @@ -47,24 +44,17 @@ const optionsStyle: React.CSSProperties = { gap: spacing.sm } -const optionButtonStyle: React.CSSProperties = { +const cancelButtonStyle: React.CSSProperties = { padding: `${spacing.md} ${spacing.lg}`, - backgroundColor: "#f5f5f5", - color: "#1a1a1a", + backgroundColor: "transparent", + color: "#666666", border: "1px solid #e0e0e0", borderRadius: "8px", fontSize: textSize.md, cursor: "pointer", - textAlign: "left", - transition: "all 0.2s ease", - fontFamily: "inherit" -} - -const cancelButtonStyle: React.CSSProperties = { - ...optionButtonStyle, - backgroundColor: "transparent", - color: "#666666", textAlign: "center", + transition: "all 0.2s ease", + fontFamily: "inherit", marginTop: spacing.sm } @@ -82,56 +72,72 @@ const PauseMenu = ({ onSelect, onCancel }: PauseMenuProps) => {

Pause notifications?

{isDebugMode && ( )} diff --git a/components/ui/PrivacyBadge.tsx b/components/ui/PrivacyBadge.tsx index 15b23b8..6acebd6 100644 --- a/components/ui/PrivacyBadge.tsx +++ b/components/ui/PrivacyBadge.tsx @@ -1,4 +1,4 @@ -import { spacing, textSize } from "../../design-system" +import { iconSize, spacing, textSize } from "../../design-system" const PrivacyBadge = () => { const containerStyle: React.CSSProperties = { @@ -21,8 +21,8 @@ const PrivacyBadge = () => { return (
{ useGoogleFonts() const { currentView: reminderView, - currentProduct, + product, reminderId, reminderStartTime, pluginClosed, @@ -91,16 +90,7 @@ const App = () => { setLocalView(VIEW.PRODUCT) } - const handleShowIDontNeedIt = async (product: Product | null) => { - // Update product state to dontNeedIt - if (product) { - try { - await ProductActionManager.dontNeedIt(product) - console.log("[Amazon] Product marked as dontNeedIt") - } catch (error) { - console.error("[Amazon] Failed to execute dontNeedIt:", error) - } - } + const handleShowIDontNeedIt = (_product: Product | null) => { setLocalView(VIEW.I_DONT_NEED_IT) } @@ -133,12 +123,12 @@ const App = () => { if (regex.test(window.location.href)) { // Extract product ID from URL const url = window.location.href - const productId = getAmazonProductId(url) + const _productId = getAmazonProductId(url) if (currentView === VIEW.EARLY_RETURN) { return ( { if (currentView === VIEW.OLD_FLAME) { return ( { return ( (null) - const [currentProduct, setCurrentProduct] = useState(null) + const [product, setProduct] = useState(null) const [reminderId, setReminderId] = useState(null) const [reminderDuration, setReminderDuration] = useState(null) const [reminderStartTime, setReminderStartTime] = useState( @@ -41,7 +41,7 @@ export function useProductPageState({ ) const [pluginClosed, setPluginClosedState] = useState(true) const [tabIdSession, setTabIdSession] = useState(null) - const [currentProductId, setCurrentProductId] = useState(null) + const [urlChangeCounter, setUrlChangeCounter] = useState(0) useEffect(() => { ChromeMessaging.getTabId().then((tabId) => { @@ -50,48 +50,28 @@ export function useProductPageState({ }, []) useEffect(() => { - const updateProductId = () => { - const url = window.location.href - const productId = getProductId(url) - setCurrentProductId((prev) => { - if (prev !== productId) { - console.log( - "[useProductPageState] ProductId changed:", - prev, - "->", - productId - ) - return productId - } - return prev - }) + const handleUrlChange = () => { + console.log("[useProductPageState] URL changed, triggering refresh") + setUrlChangeCounter((prev) => prev + 1) } - // Set initial productId - updateProductId() + // Set initial + handleUrlChange() - // Listen for popstate (browser back/forward) - window.addEventListener("popstate", updateProductId) - - // Listen for full page navigations (when page is fully loaded) - window.addEventListener("load", updateProductId) + // Listen for URL changes + window.addEventListener("popstate", handleUrlChange) + window.addEventListener("load", handleUrlChange) return () => { - window.removeEventListener("popstate", updateProductId) - window.removeEventListener("load", updateProductId) + window.removeEventListener("popstate", handleUrlChange) + window.removeEventListener("load", handleUrlChange) } - }, [getProductId]) + }, []) useEffect(() => { const checkForPendingReminder = async () => { - const productId = currentProductId || getProductId(window.location.href) - console.log( - "[useProductPageState] Using productId:", - productId, - "(from state:", - currentProductId, - ")" - ) + const productId = getProductId(window.location.href) + console.log("[useProductPageState] Using productId:", productId) if (productId) { try { @@ -105,6 +85,7 @@ export function useProductPageState({ ) setPluginClosedState(true) setCurrentView(null) + setProduct(null) return } else if (snoozedUntil) { // Snooze has expired - clear it @@ -112,7 +93,7 @@ export function useProductPageState({ await storage.clearGlobalSnooze() } - // Check 2: Product has terminal state (I_NEED_THIS) + // Check 2: Product has terminal state (I_NEED_THIS only) const compositeKey = `${marketplace}-${productId}` console.log( "[useProductPageState] Checking product storage for key:", @@ -129,6 +110,7 @@ export function useProductPageState({ ) setPluginClosedState(true) setCurrentView(null) + setProduct(null) return } @@ -140,6 +122,7 @@ export function useProductPageState({ ) setPluginClosedState(true) setCurrentView(null) + setProduct(null) return } @@ -149,6 +132,25 @@ export function useProductPageState({ ) setPluginClosedState(false) + // Extract and merge product (fresh DOM data + stored state) + try { + const freshProduct = extractProduct(marketplace, productId) + const mergedProduct = existingProduct + ? { ...freshProduct, state: existingProduct.state } + : freshProduct + setProduct(mergedProduct) + console.log( + "[useProductPageState] Product extracted and merged:", + mergedProduct.id + ) + } catch (error) { + console.error( + "[useProductPageState] Failed to extract product:", + error + ) + setProduct(null) + } + // Check for pending reminders (early return / old flame views) const reminders = await storage.getReminders() const pendingReminder = reminders.find( @@ -179,15 +181,15 @@ export function useProductPageState({ setReminderDuration(null) setReminderStartTime(null) setCurrentView(null) + // Keep product - it was already extracted above return } + setReminderId(pendingReminder.id) setReminderDuration(pendingReminder.duration) setReminderStartTime( pendingReminder.reminderTime - pendingReminder.duration ) - const product = extractProduct(marketplace, productId) - setCurrentProduct(product) const now = Date.now() if (pendingReminder.reminderTime > now) { @@ -253,7 +255,7 @@ export function useProductPageState({ StorageProxyService.addChangeListener(storageListener) return removeListener - }, [getProductId, marketplace, tabIdSession, currentProductId]) + }, [getProductId, marketplace, tabIdSession, urlChangeCounter]) const setPluginClosed = async (closed: boolean) => { console.log("[useProductPageState] Setting global plugin closed:", closed) @@ -263,7 +265,7 @@ export function useProductPageState({ return { currentView, - currentProduct, + product, reminderId, reminderDuration, reminderStartTime, diff --git a/style.css b/style.css index 9337a05..797e847 100644 --- a/style.css +++ b/style.css @@ -88,3 +88,39 @@ body { margin: 0; line-height: 1.4; } + +/* Circular icon button base */ +.circular-button { + padding: 4px; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + border: none; + transition: background-color 0.2s ease; + background-color: transparent; + cursor: pointer; +} + +/* Option button (for menus like PauseMenu, SleepOnIt) */ +.option-button { + padding: 12px 16px; + border-radius: 8px; + border: 2px solid rgba(255, 255, 255, 0.2); + background-color: transparent; + color: var(--text-color-light); + font-size: 14px; + font-weight: 500; + cursor: pointer; + transition: all 0.2s ease; +} + +.option-button:hover { + background-color: rgba(255, 255, 255, 0.1); + transform: translateY(-1px); +} + +.option-button-selected { + border-color: var(--primary-button-color); + background-color: rgba(104, 195, 212, 0.2); +} diff --git a/tests/e2e/close-flow.spec.ts b/tests/e2e/close-flow.spec.ts index cda723a..0e540d1 100644 --- a/tests/e2e/close-flow.spec.ts +++ b/tests/e2e/close-flow.spec.ts @@ -1,6 +1,10 @@ import { expect, test } from "./fixtures" import { OverlayPage } from "./page-objects/OverlayPage" -import { TEST_CONFIG } from "./test-config" +import { + PRIMARY_PRODUCT_ID, + SECONDARY_PRODUCT_ID, + TEST_CONFIG +} from "./test-config" import { navigateToProduct } from "./utils/extension-helpers" test.describe('ThinkTwice "Close and Pause" Flow', () => { @@ -10,12 +14,13 @@ test.describe('ThinkTwice "Close and Pause" Flow', () => { extensionHelper }) => { const page = await extensionContext.newPage() + const productId = PRIMARY_PRODUCT_ID // Clear storage await extensionHelper.clearStorage() // Navigate to Amazon product page - await navigateToProduct(page, TEST_CONFIG.AMAZON_PRODUCT_IDS.PRIMARY) + await navigateToProduct(page, productId) // Create overlay page object const overlayPage = new OverlayPage(page, extensionId) @@ -46,7 +51,7 @@ test.describe('ThinkTwice "Close and Pause" Flow', () => { // Open a NEW tab to the same product const newPage = await extensionContext.newPage() - await navigateToProduct(newPage, TEST_CONFIG.AMAZON_PRODUCT_IDS.PRIMARY) + await navigateToProduct(newPage, productId) // Verify overlay is NOT visible in the new tab (global close affects all tabs) const newOverlayPage = new OverlayPage(newPage, extensionId) @@ -59,12 +64,14 @@ test.describe('ThinkTwice "Close and Pause" Flow', () => { extensionHelper }) => { const page = await extensionContext.newPage() + const productId1 = SECONDARY_PRODUCT_ID + const productId2 = PRIMARY_PRODUCT_ID // Clear storage await extensionHelper.clearStorage() // Navigate to Amazon product page - await navigateToProduct(page, TEST_CONFIG.AMAZON_PRODUCT_IDS.PRIMARY) + await navigateToProduct(page, productId1) // Create overlay page object const overlayPage = new OverlayPage(page, extensionId) @@ -86,7 +93,7 @@ test.describe('ThinkTwice "Close and Pause" Flow', () => { // Navigate to a DIFFERENT product page in a NEW tab const newPage = await extensionContext.newPage() - await navigateToProduct(newPage, TEST_CONFIG.AMAZON_PRODUCT_IDS.SECONDARY) + await navigateToProduct(newPage, productId2) // Verify overlay is NOT shown (global snooze active) const newOverlayPage = new OverlayPage(newPage, extensionId) @@ -99,12 +106,13 @@ test.describe('ThinkTwice "Close and Pause" Flow', () => { extensionHelper }) => { const page = await extensionContext.newPage() + const productId = PRIMARY_PRODUCT_ID // Clear storage await extensionHelper.clearStorage() // Navigate to Amazon product page - await navigateToProduct(page, TEST_CONFIG.AMAZON_PRODUCT_IDS.PRIMARY) + await navigateToProduct(page, productId) // Create overlay page object const overlayPage = new OverlayPage(page, extensionId) @@ -138,12 +146,14 @@ test.describe('ThinkTwice "Close and Pause" Flow', () => { test.setTimeout(70000) const page = await extensionContext.newPage() + const productId1 = SECONDARY_PRODUCT_ID + const productId2 = PRIMARY_PRODUCT_ID // Clear storage await extensionHelper.clearStorage() // Navigate to Amazon product page - await navigateToProduct(page, "B005EJH6Z4") + await navigateToProduct(page, productId1) // Create overlay page object const overlayPage = new OverlayPage(page, extensionId) @@ -167,7 +177,7 @@ test.describe('ThinkTwice "Close and Pause" Flow', () => { // Open a NEW tab immediately console.log("Opening new tab to verify global snooze...") const newPage = await extensionContext.newPage() - await navigateToProduct(newPage, TEST_CONFIG.AMAZON_PRODUCT_IDS.SECONDARY) + await navigateToProduct(newPage, productId2) // Verify overlay is NOT visible in the new tab (global snooze active) const newOverlayPage = new OverlayPage(newPage, extensionId) diff --git a/tests/e2e/extension.spec.ts b/tests/e2e/extension.spec.ts index 9c98957..2f36d3d 100644 --- a/tests/e2e/extension.spec.ts +++ b/tests/e2e/extension.spec.ts @@ -1,6 +1,6 @@ import { expect, test } from "./fixtures" import { OverlayPage } from "./page-objects/OverlayPage" -import { TEST_CONFIG } from "./test-config" +import { PRIMARY_PRODUCT_ID } from "./test-config" import { navigateToProduct } from "./utils/extension-helpers" test.describe("ThinkTwice Plugin E2E", () => { @@ -20,7 +20,7 @@ test.describe("ThinkTwice Plugin E2E", () => { // Navigate to a known valid Amazon product page console.log("Navigating to Amazon product page...") - await navigateToProduct(page, TEST_CONFIG.AMAZON_PRODUCT_IDS.PRIMARY) + await navigateToProduct(page, PRIMARY_PRODUCT_ID) // Create overlay page object const overlayPage = new OverlayPage(page, extensionId) diff --git a/tests/e2e/fixtures.ts b/tests/e2e/fixtures.ts index 3b4bf06..500a208 100644 --- a/tests/e2e/fixtures.ts +++ b/tests/e2e/fixtures.ts @@ -37,8 +37,9 @@ export const test = base.extend({ "../../tmp/test-user-data-" + Math.random().toString(36).substring(7) ) + const isHeaded = process.env.HEADED === "true" const context = await chromium.launchPersistentContext(userDataDir, { - headless: !!process.env.CI || process.env.HEADLESS === "true", + headless: !isHeaded, // Default headless, set HEADED=true for headed mode userAgent: "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36", locale: "en-US", @@ -51,7 +52,10 @@ export const test = base.extend({ "--disable-dev-shm-usage", "--disable-blink-features=AutomationControlled", "--disable-features=IsolateOrigins,site-per-process", - "--disable-site-isolation-trials" + "--disable-site-isolation-trials", + "--window-size=1280,720", + "--disable-web-security", + "--disable-features=VizDisplayCompositor" ] }) @@ -72,9 +76,13 @@ export const test = base.extend({ get: () => ["en-US", "en"] }) - // Add chrome object - // eslint-disable-next-line @typescript-eslint/no-explicit-any - ;(window as any).chrome = { runtime: {} } + // Add chrome object ONLY on non-extension pages + // Don't override the real chrome API on extension pages + + if (!location.href.startsWith("chrome-extension://")) { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + ;(window as any).chrome = { runtime: {} } + } }) await use(context) diff --git a/tests/e2e/idontneedit.spec.ts b/tests/e2e/idontneedit.spec.ts index 59ff1e3..921bfce 100644 --- a/tests/e2e/idontneedit.spec.ts +++ b/tests/e2e/idontneedit.spec.ts @@ -1,6 +1,6 @@ -import { test } from "./fixtures" +import { expect, test } from "./fixtures" import { OverlayPage } from "./page-objects/OverlayPage" -import { TEST_CONFIG } from "./test-config" +import { PRIMARY_PRODUCT_ID, TEST_CONFIG } from "./test-config" import { navigateToProduct } from "./utils/extension-helpers" import { buildProductId } from "./utils/product-helpers" @@ -11,12 +11,13 @@ test.describe('ThinkTwice "I don\'t really need it" Flow', () => { extensionHelper }) => { const page = await extensionContext.newPage() + const productId = PRIMARY_PRODUCT_ID // Clear storage to ensure clean state await extensionHelper.clearStorage() // Navigate to a known valid Amazon product page - await navigateToProduct(page, TEST_CONFIG.AMAZON_PRODUCT_IDS.PRIMARY) + await navigateToProduct(page, productId) // Create overlay page object const overlayPage = new OverlayPage(page, extensionId) @@ -33,18 +34,21 @@ test.describe('ThinkTwice "I don\'t really need it" Flow', () => { ) // Verify product state is saved to storage - const expectedProductId = buildProductId( - "amazon", - TEST_CONFIG.AMAZON_PRODUCT_IDS.PRIMARY - ) + const expectedProductId = buildProductId("amazon", productId) await extensionHelper.assertProductState( expectedProductId, TEST_CONFIG.STATES.DONT_NEED_IT ) - // Verify it auto-closes after roughly 4 seconds - await overlayPage.expectCelebrationHidden( - TEST_CONFIG.TEXT.CELEBRATION_DONT_NEED - ) + // Verify tab closes or overlay disappears after celebration (4 seconds) + try { + await page.waitForTimeout(5000) + if (!page.isClosed()) { + await overlayPage.expectHidden(2000) + } + } catch { + expect(page.isClosed()).toBe(true) + console.log("[Test] Tab was closed as expected") + } }) }) diff --git a/tests/e2e/ineedit.spec.ts b/tests/e2e/ineedit.spec.ts index da95e09..438b5ff 100644 --- a/tests/e2e/ineedit.spec.ts +++ b/tests/e2e/ineedit.spec.ts @@ -1,6 +1,10 @@ import { expect, test } from "./fixtures" import { OverlayPage } from "./page-objects/OverlayPage" -import { TEST_CONFIG } from "./test-config" +import { + PRIMARY_PRODUCT_ID, + SECONDARY_PRODUCT_ID, + TEST_CONFIG +} from "./test-config" import { navigateToProduct } from "./utils/extension-helpers" import { buildProductId } from "./utils/product-helpers" @@ -11,12 +15,13 @@ test.describe('ThinkTwice "I need it" Flow', () => { extensionHelper }) => { const page = await extensionContext.newPage() + const productId = PRIMARY_PRODUCT_ID // Clear storage to ensure clean state await extensionHelper.clearStorage() // Navigate to product page - await navigateToProduct(page, TEST_CONFIG.AMAZON_PRODUCT_IDS.PRIMARY) + await navigateToProduct(page, productId) // Create overlay page object const overlayPage = new OverlayPage(page, extensionId) @@ -45,10 +50,7 @@ test.describe('ThinkTwice "I need it" Flow', () => { await overlayPage.expectHidden(2000) // Verify product state is saved to storage - const expectedProductId = buildProductId( - "amazon", - TEST_CONFIG.AMAZON_PRODUCT_IDS.PRIMARY - ) + const expectedProductId = buildProductId("amazon", productId) await extensionHelper.assertProductState( expectedProductId, TEST_CONFIG.STATES.I_NEED_THIS @@ -59,7 +61,7 @@ test.describe('ThinkTwice "I need it" Flow', () => { // Navigate to the same product URL again const newPage = await extensionContext.newPage() - await navigateToProduct(newPage, TEST_CONFIG.AMAZON_PRODUCT_IDS.PRIMARY) + await navigateToProduct(newPage, productId) // Wait a bit for the extension to check the product state await newPage.waitForTimeout(2000) @@ -75,12 +77,14 @@ test.describe('ThinkTwice "I need it" Flow', () => { extensionHelper }) => { const page = await extensionContext.newPage() + const productId1 = SECONDARY_PRODUCT_ID + const productId2 = PRIMARY_PRODUCT_ID // Clear storage to ensure clean state await extensionHelper.clearStorage() - // Navigate to first product (PRIMARY) - await navigateToProduct(page, TEST_CONFIG.AMAZON_PRODUCT_IDS.PRIMARY) + // Navigate to first product + await navigateToProduct(page, productId1) // Create overlay page object const overlayPage = new OverlayPage(page, extensionId) @@ -105,8 +109,8 @@ test.describe('ThinkTwice "I need it" Flow', () => { // Verify overlay is hidden for Product A after auto-close await overlayPage.expectHidden(2000) - // Navigate to a different product (SECONDARY) - await navigateToProduct(page, TEST_CONFIG.AMAZON_PRODUCT_IDS.SECONDARY) + // Navigate to a different product + await navigateToProduct(page, productId2) // Wait for extension to initialize after navigation await page.waitForTimeout(1000) diff --git a/tests/e2e/page-objects/OverlayPage.ts b/tests/e2e/page-objects/OverlayPage.ts index bc85e0f..47cc9e8 100644 --- a/tests/e2e/page-objects/OverlayPage.ts +++ b/tests/e2e/page-objects/OverlayPage.ts @@ -112,6 +112,17 @@ export class OverlayPage { await button.click() } + /** + * Click any button by name + */ + async clickButton(name: string | RegExp): Promise { + const button = this.getButton(name) + await expect(button).toBeVisible({ + timeout: TEST_CONFIG.TIMEOUTS.BUTTON_VISIBLE + }) + await button.click() + } + /** * Wait for overlay to be attached to the DOM */ diff --git a/tests/e2e/setup.ts b/tests/e2e/setup.ts index 5f4d1e7..597eaa2 100644 --- a/tests/e2e/setup.ts +++ b/tests/e2e/setup.ts @@ -1,7 +1,7 @@ import path from "path" import { chromium, expect, test as setup } from "@playwright/test" -import { TEST_CONFIG } from "./test-config" +import { PRIMARY_PRODUCT_ID } from "./test-config" const EXTENSION_PATH = path.resolve(__dirname, "../../build/chrome-mv3-dev") @@ -9,8 +9,9 @@ setup("Verify extension is loadable", async () => { console.log("Setup: Verifying extension load from", EXTENSION_PATH) const userDataDir = path.join(__dirname, "../../tmp/test-user-data-setup") + const isHeaded = process.env.HEADED === "true" const context = await chromium.launchPersistentContext(userDataDir, { - headless: process.env.CI ? true : false, // Headless in CI, headed locally for debugging + headless: !isHeaded, // Default headless, set HEADED=true for headed mode userAgent: "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36", viewport: { width: 1280, height: 720 }, @@ -24,7 +25,10 @@ setup("Verify extension is loadable", async () => { "--disable-dev-shm-usage", "--disable-blink-features=AutomationControlled", "--disable-features=IsolateOrigins,site-per-process", - "--disable-site-isolation-trials" + "--disable-site-isolation-trials", + "--window-size=1280,720", + "--disable-web-security", + "--disable-features=VizDisplayCompositor" ] }) @@ -47,9 +51,13 @@ setup("Verify extension is loadable", async () => { get: () => ["en-US", "en"] }) - // Add chrome object - // eslint-disable-next-line @typescript-eslint/no-explicit-any - ;(window as any).chrome = { runtime: {} } + // Add chrome object ONLY on non-extension pages + // Don't override the real chrome API on extension pages + + if (!location.href.startsWith("chrome-extension://")) { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + ;(window as any).chrome = { runtime: {} } + } }) // In headless mode, we can't navigate to chrome:// URLs @@ -66,10 +74,9 @@ setup("Verify extension is loadable", async () => { "sec-ch-ua-platform": '"Windows"' }) - await page.goto( - `https://www.amazon.com/dp/${TEST_CONFIG.AMAZON_PRODUCT_IDS.PRIMARY}`, - { waitUntil: "load" } - ) + await page.goto(`https://www.amazon.com/dp/${PRIMARY_PRODUCT_ID}`, { + waitUntil: "load" + }) // Plasmo injects a custom element with the tag 'plasmo-csui' // We'll wait for this standard selector diff --git a/tests/e2e/sleeponit.spec.ts b/tests/e2e/sleeponit.spec.ts index 3a1b08a..529b973 100644 --- a/tests/e2e/sleeponit.spec.ts +++ b/tests/e2e/sleeponit.spec.ts @@ -1,7 +1,7 @@ import { expect, test } from "./fixtures" import { OverlayPage } from "./page-objects/OverlayPage" import { PopupPage } from "./page-objects/PopupPage" -import { TEST_CONFIG } from "./test-config" +import { PRIMARY_PRODUCT_ID, TEST_CONFIG } from "./test-config" import { navigateToProduct } from "./utils/extension-helpers" import { extractProductInfo } from "./utils/product-helpers" @@ -12,12 +12,13 @@ test.describe('ThinkTwice "Sleep on it" Flow', () => { extensionHelper }) => { const page = await extensionContext.newPage() + const productId = PRIMARY_PRODUCT_ID // Clear storage to ensure clean state await extensionHelper.clearStorage() // Navigate to the test product page - await navigateToProduct(page, TEST_CONFIG.AMAZON_PRODUCT_IDS.PRIMARY) + await navigateToProduct(page, productId) // Create overlay page object const overlayPage = new OverlayPage(page, extensionId) @@ -73,12 +74,13 @@ test.describe('ThinkTwice "Sleep on it" Flow', () => { test.setTimeout(120000) // 2 minutes const page = await extensionContext.newPage() + const productId = PRIMARY_PRODUCT_ID // Clear storage to ensure clean state await extensionHelper.clearStorage() // Navigate to the test product page - await navigateToProduct(page, TEST_CONFIG.AMAZON_PRODUCT_IDS.PRIMARY) + await navigateToProduct(page, productId) // Extract product information const productInfo = await extractProductInfo(page) @@ -156,12 +158,13 @@ test.describe('ThinkTwice "Sleep on it" Flow', () => { extensionHelper }) => { const page = await extensionContext.newPage() + const productId = PRIMARY_PRODUCT_ID // Clear storage to ensure clean state await extensionHelper.clearStorage() // Navigate to the test product page - await navigateToProduct(page, TEST_CONFIG.AMAZON_PRODUCT_IDS.PRIMARY) + await navigateToProduct(page, productId) // Extract product information const productInfo = await extractProductInfo(page) @@ -208,7 +211,7 @@ test.describe('ThinkTwice "Sleep on it" Flow', () => { await productPage.waitForLoadState("load") // Verify we're on the correct product page - expect(productPage.url()).toContain(TEST_CONFIG.AMAZON_PRODUCT_IDS.PRIMARY) + expect(productPage.url()).toContain(productId) console.log("[Test] Product page opened successfully") // Reload the popup to see updated state @@ -238,7 +241,7 @@ test.describe('ThinkTwice "Sleep on it" Flow', () => { await extensionHelper.clearStorage() // Navigate to the test product page - await navigateToProduct(page, TEST_CONFIG.AMAZON_PRODUCT_IDS.PRIMARY) + await navigateToProduct(page, PRIMARY_PRODUCT_ID) // Create overlay page object and complete sleep on it flow const overlayPage = new OverlayPage(page, extensionId) @@ -260,7 +263,7 @@ test.describe('ThinkTwice "Sleep on it" Flow', () => { // Go back to the same product url const newPage = await extensionContext.newPage() - await navigateToProduct(newPage, TEST_CONFIG.AMAZON_PRODUCT_IDS.PRIMARY) + await navigateToProduct(newPage, PRIMARY_PRODUCT_ID) // Create overlay page object for the new page const newOverlayPage = new OverlayPage(newPage, extensionId) @@ -279,4 +282,361 @@ test.describe('ThinkTwice "Sleep on it" Flow', () => { await newPage.close() }) + + test("should show BackToAnOldFlame view when returning after timer expires", async ({ + extensionContext, + extensionId, + extensionHelper + }) => { + test.setTimeout(120000) // 2 minutes + + const page = await extensionContext.newPage() + const productId = PRIMARY_PRODUCT_ID + + // Clear storage to ensure clean state + await extensionHelper.clearStorage() + + // Navigate to the test product page + await navigateToProduct(page, productId) + + // Create overlay page object and complete sleep on it flow with 1 minute timer + const overlayPage = new OverlayPage(page, extensionId) + await overlayPage.expectAttached() + await overlayPage.clickSleepOnIt() + await overlayPage.selectDuration(/1 minute/i) + await overlayPage.clickSetReminder() + await overlayPage.expectSuccessMessage() + + // Wait for tab to close + try { + await page.waitForTimeout(6000) + if (!page.isClosed()) { + await page.close() + } + } catch { + // Expected: page closed + } + + // Wait for timer to expire + console.log("[Test] Waiting for 1 minute timer to expire...") + await page.waitForTimeout(TEST_CONFIG.TIMEOUTS.REMINDER_1MIN) + + // Go back to the same product url AFTER timer expired + const newPage = await extensionContext.newPage() + await navigateToProduct(newPage, productId) + + // Create overlay page object for the new page + const newOverlayPage = new OverlayPage(newPage, extensionId) + await newOverlayPage.expectAttached() + + // Verify BackToAnOldFlame view is shown + await newOverlayPage.expectTextVisible("You're back!", 5000) + await newOverlayPage.expectTextVisible("thoughtful", 5000) + + // Verify buttons are present + await expect(newOverlayPage.getButton(/Yes, I want it/i)).toBeVisible() + await expect(newOverlayPage.getButton(/I don't need it/i)).toBeVisible() + await expect(newOverlayPage.getButton(/I'm still not sure/i)).toBeVisible() + + await newPage.close() + }) + + test('should delete reminder when clicking "I need this now" during early return', async ({ + extensionContext, + extensionId, + extensionHelper + }) => { + const page = await extensionContext.newPage() + const productId = PRIMARY_PRODUCT_ID + + // Clear storage to ensure clean state + await extensionHelper.clearStorage() + + // Navigate to the test product page + await navigateToProduct(page, productId) + + // Extract product information + const productInfo = await extractProductInfo(page) + + // Create overlay page object and complete sleep on it flow + const overlayPage = new OverlayPage(page, extensionId) + await overlayPage.expectAttached() + await overlayPage.clickSleepOnIt() + await overlayPage.selectDuration(/24 hours/i) + await overlayPage.clickSetReminder() + await overlayPage.expectSuccessMessage() + + // Wait for tab to close + try { + await page.waitForTimeout(6000) + if (!page.isClosed()) { + await page.close() + } + } catch { + // Expected: page closed + } + + // Open popup and verify product is in "Sleeping on it" section + const popupPageObj = new PopupPage( + await extensionContext.newPage(), + extensionId + ) + await popupPageObj.goto() + if (productInfo.name) { + await popupPageObj.expectProductInSleeping(productInfo.name) + } + await popupPageObj.page.close() + + // Go back to the same product url BEFORE timer expires + const newPage = await extensionContext.newPage() + await navigateToProduct(newPage, productId) + + // Create overlay page object for the new page + const newOverlayPage = new OverlayPage(newPage, extensionId) + await newOverlayPage.expectAttached() + + // Verify EarlyReturnFromSleep view is shown + await newOverlayPage.expectTextVisible("You're back!", 5000) + + // Click "I need this now" button + await newOverlayPage.clickButton(/I need this now/i) + + // Verify celebration is shown + await newOverlayPage.expectCelebrationVisible( + TEST_CONFIG.TEXT.CELEBRATION_NEED_IT + ) + + // Wait for overlay to hide + await newOverlayPage.expectHidden(TEST_CONFIG.TIMEOUTS.CELEBRATION_FADE) + + // Open popup again and verify product is NOT in "Sleeping on it" section + const popupPageObj2 = new PopupPage( + await extensionContext.newPage(), + extensionId + ) + await popupPageObj2.goto() + if (productInfo.name) { + await popupPageObj2.expectProductNotInSleeping(productInfo.name) + } + + console.log( + '[Test] Successfully verified reminder deletion after "I need this now"' + ) + }) + + test('should delete reminder when clicking "Yes, I want it" after timer expires', async ({ + extensionContext, + extensionId, + extensionHelper + }) => { + test.setTimeout(120000) // 2 minutes + + const page = await extensionContext.newPage() + const productId = PRIMARY_PRODUCT_ID + + // Clear storage to ensure clean state + await extensionHelper.clearStorage() + + // Navigate to the test product page + await navigateToProduct(page, productId) + + // Extract product information + const productInfo = await extractProductInfo(page) + + // Create overlay page object and complete sleep on it flow with 1 minute timer + const overlayPage = new OverlayPage(page, extensionId) + await overlayPage.expectAttached() + await overlayPage.clickSleepOnIt() + await overlayPage.selectDuration(/1 minute/i) + await overlayPage.clickSetReminder() + await overlayPage.expectSuccessMessage() + + // Wait for tab to close + try { + await page.waitForTimeout(6000) + if (!page.isClosed()) { + await page.close() + } + } catch { + // Expected: page closed + } + + // Wait for timer to expire + console.log("[Test] Waiting for 1 minute timer to expire...") + await page.waitForTimeout(TEST_CONFIG.TIMEOUTS.REMINDER_1MIN) + + // Open popup and verify product is still in "Sleeping on it" section + const popupPageObj = new PopupPage( + await extensionContext.newPage(), + extensionId + ) + await popupPageObj.goto() + if (productInfo.name) { + await popupPageObj.expectProductInSleeping(productInfo.name) + } + await popupPageObj.page.close() + + // Go back to the same product url AFTER timer expired + const newPage = await extensionContext.newPage() + await navigateToProduct(newPage, productId) + + // Create overlay page object for the new page + const newOverlayPage = new OverlayPage(newPage, extensionId) + await newOverlayPage.expectAttached() + + // Verify BackToAnOldFlame view is shown + await newOverlayPage.expectTextVisible("You're back!", 5000) + + // Click "Yes, I want it" button + await newOverlayPage.clickButton(/Yes, I want it/i) + + // Verify celebration is shown + await newOverlayPage.expectCelebrationVisible( + TEST_CONFIG.TEXT.CELEBRATION_NEED_IT + ) + + // Wait for overlay to hide + await newOverlayPage.expectHidden(TEST_CONFIG.TIMEOUTS.CELEBRATION_FADE) + + // Open popup again and verify product is NOT in "Sleeping on it" section + const popupPageObj2 = new PopupPage( + await extensionContext.newPage(), + extensionId + ) + await popupPageObj2.goto() + if (productInfo.name) { + await popupPageObj2.expectProductNotInSleeping(productInfo.name) + } + + console.log( + '[Test] Successfully verified reminder deletion after "Yes, I want it"' + ) + }) + + test("should show EarlyReturnFromSleep on multiple early returns", async ({ + extensionContext, + extensionId, + extensionHelper + }) => { + const page = await extensionContext.newPage() + const productId = PRIMARY_PRODUCT_ID + + // Clear storage to ensure clean state + await extensionHelper.clearStorage() + + // Navigate to the test product page + await navigateToProduct(page, productId) + + // Create overlay page object and complete sleep on it flow + const overlayPage = new OverlayPage(page, extensionId) + await overlayPage.expectAttached() + await overlayPage.clickSleepOnIt() + await overlayPage.selectDuration(/24 hours/i) + await overlayPage.clickSetReminder() + await overlayPage.expectSuccessMessage() + + // Wait for tab to close + try { + await page.waitForTimeout(6000) + if (!page.isClosed()) { + await page.close() + } + } catch { + // Expected: page closed + } + + // First early return + const newPage1 = await extensionContext.newPage() + await navigateToProduct(newPage1, productId) + const newOverlayPage1 = new OverlayPage(newPage1, extensionId) + await newOverlayPage1.expectAttached() + await newOverlayPage1.expectTextVisible("You're back!", 5000) + await expect(newOverlayPage1.getButton(/I need this now/i)).toBeVisible() + + // Click "I'll wait" + await newOverlayPage1.clickButton(/I'll wait/i) + await newOverlayPage1.expectHidden(5000) + await newPage1.close() + + // Second early return - should still show EarlyReturnFromSleep + const newPage2 = await extensionContext.newPage() + await navigateToProduct(newPage2, productId) + const newOverlayPage2 = new OverlayPage(newPage2, extensionId) + await newOverlayPage2.expectAttached() + await newOverlayPage2.expectTextVisible("You're back!", 5000) + await expect(newOverlayPage2.getButton(/I need this now/i)).toBeVisible() + + console.log( + "[Test] Successfully verified multiple early returns show EarlyReturnFromSleep" + ) + + await newPage2.close() + }) + + test("should show BackToAnOldFlame at exact expiration time", async ({ + extensionContext, + extensionId, + extensionHelper + }) => { + test.setTimeout(120000) // 2 minutes + + const page = await extensionContext.newPage() + const productId = PRIMARY_PRODUCT_ID + + // Clear storage to ensure clean state + await extensionHelper.clearStorage() + + // Navigate to the test product page + await navigateToProduct(page, productId) + + // Record start time + const startTime = Date.now() + + // Create overlay page object and complete sleep on it flow with 1 minute timer + const overlayPage = new OverlayPage(page, extensionId) + await overlayPage.expectAttached() + await overlayPage.clickSleepOnIt() + await overlayPage.selectDuration(/1 minute/i) + await overlayPage.clickSetReminder() + await overlayPage.expectSuccessMessage() + + // Wait for tab to close + try { + await page.waitForTimeout(6000) + if (!page.isClosed()) { + await page.close() + } + } catch { + // Expected: page closed + } + + // Wait until exactly 1 minute has passed + const elapsedTime = Date.now() - startTime + const remainingTime = TEST_CONFIG.TIMEOUTS.REMINDER_1MIN - elapsedTime + if (remainingTime > 0) { + await page.waitForTimeout(remainingTime) + } + + // Go back to the same product url at expiration time + const newPage = await extensionContext.newPage() + await navigateToProduct(newPage, productId) + + // Create overlay page object for the new page + const newOverlayPage = new OverlayPage(newPage, extensionId) + await newOverlayPage.expectAttached() + + // Verify BackToAnOldFlame view is shown (not EarlyReturnFromSleep) + await newOverlayPage.expectTextVisible("You're back!", 5000) + await newOverlayPage.expectTextVisible("thoughtful", 5000) + + // Verify buttons are for BackToAnOldFlame + await expect(newOverlayPage.getButton(/Yes, I want it/i)).toBeVisible() + await expect(newOverlayPage.getButton(/I'm still not sure/i)).toBeVisible() + + console.log( + "[Test] Successfully verified BackToAnOldFlame at exact expiration time" + ) + + await newPage.close() + }) }) diff --git a/tests/e2e/test-config.ts b/tests/e2e/test-config.ts index aa083f4..d18fd49 100644 --- a/tests/e2e/test-config.ts +++ b/tests/e2e/test-config.ts @@ -15,19 +15,12 @@ export const TEST_CONFIG = { * Amazon product IDs used across E2E tests. * These should be stable, long-lived products. */ - AMAZON_PRODUCT_IDS: { - /** - * Primary test product - used in most test cases - * Current: Anker PowerCore 10000 Portable Charger - */ - PRIMARY: "B005EJH6Z4", - - /** - * Secondary test product - used for multi-tab scenarios - * Should be different from PRIMARY to test different products - */ - SECONDARY: "B07BMKXBVW" - }, + AMAZON_PRODUCT_IDS: [ + "B005EJH6Z4", + "B07BMKXBVW", + "B09B8V1LZ3", + "B0F33VR8LB" + ] as const, /** * Timeout values in milliseconds @@ -85,3 +78,15 @@ export const TEST_CONFIG = { SLEEPING: "sleeping" } } as const + +/** + * Get a random product ID from the configured product IDs + */ +function getRandomProductId(): string { + const ids = TEST_CONFIG.AMAZON_PRODUCT_IDS + const idx = Math.floor(Math.random() * ids.length) + return ids[idx] +} + +export const PRIMARY_PRODUCT_ID = getRandomProductId() +export const SECONDARY_PRODUCT_ID = getRandomProductId() diff --git a/tests/e2e/utils/error-helpers.ts b/tests/e2e/utils/error-helpers.ts index 1810c58..7de532b 100644 --- a/tests/e2e/utils/error-helpers.ts +++ b/tests/e2e/utils/error-helpers.ts @@ -93,4 +93,3 @@ export async function waitForTabClosureOrTimeout( return page.isClosed() } } - diff --git a/tests/e2e/utils/product-helpers.ts b/tests/e2e/utils/product-helpers.ts index c4c1bd9..31cf739 100644 --- a/tests/e2e/utils/product-helpers.ts +++ b/tests/e2e/utils/product-helpers.ts @@ -53,4 +53,3 @@ export function extractProductIdFromUrl(url: string): string | null { return null } - diff --git a/tests/e2e/utils/wait-helpers.ts b/tests/e2e/utils/wait-helpers.ts index ba059d9..b2bade8 100644 --- a/tests/e2e/utils/wait-helpers.ts +++ b/tests/e2e/utils/wait-helpers.ts @@ -56,4 +56,3 @@ export async function waitForCelebrationHidden( timeout: timeout || TEST_CONFIG.TIMEOUTS.CELEBRATION_FADE }) } - diff --git a/tests/flows/Idontneedit-flow.md b/tests/flows/Idontneedit-flow.md index bf91d23..c20daff 100644 --- a/tests/flows/Idontneedit-flow.md +++ b/tests/flows/Idontneedit-flow.md @@ -39,11 +39,10 @@ The "I don't really need it" action marks the product as unwanted, records this ### 5. Auto-Close -- **Timer**: After 4 seconds, the `Celebration` component triggers its `onClose` callback. -- **Cleanup**: - - `setPluginClosed(true)` is called. - - The tab session state is updated in storage to mark `pluginClosed: true`. -- **Result**: The extension overlay is removed from the DOM (`shouldShowOverlay` becomes `false`), effectively ending the interaction for that product/session. +- **Timer**: After 4 seconds, the celebration triggers tab closure. +- **Action**: Calls `ChromeMessaging.closeCurrentTab()`. +- **Fallback**: If tab close fails, calls `onClose()` to hide the overlay. +- **Result**: The browser tab closes, removing the temptation to purchase. If the user navigates to the same product again in the future, the overlay will appear again (unlike "I need it" which is a terminal state). ## Related Files diff --git a/views/BackToAnOldFlame.tsx b/views/BackToAnOldFlame.tsx index 7e84dbc..5d499d0 100644 --- a/views/BackToAnOldFlame.tsx +++ b/views/BackToAnOldFlame.tsx @@ -1,10 +1,24 @@ +/** + * BackToAnOldFlame View + * + * Shown when a user returns to a product page AFTER the "Sleep on it" reminder timer has expired. + * This indicates they successfully waited the full duration and are now thoughtfully reconsidering. + * + * Scenario: User clicked "Sleep on it", reminder expired (e.g., after 24 hours), and user came back. + * + * Key differences from EarlyReturnFromSleep: + * - Uses "Thoughtful" icon (vs Clock icon) + * - Messaging emphasizes successful waiting and thoughtful decision-making + * - Button options: "Yes, I want it" / "I don't need it" / "I'm still not sure" + */ + import { useState } from "react" import thoughtfulIcon from "url:../assets/icons/Icons/Thoughtful.svg" import Button from "../components/ui/Button" import Card from "../components/ui/Card" import Header from "../components/ui/Header" -import { spacing, textSize } from "../design-system" +import { iconSize, layout, typography } from "../design-system" import { ProductActionManager } from "../managers/ProductActionManager" import { ChromeMessaging } from "../services/ChromeMessaging" import type { Product } from "../storage" @@ -20,18 +34,11 @@ type BackToAnOldFlameProps = { } const titleStyle: React.CSSProperties = { - fontSize: textSize.xl, - fontWeight: "bold", - color: "var(--text-color-light)", - textAlign: "center", - margin: `0 0 ${spacing.md} 0`, - lineHeight: "1.3" + ...typography.title } const actionsStyle: React.CSSProperties = { - display: "flex", - flexDirection: "column", - gap: spacing.md + ...layout.actionsContainer } const BackToAnOldFlame = ({ @@ -132,7 +139,7 @@ const BackToAnOldFlame = ({ thoughtful } /> @@ -142,10 +149,8 @@ const BackToAnOldFlame = ({

Have you made up your mind?

diff --git a/views/Celebration.tsx b/views/Celebration.tsx index 07d3428..a46e86d 100644 --- a/views/Celebration.tsx +++ b/views/Celebration.tsx @@ -3,7 +3,7 @@ import { useEffect } from "react" import Card from "../components/ui/Card" import Header from "../components/ui/Header" import PrivacyBadge from "../components/ui/PrivacyBadge" -import { spacing, textSize } from "../design-system" +import { iconSize, typography } from "../design-system" type CelebrationProps = { icon: string @@ -16,21 +16,12 @@ type CelebrationProps = { } const titleStyle: React.CSSProperties = { - fontSize: textSize.xl, - fontWeight: "bold", - color: "var(--text-color-light)", - textAlign: "center", - margin: `0 0 ${spacing.md} 0`, - lineHeight: "1.3" + ...typography.title } const subtitleStyle: React.CSSProperties = { - fontSize: textSize.md, - color: "var(--text-color-light)", - textAlign: "center", - margin: 0, - opacity: "0.9", - lineHeight: "1.4" + ...typography.subtitle, + margin: 0 } const Celebration = ({ @@ -62,7 +53,7 @@ const Celebration = ({ {iconAlt} } centerIconAlt={iconAlt} diff --git a/views/EarlyReturnFromSleep.tsx b/views/EarlyReturnFromSleep.tsx index d9ae85e..e25397d 100644 --- a/views/EarlyReturnFromSleep.tsx +++ b/views/EarlyReturnFromSleep.tsx @@ -1,10 +1,24 @@ +/** + * EarlyReturnFromSleep View + * + * Shown when a user returns to a product page BEFORE the "Sleep on it" reminder timer has expired. + * This indicates they couldn't resist and came back early, breaking their commitment to wait. + * + * Scenario: User clicked "Sleep on it" (e.g., 24 hours), but came back after only 2 hours. + * + * Key differences from BackToAnOldFlame: + * - Uses "Clock" icon (vs Thoughtful icon) + * - Messaging emphasizes timing and encourages continuing to wait + * - Button options: "I need this now" / "I'll wait" / "I don't need it" + */ + import { useState } from "react" import clockIcon from "url:../assets/icons/Icons/Clock.svg" import Button from "../components/ui/Button" import Card from "../components/ui/Card" import Header from "../components/ui/Header" -import { spacing, textSize } from "../design-system" +import { iconSize, layout, typography } from "../design-system" import { ProductActionManager } from "../managers/ProductActionManager" import { ChromeMessaging } from "../services/ChromeMessaging" import type { Product } from "../storage" @@ -20,27 +34,15 @@ type EarlyReturnFromSleepProps = { } const titleStyle: React.CSSProperties = { - fontSize: textSize.xl, - fontWeight: "bold", - color: "var(--text-color-light)", - textAlign: "center", - margin: `0 0 ${spacing.md} 0`, - lineHeight: "1.3" + ...typography.title } const subtitleStyle: React.CSSProperties = { - fontSize: textSize.md, - color: "var(--text-color-light)", - textAlign: "center", - margin: `0 0 ${spacing.xxl} 0`, - opacity: "0.9", - lineHeight: "1.4" + ...typography.subtitle } const actionsStyle: React.CSSProperties = { - display: "flex", - flexDirection: "column", - gap: spacing.md + ...layout.actionsContainer } const EarlyReturnFromSleep = ({ @@ -143,7 +145,7 @@ const EarlyReturnFromSleep = ({ clock } /> diff --git a/views/IDontNeedIt.tsx b/views/IDontNeedIt.tsx index c341efa..ee2df0e 100644 --- a/views/IDontNeedIt.tsx +++ b/views/IDontNeedIt.tsx @@ -1,3 +1,4 @@ +import React from "react" import lightbulbIcon from "url:../assets/icons/Icons/Lightbulb.svg" import starIcon from "url:../assets/icons/Icons/Star.svg" import trophyIcon from "url:../assets/icons/Icons/Trophy.svg" @@ -6,7 +7,8 @@ import Button from "../components/ui/Button" import Card from "../components/ui/Card" import Header from "../components/ui/Header" import PrivacyBadge from "../components/ui/PrivacyBadge" -import { commonSpacing, spacing, textSize } from "../design-system" +import { commonSpacing, iconSize, layout, typography } from "../design-system" +import { ChromeMessaging } from "../services/ChromeMessaging" import Celebration from "./Celebration" // Feature flag: Set to true to show investment options view @@ -18,31 +20,36 @@ type IDontNeedItProps = { } const titleStyle: React.CSSProperties = { - fontSize: textSize.xl, - fontWeight: "bold", - color: "var(--text-color-light)", - textAlign: "center", - margin: `0 0 ${spacing.md} 0`, - lineHeight: "1.3" + ...typography.title } const questionStyle: React.CSSProperties = { - fontSize: textSize.lg, - color: "var(--text-color-light)", - textAlign: "center", + ...typography.subtitleLarge, margin: `0 0 ${commonSpacing.sectionMargin} 0`, fontWeight: "500" } const optionsContainerStyle: React.CSSProperties = { - display: "flex", - flexDirection: "column", - gap: spacing.md, + ...layout.actionsContainer, width: "100%", marginBottom: commonSpacing.sectionMargin } const IDontNeedIt = ({ onBack, onClose }: IDontNeedItProps) => { + // Handle tab close after celebration + const handleCelebrationClose = async () => { + console.log("[IDontNeedIt] Requesting tab close...") + try { + await ChromeMessaging.closeCurrentTab() + console.log("[IDontNeedIt] Tab close successful") + } catch (error) { + console.error("[IDontNeedIt] Tab close failed:", error) + if (onClose) { + onClose() + } + } + } + // When feature flag is disabled, show celebration instead of investment options if (!SHOW_INVESTMENT_OPTIONS) { return ( @@ -53,7 +60,7 @@ const IDontNeedIt = ({ onBack, onClose }: IDontNeedItProps) => { subtitle="Your future self will thank you for being so thoughtful." autoCloseDelay={4000} onBack={onBack} - onClose={onClose} + onClose={handleCelebrationClose} /> ) } @@ -83,7 +90,7 @@ const IDontNeedIt = ({ onBack, onClose }: IDontNeedItProps) => { star } centerIconAlt="star" @@ -119,7 +126,11 @@ const IDontNeedIt = ({ onBack, onClose }: IDontNeedItProps) => { lightbulb

Did you know? Not saving enough is the #1 regret of diff --git a/views/ProductView.tsx b/views/ProductView.tsx index bd4c307..f860c76 100644 --- a/views/ProductView.tsx +++ b/views/ProductView.tsx @@ -1,4 +1,4 @@ -import { useEffect, useState } from "react" +import { useState } from "react" import clockIcon from "url:../assets/icons/Icons/Clock.svg" import lightbulbIcon from "url:../assets/icons/Icons/Lightbulb.svg" import thoughtfulIcon from "url:../assets/icons/Icons/Thoughtful.svg" @@ -10,15 +10,19 @@ import Card from "../components/ui/Card" import Header from "../components/ui/Header" import PauseMenu, { type PauseDuration } from "../components/ui/PauseMenu" import PrivacyBadge from "../components/ui/PrivacyBadge" -import { spacing, textSize } from "../design-system" +import { + iconSize, + layout, + spacing, + textSize, + typography +} from "../design-system" import { ProductActionManager } from "../managers/ProductActionManager" import type { Product } from "../storage" -import { storage } from "../storage" -import { extractProduct } from "../utils/productExtractor" +import { ProductState, storage } from "../storage" type ProductViewProps = { - productId?: string | null - marketplace: "amazon" + product: Product | null onShowIDontNeedIt: (product: Product | null) => void onShowSleepOnIt: (product: Product | null) => void onShowINeedIt: () => void @@ -35,8 +39,7 @@ const headerStyle: React.CSSProperties = { } const titleStyle: React.CSSProperties = { - fontSize: textSize.xxl, - fontWeight: "bold", + ...typography.titleLarge, margin: "0", display: "flex", alignItems: "center", @@ -45,9 +48,7 @@ const titleStyle: React.CSSProperties = { } const subtitleStyle: React.CSSProperties = { - fontSize: textSize.lg, - color: "var(--text-color-light)", - opacity: "0.8", + ...typography.subtitleLarge, margin: "0", display: "flex", alignItems: "center", @@ -56,65 +57,46 @@ const subtitleStyle: React.CSSProperties = { } const bodyStyle: React.CSSProperties = { - display: "flex", - flexDirection: "column", - gap: spacing.md + ...layout.actionsContainer } const actionsStyle: React.CSSProperties = { - display: "flex", - flexDirection: "column", - gap: spacing.md + ...layout.actionsContainer } const actionsGroupStyle: React.CSSProperties = { - display: "flex", - gap: spacing.md + ...layout.actionsGroup } const ProductView = ({ - productId, - marketplace, + product, onShowIDontNeedIt, onShowSleepOnIt, onShowINeedIt, onClose }: ProductViewProps) => { - const [extractedProduct, setExtractedProduct] = useState(null) const [showPauseMenu, setShowPauseMenu] = useState(false) const [showPrivacyInfo, setShowPrivacyInfo] = useState(false) - useEffect(() => { - if (productId) { - try { - const product = extractProduct(marketplace, productId) - setExtractedProduct(product) - } catch (error) { - console.error("[ProductView] Failed to extract product:", error) - setExtractedProduct(null) - } - } - }, [productId, marketplace]) - const handleSleepOnIt = () => { - onShowSleepOnIt(extractedProduct) + onShowSleepOnIt(product) } const handleIDontNeedIt = async () => { - if (extractedProduct) { + if (product) { try { - await ProductActionManager.dontNeedIt(extractedProduct) + await ProductActionManager.dontNeedIt(product) } catch (error) { console.error("[ProductView] Failed to execute dontNeedIt:", error) } } - onShowIDontNeedIt(extractedProduct) + onShowIDontNeedIt(product) } const handleINeedIt = async () => { - if (extractedProduct) { + if (product) { try { - await ProductActionManager.needIt(extractedProduct) + await ProductActionManager.needIt(product) } catch (error) { console.error("[ProductView] Failed to execute needIt:", error) } @@ -178,6 +160,19 @@ const ProductView = ({ setShowPrivacyInfo(!showPrivacyInfo) } + const handleIntroSentect = (product: Product | null): string => { + console.log("[DEBUG] PRODUCT ---->", product) + if (!product) { + return "" + } + + if (product.state && product.state === ProductState.DONT_NEED_IT) { + return "You are back! Are you thinking about buying this product?" + } + + return "Quick thought before you buy" + } + return ( }>

- Quick thought before you buy + {handleIntroSentect(product)}

@@ -252,6 +247,7 @@ const ProductView = ({ onClick={handleIDontNeedIt}> I don't really need it +