From 1678fc641cbaf3676e0b70e26b072e087258a9e1 Mon Sep 17 00:00:00 2001 From: emily Date: Sat, 28 Feb 2026 16:54:56 -0500 Subject: [PATCH 1/2] feat(ui): add countdown timer before tab close Replace instant tab close with 5-second countdown in BackToAnOldFlame, EarlyReturnFromSleep, IDontNeedIt, and SleepOnIt views. Bump @playwright/test to 1.58.2. Made-with: Cursor --- package-lock.json | 48 ++---- package.json | 2 +- research.md | 279 +++++++++++++++++++++++++++++++++ views/BackToAnOldFlame.tsx | 58 ++++--- views/EarlyReturnFromSleep.tsx | 67 ++++---- views/IDontNeedIt.tsx | 56 +++++-- views/SleepOnIt.tsx | 35 +++-- 7 files changed, 437 insertions(+), 108 deletions(-) create mode 100644 research.md diff --git a/package-lock.json b/package-lock.json index b4dd154..361019f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "maurice-plugin", - "version": "0.3.2", + "version": "0.3.3", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "maurice-plugin", - "version": "0.3.2", + "version": "0.3.3", "dependencies": { "plasmo": "^0.90.5", "react": "^18.3.1", @@ -15,7 +15,7 @@ "devDependencies": { "@eslint/js": "^9.39.0", "@ianvs/prettier-plugin-sort-imports": "4.1.1", - "@playwright/test": "^1.58.0", + "@playwright/test": "^1.58.2", "@types/chrome": "0.0.258", "@types/node": "^20.19.30", "@types/react": "^19.1.10", @@ -64,7 +64,6 @@ "node_modules/@babel/core": { "version": "7.28.5", "license": "MIT", - "peer": true, "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.5", @@ -1902,7 +1901,6 @@ "node_modules/@parcel/core": { "version": "2.9.3", "license": "MIT", - "peer": true, "dependencies": { "@mischnic/json-sourcemap": "^0.1.0", "@parcel/cache": "2.9.3", @@ -4724,13 +4722,13 @@ "license": "ISC" }, "node_modules/@playwright/test": { - "version": "1.58.0", - "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.58.0.tgz", - "integrity": "sha512-fWza+Lpbj6SkQKCrU6si4iu+fD2dD3gxNHFhUPxsfXBPhnv3rRSQVd0NtBUT9Z/RhF/boCBcuUaMUSTRTopjZg==", + "version": "1.58.2", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.58.2.tgz", + "integrity": "sha512-akea+6bHYBBfA9uQqSYmlJXn61cTa+jbO87xVLCWbTqbWadRVmhxlXATaOjOgcBaWU4ePo0wB41KMFv3o35IXA==", "dev": true, "license": "Apache-2.0", "dependencies": { - "playwright": "1.58.0" + "playwright": "1.58.2" }, "bin": { "playwright": "cli.js" @@ -4925,7 +4923,6 @@ "node_modules/@svgr/core": { "version": "6.5.1", "license": "MIT", - "peer": true, "dependencies": { "@babel/core": "^7.19.6", "@svgr/babel-preset": "^6.5.1", @@ -5331,7 +5328,6 @@ "integrity": "sha512-WJtwWJu7UdlvzEAUm484QNg5eAoq5QR08KDNx7g45Usrs2NtOPiX8ugDqmKdXkyL03rBqU5dYNYVQetEpBHq2g==", "devOptional": true, "license": "MIT", - "peer": true, "dependencies": { "undici-types": "~6.21.0" } @@ -5401,7 +5397,6 @@ "version": "8.46.2", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.46.2", "@typescript-eslint/types": "8.46.2", @@ -5693,7 +5688,6 @@ "node_modules/acorn": { "version": "8.15.0", "license": "MIT", - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -6066,7 +6060,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "baseline-browser-mapping": "^2.8.19", "caniuse-lite": "^1.0.30001751", @@ -7437,7 +7430,6 @@ "version": "0.18.20", "hasInstallScript": true, "license": "MIT", - "peer": true, "bin": { "esbuild": "bin/esbuild" }, @@ -7491,7 +7483,6 @@ "version": "9.39.0", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -8332,7 +8323,6 @@ "version": "4.7.8", "devOptional": true, "license": "MIT", - "peer": true, "dependencies": { "minimist": "^1.2.5", "neo-async": "^2.6.2", @@ -9185,7 +9175,6 @@ "version": "2.6.1", "dev": true, "license": "MIT", - "peer": true, "bin": { "jiti": "lib/jiti-cli.mjs" } @@ -9749,8 +9738,7 @@ "node_modules/lodash": { "version": "4.17.21", "devOptional": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/lodash.ismatch": { "version": "4.4.0", @@ -10730,13 +10718,13 @@ } }, "node_modules/playwright": { - "version": "1.58.0", - "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.58.0.tgz", - "integrity": "sha512-2SVA0sbPktiIY/MCOPX8e86ehA/e+tDNq+e5Y8qjKYti2Z/JG7xnronT/TXTIkKbYGWlCbuucZ6dziEgkoEjQQ==", + "version": "1.58.2", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.58.2.tgz", + "integrity": "sha512-vA30H8Nvkq/cPBnNw4Q8TWz1EJyqgpuinBcHET0YVJVFldr8JDNiU9LaWAE1KqSkRYazuaBhTpB5ZzShOezQ6A==", "dev": true, "license": "Apache-2.0", "dependencies": { - "playwright-core": "1.58.0" + "playwright-core": "1.58.2" }, "bin": { "playwright": "cli.js" @@ -10749,9 +10737,9 @@ } }, "node_modules/playwright-core": { - "version": "1.58.0", - "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.58.0.tgz", - "integrity": "sha512-aaoB1RWrdNi3//rOeKuMiS65UCcgOVljU46At6eFcOFPFHWtd2weHRRow6z/n+Lec0Lvu0k9ZPKJSjPugikirw==", + "version": "1.58.2", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.58.2.tgz", + "integrity": "sha512-yZkEtftgwS8CsfYo7nm0KE8jsvm6i/PTgVtB8DL726wNf6H2IMsDuxCpJj59KDaxCtSnrWan2AeDqM7JBaultg==", "dev": true, "license": "Apache-2.0", "bin": { @@ -10786,7 +10774,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", @@ -10896,7 +10883,6 @@ "version": "3.6.2", "dev": true, "license": "MIT", - "peer": true, "bin": { "prettier": "bin/prettier.cjs" }, @@ -11007,7 +10993,6 @@ "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", "license": "MIT", - "peer": true, "dependencies": { "loose-envify": "^1.1.0" }, @@ -11020,7 +11005,6 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", "license": "MIT", - "peer": true, "dependencies": { "loose-envify": "^1.1.0", "scheduler": "^0.23.2" @@ -12519,7 +12503,6 @@ "version": "5.9.3", "devOptional": true, "license": "Apache-2.0", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -12664,7 +12647,6 @@ "node_modules/vue": { "version": "3.3.4", "license": "MIT", - "peer": true, "dependencies": { "@vue/compiler-dom": "3.3.4", "@vue/compiler-sfc": "3.3.4", diff --git a/package.json b/package.json index 27caa8b..e13c4a0 100644 --- a/package.json +++ b/package.json @@ -28,7 +28,7 @@ "devDependencies": { "@eslint/js": "^9.39.0", "@ianvs/prettier-plugin-sort-imports": "4.1.1", - "@playwright/test": "^1.58.0", + "@playwright/test": "^1.58.2", "@types/chrome": "0.0.258", "@types/node": "^20.19.30", "@types/react": "^19.1.10", diff --git a/research.md b/research.md new file mode 100644 index 0000000..14bc487 --- /dev/null +++ b/research.md @@ -0,0 +1,279 @@ +# Celebration View Research + +## Overview + +The celebration view is a reusable component that displays success messages to users when they make thoughtful purchasing decisions. It's designed to provide positive reinforcement across multiple user flows in the ThinkTwice plugin. + +## Core Component + +### Celebration.tsx + +Location: `views/Celebration.tsx` + +**Purpose**: A generic, reusable celebration component that displays: + +- A centered icon +- A title message +- An optional subtitle +- Privacy badge footer +- Optional auto-close functionality + +**Props**: + +```typescript +{ + icon: string // Path to icon image + iconAlt?: string // Alt text for icon (default: "celebration") + title: string // Main celebration message + subtitle?: string // Optional secondary message + autoCloseDelay?: number | null // Auto-close timer in ms (null = manual close) + onClose?: () => void // Close callback + onBack?: () => void // Back navigation callback +} +``` + +**Key Features**: + +- Uses `useEffect` hook to implement auto-close timer +- Integrates with Card and Header UI components +- Displays large icon (iconSize.large) centered in header +- Clean, minimal design focused on the celebration message + +## Celebration Types and Reuse + +The base `Celebration` component is reused in 6 different scenarios with different configurations: + +### 1. CelebrateThoughtfulPurchase + +**File**: `views/CelebrateThoughtfulPurchase.tsx` + +- **Icon**: Trophy (`Trophy.svg`) +- **Title**: "Well done! You made a thoughtful choice! 🎉" +- **Subtitle**: "You took the time to think it through." +- **Auto-close**: 2 seconds +- **Trigger**: When user returns after "Sleep on it" timer expires and chooses "Yes, I want it" + +### 2. IDontNeedIt + +**File**: `views/IDontNeedIt.tsx` + +- **Icon**: Trophy (`Trophy.svg`) +- **Title**: "Well done for choosing not to buy! 🎉" +- **Subtitle**: "Your future self will thank you for being so thoughtful." +- **Auto-close**: 4 seconds +- **Special behavior**: Closes browser tab after celebration +- **Trigger**: When user selects "I don't really need it" from main product view + +### 3. INeedIt + +**File**: `views/INeedIt.tsx` + +- **Icon**: Trophy (`Trophy.svg`) +- **Title**: "Trusting your decision is powerful! 🎉" +- **Subtitle**: "Enjoy your purchase, you've been thoughtful about it." +- **Auto-close**: 4 seconds +- **Trigger**: When user selects "I need it" from main product view + +### 4. EarlyReturnFromSleep (Keep Waiting) + +**File**: `views/EarlyReturnFromSleep.tsx` + +- **Icon**: Clock (`Clock.svg`) +- **Title**: "🎉 Great choice! Keep it up!" +- **Subtitle**: "Closing tab..." +- **Auto-close**: Manual (but tab closes after 2s) +- **Trigger**: When user returns before timer expires and chooses "I'll wait" + +### 5. EarlyReturnFromSleep (Don't Need It) + +**File**: `views/EarlyReturnFromSleep.tsx` + +- **Icon**: Clock (`Clock.svg`) +- **Title**: "🎉 Great choice! Keep it up!" +- **Subtitle**: "Closing tab..." +- **Auto-close**: Manual (but tab closes after 2s) +- **Trigger**: When user returns before timer expires and chooses "I don't need it" + +### 6. BackToAnOldFlame (Don't Need It) + +**File**: `views/BackToAnOldFlame.tsx` + +- **Icon**: Thoughtful (`Thoughtful.svg`) +- **Title**: "🎉 Awesome!" +- **Subtitle**: "Closing tab..." +- **Auto-close**: 2 seconds +- **Special behavior**: Closes browser tab after celebration +- **Trigger**: User returns after timer expires and chooses "I don't need it" + +### 7. BackToAnOldFlame (Need It) + +**File**: `views/BackToAnOldFlame.tsx` + +- **Icon**: Thoughtful (`Thoughtful.svg`) +- **Title**: "🎉 Awesome!" +- **Subtitle**: "You made a thoughtful choice! Enjoy your purchase!" +- **Auto-close**: 4 seconds +- **Trigger**: User returns after timer expires and chooses "Yes, I want it" + +## Workflow Architecture + +### Main Orchestrator: amazon.tsx + +Location: `contents/amazon.tsx` + +The content script acts as the main view controller, managing which view to display based on: + +- User interactions (local view state) +- Product state and pending reminders (from `useProductPageState` hook) + +**View Flow**: + +``` +ProductView (initial) + ├─> "I don't need it" → IDontNeedIt (celebration) + ├─> "I need it" → INeedIt (celebration) + └─> "Sleep on it" → SleepOnIt + └─> Creates reminder, closes plugin + +[User returns to product page] + ├─> If BEFORE timer expires → EarlyReturnFromSleep + │ ├─> "I need this now" → INeedIt (celebration) + │ ├─> "I'll wait" → Celebration (Clock icon) + │ └─> "I don't need it" → Celebration (Clock icon) + │ + └─> If AFTER timer expires → BackToAnOldFlame + ├─> "Yes, I want it" → Celebration (Thoughtful icon) + ├─> "I don't need it" → Celebration (Thoughtful icon) + └─> "I'm still not sure" → Closes overlay +``` + +### State Management: useProductPageState + +Location: `hooks/useProductPageState.ts` + +**Responsibilities**: + +- Determines which view to show (`product`, `earlyreturn`, `oldflame`, or `null`) +- Checks product state and pending reminders +- Handles global snooze and plugin close states +- Listens for storage changes and updates views accordingly + +**Key Logic**: + +1. **Early Return Detection**: If `reminderTime > now`, show `EarlyReturnFromSleep` +2. **Old Flame Detection**: If `reminderTime <= now`, show `BackToAnOldFlame` +3. **Terminal State**: If product has `I_NEED_THIS` state, hide overlay entirely + +### View Hierarchy + +``` +amazon.tsx (Content Script) + ├─> useProductPageState (determines reminder-based views) + ├─> Local state (manages user-triggered views) + └─> Renders appropriate view: + ├─> ProductView + ├─> SleepOnIt + ├─> EarlyReturnFromSleep + ├─> BackToAnOldFlame + ├─> IDontNeedIt + ├─> INeedIt + └─> CelebrateThoughtfulPurchase +``` + +## Design Patterns + +### 1. Component Reusability + +The `Celebration` component follows the **Single Responsibility Principle** - it only handles displaying a celebration message. All business logic (state management, storage updates, navigation) is handled by parent components. + +### 2. Composition Over Inheritance + +Rather than creating separate celebration components, the codebase uses composition: + +- Base `Celebration.tsx` provides the UI structure +- Wrapper components (`CelebrateThoughtfulPurchase.tsx`, etc.) provide specific configurations +- Parent components handle the workflow logic + +### 3. State-Driven Views + +The view system uses a combination of: + +- **Global state** (from `useProductPageState`) - determines reminder-based views +- **Local state** (in `amazon.tsx`) - tracks user-triggered navigation +- **Priority**: Reminder views take precedence over local views + +### 4. Auto-Close Strategy + +Different celebrations use different auto-close timings: + +- **2 seconds**: Quick feedback for actions that close tabs +- **4 seconds**: Longer reinforcement for major decisions +- **Manual close**: When tab will be programmatically closed + +## Icon Usage + +Available celebration icons in `assets/icons/Icons/`: + +- **Trophy.svg**: General achievement, positive reinforcement (most common) +- **Clock.svg**: Time-related achievements (early return scenarios) +- **Thoughtful.svg**: Thoughtful decision-making (completed waiting period) +- **Star.svg**: Not currently used in celebrations +- **Confetti.svg**: Not currently used in celebrations + +## Technical Implementation Details + +### Auto-Close Mechanism + +```typescript +useEffect(() => { + if (autoCloseDelay !== null && autoCloseDelay > 0 && onClose) { + const timer = setTimeout(() => { + console.log("[Celebration] Auto-closing after delay...") + onClose() + }, autoCloseDelay) + + return () => clearTimeout(timer) + } +}, [autoCloseDelay, onClose]) +``` + +### Tab Closing Pattern + +Several celebrations close the browser tab after display: + +1. Show celebration component +2. Wait for auto-close delay (or fixed timeout) +3. Call `ChromeMessaging.closeCurrentTab()` +4. Fallback to `onClose()` if tab close fails + +Examples: + +- `IDontNeedIt`: 4s delay before tab close +- `EarlyReturnFromSleep`: 2s delay before tab close +- `BackToAnOldFlame`: 2s delay before tab close + +## Key Findings + +1. **Single Component, Multiple Uses**: The `Celebration` component is successfully reused across 6-7 different scenarios with just prop configuration changes. + +2. **Consistent User Experience**: All celebrations follow the same visual pattern (centered icon, title, subtitle) creating a cohesive experience. + +3. **Flexible Configuration**: The component's props allow for both automated (auto-close) and manual (user-triggered) closing behaviors. + +4. **Clear Separation of Concerns**: Business logic lives in parent components/views, while `Celebration` only handles presentation. + +5. **Context-Aware Messaging**: Each celebration type uses appropriate icons and messages that match the user's action and decision context. + +6. **Progressive Disclosure**: The workflow gradually guides users from initial decision to final action, with celebrations reinforcing positive behaviors at each step. + +## Recommendations for Future Development + +1. **Icon Variety**: Consider using the currently unused icons (Star.svg, Confetti.svg) for different achievement levels or milestones. + +2. **Animation Support**: Add optional celebration animations (confetti, sparkles) for major achievements. + +3. **Customization**: Add theming support to allow different visual styles while maintaining the same component structure. + +4. **Analytics Integration**: Track which celebration types are most effective at reinforcing desired behaviors. + +5. **A/B Testing**: Test different auto-close delays to optimize user experience and message retention. diff --git a/views/BackToAnOldFlame.tsx b/views/BackToAnOldFlame.tsx index 5d499d0..cee1deb 100644 --- a/views/BackToAnOldFlame.tsx +++ b/views/BackToAnOldFlame.tsx @@ -12,7 +12,7 @@ * - Button options: "Yes, I want it" / "I don't need it" / "I'm still not sure" */ -import { useState } from "react" +import { useEffect, useState } from "react" import thoughtfulIcon from "url:../assets/icons/Icons/Thoughtful.svg" import Button from "../components/ui/Button" @@ -51,10 +51,41 @@ const BackToAnOldFlame = ({ "dontNeed" | "needIt" | null >(null) const [processing, setProcessing] = useState(false) + const [countdown, setCountdown] = useState(null) // Calculate actual time waited const timeWaitedFormatted = formatDuration(Date.now() - reminderStartTime) + // Handle countdown and tab close + useEffect(() => { + if (countdown === null) return + + if (countdown === 0) { + // Close the tab when countdown reaches 0 + const closeTab = async () => { + console.log("[BackToAnOldFlame] Requesting tab close...") + try { + await ChromeMessaging.closeCurrentTab() + console.log("[BackToAnOldFlame] Tab close request successful") + } catch (error) { + console.error("[BackToAnOldFlame] Tab close failed:", error) + if (onClose) { + onClose() + } + } + } + closeTab() + return + } + + // Decrement countdown every second + const timer = setTimeout(() => { + setCountdown(countdown - 1) + }, 1000) + + return () => clearTimeout(timer) + }, [countdown, onClose]) + const handleDontNeedIt = async () => { setProcessing(true) @@ -69,6 +100,7 @@ const BackToAnOldFlame = ({ } setCelebrationType("dontNeed") + setCountdown(5) // Start 5-second countdown } const handleINeedIt = async () => { @@ -91,30 +123,18 @@ const BackToAnOldFlame = ({ setCelebrationType("needIt") } - const handleCelebrationClose = async () => { - console.log("[BackToAnOldFlame] Requesting tab close after celebration...") - - try { - await ChromeMessaging.closeCurrentTab() - console.log("[BackToAnOldFlame] Tab close request successful") - } catch (error) { - console.error("[BackToAnOldFlame] Tab close failed:", error) - // Fallback: hide the overlay - if (onClose) { - onClose() - } - } - } - if (celebrationType === "dontNeed") { return ( 0 + ? `Closing tab in ${countdown}` + : "Closing tab..." + } + onClose={onClose} /> ) } diff --git a/views/EarlyReturnFromSleep.tsx b/views/EarlyReturnFromSleep.tsx index e25397d..8acf5d7 100644 --- a/views/EarlyReturnFromSleep.tsx +++ b/views/EarlyReturnFromSleep.tsx @@ -12,7 +12,7 @@ * - Button options: "I need this now" / "I'll wait" / "I don't need it" */ -import { useState } from "react" +import { useEffect, useState } from "react" import clockIcon from "url:../assets/icons/Icons/Clock.svg" import Button from "../components/ui/Button" @@ -54,29 +54,45 @@ const EarlyReturnFromSleep = ({ }: EarlyReturnFromSleepProps) => { const [showCelebration, setShowCelebration] = useState(false) const [processing, setProcessing] = useState(false) + const [countdown, setCountdown] = useState(null) // Calculate actual time waited const timeWaitedFormatted = formatDuration(Date.now() - reminderStartTime) + // Handle countdown and tab close + useEffect(() => { + if (countdown === null) return + + if (countdown === 0) { + // Close the tab when countdown reaches 0 + const closeTab = async () => { + console.log("[EarlyReturnFromSleep] Requesting tab close...") + try { + await ChromeMessaging.closeCurrentTab() + console.log("[EarlyReturnFromSleep] Tab close request successful") + } catch (error) { + console.error("[EarlyReturnFromSleep] Tab close failed:", error) + if (onClose) { + onClose() + } + } + } + closeTab() + return + } + + // Decrement countdown every second + const timer = setTimeout(() => { + setCountdown(countdown - 1) + }, 1000) + + return () => clearTimeout(timer) + }, [countdown, onClose]) + const handleKeepWaiting = async () => { setProcessing(true) setShowCelebration(true) - - // Wait 2 seconds to show celebration message, then close tab - setTimeout(async () => { - console.log("[EarlyReturnFromSleep] Requesting tab close...") - - try { - await ChromeMessaging.closeCurrentTab() - console.log("[EarlyReturnFromSleep] Tab close request successful") - } catch (error) { - console.error("[EarlyReturnFromSleep] Tab close failed:", error) - // Fallback: hide the overlay - if (onClose) { - onClose() - } - } - }, 2000) + setCountdown(5) // Start 5-second countdown } const handleINeedIt = async () => { @@ -113,16 +129,7 @@ const EarlyReturnFromSleep = ({ } } setShowCelebration(true) - - // Wait 2 seconds to show celebration message, then close tab - setTimeout(async () => { - try { - await ChromeMessaging.closeCurrentTab() - } catch (error) { - console.error("[EarlyReturnFromSleep] Tab close failed:", error) - if (onClose) onClose() - } - }, 2000) + setCountdown(5) // Start 5-second countdown } if (showCelebration) { @@ -131,7 +138,11 @@ const EarlyReturnFromSleep = ({ icon={clockIcon} iconAlt="clock" title="🎉 Great choice! Keep it up!" - subtitle="Closing tab..." + subtitle={ + countdown !== null && countdown > 0 + ? `Closing tab in ${countdown}` + : "Closing tab..." + } onClose={onClose} /> ) diff --git a/views/IDontNeedIt.tsx b/views/IDontNeedIt.tsx index ee2df0e..b9388b4 100644 --- a/views/IDontNeedIt.tsx +++ b/views/IDontNeedIt.tsx @@ -36,19 +36,44 @@ const optionsContainerStyle: React.CSSProperties = { } 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() + const [countdown, setCountdown] = React.useState(null) + + // Handle countdown and tab close + React.useEffect(() => { + if (countdown === null && !SHOW_INVESTMENT_OPTIONS) { + // Start countdown after component mounts + setCountdown(5) + } + }, []) + + React.useEffect(() => { + if (countdown === null) return + + if (countdown === 0) { + // Close the tab when countdown reaches 0 + const closeTab = 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() + } + } } + closeTab() + return } - } + + // Decrement countdown every second + const timer = setTimeout(() => { + setCountdown(countdown - 1) + }, 1000) + + return () => clearTimeout(timer) + }, [countdown, onClose]) // When feature flag is disabled, show celebration instead of investment options if (!SHOW_INVESTMENT_OPTIONS) { @@ -57,10 +82,13 @@ const IDontNeedIt = ({ onBack, onClose }: IDontNeedItProps) => { icon={trophyIcon} iconAlt="trophy" title="Well done for choosing not to buy! 🎉" - subtitle="Your future self will thank you for being so thoughtful." - autoCloseDelay={4000} + subtitle={ + countdown !== null + ? `Closing tab in ${countdown}` + : "Your future self will thank you for being so thoughtful." + } onBack={onBack} - onClose={handleCelebrationClose} + onClose={onClose} /> ) } diff --git a/views/SleepOnIt.tsx b/views/SleepOnIt.tsx index e40da11..5f9e6e4 100644 --- a/views/SleepOnIt.tsx +++ b/views/SleepOnIt.tsx @@ -50,6 +50,7 @@ const SleepOnIt = ({ ) // Default: 24 hours const [saved, setSaved] = useState(false) const [saving, setSaving] = useState(false) + const [countdown, setCountdown] = useState(null) const durationOptions = [ { label: "1 minute", value: 1 * 60 * 1000 }, @@ -97,6 +98,7 @@ const SleepOnIt = ({ console.log("[SleepOnIt] Reminder saved successfully") setSaved(true) + setCountdown(5) // Start countdown when saved } catch (error) { console.error("[SleepOnIt] Failed to save reminder:", error) alert("Failed to save reminder. Please check the console for details.") @@ -105,30 +107,35 @@ const SleepOnIt = ({ } } - // Auto-close tab after 4 seconds when reminder is saved + // Handle countdown and tab close useEffect(() => { - if (saved) { - console.log( - "[SleepOnIt] Reminder saved, scheduling tab close in 4 seconds..." - ) - const timer = setTimeout(async () => { - console.log("[SleepOnIt] Requesting tab close...") + if (countdown === null) return + if (countdown === 0) { + // Close the tab when countdown reaches 0 + const closeTab = async () => { + console.log("[SleepOnIt] Requesting tab close...") try { await ChromeMessaging.closeCurrentTab() console.log("[SleepOnIt] Tab close request successful") } catch (error) { console.error("[SleepOnIt] Tab close failed:", error) - // Fallback: hide the overlay if (onClose) { onClose() } } - }, 4000) // 4 seconds - - return () => clearTimeout(timer) + } + closeTab() + return } - }, [saved, onClose]) + + // Decrement countdown every second + const timer = setTimeout(() => { + setCountdown(countdown - 1) + }, 1000) + + return () => clearTimeout(timer) + }, [countdown, onClose]) return ( }> @@ -176,7 +183,9 @@ const SleepOnIt = ({ ) : (

- ✓ Reminder saved! Hold tight and remember about the goal! + {countdown !== null && countdown > 0 + ? `Closing tab in ${countdown}` + : "✓ Reminder saved!"}

)}
From f2ee1a14583216cf4465fc63e32a80d2f76025e2 Mon Sep 17 00:00:00 2001 From: Francesco Vertemati <63065831+vertefra@users.noreply.github.com> Date: Sat, 28 Feb 2026 17:15:41 -0500 Subject: [PATCH 2/2] Delete research.md --- research.md | 279 ---------------------------------------------------- 1 file changed, 279 deletions(-) delete mode 100644 research.md diff --git a/research.md b/research.md deleted file mode 100644 index 14bc487..0000000 --- a/research.md +++ /dev/null @@ -1,279 +0,0 @@ -# Celebration View Research - -## Overview - -The celebration view is a reusable component that displays success messages to users when they make thoughtful purchasing decisions. It's designed to provide positive reinforcement across multiple user flows in the ThinkTwice plugin. - -## Core Component - -### Celebration.tsx - -Location: `views/Celebration.tsx` - -**Purpose**: A generic, reusable celebration component that displays: - -- A centered icon -- A title message -- An optional subtitle -- Privacy badge footer -- Optional auto-close functionality - -**Props**: - -```typescript -{ - icon: string // Path to icon image - iconAlt?: string // Alt text for icon (default: "celebration") - title: string // Main celebration message - subtitle?: string // Optional secondary message - autoCloseDelay?: number | null // Auto-close timer in ms (null = manual close) - onClose?: () => void // Close callback - onBack?: () => void // Back navigation callback -} -``` - -**Key Features**: - -- Uses `useEffect` hook to implement auto-close timer -- Integrates with Card and Header UI components -- Displays large icon (iconSize.large) centered in header -- Clean, minimal design focused on the celebration message - -## Celebration Types and Reuse - -The base `Celebration` component is reused in 6 different scenarios with different configurations: - -### 1. CelebrateThoughtfulPurchase - -**File**: `views/CelebrateThoughtfulPurchase.tsx` - -- **Icon**: Trophy (`Trophy.svg`) -- **Title**: "Well done! You made a thoughtful choice! 🎉" -- **Subtitle**: "You took the time to think it through." -- **Auto-close**: 2 seconds -- **Trigger**: When user returns after "Sleep on it" timer expires and chooses "Yes, I want it" - -### 2. IDontNeedIt - -**File**: `views/IDontNeedIt.tsx` - -- **Icon**: Trophy (`Trophy.svg`) -- **Title**: "Well done for choosing not to buy! 🎉" -- **Subtitle**: "Your future self will thank you for being so thoughtful." -- **Auto-close**: 4 seconds -- **Special behavior**: Closes browser tab after celebration -- **Trigger**: When user selects "I don't really need it" from main product view - -### 3. INeedIt - -**File**: `views/INeedIt.tsx` - -- **Icon**: Trophy (`Trophy.svg`) -- **Title**: "Trusting your decision is powerful! 🎉" -- **Subtitle**: "Enjoy your purchase, you've been thoughtful about it." -- **Auto-close**: 4 seconds -- **Trigger**: When user selects "I need it" from main product view - -### 4. EarlyReturnFromSleep (Keep Waiting) - -**File**: `views/EarlyReturnFromSleep.tsx` - -- **Icon**: Clock (`Clock.svg`) -- **Title**: "🎉 Great choice! Keep it up!" -- **Subtitle**: "Closing tab..." -- **Auto-close**: Manual (but tab closes after 2s) -- **Trigger**: When user returns before timer expires and chooses "I'll wait" - -### 5. EarlyReturnFromSleep (Don't Need It) - -**File**: `views/EarlyReturnFromSleep.tsx` - -- **Icon**: Clock (`Clock.svg`) -- **Title**: "🎉 Great choice! Keep it up!" -- **Subtitle**: "Closing tab..." -- **Auto-close**: Manual (but tab closes after 2s) -- **Trigger**: When user returns before timer expires and chooses "I don't need it" - -### 6. BackToAnOldFlame (Don't Need It) - -**File**: `views/BackToAnOldFlame.tsx` - -- **Icon**: Thoughtful (`Thoughtful.svg`) -- **Title**: "🎉 Awesome!" -- **Subtitle**: "Closing tab..." -- **Auto-close**: 2 seconds -- **Special behavior**: Closes browser tab after celebration -- **Trigger**: User returns after timer expires and chooses "I don't need it" - -### 7. BackToAnOldFlame (Need It) - -**File**: `views/BackToAnOldFlame.tsx` - -- **Icon**: Thoughtful (`Thoughtful.svg`) -- **Title**: "🎉 Awesome!" -- **Subtitle**: "You made a thoughtful choice! Enjoy your purchase!" -- **Auto-close**: 4 seconds -- **Trigger**: User returns after timer expires and chooses "Yes, I want it" - -## Workflow Architecture - -### Main Orchestrator: amazon.tsx - -Location: `contents/amazon.tsx` - -The content script acts as the main view controller, managing which view to display based on: - -- User interactions (local view state) -- Product state and pending reminders (from `useProductPageState` hook) - -**View Flow**: - -``` -ProductView (initial) - ├─> "I don't need it" → IDontNeedIt (celebration) - ├─> "I need it" → INeedIt (celebration) - └─> "Sleep on it" → SleepOnIt - └─> Creates reminder, closes plugin - -[User returns to product page] - ├─> If BEFORE timer expires → EarlyReturnFromSleep - │ ├─> "I need this now" → INeedIt (celebration) - │ ├─> "I'll wait" → Celebration (Clock icon) - │ └─> "I don't need it" → Celebration (Clock icon) - │ - └─> If AFTER timer expires → BackToAnOldFlame - ├─> "Yes, I want it" → Celebration (Thoughtful icon) - ├─> "I don't need it" → Celebration (Thoughtful icon) - └─> "I'm still not sure" → Closes overlay -``` - -### State Management: useProductPageState - -Location: `hooks/useProductPageState.ts` - -**Responsibilities**: - -- Determines which view to show (`product`, `earlyreturn`, `oldflame`, or `null`) -- Checks product state and pending reminders -- Handles global snooze and plugin close states -- Listens for storage changes and updates views accordingly - -**Key Logic**: - -1. **Early Return Detection**: If `reminderTime > now`, show `EarlyReturnFromSleep` -2. **Old Flame Detection**: If `reminderTime <= now`, show `BackToAnOldFlame` -3. **Terminal State**: If product has `I_NEED_THIS` state, hide overlay entirely - -### View Hierarchy - -``` -amazon.tsx (Content Script) - ├─> useProductPageState (determines reminder-based views) - ├─> Local state (manages user-triggered views) - └─> Renders appropriate view: - ├─> ProductView - ├─> SleepOnIt - ├─> EarlyReturnFromSleep - ├─> BackToAnOldFlame - ├─> IDontNeedIt - ├─> INeedIt - └─> CelebrateThoughtfulPurchase -``` - -## Design Patterns - -### 1. Component Reusability - -The `Celebration` component follows the **Single Responsibility Principle** - it only handles displaying a celebration message. All business logic (state management, storage updates, navigation) is handled by parent components. - -### 2. Composition Over Inheritance - -Rather than creating separate celebration components, the codebase uses composition: - -- Base `Celebration.tsx` provides the UI structure -- Wrapper components (`CelebrateThoughtfulPurchase.tsx`, etc.) provide specific configurations -- Parent components handle the workflow logic - -### 3. State-Driven Views - -The view system uses a combination of: - -- **Global state** (from `useProductPageState`) - determines reminder-based views -- **Local state** (in `amazon.tsx`) - tracks user-triggered navigation -- **Priority**: Reminder views take precedence over local views - -### 4. Auto-Close Strategy - -Different celebrations use different auto-close timings: - -- **2 seconds**: Quick feedback for actions that close tabs -- **4 seconds**: Longer reinforcement for major decisions -- **Manual close**: When tab will be programmatically closed - -## Icon Usage - -Available celebration icons in `assets/icons/Icons/`: - -- **Trophy.svg**: General achievement, positive reinforcement (most common) -- **Clock.svg**: Time-related achievements (early return scenarios) -- **Thoughtful.svg**: Thoughtful decision-making (completed waiting period) -- **Star.svg**: Not currently used in celebrations -- **Confetti.svg**: Not currently used in celebrations - -## Technical Implementation Details - -### Auto-Close Mechanism - -```typescript -useEffect(() => { - if (autoCloseDelay !== null && autoCloseDelay > 0 && onClose) { - const timer = setTimeout(() => { - console.log("[Celebration] Auto-closing after delay...") - onClose() - }, autoCloseDelay) - - return () => clearTimeout(timer) - } -}, [autoCloseDelay, onClose]) -``` - -### Tab Closing Pattern - -Several celebrations close the browser tab after display: - -1. Show celebration component -2. Wait for auto-close delay (or fixed timeout) -3. Call `ChromeMessaging.closeCurrentTab()` -4. Fallback to `onClose()` if tab close fails - -Examples: - -- `IDontNeedIt`: 4s delay before tab close -- `EarlyReturnFromSleep`: 2s delay before tab close -- `BackToAnOldFlame`: 2s delay before tab close - -## Key Findings - -1. **Single Component, Multiple Uses**: The `Celebration` component is successfully reused across 6-7 different scenarios with just prop configuration changes. - -2. **Consistent User Experience**: All celebrations follow the same visual pattern (centered icon, title, subtitle) creating a cohesive experience. - -3. **Flexible Configuration**: The component's props allow for both automated (auto-close) and manual (user-triggered) closing behaviors. - -4. **Clear Separation of Concerns**: Business logic lives in parent components/views, while `Celebration` only handles presentation. - -5. **Context-Aware Messaging**: Each celebration type uses appropriate icons and messages that match the user's action and decision context. - -6. **Progressive Disclosure**: The workflow gradually guides users from initial decision to final action, with celebrations reinforcing positive behaviors at each step. - -## Recommendations for Future Development - -1. **Icon Variety**: Consider using the currently unused icons (Star.svg, Confetti.svg) for different achievement levels or milestones. - -2. **Animation Support**: Add optional celebration animations (confetti, sparkles) for major achievements. - -3. **Customization**: Add theming support to allow different visual styles while maintaining the same component structure. - -4. **Analytics Integration**: Track which celebration types are most effective at reinforcing desired behaviors. - -5. **A/B Testing**: Test different auto-close delays to optimize user experience and message retention.