@ng-native/expo 0.9.0 → 0.11.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.
@@ -5,7 +5,8 @@
5
5
  * `expo-widgets` starts a stand-in with no id, and `start()` answers false.
6
6
  */
7
7
  import { DestroyRef, ErrorHandler, effect, inject, signal, untracked, } from '@angular/core';
8
- import { expoModule } from "./native.js";
8
+ import { expoModule, isolated } from "./native.js";
9
+ import { WIDGET_EVENTS } from "./widget.js";
9
10
  /**
10
11
  * A Live Activity drawn by `layout`, an Angular component, as `expo-widgets`' `createLiveActivity`
11
12
  * makes one. `name` matches the activity's name in the `expo-widgets` plugin's config.
@@ -54,17 +55,31 @@ const STAND_IN = {
54
55
  /** A widget in Node: it draws nothing and holds no timeline, and `widget()` keeps it in step. */
55
56
  const WIDGET_STAND_IN = {
56
57
  updateSnapshot: () => { },
58
+ updateTimeline: () => { },
57
59
  getTimeline: async () => [],
58
60
  reload: () => { },
59
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
+ }
60
72
  /**
61
73
  * Keeps a Live Activity of the factory's kind in step with `props`: once started, every change to the
62
74
  * signal updates it. One left running from before the app started is picked up rather than started
63
75
  * again, since iOS caps how many an app runs. Destroying the injector stops the updates and leaves
64
- * the activity running, as it outlives the app. Call it in an injection context.
76
+ * the activity running, as it outlives the app. Taps on its buttons go to `onTaps`. Call it in an
77
+ * injection context.
65
78
  */
