Skip to content

feat(notifications): add label to action types for bulletin button#458

Merged
grimen merged 17 commits intomainfrom
feat--bulletin-dismissible-and-button-action
Feb 26, 2026
Merged

feat(notifications): add label to action types for bulletin button#458
grimen merged 17 commits intomainfrom
feat--bulletin-dismissible-and-button-action

Conversation

@esaugomez31
Copy link
Collaborator

@esaugomez31 esaugomez31 commented Feb 11, 2026

Summary

  • Add optional label field to OpenDeepLinkAction and OpenExternalLinkAction across all layers (proto, Rust, GraphQL, TypeScript)
  • The label is used by the mobile app to display an action button on bulletin cards. The button is purely visual — it triggers the same action as tapping the card (open deep link or external URL)
  • Update GraphQL schemas (admin input + notifications subgraph output)
  • Update protobuf: label lives on the Action message (outside oneof) so it applies to both action types
  • Backward-compatible serde for ExternalUrl (supports old string format and new struct format)

Tests

Rust unit tests

  • Serde roundtrip with action label
  • Serde backward compat for ExternalUrl as string (old format)
  • Serde backward compat without label (old records)
  • Action label returns Some when set on deep link
  • Action label returns Some when set on external URL
  • Action label returns None when not set
  • Push message uses localized content when available
  • Push message uses default content
  • Icon returns value when set

e2e BATS

  • Create bulletin with label and external URL action, verify action.label is returned
  • Create bulletin with label and deep link action, verify action.label is returned
  • Create bulletin without label, verify action.label is null
  • List stateful notifications, paginate with cursor
  • Acknowledge stateful notification
  • List unacknowledged with bulletin enabled, paginate
  • List without bulletin enabled, paginate
  • Unacknowledged count

@esaugomez31 esaugomez31 self-assigned this Feb 11, 2026
Copilot AI review requested due to automatic review settings February 11, 2026 21:11

This comment was marked as outdated.

Copilot AI review requested due to automatic review settings February 11, 2026 21:46

This comment was marked as outdated.

@esaugomez31 esaugomez31 marked this pull request as ready for review February 18, 2026 14:41
@grimen grimen changed the title feat: bulletin dismissible and button action feat(bulletin): bulletin dismissible and button action Feb 18, 2026
@grimen grimen requested review from dolcalmi and grimen February 18, 2026 21:04
@grimen
Copy link

grimen commented Feb 18, 2026

@blink-claw-bot review

Copy link

@blink-claw-bot blink-claw-bot left a comment

Choose a reason for hiding this comment

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

Review: feat(bulletin): bulletin dismissible and button action

Nice, clean feature addition. The bulletinButton field flows correctly through all layers (GraphQL → TypeScript → protobuf → gRPC → Rust → back to GraphQL). Good test coverage on both the Rust unit tests and BATS integration tests.

✅ What looks good

  • Backward compatibility handled well — #[serde(default)] on the new field, and an explicit deserialization test for old payloads without bulletin_button
  • Protobuf schema uses optional correctly (field 8, no conflicts)
  • Rust tests are thorough: serde roundtrip, backward compat, coexistence with action, localization
  • BATS tests cover all three scenarios: external URL action, no button, and deep link action
  • GraphQL schema additions are consistent with existing patterns (BulletinButton type, optional field on StatefulNotification)

Issues

  1. bulletinButton validation is missing on the input side — The BulletinButtonInput.label is String! (non-null) in GraphQL, which prevents empty-via-null. But an empty string "" would pass through. Consider whether an empty label is valid. If not, add validation similar to how openDeepLink/openExternalUrl inputs are validated in marketing-notification-trigger.ts. (minor)

  2. No mutual exclusion between bulletinButton and the absence of an action — A button with a label but no action (no openDeepLink or openExternalUrl) would render a button that does nothing when tapped. Should the API enforce that bulletinButton requires an action? At minimum, document the expected behavior. (minor — depends on client-side handling)

  3. PR title mentions "dismissible" but the diff doesn't show dismissible changes — The title says "bulletin dismissible and button action" but I only see button/action additions. Is the dismissible part handled elsewhere, or is it implicit in the existing acknowledge flow? Clarifying the PR description would help. (clarification)

Nits

  • nit: notifications_pb.js has a lot of whitespace reformatting (indentation changes in toObject methods throughout the file). This is likely from regenerating the protobuf JS code, but it inflates the diff significantly. If there's a way to regenerate only the changed parts, it'd make reviews easier in the future.

  • nit: In notifications_pb.js, the icon field changed from jspb.Message.getFieldWithDefault(msg, 7, 0) to (f = jspb.Message.getField(msg, 7)) == null ? undefined : f. Same for DeepLink.screen, DeepLink.action, Action.externalUrl, and NotificationSettings.locale. These semantic changes (default 0/""undefined when unset) are correct for optional proto fields but are unrelated to the bulletin button feature. Worth noting in the PR description that the codegen was re-run.

Verdict

