From 4764638bbb90c6a61ed9fd830d81a2f91a23def7 Mon Sep 17 00:00:00 2001 From: bardonadam Date: Tue, 26 May 2026 18:43:42 +0700 Subject: [PATCH 1/4] Add alert live activity DX support --- package-lock.json | 4 +- package.json | 2 +- src/ActivitySmith.ts | 96 +++++++++++++++++++++++++++++++++++------ tests/resources.test.js | 51 ++++++++++++++++++++++ 4 files changed, 137 insertions(+), 16 deletions(-) diff --git a/package-lock.json b/package-lock.json index 32db8ba..245c273 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "activitysmith", - "version": "1.3.1", + "version": "1.4.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "activitysmith", - "version": "1.3.1", + "version": "1.4.0", "license": "MIT", "devDependencies": { "typescript": "^5.3.3", diff --git a/package.json b/package.json index 00c57d8..1907b20 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "activitysmith", - "version": "1.3.1", + "version": "1.4.0", "description": "Official ActivitySmith Node.js SDK", "keywords": [ "activitysmith", diff --git a/src/ActivitySmith.ts b/src/ActivitySmith.ts index ba31f87..d6ec984 100644 --- a/src/ActivitySmith.ts +++ b/src/ActivitySmith.ts @@ -1,6 +1,6 @@ import { Configuration, PushNotificationsApi, LiveActivitiesApi, MetricsApi } from "../generated/index"; -const SDK_VERSION = "1.3.1"; +const SDK_VERSION = "1.4.0"; const SDK_HEADER_NAME = "X-ActivitySmith-SDK"; const SDK_HEADER_VALUE = `node-v${SDK_VERSION}`; @@ -28,18 +28,57 @@ type MetricUpdateOptions = Omit; type MetricInitOverrides = Parameters[1]; type ChannelTargetInput = { channels?: string[] }; type PushSendRequest = PushRequestBody & { channels?: string[] }; -type LiveStartSendRequest = StartRequestBody & { channels?: string[] }; -type LiveStreamSendRequest = StreamRequestBody & { channels?: string[] }; const LiveActivityTypes = { segmentedProgress: "segmented_progress", progress: "progress", metrics: "metrics", stats: "stats", + alert: "alert", } as const; -function withTargetChannels( - request: T & { channels?: string[] }, +export type LiveActivityType = (typeof LiveActivityTypes)[keyof typeof LiveActivityTypes]; + +export type LiveActivityAlertIcon = { + symbol: string; + color?: string; +}; + +export type LiveActivityAlertBadge = { + title: string; + color?: string; +}; + +export type LiveActivityContentState = Record & { + title?: string; + subtitle?: string; + type?: LiveActivityType | string; + message?: string; + icon?: LiveActivityAlertIcon; + badge?: LiveActivityAlertBadge; + color?: string; +}; + +type LiveStartSendRequest = Omit & { + content_state: LiveActivityContentState; + channels?: string[]; +}; +type LiveUpdateSendRequest = Omit & { + content_state: LiveActivityContentState; +}; +type LiveEndSendRequest = Omit & { + content_state: LiveActivityContentState; +}; +type LiveStreamSendRequest = Omit & { + content_state: LiveActivityContentState; + channels?: string[]; +}; +type LiveStreamDeleteSendRequest = Omit & { + content_state?: LiveActivityContentState; +}; + +function withTargetChannels( + request: T & { target?: ChannelTargetInput; channels?: string[] }, ): T { const channels = request.channels; if (!channels || channels.length === 0 || request.target) { @@ -54,6 +93,20 @@ function withTargetChannels( } as T; } +function withoutAlertRootColor( + request: T, +): T { + if (request.content_state?.type !== LiveActivityTypes.alert || !("color" in request.content_state)) { + return request; + } + + const { color: _ignored, ...contentState } = request.content_state; + return { + ...request, + content_state: contentState, + }; +} + function hasMediaValue(media: unknown): boolean { if (typeof media === "string") { return media.trim().length > 0; @@ -140,24 +193,36 @@ export class LiveActivitiesResource { start(request: LiveStartSendRequest, initOverrides?: LiveInitOverrides) { return this.api.startLiveActivity( - { liveActivityStartRequest: withTargetChannels(request) }, + { + liveActivityStartRequest: withoutAlertRootColor( + withTargetChannels(request), + ) as StartRequestBody, + }, initOverrides, ); } - update(request: UpdateRequestBody, initOverrides?: LiveInitOverrides) { - return this.api.updateLiveActivity({ liveActivityUpdateRequest: request }, initOverrides); + update(request: LiveUpdateSendRequest, initOverrides?: LiveInitOverrides) { + return this.api.updateLiveActivity( + { liveActivityUpdateRequest: withoutAlertRootColor(request) as UpdateRequestBody }, + initOverrides, + ); } - end(request: EndRequestBody, initOverrides?: LiveInitOverrides) { - return this.api.endLiveActivity({ liveActivityEndRequest: request }, initOverrides); + end(request: LiveEndSendRequest, initOverrides?: LiveInitOverrides) { + return this.api.endLiveActivity( + { liveActivityEndRequest: withoutAlertRootColor(request) as EndRequestBody }, + initOverrides, + ); } stream(streamKey: string, request: LiveStreamSendRequest, initOverrides?: LiveInitOverrides) { return this.api.reconcileLiveActivityStream( { streamKey, - liveActivityStreamRequest: withTargetChannels(request), + liveActivityStreamRequest: withoutAlertRootColor( + withTargetChannels(request), + ) as StreamRequestBody, }, initOverrides, ); @@ -165,12 +230,17 @@ export class LiveActivitiesResource { endStream( streamKey: string, - request?: StreamDeleteRequestBody, + request?: LiveStreamDeleteSendRequest, initOverrides?: LiveInitOverrides, ) { if (request) { return this.api.endLiveActivityStream( - { streamKey, liveActivityStreamDeleteRequest: request }, + { + streamKey, + liveActivityStreamDeleteRequest: withoutAlertRootColor( + request, + ) as StreamDeleteRequestBody, + }, initOverrides, ); } diff --git a/tests/resources.test.js b/tests/resources.test.js index 26c0388..d1741a1 100644 --- a/tests/resources.test.js +++ b/tests/resources.test.js @@ -255,6 +255,57 @@ describe("resource wrappers", () => { expect(startSpy).toHaveBeenCalledWith({ liveActivityStartRequest: payload }, undefined); }); + it("passes through alert content_state with icon and badge colors", async () => { + const ActivitySmith = require("../dist/src/index.js"); + const generated = require("../dist/generated/index.js"); + + const streamSpy = vi + .spyOn(generated.LiveActivitiesApi.prototype, "reconcileLiveActivityStream") + .mockResolvedValue({ operation: "started", stream_key: "customer-ops" }); + + const client = new ActivitySmith({ apiKey: "test" }); + const payload = { + content_state: { + title: "Reactivation", + message: "Lumen came back after 2 weeks", + type: ActivitySmith.liveActivityTypes.alert, + color: "red", + icon: { + symbol: "sparkles", + color: "yellow", + }, + badge: { + title: "Customer", + color: "magenta", + }, + }, + }; + + await client.liveActivities.stream("customer-ops", payload); + + expect(streamSpy).toHaveBeenCalledWith( + { + streamKey: "customer-ops", + liveActivityStreamRequest: { + content_state: { + title: "Reactivation", + message: "Lumen came back after 2 weeks", + type: ActivitySmith.liveActivityTypes.alert, + icon: { + symbol: "sparkles", + color: "yellow", + }, + badge: { + title: "Customer", + color: "magenta", + }, + }, + }, + }, + undefined, + ); + }); + it("wraps live activity stream payloads for short methods", async () => { const ActivitySmith = require("../dist/src/index.js"); const generated = require("../dist/generated/index.js"); From 826fcd9793643794be71d6a6dd5a5be1180867a5 Mon Sep 17 00:00:00 2001 From: bardonadam Date: Wed, 27 May 2026 20:53:47 +0700 Subject: [PATCH 2/4] Prepare alert live activity helpers --- README.md | 30 ++++++++++++++++++++- src/ActivitySmith.ts | 59 +++++++++++++++++++++++++++-------------- tests/resources.test.js | 25 +++++------------ 3 files changed, 75 insertions(+), 39 deletions(-) diff --git a/README.md b/README.md index b23a68b..b4e7b49 100644 --- a/README.md +++ b/README.md @@ -126,12 +126,13 @@ await activitysmith.notifications.send({ ## Live Activities -There are four types of Live Activities: +There are five types of Live Activities: - `stats`: best for showing business numbers side by side, such as revenue, sales, new users, conversion, refunds, or any other value you want visible at a glance - `metrics`: best for live percentage values that change often, like server CPU, memory usage, disk usage, or error rate - `segmented_progress`: best for anything that moves through clear stages, like deployments, onboarding flows, backups, ETL pipelines, migrations, and AI agent runs - `progress`: best for tracking real-time progress with percentage, like tasks, backups, migrations, syncs, or uploads +- `alert`: best for status updates, such as feature adoption, reactivation, onboarding blockers, incidents, escalations, and other operational states ### Start & Update Live Activity @@ -216,6 +217,32 @@ await activitysmith.liveActivities.stream("search-reindex", { }); ``` +#### Alert + +

