expo-live-activity 0.4.3-alpha1 → 0.5.0-alpha1

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
@@ -132,9 +132,12 @@ The `state` object should include:
132
132
  {
133
133
  title: string;
134
134
  subtitle?: string;
135
- progressBar: { // Only one property, either date or progress, is available at a time as they share a single progress bar component
136
- date?: number; // Set as epoch time in milliseconds. This is used as an end date in a timer.
137
- progress?: number; //Set amount of progress in the progress bar
135
+ progressBar: { // Only one property (date, progress, or elapsedTimer) is available at a time
136
+ date?: number; // Set as epoch time in milliseconds. This is used as an end date in a countdown timer.
137
+ progress?: number; // Set amount of progress in the progress bar (0-1)
138
+ elapsedTimer?: { // Count up timer (elapsed time from start)
139
+ startDate: number; // Epoch time in milliseconds when the timer started
140
+ };
138
141
  };
139
142
  imageName?: string; // Matches the name of the image in 'assets/liveActivity'
140
143
  dynamicIslandImageName?: string; // Matches the name of the image in 'assets/liveActivity'
@@ -157,7 +160,7 @@ The `config` object should include:
157
160
  padding?: Padding // number | {top?: number bottom?: number ...}
158
161
  imagePosition?: ImagePosition; // 'left' | 'right';
159
162
  imageAlign?: ImageAlign; // 'top' | 'center' | 'bottom'
160
- imageSize?: ImageSize // { width: number|`${number}%`, height: number|`${number}%` } | undefined (defaults to 64pt)
163
+ imageSize?: ImageSize // { width?: number|`${number}%`, height?: number|`${number}%` } | undefined (defaults to 64pt)
161
164
  contentFit?: ImageContentFit; // 'cover' | 'contain' | 'fill' | 'none' | 'scale-down'
162
165
  };
163
166
  ```
@@ -202,6 +205,26 @@ const activityId = LiveActivity.startActivity(state, config)
202
205
 
203
206
  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.
204
207
 
208
+ Using an elapsed timer:
209
+
210
+ ```typescript
211
+ const elapsedTimerState: LiveActivity.LiveActivityState = {
212
+ title: 'Walk in Progress',
213
+ subtitle: 'With Max the Dog',
214
+ progressBar: {
215
+ elapsedTimer: {
216
+ startDate: Date.now() - 5 * 60 * 1000, // Started 5 minutes ago
217
+ },
218
+ },
219
+ imageName: 'dog_walking',
220
+ dynamicIslandImageName: 'dog_icon',
221
+ }
222
+
223
+ const activityId = LiveActivity.startActivity(elapsedTimerState, config)
224
+ ```
225
+
226
+ The elapsed timer will automatically update every second based on the `startDate` you provide.
227
+
205
228
  Subscribing to push token changes:
206
229
 
207
230
  ```typescript
@@ -246,7 +269,8 @@ Example payload for starting Live Activity:
246
269
  "timerEndDateInMilliseconds": 1754410997000,
247
270
  "progress": 0.5,
248
271
  "imageName": "live_activity_image",
249
- "dynamicIslandImageName": "dynamic_island_image"
272
+ "dynamicIslandImageName": "dynamic_island_image",
273
+ "elapsedTimerStartDateInMilliseconds": null
250
274
  },
251
275
  "timestamp": 1754491435000, // timestamp of when the push notification was sent
252
276
  "attributes-type": "LiveActivityAttributes",
@@ -290,7 +314,36 @@ Example payload for updating Live Activity:
290
314
  }
