@idealyst/live-activity 1.2.114
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/android/build.gradle +39 -0
- package/android/src/main/AndroidManifest.xml +7 -0
- package/android/src/main/java/io/idealyst/liveactivity/IdealystLiveActivityModule.kt +299 -0
- package/android/src/main/java/io/idealyst/liveactivity/IdealystLiveActivityPackage.kt +16 -0
- package/android/src/main/java/io/idealyst/liveactivity/LiveUpdateNotification.kt +265 -0
- package/idealyst-live-activity.podspec +22 -0
- package/ios/IdealystLiveActivity-Bridging-Header.h +2 -0
- package/ios/IdealystLiveActivity.mm +55 -0
- package/ios/IdealystLiveActivity.swift +571 -0
- package/ios/Templates/ActivityAttributes.swift +66 -0
- package/ios/Templates/DeliveryActivityView.swift +143 -0
- package/ios/Templates/IdealystActivityBundle.swift +18 -0
- package/ios/Templates/MediaActivityView.swift +124 -0
- package/ios/Templates/ProgressActivityView.swift +164 -0
- package/ios/Templates/TimerActivityView.swift +110 -0
- package/package.json +80 -0
- package/src/NativeLiveActivitySpec.ts +49 -0
- package/src/activity/activity.native.ts +198 -0
- package/src/activity/activity.web.ts +78 -0
- package/src/activity/useLiveActivity.ts +267 -0
- package/src/constants.ts +39 -0
- package/src/errors.ts +57 -0
- package/src/index.native.ts +59 -0
- package/src/index.ts +61 -0
- package/src/index.web.ts +2 -0
- package/src/templates/presets.ts +91 -0
- package/src/templates/types.ts +14 -0
- package/src/types.ts +343 -0
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
// ============================================================================
|
|
2
|
+
// Native Live Activity Implementation
|
|
3
|
+
// Bridges to the IdealystLiveActivity TurboModule (iOS: ActivityKit, Android: ProgressStyle).
|
|
4
|
+
// ============================================================================
|
|
5
|
+
|
|
6
|
+
import { NativeEventEmitter, Platform } from 'react-native';
|
|
7
|
+
import type {
|
|
8
|
+
LiveActivityEventHandler,
|
|
9
|
+
LiveActivityEvent,
|
|
10
|
+
} from '../types';
|
|
11
|
+
import { createLiveActivityError, normalizeLiveActivityError } from '../errors';
|
|
12
|
+
import { LIVE_ACTIVITY_EVENT } from '../constants';
|
|
13
|
+
import NativeLiveActivity from '../NativeLiveActivitySpec';
|
|
14
|
+
|
|
15
|
+
function assertNativeModule(): void {
|
|
16
|
+
if (!NativeLiveActivity) {
|
|
17
|
+
throw createLiveActivityError(
|
|
18
|
+
'not_available',
|
|
19
|
+
'@idealyst/live-activity native module is not linked. Ensure the package is installed and your project is rebuilt.',
|
|
20
|
+
);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// ============================================================================
|
|
25
|
+
// Availability
|
|
26
|
+
// ============================================================================
|
|
27
|
+
|
|
28
|
+
export async function checkAvailability(): Promise<{
|
|
29
|
+
supported: boolean;
|
|
30
|
+
enabled: boolean;
|
|
31
|
+
}> {
|
|
32
|
+
if (!NativeLiveActivity) {
|
|
33
|
+
return { supported: false, enabled: false };
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
try {
|
|
37
|
+
const supported = NativeLiveActivity.isSupported();
|
|
38
|
+
const enabled = supported ? await NativeLiveActivity.isEnabled() : false;
|
|
39
|
+
return { supported, enabled };
|
|
40
|
+
} catch {
|
|
41
|
+
return { supported: false, enabled: false };
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// ============================================================================
|
|
46
|
+
// Lifecycle
|
|
47
|
+
// ============================================================================
|
|
48
|
+
|
|
49
|
+
export async function start(
|
|
50
|
+
templateType: string,
|
|
51
|
+
attributesJson: string,
|
|
52
|
+
contentStateJson: string,
|
|
53
|
+
optionsJson: string,
|
|
54
|
+
): Promise<string> {
|
|
55
|
+
assertNativeModule();
|
|
56
|
+
|
|
57
|
+
try {
|
|
58
|
+
return await NativeLiveActivity.startActivity(
|
|
59
|
+
templateType,
|
|
60
|
+
attributesJson,
|
|
61
|
+
contentStateJson,
|
|
62
|
+
optionsJson,
|
|
63
|
+
);
|
|
64
|
+
} catch (error) {
|
|
65
|
+
throw normalizeLiveActivityError(error);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export async function update(
|
|
70
|
+
activityId: string,
|
|
71
|
+
contentStateJson: string,
|
|
72
|
+
alertConfigJson: string | null,
|
|
73
|
+
): Promise<void> {
|
|
74
|
+
assertNativeModule();
|
|
75
|
+
|
|
76
|
+
try {
|
|
77
|
+
await NativeLiveActivity.updateActivity(
|
|
78
|
+
activityId,
|
|
79
|
+
contentStateJson,
|
|
80
|
+
alertConfigJson,
|
|
81
|
+
);
|
|
82
|
+
} catch (error) {
|
|
83
|
+
throw normalizeLiveActivityError(error);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export async function end(
|
|
88
|
+
activityId: string,
|
|
89
|
+
finalContentStateJson: string | null,
|
|
90
|
+
dismissalPolicy: string,
|
|
91
|
+
dismissAfter: number | null,
|
|
92
|
+
): Promise<void> {
|
|
93
|
+
assertNativeModule();
|
|
94
|
+
|
|
95
|
+
try {
|
|
96
|
+
await NativeLiveActivity.endActivity(
|
|
97
|
+
activityId,
|
|
98
|
+
finalContentStateJson,
|
|
99
|
+
dismissalPolicy,
|
|
100
|
+
// TurboModule codegen requires non-optional number — use -1 as sentinel
|
|
101
|
+
dismissAfter ?? -1,
|
|
102
|
+
);
|
|
103
|
+
} catch (error) {
|
|
104
|
+
throw normalizeLiveActivityError(error);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export async function endAll(
|
|
109
|
+
dismissalPolicy: string,
|
|
110
|
+
dismissAfter: number | null,
|
|
111
|
+
): Promise<void> {
|
|
112
|
+
assertNativeModule();
|
|
113
|
+
|
|
114
|
+
try {
|
|
115
|
+
await NativeLiveActivity.endAllActivities(
|
|
116
|
+
dismissalPolicy,
|
|
117
|
+
dismissAfter ?? -1,
|
|
118
|
+
);
|
|
119
|
+
} catch (error) {
|
|
120
|
+
throw normalizeLiveActivityError(error);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// ============================================================================
|
|
125
|
+
// Queries
|
|
126
|
+
// ============================================================================
|
|
127
|
+
|
|
128
|
+
export async function getActivity(
|
|
129
|
+
activityId: string,
|
|
130
|
+
): Promise<string | null> {
|
|
131
|
+
assertNativeModule();
|
|
132
|
+
|
|
133
|
+
try {
|
|
134
|
+
return await NativeLiveActivity.getActivity(activityId);
|
|
135
|
+
} catch {
|
|
136
|
+
return null;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export async function listActivities(): Promise<string> {
|
|
141
|
+
assertNativeModule();
|
|
142
|
+
|
|
143
|
+
try {
|
|
144
|
+
return await NativeLiveActivity.listActivities();
|
|
145
|
+
} catch {
|
|
146
|
+
return '[]';
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
export async function getPushToken(
|
|
151
|
+
activityId: string,
|
|
152
|
+
): Promise<string | null> {
|
|
153
|
+
assertNativeModule();
|
|
154
|
+
|
|
155
|
+
try {
|
|
156
|
+
return await NativeLiveActivity.getPushToken(activityId);
|
|
157
|
+
} catch {
|
|
158
|
+
return null;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// ============================================================================
|
|
163
|
+
// Events
|
|
164
|
+
// ============================================================================
|
|
165
|
+
|
|
166
|
+
let emitter: NativeEventEmitter | null = null;
|
|
167
|
+
|
|
168
|
+
function getEmitter(): NativeEventEmitter {
|
|
169
|
+
if (!emitter) {
|
|
170
|
+
emitter = new NativeEventEmitter(NativeLiveActivity as any);
|
|
171
|
+
}
|
|
172
|
+
return emitter;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
export function addEventListener(
|
|
176
|
+
handler: LiveActivityEventHandler,
|
|
177
|
+
): () => void {
|
|
178
|
+
if (!NativeLiveActivity) {
|
|
179
|
+
return () => {};
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
const subscription = getEmitter().addListener(
|
|
183
|
+
LIVE_ACTIVITY_EVENT,
|
|
184
|
+
(rawEvent: unknown) => {
|
|
185
|
+
try {
|
|
186
|
+
const event =
|
|
187
|
+
typeof rawEvent === 'string'
|
|
188
|
+
? (JSON.parse(rawEvent) as LiveActivityEvent)
|
|
189
|
+
: (rawEvent as LiveActivityEvent);
|
|
190
|
+
handler(event);
|
|
191
|
+
} catch {
|
|
192
|
+
// Malformed event — ignore
|
|
193
|
+
}
|
|
194
|
+
},
|
|
195
|
+
);
|
|
196
|
+
|
|
197
|
+
return () => subscription.remove();
|
|
198
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
// ============================================================================
|
|
2
|
+
// Web Stub Implementation
|
|
3
|
+
// Live Activities are not supported on web — all functions return graceful defaults.
|
|
4
|
+
// ============================================================================
|
|
5
|
+
|
|
6
|
+
import type {
|
|
7
|
+
LiveActivityEventHandler,
|
|
8
|
+
LiveActivityEvent,
|
|
9
|
+
} from '../types';
|
|
10
|
+
import { createLiveActivityError } from '../errors';
|
|
11
|
+
|
|
12
|
+
const NOT_SUPPORTED_ERROR = createLiveActivityError(
|
|
13
|
+
'not_supported',
|
|
14
|
+
'Live Activities are not supported on web',
|
|
15
|
+
);
|
|
16
|
+
|
|
17
|
+
export async function checkAvailability(): Promise<{
|
|
18
|
+
supported: boolean;
|
|
19
|
+
enabled: boolean;
|
|
20
|
+
}> {
|
|
21
|
+
return { supported: false, enabled: false };
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export async function start(
|
|
25
|
+
_templateType: string,
|
|
26
|
+
_attributesJson: string,
|
|
27
|
+
_contentStateJson: string,
|
|
28
|
+
_optionsJson: string,
|
|
29
|
+
): Promise<string> {
|
|
30
|
+
throw NOT_SUPPORTED_ERROR;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export async function update(
|
|
34
|
+
_activityId: string,
|
|
35
|
+
_contentStateJson: string,
|
|
36
|
+
_alertConfigJson: string | null,
|
|
37
|
+
): Promise<void> {
|
|
38
|
+
throw NOT_SUPPORTED_ERROR;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export async function end(
|
|
42
|
+
_activityId: string,
|
|
43
|
+
_finalContentStateJson: string | null,
|
|
44
|
+
_dismissalPolicy: string,
|
|
45
|
+
_dismissAfter: number | null,
|
|
46
|
+
): Promise<void> {
|
|
47
|
+
throw NOT_SUPPORTED_ERROR;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export async function endAll(
|
|
51
|
+
_dismissalPolicy: string,
|
|
52
|
+
_dismissAfter: number | null,
|
|
53
|
+
): Promise<void> {
|
|
54
|
+
throw NOT_SUPPORTED_ERROR;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export async function getActivity(
|
|
58
|
+
_activityId: string,
|
|
59
|
+
): Promise<string | null> {
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export async function listActivities(): Promise<string> {
|
|
64
|
+
return '[]';
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export async function getPushToken(
|
|
68
|
+
_activityId: string,
|
|
69
|
+
): Promise<string | null> {
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function addEventListener(
|
|
74
|
+
_handler: LiveActivityEventHandler,
|
|
75
|
+
): () => void {
|
|
76
|
+
// No-op on web
|
|
77
|
+
return () => {};
|
|
78
|
+
}
|
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
import { useState, useCallback, useEffect, useRef } from 'react';
|
|
2
|
+
import type {
|
|
3
|
+
UseLiveActivityOptions,
|
|
4
|
+
UseLiveActivityResult,
|
|
5
|
+
LiveActivityInfo,
|
|
6
|
+
LiveActivityError,
|
|
7
|
+
LiveActivityToken,
|
|
8
|
+
LiveActivityDeps,
|
|
9
|
+
StartActivityOptions,
|
|
10
|
+
UpdateActivityOptions,
|
|
11
|
+
EndActivityOptions,
|
|
12
|
+
TemplateType,
|
|
13
|
+
} from '../types';
|
|
14
|
+
import { normalizeLiveActivityError } from '../errors';
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Factory that creates a useLiveActivity hook bound to platform-specific functions.
|
|
18
|
+
* Each platform entry point calls this with the correct implementations.
|
|
19
|
+
*/
|
|
20
|
+
export function createUseLiveActivityHook(fns: LiveActivityDeps) {
|
|
21
|
+
return function useLiveActivity(
|
|
22
|
+
options: UseLiveActivityOptions = {},
|
|
23
|
+
): UseLiveActivityResult {
|
|
24
|
+
const { autoCheckAvailability = false, onEvent } = options;
|
|
25
|
+
|
|
26
|
+
const [isSupported, setIsSupported] = useState(false);
|
|
27
|
+
const [isEnabled, setIsEnabled] = useState<boolean | null>(null);
|
|
28
|
+
const [activities, setActivities] = useState<LiveActivityInfo[]>([]);
|
|
29
|
+
const [isLoading, setIsLoading] = useState(false);
|
|
30
|
+
const [error, setError] = useState<LiveActivityError | null>(null);
|
|
31
|
+
const mountedRef = useRef(true);
|
|
32
|
+
const initializedRef = useRef(false);
|
|
33
|
+
|
|
34
|
+
const currentActivity = activities.length > 0 ? activities[0] : null;
|
|
35
|
+
|
|
36
|
+
const checkAvailability = useCallback(async (): Promise<{
|
|
37
|
+
supported: boolean;
|
|
38
|
+
enabled: boolean;
|
|
39
|
+
}> => {
|
|
40
|
+
try {
|
|
41
|
+
const result = await fns.checkAvailability();
|
|
42
|
+
if (mountedRef.current) {
|
|
43
|
+
setIsSupported(result.supported);
|
|
44
|
+
setIsEnabled(result.enabled);
|
|
45
|
+
}
|
|
46
|
+
return result;
|
|
47
|
+
} catch (err) {
|
|
48
|
+
const liveError = normalizeLiveActivityError(err);
|
|
49
|
+
if (mountedRef.current) setError(liveError);
|
|
50
|
+
return { supported: false, enabled: false };
|
|
51
|
+
}
|
|
52
|
+
}, []);
|
|
53
|
+
|
|
54
|
+
const refreshActivities = useCallback(async () => {
|
|
55
|
+
try {
|
|
56
|
+
const json = await fns.listActivities();
|
|
57
|
+
const list: LiveActivityInfo[] = JSON.parse(json);
|
|
58
|
+
if (mountedRef.current) {
|
|
59
|
+
setActivities(list);
|
|
60
|
+
}
|
|
61
|
+
} catch {
|
|
62
|
+
// Best-effort refresh
|
|
63
|
+
}
|
|
64
|
+
}, []);
|
|
65
|
+
|
|
66
|
+
const start = useCallback(
|
|
67
|
+
async <T extends TemplateType>(
|
|
68
|
+
opts: StartActivityOptions<T>,
|
|
69
|
+
): Promise<LiveActivityInfo> => {
|
|
70
|
+
setIsLoading(true);
|
|
71
|
+
setError(null);
|
|
72
|
+
try {
|
|
73
|
+
const resultJson = await fns.start(
|
|
74
|
+
opts.templateType,
|
|
75
|
+
JSON.stringify(opts.attributes),
|
|
76
|
+
JSON.stringify(opts.contentState),
|
|
77
|
+
JSON.stringify({
|
|
78
|
+
enablePushUpdates: opts.enablePushUpdates,
|
|
79
|
+
ios: opts.ios,
|
|
80
|
+
android: opts.android,
|
|
81
|
+
}),
|
|
82
|
+
);
|
|
83
|
+
const info: LiveActivityInfo = JSON.parse(resultJson);
|
|
84
|
+
if (mountedRef.current) {
|
|
85
|
+
setActivities((prev) => [info, ...prev]);
|
|
86
|
+
}
|
|
87
|
+
return info;
|
|
88
|
+
} catch (err) {
|
|
89
|
+
const liveError = normalizeLiveActivityError(err);
|
|
90
|
+
if (mountedRef.current) setError(liveError);
|
|
91
|
+
throw liveError;
|
|
92
|
+
} finally {
|
|
93
|
+
if (mountedRef.current) setIsLoading(false);
|
|
94
|
+
}
|
|
95
|
+
},
|
|
96
|
+
[],
|
|
97
|
+
);
|
|
98
|
+
|
|
99
|
+
const update = useCallback(
|
|
100
|
+
async <T extends TemplateType>(
|
|
101
|
+
activityId: string,
|
|
102
|
+
opts: UpdateActivityOptions<T>,
|
|
103
|
+
): Promise<void> => {
|
|
104
|
+
setError(null);
|
|
105
|
+
try {
|
|
106
|
+
await fns.update(
|
|
107
|
+
activityId,
|
|
108
|
+
JSON.stringify(opts.contentState),
|
|
109
|
+
opts.alert ? JSON.stringify(opts.alert) : null,
|
|
110
|
+
);
|
|
111
|
+
await refreshActivities();
|
|
112
|
+
} catch (err) {
|
|
113
|
+
const liveError = normalizeLiveActivityError(err);
|
|
114
|
+
if (mountedRef.current) setError(liveError);
|
|
115
|
+
throw liveError;
|
|
116
|
+
}
|
|
117
|
+
},
|
|
118
|
+
[refreshActivities],
|
|
119
|
+
);
|
|
120
|
+
|
|
121
|
+
const endActivity = useCallback(
|
|
122
|
+
async (activityId: string, opts?: EndActivityOptions): Promise<void> => {
|
|
123
|
+
setError(null);
|
|
124
|
+
try {
|
|
125
|
+
await fns.end(
|
|
126
|
+
activityId,
|
|
127
|
+
opts?.finalContentState
|
|
128
|
+
? JSON.stringify(opts.finalContentState)
|
|
129
|
+
: null,
|
|
130
|
+
opts?.dismissalPolicy ?? 'default',
|
|
131
|
+
opts?.dismissAfter ?? null,
|
|
132
|
+
);
|
|
133
|
+
if (mountedRef.current) {
|
|
134
|
+
setActivities((prev) =>
|
|
135
|
+
prev.filter((a) => a.id !== activityId),
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
} catch (err) {
|
|
139
|
+
const liveError = normalizeLiveActivityError(err);
|
|
140
|
+
if (mountedRef.current) setError(liveError);
|
|
141
|
+
throw liveError;
|
|
142
|
+
}
|
|
143
|
+
},
|
|
144
|
+
[],
|
|
145
|
+
);
|
|
146
|
+
|
|
147
|
+
const endAllActivities = useCallback(
|
|
148
|
+
async (opts?: EndActivityOptions): Promise<void> => {
|
|
149
|
+
setError(null);
|
|
150
|
+
try {
|
|
151
|
+
await fns.endAll(
|
|
152
|
+
opts?.dismissalPolicy ?? 'default',
|
|
153
|
+
opts?.dismissAfter ?? null,
|
|
154
|
+
);
|
|
155
|
+
if (mountedRef.current) {
|
|
156
|
+
setActivities([]);
|
|
157
|
+
}
|
|
158
|
+
} catch (err) {
|
|
159
|
+
const liveError = normalizeLiveActivityError(err);
|
|
160
|
+
if (mountedRef.current) setError(liveError);
|
|
161
|
+
throw liveError;
|
|
162
|
+
}
|
|
163
|
+
},
|
|
164
|
+
[],
|
|
165
|
+
);
|
|
166
|
+
|
|
167
|
+
const getActivity = useCallback(
|
|
168
|
+
async (activityId: string): Promise<LiveActivityInfo | null> => {
|
|
169
|
+
try {
|
|
170
|
+
const json = await fns.getActivity(activityId);
|
|
171
|
+
if (!json) return null;
|
|
172
|
+
return JSON.parse(json) as LiveActivityInfo;
|
|
173
|
+
} catch {
|
|
174
|
+
return null;
|
|
175
|
+
}
|
|
176
|
+
},
|
|
177
|
+
[],
|
|
178
|
+
);
|
|
179
|
+
|
|
180
|
+
const listAllActivities = useCallback(async (): Promise<
|
|
181
|
+
LiveActivityInfo[]
|
|
182
|
+
> => {
|
|
183
|
+
try {
|
|
184
|
+
const json = await fns.listActivities();
|
|
185
|
+
const list: LiveActivityInfo[] = JSON.parse(json);
|
|
186
|
+
if (mountedRef.current) {
|
|
187
|
+
setActivities(list);
|
|
188
|
+
}
|
|
189
|
+
return list;
|
|
190
|
+
} catch {
|
|
191
|
+
return [];
|
|
192
|
+
}
|
|
193
|
+
}, []);
|
|
194
|
+
|
|
195
|
+
const getPushToken = useCallback(
|
|
196
|
+
async (activityId: string): Promise<LiveActivityToken | null> => {
|
|
197
|
+
try {
|
|
198
|
+
const json = await fns.getPushToken(activityId);
|
|
199
|
+
if (!json) return null;
|
|
200
|
+
return JSON.parse(json) as LiveActivityToken;
|
|
201
|
+
} catch {
|
|
202
|
+
return null;
|
|
203
|
+
}
|
|
204
|
+
},
|
|
205
|
+
[],
|
|
206
|
+
);
|
|
207
|
+
|
|
208
|
+
const clearError = useCallback(() => setError(null), []);
|
|
209
|
+
|
|
210
|
+
// Subscribe to native events
|
|
211
|
+
useEffect(() => {
|
|
212
|
+
mountedRef.current = true;
|
|
213
|
+
|
|
214
|
+
const unsubscribe = fns.addEventListener((event) => {
|
|
215
|
+
if (!mountedRef.current) return;
|
|
216
|
+
|
|
217
|
+
// Update local state based on event
|
|
218
|
+
if (event.type === 'ended') {
|
|
219
|
+
setActivities((prev) =>
|
|
220
|
+
prev.filter((a) => a.id !== event.activityId),
|
|
221
|
+
);
|
|
222
|
+
} else if (event.type === 'stale') {
|
|
223
|
+
setActivities((prev) =>
|
|
224
|
+
prev.map((a) =>
|
|
225
|
+
a.id === event.activityId ? { ...a, state: 'stale' } : a,
|
|
226
|
+
),
|
|
227
|
+
);
|
|
228
|
+
} else if (event.type === 'error' && event.payload?.error) {
|
|
229
|
+
setError(event.payload.error);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// Forward to user's handler
|
|
233
|
+
onEvent?.(event);
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
// Auto-check availability on mount
|
|
237
|
+
if (autoCheckAvailability && !initializedRef.current) {
|
|
238
|
+
initializedRef.current = true;
|
|
239
|
+
checkAvailability();
|
|
240
|
+
refreshActivities();
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
return () => {
|
|
244
|
+
mountedRef.current = false;
|
|
245
|
+
unsubscribe();
|
|
246
|
+
};
|
|
247
|
+
}, [autoCheckAvailability, onEvent, checkAvailability, refreshActivities]);
|
|
248
|
+
|
|
249
|
+
return {
|
|
250
|
+
isSupported,
|
|
251
|
+
isEnabled,
|
|
252
|
+
currentActivity,
|
|
253
|
+
activities,
|
|
254
|
+
isLoading,
|
|
255
|
+
error,
|
|
256
|
+
checkAvailability,
|
|
257
|
+
start,
|
|
258
|
+
update,
|
|
259
|
+
end: endActivity,
|
|
260
|
+
endAll: endAllActivities,
|
|
261
|
+
getActivity,
|
|
262
|
+
listActivities: listAllActivities,
|
|
263
|
+
getPushToken,
|
|
264
|
+
clearError,
|
|
265
|
+
};
|
|
266
|
+
};
|
|
267
|
+
}
|
package/src/constants.ts
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { TemplateType } from './types';
|
|
2
|
+
|
|
3
|
+
// ============================================================================
|
|
4
|
+
// Template Identifiers
|
|
5
|
+
// ============================================================================
|
|
6
|
+
|
|
7
|
+
/** Mapping of template types to their native identifiers. */
|
|
8
|
+
export const TEMPLATE_IDS: Record<TemplateType, string> = {
|
|
9
|
+
delivery: 'IdealystDeliveryActivity',
|
|
10
|
+
timer: 'IdealystTimerActivity',
|
|
11
|
+
media: 'IdealystMediaActivity',
|
|
12
|
+
progress: 'IdealystProgressActivity',
|
|
13
|
+
custom: 'custom',
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
// ============================================================================
|
|
17
|
+
// Android Notification Defaults
|
|
18
|
+
// ============================================================================
|
|
19
|
+
|
|
20
|
+
export const DEFAULT_CHANNEL_ID = 'idealyst_live_activity';
|
|
21
|
+
export const DEFAULT_CHANNEL_NAME = 'Live Activities';
|
|
22
|
+
export const DEFAULT_CHANNEL_DESCRIPTION = 'Real-time activity updates';
|
|
23
|
+
export const DEFAULT_SMALL_ICON = 'ic_notification';
|
|
24
|
+
|
|
25
|
+
// ============================================================================
|
|
26
|
+
// Limits
|
|
27
|
+
// ============================================================================
|
|
28
|
+
|
|
29
|
+
/** iOS allows a maximum of 5 concurrent Live Activities per app. */
|
|
30
|
+
export const MAX_CONCURRENT_ACTIVITIES_IOS = 5;
|
|
31
|
+
|
|
32
|
+
/** Android has no hard limit, but we enforce a reasonable default. */
|
|
33
|
+
export const MAX_CONCURRENT_ACTIVITIES_ANDROID = 10;
|
|
34
|
+
|
|
35
|
+
// ============================================================================
|
|
36
|
+
// Event Names
|
|
37
|
+
// ============================================================================
|
|
38
|
+
|
|
39
|
+
export const LIVE_ACTIVITY_EVENT = 'IdealystLiveActivityEvent';
|
package/src/errors.ts
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import type { LiveActivityError, LiveActivityErrorCode } from './types';
|
|
2
|
+
|
|
3
|
+
export function createLiveActivityError(
|
|
4
|
+
code: LiveActivityErrorCode,
|
|
5
|
+
message: string,
|
|
6
|
+
originalError?: unknown,
|
|
7
|
+
): LiveActivityError {
|
|
8
|
+
return { code, message, originalError };
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Normalize an unknown error into a LiveActivityError.
|
|
13
|
+
*/
|
|
14
|
+
export function normalizeLiveActivityError(error: unknown): LiveActivityError {
|
|
15
|
+
if (
|
|
16
|
+
error &&
|
|
17
|
+
typeof error === 'object' &&
|
|
18
|
+
'code' in error &&
|
|
19
|
+
'message' in error
|
|
20
|
+
) {
|
|
21
|
+
const typed = error as { code?: string; message?: string };
|
|
22
|
+
const code = isLiveActivityErrorCode(typed.code)
|
|
23
|
+
? typed.code
|
|
24
|
+
: 'unknown';
|
|
25
|
+
return createLiveActivityError(
|
|
26
|
+
code,
|
|
27
|
+
typed.message || 'An unknown Live Activity error occurred',
|
|
28
|
+
error,
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if (error instanceof Error) {
|
|
33
|
+
return createLiveActivityError('unknown', error.message, error);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return createLiveActivityError('unknown', String(error), error);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const VALID_CODES: LiveActivityErrorCode[] = [
|
|
40
|
+
'not_available',
|
|
41
|
+
'not_supported',
|
|
42
|
+
'permission_denied',
|
|
43
|
+
'start_failed',
|
|
44
|
+
'update_failed',
|
|
45
|
+
'end_failed',
|
|
46
|
+
'activity_not_found',
|
|
47
|
+
'template_not_found',
|
|
48
|
+
'invalid_attributes',
|
|
49
|
+
'too_many_activities',
|
|
50
|
+
'unknown',
|
|
51
|
+
];
|
|
52
|
+
|
|
53
|
+
function isLiveActivityErrorCode(
|
|
54
|
+
value: unknown,
|
|
55
|
+
): value is LiveActivityErrorCode {
|
|
56
|
+
return typeof value === 'string' && VALID_CODES.includes(value as LiveActivityErrorCode);
|
|
57
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
export * from './types';
|
|
2
|
+
export {
|
|
3
|
+
TEMPLATE_IDS,
|
|
4
|
+
DEFAULT_CHANNEL_ID,
|
|
5
|
+
DEFAULT_CHANNEL_NAME,
|
|
6
|
+
DEFAULT_CHANNEL_DESCRIPTION,
|
|
7
|
+
DEFAULT_SMALL_ICON,
|
|
8
|
+
MAX_CONCURRENT_ACTIVITIES_IOS,
|
|
9
|
+
MAX_CONCURRENT_ACTIVITIES_ANDROID,
|
|
10
|
+
LIVE_ACTIVITY_EVENT,
|
|
11
|
+
} from './constants';
|
|
12
|
+
export { createLiveActivityError, normalizeLiveActivityError } from './errors';
|
|
13
|
+
|
|
14
|
+
// Template presets
|
|
15
|
+
export {
|
|
16
|
+
deliveryActivity,
|
|
17
|
+
timerActivity,
|
|
18
|
+
mediaActivity,
|
|
19
|
+
progressActivity,
|
|
20
|
+
} from './templates/presets';
|
|
21
|
+
|
|
22
|
+
// Activity functions — native implementations
|
|
23
|
+
export {
|
|
24
|
+
checkAvailability,
|
|
25
|
+
start,
|
|
26
|
+
update,
|
|
27
|
+
end,
|
|
28
|
+
endAll,
|
|
29
|
+
getActivity,
|
|
30
|
+
listActivities,
|
|
31
|
+
getPushToken,
|
|
32
|
+
addEventListener,
|
|
33
|
+
} from './activity/activity.native';
|
|
34
|
+
|
|
35
|
+
// Hook — bound to native implementations
|
|
36
|
+
import { createUseLiveActivityHook } from './activity/useLiveActivity';
|
|
37
|
+
import {
|
|
38
|
+
checkAvailability,
|
|
39
|
+
start,
|
|
40
|
+
update,
|
|
41
|
+
end,
|
|
42
|
+
endAll,
|
|
43
|
+
getActivity,
|
|
44
|
+
listActivities,
|
|
45
|
+
getPushToken,
|
|
46
|
+
addEventListener,
|
|
47
|
+
} from './activity/activity.native';
|
|
48
|
+
|
|
49
|
+
export const useLiveActivity = createUseLiveActivityHook({
|
|
50
|
+
checkAvailability,
|
|
51
|
+
start,
|
|
52
|
+
update,
|
|
53
|
+
end,
|
|
54
|
+
endAll,
|
|
55
|
+
getActivity,
|
|
56
|
+
listActivities,
|
|
57
|
+
getPushToken,
|
|
58
|
+
addEventListener,
|
|
59
|
+
});
|