Skip to content

feat(share): celebrations, achievements & game center share (PR 13)#6360

Draft
tsahimatsliah wants to merge 3 commits into
claude/website-sharing-visibility-be6b32from
claude/share-celebrations
Draft

feat(share): celebrations, achievements & game center share (PR 13)#6360
tsahimatsliah wants to merge 3 commits into
claude/website-sharing-visibility-be6b32from
claude/share-celebrations

Conversation

@tsahimatsliah

@tsahimatsliah tsahimatsliah commented Jul 22, 2026

Copy link
Copy Markdown
Member

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

Adds share/download affordances to the celebration + achievement surfaces, all behind one flag. Control is pixel-identical to the PR 1 branch.

Flag

share_celebrations (default false, appended at the end of featureManagement.ts).

Every surface gates on useShareCelebrations(), which requires both the initiative's master kill-switch (sharing_visibility) and share_celebrations, evaluated through useConditionalFeature with shouldEvaluate so GrowthBook is only hit when the surface would actually render. useShareCelebrations.spec.tsx asserts the full truth table (off/off, off/on, on/off, on/on, opted-out).

Per-item status

#13 — per-achievement image + Download/Share (headline) — shipped, with one backend dependency

  • New page packages/webapp/pages/image-generator/achievement/[...params].tsx (/image-generator/achievement/<userId>/<achievementId>), following the existing screenshot pattern exactly: getStaticPaths with fallback: 'blocking', revalidate: 60, and a single id="screenshot_wrapper" wrapper — same shape as devcards/[userId].tsx and badges/[badgeId].tsx. No OG-image library was added; there is none in this repo.
  • It fetches the profile (getProfile) and achievements (getUserAchievements) server-side and 404s for a locked achievement, so the page can never render a card claiming an achievement the user does not have.
  • It renders the new AchievementShareCard — a static, self-contained render (badge art, name, description, points, rarity tier glow, owner, unlock date, daily.dev logo) with no viewport/hover/client-only dependencies.
  • Backend dependency: the screenshot service does not know this route yet, so there is no generated PNG URL to hand out. Until it does, the Download button downloads the achievement's own badge image (achievement.image) — which works today with no backend change. Once the service learns /image-generator/achievement/<userId>/<achievementId> and exposes the resulting URL on the achievement payload, Download can point at the richer card with a one-line change. The frontend renders the screenshot_wrapper page regardless.
  • Download + Share are wired via the new AchievementShareActions component into:
    • AchievementCard.tsxopt-in via a new shareUser prop. Only AchievementsList (the achievements tab) passes it, so the card's other consumers (Game Center, AchievementsWidget, ProfileAchievementShowcase hover card, AchievementTrackerButton ×2) keep their current DOM and get no share control. Only unlocked achievements get the row.
    • AchievementShowcaseModal.tsx — the share control is rendered as a sibling of the row toggle, never inside it (a <button> nested in a <button> is invalid and would swallow row selection). With the flag off the row renders inside a React.Fragment, i.e. byte-identical DOM.

#12 — share on celebration completion — partially shipped, quests deferred

Covered, each with a deliberate copy choice:

Moment Treatment Why
Achievement unlocked (AchievementCompletionModal, celebrate phase) Labelled Download badge + share, placed directly under the +N points chip — the visual peak, before "Choose next goal" The only celebration with a real artifact (badge art) and a public destination (the profile achievements tab). Deserves the heaviest treatment.
Streak milestone (NewStreakModal) "Share your streak" + a share icon; copy-text flavoured — the milestone lives in the share text, the link is just daily.dev A streak has no per-user public page and no per-user image, so linking a profile URL would be a dead end. The brag is the text.

Deferred: quest / task claim and level-up. Reasons, stated plainly rather than shipped half-done:

  1. There is no completion modal. Quest claims celebrate inline inside QuestButton.tsx (~1250 lines) via a flying-reward particle layer plus a level-up firework overlay, rendered from two places (the header button and the panelOnly daily-quests page) — a share row bolted in there would double-render or fight the animation, exactly the composition trap this batch was warned about.
  2. There is no public artifact or destination: /daily-quests is ProtectedPage + noindex, so anyone who followed the link would hit a login wall.

