expo-live-activity 0.2.0-alpha3 → 0.2.0-alpha5

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
@@ -12,9 +12,11 @@
12
12
  - Listen and handle changes in push notification tokens associated with a live activity.
13
13
 
14
14
  ## Platform compatibility
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.
15
+ **Note:** This module is intended for use on **iOS devices only**. The minimal iOS version that supports Live Activities is 16.2. When methods are invoked on platforms other than iOS or on older iOS versions, they will log an error, ensuring that they are used in the correct context.
16
16
 
17
17
  ## Installation
18
+ > [!NOTE]
19
+ > The library isn't supported in Expo Go, to set it up correctly you need to use [Expo DevClient](https://docs.expo.dev/versions/latest/sdk/dev-client/) .
18
20
  To begin using `expo-live-activity`, follow the installation and configuration steps outlined below:
19
21
 
20
22
  ### Step 1: Installation
@@ -33,49 +35,41 @@ The module comes with a built-in config plugin that creates a target in iOS with
33
35
  }
34
36
  }
35
37
  ```
38
+ If you want to update Live Acitivity with push notifications you can add option `"enablePushNotifications": true`:
39
+ ```json
40
+ {
41
+ "expo": {
42
+ "plugins": [
43
+ [
44
+ "expo-live-activity",
45
+ {
46
+ "enablePushNotifications": true
47
+ }
48
+ ]
49
+ ]
50
+ }
51
+ }
52
+ ```
36
53
  2. **Assets configuration:**
37
54
  Place images intended for live activities in the `assets/liveActivity` folder. The plugin manages these assets automatically.
38
55
 
56
+ Then prebuild your app with:
57
+ ```sh
58
+ npx expo prebuild --clean
59
+ ```
60
+
39
61
  ### Step 3: Usage in Your React Native App
40
62
  Import the functionalities provided by the `expo-live-activity` module in your JavaScript or TypeScript files:
41
63
  ```javascript
42
64
  import * as LiveActivity from "expo-live-activity";
43
65
  ```
44
66
 
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
-
73
67
  ## API
74
68
  `expo-live-activity` module exports three primary functions to manage live activities:
75
69
 
76
70
  ### Managing Live Activities
77
- - **`startActivity(state: LiveActivityState, styles?: LiveActivityStyles)`**:
78
- 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.
71
+ - **`startActivity(state: LiveActivityState, config?: LiveActivityConfig): string | undefined`**:
72
+ 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. If the live activity can't be created (eg. on android or iOS lower than 16.2), it will return `undefined`.
79
73
 
80
74
  - **`updateActivity(id: string, state: LiveActivityState)`**:
81
75
  Update an existing live activity. The `state` object should contain updated information. The `activityId` indicates which activity should be updated.
@@ -84,23 +78,27 @@ The latter requires adding "App Groups" capability to both "main app" and "live
84
78
  Terminate an ongoing live activity. The `state` object should contain the final state of the activity. The `activityId` indicates which activity should be stopped.
85
79
 
86
80
  ### Handling Push Notification Tokens
87
- - **`addActivityTokenListener(listener: (event: ActivityTokenReceivedEvent) => void): EventSubscription)`**:
81
+ - **`addActivityTokenListener(listener: (event: ActivityTokenReceivedEvent) => void): EventSubscription | undefined`**:
88
82
  Subscribe to changes in the push notification token associated with live activities.
89
83
 
84
+
85
+ ### Deep linking
86
+ 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.
87
+
90
88
  ### State Object Structure
91
89
  The `state` object should include:
92
90
  ```javascript
93
91
  {
94
92
  title: string;
95
93
  subtitle?: string;
96
- date?: number; // Set as epoch time in milliseconds
94
+ date?: number; // Set as epoch time in milliseconds. This is used as an end date in a timer.
97
95
  imageName?: string; // Matches the name of the image in 'assets/live-activity'
98
96
  dynamicIslandImageName?: string; // Matches the name of the image in 'assets/live-activity'
99
97
  };
100
98
  ```
101
99
 
102
- ### Styles Object Structure
103
- The `styles` object should include:
100
+ ### Config Object Structure
101
+ The `config` object should include:
104
102
  ```typescript
105
103
  {
106
104
  backgroundColor?: string;
@@ -108,6 +106,7 @@ The `styles` object should include:
108
106
  subtitleColor?: string;
109
107
  progressViewTint?: string;
110
108
  progressViewLabelColor?: string;
109
+ deepLinkUrl?: string;
111
110
  timerType?: DynamicIslandTimerType; // "circular" | "digital" - defines timer appereance on the dynamic island
112
111
  };
