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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions packages/extension/src/frame/controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -129,9 +129,16 @@ export const initializeFrame = async ({
}
};

const onOptOut = () => {
sendParentMessage(extensionSiteEmbedFrameEvent.OptOutRequested, {
target: target.href,
});
};

renderPermissionPrompt({
root,
onRequestPermission,
onOptOut,
});

sendParentMessage(extensionSiteEmbedFrameEvent.Error, {
Expand Down
27 changes: 27 additions & 0 deletions packages/extension/src/frame/render.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
};
Expand Down Expand Up @@ -187,9 +199,11 @@ type PermissionRequestOutcome = 'granted' | 'dismissed' | 'failed';
export const renderPermissionPrompt = ({
root,
onRequestPermission,
onOptOut,
}: {
root: HTMLDivElement;
onRequestPermission: () => Promise<PermissionRequestOutcome>;
onOptOut?: () => void;
}): void => {
root.replaceChildren();
const { shell, card } = createPromptShell();
Expand Down Expand Up @@ -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);
};
16 changes: 15 additions & 1 deletion packages/shared/src/components/post/PostArticlePreviewEmbed.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand All @@ -73,6 +84,8 @@ export function PostArticlePreviewEmbed({
targetHref,
onTargetLinkClick,
targetLinkInNewTab = true,
onInstallPromptOptOut,
onPermissionScreenOptOut,
}: PostArticlePreviewEmbedProps): ReactElement | null {
const [extensionId, setExtensionId] = useState(() =>
getBrowserExtensionInstallId(),
Expand Down Expand Up @@ -565,7 +578,7 @@ export function PostArticlePreviewEmbed({
) : null}
</div>
{shouldPromptInstall ? (
<EmbeddedBrowsingWebPrompt />
<EmbeddedBrowsingWebPrompt onOptOut={onInstallPromptOptOut} />
) : (
<div className="relative flex min-h-0 flex-1 flex-col">
<ExtensionSiteEmbed
Expand All @@ -578,6 +591,7 @@ export function PostArticlePreviewEmbed({
targetFrameTitle="Article preview"
onTargetDomReady={onTargetDomReady}
onTargetFrameLoad={onTargetFrameLoad}
onOptOutRequested={onPermissionScreenOptOut}
onStateChange={onEmbedStateChange}
renderState={renderEmbedChrome}
/>
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -74,6 +85,8 @@ export function ArticleReaderFrame({
targetHref={targetHref}
onTargetLinkClick={onTargetLinkClick}
targetLinkInNewTab={targetLinkInNewTab}
onInstallPromptOptOut={onInstallPromptOptOut}
onPermissionScreenOptOut={onPermissionScreenOptOut}
/>
</div>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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'
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -99,6 +105,16 @@ export function EmbeddedBrowsingWebPrompt(): ReactElement | null {
>
{installButtonLabel}
</Button>
{onOptOut ? (
<Button
type="button"
variant={ButtonVariant.Tertiary}
size={ButtonSize.Small}
onClick={onOptOut}
>
I&apos;d rather not read inside daily.dev
</Button>
) : null}
</div>
</div>
</div>
Expand Down
1 change: 1 addition & 0 deletions packages/shared/src/features/extensionEmbed/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ type HandleExtensionSiteEmbedMessageOptions = {
onPermissionsReady: () => void;
onEmbeddingReady: () => void;
onReloadRequested: () => void;
onOptOutRequested: () => void;
onMissingPermission: () => void;
onError: (payload: { message: string; reason?: string }) => void;
};
Expand All @@ -78,6 +79,7 @@ export const handleExtensionSiteEmbedMessage = ({
onPermissionsReady,
onEmbeddingReady,
onReloadRequested,
onOptOutRequested,
onMissingPermission,
onError,
}: HandleExtensionSiteEmbedMessageOptions): void => {
Expand Down Expand Up @@ -106,6 +108,11 @@ export const handleExtensionSiteEmbedMessage = ({
return;
}

if (message.type === extensionSiteEmbedFrameEvent.OptOutRequested) {
onOptOutRequested();
return;
}

if (message.type !== extensionSiteEmbedFrameEvent.Error) {
return;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ export interface UseExtensionSiteEmbedOptions {
enabled?: boolean;
reconnectDelayMs?: number;
reconnectAttempts?: number;
onOptOutRequested?: () => void;
}

export interface UseExtensionSiteEmbedResult {
Expand All @@ -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<FrameMode>('permission-check');
const [status, setStatus] = useState<ExtensionSiteEmbedStatus>('idle');
Expand Down Expand Up @@ -170,6 +174,9 @@ export const useExtensionSiteEmbed = ({
setErrorReason(null);
startReconnectLoop();
},
onOptOutRequested: () => {
onOptOutRequestedRef.current?.();
},
onMissingPermission: () => {
stopReconnectLoop();
setStatus('permission-required');
Expand Down
3 changes: 3 additions & 0 deletions packages/shared/src/lib/log.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
}
Expand Down
Loading