@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.
- package/android/build.gradle +20 -0
- package/android/src/main/AndroidManifest.xml +10 -0
- package/android/src/main/java/dev/ngnative/expo/OngoingNotificationModule.kt +207 -0
- package/android/src/main/java/dev/ngnative/expo/OngoingNotificationReceiver.kt +46 -0
- package/dist/expo-ui-components.d.ts +37 -6
- package/dist/expo-ui-components.js +53 -12
- package/dist/expo-ui-components.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js.map +1 -1
- package/dist/live-activity.d.ts +25 -2
- package/dist/live-activity.js +49 -6
- package/dist/live-activity.js.map +1 -1
- package/dist/native.d.ts +5 -0
- package/dist/native.js +14 -0
- package/dist/native.js.map +1 -1
- package/dist/ongoing-notification.d.ts +133 -0
- package/dist/ongoing-notification.js +141 -0
- package/dist/ongoing-notification.js.map +1 -0
- package/dist/player.d.ts +2 -0
- package/dist/player.js +8 -2
- package/dist/player.js.map +1 -1
- package/dist/updates.d.ts +5 -1
- package/dist/updates.js +18 -14
- package/dist/updates.js.map +1 -1
- package/dist/widget.d.ts +32 -5
- package/dist/widget.js +27 -11
- package/dist/widget.js.map +1 -1
- package/expo-module.config.json +10 -0
- package/ios/LiveActivityTaps.swift +70 -0
- package/ios/NgNativeExpo.podspec +22 -0
- package/package.json +15 -4
package/dist/player.js.map
CHANGED
|
@@ -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
|
-
/**
|
|
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
|
package/dist/updates.js
CHANGED
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
* one *restarts the app*. So the shape that matters is: know one is ready, and let the app choose
|
|
15
15
|
* a moment the user will not lose anything at. Nothing here does it on its own.
|
|
16
16
|
*/
|
|
17
|
-
import { InjectionToken, Service,
|
|
17
|
+
import { InjectionToken, Service, inject, signal } from '@angular/core';
|
|
18
18
|
import { expoModule } from "./native.js";
|
|
19
19
|
import * as i0 from "@angular/core";
|
|
20
20
|
export class Updates {
|
|
@@ -39,11 +39,15 @@ export class Updates {
|
|
|
39
39
|
...(ngDevMode ? [{ debugName: "current" }] : /* istanbul ignore next */ []));
|
|
40
40
|
failure = signal(null, /* @ts-ignore */
|
|
41
41
|
...(ngDevMode ? [{ debugName: "failure" }] : /* istanbul ignore next */ []));
|
|
42
|
+
downloaded = signal(false, /* @ts-ignore */
|
|
43
|
+
...(ngDevMode ? [{ debugName: "downloaded" }] : /* istanbul ignore next */ []));
|
|
42
44
|
state = this.current.asReadonly();
|
|
43
45
|
error = this.failure.asReadonly();
|
|
44
|
-
/**
|
|
45
|
-
|
|
46
|
-
|
|
46
|
+
/**
|
|
47
|
+
* Downloaded and waiting, including while a later check runs and after one fails. The only time
|
|
48
|
+
* `apply()` does anything.
|
|
49
|
+
*/
|
|
50
|
+
ready = this.downloaded.asReadonly();
|
|
47
51
|
/**
|
|
48
52
|
* Disabled in development and in Expo Go, where the bundle comes from Metro. An app that shows
|
|
49
53
|
* an "update available" banner should hide it here rather than show one that can never resolve.
|
|
@@ -63,20 +67,20 @@ export class Updates {
|
|
|
63
67
|
try {
|
|
64
68
|
this.current.set('checking');
|
|
65
69
|
const { isAvailable } = await this.native.check();
|
|
66
|
-
if (
|
|
67
|
-
this.current.set('
|
|
68
|
-
|
|
70
|
+
if (isAvailable) {
|
|
71
|
+
this.current.set('downloading');
|
|
72
|
+
const { isNew } = await this.native.fetch();
|
|
73
|
+
if (isNew)
|
|
74
|
+
this.downloaded.set(true);
|
|
69
75
|
}
|
|
70
|
-
this.
|
|
71
|
-
|
|
72
|
-
this.current.set(isNew ? 'ready' : 'idle');
|
|
73
|
-
return isNew;
|
|
76
|
+
this.failure.set(null);
|
|
77
|
+
this.current.set(this.downloaded() ? 'ready' : 'idle');
|
|
74
78
|
}
|
|
75
79
|
catch (error) {
|
|
76
80
|
this.failure.set(error);
|
|
77
|
-
this.current.set('error');
|
|
78
|
-
return false;
|
|
81
|
+
this.current.set(this.downloaded() ? 'ready' : 'error');
|
|
79
82
|
}
|
|
83
|
+
return this.downloaded();
|
|
80
84
|
}
|
|
81
85
|
/**
|
|
82
86
|
* Restart into the downloaded update.
|
|
@@ -85,7 +89,7 @@ export class Updates {
|
|
|
85
89
|
* app knows and this does not - not mid-form, not mid-upload, usually on next foreground.
|
|
86
90
|
*/
|
|
87
91
|
async apply() {
|
|
88
|
-
if (this.
|
|
92
|
+
if (this.downloaded())
|
|
89
93
|
await this.native?.reload();
|
|
90
94
|
}
|
|
91
95
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.2.0", ngImport: i0, type: Updates, deps: [], target: i0.ɵɵFactoryTarget.Service });
|
package/dist/updates.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"updates.js","sourceRoot":"","sources":["../src/updates.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AACH,OAAO,EAAE,cAAc,EAAE,OAAO,EAAE,
|
|
1
|
+
{"version":3,"file":"updates.js","sourceRoot":"","sources":["../src/updates.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * `Updates`, bound to `expo-updates`.\n *\n * ```ts\n * private readonly updates = inject(Updates);\n *\n * // On foreground, where a restart costs the user nothing.\n * async refresh() {\n * if (await this.updates.check()) await this.updates.apply();\n * }\n * ```\n *\n * The awkward part is not checking or downloading - both are plain promises - it is that applying\n * one *restarts the app*. So the shape that matters is: know one is ready, and let the app choose\n * a moment the user will not lose anything at. Nothing here does it on its own.\n */\nimport { InjectionToken, Service, inject, signal, type Signal } from '@angular/core';\nimport { expoModule } from './native.ts';\n\nexport interface NativeUpdates {\n readonly enabled: boolean;\n check(): Promise<{ isAvailable: boolean }>;\n fetch(): Promise<{ isNew: boolean }>;\n reload(): Promise<void>;\n}\n\nexport type UpdateState = 'idle' | 'checking' | 'downloading' | 'ready' | 'error';\n\n@Service()\nexport class Updates {\n /** Overridden in a test to offer an update without a server. */\n static readonly SOURCE = new InjectionToken<NativeUpdates | null>(\n 'angular-native.updatesSource',\n {\n factory: () => {\n const expo = expoModule(\n 'expo-updates',\n () => require('expo-updates') as typeof import('expo-updates'),\n );\n if (!expo) return null;\n return {\n // False in development and in Expo Go, where the bundle comes from Metro. A banner shown\n // there is one that can never resolve.\n enabled: expo.isEnabled,\n check: async () => ({ isAvailable: (await expo.checkForUpdateAsync()).isAvailable }),\n fetch: async () => ({ isNew: (await expo.fetchUpdateAsync()).isNew }),\n reload: () => expo.reloadAsync(),\n };\n },\n },\n );\n\n private readonly native = inject(Updates.SOURCE);\n private readonly current = signal<UpdateState>('idle');\n private readonly failure = signal<unknown>(null);\n private readonly downloaded = signal(false);\n\n readonly state: Signal<UpdateState> = this.current.asReadonly();\n readonly error: Signal<unknown> = this.failure.asReadonly();\n\n /**\n * Downloaded and waiting, including while a later check runs and after one fails. The only time\n * `apply()` does anything.\n */\n readonly ready: Signal<boolean> = this.downloaded.asReadonly();\n\n /**\n * Disabled in development and in Expo Go, where the bundle comes from Metro. An app that shows\n * an \"update available\" banner should hide it here rather than show one that can never resolve.\n */\n get enabled(): boolean {\n return this.native?.enabled ?? false;\n }\n\n /**\n * Check, and download if there is one. Resolves to whether an update is now waiting.\n *\n * One method rather than two because there is nothing useful to do between them: an app that\n * checks and does not download has learnt something it cannot act on.\n */\n async check(): Promise<boolean> {\n if (!this.native?.enabled) return false;\n\n try {\n this.current.set('checking');\n const { isAvailable } = await this.native.check();\n if (isAvailable) {\n this.current.set('downloading');\n const { isNew } = await this.native.fetch();\n if (isNew) this.downloaded.set(true);\n }\n this.failure.set(null);\n this.current.set(this.downloaded() ? 'ready' : 'idle');\n } catch (error) {\n this.failure.set(error);\n this.current.set(this.downloaded() ? 'ready' : 'error');\n }\n return this.downloaded();\n }\n\n /**\n * Restart into the downloaded update.\n *\n * **This restarts the app**, which is why it is never automatic: the moment to do it is one the\n * app knows and this does not - not mid-form, not mid-upload, usually on next foreground.\n */\n async apply(): Promise<void> {\n if (this.downloaded()) await this.native?.reload();\n }\n}\n"]}
|
package/dist/widget.d.ts
CHANGED
|
@@ -20,23 +20,50 @@ export interface WidgetEntry<T extends object> {
|
|
|
20
20
|
/** What `createWidget` answers. */
|
|
21
21
|
export interface NativeWidget<T extends object> {
|
|
22
22
|
updateSnapshot(props: T): void;
|
|
23
|
+
/** Replaces the timeline: each entry's props are shown from its date. */
|
|
24
|
+
updateTimeline?(entries: readonly WidgetEntry<T>[]): void;
|
|
23
25
|
getTimeline(): Promise<readonly WidgetEntry<T>[]>;
|
|
24
26
|
reload(): void;
|
|
25
27
|
}
|
|
28
|
+
/** A tap on a button, as `expo-widgets` reports it while the app is running. */
|
|
29
|
+
export interface WidgetTap {
|
|
30
|
+
/** The widget's name, or the id of the Live Activity. */
|
|
31
|
+
readonly source: string;
|
|
32
|
+
/** The button's `target`. */
|
|
33
|
+
readonly target: string;
|
|
34
|
+
/** When it was tapped, in milliseconds since the epoch. */
|
|
35
|
+
readonly timestamp: number;
|
|
36
|
+
}
|
|
26
37
|
/** When to collect taps, and when to ask iOS to redraw the widget. */
|
|
27
38
|
export interface WidgetEvents {
|
|
28
|
-
/** A widget
|
|
29
|
-
onTap(listener: () => void): () => void;
|
|
39
|
+
/** A button of a widget or a Live Activity was tapped while the app was running. */
|
|
40
|
+
onTap(listener: (tap: WidgetTap) => void): () => void;
|
|
41
|
+
/**
|
|
42
|
+
* The taps made on Live Activities with these ids before the app was listening, each answered
|
|
43
|
+
* once. A tap can start the app, and is over before its JavaScript has loaded.
|
|
44
|
+
*/
|
|
45
|
+
takeHeld?(sources: readonly string[]): readonly WidgetTap[];
|
|
46
|
+
/**
|
|
47
|
+
* The token a server starts a Live Activity with over APNs: null until iOS issues one. One signal
|
|
48
|
+
* for the whole app, since `expo-widgets` hands the token it has only to the first to listen.
|
|
49
|
+
*/
|
|
50
|
+
pushToStartToken?(): Signal<string | null>;
|
|
30
51
|
onForeground(listener: () => void): () => void;
|
|
31
52
|
onBackground(listener: () => void): () => void;
|
|
32
53
|
}
|
|
33
|
-
export interface WidgetOptions {
|
|
54
|
+
export interface WidgetOptions<T extends object = object> {
|
|
34
55
|
/**
|
|
35
56
|
* Called with the targets of the buttons tapped since the last call, oldest first, once the write
|
|
36
|
-
* that clears them from the widget has worked, so a tap is handed over once. A handler that throws
|
|
57
|
+
* that clears them from the widget has worked, so a tap is handed over once. A handler that throws, or rejects,
|
|
37
58
|
* goes to the `ErrorHandler`, and its taps are still cleared.
|
|
38
59
|
*/
|
|
39
60
|
readonly onTaps?: (taps: readonly string[]) => void;
|
|
61
|
+
/**
|
|
62
|
+
* What the widget shows later, with the app not running: the entries after now, each the props
|
|
63
|
+
* to show from its date. Asked at every write with the signal's props, which are shown until the
|
|
64
|
+
* first of them.
|
|
65
|
+
*/
|
|
66
|
+
readonly timeline?: (props: T) => readonly WidgetEntry<T>[];
|
|
40
67
|
}
|
|
41
68
|
/** A home screen widget kept in step with a signal. */
|
|
42
69
|
export interface WidgetRef {
|
|
@@ -60,4 +87,4 @@ export declare const WIDGET_EVENTS: InjectionToken<WidgetEvents>;
|
|
|
60
87
|
* worked. Going to the background asks iOS to redraw the widget, which it otherwise does on its own
|
|
61
88
|
* schedule. Destroying the injector stops both. Call it in an injection context.
|
|
62
89
|
*/
|
|
63
|
-
export declare function widget<T extends object>(native: NativeWidget<T>, props: Signal<T>, options?: WidgetOptions): WidgetRef;
|
|
90
|
+
export declare function widget<T extends object>(native: NativeWidget<T>, props: Signal<T>, options?: WidgetOptions<T>): WidgetRef;
|
package/dist/widget.js
CHANGED
|
@@ -9,12 +9,16 @@
|
|
|
9
9
|
* the app's layout file, so this package imports `expo-widgets` only for its tap events.
|
|
10
10
|
*/
|
|
11
11
|
import { DestroyRef, ErrorHandler, InjectionToken, effect, inject, signal, untracked, } from '@angular/core';
|
|
12
|
-
import { optional } from "./native.js";
|
|
12
|
+
import { isolated, optional } from "./native.js";
|
|
13
|
+
const heldTaps = () => globalThis.expo?.modules
|
|
14
|
+
?.NgNativeLiveActivityTaps;
|
|
13
15
|
/** Overridden in a test to tap a widget that is not there. */
|
|
14
16
|
export const WIDGET_EVENTS = new InjectionToken('angular-native.widgetEvents', {
|
|
15
17
|
factory: () => {
|
|
16
18
|
const widgets = optional(() => require('expo-widgets'));
|
|
17
19
|
const native = optional(() => require('react-native'));
|
|
20
|
+
const destroyRef = inject(DestroyRef);
|
|
21
|
+
let startToken;
|
|
18
22
|
const onState = (wanted) => (listener) => {
|
|
19
23
|
const subscription = native?.AppState.addEventListener('change', (state) => {
|
|
20
24
|
if (state === wanted)
|
|
@@ -27,6 +31,16 @@ export const WIDGET_EVENTS = new InjectionToken('angular-native.widgetEvents', {
|
|
|
27
31
|
const subscription = widgets?.addUserInteractionListener(listener);
|
|
28
32
|
return () => subscription?.remove();
|
|
29
33
|
},
|
|
34
|
+
takeHeld: (sources) => heldTaps()?.takeTaps?.(sources) ?? [],
|
|
35
|
+
pushToStartToken: () => {
|
|
36
|
+
if (startToken)
|
|
37
|
+
return startToken;
|
|
38
|
+
const token = signal(null, /* @ts-ignore */
|
|
39
|
+
...(ngDevMode ? [{ debugName: "token" }] : /* istanbul ignore next */ []));
|
|
40
|
+
const subscription = widgets?.addPushToStartTokenListener?.((event) => token.set(event.activityPushToStartToken));
|
|
41
|
+
destroyRef.onDestroy(() => subscription?.remove());
|
|
42
|
+
return (startToken = token.asReadonly());
|
|
43
|
+
},
|
|
30
44
|
onForeground: onState('active'),
|
|
31
45
|
onBackground: onState('background'),
|
|
32
46
|
};
|
|
@@ -45,7 +59,16 @@ export function widget(native, props, options = {}) {
|
|
|
45
59
|
const error = signal(null, /* @ts-ignore */
|
|
46
60
|
...(ngDevMode ? [{ debugName: "error" }] : /* istanbul ignore next */ []));
|
|
47
61
|
let syncing = null;
|
|
48
|
-
|
|
62
|
+
/** A tap is recorded in the entry that was showing, which a timeline has several of. */
|
|
63
|
+
const queued = async () => (await native.getTimeline()).flatMap((entry) => entry.props.taps ?? []);
|
|
64
|
+
const write = (now) => {
|
|
65
|
+
if (options.timeline && native.updateTimeline) {
|
|
66
|
+
native.updateTimeline([{ date: new Date(), props: now }, ...options.timeline(now)]);
|
|
67
|
+
}
|
|
68
|
+
else {
|
|
69
|
+
native.updateSnapshot(now);
|
|
70
|
+
}
|
|
71
|
+
};
|
|
49
72
|
/** The taps, read again until no more land, since the write after it clears them. */
|
|
50
73
|
const collect = async () => {
|
|
51
74
|
let taps = await queued();
|
|
@@ -57,18 +80,11 @@ export function widget(native, props, options = {}) {
|
|
|
57
80
|
}
|
|
58
81
|
return taps;
|
|
59
82
|
};
|
|
60
|
-
const hand = (taps) =>
|
|
61
|
-
try {
|
|
62
|
-
options.onTaps?.(taps);
|
|
63
|
-
}
|
|
64
|
-
catch (failure) {
|
|
65
|
-
errors.handleError(failure);
|
|
66
|
-
}
|
|
67
|
-
};
|
|
83
|
+
const hand = (taps) => isolated(() => options.onTaps?.(taps), (failure) => errors.handleError(failure));
|
|
68
84
|
const run = async () => {
|
|
69
85
|
try {
|
|
70
86
|
const taps = await collect();
|
|
71
|
-
|
|
87
|
+
untracked(() => write(props()));
|
|
72
88
|
error.set(null);
|
|
73
89
|
if (taps.length)
|
|
74
90
|
hand(taps);
|
package/dist/widget.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"widget.js","sourceRoot":"","sources":["../src/widget.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * `widget`, which keeps a home screen widget from `expo-widgets` in step with a signal, and hands the\n * app the taps on its buttons.\n *\n * A widget button runs in the widget extension, not in the app, which iOS keeps suspended, so a\n * tap's own event is lost unless the app is running. The widget's layout records each tap in its\n * own props instead, as `taps`, and `widget` collects them when the app next runs: at once while it\n * is running, and when it comes back to the foreground. It takes the widget `createWidget` makes in\n * the app's layout file, so this package imports `expo-widgets` only for its tap events.\n */\nimport {\n DestroyRef,\n ErrorHandler,\n InjectionToken,\n effect,\n inject,\n signal,\n untracked,\n type Signal,\n} from '@angular/core';\nimport { optional } from './native.ts';\n\n/** One entry of a widget's timeline, as `expo-widgets` stores it. */\nexport interface WidgetEntry<T extends object> {\n readonly date: Date;\n /** The entry's props, with the targets of the buttons tapped since the app last collected them. */\n readonly props: T & { readonly taps?: readonly string[] };\n}\n\n/** What `createWidget` answers. */\nexport interface NativeWidget<T extends object> {\n updateSnapshot(props: T): void;\n getTimeline(): Promise<readonly WidgetEntry<T>[]>;\n reload(): void;\n}\n\n/** When to collect taps, and when to ask iOS to redraw the widget. */\nexport interface WidgetEvents {\n /** A widget button was tapped while the app was running. */\n onTap(listener: () => void): () => void;\n onForeground(listener: () => void): () => void;\n onBackground(listener: () => void): () => void;\n}\n\nexport interface WidgetOptions {\n /**\n * Called with the targets of the buttons tapped since the last call, oldest first, once the write\n * that clears them from the widget has worked, so a tap is handed over once. A handler that throws\n * goes to the `ErrorHandler`, and its taps are still cleared.\n */\n readonly onTaps?: (taps: readonly string[]) => void;\n}\n\n/** A home screen widget kept in step with a signal. */\nexport interface WidgetRef {\n /**\n * Why the last sync did not happen: the taps could not be read, or the widget not written. It also\n * goes to the `ErrorHandler`, and is null again once a sync works. A failing `onTaps` is not kept\n * here.\n */\n readonly error: Signal<unknown>;\n /** Collects the taps now, then shows the signal's value. */\n sync(): Promise<void>;\n /** Asks iOS to redraw the widget. */\n reload(): void;\n}\n\ntype ReactNative = typeof import('react-native');\ntype ExpoWidgets = {\n addUserInteractionListener(listener: () => void): { remove(): void };\n};\n\n/** Overridden in a test to tap a widget that is not there. */\nexport const WIDGET_EVENTS = new InjectionToken<WidgetEvents>('angular-native.widgetEvents', {\n factory: () => {\n const widgets = optional(() => require('expo-widgets') as ExpoWidgets);\n const native = optional(() => require('react-native') as ReactNative);\n const onState = (wanted: string) => (listener: () => void) => {\n const subscription = native?.AppState.addEventListener('change', (state) => {\n if (state === wanted) listener();\n });\n return () => subscription?.remove();\n };\n return {\n onTap: (listener) => {\n const subscription = widgets?.addUserInteractionListener(listener);\n return () => subscription?.remove();\n },\n onForeground: onState('active'),\n onBackground: onState('background'),\n };\n },\n});\n\n/**\n * Shows `props` on the widget and follows the signal, and hands taps on its buttons to `onTaps`.\n * A write replaces the props that hold the taps, so every write comes after the taps are read, and\n * nothing is written while they cannot be; a tap is handed over once the write that clears it has\n * worked. Going to the background asks iOS to redraw the widget, which it otherwise does on its own\n * schedule. Destroying the injector stops both. Call it in an injection context.\n */\nexport function widget<T extends object>(\n native: NativeWidget<T>,\n props: Signal<T>,\n options: WidgetOptions = {},\n): WidgetRef {\n const errors = inject(ErrorHandler);\n const events = inject(WIDGET_EVENTS);\n const error = signal<unknown>(null);\n let syncing: Promise<void> | null = null;\n\n const queued = async () => (await native.getTimeline()).at(-1)?.props.taps ?? [];\n\n /** The taps, read again until no more land, since the write after it clears them. */\n const collect = async () => {\n let taps = await queued();\n while (taps.length) {\n const more = await queued();\n if (more.length <= taps.length) break;\n taps = more;\n }\n return taps;\n };\n\n const hand = (taps: readonly string[]) => {\n try {\n options.onTaps?.(taps);\n } catch (failure) {\n errors.handleError(failure);\n }\n };\n\n const run = async () => {\n try {\n const taps = await collect();\n native.updateSnapshot(untracked(props));\n error.set(null);\n if (taps.length) hand(taps);\n } catch (failure) {\n error.set(failure);\n errors.handleError(failure);\n }\n };\n\n /**\n * One sync at a time. One asked for during it is already covered: its write reads the signal then,\n * and nothing is awaited after it.\n */\n const sync = () => (syncing ??= run().finally(() => (syncing = null)));\n\n effect(() => {\n props();\n untracked(() => void sync());\n });\n\n const stops = [\n events.onTap(() => void sync()),\n events.onForeground(() => void sync()),\n events.onBackground(() => native.reload()),\n ];\n inject(DestroyRef).onDestroy(() => stops.forEach((stop) => stop()));\n\n return { error: error.asReadonly(), sync, reload: () => native.reload() };\n}\n"]}
|
|
1
|
+
{"version":3,"file":"widget.js","sourceRoot":"","sources":["../src/widget.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * `widget`, which keeps a home screen widget from `expo-widgets` in step with a signal, and hands the\n * app the taps on its buttons.\n *\n * A widget button runs in the widget extension, not in the app, which iOS keeps suspended, so a\n * tap's own event is lost unless the app is running. The widget's layout records each tap in its\n * own props instead, as `taps`, and `widget` collects them when the app next runs: at once while it\n * is running, and when it comes back to the foreground. It takes the widget `createWidget` makes in\n * the app's layout file, so this package imports `expo-widgets` only for its tap events.\n */\nimport {\n DestroyRef,\n ErrorHandler,\n InjectionToken,\n effect,\n inject,\n signal,\n untracked,\n type Signal,\n} from '@angular/core';\nimport { isolated, optional } from './native.ts';\n\n/** One entry of a widget's timeline, as `expo-widgets` stores it. */\nexport interface WidgetEntry<T extends object> {\n readonly date: Date;\n /** The entry's props, with the targets of the buttons tapped since the app last collected them. */\n readonly props: T & { readonly taps?: readonly string[] };\n}\n\n/** What `createWidget` answers. */\nexport interface NativeWidget<T extends object> {\n updateSnapshot(props: T): void;\n /** Replaces the timeline: each entry's props are shown from its date. */\n updateTimeline?(entries: readonly WidgetEntry<T>[]): void;\n getTimeline(): Promise<readonly WidgetEntry<T>[]>;\n reload(): void;\n}\n\n/** A tap on a button, as `expo-widgets` reports it while the app is running. */\nexport interface WidgetTap {\n /** The widget's name, or the id of the Live Activity. */\n readonly source: string;\n /** The button's `target`. */\n readonly target: string;\n /** When it was tapped, in milliseconds since the epoch. */\n readonly timestamp: number;\n}\n\n/** When to collect taps, and when to ask iOS to redraw the widget. */\nexport interface WidgetEvents {\n /** A button of a widget or a Live Activity was tapped while the app was running. */\n onTap(listener: (tap: WidgetTap) => void): () => void;\n /**\n * The taps made on Live Activities with these ids before the app was listening, each answered\n * once. A tap can start the app, and is over before its JavaScript has loaded.\n */\n takeHeld?(sources: readonly string[]): readonly WidgetTap[];\n /**\n * The token a server starts a Live Activity with over APNs: null until iOS issues one. One signal\n * for the whole app, since `expo-widgets` hands the token it has only to the first to listen.\n */\n pushToStartToken?(): Signal<string | null>;\n onForeground(listener: () => void): () => void;\n onBackground(listener: () => void): () => void;\n}\n\nexport interface WidgetOptions<T extends object = object> {\n /**\n * Called with the targets of the buttons tapped since the last call, oldest first, once the write\n * that clears them from the widget has worked, so a tap is handed over once. A handler that throws, or rejects,\n * goes to the `ErrorHandler`, and its taps are still cleared.\n */\n readonly onTaps?: (taps: readonly string[]) => void;\n /**\n * What the widget shows later, with the app not running: the entries after now, each the props\n * to show from its date. Asked at every write with the signal's props, which are shown until the\n * first of them.\n */\n readonly timeline?: (props: T) => readonly WidgetEntry<T>[];\n}\n\n/** A home screen widget kept in step with a signal. */\nexport interface WidgetRef {\n /**\n * Why the last sync did not happen: the taps could not be read, or the widget not written. It also\n * goes to the `ErrorHandler`, and is null again once a sync works. A failing `onTaps` is not kept\n * here.\n */\n readonly error: Signal<unknown>;\n /** Collects the taps now, then shows the signal's value. */\n sync(): Promise<void>;\n /** Asks iOS to redraw the widget. */\n reload(): void;\n}\n\ntype ReactNative = typeof import('react-native');\ntype ExpoWidgets = {\n addUserInteractionListener(listener: (tap: WidgetTap) => void): { remove(): void };\n addPushToStartTokenListener?(listener: (event: { activityPushToStartToken: string }) => void): {\n remove(): void;\n };\n};\n\n/** This package's own native module, which holds a Live Activity's taps from launch. Absent off iOS. */\ntype HeldTaps = { takeTaps?(sources: readonly string[]): readonly WidgetTap[] };\nconst heldTaps = () =>\n (globalThis as { expo?: { modules?: { NgNativeLiveActivityTaps?: HeldTaps } } }).expo?.modules\n ?.NgNativeLiveActivityTaps;\n\n/** Overridden in a test to tap a widget that is not there. */\nexport const WIDGET_EVENTS = new InjectionToken<WidgetEvents>('angular-native.widgetEvents', {\n factory: () => {\n const widgets = optional(() => require('expo-widgets') as ExpoWidgets);\n const native = optional(() => require('react-native') as ReactNative);\n const destroyRef = inject(DestroyRef);\n let startToken: Signal<string | null> | undefined;\n const onState = (wanted: string) => (listener: () => void) => {\n const subscription = native?.AppState.addEventListener('change', (state) => {\n if (state === wanted) listener();\n });\n return () => subscription?.remove();\n };\n return {\n onTap: (listener) => {\n const subscription = widgets?.addUserInteractionListener(listener);\n return () => subscription?.remove();\n },\n takeHeld: (sources) => heldTaps()?.takeTaps?.(sources) ?? [],\n pushToStartToken: () => {\n if (startToken) return startToken;\n const token = signal<string | null>(null);\n const subscription = widgets?.addPushToStartTokenListener?.((event) =>\n token.set(event.activityPushToStartToken),\n );\n destroyRef.onDestroy(() => subscription?.remove());\n return (startToken = token.asReadonly());\n },\n onForeground: onState('active'),\n onBackground: onState('background'),\n };\n },\n});\n\n/**\n * Shows `props` on the widget and follows the signal, and hands taps on its buttons to `onTaps`.\n * A write replaces the props that hold the taps, so every write comes after the taps are read, and\n * nothing is written while they cannot be; a tap is handed over once the write that clears it has\n * worked. Going to the background asks iOS to redraw the widget, which it otherwise does on its own\n * schedule. Destroying the injector stops both. Call it in an injection context.\n */\nexport function widget<T extends object>(\n native: NativeWidget<T>,\n props: Signal<T>,\n options: WidgetOptions<T> = {},\n): WidgetRef {\n const errors = inject(ErrorHandler);\n const events = inject(WIDGET_EVENTS);\n const error = signal<unknown>(null);\n let syncing: Promise<void> | null = null;\n\n /** A tap is recorded in the entry that was showing, which a timeline has several of. */\n const queued = async () =>\n (await native.getTimeline()).flatMap((entry) => entry.props.taps ?? []);\n\n const write = (now: T) => {\n if (options.timeline && native.updateTimeline) {\n native.updateTimeline([{ date: new Date(), props: now }, ...options.timeline(now)]);\n } else {\n native.updateSnapshot(now);\n }\n };\n\n /** The taps, read again until no more land, since the write after it clears them. */\n const collect = async () => {\n let taps = await queued();\n while (taps.length) {\n const more = await queued();\n if (more.length <= taps.length) break;\n taps = more;\n }\n return taps;\n };\n\n const hand = (taps: readonly string[]) =>\n isolated(\n () => options.onTaps?.(taps),\n (failure) => errors.handleError(failure),\n );\n\n const run = async () => {\n try {\n const taps = await collect();\n untracked(() => write(props()));\n error.set(null);\n if (taps.length) hand(taps);\n } catch (failure) {\n error.set(failure);\n errors.handleError(failure);\n }\n };\n\n /**\n * One sync at a time. One asked for during it is already covered: its write reads the signal then,\n * and nothing is awaited after it.\n */\n const sync = () => (syncing ??= run().finally(() => (syncing = null)));\n\n effect(() => {\n props();\n untracked(() => void sync());\n });\n\n const stops = [\n events.onTap(() => void sync()),\n events.onForeground(() => void sync()),\n events.onBackground(() => native.reload()),\n ];\n inject(DestroyRef).onDestroy(() => stops.forEach((stop) => stop()));\n\n return { error: error.asReadonly(), sync, reload: () => native.reload() };\n}\n"]}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import ExpoModulesCore
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
The taps on a Live Activity's buttons made before any JavaScript was listening.
|
|
5
|
+
|
|
6
|
+
`expo-widgets` runs a button's tap in the app, starting it in the background when it has to, and
|
|
7
|
+
posts it as a notification that nothing keeps. The tap that starts the app is posted before the
|
|
8
|
+
app's JavaScript has loaded, so this holds what is posted from launch until JavaScript takes it.
|
|
9
|
+
*/
|
|
10
|
+
final class HeldTaps {
|
|
11
|
+
static let shared = HeldTaps()
|
|
12
|
+
|
|
13
|
+
private let lock = NSLock()
|
|
14
|
+
private var taps: [[String: Any]] = []
|
|
15
|
+
private var observer: NSObjectProtocol?
|
|
16
|
+
|
|
17
|
+
/// Holds each tap `expo-widgets` posts from now on.
|
|
18
|
+
func start() {
|
|
19
|
+
lock.lock()
|
|
20
|
+
defer { lock.unlock() }
|
|
21
|
+
guard observer == nil else { return }
|
|
22
|
+
observer = NotificationCenter.default.addObserver(
|
|
23
|
+
forName: Notification.Name("onExpoWidgetsUserInteraction"),
|
|
24
|
+
object: nil,
|
|
25
|
+
queue: nil
|
|
26
|
+
) { [weak self] notification in
|
|
27
|
+
guard let self, let tap = notification.userInfo?["eventData"] as? [String: Any] else { return }
|
|
28
|
+
self.lock.lock()
|
|
29
|
+
defer { self.lock.unlock() }
|
|
30
|
+
if self.observer != nil { self.taps.append(tap) }
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
The held taps from `sources`, which are then held no longer. JavaScript listens to
|
|
36
|
+
`expo-widgets` itself before it asks, so the first call also stops holding new ones.
|
|
37
|
+
*/
|
|
38
|
+
func take(sources: [String]) -> [[String: Any]] {
|
|
39
|
+
lock.lock()
|
|
40
|
+
defer { lock.unlock() }
|
|
41
|
+
if let observer {
|
|
42
|
+
NotificationCenter.default.removeObserver(observer)
|
|
43
|
+
self.observer = nil
|
|
44
|
+
}
|
|
45
|
+
let from = { (tap: [String: Any]) in sources.contains(tap["source"] as? String ?? "") }
|
|
46
|
+
let taken = taps.filter(from)
|
|
47
|
+
taps.removeAll(where: from)
|
|
48
|
+
return taken
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
public final class LiveActivityTapsObserver: ExpoAppDelegateSubscriber {
|
|
53
|
+
public func application(
|
|
54
|
+
_ application: UIApplication,
|
|
55
|
+
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil
|
|
56
|
+
) -> Bool {
|
|
57
|
+
HeldTaps.shared.start()
|
|
58
|
+
return true
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
public final class LiveActivityTapsModule: Module {
|
|
63
|
+
public func definition() -> ModuleDefinition {
|
|
64
|
+
Name("NgNativeLiveActivityTaps")
|
|
65
|
+
|
|
66
|
+
Function("takeTaps") { (sources: [String]) -> [[String: Any]] in
|
|
67
|
+
HeldTaps.shared.take(sources: sources)
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
require 'json'
|
|
2
|
+
|
|
3
|
+
package = JSON.parse(File.read(File.join(__dir__, '..', 'package.json')))
|
|
4
|
+
|
|
5
|
+
Pod::Spec.new do |s|
|
|
6
|
+
s.name = 'NgNativeExpo'
|
|
7
|
+
s.version = package['version']
|
|
8
|
+
s.summary = package['description']
|
|
9
|
+
s.description = package['description']
|
|
10
|
+
s.license = package['license']
|
|
11
|
+
s.author = 'Angular Native'
|
|
12
|
+
s.homepage = 'https://github.com/ng-native/ng-native'
|
|
13
|
+
s.platforms = { :ios => '16.4' }
|
|
14
|
+
s.swift_version = '5.9'
|
|
15
|
+
s.source = { git: 'https://github.com/ng-native/ng-native.git' }
|
|
16
|
+
s.static_framework = true
|
|
17
|
+
|
|
18
|
+
s.dependency 'ExpoModulesCore'
|
|
19
|
+
|
|
20
|
+
s.source_files = '**/*.swift'
|
|
21
|
+
s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES' }
|
|
22
|
+
end
|
package/package.json
CHANGED
|
@@ -1,13 +1,16 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ng-native/expo",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.11.0",
|
|
4
4
|
"description": "Expo modules as Angular services for Angular Native, plus registerExpoView for a module's native view.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"angular",
|
|
7
7
|
"react-native",
|
|
8
8
|
"angular-native",
|
|
9
9
|
"ios",
|
|
10
|
-
"android",
|
|
10
|
+
"android/build.gradle",
|
|
11
|
+
"android/src",
|
|
12
|
+
"android/build.gradle",
|
|
13
|
+
"android/src",
|
|
11
14
|
"fabric",
|
|
12
15
|
"native",
|
|
13
16
|
"expo",
|
|
@@ -148,6 +151,10 @@
|
|
|
148
151
|
"types": "./dist/notifications.d.ts",
|
|
149
152
|
"default": "./dist/notifications.js"
|
|
150
153
|
},
|
|
154
|
+
"./ongoing-notification": {
|
|
155
|
+
"types": "./dist/ongoing-notification.d.ts",
|
|
156
|
+
"default": "./dist/ongoing-notification.js"
|
|
157
|
+
},
|
|
151
158
|
"./orientation": {
|
|
152
159
|
"types": "./dist/orientation.d.ts",
|
|
153
160
|
"default": "./dist/orientation.js"
|
|
@@ -206,7 +213,7 @@
|
|
|
206
213
|
"access": "public"
|
|
207
214
|
},
|
|
208
215
|
"dependencies": {
|
|
209
|
-
"@ng-native/fabric": "0.
|
|
216
|
+
"@ng-native/fabric": "0.11.0"
|
|
210
217
|
},
|
|
211
218
|
"nx": {
|
|
212
219
|
"tags": [
|
|
@@ -445,7 +452,11 @@
|
|
|
445
452
|
"directory": "packages/expo"
|
|
446
453
|
},
|
|
447
454
|
"files": [
|
|
448
|
-
"dist"
|
|
455
|
+
"dist",
|
|
456
|
+
"ios",
|
|
457
|
+
"android/build.gradle",
|
|
458
|
+
"android/src",
|
|
459
|
+
"expo-module.config.json"
|
|
449
460
|
],
|
|
450
461
|
"scripts": {
|
|
451
462
|
"build": "ngc -p tsconfig.build.json",
|