291
315
  ```
292
316
 
293
- Where `timerEndDateInMilliseconds` value is a timestamp in milliseconds corresponding to the target point of the counter displayed in Live Activity view.
317
+ Where `timerEndDateInMilliseconds` value is a timestamp in milliseconds corresponding to the target point of the countdown displayed in Live Activity view.
318
+
319
+ Example payload for starting Live Activity with elapsed timer:
320
+
321
+ ```json
322
+ {
323
+ "aps": {
324
+ "event": "start",
325
+ "content-state": {
326
+ "title": "Walk in Progress",
327
+ "subtitle": "With Max",
328
+ "timerEndDateInMilliseconds": null,
329
+ "progress": null,
330
+ "imageName": "dog_walking",
331
+ "dynamicIslandImageName": "dog_icon",
332
+ "elapsedTimerStartDateInMilliseconds": 1754410997000
333
+ },
334
+ "timestamp": 1754491435000,
335
+ "attributes-type": "LiveActivityAttributes",
336
+ "attributes": {
337
+ "name": "WalkActivity",
338
+ "backgroundColor": "001A72",
339
+ "titleColor": "EBEBF0",
340
+ "progressViewLabelColor": "FFFFFF"
341
+ }
342
+ }
343
+ }
344
+ ```
345
+
346
+ Where `elapsedTimerStartDateInMilliseconds` is the timestamp (in milliseconds) when the elapsed timer started counting up.
294
347
 
295
348
  ## Image support
296
349
 
package/build/index.d.ts CHANGED
@@ -1,12 +1,33 @@
1
1
  import { EventSubscription } from 'expo-modules-core';
2
2
  type Voidable<T> = T | void;
3
3
  export type DynamicIslandTimerType = 'circular' | 'digital';
4
+ export type ElapsedTimer = {
5
+ startDate: number;
6
+ };
4
7
  type ProgressBarType = {
5
8
  date?: number;
6
9
  progress?: undefined;
10
+ elapsedTimer?: undefined;
11
+ currentStep?: undefined;
12
+ totalSteps?: undefined;
7
13
  } | {
8
14
  date?: undefined;
9
15
  progress?: number;
16
+ elapsedTimer?: undefined;
17
+ currentStep?: undefined;
18
+ totalSteps?: undefined;
19
+ } | {
20
+ date?: undefined;
21
+ progress?: undefined;
22
+ elapsedTimer?: ElapsedTimer;
23
+ currentStep?: undefined;
24
+ totalSteps?: undefined;
25
+ } | {
26
+ date?: undefined;
27
+ progress?: undefined;
28
+ elapsedTimer?: undefined;
29
+ currentStep?: number;
30
+ totalSteps?: number;
10
31
  };
11
32
  export type LiveActivityState = {
12
33
  title: string;
@@ -14,6 +35,7 @@ export type LiveActivityState = {
14
35
  progressBar?: ProgressBarType;
15
36
  imageName?: string;
16
37
  dynamicIslandImageName?: string;
38
+ smallImageName?: string;
17
39
  };
18
40
  export type NativeLiveActivityState = {
19
41
  title: string;
@@ -35,8 +57,8 @@ export type ImagePosition = 'left' | 'right' | 'leftStretch' | 'rightStretch';
35
57
  export type ImageAlign = 'top' | 'center' | 'bottom';
36
58
  export type ImageDimension = number | `${number}%`;
37
59
  export type ImageSize = {
38
- width: ImageDimension;
39
- height: ImageDimension;
60
+ width?: ImageDimension;
61
+ height?: ImageDimension;
40
62
  };
41
63
  export type ImageContentFit = 'cover' | 'contain' | 'fill' | 'none' | 'scale-down';
42
64
  export type LiveActivityConfig = {
@@ -51,7 +73,10 @@ export type LiveActivityConfig = {
51
73
  imagePosition?: ImagePosition;
52
74
  imageAlign?: ImageAlign;
53
75
  imageSize?: ImageSize;
76
+ smallImageSize?: ImageSize;
54
77
  contentFit?: ImageContentFit;
78
+ progressSegmentActiveColor?: string;
79
+ progressSegmentInactiveColor?: string;
55
80
  };
56
81
  export type ActivityTokenReceivedEvent = {
57
82
  activityID: string;
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
package/build/index.js CHANGED
@@ -9,8 +9,12 @@ function assertIOS(name) {
9
9
  function normalizeConfig(config) {
10
10
  if (config === undefined)
11
11
  return config;
12
- const { padding, imageSize, ...base } = config;
13
- const normalized = { ...base };
12
+ const { padding, imageSize, smallImageSize, progressSegmentActiveColor, progressSegmentInactiveColor, ...base } = config;
13
+ const normalized = {
14
+ ...base,
15
+ progressSegmentActiveColor,
16
+ progressSegmentInactiveColor,
17
+ };
14
18
  // Normalize padding: keep number in padding, object in paddingDetails
15
19
  if (typeof padding === 'number') {
16
20
  normalized.padding = padding;
@@ -18,34 +22,43 @@ function normalizeConfig(config) {
18
22
  else if (typeof padding === 'object') {
19
23
  normalized.paddingDetails = padding;
20
24
  }
21
- // Normalize imageSize: object with width/height each a number (points) or percent string like '50%'
22
- if (imageSize) {
25
+ // Helper to parse a dimension value (number or percent string like '50%')
26
+ const parseDimension = (value, fieldName) => {
27
+ if (value === undefined)
28
+ return {};
29
+ if (typeof value === 'number')
30
+ return { absolute: value };
23
31
  const regExp = /^(100(?:\.0+)?|\d{1,2}(?:\.\d+)?)%$/; // Matches 0.0% to 100.0%
24
- const { width, height } = imageSize;
25
- if (typeof width === 'number') {
26
- normalized.imageWidth = width;
27
- }
28
- else if (typeof width === 'string') {
29
- const match = width.trim().match(regExp);
30
- if (match) {
31
- normalized.imageWidthPercent = Number(match[1]);
32
- }
33
- else {
34
- throw new Error('imageSize.width percent string must be in format "0%" to "100%"');
35
- }
36
- }
37
- if (typeof height === 'number') {
38
- normalized.imageHeight = height;
39
- }
40
- else if (typeof height === 'string') {
41
- const match = height.trim().match(regExp);
42
- if (match) {
43
- normalized.imageHeightPercent = Number(match[1]);
44
- }
45
- else {
46
- throw new Error('imageSize.height percent string must be in format "0%" to "100%"');
47
- }
48
- }
32
+ const match = value.trim().match(regExp);
33
+ if (match)
34
+ return { percent: Number(match[1]) };
35
+ throw new Error(`${fieldName} percent string must be in format "0%" to "100%"`);
36
+ };
37
+ // Normalize imageSize
38
+ if (imageSize) {
39
+ const w = parseDimension(imageSize.width, 'imageSize.width');
40
+ const h = parseDimension(imageSize.height, 'imageSize.height');
41
+ if (w.absolute !== undefined)
42
+ normalized.imageWidth = w.absolute;
43
+ if (w.percent !== undefined)
44
+ normalized.imageWidthPercent = w.percent;
45
+ if (h.absolute !== undefined)
46
+ normalized.imageHeight = h.absolute;
47
+ if (h.percent !== undefined)
48
+ normalized.imageHeightPercent = h.percent;
49
+ }
50
+ // Normalize smallImageSize
51
+ if (smallImageSize) {
52
+ const w = parseDimension(smallImageSize.width, 'smallImageSize.width');
53
+ const h = parseDimension(smallImageSize.height, 'smallImageSize.height');
54
+ if (w.absolute !== undefined)
55
+ normalized.smallImageWidth = w.absolute;
56
+ if (w.percent !== undefined)
57
+ normalized.smallImageWidthPercent = w.percent;
58
+ if (h.absolute !== undefined)
59
+ normalized.smallImageHeight = h.absolute;
60
+ if (h.percent !== undefined)
61
+ normalized.smallImageHeightPercent = h.percent;
49
62
  }
50
63
  return normalized;
51
64
  }
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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","sourcesContent":["import { EventSubscription } from 'expo-modules-core'\nimport { Platform } from 'react-native'\n\nimport ExpoLiveActivityModule from './ExpoLiveActivityModule'\n\ntype Voidable<T> = T | void\n\nexport type DynamicIslandTimerType = 'circular' | 'digital'\n\ntype ProgressBarType =\n | {\n date?: number\n progress?: undefined\n }\n | {\n date?: undefined\n progress?: number\n }\n\nexport type LiveActivityState = {\n title: string\n subtitle?: string\n progressBar?: ProgressBarType\n imageName?: string\n dynamicIslandImageName?: string\n}\n\nexport type NativeLiveActivityState = {\n title: string\n subtitle?: string\n date?: number\n progress?: number\n imageName?: string\n dynamicIslandImageName?: string\n}\n\nexport type Padding =\n | {\n top?: number\n bottom?: number\n left?: number\n right?: number\n vertical?: number\n horizontal?: number\n }\n | number\n\nexport type ImagePosition = 'left' | 'right' | 'leftStretch' | 'rightStretch'\n\nexport type ImageAlign = 'top' | 'center' | 'bottom'\n\nexport type ImageDimension = number | `${number}%`\nexport type ImageSize = {\n width: ImageDimension\n height: ImageDimension\n}\n\nexport type ImageContentFit = 'cover' | 'contain' | 'fill' | 'none' | 'scale-down'\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 padding?: Padding\n imagePosition?: ImagePosition\n imageAlign?: ImageAlign\n imageSize?: ImageSize\n contentFit?: ImageContentFit\n}\n\nexport type ActivityTokenReceivedEvent = {\n activityID: string\n activityName: string\n activityPushToken: string\n}\n\nexport type ActivityPushToStartTokenReceivedEvent = {\n activityPushToStartToken: string | null\n}\n\ntype ActivityState = 'active' | 'dismissed' | 'pending' | 'stale' | 'ended'\n\nexport type ActivityUpdateEvent = {\n activityID: string\n activityName: string\n activityState: ActivityState\n}\n\nexport type LiveActivityModuleEvents = {\n onTokenReceived: (params: ActivityTokenReceivedEvent) => void\n onPushToStartTokenReceived: (params: ActivityPushToStartTokenReceivedEvent) => void\n onStateChange: (params: ActivityUpdateEvent) => void\n}\n\nfunction assertIOS(name: string) {\n const isIOS = Platform.OS === 'ios'\n\n if (!isIOS) console.error(`${name} is only available on iOS`)\n\n return isIOS\n}\n\nfunction normalizeConfig(config?: LiveActivityConfig) {\n if (config === undefined) return config\n\n const { padding, imageSize, ...base } = config\n type NormalizedConfig = LiveActivityConfig & {\n paddingDetails?: Padding\n imageWidth?: number\n imageHeight?: number\n imageWidthPercent?: number\n imageHeightPercent?: number\n }\n const normalized: NormalizedConfig = { ...base }\n\n // Normalize padding: keep number in padding, object in paddingDetails\n if (typeof padding === 'number') {\n normalized.padding = padding\n } else if (typeof padding === 'object') {\n normalized.paddingDetails = padding\n }\n\n // Normalize imageSize: object with width/height each a number (points) or percent string like '50%'\n if (imageSize) {\n const regExp = /^(100(?:\\.0+)?|\\d{1,2}(?:\\.\\d+)?)%$/ // Matches 0.0% to 100.0%\n\n const { width, height } = imageSize\n\n if (typeof width === 'number') {\n normalized.imageWidth = width\n } else if (typeof width === 'string') {\n const match = width.trim().match(regExp)\n if (match) {\n normalized.imageWidthPercent = Number(match[1])\n } else {\n throw new Error('imageSize.width percent string must be in format \"0%\" to \"100%\"')\n }\n }\n\n if (typeof height === 'number') {\n normalized.imageHeight = height\n } else if (typeof height === 'string') {\n const match = height.trim().match(regExp)\n if (match) {\n normalized.imageHeightPercent = Number(match[1])\n } else {\n throw new Error('imageSize.height percent string must be in format \"0%\" to \"100%\"')\n }\n }\n }\n\n return normalized\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): Voidable<string> {\n if (assertIOS('startActivity')) return ExpoLiveActivityModule.startActivity(state, normalizeConfig(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 */\nexport function stopActivity(id: string, state: LiveActivityState) {\n if (assertIOS('stopActivity')) 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 */\nexport function updateActivity(id: string, state: LiveActivityState) {\n if (assertIOS('updateActivity')) return ExpoLiveActivityModule.updateActivity(id, state)\n}\n\n/**\n * @param {function} updateTokenListener The listener function that will be called when an update token is received.\n */\nexport function addActivityTokenListener(\n updateTokenListener: (event: ActivityTokenReceivedEvent) => void\n): Voidable<EventSubscription> {\n if (assertIOS('addActivityTokenListener'))\n return ExpoLiveActivityModule.addListener('onTokenReceived', updateTokenListener)\n}\n\n/**\n * Adds a listener that is called when a push-to-start token is received. Supported only on iOS > 17.2.\n * On earlier iOS versions, the listener will return null as a token.\n * @param {function} listener The listener function that will be called when the observer starts and then when a push-to-start token is received.\n */\nexport function addActivityPushToStartTokenListener(\n listener: (event: ActivityPushToStartTokenReceivedEvent) => void\n): Voidable<EventSubscription> {\n if (assertIOS('addActivityPushToStartTokenListener'))\n return ExpoLiveActivityModule.addListener('onPushToStartTokenReceived', listener)\n}\n\n/**\n * @param {function} statusListener The listener function that will be called when an activity status changes.\n */\nexport function addActivityUpdatesListener(\n statusListener: (event: ActivityUpdateEvent) => void\n): Voidable<EventSubscription> {\n if (assertIOS('addActivityUpdatesListener'))\n return ExpoLiveActivityModule.addListener('onStateChange', statusListener)\n}\n"]}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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","sourcesContent":["import { EventSubscription } from 'expo-modules-core'\nimport { Platform } from 'react-native'\n\nimport ExpoLiveActivityModule from './ExpoLiveActivityModule'\n\ntype Voidable<T> = T | void\n\nexport type DynamicIslandTimerType = 'circular' | 'digital'\n\nexport type ElapsedTimer = {\n startDate: number // milliseconds timestamp (past time when timer started)\n}\n\ntype ProgressBarType =\n | {\n date?: number\n progress?: undefined\n elapsedTimer?: undefined\n currentStep?: undefined\n totalSteps?: undefined\n }\n | {\n date?: undefined\n progress?: number\n elapsedTimer?: undefined\n currentStep?: undefined\n totalSteps?: undefined\n }\n | {\n date?: undefined\n progress?: undefined\n elapsedTimer?: ElapsedTimer\n currentStep?: undefined\n totalSteps?: undefined\n }\n | {\n date?: undefined\n progress?: undefined\n elapsedTimer?: undefined\n currentStep?: number\n totalSteps?: number\n }\n\nexport type LiveActivityState = {\n title: string\n subtitle?: string\n progressBar?: ProgressBarType\n imageName?: string\n dynamicIslandImageName?: string\n smallImageName?: string\n}\n\nexport type NativeLiveActivityState = {\n title: string\n subtitle?: string\n date?: number\n progress?: number\n imageName?: string\n dynamicIslandImageName?: string\n}\n\nexport type Padding =\n | {\n top?: number\n bottom?: number\n left?: number\n right?: number\n vertical?: number\n horizontal?: number\n }\n | number\n\nexport type ImagePosition = 'left' | 'right' | 'leftStretch' | 'rightStretch'\n\nexport type ImageAlign = 'top' | 'center' | 'bottom'\n\nexport type ImageDimension = number | `${number}%`\nexport type ImageSize = {\n width?: ImageDimension\n height?: ImageDimension\n}\n\nexport type ImageContentFit = 'cover' | 'contain' | 'fill' | 'none' | 'scale-down'\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 padding?: Padding\n imagePosition?: ImagePosition\n imageAlign?: ImageAlign\n imageSize?: ImageSize\n smallImageSize?: ImageSize\n contentFit?: ImageContentFit\n progressSegmentActiveColor?: string\n progressSegmentInactiveColor?: string\n}\n\nexport type ActivityTokenReceivedEvent = {\n activityID: string\n activityName: string\n activityPushToken: string\n}\n\nexport type ActivityPushToStartTokenReceivedEvent = {\n activityPushToStartToken: string | null\n}\n\ntype ActivityState = 'active' | 'dismissed' | 'pending' | 'stale' | 'ended'\n\nexport type ActivityUpdateEvent = {\n activityID: string\n activityName: string\n activityState: ActivityState\n}\n\nexport type LiveActivityModuleEvents = {\n onTokenReceived: (params: ActivityTokenReceivedEvent) => void\n onPushToStartTokenReceived: (params: ActivityPushToStartTokenReceivedEvent) => void\n onStateChange: (params: ActivityUpdateEvent) => void\n}\n\nfunction assertIOS(name: string) {\n const isIOS = Platform.OS === 'ios'\n\n if (!isIOS) console.error(`${name} is only available on iOS`)\n\n return isIOS\n}\n\nfunction normalizeConfig(config?: LiveActivityConfig) {\n if (config === undefined) return config\n\n const { padding, imageSize, smallImageSize, progressSegmentActiveColor, progressSegmentInactiveColor, ...base } =\n config\n type NormalizedConfig = LiveActivityConfig & {\n paddingDetails?: Padding\n imageWidth?: number\n imageHeight?: number\n imageWidthPercent?: number\n imageHeightPercent?: number\n smallImageWidth?: number\n smallImageHeight?: number\n smallImageWidthPercent?: number\n smallImageHeightPercent?: number\n }\n const normalized: NormalizedConfig = {\n ...base,\n progressSegmentActiveColor,\n progressSegmentInactiveColor,\n }\n\n // Normalize padding: keep number in padding, object in paddingDetails\n if (typeof padding === 'number') {\n normalized.padding = padding\n } else if (typeof padding === 'object') {\n normalized.paddingDetails = padding\n }\n\n // Helper to parse a dimension value (number or percent string like '50%')\n const parseDimension = (\n value: ImageDimension | undefined,\n fieldName: string\n ): { absolute?: number; percent?: number } => {\n if (value === undefined) return {}\n\n if (typeof value === 'number') return { absolute: value }\n const regExp = /^(100(?:\\.0+)?|\\d{1,2}(?:\\.\\d+)?)%$/ // Matches 0.0% to 100.0%\n const match = value.trim().match(regExp)\n if (match) return { percent: Number(match[1]) }\n throw new Error(`${fieldName} percent string must be in format \"0%\" to \"100%\"`)\n }\n\n // Normalize imageSize\n if (imageSize) {\n const w = parseDimension(imageSize.width, 'imageSize.width')\n const h = parseDimension(imageSize.height, 'imageSize.height')\n if (w.absolute !== undefined) normalized.imageWidth = w.absolute\n if (w.percent !== undefined) normalized.imageWidthPercent = w.percent\n if (h.absolute !== undefined) normalized.imageHeight = h.absolute\n if (h.percent !== undefined) normalized.imageHeightPercent = h.percent\n }\n\n // Normalize smallImageSize\n if (smallImageSize) {\n const w = parseDimension(smallImageSize.width, 'smallImageSize.width')\n const h = parseDimension(smallImageSize.height, 'smallImageSize.height')\n if (w.absolute !== undefined) normalized.smallImageWidth = w.absolute\n if (w.percent !== undefined) normalized.smallImageWidthPercent = w.percent\n if (h.absolute !== undefined) normalized.smallImageHeight = h.absolute\n if (h.percent !== undefined) normalized.smallImageHeightPercent = h.percent\n }\n\n return normalized\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): Voidable<string> {\n if (assertIOS('startActivity')) return ExpoLiveActivityModule.startActivity(state, normalizeConfig(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 */\nexport function stopActivity(id: string, state: LiveActivityState) {\n if (assertIOS('stopActivity')) 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 */\nexport function updateActivity(id: string, state: LiveActivityState) {\n if (assertIOS('updateActivity')) return ExpoLiveActivityModule.updateActivity(id, state)\n}\n\n/**\n * @param {function} updateTokenListener The listener function that will be called when an update token is received.\n */\nexport function addActivityTokenListener(\n updateTokenListener: (event: ActivityTokenReceivedEvent) => void\n): Voidable<EventSubscription> {\n if (assertIOS('addActivityTokenListener'))\n return ExpoLiveActivityModule.addListener('onTokenReceived', updateTokenListener)\n}\n\n/**\n * Adds a listener that is called when a push-to-start token is received. Supported only on iOS > 17.2.\n * On earlier iOS versions, the listener will return null as a token.\n * @param {function} listener The listener function that will be called when the observer starts and then when a push-to-start token is received.\n */\nexport function addActivityPushToStartTokenListener(\n listener: (event: ActivityPushToStartTokenReceivedEvent) => void\n): Voidable<EventSubscription> {\n if (assertIOS('addActivityPushToStartTokenListener'))\n return ExpoLiveActivityModule.addListener('onPushToStartTokenReceived', listener)\n}\n\n/**\n * @param {function} statusListener The listener function that will be called when an activity status changes.\n */\nexport function addActivityUpdatesListener(\n statusListener: (event: ActivityUpdateEvent) => void\n): Voidable<EventSubscription> {\n if (assertIOS('addActivityUpdatesListener'))\n return ExpoLiveActivityModule.addListener('onStateChange', statusListener)\n}\n"]}
@@ -18,6 +18,20 @@ public class ExpoLiveActivityModule: Module {
18
18
 
19
19
  @Field
20
20
  var progress: Double?
21
+
22
+ @Field
23
+ var elapsedTimer: ElapsedTimer?
24
+
25
+ @Field
26
+ var currentStep: Int?
27
+
28
+ @Field
29
+ var totalSteps: Int?
30
+
31
+ struct ElapsedTimer: Record {
32
+ @Field
33
+ var startDate: Double?
34
+ }
21
35
  }