+ Alert Live Activity stream example +

+ +```ts +await activitysmith.liveActivities.stream("customer-ops", { + content_state: ActivitySmith.contentState({ + title: "Reactivation", + message: "Lumen came back after 2 weeks", + type: "alert", + icon: ActivitySmith.alertIcon("cloud.sun", { color: "yellow" }), + badge: ActivitySmith.alertBadge("Customer", { color: "magenta" }), + }), +}); +``` + +The `icon.symbol` value is an Apple SF Symbol name. Browse the catalog with one of these tools: + +- [ActivitySmith app](https://apps.apple.com/us/app/activitysmith/id6752254835) - Open Settings -> SF Symbols to browse 45 hand-picked icons ready to use +- [SF Symbols](https://developer.apple.com/sf-symbols/) - Apple's official macOS app +- [Interactful](https://apps.apple.com/app/interactful/id1528095640) - free third-party iOS app listing all SF Symbols under Foundations -> Iconography + +`icon` and `badge` are optional. If you omit either one, that element is not shown in the Live Activity. + ### End Live Activity Call `endStream(...)` with the same `streamKey` to dismiss the Live Activity. You can include final values before it is removed. By default, iOS removes the Live Activity after two minutes. Set `auto_dismiss_minutes` to choose a different dismissal time, including `0` for immediate dismissal. @@ -238,6 +265,7 @@ await activitysmith.liveActivities.endStream("prod-web-1", { ### Live Activity Action Live Activities can include one optional action button. Use it to open a URL from the Live Activity or trigger a backend webhook. +For alert Live Activities, set `content_state.color` to tint the action button. `icon.color` and `badge.color` only affect the icon and badge.

Live Activity with action button diff --git a/src/ActivitySmith.ts b/src/ActivitySmith.ts index d6ec984..29f3c6e 100644 --- a/src/ActivitySmith.ts +++ b/src/ActivitySmith.ts @@ -59,6 +59,14 @@ export type LiveActivityContentState = Record & { color?: string; }; +type LiveActivityAlertIconOptions = { + color?: string; +}; + +type LiveActivityAlertBadgeOptions = { + color?: string; +}; + type LiveStartSendRequest = Omit & { content_state: LiveActivityContentState; channels?: string[]; @@ -93,18 +101,28 @@ function withTargetChannels( } as T; } -function withoutAlertRootColor( - request: T, -): T { - if (request.content_state?.type !== LiveActivityTypes.alert || !("color" in request.content_state)) { - return request; - } +function compactObject>(value: T): T { + return Object.fromEntries( + Object.entries(value).filter(([, entryValue]) => entryValue !== undefined), + ) as T; +} - const { color: _ignored, ...contentState } = request.content_state; - return { - ...request, - content_state: contentState, - }; +function contentState(value: LiveActivityContentState): LiveActivityContentState { + return compactObject(value); +} + +function alertIcon( + symbol: string, + options: LiveActivityAlertIconOptions = {}, +): LiveActivityAlertIcon { + return compactObject({ symbol, color: options.color }); +} + +function alertBadge( + title: string, + options: LiveActivityAlertBadgeOptions = {}, +): LiveActivityAlertBadge { + return compactObject({ title, color: options.color }); } function hasMediaValue(media: unknown): boolean { @@ -194,8 +212,8 @@ export class LiveActivitiesResource { start(request: LiveStartSendRequest, initOverrides?: LiveInitOverrides) { return this.api.startLiveActivity( { - liveActivityStartRequest: withoutAlertRootColor( - withTargetChannels(request), + liveActivityStartRequest: withTargetChannels( + request, ) as StartRequestBody, }, initOverrides, @@ -204,14 +222,14 @@ export class LiveActivitiesResource { update(request: LiveUpdateSendRequest, initOverrides?: LiveInitOverrides) { return this.api.updateLiveActivity( - { liveActivityUpdateRequest: withoutAlertRootColor(request) as UpdateRequestBody }, + { liveActivityUpdateRequest: request as UpdateRequestBody }, initOverrides, ); } end(request: LiveEndSendRequest, initOverrides?: LiveInitOverrides) { return this.api.endLiveActivity( - { liveActivityEndRequest: withoutAlertRootColor(request) as EndRequestBody }, + { liveActivityEndRequest: request as EndRequestBody }, initOverrides, ); } @@ -220,8 +238,8 @@ export class LiveActivitiesResource { return this.api.reconcileLiveActivityStream( { streamKey, - liveActivityStreamRequest: withoutAlertRootColor( - withTargetChannels(request), + liveActivityStreamRequest: withTargetChannels( + request, ) as StreamRequestBody, }, initOverrides, @@ -237,9 +255,7 @@ export class LiveActivitiesResource { return this.api.endLiveActivityStream( { streamKey, - liveActivityStreamDeleteRequest: withoutAlertRootColor( - request, - ) as StreamDeleteRequestBody, + liveActivityStreamDeleteRequest: request as StreamDeleteRequestBody, }, initOverrides, ); @@ -329,6 +345,9 @@ export class MetricsResource { export class ActivitySmith { public static readonly liveActivityTypes = LiveActivityTypes; + public static readonly contentState = contentState; + public static readonly alertIcon = alertIcon; + public static readonly alertBadge = alertBadge; public readonly notifications: NotificationsResource; public readonly liveActivities: LiveActivitiesResource; diff --git a/tests/resources.test.js b/tests/resources.test.js index d1741a1..505e040 100644 --- a/tests/resources.test.js +++ b/tests/resources.test.js @@ -265,20 +265,14 @@ describe("resource wrappers", () => { const client = new ActivitySmith({ apiKey: "test" }); const payload = { - content_state: { + content_state: ActivitySmith.contentState({ title: "Reactivation", message: "Lumen came back after 2 weeks", type: ActivitySmith.liveActivityTypes.alert, color: "red", - icon: { - symbol: "sparkles", - color: "yellow", - }, - badge: { - title: "Customer", - color: "magenta", - }, - }, + icon: ActivitySmith.alertIcon("cloud.sun", { color: "yellow" }), + badge: ActivitySmith.alertBadge("Customer", { color: "magenta" }), + }), }; await client.liveActivities.stream("customer-ops", payload); @@ -291,14 +285,9 @@ describe("resource wrappers", () => { title: "Reactivation", message: "Lumen came back after 2 weeks", type: ActivitySmith.liveActivityTypes.alert, - icon: { - symbol: "sparkles", - color: "yellow", - }, - badge: { - title: "Customer", - color: "magenta", - }, + color: "red", + icon: { symbol: "cloud.sun", color: "yellow" }, + badge: { title: "Customer", color: "magenta" }, }, }, }, From 57b1d10e36e2e0e2b0f5c9d0b0cbf43bf73da4c5 Mon Sep 17 00:00:00 2001 From: activitysmith-bot Date: Wed, 27 May 2026 11:15:20 +0000 Subject: [PATCH 3/4] chore: regenerate SDK --- generated/models/index.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/generated/models/index.ts b/generated/models/index.ts index f3c88e3..4143470 100644 --- a/generated/models/index.ts +++ b/generated/models/index.ts @@ -191,7 +191,7 @@ export interface ContentStateEnd { */ type?: ContentStateEndTypeEnum; /** - * Optional. Accent color for progress, segmented_progress, and metrics Live Activities. + * Optional. Accent color for progress, segmented_progress, and metrics Live Activities. For alert Live Activities, this tints the action button when action is included. * @type {string} * @memberof ContentStateEnd */ @@ -360,7 +360,7 @@ export interface ContentStateStart { */ type: ContentStateStartTypeEnum; /** - * Optional. Accent color for progress, segmented_progress, and metrics Live Activities. + * Optional. Accent color for progress, segmented_progress, and metrics Live Activities. For alert Live Activities, this tints the action button when action is included. * @type {string} * @memberof ContentStateStart */ @@ -523,7 +523,7 @@ export interface ContentStateUpdate { */ type?: ContentStateUpdateTypeEnum; /** - * Optional. Accent color for progress, segmented_progress, and metrics Live Activities. + * Optional. Accent color for progress, segmented_progress, and metrics Live Activities. For alert Live Activities, this tints the action button when action is included. * @type {string} * @memberof ContentStateUpdate */ @@ -1541,7 +1541,7 @@ export interface StreamContentState { */ type?: StreamContentStateTypeEnum; /** - * Optional. Accent color for progress, segmented_progress, and metrics Live Activities. + * Optional. Accent color for progress, segmented_progress, and metrics Live Activities. For alert Live Activities, this tints the action button when action is included. * @type {string} * @memberof StreamContentState */ From c064a5d0887d3e6c405d5d0ac02742549e74fd55 Mon Sep 17 00:00:00 2001 From: bardonadam Date: Thu, 28 May 2026 15:34:40 +0700 Subject: [PATCH 4/4] docs: title-case Alert Live Activity references Why: - Alert Live Activity should use product-style casing in SDK docs and generated API comments. How: - Update README wording and generated OpenAPI-derived descriptions from alert Live Activities to Alert Live Activities. Verification: - git diff --check Initiated-by: user Implemented-by: codex --- README.md | 2 +- generated/models/index.ts | 12 ++++++------ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index b4e7b49..6beca54 100644 --- a/README.md +++ b/README.md @@ -265,7 +265,7 @@ await activitysmith.liveActivities.endStream("prod-web-1", { ### Live Activity Action Live Activities can include one optional action button. Use it to open a URL from the Live Activity or trigger a backend webhook. -For alert Live Activities, set `content_state.color` to tint the action button. `icon.color` and `badge.color` only affect the icon and badge. +For Alert Live Activities, set `content_state.color` to tint the action button. `icon.color` and `badge.color` only affect the icon and badge.

Live Activity with action button diff --git a/generated/models/index.ts b/generated/models/index.ts index 4143470..fab0751 100644 --- a/generated/models/index.ts +++ b/generated/models/index.ts @@ -191,7 +191,7 @@ export interface ContentStateEnd { */ type?: ContentStateEndTypeEnum; /** - * Optional. Accent color for progress, segmented_progress, and metrics Live Activities. For alert Live Activities, this tints the action button when action is included. + * Optional. Accent color for progress, segmented_progress, and metrics Live Activities. For Alert Live Activities, this tints the action button when action is included. * @type {string} * @memberof ContentStateEnd */ @@ -360,7 +360,7 @@ export interface ContentStateStart { */ type: ContentStateStartTypeEnum; /** - * Optional. Accent color for progress, segmented_progress, and metrics Live Activities. For alert Live Activities, this tints the action button when action is included. + * Optional. Accent color for progress, segmented_progress, and metrics Live Activities. For Alert Live Activities, this tints the action button when action is included. * @type {string} * @memberof ContentStateStart */ @@ -523,7 +523,7 @@ export interface ContentStateUpdate { */ type?: ContentStateUpdateTypeEnum; /** - * Optional. Accent color for progress, segmented_progress, and metrics Live Activities. For alert Live Activities, this tints the action button when action is included. + * Optional. Accent color for progress, segmented_progress, and metrics Live Activities. For Alert Live Activities, this tints the action button when action is included. * @type {string} * @memberof ContentStateUpdate */ @@ -676,7 +676,7 @@ export const LiveActivityActionType = { export type LiveActivityActionType = typeof LiveActivityActionType[keyof typeof LiveActivityActionType]; /** - * Optional badge for alert Live Activities. + * Optional badge for Alert Live Activities. * @export * @interface LiveActivityAlertBadge */ @@ -696,7 +696,7 @@ export interface LiveActivityAlertBadge { color?: LiveActivityColor; } /** - * Optional SF Symbol icon for alert Live Activities. + * Optional SF Symbol icon for Alert Live Activities. * @export * @interface LiveActivityAlertIcon */ @@ -1541,7 +1541,7 @@ export interface StreamContentState { */ type?: StreamContentStateTypeEnum; /** - * Optional. Accent color for progress, segmented_progress, and metrics Live Activities. For alert Live Activities, this tints the action button when action is included. + * Optional. Accent color for progress, segmented_progress, and metrics Live Activities. For Alert Live Activities, this tints the action button when action is included. * @type {string} * @memberof StreamContentState */