expo-live-activity 0.4.1-alpha1 → 0.4.2

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
@@ -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 // 'fullHeight' | 'default';
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',
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
@@ -31,8 +31,14 @@ export type Padding = {
31
31
  vertical?: number;
32
32
  horizontal?: number;
33
33
  } | number;
34
- export type ImagePosition = 'left' | 'right';
35
- export type ImageSize = 'fullHeight' | 'default';
34
+ export type ImagePosition = 'left' | 'right' | 'leftStretch' | 'rightStretch';
35
+ export type ImageAlign = 'top' | 'center' | 'bottom';
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';
36
42
  export type LiveActivityConfig = {
37
43
  backgroundColor?: string;
38
44
  titleColor?: string;
@@ -43,7 +49,9 @@ export type LiveActivityConfig = {
43
49
  timerType?: DynamicIslandTimerType;
44
50
  padding?: Padding;
45
51
  imagePosition?: ImagePosition;
52
+ imageAlign?: ImageAlign;
46
53
  imageSize?: ImageSize;
54
+ contentFit?: ImageContentFit;
47
55
  };
48
56
  export type ActivityTokenReceivedEvent = {
49
57
  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
@@ -6,21 +6,55 @@ function assertIOS(name) {
6
6
  console.error(`${name} is only available on iOS`);
7
7
  return isIOS;
8
8
  }
9
+ function normalizeConfig(config) {
10
+ if (config === undefined)
11
+ return config;
12
+ const { padding, imageSize, ...base } = config;
13
+ const normalized = { ...base };
14
+ // Normalize padding: keep number in padding, object in paddingDetails
15
+ if (typeof padding === 'number') {
16
+ normalized.padding = padding;
17
+ }
18
+ else if (typeof padding === 'object') {
19
+ normalized.paddingDetails = padding;
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
+ }
50
+ return normalized;
51
+ }
9
52
  /**
10
53
  * @param {LiveActivityState} state The state for the live activity.
11
54
  * @param {LiveActivityConfig} config Live activity config object.
12
55
  * @returns {string} The identifier of the started activity or undefined if creating live activity failed.
13
56
  */
14
57
  export function startActivity(state, config) {
15
- function normalizeConfig(config) {
16
- if (typeof config?.padding === 'number') {
17
- return { ...config, padding: config.padding, paddingDetails: undefined };
18
- }
19
- if (typeof config?.padding === 'object') {
20
- return { ...config, padding: undefined, paddingDetails: config.padding };
21
- }
22
- return config;
23
- }
24
58
  if (assertIOS('startActivity'))
25
59
  return ExpoLiveActivityModule.startActivity(state, normalizeConfig(config));
26
60
  }
@@ -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;AAqF7D,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;;;;GAIG;AACH,MAAM,UAAU,aAAa,CAAC,KAAwB,EAAE,MAA2B;IACjF,SAAS,eAAe,CAAC,MAA2B;QAClD,IAAI,OAAO,MAAM,EAAE,OAAO,KAAK,QAAQ,EAAE,CAAC;YACxC,OAAO,EAAE,GAAG,MAAM,EAAE,OAAO,EAAE,MAAM,CAAC,OAAO,EAAE,cAAc,EAAE,SAAS,EAAE,CAAA;QAC1E,CAAC;QACD,IAAI,OAAO,MAAM,EAAE,OAAO,KAAK,QAAQ,EAAE,CAAC;YACxC,OAAO,EAAE,GAAG,MAAM,EAAE,OAAO,EAAE,SAAS,EAAE,cAAc,EAAE,MAAM,CAAC,OAAO,EAAE,CAAA;QAC1E,CAAC;QACD,OAAO,MAAM,CAAA;IACf,CAAC;IAED,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'\n\nexport type ImageSize = 'fullHeight' | 'default'\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 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\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 function normalizeConfig(config?: LiveActivityConfig) {\n if (typeof config?.padding === 'number') {\n return { ...config, padding: config.padding, paddingDetails: undefined }\n }\n if (typeof config?.padding === 'object') {\n return { ...config, padding: undefined, paddingDetails: config.padding }\n }\n return config\n }\n\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\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\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"]}
@@ -59,7 +59,22 @@ public class ExpoLiveActivityModule: Module {
59
59
  var imagePosition: String?
60
60
 
61
61
  @Field
62
- var imageSize: String?
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?
72
+
73
+ @Field
74
+ var imageAlign: String?
75
+
76
+ @Field
77
+ var contentFit: String?
63
78
 
64
79
  struct PaddingDetails: Record {
65
80
  @Field var top: Int?
@@ -222,7 +237,12 @@ public class ExpoLiveActivityModule: Module {
222
237
  )
223
238
  },
224
239
  imagePosition: config.imagePosition,
225
- imageSize: config.imageSize
240
+ imageWidth: config.imageWidth,
241
+ imageHeight: config.imageHeight,
242
+ imageWidthPercent: config.imageWidthPercent,
243
+ imageHeightPercent: config.imageHeightPercent,
244
+ imageAlign: config.imageAlign,
245
+ contentFit: config.contentFit
226
246
  )
227
247
 
228
248
  let initialState = LiveActivityAttributes.ContentState(
@@ -22,7 +22,12 @@ struct LiveActivityAttributes: ActivityAttributes {
22
22
  var padding: Int?
23
23
  var paddingDetails: PaddingDetails?
24
24
  var imagePosition: String?
25
- var imageSize: String?
25
+ var imageWidth: Int?
26
+ var imageHeight: Int?
27
+ var imageWidthPercent: Double?
28
+ var imageHeightPercent: Double?
29
+ var imageAlign: String?
30
+ var contentFit: String?
26
31
 
27
32
  enum DynamicIslandTimerType: String, Codable {
28
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,14 +15,141 @@ 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
 
46
+ private var imageAlignment: Alignment {
47
+ switch attributes.imageAlign {
48
+ case "center":
49
+ return .center
50
+ case "bottom":
51
+ return .bottom
52
+ default:
53
+ return .top
54
+ }
55
+ }
56
+
57
+ private func alignedImage(imageName: String) -> 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 ?? "cover"
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
+ }
137
+ }
138
+ .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: imageAlignment)
139
+ .background(
140
+ GeometryReader { proxy in
141
+ Color.clear
142
+ .onAppear {
143
+ let s = proxy.size
144
+ if s.width > 0, s.height > 0 { imageContainerSize = s }
145
+ }
146
+ .onChange(of: proxy.size) { s in
147
+ if s.width > 0, s.height > 0 { imageContainerSize = s }
148
+ }
149
+ }
150
+ )
151
+ }
152
+
26
153
  var body: some View {
27
154
  let defaultPadding = 24
28
155
 
@@ -55,11 +182,16 @@ import WidgetKit
55
182
  )
56
183
 
57
184
  VStack(alignment: .leading) {
185
+ let position = attributes.imagePosition ?? "right"
186
+ let isStretch = position.contains("Stretch")
187
+ let isLeftImage = position.hasPrefix("left")
188
+ let hasImage = contentState.imageName != nil
189
+ let effectiveStretch = isStretch && hasImage
190
+
58
191
  HStack(alignment: .center) {
59
- if attributes.imagePosition == "left" {
192
+ if hasImage, isLeftImage {
60
193
  if let imageName = contentState.imageName {
61
- resizableImage(imageName: imageName)
62
- .applyImageSize(attributes.imageSize)
194
+ alignedImage(imageName: imageName)
63
195
  }
64
196
  }
65
197
 
@@ -75,7 +207,7 @@ import WidgetKit
75
207
  .modifier(ConditionalForegroundViewModifier(color: attributes.subtitleColor))
76
208
  }
77
209
 
78
- if attributes.imageSize == "fullHeight" {
210
+ if effectiveStretch {
79
211
  if let date = contentState.timerEndDateInMilliseconds {
80
212
  ProgressView(timerInterval: Date.toTimerInterval(miliseconds: date))
81
213
  .tint(progressViewTint)
@@ -86,18 +218,17 @@ import WidgetKit
86
218
  .modifier(ConditionalForegroundViewModifier(color: attributes.progressViewLabelColor))
87
219
  }
88
220
  }
89
- }
221
+ }.layoutPriority(1)
90
222
 
91
- if attributes.imagePosition == "right" || attributes.imagePosition == nil {
223
+ if hasImage, !isLeftImage { // right side (default)
92
224
  Spacer()
93
225
  if let imageName = contentState.imageName {
94
- resizableImage(imageName: imageName)
95
- .applyImageSize(attributes.imageSize)
226
+ alignedImage(imageName: imageName)
96
227
  }
97
228
  }
98
229
  }
99
230
 
100
- if attributes.imageSize != "fullHeight" {
231
+ if !effectiveStretch {
101
232
  if let date = contentState.timerEndDateInMilliseconds {
102
233
  ProgressView(timerInterval: Date.toTimerInterval(miliseconds: date))
103
234
  .tint(progressViewTint)
@@ -23,7 +23,12 @@ struct LiveActivityAttributes: ActivityAttributes {
23
23
  var padding: Int?
24
24
  var paddingDetails: PaddingDetails?
25
25
  var imagePosition: String?
26
- var imageSize: String?
26
+ var imageWidth: Int?
27
+ var imageHeight: Int?
28
+ var imageWidthPercent: Double?
29
+ var imageHeightPercent: Double?
30
+ var imageAlign: String?
31
+ var contentFit: String?
27
32
 
28
33
  enum DynamicIslandTimerType: String, Codable {
29
34
  case circular
@@ -139,7 +144,6 @@ struct LiveActivityWidget: Widget {
139
144
  VStack {
140
145
  Spacer()
141
146
  resizableImage(imageName: imageName)
142
- .frame(maxHeight: 64)
143
147
  Spacer()
144
148
  }
145
149
  }
@@ -6,14 +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: String?) -> some View {
12
- switch size {
13
- case "fullHeight":
14
- frame(maxHeight: .infinity)
15
- default:
16
- frame(maxHeight: 64)
17
- }
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)
18
32
  }
19
33
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "expo-live-activity",
3
- "version": "0.4.1-alpha1",
3
+ "version": "0.4.2",
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",
@@ -11,7 +11,7 @@ const withWidgetExtensionEntitlements_1 = require("./withWidgetExtensionEntitlem
11
11
  const withXcode_1 = require("./withXcode");
12
12
  const withWidgetsAndLiveActivities = (config, props) => {
13
13
  const deploymentTarget = '16.2';
14
- const targetName = `${config_plugins_1.IOSConfig.XcodeUtils.sanitizedName(config.name)}LiveActivity`;
14
+ const targetName = 'LiveActivity';
15
15
  const bundleIdentifier = `${config.ios?.bundleIdentifier}.${targetName}`;
16
16
  config.ios = {
17
17
  ...config.ios,
@@ -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;
@@ -1,4 +1,4 @@
1
- import { IOSConfig, withPlugins } from 'expo/config-plugins'
1
+ import { withPlugins } from 'expo/config-plugins'
2
2
 
3
3
  import type { LiveActivityConfigPlugin } from './types'
4
4
  import { withConfig } from './withConfig'
@@ -9,7 +9,7 @@ import { withXcode } from './withXcode'
9
9
 
10
10
  const withWidgetsAndLiveActivities: LiveActivityConfigPlugin = (config, props) => {
11
11
  const deploymentTarget = '16.2'
12
- const targetName = `${IOSConfig.XcodeUtils.sanitizedName(config.name)}LiveActivity`
12
+ const targetName = 'LiveActivity'
13
13
  const bundleIdentifier = `${config.ios?.bundleIdentifier}.${targetName}`
14
14
 
15
15
  config.ios = {
@@ -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
@@ -45,9 +45,17 @@ export type Padding =
45
45
  }
46
46
  | number
47
47
 
48
- export type ImagePosition = 'left' | 'right'
48
+ export type ImagePosition = 'left' | 'right' | 'leftStretch' | 'rightStretch'
49
49
 
50
- export type ImageSize = 'fullHeight' | 'default'
50
+ export type ImageAlign = 'top' | 'center' | 'bottom'
51
+
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'
51
59
 
52
60
  export type LiveActivityConfig = {
53
61
  backgroundColor?: string
@@ -59,7 +67,9 @@ export type LiveActivityConfig = {
59
67
  timerType?: DynamicIslandTimerType
60
68
  padding?: Padding
61
69
  imagePosition?: ImagePosition
70
+ imageAlign?: ImageAlign
62
71
  imageSize?: ImageSize
72
+ contentFit?: ImageContentFit
63
73
  }
64
74
 
65
75
  export type ActivityTokenReceivedEvent = {
@@ -94,22 +104,64 @@ function assertIOS(name: string) {
94
104
  return isIOS
95
105
  }
96
106
 
107
+ function normalizeConfig(config?: LiveActivityConfig) {
108
+ if (config === undefined) return config
109
+
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
+ }
118
+ const normalized: NormalizedConfig = { ...base }
119
+
120
+ // Normalize padding: keep number in padding, object in paddingDetails
121
+ if (typeof padding === 'number') {
122
+ normalized.padding = padding
123
+ } else if (typeof padding === 'object') {
124
+ normalized.paddingDetails = padding
125
+ }
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
+
156
+ return normalized
157
+ }
158
+
97
159
  /**
98
160
  * @param {LiveActivityState} state The state for the live activity.
99
161
  * @param {LiveActivityConfig} config Live activity config object.
100
162
  * @returns {string} The identifier of the started activity or undefined if creating live activity failed.
101
163
  */
102
164
  export function startActivity(state: LiveActivityState, config?: LiveActivityConfig): Voidable<string> {
103
- function normalizeConfig(config?: LiveActivityConfig) {
104
- if (typeof config?.padding === 'number') {
105
- return { ...config, padding: config.padding, paddingDetails: undefined }
106
- }
107
- if (typeof config?.padding === 'object') {
108
- return { ...config, padding: undefined, paddingDetails: config.padding }
109
- }
110
- return config
111
- }
112
-
113
165
  if (assertIOS('startActivity')) return ExpoLiveActivityModule.startActivity(state, normalizeConfig(config))
114
166
  }
115
167