Looks good overall. The core feature is well-implemented with proper test coverage. The issues raised are minor — the empty label and button-without-action cases are worth considering but not blocking.

🤖 Review by Claude Opus 4.6 via Blink-Claw-Bot

@grimen
Copy link

grimen commented Feb 18, 2026

Review: feat(bulletin): bulletin dismissible and button action

Nice, clean feature addition. The bulletinButton field flows correctly through all layers (GraphQL → TypeScript → protobuf → gRPC → Rust → back to GraphQL). Good test coverage on both the Rust unit tests and BATS integration tests.

✅ What looks good

  • Backward compatibility handled well — #[serde(default)] on the new field, and an explicit deserialization test for old payloads without bulletin_button
  • Protobuf schema uses optional correctly (field 8, no conflicts)
  • Rust tests are thorough: serde roundtrip, backward compat, coexistence with action, localization
  • BATS tests cover all three scenarios: external URL action, no button, and deep link action
  • GraphQL schema additions are consistent with existing patterns (BulletinButton type, optional field on StatefulNotification)

Issues

  1. bulletinButton validation is missing on the input side — The BulletinButtonInput.label is String! (non-null) in GraphQL, which prevents empty-via-null. But an empty string "" would pass through. Consider whether an empty label is valid. If not, add validation similar to how openDeepLink/openExternalUrl inputs are validated in marketing-notification-trigger.ts. (minor)
  2. No mutual exclusion between bulletinButton and the absence of an action — A button with a label but no action (no openDeepLink or openExternalUrl) would render a button that does nothing when tapped. Should the API enforce that bulletinButton requires an action? At minimum, document the expected behavior. (minor — depends on client-side handling)
  3. PR title mentions "dismissible" but the diff doesn't show dismissible changes — The title says "bulletin dismissible and button action" but I only see button/action additions. Is the dismissible part handled elsewhere, or is it implicit in the existing acknowledge flow? Clarifying the PR description would help. (clarification)

Nits

  • nit: notifications_pb.js has a lot of whitespace reformatting (indentation changes in toObject methods throughout the file). This is likely from regenerating the protobuf JS code, but it inflates the diff significantly. If there's a way to regenerate only the changed parts, it'd make reviews easier in the future.
  • nit: In notifications_pb.js, the icon field changed from jspb.Message.getFieldWithDefault(msg, 7, 0) to (f = jspb.Message.getField(msg, 7)) == null ? undefined : f. Same for DeepLink.screen, DeepLink.action, Action.externalUrl, and NotificationSettings.locale. These semantic changes (default 0/""undefined when unset) are correct for optional proto fields but are unrelated to the bulletin button feature. Worth noting in the PR description that the codegen was re-run.

Verdict

Looks good overall. The core feature is well-implemented with proper test coverage. The issues raised are minor — the empty label and button-without-action cases are worth considering but not blocking.

🤖 Review by Claude Opus 4.6 via Blink-Claw-Bot

@esaugomez31 Have a look.

@grimen grimen requested review from bas4r, littledino2112 and openoms and removed request for openoms February 19, 2026 09:18
@github-actions github-actions bot added the ci label Feb 20, 2026
pub body: String,
pub deep_link: Option<String>,
pub action: Option<NotificationAction>,
pub bulletin_button: Option<BulletinButton>,

Choose a reason for hiding this comment

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

according to answers this should be something more generic. action_label or label if it is part of NotificatioAction structure? @grimen

Copy link

Choose a reason for hiding this comment

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

@designsats Let's discuss this new action/button design more in detail tomorrow UX/UI call - seems like the introduction of it raises more questions than answers for me currently.

Copy link

@littledino2112 littledino2112 left a comment

Choose a reason for hiding this comment

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

Pre-existing bugs in bats tests (not introduced by this PR, but worth fixing)

These exist on main already — since this PR extends these test files, it's a good opportunity to fix them and harden the test suite. Other than this, I think the added changes look good.

Bug 1: Undefined variable $n_bulletins (lines 103, 179)

Two tests assign to n_notifications but assert against $n_bulletins which is never defined:

Line 102-103 (test: "list unacknowledged stateful notifications with bulletin enabled"):

n_notifications=$(graphql_output '...unacknowledgedStatefulNotificationsWithBulletinEnabled.nodes | length')
[[ $n_bulletins -eq 0 ]] || exit 1    # ← $n_bulletins is never set, always evaluates as 0

Line 178-179 (test: "list stateful notifications without bulletin enabled"):

n_notifications=$(graphql_output '...listStatefulNotificationsWithoutBulletinEnabled.nodes | length')
[[ $n_bulletins -eq 0 ]] || exit 1    # ← same bug

In bash, an unset variable in [[ $n_bulletins -eq 0 ]] evaluates as 0, so the assertion always passes regardless of the actual notification count. The initial empty-state precondition check is completely defeated.

Fix: Replace $n_bulletins with $n_notifications on both lines.

Bug 2: Wrong GraphQL response path (line 178)

