From c486e0463e405f0646f2646d6095895e82386963 Mon Sep 17 00:00:00 2001 From: Chris Bongers Date: Tue, 12 May 2026 16:03:13 +0200 Subject: [PATCH 1/2] feat: clearer opt-out method --- packages/extension/src/frame/controller.ts | 7 +++++ packages/extension/src/frame/render.ts | 27 +++++++++++++++++++ .../post/PostArticlePreviewEmbed.tsx | 16 ++++++++++- .../post/reader/ArticleReaderFrame.tsx | 15 ++++++++++- .../reader/hooks/useLegacyPostLayoutOptOut.ts | 25 +++++++++++------ .../EmbeddedBrowsingWebPrompt.tsx | 18 ++++++++++++- .../src/features/extensionEmbed/common.ts | 1 + .../extensionEmbed/extensionEmbedMessaging.ts | 7 +++++ .../extensionEmbed/useExtensionSiteEmbed.ts | 7 +++++ packages/shared/src/lib/log.ts | 3 +++ 10 files changed, 115 insertions(+), 11 deletions(-) diff --git a/packages/extension/src/frame/controller.ts b/packages/extension/src/frame/controller.ts index ac27034f46..68b205cde8 100644 --- a/packages/extension/src/frame/controller.ts +++ b/packages/extension/src/frame/controller.ts @@ -129,9 +129,16 @@ export const initializeFrame = async ({ } }; + const onOptOut = () => { + sendParentMessage(extensionSiteEmbedFrameEvent.OptOutRequested, { + target: target.href, + }); + }; + renderPermissionPrompt({ root, onRequestPermission, + onOptOut, }); sendParentMessage(extensionSiteEmbedFrameEvent.Error, { diff --git a/packages/extension/src/frame/render.ts b/packages/extension/src/frame/render.ts index 4271b361a8..5851011186 100644 --- a/packages/extension/src/frame/render.ts +++ b/packages/extension/src/frame/render.ts @@ -140,6 +140,18 @@ const ensureStyles = (): void => { transform: translateY(-1px); } .embedded-browsing-button:disabled { opacity: 0.6; cursor: not-allowed; } + .embedded-browsing-button-secondary { + background: transparent; + color: #cfd6e6; + font-weight: 500; + min-width: 0; + padding: 0 0.75rem; + } + .embedded-browsing-button-secondary:hover { + opacity: 1; + color: #ffffff; + transform: none; + } `; document.head.appendChild(style); }; @@ -187,9 +199,11 @@ type PermissionRequestOutcome = 'granted' | 'dismissed' | 'failed'; export const renderPermissionPrompt = ({ root, onRequestPermission, + onOptOut, }: { root: HTMLDivElement; onRequestPermission: () => Promise; + onOptOut?: () => void; }): void => { root.replaceChildren(); const { shell, card } = createPromptShell(); @@ -241,6 +255,19 @@ export const renderPermissionPrompt = ({ }); actions.append(button); + + if (onOptOut) { + const optOutButton = document.createElement('button'); + optOutButton.type = 'button'; + optOutButton.className = + 'embedded-browsing-button embedded-browsing-button-secondary'; + optOutButton.textContent = "I'd rather not read inside daily.dev"; + optOutButton.addEventListener('click', () => { + onOptOut(); + }); + actions.append(optOutButton); + } + card.append(heading, description, status, actions); root.append(shell); }; diff --git a/packages/shared/src/components/post/PostArticlePreviewEmbed.tsx b/packages/shared/src/components/post/PostArticlePreviewEmbed.tsx index b0553a659d..a7194648c8 100644 --- a/packages/shared/src/components/post/PostArticlePreviewEmbed.tsx +++ b/packages/shared/src/components/post/PostArticlePreviewEmbed.tsx @@ -58,6 +58,17 @@ type PostArticlePreviewEmbedProps = { targetHref?: string; onTargetLinkClick?: () => void; targetLinkInNewTab?: boolean; + /** + * Opt-out action shown inside the "install the extension" prompt. When + * provided, the prompt surfaces an "I'd rather not read inside daily.dev" + * button below the Install CTA. + */ + onInstallPromptOptOut?: () => void; + /** + * Opt-out action shown inside the in-iframe permission screen rendered by + * the extension's frame. Bridged from the iframe via `postMessage`. + */ + onPermissionScreenOptOut?: () => void; }; export function PostArticlePreviewEmbed({ @@ -73,6 +84,8 @@ export function PostArticlePreviewEmbed({ targetHref, onTargetLinkClick, targetLinkInNewTab = true, + onInstallPromptOptOut, + onPermissionScreenOptOut, }: PostArticlePreviewEmbedProps): ReactElement | null { const [extensionId, setExtensionId] = useState(() => getBrowserExtensionInstallId(), @@ -565,7 +578,7 @@ export function PostArticlePreviewEmbed({ ) : null} {shouldPromptInstall ? ( - + ) : (
diff --git a/packages/shared/src/components/post/reader/ArticleReaderFrame.tsx b/packages/shared/src/components/post/reader/ArticleReaderFrame.tsx index 0871af02e0..063929fdd1 100644 --- a/packages/shared/src/components/post/reader/ArticleReaderFrame.tsx +++ b/packages/shared/src/components/post/reader/ArticleReaderFrame.tsx @@ -1,10 +1,12 @@ import type { ReactElement, Ref } from 'react'; -import React from 'react'; +import React, { useCallback } from 'react'; import classNames from 'classnames'; import type { Post } from '../../../graphql/posts'; import { PostArticlePreviewEmbed } from '../PostArticlePreviewEmbed'; import { ReaderFallback } from './ReaderFallback'; import { ReaderHeaderActionGroup } from './ReaderHeaderActionButtons'; +import { useLegacyPostLayoutOptOut } from './hooks/useLegacyPostLayoutOptOut'; +import { TargetId } from '../../../lib/log'; type ArticleReaderFrameProps = { post: Post; @@ -35,6 +37,15 @@ export function ArticleReaderFrame({ onTargetLinkClick, targetLinkInNewTab, }: ArticleReaderFrameProps): ReactElement { + const { optOut } = useLegacyPostLayoutOptOut(); + const onInstallPromptOptOut = useCallback( + () => optOut(TargetId.ReaderInstallPrompt), + [optOut], + ); + const onPermissionScreenOptOut = useCallback( + () => optOut(TargetId.ReaderPermissionPrompt), + [optOut], + ); const isFallback = !targetUrl || !isEmbeddable; if (isFallback) { @@ -74,6 +85,8 @@ export function ArticleReaderFrame({ targetHref={targetHref} onTargetLinkClick={onTargetLinkClick} targetLinkInNewTab={targetLinkInNewTab} + onInstallPromptOptOut={onInstallPromptOptOut} + onPermissionScreenOptOut={onPermissionScreenOptOut} />
); diff --git a/packages/shared/src/components/post/reader/hooks/useLegacyPostLayoutOptOut.ts b/packages/shared/src/components/post/reader/hooks/useLegacyPostLayoutOptOut.ts index ea5eb98201..40c681adba 100644 --- a/packages/shared/src/components/post/reader/hooks/useLegacyPostLayoutOptOut.ts +++ b/packages/shared/src/components/post/reader/hooks/useLegacyPostLayoutOptOut.ts @@ -3,22 +3,31 @@ import { useSettingsContext } from '../../../../contexts/SettingsContext'; import { useLogContext } from '../../../../contexts/LogContext'; import { LogEvent, TargetId } from '../../../../lib/log'; +type OptOutSource = + | TargetId.ReaderHeader + | TargetId.ReaderInstallPrompt + | TargetId.ReaderPermissionPrompt; + export function useLegacyPostLayoutOptOut(): { isOptedOut: boolean; - optOut: () => void; + optOut: (source?: OptOutSource) => void; optIn: () => void; } { const { flags, updateFlag } = useSettingsContext(); const { logEvent } = useLogContext(); const isOptedOut = flags?.legacyPostLayoutOptOut ?? false; - const optOut = useCallback(() => { - updateFlag('legacyPostLayoutOptOut', true); - logEvent({ - event_name: LogEvent.ToggleEmbeddedReader, - target_id: TargetId.Off, - }); - }, [logEvent, updateFlag]); + const optOut = useCallback( + (source: OptOutSource = TargetId.ReaderHeader) => { + updateFlag('legacyPostLayoutOptOut', true); + logEvent({ + event_name: LogEvent.ToggleEmbeddedReader, + target_id: TargetId.Off, + extra: JSON.stringify({ source }), + }); + }, + [logEvent, updateFlag], + ); const optIn = useCallback(() => { updateFlag('legacyPostLayoutOptOut', false); diff --git a/packages/shared/src/features/extensionEmbed/EmbeddedBrowsingWebPrompt.tsx b/packages/shared/src/features/extensionEmbed/EmbeddedBrowsingWebPrompt.tsx index 1c0ca93b77..a2821ba1a6 100644 --- a/packages/shared/src/features/extensionEmbed/EmbeddedBrowsingWebPrompt.tsx +++ b/packages/shared/src/features/extensionEmbed/EmbeddedBrowsingWebPrompt.tsx @@ -22,13 +22,19 @@ import { useLogContext } from '../../contexts/LogContext'; import { LogEvent } from '../../lib/log'; import styles from './EmbeddedBrowsingWebPrompt.module.css'; +type EmbeddedBrowsingWebPromptProps = { + onOptOut?: () => void; +}; + /** * Webapp prompt shown when the daily.dev extension isn't installed in this * browser. The "extension installed but permission not granted" case is * handled inside the iframe itself (`packages/extension/src/frame/render.ts`) * so the Enable click carries the user gesture Chrome requires. */ -export function EmbeddedBrowsingWebPrompt(): ReactElement | null { +export function EmbeddedBrowsingWebPrompt({ + onOptOut, +}: EmbeddedBrowsingWebPromptProps = {}): ReactElement | null { const { logEvent } = useLogContext(); // Defensive: callers already gate on `isExtensionCapableBrowser` so this // shouldn't render on unsupported browsers, but bail just in case. @@ -99,6 +105,16 @@ export function EmbeddedBrowsingWebPrompt(): ReactElement | null { > {installButtonLabel} + {onOptOut ? ( + + ) : null} diff --git a/packages/shared/src/features/extensionEmbed/common.ts b/packages/shared/src/features/extensionEmbed/common.ts index 1612bb5881..74b05c7b0b 100644 --- a/packages/shared/src/features/extensionEmbed/common.ts +++ b/packages/shared/src/features/extensionEmbed/common.ts @@ -13,6 +13,7 @@ export const extensionSiteEmbedFrameEvent = { PermissionsReady: 'daily-extension-site-embed-permissions-ready', EmbeddingReady: 'daily-extension-site-embed-embedding-ready', ReloadRequested: 'daily-extension-site-embed-reload-requested', + OptOutRequested: 'daily-extension-site-embed-opt-out-requested', Error: 'daily-extension-site-embed-error', } as const; diff --git a/packages/shared/src/features/extensionEmbed/extensionEmbedMessaging.ts b/packages/shared/src/features/extensionEmbed/extensionEmbedMessaging.ts index 628d261022..cf0df8d24c 100644 --- a/packages/shared/src/features/extensionEmbed/extensionEmbedMessaging.ts +++ b/packages/shared/src/features/extensionEmbed/extensionEmbedMessaging.ts @@ -67,6 +67,7 @@ type HandleExtensionSiteEmbedMessageOptions = { onPermissionsReady: () => void; onEmbeddingReady: () => void; onReloadRequested: () => void; + onOptOutRequested: () => void; onMissingPermission: () => void; onError: (payload: { message: string; reason?: string }) => void; }; @@ -78,6 +79,7 @@ export const handleExtensionSiteEmbedMessage = ({ onPermissionsReady, onEmbeddingReady, onReloadRequested, + onOptOutRequested, onMissingPermission, onError, }: HandleExtensionSiteEmbedMessageOptions): void => { @@ -106,6 +108,11 @@ export const handleExtensionSiteEmbedMessage = ({ return; } + if (message.type === extensionSiteEmbedFrameEvent.OptOutRequested) { + onOptOutRequested(); + return; + } + if (message.type !== extensionSiteEmbedFrameEvent.Error) { return; } diff --git a/packages/shared/src/features/extensionEmbed/useExtensionSiteEmbed.ts b/packages/shared/src/features/extensionEmbed/useExtensionSiteEmbed.ts index 7e36ec230b..1f797eb750 100644 --- a/packages/shared/src/features/extensionEmbed/useExtensionSiteEmbed.ts +++ b/packages/shared/src/features/extensionEmbed/useExtensionSiteEmbed.ts @@ -23,6 +23,7 @@ export interface UseExtensionSiteEmbedOptions { enabled?: boolean; reconnectDelayMs?: number; reconnectAttempts?: number; + onOptOutRequested?: () => void; } export interface UseExtensionSiteEmbedResult { @@ -48,7 +49,10 @@ export const useExtensionSiteEmbed = ({ enabled = true, reconnectDelayMs = extensionSiteEmbedReconnectDelayMs, reconnectAttempts = extensionSiteEmbedReconnectAttempts, + onOptOutRequested, }: UseExtensionSiteEmbedOptions): UseExtensionSiteEmbedResult => { + const onOptOutRequestedRef = useRef(onOptOutRequested); + onOptOutRequestedRef.current = onOptOutRequested; const [frameNonce, setFrameNonce] = useState(0); const [frameMode, setFrameMode] = useState('permission-check'); const [status, setStatus] = useState('idle'); @@ -170,6 +174,9 @@ export const useExtensionSiteEmbed = ({ setErrorReason(null); startReconnectLoop(); }, + onOptOutRequested: () => { + onOptOutRequestedRef.current?.(); + }, onMissingPermission: () => { stopReconnectLoop(); setStatus('permission-required'); diff --git a/packages/shared/src/lib/log.ts b/packages/shared/src/lib/log.ts index 513a93a640..85f167d9b8 100644 --- a/packages/shared/src/lib/log.ts +++ b/packages/shared/src/lib/log.ts @@ -625,6 +625,9 @@ export enum TargetId { GoToFeed = 'go to feed', OptOut = 'opt_out', OptIn = 'opt_in', + ReaderHeader = 'reader header', + ReaderInstallPrompt = 'reader install prompt', + ReaderPermissionPrompt = 'reader permission prompt', IOS = 'ios', Android = 'android', } From 538c2364ef4dfae0af29fe2d914d2c0578c69dce Mon Sep 17 00:00:00 2001 From: Chris Bongers Date: Tue, 12 May 2026 16:10:02 +0200 Subject: [PATCH 2/2] feat: clearer opt-out method --- .../src/components/post/reader/ReaderHeaderActionButtons.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/shared/src/components/post/reader/ReaderHeaderActionButtons.tsx b/packages/shared/src/components/post/reader/ReaderHeaderActionButtons.tsx index c55dc485bc..6ec2c98cc9 100644 --- a/packages/shared/src/components/post/reader/ReaderHeaderActionButtons.tsx +++ b/packages/shared/src/components/post/reader/ReaderHeaderActionButtons.tsx @@ -82,7 +82,7 @@ export function ReaderLegacyLayoutToggleButton({ size={ButtonSize.Small} type="button" className={iconButtonClassName} - onClick={isClassicTarget ? optOut : optIn} + onClick={isClassicTarget ? () => optOut() : optIn} aria-label={ isClassicTarget ? 'Use classic post layout' : 'Use embedded reader' }