diff --git a/apps/browser-extension/src/entrypoints/content.ts b/apps/browser-extension/src/entrypoints/content.ts index 78db4732a..49730d940 100644 --- a/apps/browser-extension/src/entrypoints/content.ts +++ b/apps/browser-extension/src/entrypoints/content.ts @@ -9,8 +9,7 @@ import { BoolResponse as messageBoolResponse } from '@/utils/types/messaging/Boo import { t } from '@/i18n/StandaloneI18n'; -import { defineContentScript } from '#imports'; -import { createShadowRootUi } from '#imports'; +import { defineContentScript, createShadowRootUi } from '#imports'; export default defineContentScript({ matches: [''], diff --git a/apps/browser-extension/src/entrypoints/popup/components/ClipboardCountdownBar.tsx b/apps/browser-extension/src/entrypoints/popup/components/ClipboardCountdownBar.tsx new file mode 100644 index 000000000..4f0934998 --- /dev/null +++ b/apps/browser-extension/src/entrypoints/popup/components/ClipboardCountdownBar.tsx @@ -0,0 +1,108 @@ +import React, { useEffect, useState, useRef } from 'react'; +import { onMessage, sendMessage } from 'webext-bridge/popup'; + +/** + * Clipboard countdown bar component. + */ +export const ClipboardCountdownBar: React.FC = () => { + const [isVisible, setIsVisible] = useState(false); + const animationRef = useRef(null); + const currentCountdownIdRef = useRef(0); + + /** + * Starts the countdown animation. + */ + const startAnimation = (remaining: number, total: number) : void => { + // Use a small delay to ensure the component is fully rendered + setTimeout(() => { + if (animationRef.current) { + // Calculate the starting percentage based on remaining time + const percentage = (remaining / total) * 100; + + // Reset any existing animation + animationRef.current.style.transition = 'none'; + animationRef.current.style.width = `${percentage}%`; + + // Force browser to flush styles + void animationRef.current.offsetHeight; + + // Start animation from current position to 0 + requestAnimationFrame(() => { + if (animationRef.current) { + animationRef.current.style.transition = `width ${remaining}s linear`; + animationRef.current.style.width = '0%'; + } + }); + } + }, 10); + }; + + useEffect(() => { + // Request current countdown state on mount + sendMessage('GET_CLIPBOARD_COUNTDOWN_STATE', {}, 'background').then((state) => { + const countdownState = state as { remaining: number; total: number; id: number } | null; + if (countdownState && countdownState.remaining > 0) { + currentCountdownIdRef.current = countdownState.id; + setIsVisible(true); + startAnimation(countdownState.remaining, countdownState.total); + } + }).catch(() => { + // No active countdown + }); + // Listen for countdown updates from background script + const unsubscribe = onMessage('CLIPBOARD_COUNTDOWN', ({ data }) => { + const { remaining, total, id } = data as { remaining: number; total: number; id: number }; + setIsVisible(remaining > 0); + + // Check if this is a new countdown (different ID) + const isNewCountdown = id !== currentCountdownIdRef.current; + + // Start animation when new countdown begins + if (isNewCountdown && remaining > 0) { + currentCountdownIdRef.current = id; + startAnimation(remaining, total); + } + }); + + // Listen for clipboard cleared message + const unsubscribeClear = onMessage('CLIPBOARD_CLEARED', () => { + setIsVisible(false); + currentCountdownIdRef.current = 0; + if (animationRef.current) { + animationRef.current.style.transition = 'none'; + animationRef.current.style.width = '0%'; + } + }); + + // Listen for countdown cancelled message + const unsubscribeCancel = onMessage('CLIPBOARD_COUNTDOWN_CANCELLED', () => { + setIsVisible(false); + currentCountdownIdRef.current = 0; + if (animationRef.current) { + animationRef.current.style.transition = 'none'; + animationRef.current.style.width = '0%'; + } + }); + + return () : void => { + // Clean up listeners + unsubscribe(); + unsubscribeClear(); + unsubscribeCancel(); + }; + }, []); + + if (!isVisible) { + return null; + } + + return ( +
+
+
+ ); +}; diff --git a/apps/browser-extension/src/entrypoints/popup/components/CredentialDetails/TotpBlock.tsx b/apps/browser-extension/src/entrypoints/popup/components/CredentialDetails/TotpBlock.tsx index 8261aae99..6705ddb71 100644 --- a/apps/browser-extension/src/entrypoints/popup/components/CredentialDetails/TotpBlock.tsx +++ b/apps/browser-extension/src/entrypoints/popup/components/CredentialDetails/TotpBlock.tsx @@ -1,6 +1,7 @@ import * as OTPAuth from 'otpauth'; import React, { useState, useEffect } from 'react'; import { useTranslation } from 'react-i18next'; +import { sendMessage } from 'webext-bridge/popup'; import { useDb } from '@/entrypoints/popup/context/DbContext'; @@ -68,6 +69,9 @@ const TotpBlock: React.FC = ({ credentialId }) => { try { await navigator.clipboard.writeText(code); setCopiedId(id); + + // Notify background script that clipboard was copied + await sendMessage('CLIPBOARD_COPIED', { value: code }, 'background'); // Reset copied state after 2 seconds setTimeout(() => { diff --git a/apps/browser-extension/src/entrypoints/popup/components/FormInputCopyToClipboard.tsx b/apps/browser-extension/src/entrypoints/popup/components/FormInputCopyToClipboard.tsx index cada2cd24..0c57aee98 100644 --- a/apps/browser-extension/src/entrypoints/popup/components/FormInputCopyToClipboard.tsx +++ b/apps/browser-extension/src/entrypoints/popup/components/FormInputCopyToClipboard.tsx @@ -1,5 +1,6 @@ import React, { useState, useEffect } from 'react'; import { useTranslation } from 'react-i18next'; +import { sendMessage } from 'webext-bridge/popup'; import { ClipboardCopyService } from '@/entrypoints/popup/utils/ClipboardCopyService'; @@ -81,6 +82,9 @@ export const FormInputCopyToClipboard: React.FC = try { await navigator.clipboard.writeText(value); clipboardService.setCopied(id); + + // Notify background script that clipboard was copied + await sendMessage('CLIPBOARD_COPIED', { value }, 'background'); // Reset copied state after 2 seconds setTimeout(() => {