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 +2 -0
- package/README.md +40 -29
- package/build/index.d.ts +13 -0
- package/build/index.d.ts.map +1 -1
- package/build/index.js +10 -1
- package/build/index.js.map +1 -1
- package/ios/Data+download.swift +0 -7
- package/ios/ExpoLiveActivityModule.swift +64 -25
- package/ios/Helpers.swift +4 -10
- package/ios/LiveActivityAttributes.swift +14 -8
- package/ios/LiveActivityExceptions.swift +25 -0
- package/ios-files/Color+hex.swift +8 -8
- package/ios-files/Date+toTimerInterval.swift +1 -7
- package/ios-files/Image+dynamic.swift +0 -6
- package/ios-files/LiveActivityView.swift +66 -21
- package/ios-files/LiveActivityWidget.swift +19 -11
- package/ios-files/LiveActivityWidgetBundle.swift +0 -7
- package/ios-files/View+applyIfPresent.swift +0 -7
- package/ios-files/View+applyWidgetURL.swift +3 -8
- package/ios-files/ViewHelpers.swift +12 -6
- package/package.json +4 -6
- package/plugin/build/index.js +1 -1
- package/plugin/build/withPlist.d.ts +1 -3
- package/plugin/build/withPlist.js +3 -18
- package/plugin/src/index.ts +1 -1
- package/plugin/src/withPlist.ts +3 -20
- package/src/index.ts +29 -1
package/.swiftformat
ADDED
package/README.md
CHANGED
|
@@ -1,16 +1,18 @@
|
|
|
1
|
+

|
|
2
|
+
|
|
1
3
|
> [!WARNING]
|
|
2
|
-
> This library is in early development stage
|
|
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
|
|
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
|
|
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
|
|
13
|
-
- Listen and handle changes in push notification tokens associated with a
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
230
|
-
"subtitle": "Live
|
|
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
|
|
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
|
|
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
|
|
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 "
|
|
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;
|
package/build/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
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,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;
|
|
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.
|
package/build/index.js.map
CHANGED
|
@@ -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;
|
|
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"]}
|
package/ios/Data+download.swift
CHANGED
|
@@ -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(
|
|
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(
|
|
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
|
|
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") {
|
|
176
|
-
|
|
177
|
-
guard
|
|
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
|
|
212
|
-
|
|
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
|
|
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
|
|
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 {
|
|
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
|
|
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
|
-
|
|
11
|
-
|
|
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
|
-
|
|
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
|
|
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) &
|
|
23
|
-
green: Double((rgbValue >> 16) &
|
|
24
|
-
blue: Double((rgbValue >> 08) &
|
|
25
|
-
opacity: Double((rgbValue >> 00) &
|
|
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) &
|
|
31
|
-
green: Double((rgbValue >> 08) &
|
|
32
|
-
blue: Double((rgbValue >> 00) &
|
|
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
|
-
|
|
5
|
+
now ... max(now, Date(timeIntervalSince1970: miliseconds / 1000))
|
|
12
6
|
}
|
|
13
7
|
}
|
|
@@ -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
|
-
|
|
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
|
|
52
|
-
|
|
53
|
-
|
|
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
|
|
58
|
-
|
|
59
|
-
.
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
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(
|
|
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
|
-
|
|
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(
|
|
64
|
-
.
|
|
65
|
-
|
|
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
|
-
// 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
|
|
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.
|
|
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
|
},
|
package/plugin/build/index.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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;
|
package/plugin/src/index.ts
CHANGED
package/plugin/src/withPlist.ts
CHANGED
|
@@ -1,26 +1,9 @@
|
|
|
1
|
-
import { ConfigPlugin,
|
|
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
|
|
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
|
-
|
|
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
|
/**
|