expo-live-activity 0.2.0-alpha2 → 0.2.0-alpha4

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,3 +1,6 @@
1
+ > [!WARNING]
2
+ > This library is in early development stage, breaking changes can be introduced in minor version upgrades.
3
+
1
4
  # expo-live-activity
2
5
 
3
6
  `expo-live-activity` is a React Native module designed for use with Expo to manage and display live activities on iOS devices exclusively. This module leverages the Live Activities feature introduced in iOS 16, allowing developers to deliver timely updates right on the lock screen.
@@ -6,6 +9,7 @@
6
9
  - Start, update, and stop live activities directly from your React Native application.
7
10
  - Easy integration with a comprehensive API.
8
11
  - Custom image support within live activities with a pre-configured path.
12
+ - Listen and handle changes in push notification tokens associated with a live activity.
9
13
 
10
14
  ## Platform compatibility
11
15
  **Note:** This module is intended for use on **iOS devices only**. When methods are invoked on platforms other than iOS, they will throw an error, ensuring that they are used in the correct context.
@@ -38,18 +42,54 @@ Import the functionalities provided by the `expo-live-activity` module in your J
38
42
  import * as LiveActivity from "expo-live-activity";
39
43
  ```
40
44
 
45
+ ## Push notifications
46
+ By default, updating live activity is possible only via API. There is also a way to update live activity using push notifications. To enable that feature, add `"enablePushNotifications": true`. Then, the notification payload should be looking like this:
47
+
48
+ ```json
49
+ {
50
+ "aps":{
51
+ "event":"update",
52
+ "content-state":{
53
+ "title":"Hello",
54
+ "subtitle":"World",
55
+ "date":1754064245000
56
+ },
57
+ "timestamp":1754063621319
58
+ }
59
+ }
60
+ ```
61
+
62
+ Where `date` value is a timestamp in milliseconds corresponding to the target point of the counter displayed in live activity view.
63
+
64
+ ## Image support
65
+ Live activity view also supports image display. There are two dedicated fields for that:
66
+ - `imageName`
67
+ - `dynamicIslandImageName`
68
+ Currently, it's possible to set them only via API, but we plan on to add that feature to push notifications as well. The value of each field can be:
69
+ - a string which maps to an asset name
70
+ - a URL to remote image
71
+ The latter requires adding "App Groups" capability to both "main app" and "live activity" targets.
72
+
41
73
  ## API
42
74
  `expo-live-activity` module exports three primary functions to manage live activities:
43
75
 
44
- - **`startActivity(state, styles)`**:
45
- Start a new live activity. Takes a `state` configuration object for initial activity state and an optional `styles` object to customize appearance. It returns the `ID` of the created live activity, which should be stored for future reference.
76
+ ### Managing Live Activities
77
+ - **`startActivity(state: LiveActivityState, config?: LiveActivityConfig)`**:
78
+ Start a new live activity. Takes a `state` configuration object for initial activity state and an optional `config` object to customize appearance or behavior. It returns the `ID` of the created live activity, which should be stored for future reference.
46
79
 
47
- - **`updateActivity(activityId, state)`**:
80
+ - **`updateActivity(id: string, state: LiveActivityState)`**:
48
81
  Update an existing live activity. The `state` object should contain updated information. The `activityId` indicates which activity should be updated.
49
82
 
50
- - **`stopActivity(activityId, state)`**:
83
+ - **`stopActivity(id: string, state: LiveActivityState)`**:
51
84
  Terminate an ongoing live activity. The `state` object should contain the final state of the activity. The `activityId` indicates which activity should be stopped.
52
85
 
86
+ ### Handling Push Notification Tokens
87
+ - **`addActivityTokenListener(listener: (event: ActivityTokenReceivedEvent) => void): EventSubscription)`**:
88
+ Subscribe to changes in the push notification token associated with live activities.
89
+
90
+ ### Deep linking
91
+ When starting a new live activity, it's possible to pass `deepLinkUrl` field in `config` object. This can be any string that you can handle in your main app target.
92
+
53
93
  ### State Object Structure
54
94
  The `state` object should include:
55
95
  ```javascript
@@ -62,8 +102,8 @@ The `state` object should include:
62
102
  };
63
103
  ```
64
104
 
65
- ### Styles Object Structure
66
- The `styles` object should include:
105
+ ### Config Object Structure
106
+ The `config` object should include:
67
107
  ```typescript
68
108
  {
69
109
  backgroundColor?: string;
@@ -71,11 +111,13 @@ The `styles` object should include:
71
111
  subtitleColor?: string;
72
112
  progressViewTint?: string;
73
113
  progressViewLabelColor?: string;
114
+ deepLinkUrl?: string;
74
115
  timerType?: DynamicIslandTimerType; // "circular" | "digital" - defines timer appereance on the dynamic island
75
116
  };
