Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
69 changes: 61 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ See [API reference](https://activitysmith.com/docs/api-reference/introduction)
- [Start & Update Live Activity](#start--update-live-activity)
- [End Live Activity](#end-live-activity)
- [Live Activity Action](#live-activity-action)
- [Icons and Badges](#icons-and-badges)
- [Live Activity Colors](#live-activity-colors)
- [Channels](#channels)
- [Widgets](#widgets)

Expand Down Expand Up @@ -235,14 +237,6 @@ await activitysmith.liveActivities.stream("customer-ops", {
});
```

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.
Expand Down Expand Up @@ -316,6 +310,65 @@ await activitysmith.liveActivities.stream("search-reindex", {
});
```

### Icons and Badges

Add more context to Live Activities with icons and badges.

#### Icon

Supported Live Activity types: `stats`, `metrics`, `progress`, `segmented_progress`, and `alert`.

<p align="center">
<img src="https://cdn.activitysmith.com/features/metrics-live-activity-with-icon.png" alt="Metrics Live Activity with an SF Symbol icon on the iPhone Lock Screen" width="680" />
</p>

```ts
await activitysmith.liveActivities.stream("prod-web-1", {
content_state: ActivitySmith.contentState({
title: "Server Health",
subtitle: "prod-web-1",
type: "metrics",
icon: ActivitySmith.alertIcon("server.rack", { color: "blue" }),
metrics: [
{ label: "CPU", value: 18, unit: "%" },
{ label: "MEM", value: 42, unit: "%" },
],
}),
});
```

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

#### Badge

Badges are supported by `alert`, `progress`, and `segmented_progress` Live Activities.

<p align="center">
<img src="https://cdn.activitysmith.com/features/progress-live-activity-with-badge.png" alt="Progress Live Activity with a badge on the iPhone Lock Screen" width="680" />
</p>

```ts
await activitysmith.liveActivities.stream("nightly-database-backup", {
content_state: ActivitySmith.contentState({
title: "Nightly Database Backup",
subtitle: "verify restore",
type: "progress",
badge: ActivitySmith.alertBadge("S3", { color: "cyan" }),
percentage: 62,
}),
});
```

### Live Activity Colors

Choose from these colors for the Live Activity accent, including progress bars and action buttons, or apply them to an individual icon or badge:

`lime`, `green`, `cyan`, `blue`, `purple`, `magenta`, `red`, `orange`, `yellow`, `gray`

## Channels

Channels are used to target specific team members or devices. Can be used for both push notifications and live activities.
Expand Down
32 changes: 16 additions & 16 deletions generated/models/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,7 @@ export interface ChannelTarget {
channels: Array<string>;
}
/**
* End payload requires title. For segmented_progress include current_step and optionally number_of_steps. For progress include percentage or value with upper_limit. For metrics and stats include a non-empty metrics array. For alert include message, with optional icon and badge. Type is optional when ending an existing activity. You can send an updated number_of_steps here if the workflow changed after start.
* End payload requires title. For segmented_progress include current_step and optionally number_of_steps. For progress include percentage or value with upper_limit. For metrics and stats include a non-empty metrics array. For alert include message. Optional icon is supported by all Live Activity types. Optional badge is supported by alert, progress, and segmented_progress. Type is optional when ending an existing activity. You can send an updated number_of_steps here if the workflow changed after start.
* @export
* @interface ContentStateEnd
*/
Expand Down Expand Up @@ -173,13 +173,13 @@ export interface ContentStateEnd {
*/
message?: string;
/**
* Optional SF Symbol icon for type=alert.
* Optional SF Symbol icon. Supported by alert, progress, segmented_progress, metrics, and stats.
* @type {LiveActivityAlertIcon}
* @memberof ContentStateEnd
*/
icon?: LiveActivityAlertIcon;
/**
* Optional badge for type=alert.
* Optional badge. Supported by alert, progress, and segmented_progress.
* @type {LiveActivityAlertBadge}
* @memberof ContentStateEnd
*/
Expand Down Expand Up @@ -281,7 +281,7 @@ export const ContentStateEndStepColorsEnum = {
export type ContentStateEndStepColorsEnum = typeof ContentStateEndStepColorsEnum[keyof typeof ContentStateEndStepColorsEnum];

/**
* Start payload requires title and type. For segmented_progress include number_of_steps and current_step. For progress include percentage or value with upper_limit. For metrics and stats include a non-empty metrics array. For alert include message, with optional icon and badge. For segmented_progress, number_of_steps is not locked and can be changed in later update or end calls.
* Start payload requires title and type. For segmented_progress include number_of_steps and current_step. For progress include percentage or value with upper_limit. For metrics and stats include a non-empty metrics array. For alert include message. Optional icon is supported by all Live Activity types. Optional badge is supported by alert, progress, and segmented_progress. For segmented_progress, number_of_steps is not locked and can be changed in later update or end calls.
* @export
* @interface ContentStateStart
*/
Expand Down Expand Up @@ -342,13 +342,13 @@ export interface ContentStateStart {
*/
message?: string;
/**
* Optional SF Symbol icon for type=alert.
* Optional SF Symbol icon. Supported by alert, progress, segmented_progress, metrics, and stats.
* @type {LiveActivityAlertIcon}
* @memberof ContentStateStart
*/
icon?: LiveActivityAlertIcon;
/**
* Optional badge for type=alert.
* Optional badge. Supported by alert, progress, and segmented_progress.
* @type {LiveActivityAlertBadge}
* @memberof ContentStateStart
*/
Expand Down Expand Up @@ -444,7 +444,7 @@ export const ContentStateStartStepColorsEnum = {
export type ContentStateStartStepColorsEnum = typeof ContentStateStartStepColorsEnum[keyof typeof ContentStateStartStepColorsEnum];

/**
* Update payload requires title. For segmented_progress include current_step and optionally number_of_steps. For progress include percentage or value with upper_limit. For metrics and stats include a non-empty metrics array. For alert include message, with optional icon and badge. Type is optional when updating an existing activity. You can increase or decrease number_of_steps during updates.
* Update payload requires title. For segmented_progress include current_step and optionally number_of_steps. For progress include percentage or value with upper_limit. For metrics and stats include a non-empty metrics array. For alert include message. Optional icon is supported by all Live Activity types. Optional badge is supported by alert, progress, and segmented_progress. Type is optional when updating an existing activity. You can increase or decrease number_of_steps during updates.
* @export
* @interface ContentStateUpdate
*/
Expand Down Expand Up @@ -505,13 +505,13 @@ export interface ContentStateUpdate {
*/
message?: string;
/**
* Optional SF Symbol icon for type=alert.
* Optional SF Symbol icon. Supported by alert, progress, segmented_progress, metrics, and stats.
* @type {LiveActivityAlertIcon}
* @memberof ContentStateUpdate
*/
icon?: LiveActivityAlertIcon;
/**
* Optional badge for type=alert.
* Optional badge. Supported by alert, progress, and segmented_progress.
* @type {LiveActivityAlertBadge}
* @memberof ContentStateUpdate
*/
Expand Down Expand Up @@ -646,7 +646,7 @@ export interface LiveActivityAction {
*/
type: LiveActivityActionType;
/**
* HTTPS URL. For open_url it is opened in browser. For webhook it is called by ActivitySmith backend.
* Action URL. For open_url, use an HTTPS or shortcuts:// URL. For webhook, use an HTTPS URL called by the ActivitySmith backend.
* @type {string}
* @memberof LiveActivityAction
*/
Expand Down Expand Up @@ -676,7 +676,7 @@ export const LiveActivityActionType = {
export type LiveActivityActionType = typeof LiveActivityActionType[keyof typeof LiveActivityActionType];

/**
* Optional badge for Alert Live Activities.
* Optional badge for Live Activities.
* @export
* @interface LiveActivityAlertBadge
*/
Expand All @@ -696,7 +696,7 @@ export interface LiveActivityAlertBadge {
color?: LiveActivityColor;
}
/**
* Optional SF Symbol icon for Alert Live Activities.
* Optional SF Symbol icon for Live Activities.
* @export
* @interface LiveActivityAlertIcon
*/
Expand Down Expand Up @@ -1313,7 +1313,7 @@ export interface PushNotificationAction {
*/
type: PushNotificationActionType;
/**
* HTTPS URL. For open_url it is opened in browser. For webhook it is called by ActivitySmith backend.
* Action URL. For open_url, use an HTTPS or shortcuts:// URL. For webhook, use an HTTPS URL called by the ActivitySmith backend.
* @type {string}
* @memberof PushNotificationAction
*/
Expand Down Expand Up @@ -1374,7 +1374,7 @@ export interface PushNotificationRequest {
*/
media?: string;
/**
* Optional HTTPS URL opened when user taps the notification body. Overrides the default tap target from `media` when both are provided.
* Optional HTTPS or shortcuts:// URL opened when user taps the notification body. Overrides the default tap target from `media` when both are provided.
* @type {string}
* @memberof PushNotificationRequest
*/
Expand Down Expand Up @@ -1571,13 +1571,13 @@ export interface StreamContentState {
*/
message?: string;
/**
* Optional SF Symbol icon for type=alert.
* Optional SF Symbol icon. Supported by alert, progress, segmented_progress, metrics, and stats.
* @type {LiveActivityAlertIcon}
* @memberof StreamContentState
*/
icon?: LiveActivityAlertIcon;
/**
* Optional badge for type=alert.
* Optional badge. Supported by alert, progress, and segmented_progress.
* @type {LiveActivityAlertBadge}
* @memberof StreamContentState
*/
Expand Down
110 changes: 108 additions & 2 deletions tests/resources.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,50 @@ describe("resource wrappers", () => {
expect(sendSpy).toHaveBeenCalledWith({ pushNotificationRequest: payload }, undefined);
});

it("preserves shortcuts redirection for notifications.send", async () => {
const ActivitySmith = require("../dist/src/index.js");
const generated = require("../dist/generated/index.js");

const sendSpy = vi
.spyOn(generated.PushNotificationsApi.prototype, "sendPushNotification")
.mockResolvedValue({ success: true });

const client = new ActivitySmith({ apiKey: "test" });
const payload = {
title: "Task finished",
redirection: "shortcuts://run-shortcut?name=Jarvis",
};

await client.notifications.send(payload);

expect(sendSpy).toHaveBeenCalledWith({ pushNotificationRequest: payload }, undefined);
});

it("preserves shortcuts open_url actions for notifications.send", async () => {
const ActivitySmith = require("../dist/src/index.js");
const generated = require("../dist/generated/index.js");

const sendSpy = vi
.spyOn(generated.PushNotificationsApi.prototype, "sendPushNotification")
.mockResolvedValue({ success: true });

const client = new ActivitySmith({ apiKey: "test" });
const payload = {
title: "Task finished",
actions: [
{
title: "Run Shortcut",
type: "open_url",
url: "shortcuts://run-shortcut?name=Jarvis",
},
],
};

await client.notifications.send(payload);

expect(sendSpy).toHaveBeenCalledWith({ pushNotificationRequest: payload }, undefined);
});

it("rejects media with actions for notifications.send", async () => {
const ActivitySmith = require("../dist/src/index.js");

Expand Down Expand Up @@ -295,6 +339,68 @@ describe("resource wrappers", () => {
);
});

it("passes through icon and badge on non-alert live activity types", 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: "prod-web-1" });

const client = new ActivitySmith({ apiKey: "test" });
await client.liveActivities.stream("prod-web-1", {
content_state: ActivitySmith.contentState({
title: "Server Health",
subtitle: "prod-web-1",
type: ActivitySmith.liveActivityTypes.metrics,
icon: ActivitySmith.alertIcon("server.rack", { color: "blue" }),
metrics: [{ label: "CPU", value: 18, unit: "%" }],
}),
});
await client.liveActivities.stream("nightly-database-backup", {
content_state: ActivitySmith.contentState({
title: "Nightly Database Backup",
subtitle: "verify restore",
type: ActivitySmith.liveActivityTypes.progress,
badge: ActivitySmith.alertBadge("S3", { color: "cyan" }),
percentage: 62,
}),
});

expect(streamSpy).toHaveBeenNthCalledWith(
1,
{
streamKey: "prod-web-1",
liveActivityStreamRequest: {
content_state: {
title: "Server Health",
subtitle: "prod-web-1",
type: ActivitySmith.liveActivityTypes.metrics,
icon: { symbol: "server.rack", color: "blue" },
metrics: [{ label: "CPU", value: 18, unit: "%" }],
},
},
},
undefined,
);
expect(streamSpy).toHaveBeenNthCalledWith(
2,
{
streamKey: "nightly-database-backup",
liveActivityStreamRequest: {
content_state: {
title: "Nightly Database Backup",
subtitle: "verify restore",
type: ActivitySmith.liveActivityTypes.progress,
badge: { title: "S3", color: "cyan" },
percentage: 62,
},
},
},
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");
Expand Down Expand Up @@ -394,7 +500,7 @@ describe("resource wrappers", () => {
action: {
title: "Open Workflow",
type: "open_url",
url: "https://github.com/acme/payments-api/actions/runs/1234567890",
url: "shortcuts://run-shortcut?name=Deploy%20Status",
},
};

Expand Down Expand Up @@ -426,7 +532,7 @@ describe("resource wrappers", () => {
action: {
title: "Open Workflow",
type: "open_url",
url: "https://github.com/acme/payments-api/actions/runs/1234567890",
url: "shortcuts://run-shortcut?name=Deploy%20Status",
},
};

Expand Down