feat(onboarding): new tab activation primer before signup wall#6107
Open
tsahimatsliah wants to merge 27 commits into
Open
feat(onboarding): new tab activation primer before signup wall#6107tsahimatsliah wants to merge 27 commits into
tsahimatsliah wants to merge 27 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) <noreply@anthropic.com>
|
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) <noreply@anthropic.com>
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) <noreply@anthropic.com>
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) <noreply@anthropic.com>
… 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) <noreply@anthropic.com>
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) <noreply@anthropic.com>
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) <noreply@anthropic.com>
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) <noreply@anthropic.com>
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) <noreply@anthropic.com>
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) <noreply@anthropic.com>
…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) <noreply@anthropic.com>
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) <noreply@anthropic.com>
… 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) <noreply@anthropic.com>
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) <noreply@anthropic.com>
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) <noreply@anthropic.com>
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) <noreply@anthropic.com>
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) <noreply@anthropic.com>
…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) <noreply@anthropic.com>
…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) <noreply@anthropic.com>
… 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) <noreply@anthropic.com>
…Keep-it annotation
Addressing the priority issues from the second design review:
1. Feed peek was wasted in the video composite (modal covered the
blur, caption promised what the image couldn't deliver). Adopted
Option B from the review: drop the video composite entirely. Now
two clean artifacts stacked vertically — Chrome dialog mockup on
top (clean dark background, no busy backdrop), feed screenshot
placeholder below with the "every new tab" caption.
2. Chrome's real palette puts "Change it back" as the prominent
primary blue and "Keep it" as the lighter secondary — exactly
the conversion problem. Mockup now mirrors that reality
faithfully so the user recognizes the actual dialog, but we
compensate aggressively: brand-green ring + animate-pulse on
"Keep it", plus the existing animated arrow and "Tap this one"
label below the dialog. The annotation has to be louder than
Chrome's primary styling.
3. Subhead was filler. Rewrote to do real work:
"In 1 second, Chrome will pop up this exact box. Tap the left
button." Now the subhead reinforces what (this exact box), when
(1 second), and which button (left).
4. ⌘T was Mac-only — read as gibberish on Windows/Linux/ChromeOS.
Replaced with universal: "Every time you open a new tab, you'll
see this."
Minor polish:
- Dropped headline trailing period (punchier as a directive).
- Step 1 of 1 pill gets a hairline border and slightly larger
typo-footnote sizing so it doesn't read as a tooltip.
- Bullets wrapped in a centered max-w-[24rem] block so they're
visually centered as a unit while keeping left-aligned scannable
rows underneath.
Also deletes the temporary /activate-demo.mp4 binary that was a
stopgap for testing — now the page renders without it.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adopts the corrected design brief. The job of this page isn't "show
the feed payoff" — there's no feed to show yet — it's "remove every
surprise between install and the Chrome bubble so the reflex is Keep
it, not get-this-away-from-me."
Changes:
1. Three-step journey rail under the dialog mock:
Keep it (1, highlighted) → Sign in (5s) (2) → Tab live (3)
The user has now seen the signup wall coming, so when they hit
it 5 seconds from now, no surprise-fueled buyer's remorse.
2. Step pill honesty: "Step 1 of 1 · Takes 2 seconds" was lying —
there are actually three steps. Changed to "Step 1 of 3 · ~10
seconds total". Honesty pre-empts the same reflex it was trying
to suppress.
3. Trust bullets reordered — Reversibility moves to FIRST. It's the
direct antidote to Chrome's "what if I don't like this" reflex.
Private and Curated stay as supporting evidence.
4. Social proof under CTA: "Trusted by millions of developers ·
4.8★ on the Chrome Web Store" — reframes Keep it from "trust
this stranger" to "join the obvious default."
5. CTA microcopy: "Then a 5-second sign-in, and you're in." —
the journey preview embedded in the moment of commitment.
6. "Why is Chrome asking this?" disclosure restored as a visible,
clickable single line under the bullets. The skeptical user
(most likely to click Change it back) is also the most likely
to open the disclosure and have their paranoia drop. Fires
LogEvent.ExtensionPrimerWhyChromeExpanded on first expansion
so we can segment conversion by openers vs non-openers — the
review's bet is openers convert 1.5-2x.
7. Subhead tightened: drop "In 1 second" — adds wording, no signal.
Page structure now: pill → headline → subhead → dialog mock →
journey rail → CTA → CTA microcopy → social proof → 3 bullets →
disclosure. Every element either predicts a moment, defuses an
objection, or transfers muscle memory.
The one metric to watch (per review):
ExtensionNewTabActivated / ExtensionPrimerShown — full survival
rate through the Chrome bubble. Segment by ExtensionPrimerWhy-
ChromeExpanded to test the disclosure hypothesis.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Strips the activate page to the three elements that actually do
muscle-memory transfer for the upcoming Chrome bubble:
1. Headline: "One click and you're in" — forward intent, zero
defensiveness, zero warning tone. The user already chose to
install; the page does not re-sell them.
2. Demo video — large, centered, autoplay/loop/muted. Subtitles
intended to be burned into the video itself (one artifact
teaching the action visually AND verbally, which is how humans
learn motor sequences). Subtitle script for the production
re-record is in the TODO above ACTIVATION_DEMO_URL.
3. One button: "Activate new tab" — triggers the sequence. The
video does the Keep-it mirror; the button just starts the flow.
Stripped (every element below was re-selling the user / planting
doubts that didn't exist / re-explaining what the video already
shows):
- Step pill ("Step 1 of 3 · ~10 seconds total")
- Subhead
- Static Chrome dialog mockup (ChromeDialogMockup) — replaced by the
video, which does the same job and adds the cursor motion
- 3-step journey rail (JourneyRail)
- CTA microcopy ("Then a 5-second sign-in, and you're in.")
- Social proof line ("Trusted by millions of developers · 4.8★ …")
- Three trust bullets (TrustBullets) — Reversible / Private /
Curated. Each one raised an objection the user hadn't formed yet;
cumulatively they read as defensive theatre.
- "Why is Chrome asking this?" disclosure (WhyChromeAsks)
State machine, bridges, heartbeat, polling, telemetry, recovery
screen — all untouched. Only the non-recovery presentation layer
was rewritten.
Restored /activate-demo.mp4 in webapp/public (was deleted in a
previous iteration when the video composite was abandoned). Same
TODO(BEFORE-MERGE) as before: upload to Cloudinary, drop the binary
from the repo.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
… one reassurance
The previous "minimal" pass confused minimal with clear. Five elements,
each one explicitly teaching the upcoming Chrome interaction:
1. Headline (explicit, not poetic):
"Almost there — Chrome will ask one quick question."
Tells the user what's about to happen. No surprise to recoil from.
2. Sub-line (which button, what it does):
"Tap "Keep it" to finish setting up your new tab."
Names the click and the outcome.
3. Video — sized large. Page container widens to max-w-[48rem]
(was 36rem) and the video drops its own width cap to fill that
container. ~50% of viewport height on a 13" laptop. Burned-in
subtitles still TODO for the re-record.
4. CTA: "Keep it" — mirrors Chrome's button now that the page has
taught the word.
5. One reassurance line under the CTA (replaces the three trust
bullets):
"Takes 2 seconds. Reversible anytime."
Reversibility is the one bullet that actively defuses
"Change it back". Privacy and curation are battles already won
at the Chrome Web Store listing.
Recovery headline changed from "Almost there" to "Let's turn it back
on" to avoid collision with the new primary headline. Recovery body
shortened.
Same five-element philosophy applied — every element on this page
reinforces "in 2 seconds, tap the left button." Everything else is
removed.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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