Skip to content

feat(reports): default health share links to timed expiry - #2150

Merged
Asherlc merged 4 commits into
mainfrom
Asherlc/health-story-sharing
Jul 27, 2026
Merged

Asherlc merged 4 commits into
mainfrom
Asherlc/health-story-sharing

Conversation

@Asherlc

@Asherlc Asherlc commented Jul 27, 2026

Copy link
Copy Markdown
Owner

Summary

  • Web and mobile health-report share buttons now default to a 7-day link and let users choose 7, 30, or 90 days before creating the share.
  • The chosen expiresInDays is sent to healthReport.generate; success copy (web) and the native share message (mobile) include the server-returned expiresAt.
  • Adds focused docs/TDD plan for this privacy-first slice; closes tracking issue Health report share links: default 7-day expiry with 7/30/90 choice #2055.

Test plan

  • Web: open weekly/monthly report, confirm 7 days is selected by default, share, and verify clipboard status includes the expiry date.
  • Web: choose 30 and 90 days, share again, and confirm Shared Reports shows matching expiries.
  • Mobile: open Reports, confirm the same 7/30/90 controls and that the share sheet message includes the expiry line.
  • Confirm permanent API links remain available only by omitting/null expiresInDays (clients always send a duration).
  • CI: unit + mobile share-button tests pass.

Summary by cubic

Defaulted health-report share links to timed expiry. Web and mobile now create links that expire after 7 days by default, with a user choice of 7/30/90 days and clear expiry messaging.

  • New Features
    • Share buttons send expiresInDays (7/30/90; default 7) to healthReport.generate.
    • Web shows “Link copied · expires {date}” using formatDateMedium from @dofek/format; mobile share message adds an “Expires {date}” line.
    • Centralized expiry options and default in dofek-server/health-report-share-expiry; web radio name/ids are unique per instance so multiple buttons stay independent.
    • Updated Storybook mocks and tests (web/mobile/server); improved RN test setup; docs in docs/health-report-sharing.md; permanent links remain API-only by omitting/null expiresInDays (clients always send a duration).

Written for commit 74f5164. Summary will update on new commits.

Review in cubic

Web and mobile share buttons now send expiresInDays (7/30/90, default 7) so new links are time-limited, and success/share copy surfaces the server-returned expiresAt.

Co-authored-by: Cursor <cursoragent@cursor.com>
Copilot AI review requested due to automatic review settings July 27, 2026 02:37
@cursor

cursor Bot commented Jul 27, 2026

Copy link
Copy Markdown

Bugbot is not enabled for your account, so this pull request was not reviewed.

Enable Bugbot in the Cursor dashboard to get automatic reviews on future PRs.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@codereviewbot-ai

Copy link
Copy Markdown

🤖 Review skipped: Repository rate limit exceeded. Free accounts are limited to 2 reviews per 4 hours per repository. Upgrade to a paid plan for unlimited reviews.

@coderabbitai

coderabbitai Bot commented Jul 27, 2026

Copy link
Copy Markdown

Warning

Review limit reached

@Asherlc, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 4 seconds

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 9e2a5cff-058f-450e-a5e1-08d620d2dbcb

📥 Commits

Reviewing files that changed from the base of the PR and between 4b657e5 and 74f5164.

📒 Files selected for processing (12)
  • docs/health-report-sharing.md
  • docs/superpowers/plans/2026-07-26-health-report-share-expiry.md
  • packages/mobile/components/HealthReportShareButton.stories.tsx
  • packages/mobile/components/HealthReportShareButton.test.tsx
  • packages/mobile/components/HealthReportShareButton.tsx
  • packages/mobile/test-setup.ts
  • packages/server/package.json
  • packages/server/src/health-report-share-expiry.test.ts
  • packages/server/src/health-report-share-expiry.ts
  • packages/web/src/components/HealthReportShareButton.stories.tsx
  • packages/web/src/components/HealthReportShareButton.test.tsx
  • packages/web/src/components/HealthReportShareButton.tsx

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@Asherlc

Asherlc commented Jul 27, 2026

Copy link
Copy Markdown
Owner Author

Tracking issue: #2055

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

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry @Asherlc, you have reached your weekly rate limit of 500000 diff characters.

Please try again later or upgrade to continue using Sourcery

@codereviewbot-ai

