Skip to content

Commit 8cb247a

Browse files
committed
Merge branch 'funnel/signup-hero-blocks' of github.com:dailydotdev/apps into funnel/signup-hero-blocks
2 parents 5dc75f6 + 9e93126 commit 8cb247a

335 files changed

Lines changed: 23036 additions & 5577 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

AGENTS.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,10 @@ We're a startup. We move fast, iterate quickly, and embrace change. When impleme
1616
- Use early returns instead of if-else blocks for cleaner, flatter code
1717
- Handle the errors or checks first and return early then proceed with happy path at the end of code block
1818

19+
**Comments:**
20+
- Do not add comments that restate what the code already says. If a variable name, condition, or function call makes the intent clear, no comment is needed.
21+
- Only comment to explain *why* something non-obvious is done (a constraint, a gotcha, a deliberate trade-off) — never to narrate *what* the code does line by line.
22+
1923
**Invariant handling:**
2024
- Do not silently ignore impossible states (for example, no-op rollback fallbacks in mutation/cache flows)
2125
- Fail fast with a clear thrown error message when an internal invariant is violated

package.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,5 +23,8 @@
2323
},
2424
"volta": {
2525
"node": "24.14.0"
26+
},
27+
"devDependencies": {
28+
"eslint-plugin-unused-imports": "3.2.0"
2629
}
2730
}