22
36
 
23
37
  @Field
@@ -25,6 +39,9 @@ public class ExpoLiveActivityModule: Module {
25
39
 
26
40
  @Field
27
41
  var dynamicIslandImageName: String?
42
+
43
+ @Field
44
+ var smallImageName: String?
28
45
  }
29
46
 
30
47
  struct LiveActivityConfig: Record {
@@ -70,12 +87,30 @@ public class ExpoLiveActivityModule: Module {
70
87
  @Field
71
88
  var imageHeightPercent: Double?
72
89
 
90
+ @Field
91
+ var smallImageWidth: Int?
92
+
93
+ @Field
94
+ var smallImageHeight: Int?
95
+
96
+ @Field
97
+ var smallImageWidthPercent: Double?
98
+
99
+ @Field
100
+ var smallImageHeightPercent: Double?
101
+
73
102
  @Field
74
103
  var imageAlign: String?
75
104
 
76
105
  @Field
77
106
  var contentFit: String?
78
107
 
108
+ @Field
109
+ var progressSegmentActiveColor: String?
110
+
111
+ @Field
112
+ var progressSegmentInactiveColor: String?
113
+
79
114
  struct PaddingDetails: Record {
80
115
  @Field var top: Int?
81
116
  @Field var bottom: Int?
@@ -136,6 +171,10 @@ public class ExpoLiveActivityModule: Module {
136
171
  if let name = state.dynamicIslandImageName {
137
172
  newState.dynamicIslandImageName = try await resolveImage(from: name)
138
173
  }
174
+
175
+ if let name = state.smallImageName {
176
+ newState.smallImageName = try await resolveImage(from: name)
177
+ }
139
178
  }
140
179
 
141
180
  private func observePushToStartToken() {
@@ -197,6 +236,11 @@ public class ExpoLiveActivityModule: Module {
197
236
  ?? false
198
237
  }
199
238
 
239
+ private var silentOnUnsupportedOS: Bool {
240
+ Bundle.main.object(forInfoDictionaryKey: "ExpoLiveActivity_SilentOnUnsupportedOS") as? Bool
241
+ ?? false
242
+ }
243
+
200
244
  public func definition() -> ModuleDefinition {
201
245
  Name("ExpoLiveActivity")
202
246
 
@@ -214,7 +258,12 @@ public class ExpoLiveActivityModule: Module {
214
258
 
215
259
  Function("startActivity") {
216
260
  (state: LiveActivityState, maybeConfig: LiveActivityConfig?) -> String in
217
- guard #available(iOS 16.2, *) else { throw UnsupportedOSException("16.2") }
261
+ guard #available(iOS 16.2, *) else {
262
+ if silentOnUnsupportedOS {
263
+ return ""
264
+ }
265
+ throw UnsupportedOSException("16.2")
266
+ }
218
267
 
219
268
  guard ActivityAuthorizationInfo().areActivitiesEnabled else {
220
269
  throw LiveActivitiesNotEnabledException()
@@ -248,15 +297,27 @@ public class ExpoLiveActivityModule: Module {
248
297
  imageHeight: config.imageHeight,
249
298
  imageWidthPercent: config.imageWidthPercent,
250
299
  imageHeightPercent: config.imageHeightPercent,
300
+ smallImageWidth: config.smallImageWidth,
301
+ smallImageHeight: config.smallImageHeight,
302
+ smallImageWidthPercent: config.smallImageWidthPercent,
303
+ smallImageHeightPercent: config.smallImageHeightPercent,
251
304
  imageAlign: config.imageAlign,
252
- contentFit: config.contentFit
305
+ contentFit: config.contentFit,
306
+ progressSegmentActiveColor: config.progressSegmentActiveColor,
307
+ progressSegmentInactiveColor: config.progressSegmentInactiveColor
253
308
  )
254
309
 
255
310
  let initialState = LiveActivityAttributes.ContentState(
256
311
  title: state.title,
257
312
  subtitle: state.subtitle,
258
313
  timerEndDateInMilliseconds: state.progressBar?.date,
259
- progress: state.progressBar?.progress
314
+ progress: state.progressBar?.progress,
315
+ imageName: state.imageName,
316
+ dynamicIslandImageName: state.dynamicIslandImageName,
317
+ smallImageName: state.smallImageName,
318
+ elapsedTimerStartDateInMilliseconds: state.progressBar?.elapsedTimer?.startDate,
319
+ currentStep: state.progressBar?.currentStep,
320
+ totalSteps: state.progressBar?.totalSteps
260
321
  )
261
322
 
262
323
  let activity = try Activity.request(
@@ -278,7 +339,12 @@ public class ExpoLiveActivityModule: Module {
278
339
  }
279
340
 
280
341
  Function("stopActivity") { (activityId: String, state: LiveActivityState) in
281
- guard #available(iOS 16.2, *) else { throw UnsupportedOSException("16.2") }
342
+ guard #available(iOS 16.2, *) else {
343
+ if silentOnUnsupportedOS {
344
+ return
345
+ }
346
+ throw UnsupportedOSException("16.2")
347
+ }
282
348
 
283
349
  guard
284
350
  let activity = Activity<LiveActivityAttributes>.activities.first(where: {
@@ -292,7 +358,13 @@ public class ExpoLiveActivityModule: Module {
292
358
  title: state.title,
293
359
  subtitle: state.subtitle,
294
360
  timerEndDateInMilliseconds: state.progressBar?.date,
295
- progress: state.progressBar?.progress
361
+ progress: state.progressBar?.progress,
362
+ imageName: state.imageName,
363
+ dynamicIslandImageName: state.dynamicIslandImageName,
364
+ smallImageName: state.smallImageName,
365
+ elapsedTimerStartDateInMilliseconds: state.progressBar?.elapsedTimer?.startDate,
366
+ currentStep: state.progressBar?.currentStep,
367
+ totalSteps: state.progressBar?.totalSteps
296
368
  )
297
369
  try await updateImages(state: state, newState: &newState)
298
370
  await activity.end(
@@ -304,6 +376,9 @@ public class ExpoLiveActivityModule: Module {
304
376
 
305
377
  Function("updateActivity") { (activityId: String, state: LiveActivityState) in
306
378
  guard #available(iOS 16.2, *) else {
379
+ if silentOnUnsupportedOS {
380
+ return
381
+ }
307
382
  throw UnsupportedOSException("16.2")
308
383
  }
309
384
 
@@ -319,7 +394,13 @@ public class ExpoLiveActivityModule: Module {
319
394
  title: state.title,
320
395
  subtitle: state.subtitle,
321
396
  timerEndDateInMilliseconds: state.progressBar?.date,
322
- progress: state.progressBar?.progress
397
+ progress: state.progressBar?.progress,
398
+ imageName: state.imageName,
399
+ dynamicIslandImageName: state.dynamicIslandImageName,
400
+ smallImageName: state.smallImageName,
401
+ elapsedTimerStartDateInMilliseconds: state.progressBar?.elapsedTimer?.startDate,
402
+ currentStep: state.progressBar?.currentStep,
403
+ totalSteps: state.progressBar?.totalSteps
323
404
  )
324
405
  try await updateImages(state: state, newState: &newState)
325
406
  await activity.update(ActivityContent(state: newState, staleDate: nil))
@@ -9,6 +9,10 @@ struct LiveActivityAttributes: ActivityAttributes {
9
9
  var progress: Double?
10
10
  var imageName: String?
11
11
  var dynamicIslandImageName: String?
12
+ var smallImageName: String?
13
+ var elapsedTimerStartDateInMilliseconds: Double?
14
+ var currentStep: Int?
15
+ var totalSteps: Int?
12
16
  }
13
17
 
14
18
  var name: String
@@ -26,8 +30,14 @@ struct LiveActivityAttributes: ActivityAttributes {
26
30
  var imageHeight: Int?
27
31
  var imageWidthPercent: Double?
28
32
  var imageHeightPercent: Double?
33
+ var smallImageWidth: Int?
34
+ var smallImageHeight: Int?
35
+ var smallImageWidthPercent: Double?
36
+ var smallImageHeightPercent: Double?
29
37
  var imageAlign: String?
30
38
  var contentFit: String?
39
+ var progressSegmentActiveColor: String?
40
+ var progressSegmentInactiveColor: String?
31
41
 
32
42
  enum DynamicIslandTimerType: String, Codable {
33
43
  case circular
@@ -0,0 +1,58 @@
1
+ import SwiftUI
2
+ import WidgetKit
3
+
4
+ extension LiveActivityAttributes.ContentState {
5
+ var hasSegmentedProgress: Bool {
6
+ currentStep != nil && (totalSteps ?? 0) > 0
7
+ }
8
+
9
+ func logSegmentedProgressWarningIfNeeded() {
10
+ #if DEBUG
11
+ if hasSegmentedProgress,
12
+ elapsedTimerStartDateInMilliseconds != nil
13
+ || timerEndDateInMilliseconds != nil
14
+ || progress != nil
15
+ {
16
+ DebugLog("⚠️[ExpoLiveActivity] Both segmented and regular progress provided; showing segmented")
17
+ }
18
+ #endif
19
+ }
20
+ }
21
+
22
+ extension LiveActivityAttributes {
23
+ var segmentActiveColor: Color {
24
+ progressSegmentActiveColor.map { Color(hex: $0) } ?? Color.blue
25
+ }
26
+
27
+ var segmentInactiveColor: Color {
28
+ progressSegmentInactiveColor.map { Color(hex: $0) } ?? Color.gray.opacity(0.3)
29
+ }
30
+
31
+ func resolvedPadding(defaultPadding: Int) -> EdgeInsets {
32
+ let top = CGFloat(
33
+ paddingDetails?.top
34
+ ?? paddingDetails?.vertical
35
+ ?? padding
36
+ ?? defaultPadding
37
+ )
38
+ let bottom = CGFloat(
39
+ paddingDetails?.bottom
40
+ ?? paddingDetails?.vertical
41
+ ?? padding
42
+ ?? defaultPadding
43
+ )
44
+ let leading = CGFloat(
45
+ paddingDetails?.left
46
+ ?? paddingDetails?.horizontal
47
+ ?? padding
48
+ ?? defaultPadding
49
+ )
50
+ let trailing = CGFloat(
51
+ paddingDetails?.right
52
+ ?? paddingDetails?.horizontal
53
+ ?? padding
54
+ ?? defaultPadding
55
+ )
56
+ return EdgeInsets(top: top, leading: leading, bottom: bottom, trailing: trailing)
57
+ }
58
+ }