Copy link
Copy Markdown

🤖 Review skipped: Repository rate limit exceeded. Free accounts are limited to 2 reviews per 4 hours per repository. Upgrade to a paid plan for unlimited reviews.

@qodo-code-review

Copy link
Copy Markdown

PR Summary by Qodo

Default health-report share links to 7-day expiry (web + mobile)

✨ Enhancement 🧪 Tests 📝 Documentation 🕐 40+ Minutes

Grey Divider

AI Description

• Add 7/30/90-day expiry controls (default 7) to web and mobile share buttons.
• Send expiresInDays to healthReport.generate and surface returned expiresAt in share copy.
• Add unit tests, Storybook updates, and docs/TDD plan for the expiry behavior.
Diagram

graph TD
  WEB["Web ShareButton"] --> TRPC["tRPC client"] --> API["Server healthReport.generate"] --> REPO["HealthReportRepository"] --> LINK["Share link + expiresAt"]
  MOB["Mobile ShareButton"] --> TRPC
  LINK --> WEBOUT["Clipboard + status"]
  LINK --> MOBO["Native Share message"]
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Server-enforced default expiry when `expiresInDays` is omitted
  • ➕ Protects privacy even for older clients that don’t send the new field
  • ➕ Centralizes expiry policy on the backend
  • ➖ Behavior change for any client that relied on omission to create permanent links
  • ➖ Rollout risk without coordinated client release/feature flag
2. Drive expiry options from shared config (server or shared package)
  • ➕ Avoids duplicating 7/30/90 constants across web and mobile
  • ➕ Allows changing allowed durations without touching UI code
  • ➖ Extra plumbing for a small, stable option set
  • ➖ Still needs client UX for dynamic option rendering

Recommendation: The PR’s approach is appropriate for an incremental privacy-first slice: it keeps the server contract backward-compatible (null still supports non-expiring links) while ensuring current web/mobile always create time-limited links by explicitly sending 7/30/90 days. Consider a server-enforced default only if you need to guarantee expiry for legacy clients.

Files changed (9) +340 / -66

Enhancement (2) +116 / -6
HealthReportShareButton.tsxAdd expiry radio controls and include expiry in native Share message +67/-3

Add expiry radio controls and include expiry in native Share message

• Adds 7/30/90-day expiry selection (default 7) and sends 'expiresInDays' with the 'healthReport.generate' mutation. Updates the share sheet message to include a formatted expiry line when 'report.expiresAt' is present.

packages/mobile/components/HealthReportShareButton.tsx

HealthReportShareButton.tsxAdd expiry selector and show expiry in 'Link copied' status +49/-3

Add expiry selector and show expiry in 'Link copied' status

• Introduces 7/30/90-day expiry selection (default 7) and passes 'expiresInDays' to 'healthReport.generate'. On success, updates clipboard status to “Link copied · expires …” using 'formatDateMedium(report.expiresAt)' when present.

packages/web/src/components/HealthReportShareButton.tsx

Tests (2) +100 / -10
HealthReportShareButton.test.tsxAdd mobile tests for default expiry, selection, and share payload +47/-6

Add mobile tests for default expiry, selection, and share payload

• Extends the tRPC mock to return 'expiresAt' and adds assertions that 7 days is selected by default. Verifies mutate payloads include 'expiresInDays' (7/30/90) and that the native share message appends an “Expires …” line with 'formatDateMedium'.

packages/mobile/components/HealthReportShareButton.test.tsx

HealthReportShareButton.test.tsxAdd web tests for default expiry, selection, and status copy +53/-4

Add web tests for default expiry, selection, and status copy

• Extends the tRPC mock to include 'expiresAt' and asserts the default 7-day option is selected. Verifies mutate payloads include 'expiresInDays' (7/30/90) and that the success message includes the formatted expiry date.

packages/web/src/components/HealthReportShareButton.test.tsx

Documentation (2) +107 / -0
health-report-sharing.mdDocument health report sharing + expiry semantics +16/-0

Document health report sharing + expiry semantics

• Adds a concise overview of how shared health reports are generated and accessed via tokenized public URLs. Documents the server 'expiresInDays' contract (1–90 or null), the client defaults/options (7/30/90), and that UI messaging includes formatted 'expiresAt'.

docs/health-report-sharing.md

