feat(onboarding): new tab activation primer before signup wall#6107
Open
tsahimatsliah wants to merge 23 commits into
Open
feat(onboarding): new tab activation primer before signup wall#6107tsahimatsliah wants to merge 23 commits into
tsahimatsliah wants to merge 23 commits into
Conversation
Insert a permission-priming step at the very top of the post-install onboarding flow so users see an explicit "this is what Chrome is about to ask and which button to tap" screen before they encounter Chrome's "Change back to Google?" override-confirmation bubble. The screen recreates the dialog visually, highlights the "Keep it" button with a brand-color callout, and addresses developer skepticism with concrete trust claims (no browsing history, no clickbait, reversible in chrome://extensions). The webapp asks the extension to programmatically open chrome://newtab via the existing ping content-script bridge so the bubble appears while the user is still primed. Success is detected via a localStorage signal written by the ping script after the new-tab page broadcasts activation; failure falls through to a recovery screen with a "I activated it" retry and a "Continue without new tab" skip path. Gated behind featureOnboardingPermissionPrimer (default off) for A/B rollout. Re-entry via the existing ?r=extension param from HijackingLoginStrip now correctly skips the primer. Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
TEMPORARY — remove before merging. Forces the new tab activation primer to render for every fresh install regardless of the GrowthBook featureOnboardingPermissionPrimer value. Marked with TODO(REMOVE-BEFORE-MERGE) so the override is easy to find and revert. Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
Previously the install URL was hardcoded to the production Rebrandly redirect (https://r.daily.dev/install), which always bounced the post-install tab to app.daily.dev — even for dev/staging builds. That made the local primer flow untestable end-to-end: installing a locally-built extension would open the production webapp, where the new code does not exist. Now in non-production builds the install URL points directly at the configured webapp's /onboarding path. Production behavior is unchanged. Also adds a TODO-marked one-shot console.info on the onboarding page that surfaces the primer gating conditions for local QA — to be removed along with the FORCE_PRIMER_FOR_TESTING override before merge. Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
Broadens the FORCE_PRIMER_FOR_TESTING bypass so the primer is visible even when the user is already logged in, has completed it before, lacks the extension marker, or arrived via ?r=extension. Also skips the localStorage write on complete so reloading re-triggers the primer. Promotes the gating-condition console log from .info to .warn so it is hard to miss in DevTools. All marked TODO(REMOVE-BEFORE-MERGE). Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
… builds" The dev webapp URL in .env points at local.fylla.dev, which is not actually wired up on every developer's machine, so the post-install tab opened a blank page instead of the local webapp. Restore the original production Rebrandly redirect. To exercise the primer locally, navigate to <your-local-webapp>/onboarding directly — the FORCE_PRIMER_FOR_TESTING override in onboarding.tsx renders the primer on every visit regardless of how you arrived. Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
In dev builds (NODE_ENV=development) route the post-install tab to https://app.staging.daily.dev:5002/onboarding so the primer flow can be tested locally end-to-end. Production builds keep the Rebrandly redirect unchanged. Marked TODO(REMOVE-BEFORE-MERGE). Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
Replace the NODE_ENV check with a literal staging URL so the post-install tab reliably opens the local primer flow regardless of how the extension is built. Marked TODO(REMOVE-BEFORE-MERGE). Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
Testing override so the full visual flow can be simulated locally: after the primer triggers the new tab (and Chrome's confirmation bubble appears in that new tab), redirect the originating tab to https://app.daily.dev/onboarding so the user lands on the real production signup wall. In production this redirect is unnecessary because the primer is already served from app.daily.dev. Marked TODO(REMOVE-BEFORE-MERGE). Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
Reworks the primer recovery state to focus on a single, clear action:
re-enable daily.dev. Removes the "Continue without new tab" skip path.
The new primary CTA bounces a request through the existing ping
content-script bridge to the extension's service worker, which calls
chrome.tabs.create({ url: 'chrome://extensions' }) — web origins cannot
navigate to chrome:// URLs directly. Also adds a stylized mockup of the
daily.dev card on the extensions page with a callout on the on/off
toggle so users know exactly what to flip.
If the bridge fails (extension already disabled, content script gone),
nothing visibly happens, but the mockup gives the user enough context to
navigate manually.
Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
Testing override so the new tab does not fall into the boot/API "Connection lost" UI when running an unsigned local extension build. On the very first new tab after install we (a) broadcast activation back to the primer and (b) immediately redirect the tab to app.daily.dev/onboarding so the user lands on the real signup wall. Implemented at the top of the App component (before providers render) so no API call ever fires. Subsequent new tabs and the action-button new tab (?source=button) render normally. Marked TODO(REMOVE-BEFORE-MERGE). Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
…ensions page Two fixes: 1. Remove the post-trigger window.location.href redirect from the primer tab. The redirect lives on the new tab itself (extension App.tsx), so the primer tab should stay in its waiting state and detect activation via the localStorage signal. This is the intended split: new tab handles its own handoff, primer tab observes the success signal. 2. The "Open extensions page" button now falls through to copying chrome://extensions to the clipboard with an inline confirmation message if the extension bridge fails (the most common failure path: the user picked "Change it back", Chrome disabled the extension, and the service worker is no longer there to receive the bridge message). Also handles the rare case where clipboard write is blocked by showing a plain instruction. Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
Per request, the "Open extensions page" button no longer copies to clipboard when the bridge fails. It tries to navigate via the extension service worker, and if that fails (almost always because Chrome disabled the extension when the user picked "Change it back") shows an inline message telling the user to open chrome://extensions manually. Web pages cannot navigate to chrome:// URLs without an extension proxying the call, so when the extension is disabled the only option is to instruct the user. No JS workaround exists for this Chrome browser restriction. Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
… page The primer no longer lives inside /onboarding. A new /activate page hosts only the new-tab activation flow — no auth wall, no funnel, no gating. The user stays on that page until the new-tab override is detected (via the localStorage bridge), at which point we router.replace to /onboarding so the existing signup flow takes over. This makes the post-install funnel two clearly separated steps instead of a conditional state inside /onboarding: install → /activate (primer) → success → /onboarding (signup) Also removes all the testing scaffolding I had layered on /onboarding (FORCE_PRIMER_FOR_TESTING, debug console.warn, shouldShowPrimerProd, ?r=extension skip, related imports). /onboarding goes back to its pre-PR shape. Install URL constant now points at /activate so the post-install tab opens the primer directly. Production deployment also needs the Rebrandly redirect updated to /activate (noted in the comment). Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
Adds a 2s heartbeat from the primer page to the extension service
worker via the existing content-script bridge. Chrome has no event for
"user picked Change it back", but it disables our extension as a side
effect — at which point browser.runtime.sendMessage from the content
script starts throwing "Extension context invalidated". The heartbeat
catches that and flips the primer to the recovery screen after two
consecutive missed pings (~4-7s), well before the 10s post-trigger
blind timeout would fire.
Mechanics:
- New ExtensionMessageType.PingExtensionAlive
- Background returns { alive: true }
- New bridge helper pingExtensionFromPage with 3s timeout
- Ping content script forwards and catches throws/rejects
- Primer runs interval heartbeat from mount until completion/recovery
- One forgiveness slot per cycle to absorb service-worker cold starts
Also threads the existing recovery transition through a single
goToRecovery helper so the three paths (storage-key rejection signal,
post-trigger timeout, heartbeat failure) stay in sync on log events
and idempotency.
Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
Per design feedback: the page was content-heavy and felt
over-explanatory, which can trigger more suspicion from developers
instead of less. Pared back to a single focused screen:
• Headline jumps from LargeTitle to Mega2: "Please activate your new tab"
• Removed the subhead paragraph entirely — the visual mockup
already shows "Change back to Google? — tap Keep it" in literal
Chrome UI, so the prose was redundant.
• CTA copy shortened from "Activate new tab" to "Activate".
• Three trust bullets collapsed to a single privacy-first one-liner
under the button: "Privacy-first — no browsing history collected."
Privacy is the developer's primary concern at activation time;
keeping it small and singular reads as confidence, not hedging.
• Replaced the expandable "Why does Chrome ask this?" disclosure
with one always-visible caption: "Chrome shows this prompt for
every extension that changes the new tab — it's a standard
privacy check."
• Left a TODO marker where the static Chrome dialog mockup currently
sits, documenting the planned swap for a short autoplay/loop/muted
video showing the click target.
Recovery screen untouched — it already has the right tone.
Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
Replaces the Chrome dialog mockup on the activate page with a clearly
marked placeholder box where the demo video/GIF will live. The box uses
a 16:9 aspect ratio, a dashed border, a muted background, and a
play-icon + label so it reads as "intentionally empty, swap in a video
here" rather than broken.
When the recording is ready, the swap is a one-line change:
<VideoPlaceholder />
becomes
<video src={url} poster={posterUrl} muted autoPlay loop playsInline
disablePictureInPicture controls={false} className="..." />
ChromeDialogMockup is kept defined (with eslint-disable for the unused
warning) until the real video lands, in case we need to fall back to
the static mockup again. Will be deleted once the video is wired up.
Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
Applies the design review's biggest moves so the screen does its one
real job: transfer the muscle memory for tapping "Keep it" on the
Chrome dialog that's about to appear.
• Headline changes from "Please activate your new tab" (a beg) to
"Tap 'Keep it' when Chrome asks." (a coaching cue). Smaller too —
Title1 instead of Mega2, so it sits on an instruction card, not a
marketing hero.
• CTA button text changes from "Activate" to "Keep it" — same word,
same hand position, same outcome as the Chrome bubble. The button
is now training the click that's 1 second away.
• Replaces the video placeholder with a layered visual: the static
Chrome dialog mockup (already with the brand-color glow on the
"Keep it" button) sits on top of a stylized blurred peek of the
new-tab feed. The feed peek counters Chrome's "Change back to
Google?" framing — gives the user something concrete to keep,
instead of an "unknown extension."
• Three trust bullets restored under the CTA — Private / Curated /
Reversible — each a single line, no defensive "Chrome shows this
prompt…" footnote (which was inviting the very objection it tried
to dispel). Bullets are tertiary text, footnote size — present
without dominating.
• Adds a "Step 1 of 1 · Takes 2 seconds" chip above the headline as
an urgency tell — communicates this is the only friction, no
surprise downstream steps.
• Tightens vertical density (gap-5 inner, py-8 outer) so headline +
visual + CTA + bullets all sit above the fold on a 13" laptop.
Recovery screen unchanged.
Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
…meMs
Closes the remaining gaps from the design review:
• Adds the missing one-line subhead under the headline:
"Chrome's confirmation pops up the moment you click below."
Headline + subhead now form a tight instruction card (per
"reduce headline size ~30%, pair it immediately with a one-line
subhead"). Headline sized to LargeTitle so it carries weight
without dominating.
• Adds the missing caption under the layered visual:
"This is what opens every time you hit ⌘T."
This is the payoff phrase the review called out — turns
"Keep it" from "keep some unknown extension" into "keep this
feed I'm looking at."
• Adds the KeepItClickTimeMs telemetry. Captures Date.now() in a
ref when the primer CTA is clicked, then computes the delta and
attaches it as `extra: { keep_it_click_time_ms: N }` on the
ExtensionNewTabActivated event. Small values = priming worked;
large values = the user hesitated on Chrome's bubble.
Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
…eo placeholder Per feedback the feed-cards-grid backdrop wasn't reading right. Restored the simpler video/GIF placeholder slot (dashed box, play-icon, label) so the layout, sizing, and position are exercised in advance — when the real recording is ready, replacing <VideoPlaceholder /> with a <video> element is a one-line swap. Kept the payoff caption underneath the slot: "This is what opens every time you hit ⌘T." Deleted ChromeDialogMockup entirely (it was the basis of the layered visual that's now gone). Going forward the real dialog and click target live in the video itself — much higher fidelity than any hand-rolled mockup could be. Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
… Cloudinary before merge)
I didn't have Cloudinary credentials in this environment, so the demo
video file (~395 KB) is committed to webapp/public/activate-demo.mp4
and the primer's <video> element references it as /activate-demo.mp4.
Next.js serves public files at the root path, so the URL resolves on
both local dev (app.staging.daily.dev:5002) and any future deploy.
Replaces the dashed-box VideoPlaceholder with a real <video> element
using the canonical autoplay-loop attributes pattern from
OnboardingPlusVariationV1.tsx: muted + autoPlay + loop + playsInline +
disablePictureInPicture + controls={false}.
TODO(BEFORE-MERGE): upload the video to Cloudinary and swap the
ACTIVATION_DEMO_URL constant for the hosted URL (plus a poster image
URL). Then drop the binary from the repo. Marked clearly in the
component comment.
Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
chrome://newtab(via the existing ping content-script bridge) so the dialog appears while the user is still primed; success is detected via a localStorage signal, failure falls through to a recovery screen.chrome://extensions), and an expandable "Why does Chrome ask this?" disclosure.featureOnboardingPermissionPrimer(default off) for A/B rollout. Re-entry via the existing?r=extensionparam fromHijackingLoginStripnow correctly skips the primer (the param was previously unread).Why
Today only a minority of installs end up with the daily.dev new tab actually active, because the user first hits the signup wall and only later encounters Chrome's confirmation bubble — at which point the "Change back to Google?" framing nudges them toward reverting. New-tab activation correlates strongly with retention, so converting more of those bubble interactions is a bigger lever than signup itself.
Implementation notes
chrome://newtabis used inchrome.tabs.create—chrome.runtime.getURL('index.html')would load the override page directly viachrome-extension://and would not register as an NTP visit, deferring the bubble.chrome.management.onDisabledwas considered as a rejection signal but does not fire for the listening extension's own self-disable; the primer relies on a 10s timeout + recovery screen instead.#1a73e8/#d3e3fd) so the preview matches what the user sees seconds later.Telemetry
New
LogEvents for the full funnel:ExtensionPrimerShown,ExtensionPrimerCtaClick,ExtensionNewTabTriggeredExtensionNewTabActivated(also fires once per install from the new tab page itself)ExtensionDialogRejected,ExtensionPrimerRecoveryShown,ExtensionPrimerSkippedTest plan
featureOnboardingPermissionPrimeron: confirm primer renders before the signup wall.ExtensionNewTabActivatedfires and the primer auto-advances to signup.HijackingPagestill works and routing through?r=extensionskips the primer on re-entry.🤖 Generated with Claude Code
Preview domain
https://claude-quirky-murdock-99152e.preview.app.daily.dev