tauri-plugin-live-activity 0.1.0-alpha.3 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,12 +1,12 @@
1
1
  # tauri-plugin-live-activity
2
2
 
3
- [![npm](https://img.shields.io/npm/v/tauri-plugin-live-activity/next)](https://www.npmjs.com/package/tauri-plugin-live-activity)
3
+ [![npm](https://img.shields.io/npm/v/tauri-plugin-live-activity)](https://www.npmjs.com/package/tauri-plugin-live-activity)
4
4
  [![crates.io](https://img.shields.io/crates/v/tauri-plugin-live-activity)](https://crates.io/crates/tauri-plugin-live-activity)
5
5
  [![CI](https://github.com/1eedaegon/tauri-plugin-live-activity/actions/workflows/ci.yml/badge.svg)](https://github.com/1eedaegon/tauri-plugin-live-activity/actions/workflows/ci.yml)
6
6
  [![License](https://img.shields.io/badge/license-MIT%20OR%20Apache--2.0-blue)](LICENSE)
7
7
 
8
- Live Activities on iPhone and iPad, and ongoing notifications on Android, for Tauri 2.
9
- Community plugin. **Early alpha.** [한국어](https://github.com/1eedaegon/tauri-plugin-live-activity/blob/main/README.ko.md)
8
+ A Tauri 2 plugin for iOS Live Activities (ActivityKit and Dynamic Island) and Android Live Updates, with an ongoing notification fallback.
9
+ Community plugin. **Before 1.0, a minor version may change the API.** [한국어](https://github.com/1eedaegon/tauri-plugin-live-activity/blob/main/README.ko.md)
10
10
 
11
11
  | Platform | Backend |
12
12
  | --- | --- |
@@ -14,67 +14,115 @@ Community plugin. **Early alpha.** [한국어](https://github.com/1eedaegon/taur
14
14
  | Android API 26+ | Ongoing notifications · Live Update promotion on eligible API 36+ devices |
15
15
  | macOS / Windows / Linux | Not implemented |
16
16
 
17
+ ## Demo
18
+
17
19
  <table>
18
- <tr><th>Mochi demo</th><th>iOS Live Activities</th><th>Android Live Update</th></tr>
20
+ <tr><th>iOS Lock Screen</th><th>iOS Dynamic Island</th></tr>
21
+ <tr>
22
+ <td><img src="https://raw.githubusercontent.com/1eedaegon/tauri-plugin-live-activity/main/docs/images/ios-layout-paths.png" width="400" alt="Mochi's face drawn from SVG paths on the iOS Lock Screen"></td>
23
+ <td><img src="https://raw.githubusercontent.com/1eedaegon/tauri-plugin-live-activity/main/docs/images/ios-island-expanded.png" width="400" alt="Mochi's snack in the expanded Dynamic Island"></td>
24
+ </tr>
25
+ <tr><th>Android notification</th><th>Samsung Now Bar</th></tr>
19
26
  <tr>
20
- <td><img src="https://raw.githubusercontent.com/1eedaegon/tauri-plugin-live-activity/main/docs/images/mochi-demo.png" width="240" alt="Mochi's snack delivery in the Tauri demo app"></td>
21
- <td><img src="https://raw.githubusercontent.com/1eedaegon/tauri-plugin-live-activity/main/docs/images/mochi-ios-live-activity.png" width="240" alt="Mochi's snack delivery and nap timer on the iPhone Lock Screen"></td>
22
- <td><img src="https://raw.githubusercontent.com/1eedaegon/tauri-plugin-live-activity/main/docs/images/mochi-android-live-activity.png" width="240" alt="Mochi's snack delivery shown by Android's native notification UI"></td>
27
+ <td><img src="https://raw.githubusercontent.com/1eedaegon/tauri-plugin-live-activity/main/docs/images/android-layout-android.png" width="400" alt="Mochi's nap as an Android notification with a progress bar and a Wake Mochi action"></td>
28
+ <td><img src="https://raw.githubusercontent.com/1eedaegon/tauri-plugin-live-activity/main/docs/images/samsung-now-bar.png" width="400" alt="Mochi's snack delivery in the One UI Now Bar on a locked Galaxy S23"></td>
23
29
  </tr>
24
30
  </table>
25
31
 
26
- Actual captures from iOS 27.2 Simulator and an Android API 37 emulator.
27
- The demo follows a cat's snack delivery and nap timer, using state supplied by the app.
28
- The iOS demo also includes an [expanded Dynamic Island view](https://raw.githubusercontent.com/1eedaegon/tauri-plugin-live-activity/main/docs/images/mochi-ios-dynamic-island.png).
29
- On older Android versions, the same example uses an
30
- [ongoing notification](https://raw.githubusercontent.com/1eedaegon/tauri-plugin-live-activity/main/docs/images/mochi-android-notification-fallback.png).
32
+ These come from the [demo app](https://github.com/1eedaegon/tauri-plugin-live-activity/tree/main/examples/demo),
33
+ which follows a cat named Mochi. iOS: iPhone 17 simulator, iOS 26.3. Android: emulator,
34
+ Android 16 (API 36). Now Bar: Galaxy S23, One UI 9 (Android 17), locked.
31
35
 
32
36
  ## Install
33
37
 
34
- Requires Tauri 2 and Rust 1.90+.
35
- Android requires API 26+, an updated WebView, and Android SDK 37 to build.
36
- See the [version policy](https://github.com/1eedaegon/tauri-plugin-live-activity/blob/main/docs/compatibility.md) for feature and toolchain requirements.
38
+ Requires Tauri 2 and Rust 1.90+. Run each line from the app's root folder. The npm package
39
+ and the crate must have the same minor version.
37
40
 
38
41
  ```sh
39
- npm install tauri-plugin-live-activity@next
42
+ npm i tauri-plugin-live-activity@0.2 && cargo add tauri-plugin-live-activity@0.2 --manifest-path src-tauri/Cargo.toml
43
+ for p in default allow-request-permissions allow-start allow-update allow-end allow-start-layout allow-update-layout allow-end-layout; do npx tauri permission add live-activity:$p; done
40
44
  ```
41
45
 
42
- `src-tauri/Cargo.toml`:
43
-
44
- ```toml
45
- [dependencies]
46
- tauri-plugin-live-activity = "=0.1.0-alpha.3"
47
- ```
48
-
49
- Register the plugin in `src-tauri/src/lib.rs`:
46
+ Then register the plugin in `src-tauri/src/lib.rs`:
50
47
 
51
48
  ```rust
52
49
  tauri::Builder::default()
53
50
  .plugin(tauri_plugin_live_activity::init())
54
- // ...
55
51
  ```
56
52
 
57
- Add these permissions to your capability file:
53
+ ### iOS
58
54
 
59
- ```json
60
- "permissions": [
61
- "live-activity:default",
62
- "live-activity:allow-request-permissions",
63
- "live-activity:allow-start",
64
- "live-activity:allow-update",
65
- "live-activity:allow-end"
66
- ]
55
+ A widget extension built from the plugin's template draws the activities. Run these on a Mac
56
+ after `tauri ios init`, in one shell. In order, they:
57
+
58
+ 1. find the plugin's folder (`jq` comes with macOS 15 and later);
59
+ 2. copy the widget, the button intent and the extension's Info.plist, which sets
60
+ `TauriLiveActivityLayout` to 2;
61
+ 3. add `NSSupportsLiveActivities` to `src-tauri/Info.ios.plist`, which Tauri merges into the
62
+ app's Info.plist;
63
+ 4. prepare the Tauri dependency of the plugin's Swift package. On an Apple silicon Mac, use
64
+ `--target aarch64-apple-ios-sim`.
65
+
66
+ ```sh
67
+ LA=$(cargo metadata --format-version 1 --manifest-path src-tauri/Cargo.toml | jq -r '.packages[] | select(.name == "tauri-plugin-live-activity") | .manifest_path | rtrimstr("Cargo.toml")')
68
+ mkdir -p src-tauri/gen/apple/LiveActivityWidget && cp "$LA"templates/ios/* src-tauri/gen/apple/LiveActivityWidget/
69
+ /usr/libexec/PlistBuddy -c 'Add :NSSupportsLiveActivities bool true' src-tauri/Info.ios.plist
70
+ IPHONEOS_DEPLOYMENT_TARGET=16.1 cargo check --manifest-path src-tauri/Cargo.toml --target x86_64-apple-ios
67
71
  ```
68
72
 
69
- **iOS needs a Widget Extension.** Follow the [iOS setup](https://github.com/1eedaegon/tauri-plugin-live-activity/blob/main/docs/ios-integration.md).
70
- For Android permission and channel behavior, see [Android setup](https://github.com/1eedaegon/tauri-plugin-live-activity/blob/main/docs/android-integration.md).
73
+ Tauri builds the Xcode project with XcodeGen, and a new target is a block of XcodeGen YAML, so
74
+ this step does not fit on one line. Paste the block, then the line after it:
75
+
76
+ ```sh
77
+ cat > src-tauri/gen/apple/live-activity.yml <<EOF
78
+ packages:
79
+ LiveActivity: { path: "${LA}ios" }
80
+ targets:
81
+ $(sed -n 's/^name: //p' src-tauri/gen/apple/project.yml)_iOS:
82
+ sources: [LiveActivityWidget/LiveActivityActionIntent.swift]
83
+ dependencies: [{ target: LiveActivityWidget }]
84
+ settings: { base: { SWIFT_VERSION: "5.0" } }
85
+ LiveActivityWidget:
86
+ type: app-extension
87
+ platform: iOS
88
+ deploymentTarget: "16.1"
89
+ sources: [LiveActivityWidget]
90
+ dependencies: [{ package: LiveActivity, product: LiveActivityShared }, { package: LiveActivity, product: LiveActivityUI }]
91
+ info: { path: LiveActivityWidget/Info.plist, properties: { NSExtension: { NSExtensionPointIdentifier: com.apple.widgetkit-extension }, TauriLiveActivityLayout: 2 } }
92
+ settings: { base: { PRODUCT_BUNDLE_IDENTIFIER: $(jq -r .identifier src-tauri/tauri.conf.json).widget, SWIFT_VERSION: "5.0" } }
93
+ EOF
94
+ printf '\ninclude: [live-activity.yml]\n' >> src-tauri/gen/apple/project.yml && xcodegen generate --spec src-tauri/gen/apple/project.yml
95
+ ```
71
96
 
72
- ## Example: snacks for Mochi
97
+ The intent file goes into both targets: the widget builds its buttons from it, and the system
98
+ runs it in the app. `tauri ios init` writes `project.yml` again; run the last line after it.
99
+ The extension gets version 1.0 (1); give it the app's version before an App Store upload.
100
+
101
+ ### Android
102
+
103
+ The plugin's manifest adds the notification permissions and its receivers. Builds need Android
104
+ SDK 37. The minimum is Android 8 (API 26); a new Tauri app starts at 24. The first line sets it
105
+ for `tauri android init`, the second for a project generated before:
106
+
107
+ ```sh
108
+ node -e 'f="src-tauri/tauri.conf.json";c=require("./"+f);(c.bundle.android??={}).minSdkVersion=26;require("fs").writeFileSync(f,JSON.stringify(c,null,2)+"\n")'
109
+ perl -pi -e 's/minSdk = 2[0-5]$/minSdk = 26/' src-tauri/gen/android/app/build.gradle.kts
110
+ ```
111
+
112
+ ## Usage
113
+
114
+ The plugin shows state that the app sends. The app still owns the task, background work and
115
+ how often it updates. OS permissions, Focus and promotion rules decide where the activity
116
+ appears. `fallback: 'none'` rejects an unavailable native backend; it cannot force Android to
117
+ promote a notification.
118
+
119
+ Captures: iOS 26.3 on the iPhone 17 simulator, Android 16 on an emulator. The demo adds
120
+ Mochi's colors and padding to the code shown here.
121
+
122
+ ### Progress
73
123
 
74
124
  ```ts
75
- import {
76
- getCapabilities, requestPermissions, start, update, end,
77
- } from 'tauri-plugin-live-activity';
125
+ import { getCapabilities, requestPermissions, start, update, end } from 'tauri-plugin-live-activity';
78
126
 
79
127
  const { platform } = await getCapabilities();
80
128
  await requestPermissions(platform === 'android' ? 'notifications' : 'liveActivities');
@@ -84,55 +132,454 @@ const snack = await start({
84
132
  schemaVersion: 1,
85
133
  template: 'progress',
86
134
  fallback: 'notification',
87
- snapshot: {
88
- revision: 0,
89
- title: 'Mochi’s snack delivery',
90
- status: 'Packing a tiny treat',
91
- progress: 0.15,
92
- },
135
+ snapshot: { revision: 0, title: 'Mochi’s snack delivery', status: 'Packing a tiny treat', progress: 0.15 },
93
136
  });
94
-
95
137
  await update({
96
138
  id: snack.id,
139
+ snapshot: { revision: 1, title: 'Mochi’s snack delivery', status: 'On the way to Mochi', progress: 0.65 },
140
+ });
141
+ await end({ id: snack.id, policy: { kind: 'immediate' } });
142
+ ```
143
+
144
+ Each update sends a complete snapshot with a higher revision. `list()` finds the activities
145
+ again after a restart.
146
+
147
+ <table>
148
+ <tr><th>iOS 26.3 simulator</th><th>Android 16 emulator</th></tr>
149
+ <tr>
150
+ <td><img src="https://raw.githubusercontent.com/1eedaegon/tauri-plugin-live-activity/main/docs/images/ios-v1-progress.png" width="400" alt="Mochi's snack delivery at 65% on the iOS Lock Screen"></td>
151
+ <td><img src="https://raw.githubusercontent.com/1eedaegon/tauri-plugin-live-activity/main/docs/images/android-v1-progress.png" width="400" alt="Mochi's snack delivery with a progress bar in the Android notification shade"></td>
152
+ </tr>
153
+ </table>
154
+
155
+ On iOS the cat comes from the demo's own widget. The plugin's template shows the same title,
156
+ status and bar.
157
+
158
+ ### Timer
159
+
160
+ ```ts
161
+ import { start } from 'tauri-plugin-live-activity';
162
+
163
+ const now = Date.now();
164
+ await start({
165
+ activityKey: 'mochi:nap',
166
+ schemaVersion: 1,
167
+ template: 'timer',
168
+ fallback: 'notification',
97
169
  snapshot: {
98
- revision: 1,
99
- title: 'Mochi’s snack delivery',
100
- status: 'On the way to Mochi',
101
- progress: 0.65,
170
+ revision: 0, title: 'Mochi’s nap', status: 'A little snooze, a little peace',
171
+ timer: { startedAt: now, endsAt: now + 5 * 60_000 },
102
172
  },
103
173
  });
174
+ ```
104
175
 
105
- // When the snacks arrive:
106
- await end({ id: snack.id, policy: { kind: 'immediate' } });
176
+ Times are Unix milliseconds. The system counts down; the app sends nothing per second.
177
+
178
+ <table>
179
+ <tr><th>iOS 26.3 simulator</th><th>Android 16 emulator</th></tr>
180
+ <tr>
181
+ <td><img src="https://raw.githubusercontent.com/1eedaegon/tauri-plugin-live-activity/main/docs/images/ios-v1-timer.png" width="400" alt="Mochi's nap counting down from 4:48 on the iOS Lock Screen"></td>
182
+ <td><img src="https://raw.githubusercontent.com/1eedaegon/tauri-plugin-live-activity/main/docs/images/android-v1-timer.png" width="400" alt="Mochi's nap with the countdown in the Android notification header"></td>
183
+ </tr>
184
+ </table>
185
+
186
+ ## Layouts
187
+
188
+ With a layout the app designs the activity itself: a tree of nodes, a table of styles and a
189
+ map of values. On iOS the plugin's widget template draws it, with no SwiftUI in the app.
190
+ Android shows part of it as a notification. The tree is fixed at start; an update sends new
191
+ values with the same keys and types. Layout, styles and values share ActivityKit's 4 KiB
192
+ limit.
193
+
194
+ ### Text, values and styles
195
+
196
+ ```ts
197
+ import { layout, startLayout, updateLayout, endLayout } from 'tauri-plugin-live-activity';
198
+
199
+ const { vstack, bound, text } = layout;
200
+
201
+ const card = await startLayout({
202
+ activityKey: 'mochi:text',
203
+ schemaVersion: 2,
204
+ fallback: 'notification',
205
+ layout: {
206
+ lockScreen: vstack([
207
+ bound('title', { style: 'title' }),
208
+ bound('status', { style: 'status', unless: 'arrived' }),
209
+ text('Snack time! Made it home', { style: 'done', if: 'arrived' }),
210
+ ], { alignment: 'leading' }),
211
+ },
212
+ styles: {
213
+ title: { font: 'headline' },
214
+ status: { font: 'subheadline', color: 'secondary' },
215
+ done: { font: 'subheadline', weight: 'bold', color: '#C9654D' },
216
+ },
217
+ snapshot: { revision: 0, values: { title: 'Mochi’s snack', status: 'Packing a tiny treat', arrived: false } },
218
+ });
219
+
220
+ await updateLayout({
221
+ id: card.id,
222
+ snapshot: { revision: 1, values: { title: 'Mochi’s snack', status: 'Packing a tiny treat', arrived: true } },
223
+ });
224
+
225
+ await endLayout({ id: card.id, policy: { kind: 'immediate' } });
226
+ ```
227
+
228
+ `bound` shows a value, `text` a literal. `if` shows a node only while a value is truthy,
229
+ `unless` only while it is not. The examples below show only the members after `fallback`; the
230
+ builders are all in `layout`.
231
+
232
+ <table>
233
+ <tr><th></th><th>iOS 26.3 simulator</th><th>Android 16 emulator</th></tr>
234
+ <tr>
235
+ <td><code>arrived: false</code></td>
236
+ <td><img src="https://raw.githubusercontent.com/1eedaegon/tauri-plugin-live-activity/main/docs/images/ios-layout-text.png" width="380" alt="Title and status text on the iOS Lock Screen"></td>
237
+ <td><img src="https://raw.githubusercontent.com/1eedaegon/tauri-plugin-live-activity/main/docs/images/android-layout-text.png" width="380" alt="The same title and status as an Android notification"></td>
238
+ </tr>
239
+ <tr>
240
+ <td><code>arrived: true</code></td>
241
+ <td><img src="https://raw.githubusercontent.com/1eedaegon/tauri-plugin-live-activity/main/docs/images/ios-layout-text-arrived.png" width="380" alt="The status replaced by Snack time in bold coral on iOS"></td>
242
+ <td><img src="https://raw.githubusercontent.com/1eedaegon/tauri-plugin-live-activity/main/docs/images/android-layout-text-arrived.png" width="380" alt="The body replaced by Snack time on Android"></td>
243
+ </tr>
244
+ </table>
245
+
246
+ Android drops styles: the first text is the title and the second the body.
247
+
248
+ ### SF Symbols
249
+
250
+ ```ts
251
+ layout: {
252
+ lockScreen: hstack([
253
+ symbol('cat.fill'),
254
+ symbol('heart.fill', { rendering: 'multicolor' }),
255
+ symbol('speaker.wave.3.fill', { variableBind: 'purr' }),
256
+ bound('title'),
257
+ ], { spacing: 12 }),
258
+ },
259
+ snapshot: { revision: 0, values: { title: 'Purr meter', purr: 0.35 } },
260
+ ```
261
+
262
+ `variableBind` fills a variable symbol from a number between 0 and 1.
263
+
264
+ | iOS 26.3 simulator | Android |
265
+ | --- | --- |
266
+ | <img src="https://raw.githubusercontent.com/1eedaegon/tauri-plugin-live-activity/main/docs/images/ios-layout-symbols.png" width="400" alt="A cat, a multicolor heart and a speaker filled to 35% on the iOS Lock Screen"> | Not shown. Listed as `layout.symbol` in `degradedFeatures`. |
267
+
268
+ ### Image
269
+
270
+ ```ts
271
+ layout: {
272
+ lockScreen: hstack([image('mochi', { resizable: true, style: 'portrait' }), bound('title')], { spacing: 12 }),
273
+ },
274
+ styles: { portrait: { frame: { width: 56, height: 56 } } },
275
+ snapshot: { revision: 0, values: { title: 'Mochi’s portrait' } },
276
+ ```
277
+
278
+ `image` names a picture in the widget extension's asset catalog. Put an `Assets.xcassets`
279
+ folder in `src-tauri/gen/apple/LiveActivityWidget` and run
280
+ `xcodegen generate --spec src-tauri/gen/apple/project.yml`; the widget target picks it up. The
281
+ demo's `mochi` is a PNG drawn from an SVG file.
282
+
283
+ | iOS 26.3 simulator | Android |
284
+ | --- | --- |
285
+ | <img src="https://raw.githubusercontent.com/1eedaegon/tauri-plugin-live-activity/main/docs/images/ios-layout-image.png" width="400" alt="Mochi's portrait from the widget's asset catalog on the iOS Lock Screen"> | Not shown. Listed as `layout.image`. |
286
+
287
+ ### Paths (SVG path data)
288
+
289
+ ```ts
290
+ const head = 'M14 40C12 32 11 22 14 14Q15 11 18 13L35 27Q50 23 65 27L82 13Q85 11 86 14C89 22 88 32 86 40C93 48 94 61 87 71C79 81 66 85 50 85C34 85 21 81 13 71C6 61 7 48 14 40Z';
291
+ layout: {
292
+ lockScreen: zstack([
293
+ path(head, [100, 90], { style: 'fur' }),
294
+ path(head, [100, 90], { stroke: 1.5 }),
295
+ path('M28 54Q33 58 38 54M62 54Q67 58 72 54', [100, 90], { stroke: 2 }),
296
+ path('M48 64Q50 62 52 64Q50 66 48 64Z', [100, 90], { style: 'nose' }),
297
+ ], { style: 'face' }),
298
+ },
299
+ styles: { face: { frame: { width: 60, height: 54 } }, fur: { color: '#F6E9CD' }, nose: { color: '#C9654D' } },
300
+ snapshot: { revision: 0, values: {} },
301
+ ```
302
+
303
+ `path` draws the `d` of an SVG `<path>` in a `box` of `[width, height]`, scaled to fit. It is
304
+ filled, or stroked when `stroke` gives a line width. `d` takes `M L H V C S Q T Z`, up to
305
+ 512 bytes. Arcs (`A`) are not supported; convert them to curves first.
306
+
307
+ | iOS 26.3 simulator | Android |
308
+ | --- | --- |
309
+ | <img src="https://raw.githubusercontent.com/1eedaegon/tauri-plugin-live-activity/main/docs/images/ios-layout-paths.png" width="400" alt="Mochi's face drawn from four paths on the iOS Lock Screen"> | Not shown. Listed as `layout.path`. |
310
+
311
+ ### Gauge
312
+
313
+ ```ts
314
+ layout: {
315
+ lockScreen: hstack([gauge('bowl', { max: 100, kind: 'circular' }), gauge('bowl', { max: 100 })], { spacing: 12 }),
316
+ },
317
+ snapshot: { revision: 0, values: { bowl: 80 } },
318
+ ```
319
+
320
+ A gauge shows a number between `min` and `max` (0 and 1 when left out), as a ring or a bar.
321
+
322
+ | iOS 26.3 simulator | Android |
323
+ | --- | --- |
324
+ | <img src="https://raw.githubusercontent.com/1eedaegon/tauri-plugin-live-activity/main/docs/images/ios-layout-gauge.png" width="400" alt="A circular and a linear gauge at 80 of 100 on the iOS Lock Screen"> | Not shown. Listed as `layout.gauge`. |
325
+
326
+ ### Progress
327
+
328
+ ```ts
329
+ layout: {
330
+ lockScreen: vstack([bound('title'), progress('done', { kind: 'circular' }), progress('done')]),
331
+ },
332
+ snapshot: { revision: 0, values: { title: 'Mochi’s snack delivery', done: 0.65 } },
107
333
  ```
108
334
 
109
- Use increasing revisions and send a complete snapshot on each update. `list()`
110
- reconciles activities after an app restart. A timer uses `template: 'timer'` and
111
- `timer: { startedAt, endsAt }` instead of `progress`; times are Unix milliseconds.
335
+ A progress view shows a number from 0 to 1 as a bar or a ring.
112
336
 
113
- The plugin displays state. Your app still owns the underlying task, background execution,
114
- and update frequency. OS permissions, Focus settings and promotion policy control
115
- where it appears. `fallback: 'none'` rejects an unavailable native backend; it cannot
116
- force Android to promote a notification.
337
+ <table>
338
+ <tr><th>iOS 26.3 simulator</th><th>Android 16 emulator</th></tr>
339
+ <tr>
340
+ <td><img src="https://raw.githubusercontent.com/1eedaegon/tauri-plugin-live-activity/main/docs/images/ios-layout-progress.png" width="400" alt="A ring and a bar at 65% on the iOS Lock Screen"></td>
341
+ <td><img src="https://raw.githubusercontent.com/1eedaegon/tauri-plugin-live-activity/main/docs/images/android-layout-progress.png" width="400" alt="The first progress view as the bar of an Android notification"></td>
342
+ </tr>
343
+ </table>
117
344
 
118
- ## Try it locally
345
+ Android turns the first progress view into the notification's bar.
346
+
347
+ ### Timer
348
+
349
+ ```ts
350
+ layout: {
351
+ lockScreen: vstack([bound('title'), timerText('start', 'end')]),
352
+ },
353
+ snapshot: { revision: 0, values: { title: 'Mochi’s nap', start: Date.now(), end: Date.now() + 5 * 60_000 } },
354
+ ```
355
+
356
+ `timerText` counts down to the `end` value, or up from `start` with `countsDown: false`.
357
+
358
+ <table>
359
+ <tr><th>iOS 26.3 simulator</th><th>Android 16 emulator</th></tr>
360
+ <tr>
361
+ <td><img src="https://raw.githubusercontent.com/1eedaegon/tauri-plugin-live-activity/main/docs/images/ios-layout-timer.png" width="400" alt="A countdown at 4:50 under Mochi's nap on the iOS Lock Screen"></td>
362
+ <td><img src="https://raw.githubusercontent.com/1eedaegon/tauri-plugin-live-activity/main/docs/images/android-layout-timer.png" width="400" alt="The countdown as the chronometer in the Android notification header"></td>
363
+ </tr>
364
+ </table>
365
+
366
+ Android shows the first timer as the notification's chronometer.
367
+
368
+ ### Stacks
369
+
370
+ ```ts
371
+ layout: {
372
+ lockScreen: vstack([
373
+ hstack([zstack([symbol('circle.fill'), symbol('pawprint.fill', { style: 'mark' })]), bound('title'), spacer(), bound('eta')]),
374
+ divider(),
375
+ hstack([bound('from'), spacer(), bound('to')]),
376
+ ], { spacing: 10 }),
377
+ },
378
+ styles: { mark: { size: 13, color: 'white' } },
379
+ snapshot: { revision: 0, values: { title: 'Snack route', eta: '4 min', from: 'Kitchen', to: 'Mochi’s bowl' } },
380
+ ```
119
381
 
120
- The [demo](https://github.com/1eedaegon/tauri-plugin-live-activity/tree/main/examples/demo)
121
- includes snack delivery, a nap timer and lifecycle checks through the real Tauri bridge.
382
+ `vstack`, `hstack` and `zstack` put nodes in a column, in a row or on top of each other.
383
+ `spacer` takes the free space and `divider` draws a line.
384
+
385
+ | iOS 26.3 simulator | Android |
386
+ | --- | --- |
387
+ | <img src="https://raw.githubusercontent.com/1eedaegon/tauri-plugin-live-activity/main/docs/images/ios-layout-stacks.png" width="400" alt="A paw badge, title and time in a row, a line, and From and To below on the iOS Lock Screen"> | Only the first two texts, as the title and the body. Not captured. |
388
+
389
+ ### Buttons
390
+
391
+ ```ts
392
+ layout: {
393
+ lockScreen: vstack([
394
+ bound('title'),
395
+ bound('status'),
396
+ hstack([button('pet', text('Pet Mochi')), button('done', text('All done'))], { spacing: 8 }),
397
+ ], { alignment: 'leading' }),
398
+ },
399
+ snapshot: { revision: 0, values: { title: 'Mochi wants attention', status: 'No pets yet', pets: 0 } },
400
+ ```
401
+
402
+ A tap reports the button's action ID. It does not change the activity: update or end it in a
403
+ handler. `onAction` hears taps while the web view runs:
404
+
405
+ ```ts
406
+ import { endLayout, onAction } from 'tauri-plugin-live-activity';
407
+
408
+ await onAction(async ({ actionId, activityId }) => {
409
+ if (actionId === 'done') await endLayout({ id: activityId, policy: { kind: 'immediate' } });
410
+ });
411
+ ```
412
+
413
+ The system can start the app in the background for a tap, and Tauri may then create no web
414
+ view. Taps that must work then belong in Rust. Register the handler on the built `App`
415
+ before `run`:
416
+
417
+ ```rust
418
+ use tauri_plugin_live_activity::{EndInput, EndPolicy, LiveActivityExt};
419
+
420
+ let app = tauri::Builder::default()
421
+ .plugin(tauri_plugin_live_activity::init())
422
+ .build(tauri::generate_context!())
423
+ .expect("error while building tauri application");
424
+ let handle = app.handle().clone();
425
+ app.live_activity().on_action(move |event| {
426
+ if event.action_id == "done" {
427
+ let input = EndInput { id: event.activity_id, policy: EndPolicy::Immediate {} };
428
+ let _ = handle.live_activity().end_layout(input);
429
+ }
430
+ });
431
+ app.run(|_, _| {});
432
+ ```
433
+
434
+ Rust handlers run first, on a plugin thread, and may call the plugin. `onAction` needs
435
+ `core:event:default`, which a new app has.
436
+
437
+ <table>
438
+ <tr><th></th><th>iOS 26.3 simulator</th><th>Android 16 emulator</th></tr>
439
+ <tr>
440
+ <td>Start</td>
441
+ <td><img src="https://raw.githubusercontent.com/1eedaegon/tauri-plugin-live-activity/main/docs/images/ios-layout-buttons-before.png" width="380" alt="Pet Mochi and All done buttons on the iOS Lock Screen"></td>
442
+ <td><img src="https://raw.githubusercontent.com/1eedaegon/tauri-plugin-live-activity/main/docs/images/android-layout-buttons-before.png" width="380" alt="Pet Mochi and All done as Android notification actions"></td>
443
+ </tr>
444
+ <tr>
445
+ <td>After taps on Pet Mochi</td>
446
+ <td><img src="https://raw.githubusercontent.com/1eedaegon/tauri-plugin-live-activity/main/docs/images/ios-layout-buttons-after.png" width="380" alt="The status reads Purring. 1 pet so far after one tap on iOS"></td>
447
+ <td><img src="https://raw.githubusercontent.com/1eedaegon/tauri-plugin-live-activity/main/docs/images/android-layout-buttons-after.png" width="380" alt="The body reads Purring. 2 pets so far after two taps on Android"></td>
448
+ </tr>
449
+ </table>
450
+
451
+ The demo's Rust handler counts the pets and updates the status. Buttons need iOS 17
452
+ (`getCapabilities()` reports `actions`). Android shows up to three buttons as notification
453
+ actions. On Android, a tap that starts a killed app waits in memory and is lost if the Tauri
454
+ runtime does not start.
455
+
456
+ ### Dynamic Island
457
+
458
+ ```ts
459
+ layout: {
460
+ lockScreen: vstack([bound('title'), bound('status'), progress('done')]),
461
+ island: {
462
+ expanded: {
463
+ leading: image('mochi', { resizable: true, style: 'portrait' }),
464
+ center: bound('title'),
465
+ trailing: bound('eta'),
466
+ bottom: vstack([bound('status'), progress('done')]),
467
+ },
468
+ compactLeading: symbol('pawprint.fill'),
469
+ compactTrailing: bound('eta'),
470
+ minimal: progress('done', { kind: 'circular' }),
471
+ },
472
+ keyline: '#A3D9C2',
473
+ },
474
+ styles: { portrait: { frame: { width: 44, height: 44 } } },
475
+ snapshot: { revision: 0, values: { title: 'Mochi’s snack', status: 'Packing a tiny treat', done: 0.15, eta: '9 min' } },
476
+ ```
477
+
478
+ `island` gives each part its own nodes. A part left out gets a default: the expanded view
479
+ shows the lock screen tree, compact leading and minimal show its first symbol. `keyline` tints
480
+ the island's edge.
481
+
482
+ <table>
483
+ <tr><th>Compact</th><th>Minimal</th><th>Expanded</th></tr>
484
+ <tr>
485
+ <td><img src="https://raw.githubusercontent.com/1eedaegon/tauri-plugin-live-activity/main/docs/images/ios-island-compact.png" width="270" alt="A paw on the left and 9 min on the right of the compact Dynamic Island"></td>
486
+ <td><img src="https://raw.githubusercontent.com/1eedaegon/tauri-plugin-live-activity/main/docs/images/ios-island-minimal.png" width="180" alt="A progress ring in the minimal Dynamic Island"></td>
487
+ <td><img src="https://raw.githubusercontent.com/1eedaegon/tauri-plugin-live-activity/main/docs/images/ios-island-expanded.png" width="265" alt="Mochi's portrait, title, time, status and bar in the expanded Dynamic Island"></td>
488
+ </tr>
489
+ </table>
490
+
491
+ iOS 26.3 on the iPhone 17 simulator. Android has no Dynamic Island and lists `layout.island`.
492
+
493
+ ### Android
494
+
495
+ A notification cannot draw a layout. Android takes these parts of the lock screen tree, in
496
+ order, with the current values:
497
+
498
+ | Layout | Notification |
499
+ | --- | --- |
500
+ | first `text` | title |
501
+ | second `text` | body |
502
+ | first `progress` | progress bar |
503
+ | first `timerText` | chronometer |
504
+ | up to three `button`s | actions, labelled with their first text |
505
+ | `android.visibility` | lock screen visibility: `public`, `private` (default) or `secret` |
506
+
507
+ Everything else is listed in `degradedFeatures`, here `layout.symbol` and `layout.styles`:
508
+
509
+ ```ts
510
+ layout: {
511
+ lockScreen: vstack([
512
+ hstack([symbol('moon.zzz.fill'), bound('title', { style: 'title' })], { spacing: 8 }),
513
+ bound('status'),
514
+ progress('sleep'),
515
+ button('wake', text('Wake Mochi')),
516
+ ], { alignment: 'leading' }),
517
+ },
518
+ styles: { title: { font: 'headline' } },
519
+ snapshot: { revision: 0, values: { title: 'Mochi’s nap', status: 'Dozing off', sleep: 0.2 } },
520
+ android: { visibility: 'public' },
521
+ ```
522
+
523
+ | iOS | Android 16 emulator |
524
+ | --- | --- |
525
+ | Drawn as written, like the examples above. | <img src="https://raw.githubusercontent.com/1eedaegon/tauri-plugin-live-activity/main/docs/images/android-layout-android.png" width="400" alt="Mochi's nap at 60% with a Wake Mochi action in the Android notification shade"> |
526
+
527
+ The capture is after an update to `Fast asleep` and `0.6`. On Android 16 and later the plugin
528
+ requests promotion to a Live Update where the app may post one; `promotionState` reports what
529
+ Android did.
530
+
531
+ ### Samsung Now Bar
532
+
533
+ One UI shows a promoted notification in the Now Bar only for apps on Samsung's allow list, or
534
+ for every app while the user has the developer option "Live notifications for all apps" on.
535
+ Samsung adds apps to the list privately; no public way to apply was found. To show the title
536
+ and the body there on Android 17 and later, turn on the plugin's One UI extras:
537
+
538
+ ```sh
539
+ node -e 'f="src-tauri/tauri.conf.json";c=require("./"+f);((c.plugins??={})["live-activity"]??={}).android={samsungNowBar:true};require("fs").writeFileSync(f,JSON.stringify(c,null,2)+"\n")'
540
+ ```
541
+
542
+ | Galaxy S23, One UI 9 (Android 17), locked |
543
+ | --- |
544
+ | <img src="https://raw.githubusercontent.com/1eedaegon/tauri-plugin-live-activity/main/docs/images/samsung-now-bar.png" width="400" alt="Mochi's snack delivery and On the way to Mochi in the One UI Now Bar"> |
545
+
546
+ This is the demo's Now Bar example (title, status and a progress bar, with
547
+ `android: { visibility: 'public' }`) after one update, with the developer option on. The
548
+ extras do not put the app on the allow list. An app that Samsung lists without its `RON` flag
549
+ also needs this manifest entry, which the plugin does not add:
550
+
551
+ ```sh
552
+ f=src-tauri/gen/android/app/src/main/AndroidManifest.xml; grep -q ongoing_activity $f || perl -0pi -e 's|<application\b[^>]*>|$&\n <meta-data android:name="com.samsung.android.support.ongoing_activity" android:value="true" />|' $f
553
+ ```
554
+
555
+ ## Plugin configuration
556
+
557
+ `plugins.live-activity` in `tauri.conf.json` is optional:
558
+
559
+ ```json
560
+ { "plugins": { "live-activity": { "receipts": "redacted", "android": { "samsungNowBar": true } } } }
561
+ ```
562
+
563
+ - `receipts`: iOS stores a receipt of each accepted request, so that a late ActivityKit read
564
+ cannot bring back an older revision, also after a restart. `full`, the default, keeps the
565
+ snapshot and its text. `redacted` keeps only a SHA-256 digest of it; a digest hides only
566
+ values that cannot be guessed. Android ignores the option for now and stores records in
567
+ full in app-private preferences.
568
+ - `android.samsungNowBar`: the [Samsung Now Bar](#samsung-now-bar) extras; off when absent.
569
+
570
+ ## Try the demo
571
+
572
+ <img src="https://raw.githubusercontent.com/1eedaegon/tauri-plugin-live-activity/main/docs/images/ios-demo-picker.png" width="300" alt="The demo's Layouts tab with one chip per example">
573
+
574
+ The demo's Layouts tab starts each layout example of this page. From the repository root,
575
+ with the mobile toolchain configured:
122
576
 
123
577
  ```sh
124
- npm ci
125
- npm test
126
- npm run build:demo
127
- # With the mobile toolchain configured:
128
- cd examples/demo
129
- npm run tauri -- android dev
130
- # or: npm run tauri -- ios dev
131
- ```
132
-
133
- See [validation results](https://github.com/1eedaegon/tauri-plugin-live-activity/blob/main/docs/validation.md)
134
- and [OS coverage](https://github.com/1eedaegon/tauri-plugin-live-activity/blob/main/docs/platform-coverage.md)
135
- for tested versions and remaining device checks.
578
+ npm ci && npm run build:demo && cd examples/demo && npm run tauri -- ios dev
579
+ ```
580
+
581
+ Use `android dev` for Android. The [docs](https://github.com/1eedaegon/tauri-plugin-live-activity/tree/main/docs)
582
+ hold the request contract, the setup details and the test records.
136
583
 
137
584
  ## License
138
585