Skip to content

feat: streak-triggered ask-for-review strip#6084

Open
tsahimatsliah wants to merge 20 commits into
mainfrom
feat/ask-for-review-prompt
Open

feat: streak-triggered ask-for-review strip#6084
tsahimatsliah wants to merge 20 commits into
mainfrom
feat/ask-for-review-prompt

Conversation

@tsahimatsliah
Copy link
Copy Markdown
Member

@tsahimatsliah tsahimatsliah commented May 20, 2026

Summary

Inline strip rendered at the top of the post modal that asks long-streak users for a store review. Two inline steps; happy users go to the right platform's review URL, unhappy users land in the existing Feedback modal.

  • Step 1: "Enjoying daily.dev so far?" [Yes] [No] [ x ]
  • Step 2 (Yes): "Awesome! Leave a quick {Chrome Web Store|App Store|...} review" with a [Leave a review] CTA opening the platform URL in a new tab.
  • No path: routes to LazyModal.Feedback with FeedbackCategory.UxIssue prefilled.

Behavior

  • Trigger gates: logged-in, streaks enabled, streak.current >= streakThreshold, GrowthBook variant enabled, destination available for the user's platform, no streak-milestone modal pending, not yet shown this session.
  • Re-ask loop: step-1 dismissal sets localStorage[askForReview:dismissedAt] and the strip stays hidden for cooldownDays. After the cooldown elapses, the next 3-day streak shows it again. Engagement past step 1 (Leave a review, No, step-2 dismiss) is permanent via ActionType.AskedForReviewComplete.
  • Once per session: sessionStorage[askForReview:shownThisSession] prevents re-mounting on subsequent posts.
  • Platform routing (getReviewDestination):
    • Extension: Chrome / Edge / Firefox Add-ons
    • Webapp iOS Safari -> App Store
    • Webapp Android -> Play Store
    • Webapp Chrome/Edge/Brave desktop -> Chrome Web Store / Edge Add-ons
    • Webapp Firefox/Safari desktop -> X share intent (fallback)

GrowthBook

featureAskForReview = { enabled: false, streakThreshold: 3, cooldownDays: 14 }

Recommend rolling out at low percentage first: the day the flag flips on, every existing user with streak.current >= 3 becomes eligible.

Out of scope

  • Touch-1 positive-reinforcement toast after first upvote/bookmark (follow-up)
  • iOS native StoreKit bridge (uses App Store URL even inside the native webview for v1)
  • Companion overlay on third-party pages (not mounted there since BasePostContent only renders in the post page / article modal)

Test plan

  • pnpm --filter shared lint clean
  • pnpm --filter shared test clean (incl. new AskForReviewStrip, useAskForReviewVisibility, askForReview specs)
  • node ./scripts/typecheck-strict-changed.js clean
  • Open Storybook -> Components/AskForReviewStrip/Demo panel and exercise each destination + dismiss
  • Manual QA matrix (with flag temporarily defaulted true):
    • Chrome desktop webapp -> Chrome Web Store
    • Edge desktop webapp -> Edge Add-ons
    • Firefox desktop webapp -> X share intent
    • iOS Safari -> App Store URL
    • Android Chrome -> Play Store URL
    • Chrome extension newtab -> Chrome Web Store
    • Confirm strip is NOT shown when streak < 3, when already permanently completed, or within cooldown window
    • Confirm the strip does not appear on a second post in the same session
    • Confirm step-1 dismiss + advance the clock 14+ days -> strip reappears on next post with streak >= 3

Notes for reviewers

  • I verified the API completeUserAction mutation accepts an arbitrary action type: String! (packages/shared/src/graphql/actions.ts). The two-action cooldown model in the original spec was replaced with localStorage because useActions().completeAction short-circuits on duplicate completion (line 96-98), so we cannot refresh completedAt for a cooldown loop without backend changes.
  • FeedbackModal gains a defaultCategory prop; the No path passes FeedbackCategory.UxIssue.

Made with Cursor

Preview domain

https://feat-ask-for-review-prompt.preview.app.daily.dev

Adds an inline strip at the top of the post modal that asks long-streak
users for a store review (Yes routes to the right Chrome/Edge/Firefox/App
Store/Play Store URL by platform, falls back to X share for Firefox/Safari
desktop). No routes them into the existing Feedback modal with a UX-issue
category. Step-1 dismissal triggers a 14-day cooldown loop; any
engagement past step 1 is permanent. Threshold and rollout are
GrowthBook-controlled via featureAskForReview.

Covered by unit specs for the destination helper, visibility hook
(including the cooldown loop), and the strip UI. Storybook DemoPanel
exposes every state for design review.