2026-07-26-health-report-share-expiry.mdAdd TDD plan for implementing share-link expiry +91/-0

Add TDD plan for implementing share-link expiry

• Introduces a detailed TDD plan covering failing tests first, web/mobile implementation steps, Storybook updates, and verification commands. Explicitly states scope boundaries and non-goals for this slice.

docs/superpowers/plans/2026-07-26-health-report-share-expiry.md

Other (3) +17 / -50
HealthReportShareButton.stories.tsxUpdate mobile Storybook mock to return 'expiresAt' +1/-1

Update mobile Storybook mock to return 'expiresAt'

• Adjusts the mocked 'healthReport.generate' response in Storybook to include a concrete 'expiresAt' timestamp so the expiry messaging is visible in stories.

packages/mobile/components/HealthReportShareButton.stories.tsx

test-setup.tsExpose ARIA props from RN 'accessibilityState' in test mock +15/-48

Expose ARIA props from RN 'accessibilityState' in test mock

• Refactors the react-native Vitest mock to derive ARIA attributes (including 'aria-checked') from 'accessibilityState' via a helper. Enables robust radio-group assertions in mobile component tests.

packages/mobile/test-setup.ts

HealthReportShareButton.stories.tsxUpdate web Storybook mock to return 'expiresAt' +1/-1

Update web Storybook mock to return 'expiresAt'

• Adjusts the mocked mutation response in web Storybook to include a concrete 'expiresAt' timestamp so the status copy can demonstrate expiry formatting.

packages/web/src/components/HealthReportShareButton.stories.tsx

@github-actions

github-actions Bot commented Jul 27, 2026

Copy link
Copy Markdown
Contributor

Mobile Preview

Scan to open on device:

QR code for dofek://preview/pr-2150

Channel pr-2150
Deep Link dofek://preview/pr-2150
Commit 8019a45

To test on device:

  1. Build and install the preview client: PREVIEW_CHANNEL=pr-2150 pnpm expo prebuild --clean -p ios
  2. Or tap deep link on an existing preview build: dofek://preview/pr-2150

Each PR gets its own channel. Build a preview client with PREVIEW_CHANNEL=pr-{N} to test.

@github-actions

github-actions Bot commented Jul 27, 2026

Copy link
Copy Markdown
Contributor

Storybook previews for 8019a453 are ready:

This comment updates automatically on each PR push.

@qodo-code-review

qodo-code-review Bot commented Jul 27, 2026

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (0) 📘 Rule violations (0) 📜 Skill insights (0)

Context used
✅ Compliance rules (platform): 183 rules

Grey Divider


Remediation recommended

1. Duplicated SHARE_EXPIRY_OPTIONS constants ✓ Resolved 📘 Rule violation ⌂ Architecture
Description
The share-expiry options/default are defined separately in both web and mobile components, creating
duplicated cross-platform business rules that can drift over time. This violates the requirement to
centralize shared logic in a domain/shared package when web+mobile behavior must stay aligned.
Code

packages/web/src/components/HealthReportShareButton.tsx[R7-9]

+const SHARE_EXPIRY_OPTIONS = [7, 30, 90] as const;
+type ShareExpiryDays = (typeof SHARE_EXPIRY_OPTIONS)[number];
+const DEFAULT_SHARE_EXPIRY_DAYS: ShareExpiryDays = 7;
Relevance

⭐⭐⭐ High

Team often accepts deduplicating web/mobile logic into shared modules to prevent drift.

PR-#1589
PR-#1359

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
PR Compliance ID 773501 flags duplicated constants/logic across packages/web and packages/mobile
as a red flag that should be centralized. Both components independently define the same
SHARE_EXPIRY_OPTIONS and default expiry days, indicating shared logic is not centralized.

Rule 773501: Mirror web and mobile changes and centralize shared logic in domain packages
packages/web/src/components/HealthReportShareButton.tsx[7-9]
packages/mobile/components/HealthReportShareButton.tsx[10-12]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`SHARE_EXPIRY_OPTIONS` and `DEFAULT_SHARE_EXPIRY_DAYS` are duplicated in both web and mobile `HealthReportShareButton` implementations.

## Issue Context
PR Compliance ID 773501 requires shared logic used by both platforms to be centralized (e.g., in a shared/domain package) to avoid divergence.