Doing it properly means a real completion surface, which is a product decision, not a wiring change. Flagged rather than faked.

#17 — page-level share on profile → achievements — shipped

ProfileAchievements.tsx gets one share control next to the points/count line ("Share achievements"). It only evaluates the flag once achievements have loaded, are non-empty, and the profile has a username. Per-card controls use a distinct label ("Share this achievement"), so the page never presents two identically-labelled affordances.

#16 — page-level share on Game Center — skipped: the page is not public

Verified before building anything: packages/webapp/pages/game-center/index.tsx wraps its whole tree in <ProtectedPage> and its SEO sets noindex: true / nofollow. Anyone receiving a Game Center link would hit an auth wall, so a share button there would ship a broken promise. Skipped deliberately, per the plan's instruction to verify first. If Game Center is ever made public, the same ShareActions + Origin.GameCenter (already added in PR 1) drop in as a two-line change.

Composition-trap audit

Every render site of every touched component was read. Exactly one share control per surface:

  • /[userId]/achievements: one page-level control (ProfileAchievements) + one per unlocked card (AchievementsListAchievementCard). No nesting, distinct labels.
  • /[userId]: ProfileAchievementShowcase renders AchievementCard without shareUser → no control; the showcase edit modal (owner-only) has one per row.
  • Game Center, AchievementsWidget, AchievementTrackerButton: no shareUser → unchanged.
  • AchievementShowcaseModal does not compose AchievementCard (it builds its own rows), so there is no double render.

Verification

  • node ./scripts/typecheck-strict-changed.js — pass
  • pnpm --filter shared lint — pass
  • pnpm --filter webapp lint — pass
  • NODE_ENV=test pnpm --filter shared test298 suites / 2004 tests pass
  • NODE_ENV=test pnpm --filter webapp test44 suites / 297 tests pass
  • tsc --noEmit on the storybook package reports no errors in the new story (the package has a pre-existing strict backlog elsewhere)
  • build not run, per the batch instructions

New tests assert:

  • copy → navigator.clipboard.writeText and the "Copied link to clipboard" toast
  • mobile → navigator.share (native path), and that clipboard is not used in that case
  • exactly one download + one share control per surface (card, page, streak modal)
  • flag-off preserves the current UI exactly on the card, the page and the streak modal, plus the real two-flag gate in useShareCelebrations.spec.tsx

Storybook

packages/storybook/stories/components/AchievementShare.stories.tsx covers the unlocked card with share, the same card with the flag off, a locked card, the labelled celebration pair, and the generated share image in both a normal and a rarest (emerald) tier.

Note for reviewers: the repo-wide Storybook GrowthBook mock (mock/gb.ts) does return value || 'control', so a flag defaulting to false resolves to the truthy string 'control' and always reads as ON. The flag-off story therefore holds FeaturesReadyContext as not ready, which is the only honest way to render control in Storybook. Jest is unaffected and is the real flag-off guarantee.

🤖 Generated with Claude Code

Preview domain

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

Adds per-achievement share/download, a peak-moment share on the two
celebration modals, and a page-level share on the profile achievements
tab — all behind `share_celebrations` + the `sharing_visibility` master
kill-switch. Control renders every surface exactly as it does today.

- add the achievement image-generator page (screenshot_wrapper + ISR)
  and the AchievementShareCard it renders
- add AchievementShareActions (badge download + ShareActions) and wire it
  into AchievementCard (opt-in prop), the unlock modal and the showcase
  picker
- add a share row to the streak milestone modal
- add a page-level share to ProfileAchievements
- add share_celebrations flag, useShareCelebrations gate, log events

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:28pm
storybook Building Building Preview Jul 22, 2026 7:28pm

Request Review

Initiative-wide normalization: one event per entity. Page-level achievement
shares reuse ShareAchievement (target_type/target_id already distinguish
page vs card); the streak milestone share reuses the generic ShareLog with
target_type StreaksMilestone carrying the milestone context.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Without it, target_id silently switched meaning (achievement id vs user id)
under one event name. Matches the ShareProfile event shape.

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