packages/eslint-config/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
"eslint-plugin-react": "^7.37.5",
1919
"eslint-plugin-react-hooks": "^4.6.2",
2020
"eslint-plugin-tailwindcss": "^3.18.3",
21-
"eslint-plugin-unused-imports": "^3.2.0",
21+
"eslint-plugin-unused-imports": "3.2.0",
2222
"typescript": "5.6.3"
2323
},
2424
"peerDependencies": {

packages/extension/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "extension",
3-
"version": "3.45.0",
3+
"version": "3.45.4",
44
"scripts": {
55
"dev": "cross-env NODE_ENV=development cross-env TARGET_BROWSER=chrome rspack build -c rspack.config.js --watch",
66
"build": "cross-env NODE_ENV=production cross-env TARGET_BROWSER=chrome rspack build -c rspack.config.js",

packages/extension/public/_locales/es/messages.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"extensionName": {
3-
"message": "daily.dev | La forma inteligente de seguir la actualidad tech, en cada nueva pestaña"
3+
"message": "daily.dev | Sigue la actualidad tech en cada nueva pestaña"
44
},
55
"extensionDescription": {
66
"message": "Noticias tech personalizadas para tu stack, en cada nueva pestaña. Únete a más de 400.000 developers. Gratis y open source."

packages/extension/public/_locales/fr/messages.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"extensionName": {
3-
"message": "daily.dev | L’actualité des développeurs comme elle devrait l’être, dans chaque nouvel onglet"
3+
"message": "daily.dev | L’actu des devs, comme il faut, dans chaque nouvel onglet"
44
},
55
"extensionDescription": {
66
"message": "L’actualité des développeurs, personnalisée selon votre stack, dans chaque nouvel onglet. Rejoignez plus de 400 000 développeurs. Gratuit et open source."

packages/extension/src/background/index.ts

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ import { install, uninstall } from '@dailydotdev/shared/src/lib/constants';
1111
import { BOOT_LOCAL_KEY } from '@dailydotdev/shared/src/contexts/common';
1212
import { ExtensionMessageType } from '@dailydotdev/shared/src/lib/extension';
1313
import { storageWrapper as storage } from '@dailydotdev/shared/src/lib/storageWrapper';
14+
import { frameEmbeddingPermissionBridgeTiming } from '@dailydotdev/shared/src/features/extensionEmbed/pagePermissionBridge';
15+
import type { PermissionGrantResponse } from '@dailydotdev/shared/src/features/extensionEmbed/pagePermissionBridge';
1416
import {
1517
getContentScriptPermissionAndRegister,
1618
registerEmbedTargetReadyContentScript,
@@ -20,6 +22,7 @@ import {
2022
disableFrameEmbeddingForTab,
2123
enableFrameEmbeddingForTab,
2224
} from '../lib/frameEmbedding';
25+
import { requestFrameEmbeddingPermissions } from '../lib/frameEmbeddingPermissions';
2326

2427
type ChromeRuntimeMessageSender = Runtime.MessageSender;
2528
type ChromeSendResponse = (response?: unknown) => void;
@@ -67,6 +70,13 @@ const getTargetTabId = async (
6770

6871
const client = new GraphQLClient(graphqlUrl, { fetch: globalThis.fetch });
6972

73+
// Once-off signal set when the activation primer opens chrome://newtab. The
74+
// new tab page consumes it on load to route a logged-out user into onboarding
75+
// a single time. Kept in memory (no `storage` permission in prod) — the
76+
// service worker stays alive across the brief open-tab → tab-load handoff; if
77+
// it doesn't, the new tab simply falls back to its normal behavior.
78+
let activateOnboardingPending = false;
79+
7080
const excludedCompanionOrigins = [
7181
'http://127.0.0.1:5002',
7282
'http://localhost',
@@ -185,6 +195,78 @@ async function handleMessages(
185195
return disableFrameEmbeddingForTab(tabId);
186196
}
187197

198+
if (message.type === ExtensionMessageType.PingFrameEmbeddingReady) {
199+
// Lightweight liveness check used by the content script to detect when
200+
// the service worker is back online after the post-grant runtime.reload.
201+
return { ready: true };
202+
}
203+
204+
if (message.type === ExtensionMessageType.RequestOpenNewTab) {
205+
// Open a real new tab so Chrome shows the new-tab override-confirmation
206+
// bubble. The literal `chrome://newtab` URL is required —
207+
// `chrome.runtime.getURL('index.html')` loads the override page directly
208+
// via `chrome-extension://` and does NOT register as an NTP visit. We also
209+
// arm the once-off onboarding signal so the new tab can route a logged-out
210+
// user straight into onboarding a single time.
211+
try {
212+
activateOnboardingPending = true;
213+
await browser.tabs.create({ url: 'chrome://newtab', active: true });
214+
return { triggered: true };
215+
} catch (error) {
216+
activateOnboardingPending = false;
217+
return {
218+
triggered: false,
219+
error:
220+
error instanceof Error ? error.message : 'Failed to open new tab',
221+
};
222+
}
223+
}
224+
225+
if (message.type === ExtensionMessageType.ConsumeActivateOnboarding) {
226+
// The new tab page asks once on load whether it should route into
227+
// onboarding. Reset immediately so this only ever fires for the tab the
228+
// primer just opened.
229+
const pending = activateOnboardingPending;
230+
activateOnboardingPending = false;
231+
return { pending };
232+
}
233+
234+
if (message.type === ExtensionMessageType.RequestFrameEmbeddingPermissions) {
235+
// Relay path so the daily.dev page can drive chrome.permissions.request
236+
// without losing the user gesture. chrome.permissions.request is only
237+
// callable from extension pages, so the content script forwards here and
238+
// the user gesture from the originating click is preserved via the
239+
// Runtime.MessageSender.userGesture flag.
240+
try {
241+
const granted = await requestFrameEmbeddingPermissions();
242+
if (granted) {
243+
// Chromium needs a fresh extension context to pick up the just-granted
244+
// optional host permission for declarativeNetRequest — without it the
245+
// DNR session-rule call from frame.html hangs. Schedule the reload
246+
// on the next tick so the response can be delivered first; the
247+
// content script then polls until the new worker is up before
248+
// resolving the grant Promise on the page.
249+
globalThis.setTimeout(() => {
250+
browser.runtime.reload();
251+
}, frameEmbeddingPermissionBridgeTiming.reloadDelayMs);
252+
}
253+
const response: PermissionGrantResponse = {
254+
granted,
255+
willReload: granted,
256+
};
257+
return response;
258+
} catch (error) {
259+
const response: PermissionGrantResponse = {
260+
granted: false,
261+
error:
262+
error instanceof Error
263+
? error.message
264+
: 'Failed to request frame embedding permissions',
265+
};
266+
return response;
267+
}
268+
}
269+
188270
await getContentScriptPermissionAndRegister();
189271

190272
if (message.type === ExtensionMessageType.ContentLoaded) {

packages/extension/src/companion/CompanionPopupButton.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import { useContentScriptStatus } from '@dailydotdev/shared/src/hooks';
1111
import { useLogContext } from '@dailydotdev/shared/src/contexts/LogContext';
1212
import { CompanionPermission } from './CompanionPermission';
1313

14-
export const CompanionPopupButton = (): ReactElement => {
14+
export const CompanionPopupButton = (): ReactElement | null => {
1515
const { logEvent } = useLogContext();
1616
const { contentScriptGranted, isFetched } = useContentScriptStatus();
1717
const [showCompanionPermission, setShowCompanionPermission] = useState(false);
@@ -56,6 +56,9 @@ export const CompanionPopupButton = (): ReactElement => {
5656
content={<CompanionPermission />}
5757
placement="bottom-start"
5858
showArrow={false}
59+
// Portal to body so the popover escapes the v2 sidebar's
60+
// `overflow-hidden` panel.
61+
appendTo={() => document.body}
5962
container={{
6063
paddingClassName: 'px-6 py-4',
6164
bgClassName: 'bg-background-default',

packages/extension/src/newtab/App.tsx

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,13 +19,17 @@ import { defaultQueryClientConfig } from '@dailydotdev/shared/src/lib/query';
1919
import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
2020
import { useWebVitals } from '@dailydotdev/shared/src/hooks/useWebVitals';
2121
import { useGrowthBookContext } from '@dailydotdev/shared/src/components/GrowthBookProvider';
22-
import { isTesting } from '@dailydotdev/shared/src/lib/constants';
22+
import {
23+
isTesting,
24+
onboardingUrl,
25+
} from '@dailydotdev/shared/src/lib/constants';
2326
import { withFeaturesBoundary } from '@dailydotdev/shared/src/components/withFeaturesBoundary';
2427
import { LazyModalElement } from '@dailydotdev/shared/src/components/modals/LazyModalElement';
2528
import { useHostStatus } from '@dailydotdev/shared/src/hooks/useHostPermissionStatus';
2629
import ExtensionPermissionsPrompt from '@dailydotdev/shared/src/components/ExtensionPermissionsPrompt';
2730
import { useExtensionContext } from '@dailydotdev/shared/src/contexts/ExtensionContext';
2831
import { useConsoleLogo } from '@dailydotdev/shared/src/hooks/useConsoleLogo';
32+
import { ExtensionMessageType } from '@dailydotdev/shared/src/lib/extension';
2933
import { DndContextProvider } from '@dailydotdev/shared/src/contexts/DndContext';
3034
import { structuredCloneJsonPolyfill } from '@dailydotdev/shared/src/lib/structuredClone';
3135
import { useOnboardingActions } from '@dailydotdev/shared/src/hooks/auth';
@@ -136,6 +140,29 @@ function InternalApp(): ReactElement {
136140
}
137141
}, [contentScriptGranted]);
138142

143+
useEffect(() => {
144+
if (!isPageReady) {
145+
return undefined;
146+
}
147+
let isActive = true;
148+
// Ask the background once whether the activation primer opened this tab.
149+
// If so, a logged-out user is routed into onboarding a single time; every
150+
// later new tab falls back to the normal behavior below.
151+
browser.runtime
152+
.sendMessage({ type: ExtensionMessageType.ConsumeActivateOnboarding })
153+
.then((response) => {
154+
const pending = (response as { pending?: boolean } | undefined)
155+
?.pending;
156+
if (isActive && pending && !user) {
157+
window.location.href = onboardingUrl;
158+
}
159+
})
160+
.catch(() => undefined);
161+
return () => {
162+
isActive = false;
163+
};
164+
}, [isPageReady, user]);
165+
139166
useEffect(() => {
140167
document.title = unreadCount
141168
? `(${unreadCount}) ${DEFAULT_TAB_TITLE}`
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
import type { ReactElement } from 'react';
2+
import React from 'react';
3+
import {
4+
Button,
5+
ButtonSize,
6+
ButtonVariant,
7+
} from '@dailydotdev/shared/src/components/buttons/Button';
8+
import {
9+
Typography,
10+
TypographyType,
11+
} from '@dailydotdev/shared/src/components/typography/Typography';
12+
import { useAuthContext } from '@dailydotdev/shared/src/contexts/AuthContext';
13+
import { AuthTriggers } from '@dailydotdev/shared/src/lib/auth';
14+
15+
// Sticky strip pinned to the top of the new tab for logged-out users so
16+
// the auth CTAs stay reachable while scrolling. Shares the feed's
17+
// primary background so it reads as part of the same surface.
18+
export const ExtensionSignInStrip = (): ReactElement | null => {
19+
const { isAuthReady, isLoggedIn, showLogin } = useAuthContext();
20+
21+
if (!isAuthReady || isLoggedIn) {
22+
return null;
23+
}
24+
25+
const onLogIn = () =>
26+
showLogin({
27+
trigger: AuthTriggers.MainButton,
28+
options: { isLogin: true },
29+
});
30+
const onSignUp = () =>
31+
showLogin({
32+
trigger: AuthTriggers.MainButton,
33+
options: { isLogin: false },
34+
});
35+
36+
return (
37+
<section
38+
aria-label="Sign in to daily.dev"
39+
className="sticky top-0 z-3 mx-4 mb-3 laptop:mx-0"
40+
>
41+
<div className="flex items-center justify-between gap-4 rounded-12 border border-border-subtlest-quaternary bg-background-default px-6 py-4">
42+
<Typography
43+
type={TypographyType.Body}
44+
bold
45+
className="min-w-0 flex-1 text-text-primary"
46+
>
47+
Sign in to personalize your feed and save what matters.
48+
</Typography>
49+
<div className="flex shrink-0 items-center gap-2">
50+
<Button
51+
type="button"
52+
variant={ButtonVariant.Secondary}
53+
size={ButtonSize.Small}
54+
onClick={onLogIn}
55+
>
56+
Log in
57+
</Button>
58+
<Button
59+
type="button"
60+
variant={ButtonVariant.Primary}
61+
size={ButtonSize.Small}
62+
onClick={onSignUp}
63+
>
64+
Sign up
65+
</Button>
66+
</div>
67+
</div>
68+
</section>
69+
);
70+
};

0 commit comments

Comments
 (0)