Skip to content

feat(react): notification action buttons#74

Open
soufian3hm wants to merge 2 commits into
dodopayments:mainfrom
soufian3hm:feat/notification-actions
Open

feat(react): notification action buttons#74
soufian3hm wants to merge 2 commits into
dodopayments:mainfrom
soufian3hm:feat/notification-actions

Conversation

@soufian3hm

Copy link
Copy Markdown
Contributor

Closes #35.

Frontend-only first slice of notification action buttons.

What

  • @chimely/client: WellKnownPayload gains optional primary_action / secondary_action ({ label: string; url?: string }), exported as PayloadAction. Additive, per the payload contract.
  • @chimely/react: the default item renders the actions as buttons below its content. New onPrimaryActionClick / onSecondaryActionClick props (return false to veto navigation) and a renderActions escape hatch for custom UI.
  • Action URLs reuse the same safe-navigation path as action_url via a new shared followActionUrl helper: same-origin -> routerPush (when provided), otherwise a full assign; javascript: / data: / custom schemes are refused. The item click was refactored onto the same helper.
  • Buttons are siblings of the item button (a button cannot nest inside a button), so the divider moved from .chimely-item to the row via .chimely-list-row:has(> .chimely-item) -- the CTAs sit inside the item, above the line. Custom renderItem rows are unaffected (the :has scope).
  • Payloads pass through verbatim, so a malformed action (missing/blank label) renders nothing.

Deferred (as the issue frames it)

  • Server-side action-completion state (completePrimary / revertPrimary) -- the issue says defer until someone asks. No server change here.
  • Action labels come from the payload, so no new InboxLocalization strings.

Verification

biome, pnpm --filter "./packages/*" build, typecheck, and vitest are all green: @chimely/client 54 + @chimely/react 111 tests, including 6 new ones (render + follow URL, no action bar without actions, same-origin routerPush, onPrimaryActionClick veto, javascript: refused, renderActions override). A minor changeset covers both packages.

Frontend slice. WellKnownPayload gains primary_action/secondary_action
({ label, url? }, exported as PayloadAction). The default item renders
them as buttons below the content, with onPrimaryActionClick/
onSecondaryActionClick props and a renderActions escape hatch. URLs
reuse a shared safe-navigation helper (followActionUrl), and the item
divider moves to the row so the buttons sit inside it.

Closes dodopayments#35
@greptile-apps

greptile-apps Bot commented Jul 15, 2026

Copy link
Copy Markdown

Greptile Summary

This PR adds frontend notification action buttons. The main changes are:

  • Optional primary_action and secondary_action payload fields in @chimely/client.
  • Default CTA rendering plus action callbacks and a custom renderActions hook in @chimely/react.
  • Shared safe URL following for item clicks and action buttons.
  • Row and CTA styling for the new button layout.

Confidence Score: 5/5

This looks safe to merge after a small blank-label cleanup.

  • The new action prop and navigation paths are wired through the default inbox flow.
  • Unsafe action URLs still go through the existing resolver.
  • Whitespace-only action labels can render a blank clickable CTA.

packages/react/src/components/NotificationList.tsx

Important Files Changed

Filename Overview
packages/client/src/types.ts Adds PayloadAction and optional action fields to the well-known payload shape.
packages/client/src/index.ts Exports the new action payload type from the client package.
packages/react/src/Inbox.tsx Passes action callbacks and custom action rendering through to inbox content.
packages/react/src/components/InboxContent.tsx Adds action click handling and moves item URL following to the shared helper.
packages/react/src/components/NotificationList.tsx Adds default CTA rendering, with a blank-label edge case that can render a clickable empty action.
packages/react/src/navigation.ts Adds a shared helper for safe action URL following.
packages/react/src/styles.ts Moves the default item divider to the row and styles the new CTA buttons.
packages/react/src/inbox.test.tsx Adds tests for action rendering, URL following, veto callbacks, unsafe URLs, and custom action UI.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
  Payload[Notification payload actions] --> List[NotificationList]
  List --> Actions[ItemActions]
  Actions -->|default UI| Label[actionLabel]
  Actions -->|custom UI| RenderActions[renderActions]
  Label --> Buttons[CTA buttons]
  Buttons --> Click[handleActionClick]
  Click --> Veto{Callback returns false?}
  Veto -->|yes| Stop[No navigation]
  Veto -->|no| Url{Action URL present?}
  Url -->|yes| Follow[followActionUrl]
  Url -->|no| Stop
  Follow -->|same origin with routerPush| Router[routerPush path]
  Follow -->|external or no routerPush| Assign[navigation.assign]
  Follow -->|unsafe URL| Stop
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
flowchart TD
  Payload[Notification payload actions] --> List[NotificationList]
  List --> Actions[ItemActions]
  Actions -->|default UI| Label[actionLabel]
  Actions -->|custom UI| RenderActions[renderActions]
  Label --> Buttons[CTA buttons]
  Buttons --> Click[handleActionClick]
  Click --> Veto{Callback returns false?}
  Veto -->|yes| Stop[No navigation]
  Veto -->|no| Url{Action URL present?}
  Url -->|yes| Follow[followActionUrl]
  Url -->|no| Stop
  Follow -->|same origin with routerPush| Router[routerPush path]
  Follow -->|external or no routerPush| Assign[navigation.assign]
  Follow -->|unsafe URL| Stop
Loading

Reviews (1): Last reviewed commit: "feat(react): notification action buttons" | Re-trigger Greptile

Comment on lines +307 to +309
if (typeof label === 'string' && label.length > 0) {
return label;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Whitespace Labels Stay Clickable

A payload with { label: " " } passes this check, so the default row renders a blank CTA that can still run onAction and follow its URL. The payload contract says blank labels render nothing, so this malformed action becomes an invisible clickable control.

Suggested change
if (typeof label === 'string' && label.length > 0) {
return label;
}
if (typeof label === 'string' && label.trim().length > 0) {
return label;
}

Context Used: CLAUDE.md (source)

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.

Notification action buttons (primary/secondary)

2 participants