Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
562c4d5
feat(onboarding): add new tab activation primer before signup wall
tsahimatsliah May 27, 2026
02684fa
chore(onboarding): force primer on for local testing
tsahimatsliah May 27, 2026
228af2c
Merge branch 'main' into claude/quirky-murdock-99152e
tsahimatsliah May 27, 2026
79759a4
fix(extension): route post-install URL to local webapp in dev builds
tsahimatsliah May 27, 2026
9666837
chore(onboarding): show primer on every visit in testing override
tsahimatsliah May 27, 2026
e5dfe12
Revert "fix(extension): route post-install URL to local webapp in dev…
tsahimatsliah May 27, 2026
634361b
chore(extension): point dev-build install URL at local staging webapp
tsahimatsliah May 27, 2026
3680c5a
chore(extension): hardcode install URL to local staging for testing
tsahimatsliah May 27, 2026
843140a
chore(primer): redirect to production signup wall after trigger
tsahimatsliah May 27, 2026
4d159ca
feat(primer): recovery screen opens chrome://extensions
tsahimatsliah May 27, 2026
79e8f3f
ci: retrigger to retry flaky PostPage test
tsahimatsliah May 27, 2026
48cf994
chore(newtab): redirect first post-install tab to prod onboarding
tsahimatsliah May 27, 2026
23247f6
Merge branch 'main' into claude/quirky-murdock-99152e
tsahimatsliah May 27, 2026
9450238
fix(primer): drop primer-tab redirect, add clipboard fallback for ext…
tsahimatsliah May 27, 2026
3657bc9
fix(primer): drop clipboard fallback, show manual hint on bridge failure
tsahimatsliah May 27, 2026
6d28253
refactor(primer): split new-tab activation into a dedicated /activate…
tsahimatsliah May 27, 2026
7c08d28
feat(primer): heartbeat-based real-time rejection detection
tsahimatsliah May 27, 2026
38b7355
refactor(primer): simplify activate page — bigger title, fewer words
tsahimatsliah May 27, 2026
a2aa465
feat(primer): visible video/GIF placeholder slot on activate page
tsahimatsliah May 27, 2026
7e38832
refactor(primer): Nikita-style activation screen — coach, don't beg
tsahimatsliah May 27, 2026
a710835
feat(primer): full Nikita pass — subhead, feed caption, KeepItClickTi…
tsahimatsliah May 27, 2026
5ddc5b6
refactor(primer): swap feed peek + dialog mockup back to a single vid…
tsahimatsliah May 27, 2026
6cef03c
feat(primer): wire the activation demo video (hardcoded path, swap to…
tsahimatsliah May 27, 2026
b2fb98a
refactor(primer): apply review pass — separate dialog & feed, louder …
tsahimatsliah May 27, 2026
381ddc2
refactor(primer): defuse the rejection reflex, predict the whole journey
tsahimatsliah May 27, 2026
0b2c516
refactor(primer): Nikita-pure — headline + video + button, nothing else
tsahimatsliah May 27, 2026
e88f766
refactor(primer): focused middle ground — explicit copy + big video +…
tsahimatsliah May 27, 2026
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
76 changes: 76 additions & 0 deletions packages/extension/src/background/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,82 @@ async function handleMessages(
return { ready: true };
}

if (message.type === ExtensionMessageType.RequestOpenNewTab) {
// Trigger Chrome's "Keep changes / Change it back" confirmation bubble
// by opening a real new tab. The literal `chrome://newtab` URL is
// required — `chrome.runtime.getURL('index.html')` loads the override
// page directly via `chrome-extension://` and does NOT register as an
// NTP visit, so the bubble would be deferred to the next real new tab.
try {
await browser.tabs.create({ url: 'chrome://newtab', active: true });
return { triggered: true };
} catch (error) {
return {
triggered: false,
error:
error instanceof Error ? error.message : 'Failed to open new tab',
};
}
}

if (message.type === ExtensionMessageType.PingExtensionAlive) {
// Heartbeat from the primer page — used to detect when Chrome
// disables the extension after the user picks "Change it back".
return { alive: true };
}

if (message.type === ExtensionMessageType.RequestOpenExtensionsPage) {
// Open the browser's extension management page. Web origins cannot
// navigate to chrome:// URLs directly, so the primer recovery screen
// bounces the request through the background to call
// `chrome.tabs.create` here.
try {
await browser.tabs.create({ url: 'chrome://extensions', active: true });
return { opened: true };
} catch (error) {
return {
opened: false,
error:
error instanceof Error
? error.message
: 'Failed to open extensions page',
};
}
}

if (message.type === ExtensionMessageType.NewTabActivated) {
// Forwarded from the post-install new tab page so the primer (running
// in another tab on daily.dev) can learn the override was accepted.
// We broadcast to every daily.dev tab; the ping content script in each
// writes a localStorage marker the primer polls.
try {
const tabs = await browser.tabs.query({
url: ['https://daily.dev/*', 'https://*.daily.dev/*'],
});
await Promise.all(
tabs.map((tab) => {
if (typeof tab.id !== 'number') {
return Promise.resolve();
}
return browser.tabs
.sendMessage(tab.id, {
type: ExtensionMessageType.NotifyNewTabActivated,
})
.catch(() => undefined);
}),
);
return { delivered: true };
} catch (error) {
return {
delivered: false,
error:
error instanceof Error
? error.message
: 'Failed to broadcast new tab activation',
};
}
}

if (message.type === ExtensionMessageType.RequestFrameEmbeddingPermissions) {
// Relay path so the daily.dev page can drive chrome.permissions.request
// without losing the user gesture. chrome.permissions.request is only
Expand Down
62 changes: 62 additions & 0 deletions packages/extension/src/newtab/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,9 @@ import { useHostStatus } from '@dailydotdev/shared/src/hooks/useHostPermissionSt
import ExtensionPermissionsPrompt from '@dailydotdev/shared/src/components/ExtensionPermissionsPrompt';
import { useExtensionContext } from '@dailydotdev/shared/src/contexts/ExtensionContext';
import { useConsoleLogo } from '@dailydotdev/shared/src/hooks/useConsoleLogo';
import { useLogContext } from '@dailydotdev/shared/src/contexts/LogContext';
import { LogEvent } from '@dailydotdev/shared/src/lib/log';
import { ExtensionMessageType } from '@dailydotdev/shared/src/lib/extension';
import { DndContextProvider } from '@dailydotdev/shared/src/contexts/DndContext';
import { structuredCloneJsonPolyfill } from '@dailydotdev/shared/src/lib/structuredClone';
import { useOnboardingActions } from '@dailydotdev/shared/src/hooks/auth';
Expand All @@ -46,6 +49,38 @@ import { useContentScriptStatus } from '../../../shared/src/hooks';
structuredCloneJsonPolyfill();

const DEFAULT_TAB_TITLE = 'New Tab';
const NEWTAB_ACTIVATED_KEY = 'daily-extension-newtab-activated-fired';

// TODO(REMOVE-BEFORE-MERGE): testing override — on the very first new tab
// after install, broadcast activation back to the primer and immediately
// hand off this tab to the production signup wall. Bypasses any
// boot/API failures (e.g. "Connection lost") that local extension builds
// would otherwise hit. Remove the constant, the helper, and the early
// return in `App` before merging.
const FIRST_NEWTAB_REDIRECT_URL = 'https://app.daily.dev/onboarding';
let didRedirectForTesting = false;
const maybeRedirectForTesting = (): boolean => {
if (typeof window === 'undefined' || didRedirectForTesting) {
return false;
}
if (window.location.href.includes('source=')) {
return false;
}
try {
if (localStorage.getItem(NEWTAB_ACTIVATED_KEY)) {
return false;
}
localStorage.setItem(NEWTAB_ACTIVATED_KEY, Date.now().toString());
} catch {
return false;
}
didRedirectForTesting = true;
browser.runtime
.sendMessage({ type: ExtensionMessageType.NewTabActivated })
.catch(() => undefined);
window.location.href = FIRST_NEWTAB_REDIRECT_URL;
return true;
};
const router = new CustomRouter();
const queryClient = new QueryClient(defaultQueryClientConfig);
Modal.setAppElement('#__next');
Expand Down Expand Up @@ -103,6 +138,7 @@ function InternalApp(): ReactElement {
const { hostGranted, isFetching: isCheckingHostPermissions } =
useHostStatus();
const routeChangedCallbackRef = useLogPageView();
const { logEvent } = useLogContext();
useConsoleLogo();
const { user, isAuthReady } = useAuthContext();
const { growthbook } = useGrowthBookContext();
Expand Down Expand Up @@ -136,6 +172,25 @@ function InternalApp(): ReactElement {
}
}, [contentScriptGranted]);

useEffect(() => {
// Fire the activation signal the first time the daily.dev new tab
// renders for this install — the only reliable success signal we have
// for Chrome's "Keep changes / Change it back" override bubble. The
// companion broadcast lets the post-install primer in the webapp tab
// know the user accepted the override.
if (!isPageReady) {
return;
}
if (localStorage.getItem(NEWTAB_ACTIVATED_KEY)) {
return;
}
localStorage.setItem(NEWTAB_ACTIVATED_KEY, Date.now().toString());
logEvent({ event_name: LogEvent.ExtensionNewTabActivated });
browser.runtime
.sendMessage({ type: ExtensionMessageType.NewTabActivated })
.catch(() => undefined);
}, [isPageReady, logEvent]);

useEffect(() => {
document.title = unreadCount
? `(${unreadCount}) ${DEFAULT_TAB_TITLE}`
Expand Down Expand Up @@ -175,6 +230,13 @@ export default function App({
const [currentPage, setCurrentPage] = useState<string>('/');
const deviceId = useDeviceId();

// TODO(REMOVE-BEFORE-MERGE): testing override at top of file. Hooks
// above are called unconditionally to satisfy rules-of-hooks; the
// redirect short-circuits the render so providers never mount.
if (maybeRedirectForTesting()) {
return <></>;
}

return (
<RouterContext.Provider value={router}>
<ProgressiveEnhancementContextProvider>
Expand Down
121 changes: 121 additions & 0 deletions packages/extension/src/ping/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,20 @@ import type {
PagePermissionBridgeResult,
PermissionGrantResponse,
} from '@dailydotdev/shared/src/features/extensionEmbed/pagePermissionBridge';
import {
newTabActivationBridgeRequestEvent,
newTabActivationBridgeResultEvent,
newTabActivationSuccessKey,
openExtensionsPageBridgeRequestEvent,
openExtensionsPageBridgeResultEvent,
pingExtensionBridgeRequestEvent,
pingExtensionBridgeResultEvent,
} from '@dailydotdev/shared/src/features/extensionEmbed/newTabActivationBridge';
import type {
NewTabActivationBridgeResult,
OpenExtensionsPageBridgeResult,
PingExtensionBridgeResult,
} from '@dailydotdev/shared/src/features/extensionEmbed/newTabActivationBridge';

const INSTALL_MARKER = 'dailyExtensionInstalled';
const ID_MARKER = 'dailyExtensionId';
Expand Down Expand Up @@ -65,6 +79,113 @@ const waitForExtensionReady = async (): Promise<void> => {
}
};

browser.runtime.onMessage.addListener((message: { type?: string }) => {
if (message?.type === ExtensionMessageType.NotifyNewTabActivated) {
// Broadcast from the background after the post-install new tab page
// rendered. Writing localStorage here surfaces the signal to the
// primer running on this same daily.dev origin.
try {
localStorage.setItem(newTabActivationSuccessKey, Date.now().toString());
} catch {
// localStorage can fail in private contexts; the primer will fall
// back to its timeout-driven recovery screen.
}
}
return undefined;
});

window.addEventListener(newTabActivationBridgeRequestEvent, () => {
const dispatchResult = (result: NewTabActivationBridgeResult): void => {
window.dispatchEvent(
new CustomEvent<NewTabActivationBridgeResult>(
newTabActivationBridgeResultEvent,
{ detail: result },
),
);
};

browser.runtime
.sendMessage({
type: ExtensionMessageType.RequestOpenNewTab,
})
.then((response) => {
const typed = response as NewTabActivationBridgeResult | undefined;
dispatchResult({
triggered: !!typed?.triggered,
error: typed?.error,
});
})
.catch((error: unknown) => {
dispatchResult({
triggered: false,
error:
error instanceof Error
? error.message
: 'Failed to request new tab open',
});
});
});

window.addEventListener(pingExtensionBridgeRequestEvent, () => {
const dispatchResult = (result: PingExtensionBridgeResult): void => {
window.dispatchEvent(
new CustomEvent<PingExtensionBridgeResult>(
pingExtensionBridgeResultEvent,
{ detail: result },
),
);
};

// If the extension has been disabled, `browser.runtime.sendMessage`
// throws "Extension context invalidated" synchronously or rejects.
// Either way we report `alive: false` and the primer flips to recovery.
try {
browser.runtime
.sendMessage({ type: ExtensionMessageType.PingExtensionAlive })
.then((response) => {
const typed = response as { alive?: boolean } | undefined;
dispatchResult({ alive: !!typed?.alive });
})
.catch(() => {
dispatchResult({ alive: false });
});
} catch {
dispatchResult({ alive: false });
}
});

window.addEventListener(openExtensionsPageBridgeRequestEvent, () => {
const dispatchResult = (result: OpenExtensionsPageBridgeResult): void => {
window.dispatchEvent(
new CustomEvent<OpenExtensionsPageBridgeResult>(
openExtensionsPageBridgeResultEvent,
{ detail: result },
),
);
};

browser.runtime
.sendMessage({
type: ExtensionMessageType.RequestOpenExtensionsPage,
})
.then((response) => {
const typed = response as OpenExtensionsPageBridgeResult | undefined;
dispatchResult({
opened: !!typed?.opened,
error: typed?.error,
});
})
.catch((error: unknown) => {
dispatchResult({
opened: false,
error:
error instanceof Error
? error.message
: 'Failed to request extensions page',
});
});
});

window.addEventListener(pagePermissionBridgeRequestEvent, () => {
const dispatchResult = (result: PagePermissionBridgeResult): void => {
window.dispatchEvent(
Expand Down
Loading
Loading