Line 178 uses jq path .data.me.listStatefulNotificationsWithoutBulletinEnabled.nodes but the GQL query field (in bats/gql/list-stateful-notifications-without-bulletin-enabled.gql:4) is statefulNotificationsWithoutBulletinEnabledno list prefix. This means n_notifications is always null.

Note that line 207 in the same test correctly uses .data.me.statefulNotificationsWithoutBulletinEnabled.nodes, so only the initial zero-count check is broken.

Fix: Remove the list prefix:

n_notifications=$(graphql_output '.data.me.statefulNotificationsWithoutBulletinEnabled.nodes | length')

@esaugomez31 esaugomez31 force-pushed the feat--bulletin-dismissible-and-button-action branch from 1dde28c to 9c622fd Compare February 24, 2026 17:28
@esaugomez31
Copy link
Collaborator Author

Hi @grimen @dolcalmi, this seems to be a CI issue. In this PR we already addressed a previous CI problem 9c622fd (pinning DOCKER_API_VERSION=1.44), which was resolved following @openoms recommendation based on blinkbitcoin/bria#27. However, this is a different CI error.

The integration tests are currently failing, but this doesn't seem to be related to this PR since recent PRs also have failing integration tests:

  1. chore(deps): update nix flake inputs for Docker Engine 29 compat #473 — fix(ci)--nix-flake-update
  2. chore(deps): bump axios from 1.11.0 to 1.13.5 #456 — dependabot: axios-1.13.5
  3. chore(deps-dev): bump webpack from 5.99.9 to 5.104.1 #449 — dependabot: webpack-5.104.1
  4. chore(deps): bump @apollo/server from 4.12.2 to 5.4.0 #445 — dependabot: apollo/server-5.4.0
  5. chore(deps): bump next from 14.2.26 to 16.1.5 #436 — dependabot: next-16.1.5

The tests themselves pass, the issue is on the CI side. This PR doesn't include integration tests, only Rust unit tests and e2e bats tests, and both pass locally:

Rust unit tests (cargo test):

Pasted image

E2E bats tests (nix develop -c bats bats/core/notifications/notifications.bats):

Pasted image (2)

@openoms
Copy link

openoms commented Feb 24, 2026

this seems to be a CI issue. In this PR we already addressed a previous CI problem 9c622fd (pinning DOCKER_API_VERSION=1.44), which was resolved following @openoms recommendation based on blinkbitcoin/bria#27. However, this is a different CI error.

working on a fix here:

@esaugomez31 esaugomez31 force-pushed the feat--bulletin-dismissible-and-button-action branch from 9c622fd to 3a278b5 Compare February 24, 2026 21:33
@esaugomez31 esaugomez31 changed the title feat(bulletin): bulletin dismissible and button action feat(notifications): add label to action types for bulletin button Feb 26, 2026
@esaugomez31
Copy link
Collaborator Author

Hi @dolcalmi @grimen, I applied the recommendation and it looks like this:

Before

label lived in a separate BulletinButton type:

type BulletinButton {
  label: String!
}

type StatefulNotification {
  action: NotificationAction
  bulletinButton: BulletinButton
}
{
  "openDeepLink": { "screen": "PEOPLE" },
  "bulletinButton": { "label": "Learn more" }
}
{
  "openExternalUrl": { "url": "https://example.com" },
  "bulletinButton": { "label": "Learn more" }
}
pub struct BulletinButton {
    pub label: String,
}

After

label lives inside each action type:

type OpenDeepLinkAction {
  deepLink: String!
  label: String
}

type OpenExternalLinkAction {
  url: String!
  label: String
}
{
  "openDeepLink": { "screen": "PEOPLE", "label": "Learn more" }
}
{
  "openExternalUrl": { "url": "https://example.com", "label": "Learn more" }
}
pub struct DeepLink {
    pub screen: Option<DeepLinkScreen>,
    pub action: Option<DeepLinkAction>,
    pub label: Option<String>,
}

pub struct ExternalUrl {
    url: String,
    pub label: Option<String>,
}

@esaugomez31 esaugomez31 force-pushed the feat--bulletin-dismissible-and-button-action branch from 3e45a62 to 451a48b Compare February 26, 2026 05:53
@esaugomez31 esaugomez31 requested a review from dolcalmi February 26, 2026 14:37
Copy link

@dolcalmi dolcalmi left a comment

Choose a reason for hiding this comment

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

looks good, just minor changes

esaugomez31 and others added 17 commits February 26, 2026 11:25
…es in proto and Rust

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…es in API

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@esaugomez31 esaugomez31 force-pushed the feat--bulletin-dismissible-and-button-action branch from 451a48b to 8d8f10b Compare February 26, 2026 17:26
@esaugomez31 esaugomez31 requested a review from dolcalmi February 26, 2026 17:48
@grimen grimen merged commit 85afd7f into main Feb 26, 2026
16 checks passed
@grimen grimen deleted the feat--bulletin-dismissible-and-button-action branch February 26, 2026 19:00
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

8 participants