@ng-native/expo 0.10.0 → 0.12.0

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.
@@ -62,7 +62,20 @@ export interface LiveActivityOptions {
62
62
  * rejects, goes to the `ErrorHandler`.
63
63
  */
64
64
  readonly onTaps?: (taps: readonly string[]) => void;
65
+ /**
66
+ * When what the activity shows is out of date, unless it is written again first: asked at the
67
+ * start and at each update, which replaces the date before it. iOS then draws the activity with
68
+ * its layout's `environment().isStale` true. Null, or no option, is never.
69
+ */
70
+ readonly staleDate?: () => Date | null | undefined;
65
71
  }
72
+ /**
73
+ * The token a server starts a Live Activity with over APNs, with the app not running: null until
74
+ * iOS issues one, then the latest. The app has one, for every kind of activity; the push names the
75
+ * kind. It needs iOS 17.2, and `enablePushNotifications` in the `expo-widgets` plugin's config. Call
76
+ * it in an injection context.
77
+ */
78
+ export declare function pushToStartToken(): Signal<string | null>;
66
79
  /** A Live Activity kept in step with a signal. */
67
80
  export interface LiveActivityRef {
68
81
  /** Whether an activity of this kind is running and following the signal. */
@@ -55,9 +55,20 @@ const STAND_IN = {
55
55
  /** A widget in Node: it draws nothing and holds no timeline, and `widget()` keeps it in step. */
56
56
  const WIDGET_STAND_IN = {
57
57
  updateSnapshot: () => { },
58
+ updateTimeline: () => { },
58
59
  getTimeline: async () => [],
59
60
  reload: () => { },
60
61
  };
62
+ const NO_TOKEN = signal(null).asReadonly();
63
+ /**
64
+ * The token a server starts a Live Activity with over APNs, with the app not running: null until
65
+ * iOS issues one, then the latest. The app has one, for every kind of activity; the push names the
66
+ * kind. It needs iOS 17.2, and `enablePushNotifications` in the `expo-widgets` plugin's config. Call
67
+ * it in an injection context.
68
+ */
69
+ export function pushToStartToken() {
70
+ return inject(WIDGET_EVENTS).pushToStartToken?.() ?? NO_TOKEN;
71
+ }
61
72
  /**
62
73
  * Keeps a Live Activity of the factory's kind in step with `props`: once started, every change to the
63
74
  * signal updates it. One left running from before the app started is picked up rather than started
@@ -68,6 +79,7 @@ const WIDGET_STAND_IN = {
68
79
  export function liveActivity(factory, props, options = {}) {
69
80
  const errors = inject(ErrorHandler);
70
81
  const { onTaps } = options;
82
+ const staleDate = () => options.staleDate?.() ?? undefined;
71
83
  const active = signal(false, /* @ts-ignore */
72
84
  ...(ngDevMode ? [{ debugName: "active" }] : /* istanbul ignore next */ []));
73
85
  const id = signal(null, /* @ts-ignore */
@@ -107,6 +119,8 @@ export function liveActivity(factory, props, options = {}) {
107
119
  tokenSubscription = activity.addPushTokenListener((event) => pushToken.set(event.pushToken));
108
120
  activity.getPushToken().then((token) => current === activity && token !== null && pushToken.set(token), () => { });
109
121
  };
122
+ /** Rejects where the stale date cannot be had, as where the write fails. */
123
+ const write = async (activity, value) => activity.update(value, staleDate());
110
124
  const existing = running()[0];
111
125
  if (existing)
112
126
  adopt(existing);
@@ -114,7 +128,7 @@ export function liveActivity(factory, props, options = {}) {
114
128
  const next = props();
115
129
  const activity = current;
116
130
  if (activity)
117
- untracked(() => activity.update(next).catch(report));
131
+ untracked(() => write(activity, next).catch(report));
118
132
  });
119
133
  const stopTaps = onTaps && listenForTaps(onTaps);
120
134
  /**
@@ -157,14 +171,14 @@ export function liveActivity(factory, props, options = {}) {
157
171
  const value = untracked(props);
158
172
  try {
159
173
  const already = running()[0];
160
- const activity = already ?? factory.start(value, options.url);
174
+ const activity = already ?? factory.start(value, options.url, staleDate());
161
175
  if (!activity.getId()) {
162
176
  error.set(new Error('Live Activities are only on iOS.'));
163
177
  return false;
164
178
  }
165
179
  adopt(activity);
166
180
  if (already)
167
- already.update(value).catch(report);
181
+ write(already, value).catch(report);
168
182
  return true;
169
183
  }
170
184
  catch (failure) {
@@ -1 +1 @@
1
- {"version":3,"file":"live-activity.js","sourceRoot":"","sources":["../src/live-activity.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * iOS Live Activities and home-screen widgets, through `expo-widgets`: `createLiveActivity` and\n * `createWidget` draw one from an Angular component's template, and `liveActivity` keeps an\n * activity in step with a signal, on the lock screen and in the Dynamic Island. Off iOS,\n * `expo-widgets` starts a stand-in with no id, and `start()` answers false.\n */\nimport {\n DestroyRef,\n ErrorHandler,\n effect,\n inject,\n signal,\n untracked,\n type InputSignal,\n type Signal,\n type Type,\n} from '@angular/core';\nimport { expoModule, isolated } from './native.ts';\nimport { WIDGET_EVENTS, type WidgetTap } from './widget.ts';\n\n/** A widget layout: an Angular component whose `props` input the layout draws from. */\nexport type WidgetLayout<T extends object> = Type<{ readonly props: InputSignal<T> }>;\n\n/**\n * A Live Activity drawn by `layout`, an Angular component, as `expo-widgets`' `createLiveActivity`\n * makes one. `name` matches the activity's name in the `expo-widgets` plugin's config.\n *\n * ```ts\n * export const scoreActivity = createLiveActivity('Score', ScoreLayout);\n * ```\n *\n * `@ng-native/metro`'s transformer compiles the layout's template, at build time, to the source the\n * widget extension evaluates, and drops the class, so the app never runs it: the extension does,\n * with no Angular. So the class holds only its inputs, members holding a modifier from\n * `@expo/ui/swift-ui/modifiers`, and members holding a literal, and its template is inline.\n */\nexport function createLiveActivity<T extends object>(\n name: string,\n layout: WidgetLayout<T>,\n): import('expo-widgets').LiveActivityFactory<T> {\n const expo = widgets();\n if (!expo) return STAND_IN as never;\n return expo.createLiveActivity<T>(name, compiled('createLiveActivity', name, layout));\n}\n\n/**\n * A home-screen widget drawn by `layout`, as `expo-widgets`' `createWidget` makes one. The layout\n * reads what the widget is drawn in, such as its `widgetFamily`, from an `environment` input beside\n * `props`. Compiled as `createLiveActivity`'s layout is.\n */\nexport function createWidget<T extends object>(\n name: string,\n layout: WidgetLayout<T>,\n): import('expo-widgets').Widget<T> {\n const expo = widgets();\n if (!expo) return WIDGET_STAND_IN as never;\n return expo.createWidget<T>(name, compiled('createWidget', name, layout));\n}\n\n/** `expo-widgets`, which answers with stand-ins itself where there are no widgets; null in Node. */\nfunction widgets() {\n return expoModule(\n 'expo-widgets',\n () => require('expo-widgets') as typeof import('expo-widgets'),\n ['ios', 'android', 'web'],\n );\n}\n\n/** The layout's source, which the transformer put in place of the class. */\nfunction compiled(call: string, name: string, layout: unknown): never {\n if (typeof layout === 'string') return layout as never;\n throw new Error(\n `${call}('${name}', ...) takes a layout @ng-native/metro's transformer compiles, and it did not run on this file.`,\n );\n}\n\n/** What there is in Node, where no widget can be drawn: an activity that never starts. */\nconst STAND_IN = {\n start: () => ({ getId: () => '' }),\n getInstances: () => [],\n};\n\n/** A widget in Node: it draws nothing and holds no timeline, and `widget()` keeps it in step. */\nconst WIDGET_STAND_IN = {\n updateSnapshot: () => {},\n getTimeline: async () => [],\n reload: () => {},\n};\n\n/**\n * How an ended activity leaves the lock screen: `'default'` keeps it, with its final props, for up\n * to four hours; `'immediate'` removes it; `{ after }` keeps it until then.\n */\nexport type LiveActivityDismissal = 'default' | 'immediate' | { readonly after: Date };\n\ninterface Subscription {\n remove(): void;\n}\n\n/** One running activity, as `expo-widgets`' `LiveActivity` is. */\nexport interface NativeLiveActivity<T extends object> {\n getId(): string;\n update(props: T, staleDate?: Date): Promise<void>;\n end(dismissalPolicy?: LiveActivityDismissal, props?: T, contentDate?: Date): Promise<void>;\n getPushToken(): Promise<string | null>;\n addPushTokenListener(listener: (event: { pushToken: string }) => void): Subscription;\n}\n\n/** What `createLiveActivity` answers: starts activities of one kind, and lists the running ones. */\nexport interface LiveActivityFactory<T extends object> {\n start(props: T, url?: string, staleDate?: Date): NativeLiveActivity<T>;\n getInstances(): NativeLiveActivity<T>[];\n}\n\nexport interface LiveActivityOptions {\n /**\n * Called with the target of a `ui-button` tapped on a running activity of this kind, as `widget`\n * hands over a widget's. iOS runs the tap in the app, starting it in the background if it has\n * to; a tap that starts the app is held until this is listening. A handler that throws, or\n * rejects, goes to the `ErrorHandler`.\n */\n readonly onTaps?: (taps: readonly string[]) => void;\n}\n\n/** A Live Activity kept in step with a signal. */\nexport interface LiveActivityRef {\n /** Whether an activity of this kind is running and following the signal. */\n readonly active: Signal<boolean>;\n /** The running activity's id, or null. */\n readonly id: Signal<string | null>;\n /** The token a server sends the activity's updates to over APNs, once iOS issues one. */\n readonly pushToken: Signal<string | null>;\n /**\n * Why the last start or update did not happen. A start the system refuses, as when Live\n * Activities are turned off in Settings, is only kept here; a failed update also goes to the\n * `ErrorHandler`.\n */\n readonly error: Signal<unknown>;\n /**\n * Shows the activity with the signal's value, or picks up one already running. Answers whether it\n * is running now; `url` opens the app at that link when the activity is tapped.\n */\n start(options?: { url?: string }): boolean;\n /** Ends every activity of this kind with the signal's value. */\n end(dismissal?: LiveActivityDismissal): Promise<void>;\n}\n\n/**\n * Keeps a Live Activity of the factory's kind in step with `props`: once started, every change to the\n * signal updates it. One left running from before the app started is picked up rather than started\n * again, since iOS caps how many an app runs. Destroying the injector stops the updates and leaves\n * the activity running, as it outlives the app. Taps on its buttons go to `onTaps`. Call it in an\n * injection context.\n */\nexport function liveActivity<T extends object>(\n factory: LiveActivityFactory<T>,\n props: Signal<T>,\n options: LiveActivityOptions = {},\n): LiveActivityRef {\n const errors = inject(ErrorHandler);\n const { onTaps } = options;\n const active = signal(false);\n const id = signal<string | null>(null);\n const pushToken = signal<string | null>(null);\n const error = signal<unknown>(null);\n let current: NativeLiveActivity<T> | null = null;\n let tokenSubscription: Subscription | null = null;\n\n const report = (failure: unknown) => {\n error.set(failure);\n errors.handleError(failure);\n };\n\n const running = (): NativeLiveActivity<T>[] => {\n try {\n return factory.getInstances();\n } catch (failure) {\n error.set(failure);\n return [];\n }\n };\n\n const forget = () => {\n tokenSubscription?.remove();\n tokenSubscription = null;\n current = null;\n active.set(false);\n id.set(null);\n pushToken.set(null);\n };\n\n const adopt = (activity: NativeLiveActivity<T>) => {\n current = activity;\n active.set(true);\n id.set(activity.getId());\n error.set(null);\n tokenSubscription = activity.addPushTokenListener((event) => pushToken.set(event.pushToken));\n activity.getPushToken().then(\n (token) => current === activity && token !== null && pushToken.set(token),\n () => {},\n );\n };\n\n const existing = running()[0];\n if (existing) adopt(existing);\n\n effect(() => {\n const next = props();\n const activity = current;\n if (activity) untracked(() => activity.update(next).catch(report));\n });\n\n const stopTaps = onTaps && listenForTaps(onTaps);\n\n /**\n * Every widget's and activity's taps arrive here: an activity's names it by its id. Those made\n * before this was listening are taken once it is, so one that also arrives as it happens is\n * handed over once.\n */\n function listenForTaps(hand: (taps: readonly string[]) => void): () => void {\n const events = inject(WIDGET_EVENTS);\n const key = (tap: WidgetTap) => `${tap.source} ${tap.target} ${tap.timestamp}`;\n const taken = new Set<string>();\n const handOver = (targets: readonly string[]) =>\n isolated(\n () => hand(targets),\n (failure) => errors.handleError(failure),\n );\n const stop = events.onTap((tap) => {\n if (taken.delete(key(tap))) return;\n if (running().some((activity) => activity.getId() === tap.source)) handOver([tap.target]);\n });\n const held = events.takeHeld?.(running().map((activity) => activity.getId())) ?? [];\n for (const tap of held) taken.add(key(tap));\n // Not yet: the caller's own fields may not be set while this one is.\n if (held.length) queueMicrotask(() => handOver(held.map((tap) => tap.target)));\n return stop;\n }\n\n inject(DestroyRef).onDestroy(() => {\n stopTaps?.();\n tokenSubscription?.remove();\n tokenSubscription = null;\n });\n\n return {\n active: active.asReadonly(),\n id: id.asReadonly(),\n pushToken: pushToken.asReadonly(),\n error: error.asReadonly(),\n start(options = {}) {\n if (current) return true;\n const value = untracked(props);\n try {\n const already = running()[0];\n const activity = already ?? factory.start(value, options.url);\n if (!activity.getId()) {\n error.set(new Error('Live Activities are only on iOS.'));\n return false;\n }\n adopt(activity);\n if (already) already.update(value).catch(report);\n return true;\n } catch (failure) {\n error.set(failure);\n return false;\n }\n },\n async end(dismissal = 'default') {\n const value = untracked(props);\n const activities = new Set(running());\n if (current) activities.add(current);\n forget();\n await Promise.all([...activities].map((activity) => activity.end(dismissal, value))).catch(\n report,\n );\n },\n };\n}\n"]}
1
+ {"version":3,"file":"live-activity.js","sourceRoot":"","sources":["../src/live-activity.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * iOS Live Activities and home-screen widgets, through `expo-widgets`: `createLiveActivity` and\n * `createWidget` draw one from an Angular component's template, and `liveActivity` keeps an\n * activity in step with a signal, on the lock screen and in the Dynamic Island. Off iOS,\n * `expo-widgets` starts a stand-in with no id, and `start()` answers false.\n */\nimport {\n DestroyRef,\n ErrorHandler,\n effect,\n inject,\n signal,\n untracked,\n type InputSignal,\n type Signal,\n type Type,\n} from '@angular/core';\nimport { expoModule, isolated } from './native.ts';\nimport { WIDGET_EVENTS, type WidgetTap } from './widget.ts';\n\n/** A widget layout: an Angular component whose `props` input the layout draws from. */\nexport type WidgetLayout<T extends object> = Type<{ readonly props: InputSignal<T> }>;\n\n/**\n * A Live Activity drawn by `layout`, an Angular component, as `expo-widgets`' `createLiveActivity`\n * makes one. `name` matches the activity's name in the `expo-widgets` plugin's config.\n *\n * ```ts\n * export const scoreActivity = createLiveActivity('Score', ScoreLayout);\n * ```\n *\n * `@ng-native/metro`'s transformer compiles the layout's template, at build time, to the source the\n * widget extension evaluates, and drops the class, so the app never runs it: the extension does,\n * with no Angular. So the class holds only its inputs, members holding a modifier from\n * `@expo/ui/swift-ui/modifiers`, and members holding a literal, and its template is inline.\n */\nexport function createLiveActivity<T extends object>(\n name: string,\n layout: WidgetLayout<T>,\n): import('expo-widgets').LiveActivityFactory<T> {\n const expo = widgets();\n if (!expo) return STAND_IN as never;\n return expo.createLiveActivity<T>(name, compiled('createLiveActivity', name, layout));\n}\n\n/**\n * A home-screen widget drawn by `layout`, as `expo-widgets`' `createWidget` makes one. The layout\n * reads what the widget is drawn in, such as its `widgetFamily`, from an `environment` input beside\n * `props`. Compiled as `createLiveActivity`'s layout is.\n */\nexport function createWidget<T extends object>(\n name: string,\n layout: WidgetLayout<T>,\n): import('expo-widgets').Widget<T> {\n const expo = widgets();\n if (!expo) return WIDGET_STAND_IN as never;\n return expo.createWidget<T>(name, compiled('createWidget', name, layout));\n}\n\n/** `expo-widgets`, which answers with stand-ins itself where there are no widgets; null in Node. */\nfunction widgets() {\n return expoModule(\n 'expo-widgets',\n () => require('expo-widgets') as typeof import('expo-widgets'),\n ['ios', 'android', 'web'],\n );\n}\n\n/** The layout's source, which the transformer put in place of the class. */\nfunction compiled(call: string, name: string, layout: unknown): never {\n if (typeof layout === 'string') return layout as never;\n throw new Error(\n `${call}('${name}', ...) takes a layout @ng-native/metro's transformer compiles, and it did not run on this file.`,\n );\n}\n\n/** What there is in Node, where no widget can be drawn: an activity that never starts. */\nconst STAND_IN = {\n start: () => ({ getId: () => '' }),\n getInstances: () => [],\n};\n\n/** A widget in Node: it draws nothing and holds no timeline, and `widget()` keeps it in step. */\nconst WIDGET_STAND_IN = {\n updateSnapshot: () => {},\n updateTimeline: () => {},\n getTimeline: async () => [],\n reload: () => {},\n};\n\n/**\n * How an ended activity leaves the lock screen: `'default'` keeps it, with its final props, for up\n * to four hours; `'immediate'` removes it; `{ after }` keeps it until then.\n */\nexport type LiveActivityDismissal = 'default' | 'immediate' | { readonly after: Date };\n\ninterface Subscription {\n remove(): void;\n}\n\n/** One running activity, as `expo-widgets`' `LiveActivity` is. */\nexport interface NativeLiveActivity<T extends object> {\n getId(): string;\n update(props: T, staleDate?: Date): Promise<void>;\n end(dismissalPolicy?: LiveActivityDismissal, props?: T, contentDate?: Date): Promise<void>;\n getPushToken(): Promise<string | null>;\n addPushTokenListener(listener: (event: { pushToken: string }) => void): Subscription;\n}\n\n/** What `createLiveActivity` answers: starts activities of one kind, and lists the running ones. */\nexport interface LiveActivityFactory<T extends object> {\n start(props: T, url?: string, staleDate?: Date): NativeLiveActivity<T>;\n getInstances(): NativeLiveActivity<T>[];\n}\n\nexport interface LiveActivityOptions {\n /**\n * Called with the target of a `ui-button` tapped on a running activity of this kind, as `widget`\n * hands over a widget's. iOS runs the tap in the app, starting it in the background if it has\n * to; a tap that starts the app is held until this is listening. A handler that throws, or\n * rejects, goes to the `ErrorHandler`.\n */\n readonly onTaps?: (taps: readonly string[]) => void;\n /**\n * When what the activity shows is out of date, unless it is written again first: asked at the\n * start and at each update, which replaces the date before it. iOS then draws the activity with\n * its layout's `environment().isStale` true. Null, or no option, is never.\n */\n readonly staleDate?: () => Date | null | undefined;\n}\n\nconst NO_TOKEN = signal<string | null>(null).asReadonly();\n\n/**\n * The token a server starts a Live Activity with over APNs, with the app not running: null until\n * iOS issues one, then the latest. The app has one, for every kind of activity; the push names the\n * kind. It needs iOS 17.2, and `enablePushNotifications` in the `expo-widgets` plugin's config. Call\n * it in an injection context.\n */\nexport function pushToStartToken(): Signal<string | null> {\n return inject(WIDGET_EVENTS).pushToStartToken?.() ?? NO_TOKEN;\n}\n\n/** A Live Activity kept in step with a signal. */\nexport interface LiveActivityRef {\n /** Whether an activity of this kind is running and following the signal. */\n readonly active: Signal<boolean>;\n /** The running activity's id, or null. */\n readonly id: Signal<string | null>;\n /** The token a server sends the activity's updates to over APNs, once iOS issues one. */\n readonly pushToken: Signal<string | null>;\n /**\n * Why the last start or update did not happen. A start the system refuses, as when Live\n * Activities are turned off in Settings, is only kept here; a failed update also goes to the\n * `ErrorHandler`.\n */\n readonly error: Signal<unknown>;\n /**\n * Shows the activity with the signal's value, or picks up one already running. Answers whether it\n * is running now; `url` opens the app at that link when the activity is tapped.\n */\n start(options?: { url?: string }): boolean;\n /** Ends every activity of this kind with the signal's value. */\n end(dismissal?: LiveActivityDismissal): Promise<void>;\n}\n\n/**\n * Keeps a Live Activity of the factory's kind in step with `props`: once started, every change to the\n * signal updates it. One left running from before the app started is picked up rather than started\n * again, since iOS caps how many an app runs. Destroying the injector stops the updates and leaves\n * the activity running, as it outlives the app. Taps on its buttons go to `onTaps`. Call it in an\n * injection context.\n */\nexport function liveActivity<T extends object>(\n factory: LiveActivityFactory<T>,\n props: Signal<T>,\n options: LiveActivityOptions = {},\n): LiveActivityRef {\n const errors = inject(ErrorHandler);\n const { onTaps } = options;\n const staleDate = () => options.staleDate?.() ?? undefined;\n const active = signal(false);\n const id = signal<string | null>(null);\n const pushToken = signal<string | null>(null);\n const error = signal<unknown>(null);\n let current: NativeLiveActivity<T> | null = null;\n let tokenSubscription: Subscription | null = null;\n\n const report = (failure: unknown) => {\n error.set(failure);\n errors.handleError(failure);\n };\n\n const running = (): NativeLiveActivity<T>[] => {\n try {\n return factory.getInstances();\n } catch (failure) {\n error.set(failure);\n return [];\n }\n };\n\n const forget = () => {\n tokenSubscription?.remove();\n tokenSubscription = null;\n current = null;\n active.set(false);\n id.set(null);\n pushToken.set(null);\n };\n\n const adopt = (activity: NativeLiveActivity<T>) => {\n current = activity;\n active.set(true);\n id.set(activity.getId());\n error.set(null);\n tokenSubscription = activity.addPushTokenListener((event) => pushToken.set(event.pushToken));\n activity.getPushToken().then(\n (token) => current === activity && token !== null && pushToken.set(token),\n () => {},\n );\n };\n\n /** Rejects where the stale date cannot be had, as where the write fails. */\n const write = async (activity: NativeLiveActivity<T>, value: T) =>\n activity.update(value, staleDate());\n\n const existing = running()[0];\n if (existing) adopt(existing);\n\n effect(() => {\n const next = props();\n const activity = current;\n if (activity) untracked(() => write(activity, next).catch(report));\n });\n\n const stopTaps = onTaps && listenForTaps(onTaps);\n\n /**\n * Every widget's and activity's taps arrive here: an activity's names it by its id. Those made\n * before this was listening are taken once it is, so one that also arrives as it happens is\n * handed over once.\n */\n function listenForTaps(hand: (taps: readonly string[]) => void): () => void {\n const events = inject(WIDGET_EVENTS);\n const key = (tap: WidgetTap) => `${tap.source} ${tap.target} ${tap.timestamp}`;\n const taken = new Set<string>();\n const handOver = (targets: readonly string[]) =>\n isolated(\n () => hand(targets),\n (failure) => errors.handleError(failure),\n );\n const stop = events.onTap((tap) => {\n if (taken.delete(key(tap))) return;\n if (running().some((activity) => activity.getId() === tap.source)) handOver([tap.target]);\n });\n const held = events.takeHeld?.(running().map((activity) => activity.getId())) ?? [];\n for (const tap of held) taken.add(key(tap));\n // Not yet: the caller's own fields may not be set while this one is.\n if (held.length) queueMicrotask(() => handOver(held.map((tap) => tap.target)));\n return stop;\n }\n\n inject(DestroyRef).onDestroy(() => {\n stopTaps?.();\n tokenSubscription?.remove();\n tokenSubscription = null;\n });\n\n return {\n active: active.asReadonly(),\n id: id.asReadonly(),\n pushToken: pushToken.asReadonly(),\n error: error.asReadonly(),\n start(options = {}) {\n if (current) return true;\n const value = untracked(props);\n try {\n const already = running()[0];\n const activity = already ?? factory.start(value, options.url, staleDate());\n if (!activity.getId()) {\n error.set(new Error('Live Activities are only on iOS.'));\n return false;\n }\n adopt(activity);\n if (already) write(already, value).catch(report);\n return true;\n } catch (failure) {\n error.set(failure);\n return false;\n }\n },\n async end(dismissal = 'default') {\n const value = untracked(props);\n const activities = new Set(running());\n if (current) activities.add(current);\n forget();\n await Promise.all([...activities].map((activity) => activity.end(dismissal, value))).catch(\n report,\n );\n },\n };\n}\n"]}
package/dist/widget.d.ts CHANGED
@@ -20,6 +20,8 @@ export interface WidgetEntry<T extends object> {
20
20
  /** What `createWidget` answers. */
21
21
  export interface NativeWidget<T extends object> {
22
22
  updateSnapshot(props: T): void;
23
+ /** Replaces the timeline: each entry's props are shown from its date. */
24
+ updateTimeline?(entries: readonly WidgetEntry<T>[]): void;
23
25
  getTimeline(): Promise<readonly WidgetEntry<T>[]>;
24
26
  reload(): void;
25
27
  }
@@ -41,16 +43,27 @@ export interface WidgetEvents {
41
43
  * once. A tap can start the app, and is over before its JavaScript has loaded.
42
44
  */
43
45
  takeHeld?(sources: readonly string[]): readonly WidgetTap[];
46
+ /**
47
+ * The token a server starts a Live Activity with over APNs: null until iOS issues one. One signal
48
+ * for the whole app, since `expo-widgets` hands the token it has only to the first to listen.
49
+ */
50
+ pushToStartToken?(): Signal<string | null>;
44
51
  onForeground(listener: () => void): () => void;
45
52
  onBackground(listener: () => void): () => void;
46
53
  }
47
- export interface WidgetOptions {
54
+ export interface WidgetOptions<T extends object = object> {
48
55
  /**
49
56
  * Called with the targets of the buttons tapped since the last call, oldest first, once the write
50
57
  * that clears them from the widget has worked, so a tap is handed over once. A handler that throws, or rejects,
51
58
  * goes to the `ErrorHandler`, and its taps are still cleared.
52
59
  */
53
60
  readonly onTaps?: (taps: readonly string[]) => void;
61
+ /**
62
+ * What the widget shows later, with the app not running: the entries after now, each the props
63
+ * to show from its date. Asked at every write with the signal's props, which are shown until the
64
+ * first of them.
65
+ */
66
+ readonly timeline?: (props: T) => readonly WidgetEntry<T>[];
54
67
  }
55
68
  /** A home screen widget kept in step with a signal. */
56
69
  export interface WidgetRef {
@@ -74,4 +87,4 @@ export declare const WIDGET_EVENTS: InjectionToken<WidgetEvents>;
74
87
  * worked. Going to the background asks iOS to redraw the widget, which it otherwise does on its own
75
88
  * schedule. Destroying the injector stops both. Call it in an injection context.
76
89
  */
77
- export declare function widget<T extends object>(native: NativeWidget<T>, props: Signal<T>, options?: WidgetOptions): WidgetRef;
90
+ export declare function widget<T extends object>(native: NativeWidget<T>, props: Signal<T>, options?: WidgetOptions<T>): WidgetRef;
package/dist/widget.js CHANGED
@@ -17,6 +17,8 @@ export const WIDGET_EVENTS = new InjectionToken('angular-native.widgetEvents', {
17
17
  factory: () => {
18
18
  const widgets = optional(() => require('expo-widgets'));
19
19
  const native = optional(() => require('react-native'));
20
+ const destroyRef = inject(DestroyRef);
21
+ let startToken;
20
22
  const onState = (wanted) => (listener) => {
21
23
  const subscription = native?.AppState.addEventListener('change', (state) => {
22
24
  if (state === wanted)
@@ -30,6 +32,15 @@ export const WIDGET_EVENTS = new InjectionToken('angular-native.widgetEvents', {
30
32
  return () => subscription?.remove();
31
33
  },
32
34
  takeHeld: (sources) => heldTaps()?.takeTaps?.(sources) ?? [],
35
+ pushToStartToken: () => {
36
+ if (startToken)
37
+ return startToken;
38
+ const token = signal(null, /* @ts-ignore */
39
+ ...(ngDevMode ? [{ debugName: "token" }] : /* istanbul ignore next */ []));
40
+ const subscription = widgets?.addPushToStartTokenListener?.((event) => token.set(event.activityPushToStartToken));
41
+ destroyRef.onDestroy(() => subscription?.remove());
42
+ return (startToken = token.asReadonly());
43
+ },
33
44
  onForeground: onState('active'),
34
45
  onBackground: onState('background'),
35
46
  };
@@ -48,7 +59,16 @@ export function widget(native, props, options = {}) {
48
59
  const error = signal(null, /* @ts-ignore */
49
60
  ...(ngDevMode ? [{ debugName: "error" }] : /* istanbul ignore next */ []));
50
61
  let syncing = null;
51
- const queued = async () => (await native.getTimeline()).at(-1)?.props.taps ?? [];
62
+ /** A tap is recorded in the entry that was showing, which a timeline has several of. */
63
+ const queued = async () => (await native.getTimeline()).flatMap((entry) => entry.props.taps ?? []);
64
+ const write = (now) => {
65
+ if (options.timeline && native.updateTimeline) {
66
+ native.updateTimeline([{ date: new Date(), props: now }, ...options.timeline(now)]);
67
+ }
68
+ else {
69
+ native.updateSnapshot(now);
70
+ }
71
+ };
52
72
  /** The taps, read again until no more land, since the write after it clears them. */
53
73
  const collect = async () => {
54
74
  let taps = await queued();
@@ -64,7 +84,7 @@ export function widget(native, props, options = {}) {
64
84
  const run = async () => {
65
85
  try {
66
86
  const taps = await collect();
67
- native.updateSnapshot(untracked(props));
87
+ untracked(() => write(props()));
68
88
  error.set(null);
69
89
  if (taps.length)
70
90
  hand(taps);
@@ -1 +1 @@
1
- {"version":3,"file":"widget.js","sourceRoot":"","sources":["../src/widget.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AACH,OAAO,EACL,UAAU,EACV,YAAY,EACZ,cAAc,EACd,MAAM,EACN,MAAM,EACN,MAAM,EACN,SAAS,GAEV,MAAM,eAAe,CAAC;AACvB,OAAO,EAAE,QAAQ,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AAqEjD,MAAM,QAAQ,GAAG,GAAG,EAAE,CACnB,UAA+E,CAAC,IAAI,EAAE,OAAO;IAC5F,EAAE,wBAAwB,CAAC;AAE/B,8DAA8D;AAC9D,MAAM,CAAC,MAAM,aAAa,GAAG,IAAI,cAAc,CAAe,6BAA6B,EAAE;IAC3F,OAAO,EAAE,GAAG,EAAE;QACZ,MAAM,OAAO,GAAG,QAAQ,CAAC,GAAG,EAAE,CAAC,OAAO,CAAC,cAAc,CAAgB,CAAC,CAAC;QACvE,MAAM,MAAM,GAAG,QAAQ,CAAC,GAAG,EAAE,CAAC,OAAO,CAAC,cAAc,CAAgB,CAAC,CAAC;QACtE,MAAM,OAAO,GAAG,CAAC,MAAc,EAAE,EAAE,CAAC,CAAC,QAAoB,EAAE,EAAE;YAC3D,MAAM,YAAY,GAAG,MAAM,EAAE,QAAQ,CAAC,gBAAgB,CAAC,QAAQ,EAAE,CAAC,KAAK,EAAE,EAAE;gBACzE,IAAI,KAAK,KAAK,MAAM;oBAAE,QAAQ,EAAE,CAAC;YACnC,CAAC,CAAC,CAAC;YACH,OAAO,GAAG,EAAE,CAAC,YAAY,EAAE,MAAM,EAAE,CAAC;QACtC,CAAC,CAAC;QACF,OAAO;YACL,KAAK,EAAE,CAAC,QAAQ,EAAE,EAAE;gBAClB,MAAM,YAAY,GAAG,OAAO,EAAE,0BAA0B,CAAC,QAAQ,CAAC,CAAC;gBACnE,OAAO,GAAG,EAAE,CAAC,YAAY,EAAE,MAAM,EAAE,CAAC;YACtC,CAAC;YACD,QAAQ,EAAE,CAAC,OAAO,EAAE,EAAE,CAAC,QAAQ,EAAE,EAAE,QAAQ,EAAE,CAAC,OAAO,CAAC,IAAI,EAAE;YAC5D,YAAY,EAAE,OAAO,CAAC,QAAQ,CAAC;YAC/B,YAAY,EAAE,OAAO,CAAC,YAAY,CAAC;SACpC,CAAC;IACJ,CAAC;CACF,CAAC,CAAC;AAEH;;;;;;GAMG;AACH,MAAM,UAAU,MAAM,CACpB,MAAuB,EACvB,KAAgB,EAChB,UAAyB,EAAE;IAE3B,MAAM,MAAM,GAAG,MAAM,CAAC,YAAY,CAAC,CAAC;IACpC,MAAM,MAAM,GAAG,MAAM,CAAC,aAAa,CAAC,CAAC;IACrC,MAAM,KAAK,GAAG,MAAM,CAAU,IAAI;8EAAC,CAAC;IACpC,IAAI,OAAO,GAAyB,IAAI,CAAC;IAEzC,MAAM,MAAM,GAAG,KAAK,IAAI,EAAE,CAAC,CAAC,MAAM,MAAM,CAAC,WAAW,EAAE,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,EAAE,KAAK,CAAC,IAAI,IAAI,EAAE,CAAC;IAEjF,qFAAqF;IACrF,MAAM,OAAO,GAAG,KAAK,IAAI,EAAE;QACzB,IAAI,IAAI,GAAG,MAAM,MAAM,EAAE,CAAC;QAC1B,OAAO,IAAI,CAAC,MAAM,EAAE,CAAC;YACnB,MAAM,IAAI,GAAG,MAAM,MAAM,EAAE,CAAC;YAC5B,IAAI,IAAI,CAAC,MAAM,IAAI,IAAI,CAAC,MAAM;gBAAE,MAAM;YACtC,IAAI,GAAG,IAAI,CAAC;QACd,CAAC;QACD,OAAO,IAAI,CAAC;IACd,CAAC,CAAC;IAEF,MAAM,IAAI,GAAG,CAAC,IAAuB,EAAE,EAAE,CACvC,QAAQ,CACN,GAAG,EAAE,CAAC,OAAO,CAAC,MAAM,EAAE,CAAC,IAAI,CAAC,EAC5B,CAAC,OAAO,EAAE,EAAE,CAAC,MAAM,CAAC,WAAW,CAAC,OAAO,CAAC,CACzC,CAAC;IAEJ,MAAM,GAAG,GAAG,KAAK,IAAI,EAAE;QACrB,IAAI,CAAC;YACH,MAAM,IAAI,GAAG,MAAM,OAAO,EAAE,CAAC;YAC7B,MAAM,CAAC,cAAc,CAAC,SAAS,CAAC,KAAK,CAAC,CAAC,CAAC;YACxC,KAAK,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;YAChB,IAAI,IAAI,CAAC,MAAM;gBAAE,IAAI,CAAC,IAAI,CAAC,CAAC;QAC9B,CAAC;QAAC,OAAO,OAAO,EAAE,CAAC;YACjB,KAAK,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC;YACnB,MAAM,CAAC,WAAW,CAAC,OAAO,CAAC,CAAC;QAC9B,CAAC;IACH,CAAC,CAAC;IAEF;;;OAGG;IACH,MAAM,IAAI,GAAG,GAAG,EAAE,CAAC,CAAC,OAAO,KAAK,GAAG,EAAE,CAAC,OAAO,CAAC,GAAG,EAAE,CAAC,CAAC,OAAO,GAAG,IAAI,CAAC,CAAC,CAAC,CAAC;IAEvE,MAAM,CAAC,GAAG,EAAE;QACV,KAAK,EAAE,CAAC;QACR,SAAS,CAAC,GAAG,EAAE,CAAC,KAAK,IAAI,EAAE,CAAC,CAAC;IAC/B,CAAC,CAAC,CAAC;IAEH,MAAM,KAAK,GAAG;QACZ,MAAM,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,KAAK,IAAI,EAAE,CAAC;QAC/B,MAAM,CAAC,YAAY,CAAC,GAAG,EAAE,CAAC,KAAK,IAAI,EAAE,CAAC;QACtC,MAAM,CAAC,YAAY,CAAC,GAAG,EAAE,CAAC,MAAM,CAAC,MAAM,EAAE,CAAC;KAC3C,CAAC;IACF,MAAM,CAAC,UAAU,CAAC,CAAC,SAAS,CAAC,GAAG,EAAE,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC;IAEpE,OAAO,EAAE,KAAK,EAAE,KAAK,CAAC,UAAU,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,GAAG,EAAE,CAAC,MAAM,CAAC,MAAM,EAAE,EAAE,CAAC;AAC5E,CAAC","sourcesContent":["/**\n * `widget`, which keeps a home screen widget from `expo-widgets` in step with a signal, and hands the\n * app the taps on its buttons.\n *\n * A widget button runs in the widget extension, not in the app, which iOS keeps suspended, so a\n * tap's own event is lost unless the app is running. The widget's layout records each tap in its\n * own props instead, as `taps`, and `widget` collects them when the app next runs: at once while it\n * is running, and when it comes back to the foreground. It takes the widget `createWidget` makes in\n * the app's layout file, so this package imports `expo-widgets` only for its tap events.\n */\nimport {\n DestroyRef,\n ErrorHandler,\n InjectionToken,\n effect,\n inject,\n signal,\n untracked,\n type Signal,\n} from '@angular/core';\nimport { isolated, optional } from './native.ts';\n\n/** One entry of a widget's timeline, as `expo-widgets` stores it. */\nexport interface WidgetEntry<T extends object> {\n readonly date: Date;\n /** The entry's props, with the targets of the buttons tapped since the app last collected them. */\n readonly props: T & { readonly taps?: readonly string[] };\n}\n\n/** What `createWidget` answers. */\nexport interface NativeWidget<T extends object> {\n updateSnapshot(props: T): void;\n getTimeline(): Promise<readonly WidgetEntry<T>[]>;\n reload(): void;\n}\n\n/** A tap on a button, as `expo-widgets` reports it while the app is running. */\nexport interface WidgetTap {\n /** The widget's name, or the id of the Live Activity. */\n readonly source: string;\n /** The button's `target`. */\n readonly target: string;\n /** When it was tapped, in milliseconds since the epoch. */\n readonly timestamp: number;\n}\n\n/** When to collect taps, and when to ask iOS to redraw the widget. */\nexport interface WidgetEvents {\n /** A button of a widget or a Live Activity was tapped while the app was running. */\n onTap(listener: (tap: WidgetTap) => void): () => void;\n /**\n * The taps made on Live Activities with these ids before the app was listening, each answered\n * once. A tap can start the app, and is over before its JavaScript has loaded.\n */\n takeHeld?(sources: readonly string[]): readonly WidgetTap[];\n onForeground(listener: () => void): () => void;\n onBackground(listener: () => void): () => void;\n}\n\nexport interface WidgetOptions {\n /**\n * Called with the targets of the buttons tapped since the last call, oldest first, once the write\n * that clears them from the widget has worked, so a tap is handed over once. A handler that throws, or rejects,\n * goes to the `ErrorHandler`, and its taps are still cleared.\n */\n readonly onTaps?: (taps: readonly string[]) => void;\n}\n\n/** A home screen widget kept in step with a signal. */\nexport interface WidgetRef {\n /**\n * Why the last sync did not happen: the taps could not be read, or the widget not written. It also\n * goes to the `ErrorHandler`, and is null again once a sync works. A failing `onTaps` is not kept\n * here.\n */\n readonly error: Signal<unknown>;\n /** Collects the taps now, then shows the signal's value. */\n sync(): Promise<void>;\n /** Asks iOS to redraw the widget. */\n reload(): void;\n}\n\ntype ReactNative = typeof import('react-native');\ntype ExpoWidgets = {\n addUserInteractionListener(listener: (tap: WidgetTap) => void): { remove(): void };\n};\n\n/** This package's own native module, which holds a Live Activity's taps from launch. Absent off iOS. */\ntype HeldTaps = { takeTaps?(sources: readonly string[]): readonly WidgetTap[] };\nconst heldTaps = () =>\n (globalThis as { expo?: { modules?: { NgNativeLiveActivityTaps?: HeldTaps } } }).expo?.modules\n ?.NgNativeLiveActivityTaps;\n\n/** Overridden in a test to tap a widget that is not there. */\nexport const WIDGET_EVENTS = new InjectionToken<WidgetEvents>('angular-native.widgetEvents', {\n factory: () => {\n const widgets = optional(() => require('expo-widgets') as ExpoWidgets);\n const native = optional(() => require('react-native') as ReactNative);\n const onState = (wanted: string) => (listener: () => void) => {\n const subscription = native?.AppState.addEventListener('change', (state) => {\n if (state === wanted) listener();\n });\n return () => subscription?.remove();\n };\n return {\n onTap: (listener) => {\n const subscription = widgets?.addUserInteractionListener(listener);\n return () => subscription?.remove();\n },\n takeHeld: (sources) => heldTaps()?.takeTaps?.(sources) ?? [],\n onForeground: onState('active'),\n onBackground: onState('background'),\n };\n },\n});\n\n/**\n * Shows `props` on the widget and follows the signal, and hands taps on its buttons to `onTaps`.\n * A write replaces the props that hold the taps, so every write comes after the taps are read, and\n * nothing is written while they cannot be; a tap is handed over once the write that clears it has\n * worked. Going to the background asks iOS to redraw the widget, which it otherwise does on its own\n * schedule. Destroying the injector stops both. Call it in an injection context.\n */\nexport function widget<T extends object>(\n native: NativeWidget<T>,\n props: Signal<T>,\n options: WidgetOptions = {},\n): WidgetRef {\n const errors = inject(ErrorHandler);\n const events = inject(WIDGET_EVENTS);\n const error = signal<unknown>(null);\n let syncing: Promise<void> | null = null;\n\n const queued = async () => (await native.getTimeline()).at(-1)?.props.taps ?? [];\n\n /** The taps, read again until no more land, since the write after it clears them. */\n const collect = async () => {\n let taps = await queued();\n while (taps.length) {\n const more = await queued();\n if (more.length <= taps.length) break;\n taps = more;\n }\n return taps;\n };\n\n const hand = (taps: readonly string[]) =>\n isolated(\n () => options.onTaps?.(taps),\n (failure) => errors.handleError(failure),\n );\n\n const run = async () => {\n try {\n const taps = await collect();\n native.updateSnapshot(untracked(props));\n error.set(null);\n if (taps.length) hand(taps);\n } catch (failure) {\n error.set(failure);\n errors.handleError(failure);\n }\n };\n\n /**\n * One sync at a time. One asked for during it is already covered: its write reads the signal then,\n * and nothing is awaited after it.\n */\n const sync = () => (syncing ??= run().finally(() => (syncing = null)));\n\n effect(() => {\n props();\n untracked(() => void sync());\n });\n\n const stops = [\n events.onTap(() => void sync()),\n events.onForeground(() => void sync()),\n events.onBackground(() => native.reload()),\n ];\n inject(DestroyRef).onDestroy(() => stops.forEach((stop) => stop()));\n\n return { error: error.asReadonly(), sync, reload: () => native.reload() };\n}\n"]}
1
+ {"version":3,"file":"widget.js","sourceRoot":"","sources":["../src/widget.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * `widget`, which keeps a home screen widget from `expo-widgets` in step with a signal, and hands the\n * app the taps on its buttons.\n *\n * A widget button runs in the widget extension, not in the app, which iOS keeps suspended, so a\n * tap's own event is lost unless the app is running. The widget's layout records each tap in its\n * own props instead, as `taps`, and `widget` collects them when the app next runs: at once while it\n * is running, and when it comes back to the foreground. It takes the widget `createWidget` makes in\n * the app's layout file, so this package imports `expo-widgets` only for its tap events.\n */\nimport {\n DestroyRef,\n ErrorHandler,\n InjectionToken,\n effect,\n inject,\n signal,\n untracked,\n type Signal,\n} from '@angular/core';\nimport { isolated, optional } from './native.ts';\n\n/** One entry of a widget's timeline, as `expo-widgets` stores it. */\nexport interface WidgetEntry<T extends object> {\n readonly date: Date;\n /** The entry's props, with the targets of the buttons tapped since the app last collected them. */\n readonly props: T & { readonly taps?: readonly string[] };\n}\n\n/** What `createWidget` answers. */\nexport interface NativeWidget<T extends object> {\n updateSnapshot(props: T): void;\n /** Replaces the timeline: each entry's props are shown from its date. */\n updateTimeline?(entries: readonly WidgetEntry<T>[]): void;\n getTimeline(): Promise<readonly WidgetEntry<T>[]>;\n reload(): void;\n}\n\n/** A tap on a button, as `expo-widgets` reports it while the app is running. */\nexport interface WidgetTap {\n /** The widget's name, or the id of the Live Activity. */\n readonly source: string;\n /** The button's `target`. */\n readonly target: string;\n /** When it was tapped, in milliseconds since the epoch. */\n readonly timestamp: number;\n}\n\n/** When to collect taps, and when to ask iOS to redraw the widget. */\nexport interface WidgetEvents {\n /** A button of a widget or a Live Activity was tapped while the app was running. */\n onTap(listener: (tap: WidgetTap) => void): () => void;\n /**\n * The taps made on Live Activities with these ids before the app was listening, each answered\n * once. A tap can start the app, and is over before its JavaScript has loaded.\n */\n takeHeld?(sources: readonly string[]): readonly WidgetTap[];\n /**\n * The token a server starts a Live Activity with over APNs: null until iOS issues one. One signal\n * for the whole app, since `expo-widgets` hands the token it has only to the first to listen.\n */\n pushToStartToken?(): Signal<string | null>;\n onForeground(listener: () => void): () => void;\n onBackground(listener: () => void): () => void;\n}\n\nexport interface WidgetOptions<T extends object = object> {\n /**\n * Called with the targets of the buttons tapped since the last call, oldest first, once the write\n * that clears them from the widget has worked, so a tap is handed over once. A handler that throws, or rejects,\n * goes to the `ErrorHandler`, and its taps are still cleared.\n */\n readonly onTaps?: (taps: readonly string[]) => void;\n /**\n * What the widget shows later, with the app not running: the entries after now, each the props\n * to show from its date. Asked at every write with the signal's props, which are shown until the\n * first of them.\n */\n readonly timeline?: (props: T) => readonly WidgetEntry<T>[];\n}\n\n/** A home screen widget kept in step with a signal. */\nexport interface WidgetRef {\n /**\n * Why the last sync did not happen: the taps could not be read, or the widget not written. It also\n * goes to the `ErrorHandler`, and is null again once a sync works. A failing `onTaps` is not kept\n * here.\n */\n readonly error: Signal<unknown>;\n /** Collects the taps now, then shows the signal's value. */\n sync(): Promise<void>;\n /** Asks iOS to redraw the widget. */\n reload(): void;\n}\n\ntype ReactNative = typeof import('react-native');\ntype ExpoWidgets = {\n addUserInteractionListener(listener: (tap: WidgetTap) => void): { remove(): void };\n addPushToStartTokenListener?(listener: (event: { activityPushToStartToken: string }) => void): {\n remove(): void;\n };\n};\n\n/** This package's own native module, which holds a Live Activity's taps from launch. Absent off iOS. */\ntype HeldTaps = { takeTaps?(sources: readonly string[]): readonly WidgetTap[] };\nconst heldTaps = () =>\n (globalThis as { expo?: { modules?: { NgNativeLiveActivityTaps?: HeldTaps } } }).expo?.modules\n ?.NgNativeLiveActivityTaps;\n\n/** Overridden in a test to tap a widget that is not there. */\nexport const WIDGET_EVENTS = new InjectionToken<WidgetEvents>('angular-native.widgetEvents', {\n factory: () => {\n const widgets = optional(() => require('expo-widgets') as ExpoWidgets);\n const native = optional(() => require('react-native') as ReactNative);\n const destroyRef = inject(DestroyRef);\n let startToken: Signal<string | null> | undefined;\n const onState = (wanted: string) => (listener: () => void) => {\n const subscription = native?.AppState.addEventListener('change', (state) => {\n if (state === wanted) listener();\n });\n return () => subscription?.remove();\n };\n return {\n onTap: (listener) => {\n const subscription = widgets?.addUserInteractionListener(listener);\n return () => subscription?.remove();\n },\n takeHeld: (sources) => heldTaps()?.takeTaps?.(sources) ?? [],\n pushToStartToken: () => {\n if (startToken) return startToken;\n const token = signal<string | null>(null);\n const subscription = widgets?.addPushToStartTokenListener?.((event) =>\n token.set(event.activityPushToStartToken),\n );\n destroyRef.onDestroy(() => subscription?.remove());\n return (startToken = token.asReadonly());\n },\n onForeground: onState('active'),\n onBackground: onState('background'),\n };\n },\n});\n\n/**\n * Shows `props` on the widget and follows the signal, and hands taps on its buttons to `onTaps`.\n * A write replaces the props that hold the taps, so every write comes after the taps are read, and\n * nothing is written while they cannot be; a tap is handed over once the write that clears it has\n * worked. Going to the background asks iOS to redraw the widget, which it otherwise does on its own\n * schedule. Destroying the injector stops both. Call it in an injection context.\n */\nexport function widget<T extends object>(\n native: NativeWidget<T>,\n props: Signal<T>,\n options: WidgetOptions<T> = {},\n): WidgetRef {\n const errors = inject(ErrorHandler);\n const events = inject(WIDGET_EVENTS);\n const error = signal<unknown>(null);\n let syncing: Promise<void> | null = null;\n\n /** A tap is recorded in the entry that was showing, which a timeline has several of. */\n const queued = async () =>\n (await native.getTimeline()).flatMap((entry) => entry.props.taps ?? []);\n\n const write = (now: T) => {\n if (options.timeline && native.updateTimeline) {\n native.updateTimeline([{ date: new Date(), props: now }, ...options.timeline(now)]);\n } else {\n native.updateSnapshot(now);\n }\n };\n\n /** The taps, read again until no more land, since the write after it clears them. */\n const collect = async () => {\n let taps = await queued();\n while (taps.length) {\n const more = await queued();\n if (more.length <= taps.length) break;\n taps = more;\n }\n return taps;\n };\n\n const hand = (taps: readonly string[]) =>\n isolated(\n () => options.onTaps?.(taps),\n (failure) => errors.handleError(failure),\n );\n\n const run = async () => {\n try {\n const taps = await collect();\n untracked(() => write(props()));\n error.set(null);\n if (taps.length) hand(taps);\n } catch (failure) {\n error.set(failure);\n errors.handleError(failure);\n }\n };\n\n /**\n * One sync at a time. One asked for during it is already covered: its write reads the signal then,\n * and nothing is awaited after it.\n */\n const sync = () => (syncing ??= run().finally(() => (syncing = null)));\n\n effect(() => {\n props();\n untracked(() => void sync());\n });\n\n const stops = [\n events.onTap(() => void sync()),\n events.onForeground(() => void sync()),\n events.onBackground(() => native.reload()),\n ];\n inject(DestroyRef).onDestroy(() => stops.forEach((stop) => stop()));\n\n return { error: error.asReadonly(), sync, reload: () => native.reload() };\n}\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ng-native/expo",
3
- "version": "0.10.0",
3
+ "version": "0.12.0",
4
4
  "description": "Expo modules as Angular services for Angular Native, plus registerExpoView for a module's native view.",
5
5
  "keywords": [
6
6
  "angular",
@@ -213,7 +213,7 @@
213
213
  "access": "public"
214
214
  },
215
215
  "dependencies": {
216
- "@ng-native/fabric": "0.10.0"
216
+ "@ng-native/fabric": "0.12.0"
217
217
  },
218
218
  "nx": {
219
219
  "tags": [