@ng-native/expo 0.8.0 → 0.10.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.
@@ -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 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"]}
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":"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,MAAM,aAAa,CAAC;AAmFvC,mFAAmF;AACnF,MAAM,CAAC,MAAM,qBAAqB,GAAG,IAAI,cAAc,CACrD,qCAAqC,EACrC;IACE,OAAO,EAAE,GAAG,EAAE;QACZ,MAAM,MAAM,GACV,UACD,CAAC,IAAI,EAAE,OAAO,EAAE,2BAA2B,CAAC;QAC7C,IAAI,CAAC,MAAM;YAAE,OAAO,IAAI,CAAC;QACzB,OAAO;YACL,IAAI,EAAE,CAAC,EAAE,EAAE,OAAO,EAAE,OAAO,EAAE,EAAE,CAAC,MAAM,CAAC,IAAI,CAAC,EAAE,EAAE,OAAO,EAAE,OAAO,CAAC;YACjE,MAAM,EAAE,CAAC,EAAE,EAAE,EAAE,CAAC,MAAM,CAAC,MAAM,CAAC,EAAE,CAAC;YACjC,QAAQ,EAAE,CAAC,EAAE,EAAE,EAAE,CAAC,MAAM,CAAC,QAAQ,CAAC,EAAE,CAAC;YACrC,QAAQ,EAAE,CAAC,EAAE,EAAE,EAAE,CAAC,MAAM,CAAC,QAAQ,CAAC,EAAE,CAAC;YACrC,qBAAqB,EAAE,GAAG,EAAE,CAAC,MAAM,CAAC,qBAAqB,EAAE;YAC3D,QAAQ,EAAE,CAAC,QAAQ,EAAE,EAAE;gBACrB,MAAM,YAAY,GAAG,MAAM,CAAC,WAAW,CAAC,UAAU,EAAE,QAAQ,CAAC,CAAC;gBAC9D,OAAO,GAAG,EAAE,CAAC,YAAY,CAAC,MAAM,EAAE,CAAC;YACrC,CAAC;SACF,CAAC;IACJ,CAAC;CACF,CACF,CAAC;AAyBF,MAAM,YAAY,GAAG,CAAC,MAAc,EAAE,EAAE,CAAC,IAAI,IAAI,CAAC,MAAM,CAAC,CAAC,OAAO,EAAE,CAAC;AAEpE,gEAAgE;AAChE,MAAM,KAAK,GAAG,IAAI,CAAC;AAEnB,SAAS,SAAS,CAAC,EACjB,KAAK,EACL,QAAQ,EACR,OAAO,EACP,GAAG,OAAO,EACiB;IAC3B,OAAO;QACL,GAAG,OAAO;QACV,GAAG,CAAC,KAAK,IAAI;YACX,KAAK,EACH,OAAO,IAAI,KAAK;gBACd,CAAC,CAAC,EAAE,KAAK,EAAE,YAAY,CAAC,KAAK,CAAC,KAAK,CAAC,EAAE;gBACtC,CAAC,CAAC,EAAE,KAAK,EAAE,YAAY,CAAC,KAAK,CAAC,KAAK,CAAC,EAAE;SAC3C,CAAC;QACF,GAAG,CAAC,QAAQ,IAAI;YACd,QAAQ,EACN,OAAO,IAAI,QAAQ;gBACjB,CAAC,CAAC,EAAE,KAAK,EAAE,IAAI,CAAC,KAAK,CAAC,CAAC,QAAQ,CAAC,KAAK,GAAG,CAAC,QAAQ,CAAC,GAAG,IAAI,CAAC,CAAC,CAAC,GAAG,KAAK,CAAC,EAAE,GAAG,EAAE,KAAK,EAAE;gBACnF,CAAC,CAAC,QAAQ;SACf,CAAC;QACF,GAAG,CAAC,OAAO,IAAI;YACb,OAAO,EAAE,OAAO,CAAC,GAAG,CAAC,CAAC,MAAM,EAAE,EAAE,CAC9B,KAAK,IAAI,MAAM,CAAC,CAAC,CAAC,EAAE,GAAG,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,GAAG,EAAE,CAAC,CAAC,CAAC,MAAM,CAC7D;SACF,CAAC;KACH,CAAC;AACJ,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,mBAAmB,CACjC,OAA2C,EAC3C,OAAmC;IAEnC,MAAM,MAAM,GAAG,MAAM,CAAC,YAAY,CAAC,CAAC;IACpC,MAAM,MAAM,GAAG,MAAM,CAAC,qBAAqB,CAAC,CAAC;IAC7C,MAAM,EAAE,GAAG,OAAO,CAAC,EAAE,IAAI,OAAO,CAAC,OAAO,CAAC,EAAE,CAAC;IAC5C,MAAM,KAAK,GAAG,EAAE,OAAO,EAAE,OAAO,CAAC,OAAO,EAAE,GAAG,EAAE,OAAO,CAAC,GAAG,EAAE,CAAC;IAC7D,MAAM,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,QAAQ,CAAC,EAAE,CAAC,IAAI,KAAK;+EAAC,CAAC;IACrD,MAAM,QAAQ,GAAG,MAAM,CAAC,KAAK;iFAAC,CAAC;IAC/B,MAAM,KAAK,GAAG,MAAM,CAAU,IAAI;8EAAC,CAAC;IAEpC,IAAI,IAAI,GAAsC,IAAI,CAAC;IAEnD,MAAM,IAAI,GAAG,KAAK,EAAE,IAAgC,EAAE,EAAE;QACtD,IAAI,GAAG,IAAI,CAAC;QACZ,MAAM,MAAM,GAAG,MAAM,MAAO,CAAC,IAAI,CAAC,EAAE,EAAE,SAAS,CAAC,IAAI,CAAC,EAAE,KAAK,CAAC,CAAC;QAC9D,QAAQ,CAAC,GAAG,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC;QAC9B,KAAK,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;IAClB,CAAC,CAAC;IAEF,MAAM,MAAM,GAAG,GAAG,EAAE;QAClB,IAAI,GAAG,IAAI,CAAC;QACZ,MAAM,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC;QAClB,QAAQ,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC;IACtB,CAAC,CAAC;IAEF,4FAA4F;IAC5F,MAAM,MAAM,GAAG,CAAC,IAAgC,EAAE,EAAE;QAClD,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC,IAAI,IAAI,KAAK,IAAI;YAAE,OAAO;QAChD,IAAI,CAAC,IAAI,CAAC,CAAC,KAAK,CAAC,CAAC,OAAgB,EAAE,EAAE;YACpC,KAAK,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC;YACnB,MAAM,CAAC,WAAW,CAAC,OAAO,CAAC,CAAC;QAC9B,CAAC,CAAC,CAAC;IACL,CAAC,CAAC;IAEF,MAAM,CAAC,GAAG,EAAE,CAAC,MAAM,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC;IAEhC,sFAAsF;IACtF,MAAM,OAAO,GAAG,GAAG,EAAE;QACnB,MAAM,IAAI,GAAG,MAAO,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAC;QAClC,IAAI,SAAS,CAAC,MAAM,CAAC,IAAI,CAAC,MAAO,CAAC,QAAQ,CAAC,EAAE,CAAC;YAAE,MAAM,EAAE,CAAC;QACzD,IAAI,CAAC,IAAI,CAAC,MAAM;YAAE,OAAO;QACzB,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;QACF,6FAA6F;QAC7F,mEAAmE;QACnE,MAAM,CAAC,SAAS,CAAC,OAAO,CAAC,CAAC,CAAC;IAC7B,CAAC,CAAC;IAEF,IAAI,MAAM,EAAE,CAAC;QACX,MAAM,CAAC,UAAU,CAAC,CAAC,SAAS,CAAC,MAAM,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAC,CAAC;QACvD,qEAAqE;QACrE,cAAc,CAAC,OAAO,CAAC,CAAC;IAC1B,CAAC;IAED,OAAO;QACL,MAAM,EAAE,MAAM,CAAC,UAAU,EAAE;QAC3B,QAAQ,EAAE,QAAQ,CAAC,UAAU,EAAE;QAC/B,KAAK,EAAE,KAAK,CAAC,UAAU,EAAE;QACzB,KAAK,CAAC,KAAK;YACT,IAAI,CAAC,MAAM,EAAE,CAAC;gBACZ,KAAK,CAAC,GAAG,CACP,IAAI,KAAK,CACP,oFAAoF;oBAClF,yDAAyD,CAC5D,CACF,CAAC;gBACF,OAAO,KAAK,CAAC;YACf,CAAC;YACD,MAAM,KAAK,GAAG,SAAS,CAAC,OAAO,CAAC,CAAC;YACjC,IAAI,CAAC;gBACH,MAAM,IAAI,CAAC,KAAK,CAAC,CAAC;gBAClB,MAAM,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;gBACjB,uEAAuE;gBACvE,MAAM,MAAM,GAAG,SAAS,CAAC,OAAO,CAAC,CAAC;gBAClC,IAAI,MAAM,KAAK,KAAK;oBAAE,MAAM,IAAI,CAAC,MAAM,CAAC,CAAC;gBACzC,OAAO,IAAI,CAAC;YACd,CAAC;YAAC,OAAO,OAAO,EAAE,CAAC;gBACjB,KAAK,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC;gBACnB,OAAO,KAAK,CAAC;YACf,CAAC;QACH,CAAC;QACD,GAAG;YACD,MAAM,EAAE,MAAM,CAAC,EAAE,CAAC,CAAC;YACnB,MAAM,EAAE,CAAC;QACX,CAAC;QACD,qBAAqB,EAAE,GAAG,EAAE,CAAC,MAAM,EAAE,qBAAqB,EAAE;KAC7D,CAAC;AACJ,CAAC","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
  ];