## Fix Focus Areas
- packages/web/src/components/HealthReportShareButton.tsx[7-9]
- packages/mobile/components/HealthReportShareButton.tsx[10-12]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


2. Non-unique expiry radio IDs ✓ Resolved 🐞 Bug ≡ Correctness
Description
The web HealthReportShareButton hard-codes the radio name and generates deterministic ids
(share-expiry-7/30/90). If multiple share buttons are rendered on the same page, the radio groups
will interfere (selection in one unchecks the other) and duplicated IDs can mis-associate labels
with inputs.
Code

packages/web/src/components/HealthReportShareButton.tsx[R50-80]

+      <fieldset
+        aria-label="Share link expires in"
+        className="flex flex-wrap items-center gap-2 border-0 p-0"
+      >
+        <legend className="sr-only">Share link expires in</legend>
+        <span className="text-xs text-muted" aria-hidden="true">
+          Expires in
+        </span>
+        {SHARE_EXPIRY_OPTIONS.map((days) => {
+          const selected = expiresInDays === days;
+          const optionId = `share-expiry-${days}`;
+          return (
+            <label
+              key={days}
+              htmlFor={optionId}
+              className={`cursor-pointer rounded-md border px-2.5 py-1 text-xs font-medium transition-colors ${
+                selected
+                  ? "border-accent bg-accent/20 text-foreground"
+                  : "border-border bg-transparent text-muted hover:border-border-strong hover:text-foreground"
+              } ${disabled || generateReport.isPending ? "cursor-not-allowed opacity-50" : ""}`}
+            >
+              <input
+                id={optionId}
+                type="radio"
+                name="share-link-expiry"
+                value={days}
+                checked={selected}
+                disabled={disabled || generateReport.isPending}
+                className="sr-only"
+                onChange={() => setExpiresInDays(days)}
+              />
Relevance

⭐⭐⭐ High

They regularly fix DOM/accessibility uniqueness/collision issues; likely to scope radio ids/names
per instance.

PR-#1868
PR-#1197

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The component currently uses a fixed name and non-instance-scoped id scheme for the expiry
radios, which are document-global in HTML; this can cause radio-group coupling and ID collisions
across component instances.

packages/web/src/components/HealthReportShareButton.tsx[48-85]
packages/web/src/routes/weekly-report.tsx[23-33]
packages/web/src/routes/monthly-report.tsx[19-28]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
`HealthReportShareButton` (web) uses a constant radio-group `name` and deterministic option `id`s (e.g. `name="share-link-expiry"`, `id="share-expiry-7"`). When the component is rendered more than once in the same document, browsers treat same-name radios as one group and duplicate IDs break label/input association.

### Issue Context
This component is reused across report pages and may be rendered multiple times in the future (e.g., if weekly+monthly share actions appear together). The fix should make the radio group name and option IDs unique per component instance.

### Fix Focus Areas
- packages/web/src/components/HealthReportShareButton.tsx[48-85]

### Suggested fix
- Use React’s `useId()` (or an equivalent stable per-instance unique ID) to create a unique prefix.
- Derive both:
 - the `name` (e.g., `share-link-expiry-${instanceId}`)
 - and each option’s `id` (e.g., `share-expiry-${instanceId}-${days}`)
- (Optional but ideal) Add a unit test that renders two `HealthReportShareButton` instances and asserts selecting an option in one does not change the selection in the other.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

Qodo Logo

Comment thread packages/web/src/components/HealthReportShareButton.tsx Outdated
Comment thread packages/web/src/components/HealthReportShareButton.tsx
@Asherlc
Asherlc enabled auto-merge (squash) July 27, 2026 02:48
Centralize 7/30/90 share durations in dofek-server/health-report-share-expiry and scope web radio name/ids per instance so multiple share buttons stay independent.

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

Copy link
Copy Markdown

🤖 Review skipped: Repository rate limit exceeded. Free accounts are limited to 2 reviews per 4 hours per repository. Upgrade to a paid plan for unlimited reviews.

Give Stryker a related unit test for the shared client duration constants so mutation runs do not exit with no tests executed.

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

Copy link
Copy Markdown

🤖 Review skipped: Repository rate limit exceeded. Free accounts are limited to 2 reviews per 4 hours per repository. Upgrade to a paid plan for unlimited reviews.

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