expo-live-activity 0.4.1 → 0.4.3-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/Package.swift ADDED
@@ -0,0 +1,16 @@
1
+ // swift-tools-version: 5.7
2
+
3
+ import PackageDescription
4
+
5
+ let package = Package(
6
+ name: "ExpoLiveActivity",
7
+ platforms: [.iOS(.v16)],
8
+ products: [.library(name: "ExpoLiveActivity", targets: ["ExpoLiveActivity"])],
9
+ targets: [
10
+ .target(
11
+ name: "ExpoLiveActivity",
12
+ path: "ios-files",
13
+ exclude: ["LiveActivityWidgetBundle.swift"]
14
+ ),
15
+ ]
16
+ )
package/README.md CHANGED
@@ -44,7 +44,7 @@ The module comes with a built-in config plugin that creates a target in iOS with
44
44
  }
45
45
  }
46
46
  ```
47
- If you want to update Live Acitivity with push notifications you can add option `"enablePushNotifications": true`:
47
+ If you want to update Live Activity with push notifications you can add option `"enablePushNotifications": true`:
48
48
  ```json
49
49
  {
50
50
  "expo": {
@@ -62,11 +62,14 @@ The module comes with a built-in config plugin that creates a target in iOS with
62
62
  2. **Assets configuration:**
63
63
  Place images intended for Live Activities in the `assets/liveActivity` folder. The plugin manages these assets automatically.
64
64
 
65
- Then prebuild your app with:
65
+ Then prebuild your app with:
66
66
 
67
- ```sh
68
- npx expo prebuild --clean
69
- ```
67
+ ```sh
68
+ npx expo prebuild --clean
69
+ ```
70
+
71
+ > [!NOTE]
72
+ > Because of iOS limitations, the assets can't be bigger than 4KB ([native Live Activity documentation](https://developer.apple.com/documentation/activitykit/displaying-live-data-with-live-activities#Understand-constraints))
70
73
 
71
74
  ### Step 3: Usage in Your React Native App
72
75
 
@@ -153,7 +156,9 @@ The `config` object should include:
153
156
  timerType?: DynamicIslandTimerType; // "circular" | "digital" - defines timer appearance on the dynamic island
154
157
  padding?: Padding // number | {top?: number bottom?: number ...}
155
158
  imagePosition?: ImagePosition; // 'left' | 'right';
156
- imageSize?: ImageSize // 'default' | number (points maxHeight)
159
+ imageAlign?: ImageAlign; // 'top' | 'center' | 'bottom'
160
+ imageSize?: ImageSize // { width: number|`${number}%`, height: number|`${number}%` } | undefined (defaults to 64pt)
161
+ contentFit?: ImageContentFit; // 'cover' | 'contain' | 'fill' | 'none' | 'scale-down'
157
162
  };
158
163
  ```
159
164
 
@@ -186,7 +191,9 @@ const config: LiveActivity.LiveActivityConfig = {
186
191
  timerType: 'circular',
187
192
  padding: { horizontal: 20, top: 16, bottom: 16 },
188
193
  imagePosition: 'right',
189
- imageSize: 'default', // or a number e.g. 80 for custom image height
194
+ imageAlign: 'center',
195
+ imageSize: { height: '50%', width: '50%' }, // number (pt) or percentage of the image container, if empty by default is 64pt.
196
+ contentFit: 'cover',
190
197
  }
191
198
 
192
199
  const activityId = LiveActivity.startActivity(state, config)
package/build/index.d.ts CHANGED
@@ -33,7 +33,12 @@ export type Padding = {
33
33
  } | number;
34
34
  export type ImagePosition = 'left' | 'right' | 'leftStretch' | 'rightStretch';
35
35
  export type ImageAlign = 'top' | 'center' | 'bottom';
36
- export type ImageSize = number;
36
+ export type ImageDimension = number | `${number}%`;
37
+ export type ImageSize = {
38
+ width: ImageDimension;
39
+ height: ImageDimension;
40
+ };
41
+ export type ImageContentFit = 'cover' | 'contain' | 'fill' | 'none' | 'scale-down';
37
42
  export type LiveActivityConfig = {
38
43
  backgroundColor?: string;
39
44
  titleColor?: string;
@@ -46,6 +51,7 @@ export type LiveActivityConfig = {
46
51
  imagePosition?: ImagePosition;
47
52
  imageAlign?: ImageAlign;
48
53
  imageSize?: ImageSize;
54
+ contentFit?: ImageContentFit;
49
55
  };
50
56
  export type ActivityTokenReceivedEvent = {
51
57
  activityID: string;
@@ -53,7 +59,7 @@ export type ActivityTokenReceivedEvent = {
53
59
  activityPushToken: string;
54
60
  };
55
61
  export type ActivityPushToStartTokenReceivedEvent = {
56
- activityPushToStartToken: string;
62
+ activityPushToStartToken: string | null;
57
63
  };
58
64
  type ActivityState = 'active' | 'dismissed' | 'pending' | 'stale' | 'ended';
59
65
  export type ActivityUpdateEvent = {
@@ -82,8 +88,19 @@ export declare function stopActivity(id: string, state: LiveActivityState): any;
82
88
  * @param {LiveActivityState} state The updated state for the live activity.
83
89
  */
84
90
  export declare function updateActivity(id: string, state: LiveActivityState): any;
85
- export declare function addActivityTokenListener(listener: (event: ActivityTokenReceivedEvent) => void): Voidable<EventSubscription>;
91
+ /**
92
+ * @param {function} updateTokenListener The listener function that will be called when an update token is received.
93
+ */
94
+ export declare function addActivityTokenListener(updateTokenListener: (event: ActivityTokenReceivedEvent) => void): Voidable<EventSubscription>;
95
+ /**
96
+ * Adds a listener that is called when a push-to-start token is received. Supported only on iOS > 17.2.
97
+ * On earlier iOS versions, the listener will return null as a token.
98
+ * @param {function} listener The listener function that will be called when the observer starts and then when a push-to-start token is received.
99
+ */
86
100
  export declare function addActivityPushToStartTokenListener(listener: (event: ActivityPushToStartTokenReceivedEvent) => void): Voidable<EventSubscription>;
87
- export declare function addActivityUpdatesListener(listener: (event: ActivityUpdateEvent) => void): Voidable<EventSubscription>;
101
+ /**
102
+ * @param {function} statusListener The listener function that will be called when an activity status changes.
103
+ */
104
+ export declare function addActivityUpdatesListener(statusListener: (event: ActivityUpdateEvent) => void): Voidable<EventSubscription>;
88
105
  export {};
89
106
  //# sourceMappingURL=index.d.ts.map
@@ -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":"AAAA,OAAO,EAAE,iBAAiB,EAAE,MAAM,mBAAmB,CAAA;AAKrD,KAAK,QAAQ,CAAC,CAAC,IAAI,CAAC,GAAG,IAAI,CAAA;AAE3B,MAAM,MAAM,sBAAsB,GAAG,UAAU,GAAG,SAAS,CAAA;AAE3D,KAAK,eAAe,GAChB;IACE,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,QAAQ,CAAC,EAAE,SAAS,CAAA;CACrB,GACD;IACE,IAAI,CAAC,EAAE,SAAS,CAAA;IAChB,QAAQ,CAAC,EAAE,MAAM,CAAA;CAClB,CAAA;AAEL,MAAM,MAAM,iBAAiB,GAAG;IAC9B,KAAK,EAAE,MAAM,CAAA;IACb,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,WAAW,CAAC,EAAE,eAAe,CAAA;IAC7B,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,sBAAsB,CAAC,EAAE,MAAM,CAAA;CAChC,CAAA;AAED,MAAM,MAAM,uBAAuB,GAAG;IACpC,KAAK,EAAE,MAAM,CAAA;IACb,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,sBAAsB,CAAC,EAAE,MAAM,CAAA;CAChC,CAAA;AAED,MAAM,MAAM,OAAO,GACf;IACE,GAAG,CAAC,EAAE,MAAM,CAAA;IACZ,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,UAAU,CAAC,EAAE,MAAM,CAAA;CACpB,GACD,MAAM,CAAA;AAEV,MAAM,MAAM,aAAa,GAAG,MAAM,GAAG,OAAO,GAAG,aAAa,GAAG,cAAc,CAAA;AAE7E,MAAM,MAAM,UAAU,GAAG,KAAK,GAAG,QAAQ,GAAG,QAAQ,CAAA;AAEpD,MAAM,MAAM,cAAc,GAAG,MAAM,GAAG,GAAG,MAAM,GAAG,CAAA;AAClD,MAAM,MAAM,SAAS,GAAG;IACtB,KAAK,EAAE,cAAc,CAAA;IACrB,MAAM,EAAE,cAAc,CAAA;CACvB,CAAA;AAED,MAAM,MAAM,eAAe,GAAG,OAAO,GAAG,SAAS,GAAG,MAAM,GAAG,MAAM,GAAG,YAAY,CAAA;AAElF,MAAM,MAAM,kBAAkB,GAAG;IAC/B,eAAe,CAAC,EAAE,MAAM,CAAA;IACxB,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,aAAa,CAAC,EAAE,MAAM,CAAA;IACtB,gBAAgB,CAAC,EAAE,MAAM,CAAA;IACzB,sBAAsB,CAAC,EAAE,MAAM,CAAA;IAC/B,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,SAAS,CAAC,EAAE,sBAAsB,CAAA;IAClC,OAAO,CAAC,EAAE,OAAO,CAAA;IACjB,aAAa,CAAC,EAAE,aAAa,CAAA;IAC7B,UAAU,CAAC,EAAE,UAAU,CAAA;IACvB,SAAS,CAAC,EAAE,SAAS,CAAA;IACrB,UAAU,CAAC,EAAE,eAAe,CAAA;CAC7B,CAAA;AAED,MAAM,MAAM,0BAA0B,GAAG;IACvC,UAAU,EAAE,MAAM,CAAA;IAClB,YAAY,EAAE,MAAM,CAAA;IACpB,iBAAiB,EAAE,MAAM,CAAA;CAC1B,CAAA;AAED,MAAM,MAAM,qCAAqC,GAAG;IAClD,wBAAwB,EAAE,MAAM,GAAG,IAAI,CAAA;CACxC,CAAA;AAED,KAAK,aAAa,GAAG,QAAQ,GAAG,WAAW,GAAG,SAAS,GAAG,OAAO,GAAG,OAAO,CAAA;AAE3E,MAAM,MAAM,mBAAmB,GAAG;IAChC,UAAU,EAAE,MAAM,CAAA;IAClB,YAAY,EAAE,MAAM,CAAA;IACpB,aAAa,EAAE,aAAa,CAAA;CAC7B,CAAA;AAED,MAAM,MAAM,wBAAwB,GAAG;IACrC,eAAe,EAAE,CAAC,MAAM,EAAE,0BAA0B,KAAK,IAAI,CAAA;IAC7D,0BAA0B,EAAE,CAAC,MAAM,EAAE,qCAAqC,KAAK,IAAI,CAAA;IACnF,aAAa,EAAE,CAAC,MAAM,EAAE,mBAAmB,KAAK,IAAI,CAAA;CACrD,CAAA;AA8DD;;;;GAIG;AACH,wBAAgB,aAAa,CAAC,KAAK,EAAE,iBAAiB,EAAE,MAAM,CAAC,EAAE,kBAAkB,GAAG,QAAQ,CAAC,MAAM,CAAC,CAErG;AAED;;;GAGG;AACH,wBAAgB,YAAY,CAAC,EAAE,EAAE,MAAM,EAAE,KAAK,EAAE,iBAAiB,OAEhE;AAED;;;GAGG;AACH,wBAAgB,cAAc,CAAC,EAAE,EAAE,MAAM,EAAE,KAAK,EAAE,iBAAiB,OAElE;AAED;;GAEG;AACH,wBAAgB,wBAAwB,CACtC,mBAAmB,EAAE,CAAC,KAAK,EAAE,0BAA0B,KAAK,IAAI,GAC/D,QAAQ,CAAC,iBAAiB,CAAC,CAG7B;AAED;;;;GAIG;AACH,wBAAgB,mCAAmC,CACjD,QAAQ,EAAE,CAAC,KAAK,EAAE,qCAAqC,KAAK,IAAI,GAC/D,QAAQ,CAAC,iBAAiB,CAAC,CAG7B;AAED;;GAEG;AACH,wBAAgB,0BAA0B,CACxC,cAAc,EAAE,CAAC,KAAK,EAAE,mBAAmB,KAAK,IAAI,GACnD,QAAQ,CAAC,iBAAiB,CAAC,CAG7B"}
package/build/index.js CHANGED
@@ -9,7 +9,7 @@ function assertIOS(name) {
9
9
  function normalizeConfig(config) {
10
10
  if (config === undefined)
11
11
  return config;
12
- const { padding, ...base } = config;
12
+ const { padding, imageSize, ...base } = config;
13
13
  const normalized = { ...base };
14
14
  // Normalize padding: keep number in padding, object in paddingDetails
15
15
  if (typeof padding === 'number') {
@@ -18,6 +18,35 @@ function normalizeConfig(config) {
18
18
  else if (typeof padding === 'object') {
19
19
  normalized.paddingDetails = padding;
20
20
  }
21
+ // Normalize imageSize: object with width/height each a number (points) or percent string like '50%'
22
+ if (imageSize) {
23
+ 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
+ }
49
+ }
21
50
  return normalized;
22
51
  }
23
52
  /**
@@ -45,16 +74,27 @@ export function updateActivity(id, state) {
45
74
  if (assertIOS('updateActivity'))
46
75
  return ExpoLiveActivityModule.updateActivity(id, state);
47
76
  }
48
- export function addActivityTokenListener(listener) {
77
+ /**
78
+ * @param {function} updateTokenListener The listener function that will be called when an update token is received.
79
+ */
80
+ export function addActivityTokenListener(updateTokenListener) {
49
81
  if (assertIOS('addActivityTokenListener'))
50
- return ExpoLiveActivityModule.addListener('onTokenReceived', listener);
82
+ return ExpoLiveActivityModule.addListener('onTokenReceived', updateTokenListener);
51
83
  }
84
+ /**
85
+ * Adds a listener that is called when a push-to-start token is received. Supported only on iOS > 17.2.
86
+ * On earlier iOS versions, the listener will return null as a token.
87
+ * @param {function} listener The listener function that will be called when the observer starts and then when a push-to-start token is received.
88
+ */
52
89
  export function addActivityPushToStartTokenListener(listener) {
53
90
  if (assertIOS('addActivityPushToStartTokenListener'))
54
91
  return ExpoLiveActivityModule.addListener('onPushToStartTokenReceived', listener);
55
92
  }
56
- export function addActivityUpdatesListener(listener) {
93
+ /**
94
+ * @param {function} statusListener The listener function that will be called when an activity status changes.
95
+ */
96
+ export function addActivityUpdatesListener(statusListener) {
57
97
  if (assertIOS('addActivityUpdatesListener'))
58
- return ExpoLiveActivityModule.addListener('onStateChange', listener);
98
+ return ExpoLiveActivityModule.addListener('onStateChange', statusListener);
59
99
  }
60
100
  //# sourceMappingURL=index.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,QAAQ,EAAE,MAAM,cAAc,CAAA;AAEvC,OAAO,sBAAsB,MAAM,0BAA0B,CAAA;AAwF7D,SAAS,SAAS,CAAC,IAAY;IAC7B,MAAM,KAAK,GAAG,QAAQ,CAAC,EAAE,KAAK,KAAK,CAAA;IAEnC,IAAI,CAAC,KAAK;QAAE,OAAO,CAAC,KAAK,CAAC,GAAG,IAAI,2BAA2B,CAAC,CAAA;IAE7D,OAAO,KAAK,CAAA;AACd,CAAC;AAED,SAAS,eAAe,CAAC,MAA2B;IAClD,IAAI,MAAM,KAAK,SAAS;QAAE,OAAO,MAAM,CAAA;IAEvC,MAAM,EAAE,OAAO,EAAE,GAAG,IAAI,EAAE,GAAG,MAAM,CAAA;IAEnC,MAAM,UAAU,GAAqB,EAAE,GAAG,IAAI,EAAE,CAAA;IAEhD,sEAAsE;IACtE,IAAI,OAAO,OAAO,KAAK,QAAQ,EAAE,CAAC;QAChC,UAAU,CAAC,OAAO,GAAG,OAAO,CAAA;IAC9B,CAAC;SAAM,IAAI,OAAO,OAAO,KAAK,QAAQ,EAAE,CAAC;QACvC,UAAU,CAAC,cAAc,GAAG,OAAO,CAAA;IACrC,CAAC;IAED,OAAO,UAAU,CAAA;AACnB,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,aAAa,CAAC,KAAwB,EAAE,MAA2B;IACjF,IAAI,SAAS,CAAC,eAAe,CAAC;QAAE,OAAO,sBAAsB,CAAC,aAAa,CAAC,KAAK,EAAE,eAAe,CAAC,MAAM,CAAC,CAAC,CAAA;AAC7G,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,YAAY,CAAC,EAAU,EAAE,KAAwB;IAC/D,IAAI,SAAS,CAAC,cAAc,CAAC;QAAE,OAAO,sBAAsB,CAAC,YAAY,CAAC,EAAE,EAAE,KAAK,CAAC,CAAA;AACtF,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,cAAc,CAAC,EAAU,EAAE,KAAwB;IACjE,IAAI,SAAS,CAAC,gBAAgB,CAAC;QAAE,OAAO,sBAAsB,CAAC,cAAc,CAAC,EAAE,EAAE,KAAK,CAAC,CAAA;AAC1F,CAAC;AAED,MAAM,UAAU,wBAAwB,CACtC,QAAqD;IAErD,IAAI,SAAS,CAAC,0BAA0B,CAAC;QAAE,OAAO,sBAAsB,CAAC,WAAW,CAAC,iBAAiB,EAAE,QAAQ,CAAC,CAAA;AACnH,CAAC;AAED,MAAM,UAAU,mCAAmC,CACjD,QAAgE;IAEhE,IAAI,SAAS,CAAC,qCAAqC,CAAC;QAClD,OAAO,sBAAsB,CAAC,WAAW,CAAC,4BAA4B,EAAE,QAAQ,CAAC,CAAA;AACrF,CAAC;AAED,MAAM,UAAU,0BAA0B,CACxC,QAA8C;IAE9C,IAAI,SAAS,CAAC,4BAA4B,CAAC;QAAE,OAAO,sBAAsB,CAAC,WAAW,CAAC,eAAe,EAAE,QAAQ,CAAC,CAAA;AACnH,CAAC","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 ImageSize = number\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}\n\nexport type ActivityTokenReceivedEvent = {\n activityID: string\n activityName: string\n activityPushToken: string\n}\n\nexport type ActivityPushToStartTokenReceivedEvent = {\n activityPushToStartToken: string\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, ...base } = config\n type NormalizedConfig = LiveActivityConfig & { paddingDetails?: Padding }\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 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\nexport function addActivityTokenListener(\n listener: (event: ActivityTokenReceivedEvent) => void\n): Voidable<EventSubscription> {\n if (assertIOS('addActivityTokenListener')) return ExpoLiveActivityModule.addListener('onTokenReceived', listener)\n}\n\nexport function addActivityPushToStartTokenListener(\n listener: (event: ActivityPushToStartTokenReceivedEvent) => void\n): Voidable<EventSubscription> {\n if (assertIOS('addActivityPushToStartTokenListener'))\n return ExpoLiveActivityModule.addListener('onPushToStartTokenReceived', listener)\n}\n\nexport function addActivityUpdatesListener(\n listener: (event: ActivityUpdateEvent) => void\n): Voidable<EventSubscription> {\n if (assertIOS('addActivityUpdatesListener')) return ExpoLiveActivityModule.addListener('onStateChange', listener)\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\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"]}
@@ -59,11 +59,23 @@ public class ExpoLiveActivityModule: Module {
59
59
  var imagePosition: String?
60
60
 
61
61
  @Field
62
- var imageSize: Int?
62
+ var imageWidth: Int?
63
+
64
+ @Field
65
+ var imageHeight: Int?
66
+
67
+ @Field
68
+ var imageWidthPercent: Double?
69
+
70
+ @Field
71
+ var imageHeightPercent: Double?
63
72
 
64
73
  @Field
65
74
  var imageAlign: String?
66
75
 
76
+ @Field
77
+ var contentFit: String?
78
+
67
79
  struct PaddingDetails: Record {
68
80
  @Field var top: Int?
69
81
  @Field var bottom: Int?
@@ -129,6 +141,10 @@ public class ExpoLiveActivityModule: Module {
129
141
  private func observePushToStartToken() {
130
142
  guard #available(iOS 17.2, *), ActivityAuthorizationInfo().areActivitiesEnabled else { return }
131
143
 
144
+ if let initialToken = (Activity<LiveActivityAttributes>.pushToStartToken?.reduce("") { $0 + String(format: "%02x", $1) }) {
145
+ sendPushToStartToken(activityPushToStartToken: initialToken)
146
+ }
147
+
132
148
  print("Observing push to start token updates...")
133
149
  Task {
134
150
  for await data in Activity<LiveActivityAttributes>.pushToStartTokenUpdates {
@@ -184,11 +200,14 @@ public class ExpoLiveActivityModule: Module {
184
200
  public func definition() -> ModuleDefinition {
185
201
  Name("ExpoLiveActivity")
186
202
 
187
- OnCreate {
203
+ OnStartObserving("onTokenReceived") {
204
+ observeLiveActivityUpdates()
205
+ }
206
+
207
+ OnStartObserving("onPushToStartTokenReceived") {
188
208
  if pushNotificationsEnabled {
189
209
  observePushToStartToken()
190
210
  }
191
- observeLiveActivityUpdates()
192
211
  }
193
212
 
194
213
  Events("onTokenReceived", "onPushToStartTokenReceived", "onStateChange")
@@ -225,8 +244,12 @@ public class ExpoLiveActivityModule: Module {
225
244
  )
226
245
  },
227
246
  imagePosition: config.imagePosition,
228
- imageSize: config.imageSize,
229
- imageAlign: config.imageAlign
247
+ imageWidth: config.imageWidth,
248
+ imageHeight: config.imageHeight,
249
+ imageWidthPercent: config.imageWidthPercent,
250
+ imageHeightPercent: config.imageHeightPercent,
251
+ imageAlign: config.imageAlign,
252
+ contentFit: config.contentFit
230
253
  )
231
254
 
232
255
  let initialState = LiveActivityAttributes.ContentState(
@@ -22,8 +22,12 @@ struct LiveActivityAttributes: ActivityAttributes {
22
22
  var padding: Int?
23
23
  var paddingDetails: PaddingDetails?
24
24
  var imagePosition: String?
25
- var imageSize: Int?
25
+ var imageWidth: Int?
26
+ var imageHeight: Int?
27
+ var imageWidthPercent: Double?
28
+ var imageHeightPercent: Double?
26
29
  var imageAlign: String?
30
+ var contentFit: String?
27
31
 
28
32
  enum DynamicIslandTimerType: String, Codable {
29
33
  case circular
@@ -1,4 +1,5 @@
1
1
  import SwiftUI
2
+ import UIKit
2
3
 
3
4
  extension Image {
4
5
  static func dynamic(assetNameOrPath: String) -> Self {
@@ -15,3 +16,18 @@ extension Image {
15
16
  return Image(assetNameOrPath)
16
17
  }
17
18
  }
19
+
20
+ extension UIImage {
21
+ /// Attempts to load a UIImage either from the shared app group container or the main bundle.
22
+ static func dynamic(assetNameOrPath: String) -> UIImage? {
23
+ if let container = FileManager.default.containerURL(
24
+ forSecurityApplicationGroupIdentifier: "group.expoLiveActivity.sharedData"
25
+ ) {
26
+ let contentsOfFile = container.appendingPathComponent(assetNameOrPath).path
27
+ if let uiImage = UIImage(contentsOfFile: contentsOfFile) {
28
+ return uiImage
29
+ }
30
+ }
31
+ return UIImage(named: assetNameOrPath)
32
+ }
33
+ }
@@ -15,15 +15,35 @@ import WidgetKit
15
15
  }
16
16
  }
17
17
 
18
+ struct DebugLog: View {
19
+ #if DEBUG
20
+ private let message: String
21
+ init(_ message: String) {
22
+ self.message = message
23
+ print(message)
24
+ }
25
+
26
+ var body: some View {
27
+ Text(message)
28
+ .font(.caption2)
29
+ .foregroundStyle(.red)
30
+ }
31
+ #else
32
+ init(_: String) {}
33
+ var body: some View { EmptyView() }
34
+ #endif
35
+ }
36
+
18
37
  struct LiveActivityView: View {
19
38
  let contentState: LiveActivityAttributes.ContentState
20
39
  let attributes: LiveActivityAttributes
40
+ @State private var imageContainerSize: CGSize?
21
41
 
22
42
  var progressViewTint: Color? {
23
43
  attributes.progressViewTint.map { Color(hex: $0) }
24
44
  }
25
45
 
26
- private var imageAlignment: Alignment {
46
+ private var imageVerticalAlignment: VerticalAlignment {
27
47
  switch attributes.imageAlign {
28
48
  case "center":
29
49
  return .center
@@ -34,13 +54,104 @@ import WidgetKit
34
54
  }
35
55
  }
36
56
 
37
- @ViewBuilder
38
- private func alignedImage(imageName: String) -> some View {
39
- VStack {
40
- resizableImage(imageName: imageName)
41
- .applyImageSize(attributes.imageSize)
57
+ private func alignedImage(imageName: String, horizontalAlignment: HorizontalAlignment) -> some View {
58
+ let defaultHeight: CGFloat = 64
59
+ let defaultWidth: CGFloat = 64
60
+ let containerHeight = imageContainerSize?.height
61
+ let containerWidth = imageContainerSize?.width
62
+ let hasWidthConstraint = (attributes.imageWidthPercent != nil) || (attributes.imageWidth != nil)
63
+
64
+ let computedHeight: CGFloat? = {
65
+ if let percent = attributes.imageHeightPercent {
66
+ let clamped = min(max(percent, 0), 100) / 100.0
67
+ // Use the row height as a base. Fallback to default when row height is not measured yet.
68
+ let base = (containerHeight ?? defaultHeight)
69
+ return base * clamped
70
+ } else if let size = attributes.imageHeight {
71
+ return CGFloat(size)
72
+ } else if hasWidthConstraint {
73
+ // Mimic CSS: when only width is set, keep height automatic to preserve aspect ratio
74
+ return nil
75
+ } else {
76
+ // Mimic CSS: this works against CSS but provides a better default behavior.
77
+ // When no width/height is set, use a default size (64pt)
78
+ // Width will adjust automatically base on aspect ratio
79
+ return defaultHeight
80
+ }
81
+ }()
82
+
83
+ let computedWidth: CGFloat? = {
84
+ if let percent = attributes.imageWidthPercent {
85
+ let clamped = min(max(percent, 0), 100) / 100.0
86
+ let base = (containerWidth ?? defaultWidth)
87
+ return base * clamped
88
+ } else if let size = attributes.imageWidth {
89
+ return CGFloat(size)
90
+ } else {
91
+ return nil // Keep aspect fit based on height
92
+ }
93
+ }()
94
+
95
+ return ZStack(alignment: .center) {
96
+ Group {
97
+ let fit = attributes.contentFit ?? "contain"
98
+ switch fit {
99
+ case "contain":
100
+ Image.dynamic(assetNameOrPath: imageName).resizable().scaledToFit().frame(width: computedWidth, height: computedHeight)
101
+ case "fill":
102
+ Image.dynamic(assetNameOrPath: imageName).resizable().frame(
103
+ width: computedWidth,
104
+ height: computedHeight
105
+ )
106
+ case "none":
107
+ Image.dynamic(assetNameOrPath: imageName).renderingMode(.original).frame(width: computedWidth, height: computedHeight)
108
+ case "scale-down":
109
+ if let uiImage = UIImage.dynamic(assetNameOrPath: imageName) {
110
+ // Determine the target box. When width/height are nil, we use image's intrinsic dimension for comparison.
111
+ let targetHeight = computedHeight ?? uiImage.size.height
112
+ let targetWidth = computedWidth ?? uiImage.size.width
113
+ let shouldScaleDown = uiImage.size.height > targetHeight || uiImage.size.width > targetWidth
114
+
115
+ if shouldScaleDown {
116
+ Image(uiImage: uiImage)
117
+ .resizable()
118
+ .scaledToFit()
119
+ .frame(width: computedWidth, height: computedHeight)
120
+ } else {
121
+ Image(uiImage: uiImage)
122
+ .renderingMode(.original)
123
+ .frame(width: min(uiImage.size.width, targetWidth), height: min(uiImage.size.height, targetHeight))
124
+ }
125
+ } else {
126
+ DebugLog("⚠️[ExpoLiveActivity] assetNameOrPath couldn't resolve to UIImage")
127
+ }
128
+ case "cover":
129
+ Image.dynamic(assetNameOrPath: imageName).resizable().scaledToFill().frame(
130
+ width: computedWidth,
131
+ height: computedHeight
132
+ ).clipped()
133
+ default:
134
+ DebugLog("⚠️[ExpoLiveActivity] Unknown contentFit '\(fit)'")
135
+ }
136
+ }
42
137
  }
43
- .frame(maxHeight: .infinity, alignment: imageAlignment)
138
+ .frame(
139
+ maxWidth: .infinity,
140
+ maxHeight: .infinity,
141
+ alignment: Alignment(horizontal: horizontalAlignment, vertical: imageVerticalAlignment)
142
+ )
143
+ .background(
144
+ GeometryReader { proxy in
145
+ Color.clear
146
+ .onAppear {
147
+ let s = proxy.size
148
+ if s.width > 0, s.height > 0 { imageContainerSize = s }
149
+ }
150
+ .onChange(of: proxy.size) { s in
151
+ if s.width > 0, s.height > 0 { imageContainerSize = s }
152
+ }
153
+ }
154
+ )
44
155
  }
45
156
 
46
157
  var body: some View {
@@ -80,10 +191,11 @@ import WidgetKit
80
191
  let isLeftImage = position.hasPrefix("left")
81
192
  let hasImage = contentState.imageName != nil
82
193
  let effectiveStretch = isStretch && hasImage
194
+
83
195
  HStack(alignment: .center) {
84
196
  if hasImage, isLeftImage {
85
197
  if let imageName = contentState.imageName {
86
- alignedImage(imageName: imageName)
198
+ alignedImage(imageName: imageName, horizontalAlignment: .leading)
87
199
  }
88
200
  }
89
201
 
@@ -110,18 +222,16 @@ import WidgetKit
110
222
  .modifier(ConditionalForegroundViewModifier(color: attributes.progressViewLabelColor))
111
223
  }
112
224
  }
113
- }
225
+ }.layoutPriority(1)
114
226
 
115
227
  if hasImage, !isLeftImage { // right side (default)
116
- Spacer()
117
228
  if let imageName = contentState.imageName {
118
- alignedImage(imageName: imageName)
229
+ alignedImage(imageName: imageName, horizontalAlignment: .trailing)
119
230
  }
120
231
  }
121
232
  }
122
233
 
123
234
  if !effectiveStretch {
124
- // Bottom progress (hidden when using Stretch variants where progress is inline)
125
235
  if let date = contentState.timerEndDateInMilliseconds {
126
236
  ProgressView(timerInterval: Date.toTimerInterval(miliseconds: date))
127
237
  .tint(progressViewTint)
@@ -2,14 +2,30 @@ import ActivityKit
2
2
  import SwiftUI
3
3
  import WidgetKit
4
4
 
5
- struct LiveActivityAttributes: ActivityAttributes {
6
- struct ContentState: Codable, Hashable {
5
+ public struct LiveActivityAttributes: ActivityAttributes {
6
+ public struct ContentState: Codable, Hashable {
7
7
  var title: String
8
8
  var subtitle: String?
9
9
  var timerEndDateInMilliseconds: Double?
10
10
  var progress: Double?
11
11
  var imageName: String?
12
12
  var dynamicIslandImageName: String?
13
+
14
+ public init(
15
+ title: String,
16
+ subtitle: String? = nil,
17
+ timerEndDateInMilliseconds: Double? = nil,
18
+ progress: Double? = nil,
19
+ imageName: String? = nil,
20
+ dynamicIslandImageName: String? = nil
21
+ ) {
22
+ self.title = title
23
+ self.subtitle = subtitle
24
+ self.timerEndDateInMilliseconds = timerEndDateInMilliseconds
25
+ self.progress = progress
26
+ self.imageName = imageName
27
+ self.dynamicIslandImageName = dynamicIslandImageName
28
+ }
13
29
  }
14
30
 
15
31
  var name: String
@@ -23,26 +39,85 @@ struct LiveActivityAttributes: ActivityAttributes {
23
39
  var padding: Int?
24
40
  var paddingDetails: PaddingDetails?
25
41
  var imagePosition: String?
26
- var imageSize: Int?
42
+ var imageWidth: Int?
43
+ var imageHeight: Int?
44
+ var imageWidthPercent: Double?
45
+ var imageHeightPercent: Double?
27
46
  var imageAlign: String?
47
+ var contentFit: String?
48
+
49
+ public init(
50
+ name: String,
51
+ backgroundColor: String? = nil,
52
+ titleColor: String? = nil,
53
+ subtitleColor: String? = nil,
54
+ progressViewTint: String? = nil,
55
+ progressViewLabelColor: String? = nil,
56
+ deepLinkUrl: String? = nil,
57
+ timerType: DynamicIslandTimerType? = nil,
58
+ padding: Int? = nil,
59
+ paddingDetails: PaddingDetails? = nil,
60
+ imagePosition: String? = nil,
61
+ imageWidth: Int? = nil,
62
+ imageHeight: Int? = nil,
63
+ imageWidthPercent: Double? = nil,
64
+ imageHeightPercent: Double? = nil,
65
+ imageAlign: String? = nil,
66
+ contentFit: String? = nil
67
+ ) {
68
+ self.name = name
69
+ self.backgroundColor = backgroundColor
70
+ self.titleColor = titleColor
71
+ self.subtitleColor = subtitleColor
72
+ self.progressViewTint = progressViewTint
73
+ self.progressViewLabelColor = progressViewLabelColor
74
+ self.deepLinkUrl = deepLinkUrl
75
+ self.timerType = timerType
76
+ self.padding = padding
77
+ self.paddingDetails = paddingDetails
78
+ self.imagePosition = imagePosition
79
+ self.imageWidth = imageWidth
80
+ self.imageHeight = imageHeight
81
+ self.imageWidthPercent = imageWidthPercent
82
+ self.imageHeightPercent = imageHeightPercent
83
+ self.imageAlign = imageAlign
84
+ self.contentFit = contentFit
85
+ }
28
86
 
29
- enum DynamicIslandTimerType: String, Codable {
87
+ public enum DynamicIslandTimerType: String, Codable {
30
88
  case circular
31
89
  case digital
32
90
  }
33
91
 
34
- struct PaddingDetails: Codable, Hashable {
92
+ public struct PaddingDetails: Codable, Hashable {
35
93
  var top: Int?
36
94
  var bottom: Int?
37
95
  var left: Int?
38
96
  var right: Int?
39
97
  var vertical: Int?
40
98
  var horizontal: Int?
99
+
100
+ public init(
101
+ top: Int? = nil,
102
+ bottom: Int? = nil,
103
+ left: Int? = nil,
104
+ right: Int? = nil,
105
+ vertical: Int? = nil,
106
+ horizontal: Int? = nil
107
+ ) {
108
+ self.top = top
109
+ self.bottom = bottom
110
+ self.left = left
111
+ self.right = right
112
+ self.vertical = vertical
113
+ self.horizontal = horizontal
114
+ }
41
115
  }
42
116
  }
43
117
 
44
- struct LiveActivityWidget: Widget {
45
- var body: some WidgetConfiguration {
118
+ @available(iOS 16.1, *)
119
+ public struct LiveActivityWidget: Widget {
120
+ public var body: some WidgetConfiguration {
46
121
  ActivityConfiguration(for: LiveActivityAttributes.self) { context in
47
122
  LiveActivityView(contentState: context.state, attributes: context.attributes)
48
123
  .activityBackgroundTint(
@@ -72,6 +147,12 @@ struct LiveActivityWidget: Widget {
72
147
  )
73
148
  .padding(.horizontal, 5)
74
149
  .applyWidgetURL(from: context.attributes.deepLinkUrl)
150
+ } else if let progress = context.state.progress {
151
+ dynamicIslandExpandedBottomProgress(
152
+ progress: progress, progressViewTint: context.attributes.progressViewTint
153
+ )
154
+ .padding(.horizontal, 5)
155
+ .applyWidgetURL(from: context.attributes.deepLinkUrl)
75
156
  }
76
157
  }
77
158
  } compactLeading: {
@@ -87,6 +168,11 @@ struct LiveActivityWidget: Widget {
87
168
  timerType: context.attributes.timerType ?? .circular,
88
169
  progressViewTint: context.attributes.progressViewTint
89
170
  ).applyWidgetURL(from: context.attributes.deepLinkUrl)
171
+ } else if let progress = context.state.progress {
172
+ compactProgress(
173
+ progress: progress,
174
+ progressViewTint: context.attributes.progressViewTint
175
+ ).applyWidgetURL(from: context.attributes.deepLinkUrl)
90
176
  }
91
177
  } minimal: {
92
178
  if let date = context.state.timerEndDateInMilliseconds {
@@ -95,11 +181,18 @@ struct LiveActivityWidget: Widget {
95
181
  timerType: context.attributes.timerType ?? .circular,
96
182
  progressViewTint: context.attributes.progressViewTint
97
183
  ).applyWidgetURL(from: context.attributes.deepLinkUrl)
184
+ } else if let progress = context.state.progress {
185
+ compactProgress(
186
+ progress: progress,
187
+ progressViewTint: context.attributes.progressViewTint
188
+ ).applyWidgetURL(from: context.attributes.deepLinkUrl)
98
189
  }
99
190
  }
100
191
  }
101
192
  }
102
193
 
194
+ public init() {}
195
+
103
196
  @ViewBuilder
104
197
  private func compactTimer(
105
198
  endDate: Double,
@@ -140,7 +233,6 @@ struct LiveActivityWidget: Widget {
140
233
  VStack {
141
234
  Spacer()
142
235
  resizableImage(imageName: imageName)
143
- .frame(maxHeight: 64)
144
236
  Spacer()
145
237
  }
146
238
  }
@@ -163,4 +255,20 @@ struct LiveActivityWidget: Widget {
163
255
  )
164
256
  .progressViewStyle(.circular)
165
257
  }
258
+
259
+ private func compactProgress(
260
+ progress: Double,
261
+ progressViewTint: String?
262
+ ) -> some View {
263
+ ProgressView(value: progress)
264
+ .progressViewStyle(.circular)
265
+ .tint(progressViewTint.map { Color(hex: $0) })
266
+ }
267
+
268
+ private func dynamicIslandExpandedBottomProgress(progress: Double, progressViewTint: String?) -> some View {
269
+ ProgressView(value: progress)
270
+ .foregroundStyle(.white)
271
+ .tint(progressViewTint.map { Color(hex: $0) })
272
+ .padding(.top, 5)
273
+ }
166
274
  }
@@ -6,9 +6,28 @@ func resizableImage(imageName: String) -> some View {
6
6
  .scaledToFit()
7
7
  }
8
8
 
9
+ func resizableImage(imageName: String, height: CGFloat?, width: CGFloat?) -> some View {
10
+ resizableImage(imageName: imageName)
11
+ .frame(width: width, height: height)
12
+ }
13
+
14
+ private struct ContainerSizeKey: PreferenceKey {
15
+ static var defaultValue: CGSize?
16
+ static func reduce(value: inout CGSize?, nextValue: () -> CGSize?) {
17
+ value = nextValue() ?? value
18
+ }
19
+ }
20
+
9
21
  extension View {
10
- @ViewBuilder
11
- func applyImageSize(_ size: Int?) -> some View {
12
- frame(maxHeight: CGFloat(size ?? 64))
22
+ func captureContainerSize() -> some View {
23
+ background(
24
+ GeometryReader { proxy in
25
+ Color.clear.preference(key: ContainerSizeKey.self, value: proxy.size)
26
+ }
27
+ )
28
+ }
29
+
30
+ func onContainerSize(_ perform: @escaping (CGSize?) -> Void) -> some View {
31
+ onPreferenceChange(ContainerSizeKey.self, perform: perform)
13
32
  }
14
33
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "expo-live-activity",
3
- "version": "0.4.1",
3
+ "version": "0.4.3-alpha1",
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",
@@ -9,6 +9,7 @@
9
9
  "clean": "expo-module clean",
10
10
  "clean:plugin": "rm -rf plugin/build plugin/tsconfig.tsbuildinfo",
11
11
  "format:check": "prettier --check .",
12
+ "format:fix": "prettier --write . && swiftformat --exclude \"**/node_modules/\" .",
12
13
  "lint": "expo-module eslint",
13
14
  "lint:libOnly": "expo-module eslint --ignore-pattern 'example/*'",
14
15
  "test": "expo-module test",
@@ -16,7 +17,10 @@
16
17
  "prepublishOnly": "expo-module prepublishOnly",
17
18
  "expo-module": "expo-module",
18
19
  "open:ios": "xed example/ios",
19
- "typecheck": "tsc"
20
+ "typecheck": "tsc",
21
+ "generateTests": "node ./example/tests/scripts/generateFlows.js",
22
+ "runAllTests": "./example/tests/scripts/runAllTests.sh",
23
+ "generateTestReport": "node ./example/tests/scripts/generateReport.js"
20
24
  },
21
25
  "keywords": [
22
26
  "react-native",
@@ -47,7 +51,8 @@
47
51
  "expo": "~54.0.0",
48
52
  "expo-module-scripts": "^5.0.3",
49
53
  "prettier": "^3.6.2",
50
- "react-native": "0.81.4"
54
+ "react-native": "0.81.4",
55
+ "pdf-lib": "^1.17.1"
51
56
  },
52
57
  "peerDependencies": {
53
58
  "expo": "*",
@@ -102,7 +102,7 @@ function getWidgetFiles(targetPath) {
102
102
  const imagesXcassetsTarget = path.join(targetPath, 'Assets.xcassets');
103
103
  const files = fs.readdirSync(imageAssetsPath);
104
104
  files.forEach((file) => {
105
- if (path.extname(file).match(/\.(png|jpg|jpeg)$/)) {
105
+ if (path.extname(file).match(/\.(png|jpg|jpeg|svg)$/)) {
106
106
  const source = path.join(imageAssetsPath, file);
107
107
  const imageSetDir = path.join(imagesXcassetsTarget, `${path.basename(file, path.extname(file))}.imageset`);
108
108
  // Create the .imageset directory if it doesn't exist
@@ -3,8 +3,13 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  const config_plugins_1 = require("@expo/config-plugins");
4
4
  const withPlist = (expoConfig) => (0, config_plugins_1.withInfoPlist)(expoConfig, (plistConfig) => {
5
5
  const scheme = typeof expoConfig.scheme === 'string' ? expoConfig.scheme : expoConfig.ios?.bundleIdentifier;
6
- if (scheme)
7
- plistConfig.modResults.CFBundleURLTypes = [{ CFBundleURLSchemes: [scheme] }];
6
+ if (scheme) {
7
+ const existingURLTypes = plistConfig.modResults.CFBundleURLTypes || [];
8
+ const schemeExists = existingURLTypes.some((urlType) => urlType.CFBundleURLSchemes?.includes(scheme));
9
+ if (!schemeExists) {
10
+ plistConfig.modResults.CFBundleURLTypes = [...existingURLTypes, { CFBundleURLSchemes: [scheme] }];
11
+ }
12
+ }
8
13
  return plistConfig;
9
14
  });
10
15
  exports.default = withPlist;
@@ -78,7 +78,7 @@ export function getWidgetFiles(targetPath: string) {
78
78
  const files = fs.readdirSync(imageAssetsPath)
79
79
 
80
80
  files.forEach((file) => {
81
- if (path.extname(file).match(/\.(png|jpg|jpeg)$/)) {
81
+ if (path.extname(file).match(/\.(png|jpg|jpeg|svg)$/)) {
82
82
  const source = path.join(imageAssetsPath, file)
83
83
  const imageSetDir = path.join(imagesXcassetsTarget, `${path.basename(file, path.extname(file))}.imageset`)
84
84
 
@@ -3,7 +3,16 @@ import { ConfigPlugin, withInfoPlist } from '@expo/config-plugins'
3
3
  const withPlist: ConfigPlugin = (expoConfig) =>
4
4
  withInfoPlist(expoConfig, (plistConfig) => {
5
5
  const scheme = typeof expoConfig.scheme === 'string' ? expoConfig.scheme : expoConfig.ios?.bundleIdentifier
6
- if (scheme) plistConfig.modResults.CFBundleURLTypes = [{ CFBundleURLSchemes: [scheme] }]
6
+
7
+ if (scheme) {
8
+ const existingURLTypes = plistConfig.modResults.CFBundleURLTypes || []
9
+ const schemeExists = existingURLTypes.some((urlType: any) => urlType.CFBundleURLSchemes?.includes(scheme))
10
+
11
+ if (!schemeExists) {
12
+ plistConfig.modResults.CFBundleURLTypes = [...existingURLTypes, { CFBundleURLSchemes: [scheme] }]
13
+ }
14
+ }
15
+
7
16
  return plistConfig
8
17
  })
9
18
 
package/src/index.ts CHANGED
@@ -49,7 +49,13 @@ export type ImagePosition = 'left' | 'right' | 'leftStretch' | 'rightStretch'
49
49
 
50
50
  export type ImageAlign = 'top' | 'center' | 'bottom'
51
51
 
52
- export type ImageSize = number
52
+ export type ImageDimension = number | `${number}%`
53
+ export type ImageSize = {
54
+ width: ImageDimension
55
+ height: ImageDimension
56
+ }
57
+
58
+ export type ImageContentFit = 'cover' | 'contain' | 'fill' | 'none' | 'scale-down'
53
59
 
54
60
  export type LiveActivityConfig = {
55
61
  backgroundColor?: string
@@ -63,6 +69,7 @@ export type LiveActivityConfig = {
63
69
  imagePosition?: ImagePosition
64
70
  imageAlign?: ImageAlign
65
71
  imageSize?: ImageSize
72
+ contentFit?: ImageContentFit
66
73
  }
67
74
 
68
75
  export type ActivityTokenReceivedEvent = {
@@ -72,7 +79,7 @@ export type ActivityTokenReceivedEvent = {
72
79
  }
73
80
 
74
81
  export type ActivityPushToStartTokenReceivedEvent = {
75
- activityPushToStartToken: string
82
+ activityPushToStartToken: string | null
76
83
  }
77
84
 
78
85
  type ActivityState = 'active' | 'dismissed' | 'pending' | 'stale' | 'ended'
@@ -100,8 +107,14 @@ function assertIOS(name: string) {
100
107
  function normalizeConfig(config?: LiveActivityConfig) {
101
108
  if (config === undefined) return config
102
109
 
103
- const { padding, ...base } = config
104
- type NormalizedConfig = LiveActivityConfig & { paddingDetails?: Padding }
110
+ const { padding, imageSize, ...base } = config
111
+ type NormalizedConfig = LiveActivityConfig & {
112
+ paddingDetails?: Padding
113
+ imageWidth?: number
114
+ imageHeight?: number
115
+ imageWidthPercent?: number
116
+ imageHeightPercent?: number
117
+ }
105
118
  const normalized: NormalizedConfig = { ...base }
106
119
 
107
120
  // Normalize padding: keep number in padding, object in paddingDetails
@@ -111,6 +124,35 @@ function normalizeConfig(config?: LiveActivityConfig) {
111
124
  normalized.paddingDetails = padding
112
125
  }
113
126
 
127
+ // Normalize imageSize: object with width/height each a number (points) or percent string like '50%'
128
+ if (imageSize) {
129
+ const regExp = /^(100(?:\.0+)?|\d{1,2}(?:\.\d+)?)%$/ // Matches 0.0% to 100.0%
130
+
131
+ const { width, height } = imageSize
132
+
133
+ if (typeof width === 'number') {
134
+ normalized.imageWidth = width
135
+ } else if (typeof width === 'string') {
136
+ const match = width.trim().match(regExp)
137
+ if (match) {
138
+ normalized.imageWidthPercent = Number(match[1])
139
+ } else {
140
+ throw new Error('imageSize.width percent string must be in format "0%" to "100%"')
141
+ }
142
+ }
143
+
144
+ if (typeof height === 'number') {
145
+ normalized.imageHeight = height
146
+ } else if (typeof height === 'string') {
147
+ const match = height.trim().match(regExp)
148
+ if (match) {
149
+ normalized.imageHeightPercent = Number(match[1])
150
+ } else {
151
+ throw new Error('imageSize.height percent string must be in format "0%" to "100%"')
152
+ }
153
+ }
154
+ }
155
+
114
156
  return normalized
115
157
  }
116
158
 
@@ -139,12 +181,21 @@ export function updateActivity(id: string, state: LiveActivityState) {
139
181
  if (assertIOS('updateActivity')) return ExpoLiveActivityModule.updateActivity(id, state)
140
182
  }
141
183
 
184
+ /**
185
+ * @param {function} updateTokenListener The listener function that will be called when an update token is received.
186
+ */
142
187
  export function addActivityTokenListener(
143
- listener: (event: ActivityTokenReceivedEvent) => void
188
+ updateTokenListener: (event: ActivityTokenReceivedEvent) => void
144
189
  ): Voidable<EventSubscription> {
145
- if (assertIOS('addActivityTokenListener')) return ExpoLiveActivityModule.addListener('onTokenReceived', listener)
190
+ if (assertIOS('addActivityTokenListener'))
191
+ return ExpoLiveActivityModule.addListener('onTokenReceived', updateTokenListener)
146
192
  }
147
193
 
194
+ /**
195
+ * Adds a listener that is called when a push-to-start token is received. Supported only on iOS > 17.2.
196
+ * On earlier iOS versions, the listener will return null as a token.
197
+ * @param {function} listener The listener function that will be called when the observer starts and then when a push-to-start token is received.
198
+ */
148
199
  export function addActivityPushToStartTokenListener(
149
200
  listener: (event: ActivityPushToStartTokenReceivedEvent) => void
150
201
  ): Voidable<EventSubscription> {
@@ -152,8 +203,12 @@ export function addActivityPushToStartTokenListener(
152
203
  return ExpoLiveActivityModule.addListener('onPushToStartTokenReceived', listener)
153
204
  }
154
205
 
206
+ /**
207
+ * @param {function} statusListener The listener function that will be called when an activity status changes.
208
+ */
155
209
  export function addActivityUpdatesListener(
156
- listener: (event: ActivityUpdateEvent) => void
210
+ statusListener: (event: ActivityUpdateEvent) => void
157
211
  ): Voidable<EventSubscription> {
158
- if (assertIOS('addActivityUpdatesListener')) return ExpoLiveActivityModule.addListener('onStateChange', listener)
212
+ if (assertIOS('addActivityUpdatesListener'))
213
+ return ExpoLiveActivityModule.addListener('onStateChange', statusListener)
159
214
  }