66
- export function liveActivity(factory, props) {
79
+ export function liveActivity(factory, props, options = {}) {
67
80
  const errors = inject(ErrorHandler);
81
+ const { onTaps } = options;
82
+ const staleDate = () => options.staleDate?.() ?? undefined;
68
83
  const active = signal(false, /* @ts-ignore */
69
84
  ...(ngDevMode ? [{ debugName: "active" }] : /* istanbul ignore next */ []));
70
85
  const id = signal(null, /* @ts-ignore */
@@ -104,6 +119,8 @@ export function liveActivity(factory, props) {
104
119
  tokenSubscription = activity.addPushTokenListener((event) => pushToken.set(event.pushToken));
105
120
  activity.getPushToken().then((token) => current === activity && token !== null && pushToken.set(token), () => { });
106
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());
107
124
  const existing = running()[0];
108
125
  if (existing)
109
126
  adopt(existing);
@@ -111,9 +128,35 @@ export function liveActivity(factory, props) {
111
128
  const next = props();
112
129
  const activity = current;
113
130
  if (activity)
114
- untracked(() => activity.update(next).catch(report));
131
+ untracked(() => write(activity, next).catch(report));
115
132
  });
133
+ const stopTaps = onTaps && listenForTaps(onTaps);
134
+ /**
135
+ * Every widget's and activity's taps arrive here: an activity's names it by its id. Those made
136
+ * before this was listening are taken once it is, so one that also arrives as it happens is
137
+ * handed over once.
138
+ */
139
+ function listenForTaps(hand) {
140
+ const events = inject(WIDGET_EVENTS);
141
+ const key = (tap) => `${tap.source} ${tap.target} ${tap.timestamp}`;
142
+ const taken = new Set();
143
+ const handOver = (targets) => isolated(() => hand(targets), (failure) => errors.handleError(failure));
144
+ const stop = events.onTap((tap) => {
145
+ if (taken.delete(key(tap)))
146
+ return;
147
+ if (running().some((activity) => activity.getId() === tap.source))
148
+ handOver([tap.target]);
149
+ });
150
+ const held = events.takeHeld?.(running().map((activity) => activity.getId())) ?? [];
151
+ for (const tap of held)
152
+ taken.add(key(tap));
153
+ // Not yet: the caller's own fields may not be set while this one is.
154
+ if (held.length)
155
+ queueMicrotask(() => handOver(held.map((tap) => tap.target)));
156
+ return stop;
157
+ }
116
158
  inject(DestroyRef).onDestroy(() => {
159
+ stopTaps?.();
117
160
  tokenSubscription?.remove();
118
161
  tokenSubscription = null;
119
162
  });
@@ -128,14 +171,14 @@ export function liveActivity(factory, props) {
128
171
  const value = untracked(props);
129
172
  try {
130
173
  const already = running()[0];
131
- const activity = already ?? factory.start(value, options.url);
174
+ const activity = already ?? factory.start(value, options.url, staleDate());
132
175
  if (!activity.getId()) {
133
176
  error.set(new Error('Live Activities are only on iOS.'));
134
177
  return false;
135
178
  }
136
179
  adopt(activity);
137
180
  if (already)
138
- already.update(value).catch(report);
181
+ write(already, value).catch(report);
139
182
  return true;
140
183
  }
141
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 } from './native.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\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. Call it in an injection context.\n */\nexport function liveActivity<T extends object>(\n factory: LiveActivityFactory<T>,\n props: Signal<T>,\n): LiveActivityRef {\n const errors = inject(ErrorHandler);\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 inject(DestroyRef).onDestroy(() => {\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/native.d.ts CHANGED
@@ -13,6 +13,11 @@
13
13
  * a device that has no such sensor. On a device the throw is a module that is present but not
14
14
  * linked, which is the same answer for the same reason.
15
15
  */
16
+ /**
17
+ * Calls a listener the app gave. What it throws, or rejects with when it is asynchronous, goes to
18
+ * `report`, so one failing listener does not stop what calls it.
19
+ */
20
+ export declare function isolated(call: () => unknown, report: (failure: unknown) => void): void;
16
21
  export declare function optional<T>(load: () => T): T | null;
17
22
  /** The platforms an Expo module is written for. */
18
23
  export type ModulePlatform = 'ios' | 'android' | 'web';
package/dist/native.js CHANGED
@@ -13,6 +13,20 @@
13
13
  * a device that has no such sensor. On a device the throw is a module that is present but not
14
14
  * linked, which is the same answer for the same reason.
15
15
  */
16
+ /**
17
+ * Calls a listener the app gave. What it throws, or rejects with when it is asynchronous, goes to
18
+ * `report`, so one failing listener does not stop what calls it.
19
+ */
20
+ export function isolated(call, report) {
21
+ try {
22
+ const done = call();
23
+ if (done instanceof Promise)
24
+ done.catch(report);
25
+ }
26
+ catch (failure) {
27
+ report(failure);
28
+ }
29
+ }
16
30
  export function optional(load) {
17
31
  try {
18
32
  return load();
@@ -1 +1 @@
1
- {"version":3,"file":"native.js","sourceRoot":"","sources":["../src/native.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Reaching an Expo module without importing one.\n *\n * A static `import ... from 'expo-battery'` makes the file unloadable by Node, which is what used\n * to force every service into two files: the behaviour somewhere the test suite could reach, and\n * the binding somewhere it could not. A `require` inside a function has neither problem. Metro\n * still resolves the string literal at build time, so the module is bundled exactly as before and\n * an app that has not installed it still gets a build error rather than a surprise at runtime;\n * Node never evaluates the call at all, because nothing in a test asks for the real platform.\n *\n * The `catch` is doing two jobs, and only one of them is Node. In an ESM test `require` is not\n * defined, so calling it throws `ReferenceError` and the service goes inert - the same state as\n * a device that has no such sensor. On a device the throw is a module that is present but not\n * linked, which is the same answer for the same reason.\n */\nexport function optional<T>(load: () => T): T | null {\n try {\n return load();\n } catch {\n return null;\n }\n}\n\n/** The platforms an Expo module is written for. */\nexport type ModulePlatform = 'ios' | 'android' | 'web';\n\n/**\n * A module the app needs and does not have: the package is not installed, or it is and the app was\n * not rebuilt since, so its native half is not in the binary. Development builds and Expo Go carry\n * only the native modules they were built with.\n */\nexport class MissingModuleError extends Error {\n override readonly name = 'MissingModuleError';\n /** The package, as it is installed: `expo-haptics`. */\n readonly module: string;\n\n constructor(module: string, platform: ModulePlatform, cause?: unknown, message?: string) {\n super(\n message ??\n (platform === 'web'\n ? `${module} is not installed. Install it with \"npx expo install ${module}\", then restart the dev server.`\n : `${module} is not in this build of the app. Install it with \"npx expo install ${module}\", ` +\n `then rebuild the app (\"npx expo run:${platform}\", or a new EAS build): a development ` +\n `build, and Expo Go, contain only the native modules they were built with.`),\n { cause },\n );\n this.module = module;\n }\n}\n\n/**\n * The `MissingModuleError` to throw where `expoModule` answered null: the module is installed or not, but there is\n * nothing to load it on. In Node, where a test runs, that is every native module, and \"install it\"\n * would be advice to do what is already done; `inTest` says what a test does instead. On a platform\n * the module does not support, it says that.\n */\nexport function unavailable(module: string, inTest: string): MissingModuleError {\n const platform = currentPlatform();\n const message =\n platform === null\n ? `[angular-native] ${module} has no native module to load in Node, where a test runs. ${inTest}`\n : `[angular-native] ${module} is not available on ${platform}.`;\n // The platform is only read for the default message, which this replaces.\n return new MissingModuleError(module, platform ?? 'web', undefined, message);\n}\n\n/** The platform the app is running on, or null in Node, where there is none to ask. */\nexport function currentPlatform(): ModulePlatform | null {\n return optional(\n () => (require('react-native') as { Platform: { OS: string } }).Platform.OS as ModulePlatform,\n );\n}\n\n/**\n * For each Expo package whose JavaScript requires a native module as it is evaluated, and so\n * throws when its native half is not in the build, one of those native modules. Any one of them\n * says whether the package's native half is there, and each of these is registered on both iOS and\n * Android.\n */\nconst NATIVE_MODULES: Readonly<Record<string, string>> = {\n 'expo-application': 'ExpoApplication',\n 'expo-asset': 'ExpoAsset',\n 'expo-audio': 'ExpoAudio',\n 'expo-background-task': 'ExpoBackgroundTask',\n 'expo-battery': 'ExpoBattery',\n 'expo-brightness': 'ExpoBrightness',\n 'expo-clipboard': 'ExpoClipboard',\n 'expo-crypto': 'ExpoCrypto',\n 'expo-device': 'ExpoDevice',\n 'expo-document-picker': 'ExpoDocumentPicker',\n 'expo-file-system': 'FileSystem',\n 'expo-foldables': 'ExpoFoldables',\n 'expo-font': 'ExpoFontLoader',\n 'expo-image-manipulator': 'ExpoImageManipulator',\n 'expo-image-picker': 'ExponentImagePicker',\n 'expo-keep-awake': 'ExpoKeepAwake',\n 'expo-local-authentication': 'ExpoLocalAuthentication',\n 'expo-local-llm': 'ExpoLocalLlm',\n 'expo-localization': 'ExpoLocalization',\n 'expo-location': 'ExpoLocation',\n 'expo-media-library': 'ExpoMediaLibrary',\n 'expo-network': 'ExpoNetwork',\n 'expo-notifications': 'ExpoNotificationPresenter',\n 'expo-screen-capture': 'ExpoScreenCapture',\n 'expo-screen-orientation': 'ExpoScreenOrientation',\n 'expo-secure-store': 'ExpoSecureStore',\n 'expo-sensors': 'ExponentAccelerometer',\n 'expo-sqlite': 'ExpoSQLite',\n 'expo-store-review': 'ExpoStoreReview',\n 'expo-tracking-transparency': 'ExpoTrackingTransparency',\n 'expo-updates': 'ExpoUpdates',\n 'expo-video': 'ExpoVideo',\n 'expo-web-browser': 'ExpoWebBrowser',\n};\n\n/**\n * Whether `module`'s native half is missing from the build on `platform`, asked of Expo before its\n * JavaScript is evaluated. That JavaScript throws while it is being evaluated when its native module\n * is not there, and Metro reports that as fatal, before any `catch` here, whenever it is not inside\n * another module's load: in a service's factory, say. Not on the web, where a package registers its\n * module only once it is evaluated.\n */\nfunction nativeHalfMissing(module: string, platform: ModulePlatform | null): boolean {\n const name = NATIVE_MODULES[module];\n if (!name || platform === null || platform === 'web') return false;\n const core = optional(() => require('expo-modules-core') as typeof import('expo-modules-core'));\n return core !== null && !core.requireOptionalNativeModule(name);\n}\n\n/**\n * The Expo module a service needs.\n *\n * Where the module exists for the platform the app is on, a module that is missing is a mistake\n * the developer can fix, so this throws a `MissingModuleError` that says how - rather than leave a\n * feature that silently does nothing. Anywhere it cannot exist - an iOS-only module on Android, the\n * web, and Node, where a test runs with no platform at all - this answers null, and the service\n * goes inert, as code shared across platforms needs it to.\n *\n * `load` answering null or undefined counts as missing too: `requireOptionalNativeModule` answers\n * null rather than throwing. So does a package whose native half is not in the build, which `load`\n * is not called for: see `nativeHalfMissing`.\n */\nexport function expoModule<T>(\n module: string,\n load: () => T | null | undefined,\n platforms: readonly ModulePlatform[] = ['ios', 'android'],\n): T | null {\n const platform = currentPlatform();\n let failure: unknown;\n try {\n const loaded = nativeHalfMissing(module, platform) ? null : load();\n if (loaded != null) return loaded;\n } catch (error) {\n failure = error;\n }\n if (platform === null || !platforms.includes(platform)) return null;\n throw new MissingModuleError(module, platform, failure);\n}\n"]}
1
+ {"version":3,"file":"native.js","sourceRoot":"","sources":["../src/native.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Reaching an Expo module without importing one.\n *\n * A static `import ... from 'expo-battery'` makes the file unloadable by Node, which is what used\n * to force every service into two files: the behaviour somewhere the test suite could reach, and\n * the binding somewhere it could not. A `require` inside a function has neither problem. Metro\n * still resolves the string literal at build time, so the module is bundled exactly as before and\n * an app that has not installed it still gets a build error rather than a surprise at runtime;\n * Node never evaluates the call at all, because nothing in a test asks for the real platform.\n *\n * The `catch` is doing two jobs, and only one of them is Node. In an ESM test `require` is not\n * defined, so calling it throws `ReferenceError` and the service goes inert - the same state as\n * a device that has no such sensor. On a device the throw is a module that is present but not\n * linked, which is the same answer for the same reason.\n */\n/**\n * Calls a listener the app gave. What it throws, or rejects with when it is asynchronous, goes to\n * `report`, so one failing listener does not stop what calls it.\n */\nexport function isolated(call: () => unknown, report: (failure: unknown) => void): void {\n try {\n const done = call();\n if (done instanceof Promise) done.catch(report);\n } catch (failure) {\n report(failure);\n }\n}\n\nexport function optional<T>(load: () => T): T | null {\n try {\n return load();\n } catch {\n return null;\n }\n}\n\n/** The platforms an Expo module is written for. */\nexport type ModulePlatform = 'ios' | 'android' | 'web';\n\n/**\n * A module the app needs and does not have: the package is not installed, or it is and the app was\n * not rebuilt since, so its native half is not in the binary. Development builds and Expo Go carry\n * only the native modules they were built with.\n */\nexport class MissingModuleError extends Error {\n override readonly name = 'MissingModuleError';\n /** The package, as it is installed: `expo-haptics`. */\n readonly module: string;\n\n constructor(module: string, platform: ModulePlatform, cause?: unknown, message?: string) {\n super(\n message ??\n (platform === 'web'\n ? `${module} is not installed. Install it with \"npx expo install ${module}\", then restart the dev server.`\n : `${module} is not in this build of the app. Install it with \"npx expo install ${module}\", ` +\n `then rebuild the app (\"npx expo run:${platform}\", or a new EAS build): a development ` +\n `build, and Expo Go, contain only the native modules they were built with.`),\n { cause },\n );\n this.module = module;\n }\n}\n\n/**\n * The `MissingModuleError` to throw where `expoModule` answered null: the module is installed or not, but there is\n * nothing to load it on. In Node, where a test runs, that is every native module, and \"install it\"\n * would be advice to do what is already done; `inTest` says what a test does instead. On a platform\n * the module does not support, it says that.\n */\nexport function unavailable(module: string, inTest: string): MissingModuleError {\n const platform = currentPlatform();\n const message =\n platform === null\n ? `[angular-native] ${module} has no native module to load in Node, where a test runs. ${inTest}`\n : `[angular-native] ${module} is not available on ${platform}.`;\n // The platform is only read for the default message, which this replaces.\n return new MissingModuleError(module, platform ?? 'web', undefined, message);\n}\n\n/** The platform the app is running on, or null in Node, where there is none to ask. */\nexport function currentPlatform(): ModulePlatform | null {\n return optional(\n () => (require('react-native') as { Platform: { OS: string } }).Platform.OS as ModulePlatform,\n );\n}\n\n/**\n * For each Expo package whose JavaScript requires a native module as it is evaluated, and so\n * throws when its native half is not in the build, one of those native modules. Any one of them\n * says whether the package's native half is there, and each of these is registered on both iOS and\n * Android.\n */\nconst NATIVE_MODULES: Readonly<Record<string, string>> = {\n 'expo-application': 'ExpoApplication',\n 'expo-asset': 'ExpoAsset',\n 'expo-audio': 'ExpoAudio',\n 'expo-background-task': 'ExpoBackgroundTask',\n 'expo-battery': 'ExpoBattery',\n 'expo-brightness': 'ExpoBrightness',\n 'expo-clipboard': 'ExpoClipboard',\n 'expo-crypto': 'ExpoCrypto',\n 'expo-device': 'ExpoDevice',\n 'expo-document-picker': 'ExpoDocumentPicker',\n 'expo-file-system': 'FileSystem',\n 'expo-foldables': 'ExpoFoldables',\n 'expo-font': 'ExpoFontLoader',\n 'expo-image-manipulator': 'ExpoImageManipulator',\n 'expo-image-picker': 'ExponentImagePicker',\n 'expo-keep-awake': 'ExpoKeepAwake',\n 'expo-local-authentication': 'ExpoLocalAuthentication',\n 'expo-local-llm': 'ExpoLocalLlm',\n 'expo-localization': 'ExpoLocalization',\n 'expo-location': 'ExpoLocation',\n 'expo-media-library': 'ExpoMediaLibrary',\n 'expo-network': 'ExpoNetwork',\n 'expo-notifications': 'ExpoNotificationPresenter',\n 'expo-screen-capture': 'ExpoScreenCapture',\n 'expo-screen-orientation': 'ExpoScreenOrientation',\n 'expo-secure-store': 'ExpoSecureStore',\n 'expo-sensors': 'ExponentAccelerometer',\n 'expo-sqlite': 'ExpoSQLite',\n 'expo-store-review': 'ExpoStoreReview',\n 'expo-tracking-transparency': 'ExpoTrackingTransparency',\n 'expo-updates': 'ExpoUpdates',\n 'expo-video': 'ExpoVideo',\n 'expo-web-browser': 'ExpoWebBrowser',\n};\n\n/**\n * Whether `module`'s native half is missing from the build on `platform`, asked of Expo before its\n * JavaScript is evaluated. That JavaScript throws while it is being evaluated when its native module\n * is not there, and Metro reports that as fatal, before any `catch` here, whenever it is not inside\n * another module's load: in a service's factory, say. Not on the web, where a package registers its\n * module only once it is evaluated.\n */\nfunction nativeHalfMissing(module: string, platform: ModulePlatform | null): boolean {\n const name = NATIVE_MODULES[module];\n if (!name || platform === null || platform === 'web') return false;\n const core = optional(() => require('expo-modules-core') as typeof import('expo-modules-core'));\n return core !== null && !core.requireOptionalNativeModule(name);\n}\n\n/**\n * The Expo module a service needs.\n *\n * Where the module exists for the platform the app is on, a module that is missing is a mistake\n * the developer can fix, so this throws a `MissingModuleError` that says how - rather than leave a\n * feature that silently does nothing. Anywhere it cannot exist - an iOS-only module on Android, the\n * web, and Node, where a test runs with no platform at all - this answers null, and the service\n * goes inert, as code shared across platforms needs it to.\n *\n * `load` answering null or undefined counts as missing too: `requireOptionalNativeModule` answers\n * null rather than throwing. So does a package whose native half is not in the build, which `load`\n * is not called for: see `nativeHalfMissing`.\n */\nexport function expoModule<T>(\n module: string,\n load: () => T | null | undefined,\n platforms: readonly ModulePlatform[] = ['ios', 'android'],\n): T | null {\n const platform = currentPlatform();\n let failure: unknown;\n try {\n const loaded = nativeHalfMissing(module, platform) ? null : load();\n if (loaded != null) return loaded;\n } catch (error) {\n failure = error;\n }\n if (platform === null || !platforms.includes(platform)) return null;\n throw new MissingModuleError(module, platform, failure);\n}\n"]}
@@ -0,0 +1,133 @@
1
+ /**
2
+ * `ongoingNotification`, which keeps an Android ongoing notification in step with a signal: what a
3
+ * Live Activity is on iOS. Android draws it from data, not a layout, and draws the time itself: a
4
+ * `timer` counts with no update from the app. Where the content qualifies, Android 16 promotes it to
5
+ * a Live Update, with a chip in the status bar.
6
+ *
7
+ * A tap on one of its actions is stored by this package's native half, in the app's process or a
8
+ * new one Android starts for it, and handed to `onTaps` once, when the app is running. Off Android
9
+ * there is no such module, and `start()` answers false.
10
+ */
11
+ import { InjectionToken, type Signal } from '@angular/core';
12
+ /** A moment: a `Date`, or milliseconds since the epoch. */
13
+ type Moment = Date | number;
14
+ /** A button under the notification: one whose tap `onTaps` is handed, or one that opens a link. */
15
+ export type OngoingNotificationAction = {
16
+ readonly title: string;
17
+ /** What a tap hands `onTaps`. */
18
+ readonly target: string;
19
+ } | {
20
+ readonly title: string;
21
+ /** Opens the app at this link when tapped. */
22
+ readonly url: string;
23
+ };
24
+ /** What the notification shows. */
25
+ export interface OngoingNotificationContent {
26
+ readonly title: string;
27
+ readonly text?: string;
28
+ /** Time Android counts by itself: up from `since`, or down to `until`. */
29
+ readonly timer?: {
30
+ readonly since: Moment;
31
+ } | {
32
+ readonly until: Moment;
33
+ };
34
+ /** A bar filled to `value` of `max`, 1 unless given as a `ui-progress` is, or a moving one. */
35
+ readonly progress?: {
36
+ readonly value: number;
37
+ readonly max?: number;
38
+ } | {
39
+ readonly indeterminate: true;
40
+ };
41
+ /** A few characters for the status bar chip of a Live Update, where Android shows one. */
42
+ readonly chip?: string;
43
+ /** Up to three, which is what Android shows. */
44
+ readonly actions?: readonly OngoingNotificationAction[];
45
+ }
46
+ export interface OngoingNotificationOptions {
47
+ /**
48
+ * The notification channel, which the user sees by `name` in the app's notification settings.
49
+ * It is created when it does not exist yet.
50
+ */
51
+ readonly channel: {
52
+ readonly id: string;
53
+ readonly name: string;
54
+ };
55
+ /** Tells this notification from another on the same channel. The channel's id unless given. */
56
+ readonly id?: string;
57
+ /** Opens the app at this link when the notification itself is tapped. */
58
+ readonly url?: string;
59
+ /**
60
+ * Called with the targets of the actions tapped since the last call, oldest first, including
61
+ * those tapped while the app was not running. A handler that throws, or rejects, goes to the
62
+ * `ErrorHandler`.
63
+ */
64
+ readonly onTaps?: (taps: readonly string[]) => void;
65
+ }
66
+ /**
67
+ * The content as the native module takes it: each moment in milliseconds, the bar in the whole
68
+ * numbers Android draws, and every action with a target, which is what Android tells it by.
69
+ */
70
+ type NativeContent = Omit<OngoingNotificationContent, 'timer' | 'progress' | 'actions'> & {
71
+ readonly timer?: {
72
+ readonly since: number;
73
+ } | {
74
+ readonly until: number;
75
+ };
76
+ readonly progress?: {
77
+ readonly value: number;
78
+ readonly max: number;
79
+ } | {
80
+ readonly indeterminate: true;
81
+ };
82
+ readonly actions?: readonly {
83
+ title: string;
84
+ target: string;
85
+ url?: string;
86
+ }[];
87
+ };
88
+ /** This package's native module on Android. A test provides a stand-in. */
89
+ export interface NativeOngoingNotifications {
90
+ /** Asks for the notification permission where it is needed, then shows or replaces `id`. */
91
+ show(id: string, content: NativeContent, options: Pick<OngoingNotificationOptions, 'channel' | 'url'>): Promise<{
92
+ readonly promoted: boolean;
93
+ }>;
94
+ cancel(id: string): void;
95
+ isActive(id: string): boolean;
96
+ /** The targets tapped on `id` and not yet taken, which are then taken. */
97
+ takeTaps(id: string): readonly string[];
98
+ openPromotionSettings(): void;
99
+ /** An action was tapped or a notification swiped away, on any id. */
100
+ onChange(listener: () => void): () => void;
101
+ }
102
+ /** Null off Android, and in a build made before this package had a native half. */
103
+ export declare const ONGOING_NOTIFICATIONS: InjectionToken<NativeOngoingNotifications | null>;
104
+ /** An ongoing notification kept in step with a signal. */
105
+ export interface OngoingNotificationRef {
106
+ /** Whether the notification is showing and following the signal. */
107
+ readonly active: Signal<boolean>;
108
+ /** Whether Android shows it as a Live Update, which the user can turn off for the app. */
109
+ readonly promoted: Signal<boolean>;
110
+ /**
111
+ * Why the last start or update did not happen. A start the user or the system refuses, as when
112
+ * notifications are turned off, is only kept here; a failed update also goes to the
113
+ * `ErrorHandler`.
114
+ */
115
+ readonly error: Signal<unknown>;
116
+ /**
117
+ * Shows the notification with the signal's value, asking for the notification permission first
118
+ * where Android needs it. Answers whether it is showing.
119
+ */
120
+ start(): Promise<boolean>;
121
+ /** Removes the notification. */
122
+ end(): void;
123
+ /** Opens the settings screen where the user turns Live Updates on for the app. */
124
+ openPromotionSettings(): void;
125
+ }
126
+ /**
127
+ * Keeps an ongoing notification in step with `content`: once started, every change to the signal
128
+ * replaces what it shows, silently. One left showing from before the app started is picked up.
129
+ * Destroying the injector stops the updates and leaves the notification, as it outlives the app.
130
+ * Call it in an injection context.
131
+ */
132
+ export declare function ongoingNotification(content: Signal<OngoingNotificationContent>, options: OngoingNotificationOptions): OngoingNotificationRef;
133
+ export {};
@@ -0,0 +1,141 @@
1
+ /**
2
+ * `ongoingNotification`, which keeps an Android ongoing notification in step with a signal: what a
3
+ * Live Activity is on iOS. Android draws it from data, not a layout, and draws the time itself: a
4
+ * `timer` counts with no update from the app. Where the content qualifies, Android 16 promotes it to
5
+ * a Live Update, with a chip in the status bar.
6
+ *
7
+ * A tap on one of its actions is stored by this package's native half, in the app's process or a
8
+ * new one Android starts for it, and handed to `onTaps` once, when the app is running. Off Android
9
+ * there is no such module, and `start()` answers false.
10
+ */
11
+ import { DestroyRef, ErrorHandler, InjectionToken, effect, inject, signal, untracked, } from '@angular/core';
12
+ import { isolated } from "./native.js";
13
+ /** Null off Android, and in a build made before this package had a native half. */
14
+ export const ONGOING_NOTIFICATIONS = new InjectionToken('angular-native.ongoingNotifications', {
15
+ factory: () => {
16
+ const module = globalThis.expo?.modules?.NgNativeOngoingNotification;
17
+ if (!module)
18
+ return null;
19
+ return {
20
+ show: (id, content, options) => module.show(id, content, options),
21
+ cancel: (id) => module.cancel(id),
22
+ isActive: (id) => module.isActive(id),
23
+ takeTaps: (id) => module.takeTaps(id),
24
+ openPromotionSettings: () => module.openPromotionSettings(),
25
+ onChange: (listener) => {
26
+ const subscription = module.addListener('onChange', listener);
27
+ return () => subscription.remove();
28
+ },
29
+ };
30
+ },
31
+ });
32
+ const milliseconds = (moment) => new Date(moment).getTime();
33
+ /** The steps of a bar, which Android takes as whole numbers. */
34
+ const STEPS = 1000;
35
+ function forNative({ timer, progress, actions, ...content }) {
36
+ return {
37
+ ...content,
38
+ ...(timer && {
39
+ timer: 'since' in timer
40
+ ? { since: milliseconds(timer.since) }
41
+ : { until: milliseconds(timer.until) },
42
+ }),
43
+ ...(progress && {
44
+ progress: 'value' in progress
45
+ ? { value: Math.round((progress.value / (progress.max ?? 1)) * STEPS), max: STEPS }
46
+ : progress,
47
+ }),
48
+ ...(actions && {
49
+ actions: actions.map((action) => 'url' in action ? { ...action, target: action.url } : action),
50
+ }),
51
+ };
52
+ }
53
+ /**
54
+ * Keeps an ongoing notification in step with `content`: once started, every change to the signal
55
+ * replaces what it shows, silently. One left showing from before the app started is picked up.
56
+ * Destroying the injector stops the updates and leaves the notification, as it outlives the app.
57
+ * Call it in an injection context.
58
+ */
59
+ export function ongoingNotification(content, options) {
60
+ const errors = inject(ErrorHandler);
61
+ const native = inject(ONGOING_NOTIFICATIONS);
62
+ const id = options.id ?? options.channel.id;
63
+ const shown = { channel: options.channel, url: options.url };
64
+ const active = signal(native?.isActive(id) ?? false, /* @ts-ignore */
65
+ ...(ngDevMode ? [{ debugName: "active" }] : /* istanbul ignore next */ []));
66
+ const promoted = signal(false, /* @ts-ignore */
67
+ ...(ngDevMode ? [{ debugName: "promoted" }] : /* istanbul ignore next */ []));
68
+ const error = signal(null, /* @ts-ignore */
69
+ ...(ngDevMode ? [{ debugName: "error" }] : /* istanbul ignore next */ []));
70
+ let sent = null;
71
+ const show = async (next) => {
72
+ sent = next;
73
+ const result = await native.show(id, forNative(next), shown);
74
+ promoted.set(result.promoted);
75
+ error.set(null);
76
+ };
77
+ const forget = () => {
78
+ sent = null;
79
+ active.set(false);
80
+ promoted.set(false);
81
+ };
82
+ /** Shows what the signal holds while the notification is showing, unless Android has it. */
83
+ const follow = (next) => {
84
+ if (!untracked(active) || next === sent)
85
+ return;
86
+ show(next).catch((failure) => {
87
+ error.set(failure);
88
+ errors.handleError(failure);
89
+ });
90
+ };
91
+ effect(() => follow(content()));
92
+ /** The taps Android stored, and whether the user has swiped the notification away. */
93
+ const collect = () => {
94
+ const taps = native.takeTaps(id);
95
+ if (untracked(active) && !native.isActive(id))
96
+ forget();
97
+ if (!taps.length)
98
+ return;
99
+ isolated(() => options.onTaps?.(taps), (failure) => errors.handleError(failure));
100
+ // Not left to the effect: React Native runs no timers while the app is in the background, so
101
+ // the pass that would run it waits until the app is back in front.
102
+ follow(untracked(content));
103
+ };
104
+ if (native) {
105
+ inject(DestroyRef).onDestroy(native.onChange(collect));
106
+ // Not yet: the caller's own fields may not be set while this one is.
107
+ queueMicrotask(collect);
108
+ }
109
+ return {
110
+ active: active.asReadonly(),
111
+ promoted: promoted.asReadonly(),
112
+ error: error.asReadonly(),
113
+ async start() {
114
+ if (!native) {
115
+ error.set(new Error('An ongoing notification needs Android, and an app built since @ng-native/expo was ' +
116
+ 'installed ("npx expo run:android", or a new EAS build).'));
117
+ return false;
118
+ }
119
+ const first = untracked(content);
120
+ try {
121
+ await show(first);
122
+ active.set(true);
123
+ // The signal may have moved on while Android asked for the permission.
124
+ const latest = untracked(content);
125
+ if (latest !== first)
126
+ await show(latest);
127
+ return true;
128
+ }
129
+ catch (failure) {
130
+ error.set(failure);
131
+ return false;
132
+ }
133
+ },
134
+ end() {
135
+ native?.cancel(id);
136
+ forget();
137
+ },
138
+ openPromotionSettings: () => native?.openPromotionSettings(),
139
+ };
140
+ }
141
+ //# sourceMappingURL=ongoing-notification.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ongoing-notification.js","sourceRoot":"","sources":["../src/ongoing-notification.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * `ongoingNotification`, which keeps an Android ongoing notification in step with a signal: what a\n * Live Activity is on iOS. Android draws it from data, not a layout, and draws the time itself: a\n * `timer` counts with no update from the app. Where the content qualifies, Android 16 promotes it to\n * a Live Update, with a chip in the status bar.\n *\n * A tap on one of its actions is stored by this package's native half, in the app's process or a\n * new one Android starts for it, and handed to `onTaps` once, when the app is running. Off Android\n * there is no such module, and `start()` answers false.\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 } from './native.ts';\n\n/** A moment: a `Date`, or milliseconds since the epoch. */\ntype Moment = Date | number;\n\n/** A button under the notification: one whose tap `onTaps` is handed, or one that opens a link. */\nexport type OngoingNotificationAction =\n | {\n readonly title: string;\n /** What a tap hands `onTaps`. */\n readonly target: string;\n }\n | {\n readonly title: string;\n /** Opens the app at this link when tapped. */\n readonly url: string;\n };\n\n/** What the notification shows. */\nexport interface OngoingNotificationContent {\n readonly title: string;\n readonly text?: string;\n /** Time Android counts by itself: up from `since`, or down to `until`. */\n readonly timer?: { readonly since: Moment } | { readonly until: Moment };\n /** A bar filled to `value` of `max`, 1 unless given as a `ui-progress` is, or a moving one. */\n readonly progress?:\n { readonly value: number; readonly max?: number } | { readonly indeterminate: true };\n /** A few characters for the status bar chip of a Live Update, where Android shows one. */\n readonly chip?: string;\n /** Up to three, which is what Android shows. */\n readonly actions?: readonly OngoingNotificationAction[];\n}\n\nexport interface OngoingNotificationOptions {\n /**\n * The notification channel, which the user sees by `name` in the app's notification settings.\n * It is created when it does not exist yet.\n */\n readonly channel: { readonly id: string; readonly name: string };\n /** Tells this notification from another on the same channel. The channel's id unless given. */\n readonly id?: string;\n /** Opens the app at this link when the notification itself is tapped. */\n readonly url?: string;\n /**\n * Called with the targets of the actions tapped since the last call, oldest first, including\n * those tapped while the app was not running. A handler that throws, or rejects, goes to the\n * `ErrorHandler`.\n */\n readonly onTaps?: (taps: readonly string[]) => void;\n}\n\n/**\n * The content as the native module takes it: each moment in milliseconds, the bar in the whole\n * numbers Android draws, and every action with a target, which is what Android tells it by.\n */\ntype NativeContent = Omit<OngoingNotificationContent, 'timer' | 'progress' | 'actions'> & {\n readonly timer?: { readonly since: number } | { readonly until: number };\n readonly progress?:\n { readonly value: number; readonly max: number } | { readonly indeterminate: true };\n readonly actions?: readonly { title: string; target: string; url?: string }[];\n};\n\n/** This package's native module on Android. A test provides a stand-in. */\nexport interface NativeOngoingNotifications {\n /** Asks for the notification permission where it is needed, then shows or replaces `id`. */\n show(\n id: string,\n content: NativeContent,\n options: Pick<OngoingNotificationOptions, 'channel' | 'url'>,\n ): Promise<{ readonly promoted: boolean }>;\n cancel(id: string): void;\n isActive(id: string): boolean;\n /** The targets tapped on `id` and not yet taken, which are then taken. */\n takeTaps(id: string): readonly string[];\n openPromotionSettings(): void;\n /** An action was tapped or a notification swiped away, on any id. */\n onChange(listener: () => void): () => void;\n}\n\ntype ExpoModule = Omit<NativeOngoingNotifications, 'onChange'> & {\n addListener(event: 'onChange', listener: () => void): { remove(): void };\n};\n\n/** Null off Android, and in a build made before this package had a native half. */\nexport const ONGOING_NOTIFICATIONS = new InjectionToken<NativeOngoingNotifications | null>(\n 'angular-native.ongoingNotifications',\n {\n factory: () => {\n const module = (\n globalThis as { expo?: { modules?: { NgNativeOngoingNotification?: ExpoModule } } }\n ).expo?.modules?.NgNativeOngoingNotification;\n if (!module) return null;\n return {\n show: (id, content, options) => module.show(id, content, options),\n cancel: (id) => module.cancel(id),\n isActive: (id) => module.isActive(id),\n takeTaps: (id) => module.takeTaps(id),\n openPromotionSettings: () => module.openPromotionSettings(),\n onChange: (listener) => {\n const subscription = module.addListener('onChange', listener);\n return () => subscription.remove();\n },\n };\n },\n },\n);\n\n/** An ongoing notification kept in step with a signal. */\nexport interface OngoingNotificationRef {\n /** Whether the notification is showing and following the signal. */\n readonly active: Signal<boolean>;\n /** Whether Android shows it as a Live Update, which the user can turn off for the app. */\n readonly promoted: Signal<boolean>;\n /**\n * Why the last start or update did not happen. A start the user or the system refuses, as when\n * notifications are turned off, is only kept here; a failed update also goes to the\n * `ErrorHandler`.\n */\n readonly error: Signal<unknown>;\n /**\n * Shows the notification with the signal's value, asking for the notification permission first\n * where Android needs it. Answers whether it is showing.\n */\n start(): Promise<boolean>;\n /** Removes the notification. */\n end(): void;\n /** Opens the settings screen where the user turns Live Updates on for the app. */\n openPromotionSettings(): void;\n}\n\nconst milliseconds = (moment: Moment) => new Date(moment).getTime();\n\n/** The steps of a bar, which Android takes as whole numbers. */\nconst STEPS = 1000;\n\nfunction forNative({\n timer,\n progress,\n actions,\n ...content\n}: OngoingNotificationContent): NativeContent {\n return {\n ...content,\n ...(timer && {\n timer:\n 'since' in timer\n ? { since: milliseconds(timer.since) }\n : { until: milliseconds(timer.until) },\n }),\n ...(progress && {\n progress:\n 'value' in progress\n ? { value: Math.round((progress.value / (progress.max ?? 1)) * STEPS), max: STEPS }\n : progress,\n }),\n ...(actions && {\n actions: actions.map((action) =>\n 'url' in action ? { ...action, target: action.url } : action,\n ),\n }),\n };\n}\n\n/**\n * Keeps an ongoing notification in step with `content`: once started, every change to the signal\n * replaces what it shows, silently. One left showing from before the app started is picked up.\n * Destroying the injector stops the updates and leaves the notification, as it outlives the app.\n * Call it in an injection context.\n */\nexport function ongoingNotification(\n content: Signal<OngoingNotificationContent>,\n options: OngoingNotificationOptions,\n): OngoingNotificationRef {\n const errors = inject(ErrorHandler);\n const native = inject(ONGOING_NOTIFICATIONS);\n const id = options.id ?? options.channel.id;\n const shown = { channel: options.channel, url: options.url };\n const active = signal(native?.isActive(id) ?? false);\n const promoted = signal(false);\n const error = signal<unknown>(null);\n\n let sent: OngoingNotificationContent | null = null;\n\n const show = async (next: OngoingNotificationContent) => {\n sent = next;\n const result = await native!.show(id, forNative(next), shown);\n promoted.set(result.promoted);\n error.set(null);\n };\n\n const forget = () => {\n sent = null;\n active.set(false);\n promoted.set(false);\n };\n\n /** Shows what the signal holds while the notification is showing, unless Android has it. */\n const follow = (next: OngoingNotificationContent) => {\n if (!untracked(active) || next === sent) return;\n show(next).catch((failure: unknown) => {\n error.set(failure);\n errors.handleError(failure);\n });\n };\n\n effect(() => follow(content()));\n\n /** The taps Android stored, and whether the user has swiped the notification away. */\n const collect = () => {\n const taps = native!.takeTaps(id);\n if (untracked(active) && !native!.isActive(id)) forget();\n if (!taps.length) return;\n isolated(\n () => options.onTaps?.(taps),\n (failure) => errors.handleError(failure),\n );\n // Not left to the effect: React Native runs no timers while the app is in the background, so\n // the pass that would run it waits until the app is back in front.\n follow(untracked(content));\n };\n\n if (native) {\n inject(DestroyRef).onDestroy(native.onChange(collect));\n // Not yet: the caller's own fields may not be set while this one is.\n queueMicrotask(collect);\n }\n\n return {\n active: active.asReadonly(),\n promoted: promoted.asReadonly(),\n error: error.asReadonly(),\n async start() {\n if (!native) {\n error.set(\n new Error(\n 'An ongoing notification needs Android, and an app built since @ng-native/expo was ' +\n 'installed (\"npx expo run:android\", or a new EAS build).',\n ),\n );\n return false;\n }\n const first = untracked(content);\n try {\n await show(first);\n active.set(true);\n // The signal may have moved on while Android asked for the permission.\n const latest = untracked(content);\n if (latest !== first) await show(latest);\n return true;\n } catch (failure) {\n error.set(failure);\n return false;\n }\n },\n end() {\n native?.cancel(id);\n forget();\n },\n openPromotionSettings: () => native?.openPromotionSettings(),\n };\n}\n"]}
package/dist/player.d.ts CHANGED
@@ -34,6 +34,8 @@ export interface NativePlayer {
34
34
  timeUpdateEventInterval?: number;
35
35
  /** Read directly rather than from an event: `expo-audio`'s status carries no volume field. */
36
36
  readonly volume?: number;
37
+ /** `expo-video` only, where an event says it only once it changes. */
38
+ readonly muted?: boolean;
37
39
  }
38
40
  /**
39
41
  * `expo-audio`'s one status event, `playbackStatusUpdate`. Unlike `expo-video`, which splits its
package/dist/player.js CHANGED
@@ -42,7 +42,12 @@ const INITIAL = {
42
42
  * by `watchAudioPlayer` below rather than this function.
43
43
  */
44
44
  export function watchPlayer(player, options = {}) {
45
- const state = signal(INITIAL, /* @ts-ignore */
45
+ // Both are the player's own, set where it was made: no event says either until it changes.
46
+ const state = signal({
47
+ ...INITIAL,
48
+ muted: player?.muted ?? INITIAL.muted,
49
+ volume: player?.volume ?? INITIAL.volume,
50
+ }, /* @ts-ignore */
46
51
  ...(ngDevMode ? [{ debugName: "state" }] : /* istanbul ignore next */ []));
47
52
  if (!player)
48
53
  return { state: state.asReadonly(), stop: () => { } };
@@ -54,7 +59,8 @@ export function watchPlayer(player, options = {}) {
54
59
  listen(player, 'volumeChange', ({ volume }) => patch({ volume })),
55
60
  listen(player, 'playToEnd', () => patch({ ended: true, playing: false })),
56
61
  listen(player, 'timeUpdate', ({ currentTime }) => patch({ currentTime })),
57
- listen(player, 'sourceChange', () => patch({ ...INITIAL, status: 'loading' })),
62
+ // What was the old source's starts over. Muted and volume are the player's, and it keeps them.
63
+ listen(player, 'sourceChange', () => patch({ status: 'loading', playing: false, currentTime: 0, duration: 0, ended: false })),
58
64
  // The only event that says how long the video is: nothing else carries a duration.
59
65
  listen(player, 'sourceLoad', ({ duration }) => patch({ duration })),
60
66
  ];