Skip to content

feat(share): share my streak from the reading streak popup#6358

Draft
tsahimatsliah wants to merge 2 commits into
claude/website-sharing-visibility-be6b32from
claude/share-streak
Draft

feat(share): share my streak from the reading streak popup#6358
tsahimatsliah wants to merge 2 commits into
claude/website-sharing-visibility-be6b32from
claude/share-streak

Conversation

@tsahimatsliah

@tsahimatsliah tsahimatsliah commented Jul 22, 2026

Copy link
Copy Markdown
Member

Part of the sharing visibility initiative (PR 12 / issue #9). Stacked on PR 1 — base branch is claude/website-sharing-visibility-be6b32, not main.

What changed

Adds a "Share streak" action to the existing bottom action row of the reading-streak popup (ReadingStreakPopup), next to Settings. The popup renders in three places (header tooltip, sidebar popover, mobile drawer) and all three pick it up from the single insertion point.

  • useShareStreak (packages/shared/src/hooks/streaks/useShareStreak.ts) — short-url resolution via useGetShortUrl, native share on mobile, clipboard + toast on desktop (useCopyLink). The image path is separable: when an imageUrl is supplied and navigator.canShare({ files }) returns true, the native sheet carries the PNG; otherwise it degrades to link + text. canShare is feature-detected (missing implementation, fetch failure, non-ok response and a rejected share() all fall through to the link share).
  • ShareStreakButton — icon-only + tooltip on desktop, icon + label on mobile. type="button", aria-label, keyboard-operable. Share copy is streak-length aware (getStreakShareText): plain at <30 days, "the habit stuck" at 30+, "not stopping now" at 100+.
  • StreakShareCard + packages/webapp/pages/image-generator/streak/[userId].tsx — the id="screenshot_wrapper" ISR page, following image-generator/devcards/[userId].tsx and badges/[badgeId].tsx exactly. Data comes from a new public USER_STREAK_PROFILE_QUERY (userStreakProfile(id) { current max total }).
  • share_streak flag appended at the end of featureManagement.ts.

Streak data source: the popup already receives the typed UserStreak (current, max, total, weekStart, lastViewAt) — the share copy uses streak.current, no invented fields. The share link is user.permalink (the profile), tagged with the existing ReferralCampaignKey.ShareProfile. Logged via the existing LogEvent.ShareLog with Origin.ReadingStreak (added in PR 1 — not duplicated) plus provider, with_image and streak in extra.

Composition trap avoided: the popup is rendered inside an interactive Tippy tooltip with onClickOutside that closes it. Putting PR 1's ShareActions popover in here would portal its content outside the tooltip and the outside-click handler would close the popup underneath. So this surface uses the same underlying hooks as ShareActions (short-url + native share + copy) with a single-action button instead of a nested popover.

Zero streak: the action is hidden when current === 0 — there is nothing to be proud of yet, and hiding it also keeps GrowthBook from evaluating on users who'd never see it (shouldEvaluate).

Flag

share_streak, default false, appended at the very end of featureManagement.ts. Gated on that flag AND useSharingVisibility() (the initiative master kill-switch), both via useConditionalFeature with shouldEvaluate.

Flag off is pixel-identical to the base branch: no wrapper element is injected and the Settings button keeps its exact original class string (w-full on mobile / ml-auto on desktop). Asserted in ReadingStreakPopup.spec.tsx.

Image: page shipped, share path deferred on backend

The screenshot_wrapper page ships — the frontend half is done and the card renders. What is deferred is the image actually riding along in a share, because two backend pieces are missing:

⚠️ Backend dependency

  1. The screenshot service must learn the route /image-generator/streak/[userId] (same mechanism as devcards and top-reader badges).
  2. The API must expose the resulting image URL on the streak entity (e.g. userStreak.image, mirroring topReaderBadge.image). There is no client-side way to produce an image URL today — the badge/devcard flows all read a backend-generated URL.
  3. userStreakProfile(id) returns the UserStreak type so current is schema-valid, but existing call sites only select max/total. Please confirm current is served for the by-id resolver (and decide whether it should be privacy-gated).

Once (1) and (2) land this is a one-line wiring: pass that URL as imageUrl to ShareStreakButton. The file-share path is already implemented, feature-detected and covered by tests.

Verification

  • node ./scripts/typecheck-strict-changed.js — pass
  • pnpm --filter shared lint — pass
  • pnpm --filter webapp lint — pass
  • NODE_ENV=test pnpm --filter shared test — 296 suites / 1994 tests pass
  • NODE_ENV=test pnpm --filter webapp test — 44 suites / 297 tests pass
  • No build run, per instructions.

Tests assert: desktop copy → clipboard + toast; mobile → navigator.share with text + link; image attached when canShare({ files }) is true; fallback to link + text when canShare returns false and when canShare is missing entirely; flag-off preserves the current UI (no share control, no injected wrapper, original Settings classes); zero-streak hides the action.

Storybook: Components/Share/ShareStreakButton (short / long / milestone / mobile-with-label / with-image) and Components/Share/StreakShareCard (the three streak states). Note the repo-wide Storybook GrowthBook mock returns the truthy string 'control' for falsy defaults, so a "flag off" story would be misleading — Jest is the real flag-off guarantee here.

What I'd A/B

  1. Copy vs image — the headline test. Arm A: link + text only (what ships today). Arm B: same copy with the generated streak card attached on mobile. Metric: shares started → shares completed, and referred signups per share. Hypothesis: the image lifts completion on mobile but the link share out-converts on desktop where no image can ride along.
  2. Copy tiering — flat copy vs the streak-length tiers in getStreakShareText. Cheap to test, and it isolates whether the "the habit stuck" framing actually reads as bragging-with-taste or as a badge generator.
  3. Placement — action row (this PR) vs a milestone-triggered prompt at 7/30/100 days. The action row is always-available but low-salience; the milestone moment is higher intent but interrupts.

🤖 Generated with Claude Code

Preview domain

https://claude-share-streak.preview.app.daily.dev

Adds a "Share streak" action to the existing bottom action row of the
reading-streak popup, behind `share_streak` + the sharing-visibility
master gate. Flag off is pixel-identical to the base branch.

- add useShareStreak: short-url + native share, with a separable image
  path that feature-detects navigator.canShare({ files }) and degrades
  to link + text when files are unsupported or the image is missing
- add ShareStreakButton with streak-length-aware share copy
- render the streak image page (screenshot_wrapper) + StreakShareCard so
  the backend screenshot service only has to learn the route
- add share_streak flag, Storybook stories and tests

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@vercel

vercel Bot commented Jul 22, 2026

Copy link
Copy Markdown

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

Project Deployment Actions Updated (UTC)
daily-webapp Ready Ready Preview Jul 22, 2026 7:04pm
storybook Building Building Preview Jul 22, 2026 7:04pm

Request Review

Use the milestone target shape (StreaksMilestone/currentStreak) so streak
shares union with the celebrations-modal events, and evaluate share_streak
only once sharing_visibility passes.

Co-Authored-By: Claude Opus 4.8 <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