expo-live-activity 0.4.0 → 0.4.1-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/.swiftformat ADDED
@@ -0,0 +1,2 @@
1
+ --indent 2
2
+ --swift-version 5.4
package/README.md CHANGED
@@ -1,16 +1,18 @@
1
+ ![expo-live-activity by Software Mansion](https://github.com/user-attachments/assets/9f9be263-84ee-4034-a3ca-39c72c189544)
2
+
1
3
  > [!WARNING]
2
- > This library is in early development stage, breaking changes can be introduced in minor version upgrades.
4
+ > This library is in early development stage; breaking changes can be introduced in minor version upgrades.
3
5
 
4
6
  # expo-live-activity
5
7
 
6
- `expo-live-activity` is a React Native module designed for use with Expo to manage and display live activities on iOS devices exclusively. This module leverages the Live Activities feature introduced in iOS 16, allowing developers to deliver timely updates right on the lock screen.
8
+ `expo-live-activity` is a React Native module designed for use with Expo to manage and display Live Activities on iOS devices exclusively. This module leverages the Live Activities feature introduced in iOS 16, allowing developers to deliver timely updates right on the lock screen.
7
9
 
8
10
  ## Features
9
11
 
10
- - Start, update, and stop live activities directly from your React Native application.
12
+ - Start, update, and stop Live Activities directly from your React Native application.
11
13
  - Easy integration with a comprehensive API.
12
- - Custom image support within live activities with a pre-configured path.
13
- - Listen and handle changes in push notification tokens associated with a live activity.
14
+ - Custom image support within Live Activities with a pre-configured path.
15
+ - Listen and handle changes in push notification tokens associated with a Live Activity.
14
16
 
15
17
  ## Platform compatibility
16
18
 
@@ -19,7 +21,7 @@
19
21
  ## Installation
20
22
 
21
23
  > [!NOTE]
22
- > The library isn't supported in Expo Go, to set it up correctly you need to use [Expo DevClient](https://docs.expo.dev/versions/latest/sdk/dev-client/) .
24
+ > The library isn't supported in Expo Go; to set it up correctly you need to use [Expo DevClient](https://docs.expo.dev/versions/latest/sdk/dev-client/) .
23
25
  > To begin using `expo-live-activity`, follow the installation and configuration steps outlined below:
24
26
 
25
27
  ### Step 1: Installation
@@ -32,7 +34,7 @@ npm install expo-live-activity
32
34
 
33
35
  ### Step 2: Config Plugin Setup
34
36
 
35
- The module comes with a built-in config plugin that creates a target in iOS with all the necessary files. The images used in live activities should be added to a pre-defined folder in your assets directory:
37
+ The module comes with a built-in config plugin that creates a target in iOS with all the necessary files. The images used in Live Activities should be added to a pre-defined folder in your assets directory:
36
38
 
37
39
  1. **Add the config plugin to your app.json or app.config.js:**
38
40
  ```json
@@ -58,7 +60,7 @@ The module comes with a built-in config plugin that creates a target in iOS with
58
60
  }
59
61
  ```
60
62
  2. **Assets configuration:**
61
- Place images intended for live activities in the `assets/liveActivity` folder. The plugin manages these assets automatically.
63
+ Place images intended for Live Activities in the `assets/liveActivity` folder. The plugin manages these assets automatically.
62
64
 
63
65
  Then prebuild your app with:
64
66
 
@@ -76,29 +78,29 @@ import * as LiveActivity from 'expo-live-activity'
76
78
 
77
79
  ## API
78
80
 
79
- `expo-live-activity` module exports three primary functions to manage live activities:
81
+ `expo-live-activity` module exports three primary functions to manage Live Activities:
80
82
 
81
83
  ### Managing Live Activities
82
84
 
83
85
  - **`startActivity(state: LiveActivityState, config?: LiveActivityConfig): string | undefined`**:
84
- Start a new live activity. Takes a `state` configuration object for initial activity state and an optional `config` object to customize appearance or behavior. It returns the `ID` of the created live activity, which should be stored for future reference. If the live activity can't be created (eg. on android or iOS lower than 16.2), it will return `undefined`.
86
+ Start a new Live Activity. Takes a `state` configuration object for initial activity state and an optional `config` object to customize appearance or behavior. It returns the `ID` of the created Live Activity, which should be stored for future reference. If the Live Activity can't be created (eg. on android or iOS lower than 16.2), it will return `undefined`.
85
87
 
86
88
  - **`updateActivity(id: string, state: LiveActivityState)`**:
87
- Update an existing live activity. The `state` object should contain updated information. The `activityId` indicates which activity should be updated.
89
+ Update an existing Live Activity. The `state` object should contain updated information. The `activityId` indicates which activity should be updated.
88
90
 
89
91
  - **`stopActivity(id: string, state: LiveActivityState)`**:
90
- Terminate an ongoing live activity. The `state` object should contain the final state of the activity. The `activityId` indicates which activity should be stopped.
92
+ Terminate an ongoing Live Activity. The `state` object should contain the final state of the activity. The `activityId` indicates which activity should be stopped.
91
93
 
92
94
  ### Handling Push Notification Tokens
93
95
 
94
96
  - **`addActivityPushToStartTokenListener(listener: (event: ActivityPushToStartTokenReceivedEvent) => void): EventSubscription | undefined`**:
95
97
  Subscribe to changes in the push to start token for starting live acitivities with push notifications.
96
98
  - **`addActivityTokenListener(listener: (event: ActivityTokenReceivedEvent) => void): EventSubscription | undefined`**:
97
- Subscribe to changes in the push notification token associated with live activities.
99
+ Subscribe to changes in the push notification token associated with Live Activities.
98
100
 
99
101
  ### Deep linking
100
102
 
101
- When starting a new live activity, it's possible to pass `deepLinkUrl` field in `config` object. This usually should be a path to one of your screens. If you are using @react-navigation in your project, it's easiest to enable auto linking:
103
+ When starting a new Live Activity, it's possible to pass `deepLinkUrl` field in `config` object. This usually should be a path to one of your screens. If you are using @react-navigation in your project, it's easiest to enable auto linking:
102
104
 
103
105
  ```typescript
104
106
  const prefix = Linking.createURL('')
@@ -148,17 +150,20 @@ The `config` object should include:
148
150
  progressViewTint?: string;
149
151
  progressViewLabelColor?: string;
150
152
  deepLinkUrl?: string;
151
- timerType?: DynamicIslandTimerType; // "circular" | "digital" - defines timer appereance on the dynamic island
153
+ timerType?: DynamicIslandTimerType; // "circular" | "digital" - defines timer appearance on the dynamic island
154
+ padding?: Padding // number | {top?: number bottom?: number ...}
155
+ imagePosition?: ImagePosition; // 'left' | 'right';
156
+ imageSize?: ImageSize // 'fullHeight' | 'default';
152
157
  };
153
158
  ```
154
159
 
155
160
  ### Activity updates
156
161
 
157
- `LiveActivity.addActivityUpdatesListener` API allows to subscribe to changes in live activity state. This is useful for example when you want to update the live activity with new information. Handler will receive an `ActivityUpdateEvent` object which contains information about new state under `activityState` property which is of `ActivityState` type, so the possible values are: `'active'`, `'dismissed'`, `'pending'`, `'stale'` or `'ended'`. Apart from this property, the event also contains `activityId` and `activityName` which can be used to identify the live activity.
162
+ `LiveActivity.addActivityUpdatesListener` API allows to subscribe to changes in Live Activity state. This is useful for example when you want to update the Live Activity with new information. Handler will receive an `ActivityUpdateEvent` object which contains information about new state under `activityState` property which is of `ActivityState` type, so the possible values are: `'active'`, `'dismissed'`, `'pending'`, `'stale'` or `'ended'`. Apart from this property, the event also contains `activityId` and `activityName` which can be used to identify the Live Activity.
158
163
 
159
164
  ## Example Usage
160
165
 
161
- Managing a live activity:
166
+ Managing a Live Activity:
162
167
 
163
168
  ```typescript
164
169
  const state: LiveActivity.LiveActivityState = {
@@ -179,13 +184,16 @@ const config: LiveActivity.LiveActivityConfig = {
179
184
  progressViewLabelColor: '#FFFFFF',
180
185
  deepLinkUrl: '/dashboard',
181
186
  timerType: 'circular',
187
+ padding: { horizontal: 20, top: 16, bottom: 16 },
188
+ imagePosition: 'right',
189
+ imageSize: 'default',
182
190
  }
183
191
 
184
192
  const activityId = LiveActivity.startActivity(state, config)
185
193
  // Store activityId for future reference
186
194
  ```
187
195
 
188
- 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.
196
+ 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.
189
197
 
190
198
  Subscribing to push token changes:
191
199
 
@@ -193,12 +201,12 @@ Subscribing to push token changes:
193
201
  useEffect(() => {
194
202
  const updateTokenSubscription = LiveActivity.addActivityTokenListener(
195
203
  ({ activityID: newActivityID, activityName: newName, activityPushToken: newToken }) => {
196
- // Send token to a remote server to update live activity with push notifications
204
+ // Send token to a remote server to update Live Activity with push notifications
197
205
  }
198
206
  )
199
207
  const startTokenSubscription = LiveActivity.addActivityPushToStartTokenListener(
200
208
  ({ activityPushToStartToken: newActivityPushToStartToken }) => {
201
- // Send token to a remote server to start live activity with push notifications
209
+ // Send token to a remote server to start Live Activity with push notifications
202
210
  }
203
211
  )
204
212
 
@@ -214,20 +222,20 @@ useEffect(() => {
214
222
 
215
223
  ## Push notifications
216
224
 
217
- By default, starting and updating live activity is possible only via API. If you want to have possibility to start or update live activity using push notifications, you can enable that feature by adding `"enablePushNotifications": true` in the plugin config in your `app.json` or `app.config.ts` file.
225
+ By default, starting and updating Live Activity is possible only via API. If you want to have possibility to start or update Live Activity using push notifications, you can enable that feature by adding `"enablePushNotifications": true` in the plugin config in your `app.json` or `app.config.ts` file.
218
226
 
219
227
  > [!NOTE]
220
228
  > PushToStart works only for iOS 17.2 and higher.
221
229
 
222
- Example payload for starting live activity:
230
+ Example payload for starting Live Activity:
223
231
 
224
232
  ```json
225
233
  {
226
234
  "aps": {
227
235
  "event": "start",
228
236
  "content-state": {
229
- "title": "Live activity title!",
230
- "subtitle": "Live activity subtitle.",
237
+ "title": "Live Activity title!",
238
+ "subtitle": "Live Activity subtitle.",
231
239
  "timerEndDateInMilliseconds": 1754410997000,
232
240
  "progress": 0.5,
233
241
  "imageName": "live_activity_image",
@@ -243,7 +251,10 @@ Example payload for starting live activity:
243
251
  "progressViewTint": "38ACDD",
244
252
  "progressViewLabelColor": "FFFFFF",
245
253
  "deepLinkUrl": "/dashboard",
246
- "timerType": "digital"
254
+ "timerType": "digital",
255
+ "padding": 24, // or use object to control each side: { "horizontal": 20, "top": 16, "bottom": 16 }
256
+ "imagePosition": "right",
257
+ "imageSize": "default"
247
258
  },
248
259
  "alert": {
249
260
  "title": "",
@@ -254,7 +265,7 @@ Example payload for starting live activity:
254
265
  }
255
266
  ```
256
267
 
257
- Example payload for updating live activity:
268
+ Example payload for updating Live Activity:
258
269
 
259
270
  ```json
260
271
  {
@@ -272,11 +283,11 @@ Example payload for updating live activity:
272
283
  }
273
284
  ```
274
285
 
275
- Where `timerEndDateInMilliseconds` value is a timestamp in milliseconds corresponding to the target point of the counter displayed in live activity view.
286
+ Where `timerEndDateInMilliseconds` value is a timestamp in milliseconds corresponding to the target point of the counter displayed in Live Activity view.
276
287
 
277
288
  ## Image support
278
289
 
279
- Live activity view also supports image display. There are two dedicated fields in the `state` object for that:
290
+ Live Activity view also supports image display. There are two dedicated fields in the `state` object for that:
280
291
 
281
292
  - `imageName`
282
293
  - `dynamicIslandImageName`
@@ -284,7 +295,7 @@ Live activity view also supports image display. There are two dedicated fields i
284
295
  The value of each field can be:
285
296
 
286
297
  - a string which maps to an asset name
287
- - a URL to remote image - currently, it's possible to use this option only via API, but we plan on to add that feature to push notifications as well. It also requires adding "App Groups" capability to both "main app" and "live activity" targets.
298
+ - a URL to remote image - currently, it's possible to use this option only via API, but we plan on to add that feature to push notifications as well. It also requires adding "App Groups" capability to both "main app" and "Live Activity" targets.
288
299
 
289
300
  ## expo-live-activity is created by Software Mansion
290
301
 
package/build/index.d.ts CHANGED
@@ -23,6 +23,16 @@ export type NativeLiveActivityState = {
23
23
  imageName?: string;
24
24
  dynamicIslandImageName?: string;
25
25
  };
26
+ export type Padding = {
27
+ top?: number;
28
+ bottom?: number;
29
+ left?: number;
30
+ right?: number;
31
+ vertical?: number;
32
+ horizontal?: number;
33
+ } | number;
34
+ export type ImagePosition = 'left' | 'right';
35
+ export type ImageSize = 'fullHeight' | 'default';
26
36
  export type LiveActivityConfig = {
27
37
  backgroundColor?: string;
28
38
  titleColor?: string;
@@ -31,6 +41,9 @@ export type LiveActivityConfig = {
31
41
  progressViewLabelColor?: string;
32
42
  deepLinkUrl?: string;
33
43
  timerType?: DynamicIslandTimerType;
44
+ padding?: Padding;
45
+ imagePosition?: ImagePosition;
46
+ imageSize?: ImageSize;
34
47
  };
35
48
  export type ActivityTokenReceivedEvent = {
36
49
  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
@@ -12,8 +12,17 @@ function assertIOS(name) {
12
12
  * @returns {string} The identifier of the started activity or undefined if creating live activity failed.
13
13
  */
14
14
  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
+ }
15
24
  if (assertIOS('startActivity'))
16
- return ExpoLiveActivityModule.startActivity(state, config);
25
+ return ExpoLiveActivityModule.startActivity(state, normalizeConfig(config));
17
26
  }
18
27
  /**
19
28
  * @param {string} id The identifier of the activity to stop.
@@ -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;AAmE7D,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,IAAI,SAAS,CAAC,eAAe,CAAC;QAAE,OAAO,sBAAsB,CAAC,aAAa,CAAC,KAAK,EAAE,MAAM,CAAC,CAAA;AAC5F,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 LiveActivityConfig = {\n backgroundColor?: string\n titleColor?: string\n subtitleColor?: string\n progressViewTint?: string\n progressViewLabelColor?: string\n deepLinkUrl?: string\n timerType?: DynamicIslandTimerType\n}\n\nexport type ActivityTokenReceivedEvent = {\n activityID: string\n 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 if (assertIOS('startActivity')) return ExpoLiveActivityModule.startActivity(state, config)\n}\n\n/**\n * @param {string} id The identifier of the activity to stop.\n * @param {LiveActivityState} state The updated state for the live activity.\n */\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":"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,10 +1,3 @@
1
- //
2
- // Data+download.swift
3
- //
4
- //
5
- // Created by Artur Bilski on 04/08/2025.
6
- //
7
-
8
1
  extension Data {
9
2
  static func download(from url: URL) async throws -> Self {
10
3
  let (data, _) = try await URLSession.shared.data(from: url)
@@ -1,13 +1,6 @@
1
1
  import ActivityKit
2
2
  import ExpoModulesCore
3
3
 
4
- enum LiveActivityErrors: Error {
5
- case unsupportedOS
6
- case notFound
7
- case liveActivitiesNotEnabled
8
- case unexpectedError(Error)
9
- }
10
-
11
4
  public class ExpoLiveActivityModule: Module {
12
5
  struct LiveActivityState: Record {
13
6
  @Field
@@ -18,7 +11,7 @@ public class ExpoLiveActivityModule: Module {
18
11
 
19
12
  @Field
20
13
  var progressBar: ProgressBar?
21
-
14
+
22
15
  struct ProgressBar: Record {
23
16
  @Field
24
17
  var date: Double?
@@ -55,6 +48,27 @@ public class ExpoLiveActivityModule: Module {
55
48
 
56
49
  @Field
57
50
  var timerType: DynamicIslandTimerType?
51
+
52
+ @Field
53
+ var padding: Int?
54
+
55
+ @Field
56
+ var paddingDetails: PaddingDetails?
57
+
58
+ @Field
59
+ var imagePosition: String?
60
+
61
+ @Field
62
+ var imageSize: String?
63
+
64
+ struct PaddingDetails: Record {
65
+ @Field var top: Int?
66
+ @Field var bottom: Int?
67
+ @Field var left: Int?
68
+ @Field var right: Int?
69
+ @Field var vertical: Int?
70
+ @Field var horizontal: Int?
71
+ }
58
72
  }
59
73
 
60
74
  enum DynamicIslandTimerType: String, Enumerable {
@@ -78,13 +92,15 @@ public class ExpoLiveActivityModule: Module {
78
92
  sendEvent(
79
93
  "onPushToStartTokenReceived",
80
94
  [
81
- "activityPushToStartToken": activityPushToStartToken
95
+ "activityPushToStartToken": activityPushToStartToken,
82
96
  ]
83
97
  )
84
98
  }
85
99
 
86
100
  @available(iOS 16.1, *)
87
- private func sendStateChange(activity: Activity<LiveActivityAttributes>, activityState: ActivityState) {
101
+ private func sendStateChange(
102
+ activity: Activity<LiveActivityAttributes>, activityState: ActivityState
103
+ ) {
88
104
  sendEvent(
89
105
  "onStateChange",
90
106
  [
@@ -95,8 +111,9 @@ public class ExpoLiveActivityModule: Module {
95
111
  )
96
112
  }
97
113
 
98
- private func updateImages(state: LiveActivityState, newState: inout LiveActivityAttributes.ContentState) async throws
99
- {
114
+ private func updateImages(
115
+ state: LiveActivityState, newState: inout LiveActivityAttributes.ContentState
116
+ ) async throws {
100
117
  if let name = state.imageName {
101
118
  newState.imageName = try await resolveImage(from: name)
102
119
  }
@@ -157,7 +174,8 @@ public class ExpoLiveActivityModule: Module {
157
174
  }
158
175
 
159
176
  private var pushNotificationsEnabled: Bool {
160
- Bundle.main.object(forInfoDictionaryKey: "ExpoLiveActivity_EnablePushNotifications") as? Bool ?? false
177
+ Bundle.main.object(forInfoDictionaryKey: "ExpoLiveActivity_EnablePushNotifications") as? Bool
178
+ ?? false
161
179
  }
162
180
 
163
181
  public func definition() -> ModuleDefinition {
@@ -172,12 +190,17 @@ public class ExpoLiveActivityModule: Module {
172
190
 
173
191
  Events("onTokenReceived", "onPushToStartTokenReceived", "onStateChange")
174
192
 
175
- Function("startActivity") { (state: LiveActivityState, maybeConfig: LiveActivityConfig?) -> String in
176
- guard #available(iOS 16.2, *) else { throw LiveActivityErrors.unsupportedOS }
177
- guard ActivityAuthorizationInfo().areActivitiesEnabled else { throw LiveActivityErrors.liveActivitiesNotEnabled }
193
+ Function("startActivity") {
194
+ (state: LiveActivityState, maybeConfig: LiveActivityConfig?) -> String in
195
+ guard #available(iOS 16.2, *) else { throw UnsupportedOSException("16.2") }
196
+
197
+ guard ActivityAuthorizationInfo().areActivitiesEnabled else {
198
+ throw LiveActivitiesNotEnabledException()
199
+ }
178
200
 
179
201
  do {
180
202
  let config = maybeConfig ?? LiveActivityConfig()
203
+
181
204
  let attributes = LiveActivityAttributes(
182
205
  name: "ExpoLiveActivity",
183
206
  backgroundColor: config.backgroundColor,
@@ -186,8 +209,22 @@ public class ExpoLiveActivityModule: Module {
186
209
  progressViewTint: config.progressViewTint,
187
210
  progressViewLabelColor: config.progressViewLabelColor,
188
211
  deepLinkUrl: config.deepLinkUrl,
189
- timerType: config.timerType == .digital ? .digital : .circular
212
+ timerType: config.timerType == .digital ? .digital : .circular,
213
+ padding: config.padding,
214
+ paddingDetails: config.paddingDetails.map {
215
+ LiveActivityAttributes.PaddingDetails(
216
+ top: $0.top,
217
+ bottom: $0.bottom,
218
+ left: $0.left,
219
+ right: $0.right,
220
+ vertical: $0.vertical,
221
+ horizontal: $0.horizontal
222
+ )
223
+ },
224
+ imagePosition: config.imagePosition,
225
+ imageSize: config.imageSize
190
226
  )
227
+
191
228
  let initialState = LiveActivityAttributes.ContentState(
192
229
  title: state.title,
193
230
  subtitle: state.subtitle,
@@ -208,20 +245,19 @@ public class ExpoLiveActivityModule: Module {
208
245
  }
209
246
 
210
247
  return activity.id
211
- } catch let error {
212
- print("Error with live activity: \(error)")
213
- throw LiveActivityErrors.unexpectedError(error)
214
-
248
+ } catch {
249
+ throw UnexpectedErrorException(error)
215
250
  }
216
251
  }
217
252
 
218
253
  Function("stopActivity") { (activityId: String, state: LiveActivityState) in
219
- guard #available(iOS 16.2, *) else { throw LiveActivityErrors.unsupportedOS }
254
+ guard #available(iOS 16.2, *) else { throw UnsupportedOSException("16.2") }
255
+
220
256
  guard
221
257
  let activity = Activity<LiveActivityAttributes>.activities.first(where: {
222
258
  $0.id == activityId
223
259
  })
224
- else { throw LiveActivityErrors.notFound }
260
+ else { throw ActivityNotFoundException(activityId) }
225
261
 
226
262
  Task {
227
263
  print("Stopping activity with id: \(activityId)")
@@ -240,12 +276,15 @@ public class ExpoLiveActivityModule: Module {
240
276
  }
241
277
 
242
278
  Function("updateActivity") { (activityId: String, state: LiveActivityState) in
243
- guard #available(iOS 16.2, *) else { throw LiveActivityErrors.unsupportedOS }
279
+ guard #available(iOS 16.2, *) else {
280
+ throw UnsupportedOSException("16.2")
281
+ }
282
+
244
283
  guard
245
284
  let activity = Activity<LiveActivityAttributes>.activities.first(where: {
246
285
  $0.id == activityId
247
286
  })
248
- else { throw LiveActivityErrors.notFound }
287
+ else { throw ActivityNotFoundException(activityId) }
249
288
 
250
289
  Task {
251
290
  print("Updating activity with id: \(activityId)")
package/ios/Helpers.swift CHANGED
@@ -1,15 +1,9 @@
1
- //
2
- // Helpers.swift
3
- //
4
- //
5
- // Created by Artur Bilski on 04/08/2025.
6
- //
7
-
8
1
  func resolveImage(from string: String) async throws -> String {
9
2
  if let url = URL(string: string), url.scheme?.hasPrefix("http") == true,
10
- let container = FileManager.default.containerURL(
11
- forSecurityApplicationGroupIdentifier: "group.expoLiveActivity.sharedData"
12
- ) {
3
+ let container = FileManager.default.containerURL(
4
+ forSecurityApplicationGroupIdentifier: "group.expoLiveActivity.sharedData"
5
+ )
6
+ {
13
7
  let data = try await Data.download(from: url)
14
8
  let filename = UUID().uuidString + ".png"
15
9
  let fileURL = container.appendingPathComponent(filename)
@@ -1,15 +1,8 @@
1
- //
2
- // LiveActivityAttributes.swift
3
- // ExpoLiveActivity
4
- //
5
- // Created by Anna Olak on 03/06/2025.
6
- //
7
-
8
1
  import ActivityKit
9
2
  import Foundation
10
3
 
11
4
  struct LiveActivityAttributes: ActivityAttributes {
12
- public struct ContentState: Codable, Hashable {
5
+ struct ContentState: Codable, Hashable {
13
6
  var title: String
14
7
  var subtitle: String?
15
8
  var timerEndDateInMilliseconds: Double?
@@ -26,9 +19,22 @@ struct LiveActivityAttributes: ActivityAttributes {
26
19
  var progressViewLabelColor: String?
27
20
  var deepLinkUrl: String?
28
21
  var timerType: DynamicIslandTimerType?
22
+ var padding: Int?
23
+ var paddingDetails: PaddingDetails?
24
+ var imagePosition: String?
25
+ var imageSize: String?
29
26
 
30
27
  enum DynamicIslandTimerType: String, Codable {
31
28
  case circular
32
29
  case digital
33
30
  }
31
+
32
+ struct PaddingDetails: Codable, Hashable {
33
+ var top: Int?
34
+ var bottom: Int?
35
+ var left: Int?
36
+ var right: Int?
37
+ var vertical: Int?
38
+ var horizontal: Int?
39
+ }
34
40
  }
@@ -0,0 +1,25 @@
1
+ import ExpoModulesCore
2
+
3
+ final class UnsupportedOSException: GenericException<String> {
4
+ override var reason: String {
5
+ "Live Activities require iOS \(param) or later. Current version: \(UIDevice.current.systemVersion)"
6
+ }
7
+ }
8
+
9
+ final class ActivityNotFoundException: GenericException<String> {
10
+ override var reason: String {
11
+ "Activity with ID '\(param)' not found"
12
+ }
13
+ }
14
+
15
+ final class LiveActivitiesNotEnabledException: Exception {
16
+ override var reason: String {
17
+ "Live Activities are currently disabled - enable them in Settings > Face ID & Passcode > Live Activities and Settings > [Your App] > Live Activities"
18
+ }
19
+ }
20
+
21
+ final class UnexpectedErrorException: GenericException<Error> {
22
+ override var reason: String {
23
+ "An unexpected error occurred: \(param.localizedDescription)"
24
+ }
25
+ }
@@ -8,7 +8,7 @@ extension Color {
8
8
  cString.remove(at: cString.startIndex)
9
9
  }
10
10
 
11
- if (cString.count) != 6 && (cString.count) != 8 {
11
+ if (cString.count) != 6, (cString.count) != 8 {
12
12
  self.init(.white)
13
13
  return
14
14
  }
@@ -19,17 +19,17 @@ extension Color {
19
19
  if (cString.count) == 8 {
20
20
  self.init(
21
21
  .sRGB,
22
- red: Double((rgbValue >> 24) & 0xff) / 255,
23
- green: Double((rgbValue >> 16) & 0xff) / 255,
24
- blue: Double((rgbValue >> 08) & 0xff) / 255,
25
- opacity: Double((rgbValue >> 00) & 0xff) / 255
22
+ red: Double((rgbValue >> 24) & 0xFF) / 255,
23
+ green: Double((rgbValue >> 16) & 0xFF) / 255,
24
+ blue: Double((rgbValue >> 08) & 0xFF) / 255,
25
+ opacity: Double((rgbValue >> 00) & 0xFF) / 255
26
26
  )
27
27
  } else {
28
28
  self.init(
29
29
  .sRGB,
30
- red: Double((rgbValue >> 16) & 0xff) / 255,
31
- green: Double((rgbValue >> 08) & 0xff) / 255,
32
- blue: Double((rgbValue >> 00) & 0xff) / 255,
30
+ red: Double((rgbValue >> 16) & 0xFF) / 255,
31
+ green: Double((rgbValue >> 08) & 0xFF) / 255,
32
+ blue: Double((rgbValue >> 00) & 0xFF) / 255,
33
33
  opacity: 1
34
34
  )
35
35
  }
@@ -1,13 +1,7 @@
1
- //
2
- // Date+toTimerInterval.swift
3
- //
4
- //
5
- // Created by Artur Bilski on 04/08/2025.
6
- //
7
1
  import SwiftUI
8
2
 
9
3
  extension Date {
10
4
  static func toTimerInterval(miliseconds: Double) -> ClosedRange<Self> {
11
- Self.now...max(Self.now, Date(timeIntervalSince1970: miliseconds / 1000))
5
+ now ... max(now, Date(timeIntervalSince1970: miliseconds / 1000))
12
6
  }
13
7
  }
@@ -1,9 +1,3 @@
1
- //
2
- // Image+dynamic.swift
3
- //
4
- //
5
- // Created by Artur Bilski on 04/08/2025.
6
- //
7
1
  import SwiftUI
8
2
 
9
3
  extension Image {
@@ -1,10 +1,3 @@
1
- //
2
- // LiveActivityView.swift
3
- // ExpoLiveActivity
4
- //
5
- // Created by Anna Olak on 03/06/2025.
6
- //
7
-
8
1
  import SwiftUI
9
2
  import WidgetKit
10
3
 
@@ -31,8 +24,45 @@ import WidgetKit
31
24
  }
32
25
 
33
26
  var body: some View {
27
+ let defaultPadding = 24
28
+
29
+ let top = CGFloat(
30
+ attributes.paddingDetails?.top
31
+ ?? attributes.paddingDetails?.vertical
32
+ ?? attributes.padding
33
+ ?? defaultPadding
34
+ )
35
+
36
+ let bottom = CGFloat(
37
+ attributes.paddingDetails?.bottom
38
+ ?? attributes.paddingDetails?.vertical
39
+ ?? attributes.padding
40
+ ?? defaultPadding
41
+ )
42
+
43
+ let leading = CGFloat(
44
+ attributes.paddingDetails?.left
45
+ ?? attributes.paddingDetails?.horizontal
46
+ ?? attributes.padding
47
+ ?? defaultPadding
48
+ )
49
+
50
+ let trailing = CGFloat(
51
+ attributes.paddingDetails?.right
52
+ ?? attributes.paddingDetails?.horizontal
53
+ ?? attributes.padding
54
+ ?? defaultPadding
55
+ )
56
+
34
57
  VStack(alignment: .leading) {
35
58
  HStack(alignment: .center) {
59
+ if attributes.imagePosition == "left" {
60
+ if let imageName = contentState.imageName {
61
+ resizableImage(imageName: imageName)
62
+ .applyImageSize(attributes.imageSize)
63
+ }
64
+ }
65
+
36
66
  VStack(alignment: .leading, spacing: 2) {
37
67
  Text(contentState.title)
38
68
  .font(.title2)
@@ -44,27 +74,42 @@ import WidgetKit
44
74
  .font(.title3)
45
75
  .modifier(ConditionalForegroundViewModifier(color: attributes.subtitleColor))
46
76
  }
47
- }
48
77
 
49
- Spacer()
78
+ if attributes.imageSize == "fullHeight" {
79
+ if let date = contentState.timerEndDateInMilliseconds {
80
+ ProgressView(timerInterval: Date.toTimerInterval(miliseconds: date))
81
+ .tint(progressViewTint)
82
+ .modifier(ConditionalForegroundViewModifier(color: attributes.progressViewLabelColor))
83
+ } else if let progress = contentState.progress {
84
+ ProgressView(value: progress)
85
+ .tint(progressViewTint)
86
+ .modifier(ConditionalForegroundViewModifier(color: attributes.progressViewLabelColor))
87
+ }
88
+ }
89
+ }
50
90
 
51
- if let imageName = contentState.imageName {
52
- resizableImage(imageName: imageName)
53
- .frame(maxHeight: 64)
91
+ if attributes.imagePosition == "right" || attributes.imagePosition == nil {
92
+ Spacer()
93
+ if let imageName = contentState.imageName {
94
+ resizableImage(imageName: imageName)
95
+ .applyImageSize(attributes.imageSize)
96
+ }
54
97
  }
55
98
  }
56
99
 
57
- if let date = contentState.timerEndDateInMilliseconds {
58
- ProgressView(timerInterval: Date.toTimerInterval(miliseconds: date))
59
- .tint(progressViewTint)
60
- .modifier(ConditionalForegroundViewModifier(color: attributes.progressViewLabelColor))
61
- } else if let progress = contentState.progress {
62
- ProgressView(value: progress)
63
- .tint(progressViewTint)
64
- .modifier(ConditionalForegroundViewModifier(color: attributes.progressViewLabelColor))
100
+ if attributes.imageSize != "fullHeight" {
101
+ if let date = contentState.timerEndDateInMilliseconds {
102
+ ProgressView(timerInterval: Date.toTimerInterval(miliseconds: date))
103
+ .tint(progressViewTint)
104
+ .modifier(ConditionalForegroundViewModifier(color: attributes.progressViewLabelColor))
105
+ } else if let progress = contentState.progress {
106
+ ProgressView(value: progress)
107
+ .tint(progressViewTint)
108
+ .modifier(ConditionalForegroundViewModifier(color: attributes.progressViewLabelColor))
109
+ }
65
110
  }
66
111
  }
67
- .padding(24)
112
+ .padding(EdgeInsets(top: top, leading: leading, bottom: bottom, trailing: trailing))
68
113
  }
69
114
  }
70
115
 
@@ -1,16 +1,9 @@
1
- //
2
- // LiveActivityWidget.swift
3
- // LiveActivity
4
- //
5
- // Created by Anna Olak on 02/06/2025.
6
- //
7
-
8
1
  import ActivityKit
9
2
  import SwiftUI
10
3
  import WidgetKit
11
4
 
12
5
  struct LiveActivityAttributes: ActivityAttributes {
13
- public struct ContentState: Codable, Hashable {
6
+ struct ContentState: Codable, Hashable {
14
7
  var title: String
15
8
  var subtitle: String?
16
9
  var timerEndDateInMilliseconds: Double?
@@ -27,11 +20,24 @@ struct LiveActivityAttributes: ActivityAttributes {
27
20
  var progressViewLabelColor: String?
28
21
  var deepLinkUrl: String?
29
22
  var timerType: DynamicIslandTimerType?
23
+ var padding: Int?
24
+ var paddingDetails: PaddingDetails?
25
+ var imagePosition: String?
26
+ var imageSize: String?
30
27
 
31
28
  enum DynamicIslandTimerType: String, Codable {
32
29
  case circular
33
30
  case digital
34
31
  }
32
+
33
+ struct PaddingDetails: Codable, Hashable {
34
+ var top: Int?
35
+ var bottom: Int?
36
+ var left: Int?
37
+ var right: Int?
38
+ var vertical: Int?
39
+ var horizontal: Int?
40
+ }
35
41
  }
36
42
 
37
43
  struct LiveActivityWidget: Widget {
@@ -60,9 +66,11 @@ struct LiveActivityWidget: Widget {
60
66
  }
61
67
  DynamicIslandExpandedRegion(.bottom) {
62
68
  if let date = context.state.timerEndDateInMilliseconds {
63
- dynamicIslandExpandedBottom(endDate: date, progressViewTint: context.attributes.progressViewTint)
64
- .padding(.horizontal, 5)
65
- .applyWidgetURL(from: context.attributes.deepLinkUrl)
69
+ dynamicIslandExpandedBottom(
70
+ endDate: date, progressViewTint: context.attributes.progressViewTint
71
+ )
72
+ .padding(.horizontal, 5)
73
+ .applyWidgetURL(from: context.attributes.deepLinkUrl)
66
74
  }
67
75
  }
68
76
  } compactLeading: {
@@ -1,10 +1,3 @@
1
- //
2
- // LiveActivityWidgetBundle.swift
3
- // LiveActivity
4
- //
5
- // Created by Anna Olak on 02/06/2025.
6
- //
7
-
8
1
  import SwiftUI
9
2
  import WidgetKit
10
3
 
@@ -1,10 +1,3 @@
1
- //
2
- // View+applyIfPresent.swift
3
- //
4
- //
5
- // Created by Artur Bilski on 05/08/2025.
6
- //
7
-
8
1
  import SwiftUI
9
2
 
10
3
  extension View {
@@ -1,10 +1,3 @@
1
- //
2
- // View+applyWidgetURL.swift
3
- //
4
- //
5
- // Created by Artur Bilski on 05/08/2025.
6
- //
7
-
8
1
  import SwiftUI
9
2
 
10
3
  private let cachedScheme: String? = {
@@ -23,7 +16,9 @@ extension View {
23
16
  @ViewBuilder
24
17
  func applyWidgetURL(from urlString: String?) -> some View {
25
18
  applyIfPresent(urlString) { view, string in
26
- applyIfPresent(cachedScheme) { view, scheme in view.widgetURL(URL(string: scheme + "://" + string)) }
19
+ applyIfPresent(cachedScheme) { view, scheme in
20
+ view.widgetURL(URL(string: scheme + "://" + string))
21
+ }
27
22
  }
28
23
  }
29
24
  }
@@ -1,9 +1,3 @@
1
- //
2
- // ViewHelpers.swift
3
- //
4
- //
5
- // Created by Artur Bilski on 04/08/2025.
6
- //
7
1
  import SwiftUI
8
2
 
9
3
  func resizableImage(imageName: String) -> some View {
@@ -11,3 +5,15 @@ func resizableImage(imageName: String) -> some View {
11
5
  .resizable()
12
6
  .scaledToFit()
13
7
  }
8
+
9
+ 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
+ }
18
+ }
19
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "expo-live-activity",
3
- "version": "0.4.0",
3
+ "version": "0.4.1-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",
@@ -15,7 +15,8 @@
15
15
  "prepare": "expo-module prepare && rm .eslintrc.js",
16
16
  "prepublishOnly": "expo-module prepublishOnly",
17
17
  "expo-module": "expo-module",
18
- "open:ios": "xed example/ios"
18
+ "open:ios": "xed example/ios",
19
+ "typecheck": "tsc"
19
20
  },
20
21
  "keywords": [
21
22
  "react-native",
@@ -29,10 +30,7 @@
29
30
  "Anna Olak <anna.olak@swmansion.com> (https://github.com/anna1901)",
30
31
  "Artur Bilski <artur.bilski@swmansion.com> (https://github.com/arthwood)"
31
32
  ],
32
- "repository": {
33
- "type": "git",
34
- "url": "git+https://github.com/software-mansion-labs/expo-live-activity.git"
35
- },
33
+ "repository": "https://github.com/software-mansion-labs/expo-live-activity",
36
34
  "bugs": {
37
35
  "url": "https://github.com/software-mansion-labs/expo-live-activity/issues"
38
36
  },
@@ -22,7 +22,7 @@ const withWidgetsAndLiveActivities = (config, props) => {
22
22
  },
23
23
  };
24
24
  config = (0, config_plugins_1.withPlugins)(config, [
25
- [withPlist_1.default, { targetName }],
25
+ withPlist_1.default,
26
26
  [
27
27
  withXcode_1.withXcode,
28
28
  {
@@ -1,5 +1,3 @@
1
1
  import { ConfigPlugin } from '@expo/config-plugins';
2
- declare const withPlist: ConfigPlugin<{
3
- targetName: string;
4
- }>;
2
+ declare const withPlist: ConfigPlugin;
5
3
  export default withPlist;
@@ -1,25 +1,10 @@
1
1
  "use strict";
2
- var __importDefault = (this && this.__importDefault) || function (mod) {
3
- return (mod && mod.__esModule) ? mod : { "default": mod };
4
- };
5
2
  Object.defineProperty(exports, "__esModule", { value: true });
6
3
  const config_plugins_1 = require("@expo/config-plugins");
7
- const plist_1 = __importDefault(require("@expo/plist"));
8
- const fs_1 = require("fs");
9
- const path_1 = require("path");
10
- const withPlist = (expoConfig, { targetName }) => (0, config_plugins_1.withInfoPlist)(expoConfig, (plistConfig) => {
4
+ const withPlist = (expoConfig) => (0, config_plugins_1.withInfoPlist)(expoConfig, (plistConfig) => {
11
5
  const scheme = typeof expoConfig.scheme === 'string' ? expoConfig.scheme : expoConfig.ios?.bundleIdentifier;
12
- if (scheme) {
13
- const targetPath = (0, path_1.join)(plistConfig.modRequest.platformProjectRoot, targetName);
14
- const filePath = (0, path_1.join)(targetPath, 'Info.plist');
15
- const content = plist_1.default.parse((0, fs_1.readFileSync)(filePath, 'utf8'));
16
- content.CFBundleURLTypes = [
17
- {
18
- CFBundleURLSchemes: [scheme],
19
- },
20
- ];
21
- (0, fs_1.writeFileSync)(filePath, plist_1.default.build(content));
22
- }
6
+ if (scheme)
7
+ plistConfig.modResults.CFBundleURLTypes = [{ CFBundleURLSchemes: [scheme] }];
23
8
  return plistConfig;
24
9
  });
25
10
  exports.default = withPlist;
@@ -22,7 +22,7 @@ const withWidgetsAndLiveActivities: LiveActivityConfigPlugin = (config, props) =
22
22
  }
23
23
 
24
24
  config = withPlugins(config, [
25
- [withPlist, { targetName }],
25
+ withPlist,
26
26
  [
27
27
  withXcode,
28
28
  {
@@ -1,26 +1,9 @@
1
- import { ConfigPlugin, InfoPlist, withInfoPlist } from '@expo/config-plugins'
2
- import plist from '@expo/plist'
3
- import { readFileSync, writeFileSync } from 'fs'
4
- import { join as joinPath } from 'path'
1
+ import { ConfigPlugin, withInfoPlist } from '@expo/config-plugins'
5
2
 
6
- const withPlist: ConfigPlugin<{ targetName: string }> = (expoConfig, { targetName }) =>
3
+ const withPlist: ConfigPlugin = (expoConfig) =>
7
4
  withInfoPlist(expoConfig, (plistConfig) => {
8
5
  const scheme = typeof expoConfig.scheme === 'string' ? expoConfig.scheme : expoConfig.ios?.bundleIdentifier
9
-
10
- if (scheme) {
11
- const targetPath = joinPath(plistConfig.modRequest.platformProjectRoot, targetName)
12
- const filePath = joinPath(targetPath, 'Info.plist')
13
- const content = plist.parse(readFileSync(filePath, 'utf8')) as InfoPlist
14
-
15
- content.CFBundleURLTypes = [
16
- {
17
- CFBundleURLSchemes: [scheme],
18
- },
19
- ]
20
-
21
- writeFileSync(filePath, plist.build(content))
22
- }
23
-
6
+ if (scheme) plistConfig.modResults.CFBundleURLTypes = [{ CFBundleURLSchemes: [scheme] }]
24
7
  return plistConfig
25
8
  })
26
9
 
package/src/index.ts CHANGED
@@ -34,6 +34,21 @@ export type NativeLiveActivityState = {
34
34
  dynamicIslandImageName?: string
35
35
  }
36
36
 
37
+ export type Padding =
38
+ | {
39
+ top?: number
40
+ bottom?: number
41
+ left?: number
42
+ right?: number
43
+ vertical?: number
44
+ horizontal?: number
45
+ }
46
+ | number
47
+
48
+ export type ImagePosition = 'left' | 'right'
49
+
50
+ export type ImageSize = 'fullHeight' | 'default'
51
+
37
52
  export type LiveActivityConfig = {
38
53
  backgroundColor?: string
39
54
  titleColor?: string
@@ -42,6 +57,9 @@ export type LiveActivityConfig = {
42
57
  progressViewLabelColor?: string
43
58
  deepLinkUrl?: string
44
59
  timerType?: DynamicIslandTimerType
60
+ padding?: Padding
61
+ imagePosition?: ImagePosition
62
+ imageSize?: ImageSize
45
63
  }
46
64
 
47
65
  export type ActivityTokenReceivedEvent = {
@@ -82,7 +100,17 @@ function assertIOS(name: string) {
82
100
  * @returns {string} The identifier of the started activity or undefined if creating live activity failed.
83
101
  */
84
102
  export function startActivity(state: LiveActivityState, config?: LiveActivityConfig): Voidable<string> {
85
- if (assertIOS('startActivity')) return ExpoLiveActivityModule.startActivity(state, config)
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
+ if (assertIOS('startActivity')) return ExpoLiveActivityModule.startActivity(state, normalizeConfig(config))
86
114
  }
87
115
 
88
116
  /**