diff --git a/README.md b/README.md
index b23a68b..6beca54 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
+
+
+
+
+
+```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.
diff --git a/generated/models/index.ts b/generated/models/index.ts
index f3c88e3..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.
+ * 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
*/
@@ -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.
+ * 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
*/
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..29f3c6e 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,65 @@ 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 LiveActivityAlertIconOptions = {
+ color?: string;
+};
+
+type LiveActivityAlertBadgeOptions = {
+ 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 +101,30 @@ function withTargetChannels(
} as T;
}
+function compactObject>(value: T): T {
+ return Object.fromEntries(
+ Object.entries(value).filter(([, entryValue]) => entryValue !== undefined),
+ ) as T;
+}
+
+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 {
if (typeof media === "string") {
return media.trim().length > 0;
@@ -140,24 +211,36 @@ export class LiveActivitiesResource {
start(request: LiveStartSendRequest, initOverrides?: LiveInitOverrides) {
return this.api.startLiveActivity(
- { liveActivityStartRequest: withTargetChannels(request) },
+ {
+ liveActivityStartRequest: 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: 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: request as EndRequestBody },
+ initOverrides,
+ );
}
stream(streamKey: string, request: LiveStreamSendRequest, initOverrides?: LiveInitOverrides) {
return this.api.reconcileLiveActivityStream(
{
streamKey,
- liveActivityStreamRequest: withTargetChannels(request),
+ liveActivityStreamRequest: withTargetChannels(
+ request,
+ ) as StreamRequestBody,
},
initOverrides,
);
@@ -165,12 +248,15 @@ export class LiveActivitiesResource {
endStream(
streamKey: string,
- request?: StreamDeleteRequestBody,
+ request?: LiveStreamDeleteSendRequest,
initOverrides?: LiveInitOverrides,
) {
if (request) {
return this.api.endLiveActivityStream(
- { streamKey, liveActivityStreamDeleteRequest: request },
+ {
+ streamKey,
+ liveActivityStreamDeleteRequest: request as StreamDeleteRequestBody,
+ },
initOverrides,
);
}
@@ -259,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 26c0388..505e040 100644
--- a/tests/resources.test.js
+++ b/tests/resources.test.js
@@ -255,6 +255,46 @@ 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: ActivitySmith.contentState({
+ title: "Reactivation",
+ message: "Lumen came back after 2 weeks",
+ type: ActivitySmith.liveActivityTypes.alert,
+ color: "red",
+ icon: ActivitySmith.alertIcon("cloud.sun", { color: "yellow" }),
+ badge: ActivitySmith.alertBadge("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,
+ color: "red",
+ icon: { symbol: "cloud.sun", 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");