Skip to content

feat(onboarding): persona-driven swipe onboarding#5991

Open
davidercruz wants to merge 10 commits into
mainfrom
davidercruz/swipe-personas
Open

feat(onboarding): persona-driven swipe onboarding#5991
davidercruz wants to merge 10 commits into
mainfrom
davidercruz/swipe-personas

Conversation

@davidercruz
Copy link
Copy Markdown
Contributor

@davidercruz davidercruz commented May 5, 2026

Changes

Combines the swipe onboarding flow (#5865) with the persona picker (#5939) and removes the user-facing free-text prompt. Users now pick up to three personas; the prompt sent to onboardingDiscoverPosts is built silently from the persona titles + tags + the user's experienceLevel from registration. The "Show popular posts" fallback is preserved for users who skip persona selection.

Implementation notes: PersonaSelector gains a mode: 'follow' | 'seed' prop so the swipe path can read the selection without batch-following tags upfront. The pre-swipe panel is powered by SwipePersonaIntro, and /onboarding/swipe redirects back to /onboarding while preserving query params.

Events

Type event_name value
New select onboarding persona target_type: 'persona', target_id: <persona id>, `extra: { action: 'select'

Experiment

Yes — the swipe flow remains gated by the existing swipe_onboarding GrowthBook flag (default true in code, controlled at the experiment level). No new experiment introduced in this PR.

Manual Testing

Caution

Please make sure existing components are not breaking/affected by this PR

On those affected packages:

  • Have you done sanity checks in the webapp?
  • Have you done sanity checks in the extension?
  • Does this not break anything in companion?

Did you test the modified components media queries?

  • MobileL (420px)
  • Tablet (656px)
  • Laptop (1020px)

Did you test on actual mobile devices?

  • iOS (Chrome and Safari)
  • Android

Preview domain

https://davidercruz-swipe-personas.preview.app.daily.dev

@vercel
Copy link
Copy Markdown

vercel Bot commented May 5, 2026

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

Project Deployment Actions Updated (UTC)
daily-webapp Ready Ready Preview May 20, 2026 1:52pm
1 Skipped Deployment
Project Deployment Actions Updated (UTC)
storybook Ignored Ignored May 20, 2026 1:52pm

Request Review

Combines the swipe onboarding flow with the persona picker and removes
the user-facing free-text prompt. Users now pick up to three personas;
the prompt sent to onboardingDiscoverPosts is built silently from the
persona titles + tags + the user's experienceLevel from registration.
The "Show popular posts" fallback is preserved.

- PersonaSelector gains a 'seed' mode that emits onSelectionChange
  instead of immediately following persona tags (the legacy 'follow'
  mode is unchanged for the EditTag flow).
- New buildSwipePrompt utility composes a deterministic prompt string
  from personas + experience level, with unit-test coverage.
- Shared SwipePersonaIntro component renders the pre-swipe panel for
  both the funnel step and the /onboarding/swipe preview.
- featureManagement.ts skip-listed for strict typecheck while the
  bundler-resolution / @growthbook exports issue is addressed
  separately.
Right-swipes (accepts) tick swipesCount toward the 10-post unlock; left
swipes still feed handleAdaptiveSwipe so the deck keeps learning the
dislike signal but no longer move the bar or trigger the milestone burst.
Co-authored-by: Cursor <cursoragent@cursor.com>
@tomeredlich
Copy link
Copy Markdown
Contributor

check pr @claude

@claude
Copy link
Copy Markdown
Contributor

claude Bot commented May 14, 2026

Claude finished @tomeredlich's task in 4m 50s —— View job


PR Review — feat(onboarding): persona-driven swipe onboarding

  • Read PR diff and key files
  • Review modified core files
  • Review new webapp files
  • Check CLAUDE.md compliance
  • Post review

Overall: the persona → swipe handoff and the mode: 'follow' | 'seed' split look solid, and the new tests for PersonaSelector and buildSwipePrompt are well-scoped. The blockers are mostly around dead code and a couple of out-of-scope changes that should be split out before merge.


🔴 Issues

1. Dead onboardingPopBus infrastructure — no subscribers exist
packages/shared/src/components/onboarding/onboardingPopBus.ts exports subscribePersonaSelection/subscribeRecommendRequest, and PersonaSelector.handleClick calls broadcastPersonaSelection + broadcastRecommendRequest on every select. A repo-wide grep finds zero subscribe* callers. The bus is shipping unused — either wire it up to whatever was supposed to listen, or delete the module + the two broadcast calls in PersonaSelector.tsx:107,111. CLAUDE.md: "Delete dead code… Remove unused exports."
Fix this →

2. milestoneBurstKey is a dead prop
SwipeOnboardingProgressHeader.tsx:11 declares it on the props type but the function body (SwipeOnboardingProgressHeader.tsx:14-16) destructures only progressCount. The parent in FunnelSwipeOnboardingStep.tsx:248,284,474 allocates state, increments it on every milestone crossing, and threads it down for no observable effect. Either implement the burst animation it implies, or drop the state + prop entirely.

3. EditTag.headlineClassName is added but never consumed
packages/shared/src/components/onboarding/EditTag.tsx:23,31,56 adds an optional headlineClassName prop. The only caller (packages/webapp/components/onboarding/OnboardingV2.tsx:1274) doesn't pass it. Drop the prop unless a downstream use is coming in a follow-up PR.

4. packages/webapp/pages/onboarding/swipe.tsx doesn't match the PR description
The PR body says "the standalone /onboarding/swipe preview" is shared via SwipePersonaIntro, but swipe.tsx is just a getServerSideProps redirect to /onboarding. Either it should render the preview directly, or the description should be updated — currently the route exists only to redirect, which adds a server hop on a path users won't reach organically.

5. Out-of-scope global gradient change
packages/shared/src/styles/base.css:883-891 widens both .bg-gradient-funnel-default and .bg-gradient-funnel-top (22.49% → 38%, 100% → 135%). This is a global token used across the funnel, not a swipe-onboarding-specific surface. Per CLAUDE.md "Prefer component-level token swaps for one-off contrast fixes… avoid changing global tokens in base.css unless explicitly requested," this should either move to a component-scoped override or be split into its own PR with a screenshot of every funnel step it affects.

6. Out-of-scope MainLayout.tsx reshuffle
packages/shared/src/components/MainLayout.tsx moves the isPageReady declaration up by a few lines with no behavioral change. Not related to persona swipe — please drop the hunk to keep this branch's diff focused.


🟡 Recommendations

7. useConditionalFeature evaluated without shouldEvaluate
packages/webapp/pages/onboarding.tsx:307-309 evaluates swipeOnboardingFeature unconditionally on every onboarding visit. CLAUDE.md guidance: "Use useConditionalFeature with shouldEvaluate to gate evaluation." Gate on isAuthReady && !!funnelState so anonymous/unsettled visitors don't trigger GrowthBook evaluations on the funnel entry page.

8. retryFetch() loses original prompt + initialTags
packages/webapp/hooks/useAdaptiveSwipeDeck.ts:181-183 defines retryFetch = () => startDeck() with no options. If the very first startDeck({ prompt, initialTags }) fails and onOnboardingFeedRetry fires, the retry call goes out with empty prompt/selectedTags — effectively falling back to popular even though the user already picked personas. Cache the initial options in a ref and replay them on retry.

9. rightSwipedPostIds returned from useAdaptiveSwipeDeck is never consumed
packages/webapp/hooks/useAdaptiveSwipeDeck.ts:60,67,306 track and expose a Set<string> of right-swiped post IDs, but FunnelSwipeOnboardingStep doesn't read it (bookmarking is handled via getBookmarkablePost + toggleBookmark). Remove the state to avoid an unnecessary re-render path on every right swipe.

10. buildSwipePrompt will produce awkward strings for some persona titles
packages/webapp/lib/buildSwipePrompt.ts:20-23 always appends engineer after the role name. If a persona ships as "Site Reliability Engineer" or "ML Engineer", the prompt becomes "I'm a site reliability engineer engineer". Worth normalizing the role label (strip a trailing "engineer", or have the backend return a clean role noun on the persona).

11. HotAndColdModal.tsx is now 2,511 lines
The diff adds ~1,400 lines to a single component that already mixes the hot-takes voting flow with the new onboarding swipe deck (isOnboardingMode branches throughout). Even on a startup pace, this hits the readability bar in CLAUDE.md ("Writing Readable Hooks" / single-purpose helpers). Consider extracting OnboardingPostCard, OnboardingFeedEmptyState, OnboardingCardBehindParticles, and the intro animation into their own files. Not a merge blocker, but the next swipe-onboarding change will be painful otherwise.

12. Inline <style>{}</style> blocks
FunnelSwipeOnboardingStep.tsx:178-226,412-459, SwipePersonaIntro.tsx:213-249, and similar blocks inside HotAndColdModal.tsx inject @keyframes into the DOM at render time. With strict mode (double renders) and multiple deck instances, this multiplies stylesheet inserts. Move these keyframes to a shared CSS file or Tailwind config — they're not dynamic, so there's no reason to inject them per-mount.


🟢 Nits

13. Origin.OnboardingPersona = 'onboarding persona'
packages/shared/src/lib/log.ts:57 uses a space, while neighboring entries (ManageTag = 'manage_tag', EditTag = 'edit_tag', Onboarding = 'onboarding') use snake_case. There are existing space-separated values too ('collection modal'), but the local cluster is consistent — recommend 'onboarding_persona' to match.

14. headline default duplicates the title in OnboardingV2.tsx
packages/webapp/components/onboarding/OnboardingV2.tsx:1277 passes headline="Pick tags that are relevant to you", which is also the default inside EditTag.tsx:59. The explicit prop can be dropped.

15. Test mock doesn't cover the new AlertDot import in MyFeedHeading.spec.tsx
You already mock it correctly (MyFeedHeading.spec.tsx:51-56) — just confirming this is hit by the new "tags reminder" cases.


✅ What's good

  • mode: 'follow' | 'seed' split on PersonaSelector is the right abstraction; the seed path correctly avoids batch-following tags and lets the swipe deck consume the selection.
  • buildSwipePrompt.spec.ts covers the experience-level + dedupe + empty-personas branches.
  • PersonaSelector.spec.tsx exercises the multi-select cap, deselect, and broadcast paths.
  • The dismissedOnboardingCardIds controlled/uncontrolled fork on HotAndColdModal (lines 1719-1734) is implemented cleanly and lets the parent persist dismissals across mode switches.
  • feat(swipe): only advance progress on accepted posts (commit 8280f53) is a good fix — progress tied to right-swipes only matches the unlock semantics.
    • Branch: davidercruz/swipe-personas

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
tomeredlich and others added 2 commits May 20, 2026 13:01
Restore the popular-posts fallback when adaptive discovery fails, move progress above the cards with a smooth transition to 100%, and tighten completion CTA layout and copy.

Co-authored-by: Cursor <cursoragent@cursor.com>
Keep tablet and desktop on the previous swipe onboarding structure while
applying the new top bar, progress slot, persona list, and fixed footer on
small screens only.

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

@claude check pr

@claude
Copy link
Copy Markdown
Contributor

claude Bot commented May 20, 2026

Claude finished @tomeredlich's task in 3m 50s —— View job


PR Review (round 2) — feat(onboarding): persona-driven swipe onboarding

  • Compare against round-1 review and confirm fixes
  • Review code added since round 1 (a485b85, 9535f7f, 49f160d, a57e4bd)
  • Post updated review

Fixes confirmed since the previous review ✅

Most round-1 findings are addressed in cdd94a0 and a485b85:


🔴 Issues still open / introduced since round 1

1. Dead exports in swipeOnboardingGuidance.ts
getSwipeOnboardingHeadline and getSwipeOnboardingGuidanceMessage (packages/webapp/lib/swipeOnboardingGuidance.ts:37,67) are only referenced from their own spec — no production caller. The constants (SWIPE_ONBOARDING_MIN_TO_UNLOCK, SWIPE_ONBOARDING_REFINE_TARGET) are used, but the two functions and the unitWord helper are not. CLAUDE.md: "Delete dead code… Remove unused exports." Either wire them into the UI or drop them along with the matching tests.
Fix this →

2. HotAndColdModal.tsx is now 2,534 lines
This was a round-1 recommendation (then 2,511 lines). The most recent commits added another ~57 lines of mobile/tablet branching (e.g., HotAndColdModal.tsx:2235-2351) on top of the already-mixed hot-takes + onboarding logic. The isOnboardingMode branching now spans 16+ sites in the file. Extracting OnboardingPostCard, OnboardingFeedEmptyState, OnboardingCardBehindParticles, the intro animation, and the onboarding swipe-area layout into their own files is overdue. Not a merge blocker, but the next change here will compound the pain.

3. Multi-persona prompt drops "engineer" for engineering titles
buildSwipePrompt.ts:11-12,22-26 strips a trailing "engineer" before formatting. For a single persona the suffix is re-added in the template (I'm a ${roles[0]} engineer). In the multi-persona branch (I work across ${roles.join(' and ')}) it isn't, so ["Site Reliability Engineer", "Frontend"] becomes "I work across site reliability and frontend" — "site reliability" reads as a noun rather than a role. Two safer options:

  • Strip the suffix only in the single-persona path, and add a normalized "role + engineer" join in the multi-persona path; or
  • Have the backend return a clean role noun on the persona so the FE doesn't infer.
    The new spec (buildSwipePrompt.spec.ts:59-71) only covers the single-persona case — worth adding a multi-persona case before locking in the normalization.

4. pages/onboarding/swipe.tsx still just redirects
Flagged in round 1 (#4). The PR description says the standalone preview is rendered via SwipePersonaIntro, but swipe.tsx:3-15 is a getServerSideProps redirect to /onboarding. If this is intentional backwards-compat for old inbound links, update the PR body — otherwise the route adds a server hop on a path users won't reach organically. Either way, mismatch between description and behavior should be reconciled before merge.

5. Inline <style>{}</style> blocks still injected per-mount
Still present in FunnelSwipeOnboardingStep.tsx:213-272,446-493, SwipePersonaIntro.tsx:229-265, and SwipeOnboardingProgressHeader.tsx:121-131. Under strict-mode double renders and multiple deck mounts (back-and-forth between intro/swipe view), these keyframes are re-inserted into the DOM repeatedly. Move them to a shared CSS file in packages/shared/src/styles/ (or near the components) since none of the rules are dynamic.


🟡 Smaller things to consider

6. useAnimatedLoadingLabel's 550ms setInterval keeps ticking after the loading state finishes
FunnelSwipeOnboardingStep.tsx:126-157 — the cleanup path correctly clears the interval when isActive becomes false, but on every label change the component re-renders just to swap the displayed string. That's fine while loading; just worth noting that displayPercent and useAnimatedProgressPercent in SwipeOnboardingProgressHeader.tsx:52-95 both run RAF-driven animations on mount. With strict-mode double-mount, you'll get two RAF cycles per swipe. Consider guarding with if (startPercent === targetPercent) return; (already done — good), but verify there's no perceptible jitter.

7. SwipePersonaIntro RootPortal for the mobile next-button bar
SwipePersonaIntro.tsx:285-289 portals a fixed-bottom action bar. When this component is inside HotAndColdModal (which itself portals into a modal root), the portal target should still resolve fine — but worth a sanity check on iOS Safari since position: fixed + env(safe-area-inset-bottom) can sit incorrectly behind the keyboard when a future variant adds a text field.

8. swipeOnboardingPopularDeck.ts recursion
packages/webapp/lib/swipeOnboardingPopularDeck.ts:52-86 recurses up to MAX_PAGES=12 (~600 posts) sequentially. That's fine in absolute terms, but if the GraphQL endpoint is slow, the user can be staring at the loader for a long time without a timeout. Consider a Promise.race against an abort signal, or short-circuit once strictPosts.length >= STRICT_MIN_BEFORE_RELAXED rather than TARGET_STRICT_COUNT.


✅ Net assessment

Round-1 blockers are essentially resolved. The remaining items are quality-of-life and one truly dead-code finding (getSwipeOnboardingHeadline/getSwipeOnboardingGuidanceMessage). Recommend addressing #1 (and ideally #3) before merge; #2 is worth scheduling as a follow-up.
· Branch: davidercruz/swipe-personas

Remove unused swipe guidance copy helpers, fix multi-persona prompt
engineer stripping, and move onboarding keyframes into shared CSS.

Co-authored-by: Cursor <cursoragent@cursor.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.

2 participants