Co-authored-by: Cursor <cursoragent@cursor.com>
@vercel
Copy link
Copy Markdown

vercel Bot commented May 20, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
daily-webapp Ready Ready Preview May 25, 2026 10:08am
storybook Error Error May 25, 2026 10:08am

Request Review

Co-authored-by: Cursor <cursoragent@cursor.com>
Adds a floating QA panel mounted in MainLayout, gated on
?ask-for-review-qa=1, that bypasses all visibility gates and lets you
trigger the real strip + real handlers on any post page. Includes
state inspector, destination override, session/cooldown reset, and
manual Feedback modal/action-complete triggers.

Co-authored-by: Cursor <cursoragent@cursor.com>
…d modal

- Portal the strip to document.body with fixed top + z-max so it sits
  above the post modal, navigation, and X button
- Simplify to a single step (Yes / No / dismiss); fill stars in yellow
- Yes now opens a centered AskForReviewConfirmModal (star cover image,
  production-style review CTA) instead of a second inline step
- No still opens the prefilled Feedback modal
- Update tests + Storybook story labels to match the new flow

Co-authored-by: Cursor <cursoragent@cursor.com>
tsahimatsliah and others added 2 commits May 20, 2026 12:41
Use the shared cover-image success modal for the review ask and tighten the floating strip so it reads as part of the post modal instead of a full-width page banner.

Co-authored-by: Cursor <cursoragent@cursor.com>
…rompt

Co-authored-by: Cursor <cursoragent@cursor.com>

# Conflicts:
#	packages/shared/src/lib/featureManagement.ts
Replace the generic cover-image review modal with the compact dark review card that mirrors the production ask, including generated rating artwork and destination-specific platform icon.

Co-authored-by: Cursor <cursoragent@cursor.com>
Make the review question participate in the post layout so it pushes modal and page content down instead of covering navigation, and clarify the Yes/No actions with stronger visual affordance.

Co-authored-by: Cursor <cursoragent@cursor.com>
Move the ask-for-review strip out of BasePostContent (where it was rendering inside the article above the title and looking like part of the content). Portal it into the post modal overlay above the modal box with a visible gap, and render it above the page container on the post page — so it reads as a separate strip sitting on top of the article, scrolling with it.

Tighten visuals and contrast: streak avatar circle with white icon, solid bun-default pill with white text for the streak label, edge-to-edge on mobile / rounded card on tablet+. Replace the "Quick question" / "Are you enjoying daily.dev?" pair with more explicit copy ("Quick check-in" + "How is daily.dev working out for you?" + a sentence that spells out what each answer does) and clearer button labels ("I'm loving it" / "Could be better").

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Add a ?force-ask-for-review=1 query param that bypasses all gates (auth, streak, feature flag, cooldown, session, destination) so reviewers can see and visually QA the strip on any post page or in the post modal without setting up a real streak or flipping the GrowthBook flag. Falls back to Chrome Web Store as the destination when the platform has no real one.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ly renders

Reverting the portal-into-overlay approach — it was the right intent (sit physically above the modal box) but in practice the strip wasn't rendering for reviewers. Put it back in BasePostContent where it mounts reliably, and let the redesigned card visuals (rounded border, surface-float background, shadow, and the new copy/contrast) carry the "this is its own thing, not part of the article" feeling instead.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Trim the strip to feel less content-heavy:
- One short question + a small streak/CTA line, no separate "Quick check-in" label or oversized streak pill.
- Smaller padding (p-3), rounded-12, shadow-1, smaller 9px avatar circle, ButtonSize.Small actions.
- Swap the orange bun palette for cabbage-subtler / cabbage-default — the streak flame icon is replaced with FeedbackIcon since the strip is about feedback, not the streak itself.

Also persist the force-show flag in localStorage so ?force-ask-for-review=1 survives navigation from the feed into the article modal (and `?force-ask-for-review=0` clears it).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…icon contrast

- Drop the em-dash and rewrite the subtitle as something a human would say: "You've shown up {N} days in a row. Worth a sec?"
- Pin both Yes and Not really buttons to the same tablet:w-28 so they line up at equal width on tablet+ (still flex-1 on mobile).
- Flip the avatar circle from cabbage-subtler with cabbage-default icon (low contrast, washed out) to a solid cabbage-default circle with a white icon — same brand color, much more visible.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…itives

Drop the hand-rolled gradient + logo + stars + platform-icon illustration in AskForReviewConfirmModal and rebuild it on the same primitives MarketingCtaModal uses — Modal (FlexibleCenter, Small), Title, Description, CTAButton from marketing/cta/common — so it matches every other campaign popup in the product instead of looking like a one-off.

