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