113
112
  ```
@@ -123,16 +122,17 @@ const state = {
123
122
  dynamicIslandImageName: "dynamic_island_image"
124
123
  };
125
124
 
126
- const styles = {
125
+ const config = {
127
126
  backgroundColor: "#FFFFFF",
128
127
  titleColor: "#000000",
129
128
  subtitleColor: "#333333",
130
129
  progressViewTint: "#4CAF50",
131
130
  progressViewLabelColor: "#FFFFFF",
131
+ deepLinkUrl: "/dashboard",
132
132
  timerType: "circular"
133
133
  };
134
134
 
135
- const activityId = LiveActivity.startActivity(state, styles);
135
+ const activityId = LiveActivity.startActivity(state, config);
136
136
  // Store activityId for future reference
137
137
  ```
138
138
  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.
@@ -150,3 +150,32 @@ useEffect(() => {
150
150
  return () => subscription.remove();
151
151
  }, []);
152
152
  ```
153
+ ## Push notifications
154
+ By default, updating live activity is possible only via API. If you want to have possibility to update live activity using push notifications, you can enable that feature by adding `"enablePushNotifications": true` in the plugin config in your `app.json` or `app.config.ts` file. Then, the notification payload should look like this:
155
+
156
+ ```json
157
+ {
158
+ "aps":{
159
+ "event":"update",
160
+ "content-state":{
161
+ "title":"Hello",
162
+ "subtitle":"World",
163
+ "timerEndDateInMilliseconds":1754064245000,
164
+ "imageName": "live_activity_image",
165
+ "dynamicIslandImageName": "dynamic_island_image"
166
+ },
167
+ "timestamp":1754063621319 // timestamp of when the push notification was sent
168
+ }
169
+ }
170
+ ```
171
+
172
+ Where `timerEndDateInMilliseconds` value is a timestamp in milliseconds corresponding to the target point of the counter displayed in live activity view.
173
+
174
+ ## Image support
175
+ Live activity view also supports image display. There are two dedicated fields in the `state` object for that:
176
+ - `imageName`
177
+ - `dynamicIslandImageName`
178
+
179
+ The value of each field can be:
180
+ - a string which maps to an asset name
181
+ - a URL to remote image - currently, it's possible to use this option only via API, but we plan on to add that feature to push notifications as well. It also requires adding "App Groups" capability to both "main app" and "live activity" targets.
package/build/index.d.ts CHANGED
@@ -7,12 +7,13 @@ export type LiveActivityState = {
7
7
  imageName?: string;
8
8
  dynamicIslandImageName?: string;
9
9
  };
10
- export type LiveActivityStyles = {
10
+ export type LiveActivityConfig = {
11
11
  backgroundColor?: string;
12
12
  titleColor?: string;
13
13
  subtitleColor?: string;
14
14
  progressViewTint?: string;
15
15
  progressViewLabelColor?: string;
16
+ deepLinkUrl?: string;
16
17
  timerType?: DynamicIslandTimerType;
17
18
  };
18
19
  export type ActivityTokenReceivedEvent = {
@@ -24,22 +25,19 @@ export type LiveActivityModuleEvents = {
24
25
  };
25
26
  /**
26
27
  * @param {LiveActivityState} state The state for the live activity.
27
- * @param {LiveActivityStyles} styles Live activity styling object.
28
- * @returns {string} The identifier of the started activity.
29
- * @throws {Error} When function is called on a platform different from iOS.
28
+ * @param {LiveActivityConfig} config Live activity config object.
29
+ * @returns {string} The identifier of the started activity or undefined if creating live activity failed.
30
30
  */
31
- export declare function startActivity(state: LiveActivityState, styles?: LiveActivityStyles): string;
31
+ export declare function startActivity(state: LiveActivityState, config?: LiveActivityConfig): string | undefined;
32
32
  /**
33
33
  * @param {string} id The identifier of the activity to stop.
34
34
  * @param {LiveActivityState} state The updated state for the live activity.
35
- * @throws {Error} When function is called on a platform different from iOS.
36
35
  */
37
36
  export declare function stopActivity(id: string, state: LiveActivityState): any;
38
37
  /**
39
38
  * @param {string} id The identifier of the activity to update.
40
39
  * @param {LiveActivityState} state The updated state for the live activity.
41
- * @throws {Error} When function is called on a platform different from iOS.
42
40
  */
43
41
  export declare function updateActivity(id: string, state: LiveActivityState): any;
44
- export declare function addActivityTokenListener(listener: (event: ActivityTokenReceivedEvent) => void): EventSubscription;
42
+ export declare function addActivityTokenListener(listener: (event: ActivityTokenReceivedEvent) => void): EventSubscription | undefined;
45
43
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
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,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"}
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;;;;GAIG;AACH,wBAAgB,aAAa,CAAC,KAAK,EAAE,iBAAiB,EAAE,MAAM,CAAC,EAAE,kBAAkB,GAAG,MAAM,GAAG,SAAS,CAWvG;AAED;;;GAGG;AACH,wBAAgB,YAAY,CAAC,EAAE,EAAE,MAAM,EAAE,KAAK,EAAE,iBAAiB,OAShE;AAED;;;GAGG;AACH,wBAAgB,cAAc,CAAC,EAAE,EAAE,MAAM,EAAE,KAAK,EAAE,iBAAiB,OASlE;AAED,wBAAgB,wBAAwB,CAAC,QAAQ,EAAE,CAAC,KAAK,EAAE,0BAA0B,KAAK,IAAI,GAAG,iBAAiB,GAAG,SAAS,CAM7H"}
package/build/index.js CHANGED
@@ -2,41 +2,56 @@ 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 {LiveActivityStyles} styles Live activity styling object.
6
- * @returns {string} The identifier of the started activity.
7
- * @throws {Error} When function is called on a platform different from iOS.
5
+ * @param {LiveActivityConfig} config Live activity config object.
6
+ * @returns {string} The identifier of the started activity or undefined if creating live activity failed.
8
7
  */
9
- export function startActivity(state, styles) {
8
+ export function startActivity(state, config) {
10
9
  if (Platform.OS !== "ios") {
11
- throw new Error("startActivity is only available on iOS");
10
+ console.error("startActivity is only available on iOS");
11
+ return undefined;
12
+ }
13
+ try {
14
+ return ExpoLiveActivityModule.startActivity(state, config);
15
+ }
16
+ catch (error) {
17
+ console.error(`startActivity failed with an error: ${error}`);
18
+ return undefined;
12
19
  }
13
- return ExpoLiveActivityModule.startActivity(state, styles);
14
20
  }
15
21
  /**
16
22
  * @param {string} id The identifier of the activity to stop.
17
23
  * @param {LiveActivityState} state The updated state for the live activity.
18
- * @throws {Error} When function is called on a platform different from iOS.
19
24
  */
20
25
  export function stopActivity(id, state) {
21
26
  if (Platform.OS !== "ios") {
22
- throw new Error("stopActivity is only available on iOS");
27
+ console.error("stopActivity is only available on iOS");
28
+ }
29
+ try {
30
+ return ExpoLiveActivityModule.stopActivity(id, state);
31
+ }
32
+ catch (error) {
33
+ console.error(`stopActivity failed with an error: ${error}`);
23
34
  }
24
- return ExpoLiveActivityModule.stopActivity(id, state);
25
35
  }
26
36
  /**
27
37
  * @param {string} id The identifier of the activity to update.
28
38
  * @param {LiveActivityState} state The updated state for the live activity.
29
- * @throws {Error} When function is called on a platform different from iOS.
30
39
  */
31
40
  export function updateActivity(id, state) {
32
41
  if (Platform.OS !== "ios") {
33
- throw new Error("updateActivity is only available on iOS");
42
+ console.error("updateActivity is only available on iOS");
43
+ }
44
+ try {
45
+ return ExpoLiveActivityModule.updateActivity(id, state);
46
+ }
47
+ catch (error) {
48
+ console.error(`updateActivity failed with an error: ${error}`);
34
49
  }
35
- return ExpoLiveActivityModule.updateActivity(id, state);
36
50
  }
37
51
  export function addActivityTokenListener(listener) {
38
52
  if (Platform.OS !== "ios") {
39
- throw new Error("updateActivity is only available on iOS");
53
+ console.error("addActivityTokenListener is only available on iOS");
54
+ return undefined;
40
55
  }
41
56
  return ExpoLiveActivityModule.addListener('onTokenReceived', listener);
42
57
  }
@@ -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;AA+BxC;;;;;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 LiveActivityStyles = {\n backgroundColor?: string;\n titleColor?: string;\n subtitleColor?: string;\n progressViewTint?: string;\n progressViewLabelColor?: 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 {LiveActivityStyles} styles Live activity styling 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, 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 * @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"]}
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;;;;GAIG;AACH,MAAM,UAAU,aAAa,CAAC,KAAwB,EAAE,MAA2B;IACjF,IAAI,QAAQ,CAAC,EAAE,KAAK,KAAK,EAAE,CAAC;QAC1B,OAAO,CAAC,KAAK,CAAC,wCAAwC,CAAC,CAAC;QACxD,OAAO,SAAS,CAAA;IAClB,CAAC;IACD,IAAI,CAAC;QACH,OAAO,sBAAsB,CAAC,aAAa,CAAC,KAAK,EAAE,MAAM,CAAC,CAAC;IAC7D,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,OAAO,CAAC,KAAK,CAAC,uCAAuC,KAAK,EAAE,CAAC,CAAC;QAC9D,OAAO,SAAS,CAAA;IAClB,CAAC;AACH,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,YAAY,CAAC,EAAU,EAAE,KAAwB;IAC/D,IAAI,QAAQ,CAAC,EAAE,KAAK,KAAK,EAAE,CAAC;QAC1B,OAAO,CAAC,KAAK,CAAC,uCAAuC,CAAC,CAAC;IACzD,CAAC;IACD,IAAI,CAAC;QACH,OAAO,sBAAsB,CAAC,YAAY,CAAC,EAAE,EAAE,KAAK,CAAC,CAAC;IACxD,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,OAAO,CAAC,KAAK,CAAC,sCAAsC,KAAK,EAAE,CAAC,CAAC;IAC/D,CAAC;AACH,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,cAAc,CAAC,EAAU,EAAE,KAAwB;IACjE,IAAI,QAAQ,CAAC,EAAE,KAAK,KAAK,EAAE,CAAC;QAC1B,OAAO,CAAC,KAAK,CAAC,yCAAyC,CAAC,CAAC;IAC3D,CAAC;IACD,IAAI,CAAC;QACH,OAAO,sBAAsB,CAAC,cAAc,CAAC,EAAE,EAAE,KAAK,CAAC,CAAC;IAC1D,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,OAAO,CAAC,KAAK,CAAC,wCAAwC,KAAK,EAAE,CAAC,CAAC;IACjE,CAAC;AACH,CAAC;AAED,MAAM,UAAU,wBAAwB,CAAC,QAAqD;IAC5F,IAAI,QAAQ,CAAC,EAAE,KAAK,KAAK,EAAE,CAAC;QAC1B,OAAO,CAAC,KAAK,CAAC,mDAAmD,CAAC,CAAC;QACnE,OAAO,SAAS,CAAA;IAClB,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 or undefined if creating live activity failed.\n */\nexport function startActivity(state: LiveActivityState, config?: LiveActivityConfig): string | undefined {\n if (Platform.OS !== \"ios\") {\n console.error(\"startActivity is only available on iOS\");\n return undefined\n }\n try {\n return ExpoLiveActivityModule.startActivity(state, config);\n } catch (error) {\n console.error(`startActivity failed with an error: ${error}`);\n return undefined\n }\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 */\nexport function stopActivity(id: string, state: LiveActivityState) {\n if (Platform.OS !== \"ios\") {\n console.error(\"stopActivity is only available on iOS\");\n }\n try {\n return ExpoLiveActivityModule.stopActivity(id, state);\n } catch (error) {\n console.error(`stopActivity failed with an error: ${error}`);\n }\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 */\nexport function updateActivity(id: string, state: LiveActivityState) {\n if (Platform.OS !== \"ios\") {\n console.error(\"updateActivity is only available on iOS\");\n }\n try {\n return ExpoLiveActivityModule.updateActivity(id, state);\n } catch (error) {\n console.error(`updateActivity failed with an error: ${error}`);\n }\n}\n\nexport function addActivityTokenListener(listener: (event: ActivityTokenReceivedEvent) => void): EventSubscription | undefined {\n if (Platform.OS !== \"ios\") {\n console.error(\"addActivityTokenListener is only available on iOS\");\n return undefined\n }\n return ExpoLiveActivityModule.addListener('onTokenReceived', listener);\n}\n"]}
@@ -1,9 +1,10 @@
1
1
  import ActivityKit
2
2
  import ExpoModulesCore
3
3
 
4
- enum ModuleErrors: Error {
5
- case unsupported
6
- case liveActivitiesNotEnabled
4
+ enum LiveActivityErrors: Error {
5
+ case unsupportedOS
6
+ case liveActivitiesNotEnabled
7
+ case unexpetedError(Error)
7
8
  }
8
9
 
9
10
  public class ExpoLiveActivityModule: Module {
@@ -24,7 +25,7 @@ public class ExpoLiveActivityModule: Module {
24
25
  var dynamicIslandImageName: String?
25
26
  }
26
27
 
27
- struct LiveActivityStyles: Record {
28
+ struct LiveActivityConfig: Record {
28
29
  @Field
29
30
  var backgroundColor: String?
30
31
 
@@ -39,6 +40,9 @@ public class ExpoLiveActivityModule: Module {
39
40
 
40
41
  @Field
41
42
  var progressViewLabelColor: String?
43
+
44
+ @Field
45
+ var deepLinkUrl: String?
42
46
 
43
47
  @Field
44
48
  var timerType: DynamicIslandTimerType?
@@ -59,10 +63,6 @@ public class ExpoLiveActivityModule: Module {
59
63
  )
60
64
  }
61
65
 
62
- func toContentStateDate(date: Double?) -> Date? {
63
- return date.map { Date(timeIntervalSince1970: $0 / 1000) }
64
- }
65
-
66
66
  func updateImages(state: LiveActivityState, newState: inout LiveActivityAttributes.ContentState) async throws {
67
67
  if let name = state.imageName {
68
68
  print("imageName: \(name)")
@@ -80,31 +80,33 @@ public class ExpoLiveActivityModule: Module {
80
80
 
81
81
  Events("onTokenReceived")
82
82
 
83
- Function("startActivity") { (state: LiveActivityState, styles: LiveActivityStyles?) -> String in
83
+ Function("startActivity") { (state: LiveActivityState, maybeConfig: LiveActivityConfig?) -> String in
84
84
  print("Starting activity")
85
85
  if #available(iOS 16.2, *) {
86
86
  if ActivityAuthorizationInfo().areActivitiesEnabled {
87
87
  do {
88
+ let config = maybeConfig ?? LiveActivityConfig()
88
89
  let attributes = LiveActivityAttributes(
89
90
  name: "ExpoLiveActivity",
90
- backgroundColor: styles?.backgroundColor,
91
- titleColor: styles?.titleColor,
92
- subtitleColor: styles?.subtitleColor,
93
- progressViewTint: styles?.progressViewTint,
94
- progressViewLabelColor: styles?.progressViewLabelColor,
95
- timerType: styles?.timerType == .digital ? .digital : .circular
91
+ backgroundColor: config.backgroundColor,
92
+ titleColor: config.titleColor,
93
+ subtitleColor: config.subtitleColor,
94
+ progressViewTint: config.progressViewTint,
95
+ progressViewLabelColor: config.progressViewLabelColor,
96
+ deepLinkUrl: config.deepLinkUrl,
97
+ timerType: config.timerType == .digital ? .digital : .circular
96
98
  )
97
99
  let initialState = LiveActivityAttributes.ContentState(
98
100
  title: state.title,
99
101
  subtitle: state.subtitle,
100
- date: toContentStateDate(date: state.date),
102
+ timerEndDateInMilliseconds: state.date,
101
103
  )
102
104
  let pushNotificationsEnabled =
103
- Bundle.main.object(forInfoDictionaryKey: "ExpoLiveActivity_EnablePushNotifications")
105
+ Bundle.main.object(forInfoDictionaryKey: "ExpoLiveActivity_EnablePushNotifications") as? Bool
104
106
  let activity = try Activity.request(
105
107
  attributes: attributes,
106
108
  content: .init(state: initialState, staleDate: nil),
107
- pushType: pushNotificationsEnabled == nil ? nil : .token
109
+ pushType: pushNotificationsEnabled == true ? .token : nil
108
110
  )
109
111
 
110
112
  Task {
@@ -124,11 +126,12 @@ public class ExpoLiveActivityModule: Module {
124
126
  return activity.id
125
127
  } catch (let error) {
126
128
  print("Error with live activity: \(error)")
129
+ throw LiveActivityErrors.unexpetedError(error)
127
130
  }
128
131
  }
129
- throw ModuleErrors.liveActivitiesNotEnabled
132
+ throw LiveActivityErrors.liveActivitiesNotEnabled
130
133
  } else {
131
- throw ModuleErrors.unsupported
134
+ throw LiveActivityErrors.unsupportedOS
132
135
  }
133
136
  }
134
137
 
@@ -138,7 +141,7 @@ public class ExpoLiveActivityModule: Module {
138
141
  var newState = LiveActivityAttributes.ContentState(
139
142
  title: state.title,
140
143
  subtitle: state.subtitle,
141
- date: toContentStateDate(date: state.date),
144
+ timerEndDateInMilliseconds: state.date,
142
145
  )
143
146
  if let activity = Activity<LiveActivityAttributes>.activities.first(where: {
144
147
  $0.id == activityId
@@ -155,7 +158,7 @@ public class ExpoLiveActivityModule: Module {
155
158
  print("Didn't find activity with ID \(activityId)")
156
159
  }
157
160
  } else {
158
- throw ModuleErrors.unsupported
161
+ throw LiveActivityErrors.unsupportedOS
159
162
  }
160
163
  }
161
164
 
@@ -165,7 +168,7 @@ public class ExpoLiveActivityModule: Module {
165
168
  var newState = LiveActivityAttributes.ContentState(
166
169
  title: state.title,
167
170
  subtitle: state.subtitle,
168
- date: toContentStateDate(date: state.date),
171
+ timerEndDateInMilliseconds: state.date,
169
172
  )
170
173
  if let activity = Activity<LiveActivityAttributes>.activities.first(where: {
171
174
  $0.id == activityId
@@ -179,7 +182,7 @@ public class ExpoLiveActivityModule: Module {
179
182
  print("Didn't find activity with ID \(activityId)")
180
183
  }
181
184
  } else {
182
- throw ModuleErrors.unsupported
185
+ throw LiveActivityErrors.unsupportedOS
183
186
  }
184
187
  }
185
188
  }
@@ -12,7 +12,7 @@ struct LiveActivityAttributes: ActivityAttributes {
12
12
  public struct ContentState: Codable, Hashable {
13
13
  var title: String
14
14
  var subtitle: String?
15
- var date: Date?
15
+ var timerEndDateInMilliseconds: Double?
16
16
  var imageName: String?
17
17
  var dynamicIslandImageName: String?
18
18
  }
@@ -23,7 +23,8 @@ struct LiveActivityAttributes: ActivityAttributes {
23
23
  var subtitleColor: String?
24
24
  var progressViewTint: String?
25
25
  var progressViewLabelColor: String?
26
- var timerType: DynamicIslandTimerType
26
+ var deepLinkUrl: String?
27
+ var timerType: DynamicIslandTimerType?
27
28
 
28
29
  enum DynamicIslandTimerType: String, Codable {
29
30
  case circular
@@ -54,7 +54,7 @@ import WidgetKit
54
54
  }
55
55
  }
56
56
 
57
- if let date = contentState.date {
57
+ if let date = contentState.timerEndDateInMilliseconds {
58
58
  ProgressView(timerInterval: Date.toTimerInterval(miliseconds: date))
59
59
  .tint(progressViewTint)
60
60
  .modifier(ConditionalForegroundViewModifier(color: attributes.progressViewLabelColor))
@@ -13,7 +13,7 @@ struct LiveActivityAttributes: ActivityAttributes {
13
13
  public struct ContentState: Codable, Hashable {
14
14
  var title: String
15
15
  var subtitle: String?
16
- var date: Double?
16
+ var timerEndDateInMilliseconds: Double?
17
17
  var imageName: String?
18
18
  var dynamicIslandImageName: String?
19
19
  }
@@ -24,7 +24,8 @@ struct LiveActivityAttributes: ActivityAttributes {
24
24
  var subtitleColor: String?
25
25
  var progressViewTint: String?
26
26
  var progressViewLabelColor: String?
27
- var timerType: DynamicIslandTimerType
27
+ var deepLinkUrl: String?
28
+ var timerType: DynamicIslandTimerType?
28
29
 
29
30
  enum DynamicIslandTimerType: String, Codable {
30
31
  case circular
@@ -37,49 +38,53 @@ struct LiveActivityWidget: Widget {
37
38
  ActivityConfiguration(for: LiveActivityAttributes.self) { context in
38
39
  LiveActivityView(contentState: context.state, attributes: context.attributes)
39
40
  .activityBackgroundTint(
40
- context.attributes.backgroundColor != nil ? Color(hex: context.attributes.backgroundColor!) : nil
41
+ context.attributes.backgroundColor.map { Color(hex: $0) }
41
42
  )
42
43
  .activitySystemActionForegroundColor(Color.black)
43
-
44
+ .applyWidgetURL(from: context.attributes.deepLinkUrl)
44
45
  } dynamicIsland: { context in
45
46
  DynamicIsland {
46
47
  DynamicIslandExpandedRegion(.leading, priority: 1) {
47
48
  dynamicIslandExpandedLeading(title: context.state.title, subtitle: context.state.subtitle)
48
49
  .dynamicIsland(verticalPlacement: .belowIfTooWide)
49
50
  .padding(.leading, 5)
51
+ .applyWidgetURL(from: context.attributes.deepLinkUrl)
50
52
  }
51
53
  DynamicIslandExpandedRegion(.trailing) {
52
54
  if let imageName = context.state.imageName {
53
55
  dynamicIslandExpandedTrailing(imageName: imageName)
54
56
  .padding(.trailing, 5)
57
+ .applyWidgetURL(from: context.attributes.deepLinkUrl)
55
58
  }
56
59
  }
57
60
  DynamicIslandExpandedRegion(.bottom) {
58
- if let date = context.state.date {
61
+ if let date = context.state.timerEndDateInMilliseconds {
59
62
  dynamicIslandExpandedBottom(endDate: date, progressViewTint: context.attributes.progressViewTint)
60
63
  .padding(.horizontal, 5)
64
+ .applyWidgetURL(from: context.attributes.deepLinkUrl)
61
65
  }
62
66
  }
63
67
  } compactLeading: {
64
68
  if let dynamicIslandImageName = context.state.dynamicIslandImageName {
65
69
  resizableImage(imageName: dynamicIslandImageName)
66
70
  .frame(maxWidth: 23, maxHeight: 23)
71
+ .applyWidgetURL(from: context.attributes.deepLinkUrl)
67
72
  }
68
73
  } compactTrailing: {
69
- if let date = context.state.date {
74
+ if let date = context.state.timerEndDateInMilliseconds {
70
75
  compactTimer(
71
76
  endDate: date,
72
- timerType: context.attributes.timerType,
77
+ timerType: context.attributes.timerType ?? .circular,
73
78
  progressViewTint: context.attributes.progressViewTint
74
- )
79
+ ).applyWidgetURL(from: context.attributes.deepLinkUrl)
75
80
  }
76
81
  } minimal: {
77
- if let date = context.state.date {
82
+ if let date = context.state.timerEndDateInMilliseconds {
78
83
  compactTimer(
79
84
  endDate: date,
80
- timerType: context.attributes.timerType,
85
+ timerType: context.attributes.timerType ?? .circular,
81
86
  progressViewTint: context.attributes.progressViewTint
82
- )
87
+ ).applyWidgetURL(from: context.attributes.deepLinkUrl)
83
88
  }
84
89
  }
85
90
  }
@@ -100,7 +105,7 @@ struct LiveActivityWidget: Widget {
100
105
  .multilineTextAlignment(.trailing)
101
106
  } else {
102
107
  circularTimer(endDate: endDate)
103
- .tint(progressViewTint != nil ? Color(hex: progressViewTint!) : nil)
108
+ .tint(progressViewTint.map { Color(hex: $0) })
104
109
  }
105
110
  }
106
111
 
@@ -133,7 +138,7 @@ struct LiveActivityWidget: Widget {
133
138
  private func dynamicIslandExpandedBottom(endDate: Double, progressViewTint: String?) -> some View {
134
139
  ProgressView(timerInterval: Date.toTimerInterval(miliseconds: endDate))
135
140
  .foregroundStyle(.white)
136
- .tint(progressViewTint != nil ? Color(hex: progressViewTint!) : nil)
141
+ .tint(progressViewTint.map { Color(hex: $0) })
137
142
  .padding(.top, 5)
138
143
  }
139
144
 
@@ -0,0 +1,19 @@
1
+ //
2
+ // View+applyIfPresent.swift
3
+ //
4
+ //
5
+ // Created by Artur Bilski on 05/08/2025.
6
+ //
7
+
8
+ import SwiftUI
9
+
10
+ extension View {
11
+ @ViewBuilder
12
+ func applyIfPresent<T>(_ value: T?, transform: (Self, T) -> some View) -> some View {
13
+ if let value {
14
+ transform(self, value)
15
+ } else {
16
+ self
17
+ }
18
+ }
19
+ }
@@ -0,0 +1,15 @@
1
+ //
2
+ // View+applyWidgetURL.swift
3
+ //
4
+ //
5
+ // Created by Artur Bilski on 05/08/2025.
6
+ //
7
+
8
+ import SwiftUI
9
+
10
+ extension View {
11
+ @ViewBuilder
12
+ func applyWidgetURL(from urlString: String?) -> some View {
13
+ applyIfPresent(urlString) { view, string in view.widgetURL(URL(string: string))}
14
+ }
15
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "expo-live-activity",
3
- "version": "0.2.0-alpha3",
3
+ "version": "0.2.0-alpha5",
4
4
  "description": "A module for adding Live Activity to a React Native app for iOS.",
5
5
  "main": "build/index.js",
6
6
  "types": "build/index.d.ts",
@@ -97,7 +97,7 @@ function getWidgetFiles(targetPath) {
97
97
  const imagesXcassetsSource = path.join(liveActivityFilesPath, "Assets.xcassets");
98
98
  copyFolderRecursiveSync(imagesXcassetsSource, targetPath);
99
99
  // Move images to assets directory
100
- if (fs.lstatSync(imageAssetsPath).isDirectory()) {
100
+ if (fs.existsSync(imageAssetsPath) && fs.lstatSync(imageAssetsPath).isDirectory()) {
101
101
  const imagesXcassetsTarget = path.join(targetPath, "Assets.xcassets");
102
102
  const files = fs.readdirSync(imageAssetsPath);
103
103
  files.forEach((file) => {
@@ -128,6 +128,9 @@ function getWidgetFiles(targetPath) {
128
128
  }
129
129
  });
130
130
  }
131
+ else {
132
+ console.warn(`Warning: Skipping adding images to live activity because directory does not exist at path: ${imageAssetsPath}`);
133
+ }
131
134
  return widgetFiles;
132
135
  }
133
136
  function copyFileSync(source, target) {
@@ -75,7 +75,7 @@ export function getWidgetFiles(
75
75
  copyFolderRecursiveSync(imagesXcassetsSource, targetPath);
76
76
 
77
77
  // Move images to assets directory
78
- if (fs.lstatSync(imageAssetsPath).isDirectory()) {
78
+ if (fs.existsSync(imageAssetsPath) && fs.lstatSync(imageAssetsPath).isDirectory()) {
79
79
  const imagesXcassetsTarget = path.join(targetPath, "Assets.xcassets");
80
80
 
81
81
  const files = fs.readdirSync(imageAssetsPath);
@@ -114,6 +114,8 @@ export function getWidgetFiles(
114
114
  );
115
115
  }
116
116
  })
117
+ } else {
118
+ console.warn(`Warning: Skipping adding images to live activity because directory does not exist at path: ${imageAssetsPath}`);
117
119
  }
118
120
 
119
121
  return widgetFiles;
package/src/index.ts CHANGED
@@ -12,12 +12,13 @@ export type LiveActivityState = {
12
12
  dynamicIslandImageName?: string;
13
13
  };
14
14
 
15
- export type LiveActivityStyles = {
15
+ export type LiveActivityConfig = {
16
16
  backgroundColor?: string;
17
17
  titleColor?: string;
18
18
  subtitleColor?: string;
19
19
  progressViewTint?: string;
20
20
  progressViewLabelColor?: string;
21
+ deepLinkUrl?: string;
21
22
  timerType?: DynamicIslandTimerType;
22
23
  };
23
24
 
@@ -32,44 +33,56 @@ export type LiveActivityModuleEvents = {
32
33
 
33
34
  /**
34
35
  * @param {LiveActivityState} state The state for the live activity.
35
- * @param {LiveActivityStyles} styles Live activity styling object.
36
- * @returns {string} The identifier of the started activity.
37
- * @throws {Error} When function is called on a platform different from iOS.
36
+ * @param {LiveActivityConfig} config Live activity config object.
37
+ * @returns {string} The identifier of the started activity or undefined if creating live activity failed.
38
38
  */
39
- export function startActivity(state: LiveActivityState, styles?: LiveActivityStyles): string {
39
+ export function startActivity(state: LiveActivityState, config?: LiveActivityConfig): string | undefined {
40
40
  if (Platform.OS !== "ios") {
41
- throw new Error("startActivity is only available on iOS");
41
+ console.error("startActivity is only available on iOS");
42
+ return undefined
43
+ }
44
+ try {
45
+ return ExpoLiveActivityModule.startActivity(state, config);
46
+ } catch (error) {
47
+ console.error(`startActivity failed with an error: ${error}`);
48
+ return undefined
42
49
  }
43
- return ExpoLiveActivityModule.startActivity(state, styles);
44
50
  }
45
51
 
46
52
  /**
47
53
  * @param {string} id The identifier of the activity to stop.
48
54
  * @param {LiveActivityState} state The updated state for the live activity.
49
- * @throws {Error} When function is called on a platform different from iOS.
50
55
  */
51
56
  export function stopActivity(id: string, state: LiveActivityState) {
52
57
  if (Platform.OS !== "ios") {
53
- throw new Error("stopActivity is only available on iOS");
58
+ console.error("stopActivity is only available on iOS");
59
+ }
60
+ try {
61
+ return ExpoLiveActivityModule.stopActivity(id, state);
62
+ } catch (error) {
63
+ console.error(`stopActivity failed with an error: ${error}`);
54
64
  }
55
- return ExpoLiveActivityModule.stopActivity(id, state);
56
65
  }
57
66
 
58
67
  /**
59
68
  * @param {string} id The identifier of the activity to update.
60
69
  * @param {LiveActivityState} state The updated state for the live activity.
61
- * @throws {Error} When function is called on a platform different from iOS.
62
70
  */
63
71
  export function updateActivity(id: string, state: LiveActivityState) {
64
72
  if (Platform.OS !== "ios") {
65
- throw new Error("updateActivity is only available on iOS");
73
+ console.error("updateActivity is only available on iOS");
74
+ }
75
+ try {
76
+ return ExpoLiveActivityModule.updateActivity(id, state);
77
+ } catch (error) {
78
+ console.error(`updateActivity failed with an error: ${error}`);
66
79
  }
67
- return ExpoLiveActivityModule.updateActivity(id, state);
68
80
  }
69
81
 
70
- export function addActivityTokenListener(listener: (event: ActivityTokenReceivedEvent) => void): EventSubscription {
82
+ export function addActivityTokenListener(listener: (event: ActivityTokenReceivedEvent) => void): EventSubscription | undefined {
71
83
  if (Platform.OS !== "ios") {
72
- throw new Error("updateActivity is only available on iOS");
84
+ console.error("addActivityTokenListener is only available on iOS");
85
+ return undefined
73
86
  }
74
87
  return ExpoLiveActivityModule.addListener('onTokenReceived', listener);
75
88
  }