76
117
  ```
77
118
 
78
119
  ## Example Usage
120
+ Managing a live activity:
79
121
  ```javascript
80
122
  const state = {
81
123
  title: "Title",
@@ -85,16 +127,31 @@ const state = {
85
127
  dynamicIslandImageName: "dynamic_island_image"
86
128
  };
87
129
 
88
- const styles = {
130
+ const config = {
89
131
  backgroundColor: "#FFFFFF",
90
132
  titleColor: "#000000",
91
133
  subtitleColor: "#333333",
92
134
  progressViewTint: "#4CAF50",
93
135
  progressViewLabelColor: "#FFFFFF",
136
+ deepLinkUrl: "/dashboard",
94
137
  timerType: "circular"
95
138
  };
96
139
 
97
- const activityId = LiveActivity.startActivity(state, styles);
140
+ const activityId = LiveActivity.startActivity(state, config);
98
141
  // Store activityId for future reference
99
142
  ```
100
143
  This will initiate a live activity with the specified title, subtitle, image from your configured assets folder and a time to which there will be a countdown in a progress view.
144
+
145
+ Subscribing to push token changes:
146
+ ```javascript
147
+ useEffect(() => {
148
+ const subscription = LiveActivity.addActivityTokenListener(({
149
+ activityID: newActivityID,
150
+ activityPushToken: newToken
151
+ }) => {
152
+ // Send token to a remote server to update live activity with push notifications
153
+ });
154
+
155
+ return () => subscription.remove();
156
+ }, []);
157
+ ```
package/build/index.d.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import { EventSubscription } from 'expo-modules-core';
1
2
  export type DynamicIslandTimerType = 'circular' | 'digital';
2
3
  export type LiveActivityState = {
3
4
  title: string;
@@ -6,32 +7,40 @@ export type LiveActivityState = {
6
7
  imageName?: string;
7
8
  dynamicIslandImageName?: string;
8
9
  };
9
- export type LiveActivityStyles = {
10
+ export type LiveActivityConfig = {
10
11
  backgroundColor?: string;
11
12
  titleColor?: string;
12
13
  subtitleColor?: string;
13
14
  progressViewTint?: string;
14
15
  progressViewLabelColor?: string;
15
- timerType: DynamicIslandTimerType;
16
+ deepLinkUrl?: string;
17
+ timerType?: DynamicIslandTimerType;
18
+ };
19
+ export type ActivityTokenReceivedEvent = {
20
+ activityID: string;
21
+ activityPushToken: string;
22
+ };
23
+ export type LiveActivityModuleEvents = {
24
+ onTokenReceived: (params: ActivityTokenReceivedEvent) => void;
16
25
  };
17
26
  /**
18
27
  * @param {LiveActivityState} state The state for the live activity.
28
+ * @param {LiveActivityConfig} config Live activity config object.
19
29
  * @returns {string} The identifier of the started activity.
20
- * @throws {Error} When function is called on platform different than iOS.
30
+ * @throws {Error} When function is called on a platform different from iOS.
21
31
  */
22
- export declare function startActivity(state: LiveActivityState, styles?: LiveActivityStyles): string;
32
+ export declare function startActivity(state: LiveActivityState, config?: LiveActivityConfig): string;
23
33
  /**
24
34
  * @param {string} id The identifier of the activity to stop.
25
35
  * @param {LiveActivityState} state The updated state for the live activity.
26
- * @returns {string} The identifier of the stopped activity.
27
- * @throws {Error} When function is called on platform different than iOS.
36
+ * @throws {Error} When function is called on a platform different from iOS.
28
37
  */
29
- export declare function stopActivity(id: string, state: LiveActivityState): string;
38
+ export declare function stopActivity(id: string, state: LiveActivityState): any;
30
39
  /**
31
40
  * @param {string} id The identifier of the activity to update.
32
41
  * @param {LiveActivityState} state The updated state for the live activity.
33
- * @returns {string} The identifier of the updated activity.
34
- * @throws {Error} When function is called on platform different than iOS.
42
+ * @throws {Error} When function is called on a platform different from iOS.
35
43
  */
36
- export declare function updateActivity(id: string, state: LiveActivityState): string;
44
+ export declare function updateActivity(id: string, state: LiveActivityState): any;
45
+ export declare function addActivityTokenListener(listener: (event: ActivityTokenReceivedEvent) => void): EventSubscription;
37
46
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAGA,MAAM,MAAM,sBAAsB,GAAG,UAAU,GAAG,SAAS,CAAA;AAE3D,MAAM,MAAM,iBAAiB,GAAG;IAC9B,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,sBAAsB,CAAC,EAAE,MAAM,CAAC;CACjC,CAAC;AAEF,MAAM,MAAM,kBAAkB,GAAG;IAC/B,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,sBAAsB,CAAC,EAAE,MAAM,CAAC;IAChC,SAAS,EAAE,sBAAsB,CAAC;CACnC,CAAC;AAEF;;;;GAIG;AACH,wBAAgB,aAAa,CAAC,KAAK,EAAE,iBAAiB,EAAE,MAAM,CAAC,EAAE,kBAAkB,GAAG,MAAM,CAK3F;AAED;;;;;GAKG;AACH,wBAAgB,YAAY,CAAC,EAAE,EAAE,MAAM,EAAE,KAAK,EAAE,iBAAiB,GAAG,MAAM,CAKzE;AAED;;;;;GAKG;AACH,wBAAgB,cAAc,CAAC,EAAE,EAAE,MAAM,EAAE,KAAK,EAAE,iBAAiB,GAAG,MAAM,CAK3E"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,iBAAiB,EAAE,MAAM,mBAAmB,CAAC;AAEtD,MAAM,MAAM,sBAAsB,GAAG,UAAU,GAAG,SAAS,CAAA;AAE3D,MAAM,MAAM,iBAAiB,GAAG;IAC9B,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,sBAAsB,CAAC,EAAE,MAAM,CAAC;CACjC,CAAC;AAEF,MAAM,MAAM,kBAAkB,GAAG;IAC/B,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,sBAAsB,CAAC,EAAE,MAAM,CAAC;IAChC,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,SAAS,CAAC,EAAE,sBAAsB,CAAC;CACpC,CAAC;AAEF,MAAM,MAAM,0BAA0B,GAAG;IACvC,UAAU,EAAE,MAAM,CAAC;IACnB,iBAAiB,EAAE,MAAM,CAAC;CAC3B,CAAC;AAEF,MAAM,MAAM,wBAAwB,GAAG;IACrC,eAAe,EAAE,CAAC,MAAM,EAAE,0BAA0B,KAAK,IAAI,CAAC;CAC/D,CAAC;AAEF;;;;;GAKG;AACH,wBAAgB,aAAa,CAAC,KAAK,EAAE,iBAAiB,EAAE,MAAM,CAAC,EAAE,kBAAkB,GAAG,MAAM,CAK3F;AAED;;;;GAIG;AACH,wBAAgB,YAAY,CAAC,EAAE,EAAE,MAAM,EAAE,KAAK,EAAE,iBAAiB,OAKhE;AAED;;;;GAIG;AACH,wBAAgB,cAAc,CAAC,EAAE,EAAE,MAAM,EAAE,KAAK,EAAE,iBAAiB,OAKlE;AAED,wBAAgB,wBAAwB,CAAC,QAAQ,EAAE,CAAC,KAAK,EAAE,0BAA0B,KAAK,IAAI,GAAG,iBAAiB,CAKjH"}
package/build/index.js CHANGED
@@ -2,20 +2,20 @@ import ExpoLiveActivityModule from "./ExpoLiveActivityModule";
2
2
  import { Platform } from "react-native";
3
3
  /**
4
4
  * @param {LiveActivityState} state The state for the live activity.
5
+ * @param {LiveActivityConfig} config Live activity config object.
5
6
  * @returns {string} The identifier of the started activity.
6
- * @throws {Error} When function is called on platform different than iOS.
7
+ * @throws {Error} When function is called on a platform different from iOS.
7
8
  */
8
- export function startActivity(state, styles) {
9
+ export function startActivity(state, config) {
9
10
  if (Platform.OS !== "ios") {
10
11
  throw new Error("startActivity is only available on iOS");
11
12
  }
12
- return ExpoLiveActivityModule.startActivity(state, styles);
13
+ return ExpoLiveActivityModule.startActivity(state, config);
13
14
  }
14
15
  /**
15
16
  * @param {string} id The identifier of the activity to stop.
16
17
  * @param {LiveActivityState} state The updated state for the live activity.
17
- * @returns {string} The identifier of the stopped activity.
18
- * @throws {Error} When function is called on platform different than iOS.
18
+ * @throws {Error} When function is called on a platform different from iOS.
19
19
  */
20
20
  export function stopActivity(id, state) {
21
21
  if (Platform.OS !== "ios") {
@@ -26,8 +26,7 @@ export function stopActivity(id, state) {
26
26
  /**
27
27
  * @param {string} id The identifier of the activity to update.
28
28
  * @param {LiveActivityState} state The updated state for the live activity.
29
- * @returns {string} The identifier of the updated activity.
30
- * @throws {Error} When function is called on platform different than iOS.
29
+ * @throws {Error} When function is called on a platform different from iOS.
31
30
  */
32
31
  export function updateActivity(id, state) {
33
32
  if (Platform.OS !== "ios") {
@@ -35,4 +34,10 @@ export function updateActivity(id, state) {
35
34
  }
36
35
  return ExpoLiveActivityModule.updateActivity(id, state);
37
36
  }
37
+ export function addActivityTokenListener(listener) {
38
+ if (Platform.OS !== "ios") {
39
+ throw new Error("updateActivity is only available on iOS");
40
+ }
41
+ return ExpoLiveActivityModule.addListener('onTokenReceived', listener);
42
+ }
38
43
  //# sourceMappingURL=index.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,sBAAsB,MAAM,0BAA0B,CAAC;AAC9D,OAAO,EAAE,QAAQ,EAAE,MAAM,cAAc,CAAC;AAqBxC;;;;GAIG;AACH,MAAM,UAAU,aAAa,CAAC,KAAwB,EAAE,MAA2B;IACjF,IAAI,QAAQ,CAAC,EAAE,KAAK,KAAK,EAAE,CAAC;QAC1B,MAAM,IAAI,KAAK,CAAC,wCAAwC,CAAC,CAAC;IAC5D,CAAC;IACD,OAAO,sBAAsB,CAAC,aAAa,CAAC,KAAK,EAAE,MAAM,CAAC,CAAC;AAC7D,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,YAAY,CAAC,EAAU,EAAE,KAAwB;IAC/D,IAAI,QAAQ,CAAC,EAAE,KAAK,KAAK,EAAE,CAAC;QAC1B,MAAM,IAAI,KAAK,CAAC,uCAAuC,CAAC,CAAC;IAC3D,CAAC;IACD,OAAO,sBAAsB,CAAC,YAAY,CAAC,EAAE,EAAE,KAAK,CAAC,CAAC;AACxD,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,cAAc,CAAC,EAAU,EAAE,KAAwB;IACjE,IAAI,QAAQ,CAAC,EAAE,KAAK,KAAK,EAAE,CAAC;QAC1B,MAAM,IAAI,KAAK,CAAC,yCAAyC,CAAC,CAAC;IAC7D,CAAC;IACD,OAAO,sBAAsB,CAAC,cAAc,CAAC,EAAE,EAAE,KAAK,CAAC,CAAC;AAC1D,CAAC","sourcesContent":["import ExpoLiveActivityModule from \"./ExpoLiveActivityModule\";\nimport { Platform } from \"react-native\";\n\nexport type DynamicIslandTimerType = 'circular' | 'digital'\n\nexport type LiveActivityState = {\n title: string;\n subtitle?: string;\n date?: number;\n imageName?: string;\n dynamicIslandImageName?: string;\n};\n\nexport type LiveActivityStyles = {\n backgroundColor?: string;\n titleColor?: string;\n subtitleColor?: string;\n progressViewTint?: string;\n progressViewLabelColor?: string;\n timerType: DynamicIslandTimerType;\n};\n\n/**\n * @param {LiveActivityState} state The state for the live activity.\n * @returns {string} The identifier of the started activity.\n * @throws {Error} When function is called on platform different than iOS.\n */\nexport function startActivity(state: LiveActivityState, styles?: LiveActivityStyles): string {\n if (Platform.OS !== \"ios\") {\n throw new Error(\"startActivity is only available on iOS\");\n }\n return ExpoLiveActivityModule.startActivity(state, styles);\n}\n\n/**\n * @param {string} id The identifier of the activity to stop.\n * @param {LiveActivityState} state The updated state for the live activity.\n * @returns {string} The identifier of the stopped activity.\n * @throws {Error} When function is called on platform different than iOS.\n */\nexport function stopActivity(id: string, state: LiveActivityState): string {\n if (Platform.OS !== \"ios\") {\n throw new Error(\"stopActivity is only available on iOS\");\n }\n return ExpoLiveActivityModule.stopActivity(id, state);\n}\n\n/**\n * @param {string} id The identifier of the activity to update.\n * @param {LiveActivityState} state The updated state for the live activity.\n * @returns {string} The identifier of the updated activity.\n * @throws {Error} When function is called on platform different than iOS.\n */\nexport function updateActivity(id: string, state: LiveActivityState): string {\n if (Platform.OS !== \"ios\") {\n throw new Error(\"updateActivity is only available on iOS\");\n }\n return ExpoLiveActivityModule.updateActivity(id, state);\n}\n"]}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,sBAAsB,MAAM,0BAA0B,CAAC;AAC9D,OAAO,EAAE,QAAQ,EAAE,MAAM,cAAc,CAAC;AAgCxC;;;;;GAKG;AACH,MAAM,UAAU,aAAa,CAAC,KAAwB,EAAE,MAA2B;IACjF,IAAI,QAAQ,CAAC,EAAE,KAAK,KAAK,EAAE,CAAC;QAC1B,MAAM,IAAI,KAAK,CAAC,wCAAwC,CAAC,CAAC;IAC5D,CAAC;IACD,OAAO,sBAAsB,CAAC,aAAa,CAAC,KAAK,EAAE,MAAM,CAAC,CAAC;AAC7D,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,YAAY,CAAC,EAAU,EAAE,KAAwB;IAC/D,IAAI,QAAQ,CAAC,EAAE,KAAK,KAAK,EAAE,CAAC;QAC1B,MAAM,IAAI,KAAK,CAAC,uCAAuC,CAAC,CAAC;IAC3D,CAAC;IACD,OAAO,sBAAsB,CAAC,YAAY,CAAC,EAAE,EAAE,KAAK,CAAC,CAAC;AACxD,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,cAAc,CAAC,EAAU,EAAE,KAAwB;IACjE,IAAI,QAAQ,CAAC,EAAE,KAAK,KAAK,EAAE,CAAC;QAC1B,MAAM,IAAI,KAAK,CAAC,yCAAyC,CAAC,CAAC;IAC7D,CAAC;IACD,OAAO,sBAAsB,CAAC,cAAc,CAAC,EAAE,EAAE,KAAK,CAAC,CAAC;AAC1D,CAAC;AAED,MAAM,UAAU,wBAAwB,CAAC,QAAqD;IAC5F,IAAI,QAAQ,CAAC,EAAE,KAAK,KAAK,EAAE,CAAC;QAC1B,MAAM,IAAI,KAAK,CAAC,yCAAyC,CAAC,CAAC;IAC7D,CAAC;IACD,OAAO,sBAAsB,CAAC,WAAW,CAAC,iBAAiB,EAAE,QAAQ,CAAC,CAAC;AACzE,CAAC","sourcesContent":["import ExpoLiveActivityModule from \"./ExpoLiveActivityModule\";\nimport { Platform } from \"react-native\";\nimport { EventSubscription } from 'expo-modules-core';\n\nexport type DynamicIslandTimerType = 'circular' | 'digital'\n\nexport type LiveActivityState = {\n title: string;\n subtitle?: string;\n date?: number;\n imageName?: string;\n dynamicIslandImageName?: string;\n};\n\nexport type LiveActivityConfig = {\n backgroundColor?: string;\n titleColor?: string;\n subtitleColor?: string;\n progressViewTint?: string;\n progressViewLabelColor?: string;\n deepLinkUrl?: string;\n timerType?: DynamicIslandTimerType;\n};\n\nexport type ActivityTokenReceivedEvent = {\n activityID: string;\n activityPushToken: string;\n};\n\nexport type LiveActivityModuleEvents = {\n onTokenReceived: (params: ActivityTokenReceivedEvent) => void;\n};\n\n/**\n * @param {LiveActivityState} state The state for the live activity.\n * @param {LiveActivityConfig} config Live activity config object.\n * @returns {string} The identifier of the started activity.\n * @throws {Error} When function is called on a platform different from iOS.\n */\nexport function startActivity(state: LiveActivityState, config?: LiveActivityConfig): string {\n if (Platform.OS !== \"ios\") {\n throw new Error(\"startActivity is only available on iOS\");\n }\n return ExpoLiveActivityModule.startActivity(state, config);\n}\n\n/**\n * @param {string} id The identifier of the activity to stop.\n * @param {LiveActivityState} state The updated state for the live activity.\n * @throws {Error} When function is called on a platform different from iOS.\n */\nexport function stopActivity(id: string, state: LiveActivityState) {\n if (Platform.OS !== \"ios\") {\n throw new Error(\"stopActivity is only available on iOS\");\n }\n return ExpoLiveActivityModule.stopActivity(id, state);\n}\n\n/**\n * @param {string} id The identifier of the activity to update.\n * @param {LiveActivityState} state The updated state for the live activity.\n * @throws {Error} When function is called on a platform different from iOS.\n */\nexport function updateActivity(id: string, state: LiveActivityState) {\n if (Platform.OS !== \"ios\") {\n throw new Error(\"updateActivity is only available on iOS\");\n }\n return ExpoLiveActivityModule.updateActivity(id, state);\n}\n\nexport function addActivityTokenListener(listener: (event: ActivityTokenReceivedEvent) => void): EventSubscription {\n if (Platform.OS !== \"ios\") {\n throw new Error(\"updateActivity is only available on iOS\");\n }\n return ExpoLiveActivityModule.addListener('onTokenReceived', listener);\n}\n"]}
@@ -0,0 +1,13 @@
1
+ //
2
+ // Data+download.swift
3
+ //
4
+ //
5
+ // Created by Artur Bilski on 04/08/2025.
6
+ //
7
+
8
+ extension Data {
9
+ static func download(from url: URL) async throws -> Self {
10
+ let (data, _) = try await URLSession.shared.data(from: url)
11
+ return data
12
+ }
13
+ }
@@ -2,139 +2,190 @@ import ActivityKit
2
2
  import ExpoModulesCore
3
3
 
4
4
  enum ModuleErrors: Error {
5
- case unsupported
6
- case liveActivitiesNotEnabled
5
+ case unsupported
6
+ case liveActivitiesNotEnabled
7
7
  }
8
8
 
9
9
  public class ExpoLiveActivityModule: Module {
10
- struct LiveActivityState: Record {
11
- @Field
12
- var title: String
10
+ struct LiveActivityState: Record {
11
+ @Field
12
+ var title: String
13
13
 
14
- @Field
15
- var subtitle: String?
14
+ @Field
15
+ var subtitle: String?
16
16
 
17
- @Field
18
- var date: Double?
17
+ @Field
18
+ var date: Double?
19
19
 
20
- @Field
21
- var imageName: String?
20
+ @Field
21
+ var imageName: String?
22
22
 
23
- @Field
24
- var dynamicIslandImageName: String?
25
- }
23
+ @Field
24
+ var dynamicIslandImageName: String?
25
+ }
26
+
27
+ struct LiveActivityConfig: Record {
28
+ @Field
29
+ var backgroundColor: String?
30
+
31
+ @Field
32
+ var titleColor: String?
33
+
34
+ @Field
35
+ var subtitleColor: String?
36
+
37
+ @Field
38
+ var progressViewTint: String?
39
+
40
+ @Field
41
+ var progressViewLabelColor: String?
26
42
 
27
- struct LiveActivityStyles: Record {
28
- @Field
29
- var backgroundColor: String?
30
-
31
- @Field
32
- var titleColor: String?
33
-
34
- @Field
35
- var subtitleColor: String?
36
-
37
- @Field
38
- var progressViewTint: String?
39
-
40
- @Field
41
- var progressViewLabelColor: String?
42
-
43
- @Field
44
- var timerType: DynamicIslandTimerType?
43
+ @Field
44
+ var deepLinkUrl: String?
45
+
46
+ @Field
47
+ var timerType: DynamicIslandTimerType?
48
+ }
49
+
50
+ enum DynamicIslandTimerType: String, Enumerable {
51
+ case circular
52
+ case digital
53
+ }
54
+
55
+ func sendPushToken(activityID: String, activityPushToken: String) {
56
+ sendEvent(
57
+ "onTokenReceived",
58
+ [
59
+ "activityID": activityID,
60
+ "activityPushToken": activityPushToken,
61
+ ]
62
+ )
63
+ }
64
+
65
+ func toContentStateDate(date: Double?) -> Date? {
66
+ return date.map { Date(timeIntervalSince1970: $0 / 1000) }
67
+ }
68
+
69
+ func updateImages(state: LiveActivityState, newState: inout LiveActivityAttributes.ContentState) async throws {
70
+ if let name = state.imageName {
71
+ print("imageName: \(name)")
72
+ newState.imageName = try await resolveImage(from: name)
45
73
  }
46
-
47
- enum DynamicIslandTimerType: String, Enumerable {
48
- case circular
49
- case digital
74
+
75
+ if let name = state.dynamicIslandImageName {
76
+ print("dynamicIslandImageName: \(name)")
77
+ newState.dynamicIslandImageName = try await resolveImage(from: name)
50
78
  }
79
+ }
80
+
81
+ public func definition() -> ModuleDefinition {
82
+ Name("ExpoLiveActivity")
83
+
84
+ Events("onTokenReceived")
51
85
 
52
- public func definition() -> ModuleDefinition {
53
- Name("ExpoLiveActivity")
54
-
55
- Function("startActivity") { (state: LiveActivityState, styles: LiveActivityStyles? ) -> String in
56
- let date = state.date != nil ? Date(timeIntervalSince1970: state.date! / 1000) : nil
57
- print("Starting activity")
58
- if #available(iOS 16.2, *) {
59
- if ActivityAuthorizationInfo().areActivitiesEnabled {
60
- do {
61
- let counterState = LiveActivityAttributes(
62
- name: "ExpoLiveActivity",
63
- backgroundColor: styles?.backgroundColor,
64
- titleColor: styles?.titleColor,
65
- subtitleColor: styles?.subtitleColor,
66
- progressViewTint: styles?.progressViewTint,
67
- progressViewLabelColor: styles?.progressViewLabelColor,
68
- timerType: styles?.timerType == .digital ? .digital : .circular
69
- )
70
- let initialState = LiveActivityAttributes.ContentState(
71
- title: state.title,
72
- subtitle: state.subtitle,
73
- date: date,
74
- imageName: state.imageName,
75
- dynamicIslandImageName: state.dynamicIslandImageName)
76
- let activity = try Activity.request(
77
- attributes: counterState,
78
- content: .init(state: initialState, staleDate: nil), pushType: nil)
79
- return activity.id
80
- } catch (let error) {
81
- print("Error with live activity: \(error)")
82
- }
83
- }
84
- throw ModuleErrors.liveActivitiesNotEnabled
85
- } else {
86
- throw ModuleErrors.unsupported
86
+ Function("startActivity") { (state: LiveActivityState, maybeConfig: LiveActivityConfig?) -> String in
87
+ print("Starting activity")
88
+ if #available(iOS 16.2, *) {
89
+ if ActivityAuthorizationInfo().areActivitiesEnabled {
90
+ do {
91
+ let config = maybeConfig ?? LiveActivityConfig()
92
+ let attributes = LiveActivityAttributes(
93
+ name: "ExpoLiveActivity",
94
+ backgroundColor: config.backgroundColor,
95
+ titleColor: config.titleColor,
96
+ subtitleColor: config.subtitleColor,
97
+ progressViewTint: config.progressViewTint,
98
+ progressViewLabelColor: config.progressViewLabelColor,
99
+ deepLinkUrl: config.deepLinkUrl,
100
+ timerType: config.timerType == .digital ? .digital : .circular
101
+ )
102
+ let initialState = LiveActivityAttributes.ContentState(
103
+ title: state.title,
104
+ subtitle: state.subtitle,
105
+ date: toContentStateDate(date: state.date),
106
+ )
107
+ let pushNotificationsEnabled =
108
+ Bundle.main.object(forInfoDictionaryKey: "ExpoLiveActivity_EnablePushNotifications") as? Bool
109
+ let activity = try Activity.request(
110
+ attributes: attributes,
111
+ content: .init(state: initialState, staleDate: nil),
112
+ pushType: pushNotificationsEnabled == true ? .token : nil
113
+ )
114
+
115
+ Task {
116
+ for await pushToken in activity.pushTokenUpdates {
117
+ let pushTokenString = pushToken.reduce("") { $0 + String(format: "%02x", $1) }
118
+
119
+ sendPushToken(activityID: activity.id, activityPushToken: pushTokenString)
120
+ }
87
121
  }
88
- }
89
122
 
90
- Function("stopActivity") { (activityId: String, state: LiveActivityState) -> Void in
91
- if #available(iOS 16.2, *) {
92
- print("Attempting to stop")
93
- let endState = LiveActivityAttributes.ContentState(
94
- title: state.title,
95
- subtitle: state.subtitle,
96
- date: state.date != nil ? Date(timeIntervalSince1970: state.date! / 1000) : nil,
97
- imageName: state.imageName,
98
- dynamicIslandImageName: state.dynamicIslandImageName)
99
- if let activity = Activity<LiveActivityAttributes>.activities.first(where: {
100
- $0.id == activityId
101
- }) {
102
- Task {
103
- print("Stopping activity with id: \(activityId)")
104
- await activity.end(
105
- ActivityContent(state: endState, staleDate: nil),
106
- dismissalPolicy: .immediate)
107
- }
108
- } else {
109
- print("Didn't find activity with ID \(activityId)")
110
- }
111
- } else {
112
- throw ModuleErrors.unsupported
123
+ Task {
124
+ var newState = activity.content.state
125
+ try await updateImages(state: state, newState: &newState)
126
+ await activity.update(ActivityContent(state: newState, staleDate: nil))
113
127
  }
128
+
129
+ return activity.id
130
+ } catch (let error) {
131
+ print("Error with live activity: \(error)")
132
+ }
114
133
  }
134
+ throw ModuleErrors.liveActivitiesNotEnabled
135
+ } else {
136
+ throw ModuleErrors.unsupported
137
+ }
138
+ }
115
139
 
116
- Function("updateActivity") { (activityId: String, state: LiveActivityState) -> Void in
117
- if #available(iOS 16.2, *) {
118
- print("Attempting to update")
119
- let newState = LiveActivityAttributes.ContentState(
120
- title: state.title,
121
- subtitle: state.subtitle,
122
- date: state.date != nil ? Date(timeIntervalSince1970: state.date! / 1000) : nil,
123
- imageName: state.imageName,
124
- dynamicIslandImageName: state.dynamicIslandImageName)
125
- if let activity = Activity<LiveActivityAttributes>.activities.first(where: {
126
- $0.id == activityId
127
- }) {
128
- Task {
129
- print("Updating activity with id: \(activityId)")
130
- await activity.update(ActivityContent(state: newState, staleDate: nil))
131
- }
132
- } else {
133
- print("Didn't find activity with ID \(activityId)")
134
- }
135
- } else {
136
- throw ModuleErrors.unsupported
137
- }
140
+ Function("stopActivity") { (activityId: String, state: LiveActivityState) -> Void in
141
+ if #available(iOS 16.2, *) {
142
+ print("Attempting to stop")
143
+ var newState = LiveActivityAttributes.ContentState(
144
+ title: state.title,
145
+ subtitle: state.subtitle,
146
+ date: toContentStateDate(date: state.date),
147
+ )
148
+ if let activity = Activity<LiveActivityAttributes>.activities.first(where: {
149
+ $0.id == activityId
150
+ }) {
151
+ Task {
152
+ print("Stopping activity with id: \(activityId)")
153
+ try await updateImages(state: state, newState: &newState)
154
+ await activity.end(
155
+ ActivityContent(state: newState, staleDate: nil),
156
+ dismissalPolicy: .immediate
157
+ )
158
+ }
159
+ } else {
160
+ print("Didn't find activity with ID \(activityId)")
161
+ }
162
+ } else {
163
+ throw ModuleErrors.unsupported
164
+ }
165
+ }
166
+
167
+ Function("updateActivity") { (activityId: String, state: LiveActivityState) -> Void in
168
+ if #available(iOS 16.2, *) {
169
+ print("Attempting to update")
170
+ var newState = LiveActivityAttributes.ContentState(
171
+ title: state.title,
172
+ subtitle: state.subtitle,
173
+ date: toContentStateDate(date: state.date),
174
+ )
175
+ if let activity = Activity<LiveActivityAttributes>.activities.first(where: {
176
+ $0.id == activityId
177
+ }) {
178
+ Task {
179
+ print("Updating activity with id: \(activityId)")
180
+ try await updateImages(state: state, newState: &newState)
181
+ await activity.update(ActivityContent(state: newState, staleDate: nil))
182
+ }
183
+ } else {
184
+ print("Didn't find activity with ID \(activityId)")
138
185
  }
186
+ } else {
187
+ throw ModuleErrors.unsupported
188
+ }
139
189
  }
190
+ }
140
191
  }
@@ -0,0 +1,22 @@
1
+ //
2
+ // Helpers.swift
3
+ //
4
+ //
5
+ // Created by Artur Bilski on 04/08/2025.
6
+ //
7
+
8
+ func resolveImage(from string: String) async throws -> String {
9
+ if let url = URL(string: string), url.scheme?.hasPrefix("http") == true,
10
+ let container = FileManager.default.containerURL(
11
+ forSecurityApplicationGroupIdentifier: "group.expoLiveActivity.sharedData"
12
+ )
13
+ {
14
+ let data = try await Data.download(from: url)
15
+ let filename = UUID().uuidString + ".png"
16
+ let fileURL = container.appendingPathComponent(filename)
17
+ try data.write(to: fileURL)
18
+ return fileURL.lastPathComponent
19
+ } else {
20
+ return string
21
+ }
22
+ }
@@ -5,8 +5,8 @@
5
5
  // Created by Anna Olak on 03/06/2025.
6
6
  //
7
7
 
8
- import Foundation
9
8
  import ActivityKit
9
+ import Foundation
10
10
 
11
11
  struct LiveActivityAttributes: ActivityAttributes {
12
12
  public struct ContentState: Codable, Hashable {
@@ -23,10 +23,11 @@ struct LiveActivityAttributes: ActivityAttributes {
23
23
  var subtitleColor: String?
24
24
  var progressViewTint: String?
25
25
  var progressViewLabelColor: String?
26
+ var deepLinkUrl: String?
26
27
  var timerType: DynamicIslandTimerType
27
-
28
+
28
29
  enum DynamicIslandTimerType: String, Codable {
29
- case circular
30
- case digital
30
+ case circular
31
+ case digital
31
32
  }
32
33
  }