@@ -1 +1 @@
1
- {"version":3,"file":"player.js","sourceRoot":"","sources":["../src/player.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * What `videoPlayer` (`@ng-native/expo/video`) and `audioPlayer` (`@ng-native/expo/audio`) share:\n * the `Player` each answers, its state, and the watchers that keep that state.\n *\n * ```ts\n * protected readonly player = videoPlayer(this.source, { timeUpdate: 0.5 });\n *\n * // <expo-video [player]=\"player.native\" />\n * // {{ player.state().currentTime }}\n * ```\n *\n * `useVideoPlayer` and `useAudioPlayer` are the documented way in, and the player they hand back\n * is not the React part - `createVideoPlayer` gives the same object. What the hooks add is the\n * two things a component actually needs and cannot get otherwise: the player is *released* when\n * the component goes, and its changing state is *readable*, because the player's own properties\n * are plain mutable fields that no template can watch.\n *\n * The player itself is passed straight through: it is a shared object with `play()`, `seekBy()`\n * and the rest already on it, and wrapping those would be a second place for each to be wrong.\n *\n * Each player has an entry point of its own because each needs its own native module: Metro fails\n * a build on a `require` it cannot resolve, so one entry point for both made an app install both.\n */\nimport { DestroyRef, inject, signal, type Signal } from '@angular/core';\n\n/** The player's own status, which is the same word in both modules. */\nexport type PlayerStatus = 'idle' | 'loading' | 'readyToPlay' | 'error';\n\n/** The slice of a player this reads. Both modules' players satisfy it structurally. */\nexport interface NativePlayer {\n addListener(event: string, listener: (payload: never) => void): { remove(): void };\n release?(): void;\n /** Seconds between `timeUpdate` events. Zero, its default, means the event never fires. `expo-video` only. */\n timeUpdateEventInterval?: number;\n /** Read directly rather than from an event: `expo-audio`'s status carries no volume field. */\n readonly volume?: number;\n}\n\n/**\n * `expo-audio`'s one status event, `playbackStatusUpdate`. Unlike `expo-video`, which splits its\n * state across several named events, `AudioPlayer` reports everything - including errors - on a\n * single event, at the interval `updateInterval` was created with.\n */\nexport interface NativeAudioStatus {\n readonly playing: boolean;\n readonly mute: boolean;\n readonly duration: number;\n readonly currentTime: number;\n readonly isLoaded: boolean;\n readonly didJustFinish: boolean;\n readonly error: string | null;\n}\n\nexport interface PlayerState {\n readonly playing: boolean;\n readonly status: PlayerStatus;\n /** Seconds. Only moves while `timeUpdateEventInterval` is non-zero. */\n readonly currentTime: number;\n readonly duration: number;\n readonly muted: boolean;\n readonly volume: number;\n /** Set once playback has run to the end, and cleared when it starts again. */\n readonly ended: boolean;\n}\n\nconst INITIAL: PlayerState = {\n playing: false,\n status: 'idle',\n currentTime: 0,\n duration: 0,\n muted: false,\n volume: 1,\n ended: false,\n};\n\n/**\n * Watch an `expo-video` player, and stop watching when the caller is done with it.\n *\n * `timeUpdate` defaults to off, so `currentTime` would never move. A caller that wants a progress\n * bar asks for one by passing an interval, and one that does not pays nothing: the event is the\n * most frequent thing the player emits.\n *\n * `expo-audio`'s player reports its state differently - one event, not several - so it is watched\n * by `watchAudioPlayer` below rather than this function.\n */\nexport function watchPlayer(\n player: NativePlayer | null,\n options: { timeUpdate?: number } = {},\n): { state: Signal<PlayerState>; stop: () => void } {\n const state = signal(INITIAL);\n if (!player) return { state: state.asReadonly(), stop: () => {} };\n\n const patch = (change: Partial<PlayerState>) => state.update((last) => ({ ...last, ...change }));\n const subscriptions = [\n listen(player, 'statusChange', ({ status }: { status: PlayerStatus }) => patch({ status })),\n listen(player, 'playingChange', ({ isPlaying }: { isPlaying: boolean }) =>\n patch({ playing: isPlaying, ...(isPlaying ? { ended: false } : {}) }),\n ),\n listen(player, 'mutedChange', ({ muted }: { muted: boolean }) => patch({ muted })),\n listen(player, 'volumeChange', ({ volume }: { volume: number }) => patch({ volume })),\n listen(player, 'playToEnd', () => patch({ ended: true, playing: false })),\n listen(player, 'timeUpdate', ({ currentTime }: { currentTime: number }) =>\n patch({ currentTime }),\n ),\n listen(player, 'sourceChange', () => patch({ ...INITIAL, status: 'loading' })),\n // The only event that says how long the video is: nothing else carries a duration.\n listen(player, 'sourceLoad', ({ duration }: { duration: number }) => patch({ duration })),\n ];\n\n if (options.timeUpdate !== undefined) player.timeUpdateEventInterval = options.timeUpdate;\n\n return {\n state: state.asReadonly(),\n stop: () => {\n for (const subscription of subscriptions) subscription();\n // Releasing is the half a hook does that nothing else will: a player left alive holds its\n // decoder, its audio session and, on iOS, the now-playing controls.\n player.release?.();\n },\n };\n}\n\nfunction listen<T>(player: NativePlayer, event: string, handler: (payload: T) => void): () => void {\n const subscription = player.addListener(event, handler as (payload: never) => void);\n return () => subscription.remove();\n}\n\n/**\n * Watch an `expo-audio` player, and stop watching when the caller is done with it.\n *\n * `AudioPlayer` reports its whole state on `playbackStatusUpdate`, at the interval it was created\n * with, rather than splitting it across the several events `expo-video` uses - so unlike\n * `watchPlayer`, there is one listener, not several, and nothing to reset on a source change: the\n * next status update carries the new source's own state.\n */\nexport function watchAudioPlayer(player: NativePlayer | null): {\n state: Signal<PlayerState>;\n stop: () => void;\n} {\n const state = signal(INITIAL);\n if (!player) return { state: state.asReadonly(), stop: () => {} };\n\n const subscription = listen(player, 'playbackStatusUpdate', (status: NativeAudioStatus) =>\n state.set({\n playing: status.playing,\n status: status.error !== null ? 'error' : status.isLoaded ? 'readyToPlay' : 'loading',\n currentTime: status.currentTime,\n duration: status.duration,\n muted: status.mute,\n // Not on the status event; the player's own property is the current value.\n volume: player.volume ?? INITIAL.volume,\n ended: status.didJustFinish,\n }),\n );\n\n return {\n state: state.asReadonly(),\n stop: () => {\n subscription();\n // Releasing is the half a hook does that nothing else will: a player left alive holds its\n // decoder, its audio session and, on iOS, the now-playing controls.\n player.release?.();\n },\n };\n}\n\nexport interface Player<T> {\n /** The module's own player, for `play()`, `seekBy()`, and to hand to its view. */\n readonly native: T;\n readonly state: Signal<PlayerState>;\n}\n\n/**\n * Tie a player's life to the component that made it.\n *\n * Called in an injection context, which is the same constraint the hooks have and for the same\n * reason: something has to know when the thing goes away.\n */\nexport function ownPlayer<T extends NativePlayer>(\n native: T,\n watch: () => { state: Signal<PlayerState>; stop: () => void },\n): Player<T> {\n const { state, stop } = watch();\n inject(DestroyRef).onDestroy(stop);\n return { native, state };\n}\n"]}
1
+ {"version":3,"file":"player.js","sourceRoot":"","sources":["../src/player.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * What `videoPlayer` (`@ng-native/expo/video`) and `audioPlayer` (`@ng-native/expo/audio`) share:\n * the `Player` each answers, its state, and the watchers that keep that state.\n *\n * ```ts\n * protected readonly player = videoPlayer(this.source, { timeUpdate: 0.5 });\n *\n * // <expo-video [player]=\"player.native\" />\n * // {{ player.state().currentTime }}\n * ```\n *\n * `useVideoPlayer` and `useAudioPlayer` are the documented way in, and the player they hand back\n * is not the React part - `createVideoPlayer` gives the same object. What the hooks add is the\n * two things a component actually needs and cannot get otherwise: the player is *released* when\n * the component goes, and its changing state is *readable*, because the player's own properties\n * are plain mutable fields that no template can watch.\n *\n * The player itself is passed straight through: it is a shared object with `play()`, `seekBy()`\n * and the rest already on it, and wrapping those would be a second place for each to be wrong.\n *\n * Each player has an entry point of its own because each needs its own native module: Metro fails\n * a build on a `require` it cannot resolve, so one entry point for both made an app install both.\n */\nimport { DestroyRef, inject, signal, type Signal } from '@angular/core';\n\n/** The player's own status, which is the same word in both modules. */\nexport type PlayerStatus = 'idle' | 'loading' | 'readyToPlay' | 'error';\n\n/** The slice of a player this reads. Both modules' players satisfy it structurally. */\nexport interface NativePlayer {\n addListener(event: string, listener: (payload: never) => void): { remove(): void };\n release?(): void;\n /** Seconds between `timeUpdate` events. Zero, its default, means the event never fires. `expo-video` only. */\n timeUpdateEventInterval?: number;\n /** Read directly rather than from an event: `expo-audio`'s status carries no volume field. */\n readonly volume?: number;\n /** `expo-video` only, where an event says it only once it changes. */\n readonly muted?: boolean;\n}\n\n/**\n * `expo-audio`'s one status event, `playbackStatusUpdate`. Unlike `expo-video`, which splits its\n * state across several named events, `AudioPlayer` reports everything - including errors - on a\n * single event, at the interval `updateInterval` was created with.\n */\nexport interface NativeAudioStatus {\n readonly playing: boolean;\n readonly mute: boolean;\n readonly duration: number;\n readonly currentTime: number;\n readonly isLoaded: boolean;\n readonly didJustFinish: boolean;\n readonly error: string | null;\n}\n\nexport interface PlayerState {\n readonly playing: boolean;\n readonly status: PlayerStatus;\n /** Seconds. Only moves while `timeUpdateEventInterval` is non-zero. */\n readonly currentTime: number;\n readonly duration: number;\n readonly muted: boolean;\n readonly volume: number;\n /** Set once playback has run to the end, and cleared when it starts again. */\n readonly ended: boolean;\n}\n\nconst INITIAL: PlayerState = {\n playing: false,\n status: 'idle',\n currentTime: 0,\n duration: 0,\n muted: false,\n volume: 1,\n ended: false,\n};\n\n/**\n * Watch an `expo-video` player, and stop watching when the caller is done with it.\n *\n * `timeUpdate` defaults to off, so `currentTime` would never move. A caller that wants a progress\n * bar asks for one by passing an interval, and one that does not pays nothing: the event is the\n * most frequent thing the player emits.\n *\n * `expo-audio`'s player reports its state differently - one event, not several - so it is watched\n * by `watchAudioPlayer` below rather than this function.\n */\nexport function watchPlayer(\n player: NativePlayer | null,\n options: { timeUpdate?: number } = {},\n): { state: Signal<PlayerState>; stop: () => void } {\n // Both are the player's own, set where it was made: no event says either until it changes.\n const state = signal({\n ...INITIAL,\n muted: player?.muted ?? INITIAL.muted,\n volume: player?.volume ?? INITIAL.volume,\n });\n if (!player) return { state: state.asReadonly(), stop: () => {} };\n\n const patch = (change: Partial<PlayerState>) => state.update((last) => ({ ...last, ...change }));\n const subscriptions = [\n listen(player, 'statusChange', ({ status }: { status: PlayerStatus }) => patch({ status })),\n listen(player, 'playingChange', ({ isPlaying }: { isPlaying: boolean }) =>\n patch({ playing: isPlaying, ...(isPlaying ? { ended: false } : {}) }),\n ),\n listen(player, 'mutedChange', ({ muted }: { muted: boolean }) => patch({ muted })),\n listen(player, 'volumeChange', ({ volume }: { volume: number }) => patch({ volume })),\n listen(player, 'playToEnd', () => patch({ ended: true, playing: false })),\n listen(player, 'timeUpdate', ({ currentTime }: { currentTime: number }) =>\n patch({ currentTime }),\n ),\n // What was the old source's starts over. Muted and volume are the player's, and it keeps them.\n listen(player, 'sourceChange', () =>\n patch({ status: 'loading', playing: false, currentTime: 0, duration: 0, ended: false }),\n ),\n // The only event that says how long the video is: nothing else carries a duration.\n listen(player, 'sourceLoad', ({ duration }: { duration: number }) => patch({ duration })),\n ];\n\n if (options.timeUpdate !== undefined) player.timeUpdateEventInterval = options.timeUpdate;\n\n return {\n state: state.asReadonly(),\n stop: () => {\n for (const subscription of subscriptions) subscription();\n // Releasing is the half a hook does that nothing else will: a player left alive holds its\n // decoder, its audio session and, on iOS, the now-playing controls.\n player.release?.();\n },\n };\n}\n\nfunction listen<T>(player: NativePlayer, event: string, handler: (payload: T) => void): () => void {\n const subscription = player.addListener(event, handler as (payload: never) => void);\n return () => subscription.remove();\n}\n\n/**\n * Watch an `expo-audio` player, and stop watching when the caller is done with it.\n *\n * `AudioPlayer` reports its whole state on `playbackStatusUpdate`, at the interval it was created\n * with, rather than splitting it across the several events `expo-video` uses - so unlike\n * `watchPlayer`, there is one listener, not several, and nothing to reset on a source change: the\n * next status update carries the new source's own state.\n */\nexport function watchAudioPlayer(player: NativePlayer | null): {\n state: Signal<PlayerState>;\n stop: () => void;\n} {\n const state = signal(INITIAL);\n if (!player) return { state: state.asReadonly(), stop: () => {} };\n\n const subscription = listen(player, 'playbackStatusUpdate', (status: NativeAudioStatus) =>\n state.set({\n playing: status.playing,\n status: status.error !== null ? 'error' : status.isLoaded ? 'readyToPlay' : 'loading',\n currentTime: status.currentTime,\n duration: status.duration,\n muted: status.mute,\n // Not on the status event; the player's own property is the current value.\n volume: player.volume ?? INITIAL.volume,\n ended: status.didJustFinish,\n }),\n );\n\n return {\n state: state.asReadonly(),\n stop: () => {\n subscription();\n // Releasing is the half a hook does that nothing else will: a player left alive holds its\n // decoder, its audio session and, on iOS, the now-playing controls.\n player.release?.();\n },\n };\n}\n\nexport interface Player<T> {\n /** The module's own player, for `play()`, `seekBy()`, and to hand to its view. */\n readonly native: T;\n readonly state: Signal<PlayerState>;\n}\n\n/**\n * Tie a player's life to the component that made it.\n *\n * Called in an injection context, which is the same constraint the hooks have and for the same\n * reason: something has to know when the thing goes away.\n */\nexport function ownPlayer<T extends NativePlayer>(\n native: T,\n watch: () => { state: Signal<PlayerState>; stop: () => void },\n): Player<T> {\n const { state, stop } = watch();\n inject(DestroyRef).onDestroy(stop);\n return { native, state };\n}\n"]}
package/dist/updates.d.ts CHANGED
@@ -33,9 +33,13 @@ export declare class Updates {
33
33
  private readonly native;
34
34
  private readonly current;
35
35
  private readonly failure;
36
+ private readonly downloaded;
36
37
  readonly state: Signal<UpdateState>;
37
38
  readonly error: Signal<unknown>;
38
- /** Downloaded and waiting. The only state in which `apply()` does anything. */
39
+ /**
40
+ * Downloaded and waiting, including while a later check runs and after one fails. The only time
41
+ * `apply()` does anything.
42
+ */
39
43
  readonly ready: Signal<boolean>;
40
44
  /**
41
45
  * Disabled in development and in Expo Go, where the bundle comes from Metro. An app that shows