Skip to content

feat(supporter): expose chip on public + friend profile views#145

Merged
ntatschner merged 1 commit into
nextfrom
feat/supporter-chip-public-profile
Jun 1, 2026
Merged

feat(supporter): expose chip on public + friend profile views#145
ntatschner merged 1 commit into
nextfrom
feat/supporter-chip-public-profile

Conversation

@ntatschner
Copy link
Copy Markdown
Collaborator

Summary

Extends the supporter recognition pill (PR #143 self-only, PR #144 topbar) to anyone visiting a public profile or accepted-share view. Continuation of the chip rollout: #143#144#145 (this PR)#146 (discover bulk).

Server changes

  • SupporterStore gains get_by_handle_public(handle) — single-query JOIN of users + supporter_status + LATERAL most-recent completed revolut_orders. Returns Ok(None) for unknown handles, missing rows, or state=none; returns Ok(Some(_)) only for active/lapsed. Case-insensitive handle match mirrors the rest of the project.
  • PublicSummaryResponse gains supporter: Option<PublicSupporterInfo>. PublicSupporterInfo is a deliberate projection — only state + current_tier_key + name_plate. grace_until, payment timestamps, and cancelled_at are explicitly NOT exposed to strangers (fingerprinting + churn leak avoidance).
  • render_summary + render_summary_scoped now take a &dyn SupporterStore and populate supporter on every public summary response. Fail-soft on lookup error (warn log; chip just doesn't render). Three handler call sites updated: public_summary, friend_summary, preview_summary. Each takes Extension(Arc<dyn SupporterStore>); same dyn-cast pattern as share_metadata_dyn.
  • main.rs wires supporter_store_dyn + adds the Extension layer.
  • openapi.rs registers PublicSupporterInfo so the TS client regen picks it up.

Web changes

  • /u/[handle]/page.tsx extends the supporter wiring from PR feat(supporter): render tier-styled chip on self profile view #143. Self path keeps fetching /v1/me/supporter for full DTO. Public + shared paths now read data.supporter from the extended PublicSummaryResponse. Both feed the same <SupporterChip> via a ChipStatus type alias.

Tests

  • 4 new SupporterStore tests on the memstore: unknown handle, unbound handle (row exists but no handle binding — simulates Postgres JOIN miss), bound handle returns active row with case-insensitive lookup, state=none filtered out.
  • MemorySupporterStore gains bind_handle(handle, user_id) so tests can simulate the users-table side of the JOIN.
  • All existing tests pass: 8 supporter, 35 sharing, 40 web vitest.
  • cargo fmt + clippy clean on starstats-server.
  • pnpm typecheck + lint clean on web.

Test plan

  • cargo test -p starstats-server --bin starstats-server supporters:: — 8 tests pass (4 new)
  • cargo test -p starstats-server --bin starstats-server sharing_routes:: — 35 tests pass
  • cargo clippy -p starstats-server --bin starstats-server -- -D warnings clean
  • pnpm --filter web run typecheck clean
  • pnpm --filter web run test:run — 40 tests pass
  • Smoke after platform promote: visit another user's public profile (sign out + open /u/<their-handle>) and confirm their chip renders if they're a supporter. Visit your own from another's session via a share invite and confirm the chip renders on the shared path.

Follow-up

Extends the supporter recognition pill (PR #143 self-only, PR #144
topbar) to anyone visiting a public profile or accepted-share view.
Continuation of the chip rollout: #143#144 → **#145 (this PR)** →
#146 (discover bulk).

Server changes:

  - `SupporterStore` gains `get_by_handle_public(handle)` —
    single-query JOIN of `users` + `supporter_status` + LATERAL most-
    recent completed `revolut_orders`. Returns `Ok(None)` for unknown
    handles, missing rows, or `state=none`; returns `Ok(Some(_))`
    only for `active`/`lapsed`. Case-insensitive handle match
    mirrors the rest of the project.

  - `PublicSummaryResponse` gains `supporter: Option<PublicSupporterInfo>`.
    `PublicSupporterInfo` is a deliberate projection of the store
    shape — only `state` + `current_tier_key` + `name_plate` are
    surfaced. `grace_until`, payment timestamps, and `cancelled_at`
    are explicitly NOT exposed to strangers (fingerprinting + churn
    leak avoidance).

  - `render_summary` + `render_summary_scoped` now take a
    `&dyn SupporterStore` and populate `supporter` on every public
    summary response. Fail-soft on lookup error (warn log; chip
    just doesn't render). Three handler call sites updated:
    `public_summary`, `friend_summary`, `preview_summary`. Each
    takes `Extension(Arc<dyn SupporterStore>)`; same dyn-cast
    pattern as `share_metadata_dyn`.

  - `main.rs` wires `supporter_store_dyn` + adds the Extension
    layer alongside `share_metadata_dyn`.

  - `openapi.rs` registers `PublicSupporterInfo` so the TS client
    regen picks it up.

Web changes:

  - `/u/[handle]/page.tsx` extends the supporter wiring from PR #143.
    Self path keeps fetching `/v1/me/supporter` for full DTO. Public
    + shared paths now read `data.supporter` from the extended
    PublicSummaryResponse. Both feed the same `<SupporterChip>` via
    a `ChipStatus` type alias (the three-field overlap between
    `SupporterStatusDto` and `PublicSupporterInfo`).

Tests:

  - 4 new SupporterStore tests on the memstore: unknown handle,
    unbound handle (row exists but no handle binding — simulates
    Postgres JOIN miss), bound handle returns active row with
    case-insensitive lookup, state=none filtered out.
  - `MemorySupporterStore` gains `bind_handle(handle, user_id)` so
    tests can simulate the users-table side of the JOIN.
  - All existing supporter + sharing tests still pass (35 sharing,
    8 supporter, 666 filtered).

Follow-up: PR #146 — discover/profiles chip (needs bulk
`get_many_public(user_ids)` to avoid N+1).
ntatschner pushed a commit that referenced this pull request Jun 1, 2026
Surfaces the supporter pill on every signed-in page (not just
/u/<handle> self view) by piggybacking on the existing layout-level
Promise.allSettled scaffold. `getSupporterStatus` joins the existing
location / shared / catalog fan-out so the chip costs one extra
fetch per shell render — request-level cached by React anyway.

Web changes:
  - layout.tsx adds getSupporterStatus to the Promise.allSettled
    block, fail-soft to null on error so a /v1/me/supporter hiccup
    doesn't blank the chrome (same posture as the other shell
    fetches).
  - TopBar.tsx accepts an optional `supporter` prop, renders
    <SupporterChip size="sm" /> next to the @handle pill. Compact
    palette size mirrors the existing topbar density.

E2E fixture (required per `Playwright fixture default rule`):
  - api-mock.ts gets a default `/v1/me/supporter` fixture pointing
    at state=none so existing scenarios that don't care about
    supporter logic don't 599 on the new layout fetch.
  - New `supporterStatus(tier, plate)` helper for tests that DO
    want to exercise the chip.

Self-profile chip from PR #143 continues to work; the topbar chip
just makes the recognition visible on /dashboard, /journey, /sharing
etc. without forcing the user to navigate to their profile page.

Follow-up: public/friend profile chip (#145), discover/profiles
chip (#146).
@ntatschner ntatschner merged commit 63ecfcb into next Jun 1, 2026
10 checks passed
@ntatschner ntatschner deleted the feat/supporter-chip-public-profile branch June 1, 2026 01:53
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