Not coupling to boot's MarketingCta type because that's server-driven (campaignId + clearMarketingCta mutation) and would conflict with any real campaign in the boot cache. Reusing the visual building blocks keeps the look consistent without touching campaign state.

Also rewrites the copy: "Thanks for being a regular 💜" + "Mind leaving us a quick review on {platform}? It helps other devs find daily.dev." with a "Leave a review" CTA.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Make the confirm modal store-aware so the wording matches the store the user
will land on:

- ReviewDestination gains headline / body / ctaText / image so each store
  (Chrome Web Store, Edge Add-ons, Firefox Add-ons, App Store, Play Store,
  X share fallback) owns its own copy.
- AskForReviewConfirmModal now reads those fields and renders a CardCover
  hero image above the CTA — matching MarketingCtaModal layout.
- Added askForReviewPlaceholderImage in lib/image.ts as a temporary single
  asset for all destinations. Engineering will swap per-store images later.

Tightened the device routing tests:
- iPad Safari → App Store
- Chrome on iOS (CriOS) → App Store
- Sanity-check that the active destination carries store-specific
  headline / body / ctaText / image.

Full matrix (already covered + new) is now:
  Extension Chrome → Chrome Web Store
  Extension Edge   → Edge Add-ons
  Extension Firefox→ Firefox Add-ons
  iOS Safari       → App Store
  iOS Chrome       → App Store
  iPad Safari      → App Store
  Android Chrome   → Play Store
  Desktop Chrome/Brave → Chrome Web Store
  Desktop Edge     → Edge Add-ons
  Desktop Safari/Firefox → X share fallback

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
tsahimatsliah and others added 2 commits May 25, 2026 12:29
- Remove the ?force-ask-for-review URL flag and forceShow paths from
  useAskForReviewVisibility. It bypassed every gate (auth, streak, GB,
  cooldown, completed action) and persisted in localStorage forever — not
  safe to ship. Internal testing should use the existing ?ask-for-review-qa=1
  panel.
- Lazy-load AskForReviewQAPanel via next/dynamic + webpackChunkName so the
  ~300 LOC QA module is no longer parsed on every MainLayout mount.
- Add AskForReviewConfirmModal.spec.tsx (6 tests): destination copy renders,
  CTA href + target=_blank, hero image alt, click-through marks action
  complete + closes, dismiss writes cooldown + closes, App Store path.
- Move isAndroidUserAgent from askForReview.ts into lib/func.ts as
  `isAndroid`, sitting next to `isIOS` for consistency.
- Compute isCooldownActive once in the visibility hook and reuse for both
  the gate and the return value.
- Add why-comments at the strip mount point (placement choice) and at the
  CTA handler (action marked complete on click-through, not actual review
  submission — click-through is the engagement signal we re-ask against).
- Rename confirm modal close aria-label to "Dismiss review prompt" so it
  doesn't collide with the drawer's built-in Close button in tests.

37 tests passing in the ask-for-review surface (was 31). Strict typecheck
clean for changed files.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…rompt

# Conflicts:
#	packages/shared/src/lib/featureManagement.ts
The story duplicated what the in-app QA panel
(?ask-for-review-qa=1) already provides — switching destinations and streak
values to preview every state — except the QA panel runs against real auth,
streak, and GrowthBook context instead of mocks.

Storybook in this repo is for design-system primitives (Tooltip, Dropdown,
FormWrapper, etc.), not for one-off feature components behind a flag.
Removing the story drops 226 LOC of mock scaffolding, eliminates a parallel
fixture to maintain alongside the QA panel, and unblocks the failing Vercel
storybook check.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The QA panel was a dev-only test scaffold but it lived in the shared library
and got mounted by MainLayout for every visitor. Removing it entirely:

- Delete AskForReviewQAPanel.tsx (~349 LOC) and its MainLayout mount.
- Drop getQAOverride / setQAOverride / AskForReviewQAOverride / the
  ASK_FOR_REVIEW_QA_KEY storage key from lib/askForReview.ts.
- Drop the dev-only clearDismissedAt / clearShownThisSession helpers and
  the unused getDestinationById lookup table.
- Strip qa-* gate bypasses from useAskForReviewVisibility — the hook now
  evaluates the production gates only.
- Drop the spec line for clearDismissedAt.

If we need to manually preview the strip during development, the path is to
flip ask_for_review in GrowthBook dev mode or seed the streak/action state
via the existing test infra — not to ship a QA UI to every user.

Net: -461 / +6 LOC. 36 tests still passing, strict typecheck clean.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant