@ng-native/expo 0.2.0 → 0.4.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/README.md +32 -382
- package/dist/async-storage.d.ts +15 -0
- package/dist/async-storage.js +29 -0
- package/dist/async-storage.js.map +1 -0
- package/dist/audio.d.ts +10 -0
- package/dist/audio.js +26 -0
- package/dist/audio.js.map +1 -0
- package/dist/community-views.js +1 -1
- package/dist/community-views.js.map +1 -1
- package/dist/crypto.js +2 -2
- package/dist/crypto.js.map +1 -1
- package/dist/database.d.ts +18 -3
- package/dist/database.js +26 -7
- package/dist/database.js.map +1 -1
- package/dist/date-time-picker.d.ts +33 -0
- package/dist/date-time-picker.js +77 -0
- package/dist/date-time-picker.js.map +1 -0
- package/dist/dom-component.d.ts +5 -0
- package/dist/dom-component.js +57 -2
- package/dist/dom-component.js.map +1 -1
- package/dist/expo-ui-components.d.ts +292 -12
- package/dist/expo-ui-components.js +531 -41
- package/dist/expo-ui-components.js.map +1 -1
- package/dist/expo-ui.js +19 -6
- package/dist/expo-ui.js.map +1 -1
- package/dist/file-system.d.ts +7 -0
- package/dist/file-system.js +16 -3
- package/dist/file-system.js.map +1 -1
- package/dist/foldable.d.ts +78 -0
- package/dist/foldable.js +87 -0
- package/dist/foldable.js.map +1 -0
- package/dist/fonts.d.ts +3 -9
- package/dist/fonts.js +26 -16
- package/dist/fonts.js.map +1 -1
- package/dist/glass.d.ts +1 -1
- package/dist/image-picker.js +3 -9
- package/dist/image-picker.js.map +1 -1
- package/dist/index.d.ts +3 -2
- package/dist/index.js +3 -2
- package/dist/index.js.map +1 -1
- package/dist/live-activity.d.ts +88 -0
- package/dist/live-activity.js +156 -0
- package/dist/live-activity.js.map +1 -0
- package/dist/location.d.ts +17 -0
- package/dist/location.js +20 -7
- package/dist/location.js.map +1 -1
- package/dist/media-library.js +1 -3
- package/dist/media-library.js.map +1 -1
- package/dist/native.d.ts +12 -2
- package/dist/native.js +81 -10
- package/dist/native.js.map +1 -1
- package/dist/notifications.js +2 -4
- package/dist/notifications.js.map +1 -1
- package/dist/permissions.d.ts +2 -0
- package/dist/permissions.js +6 -0
- package/dist/permissions.js.map +1 -1
- package/dist/player.d.ts +24 -11
- package/dist/player.js +7 -26
- package/dist/player.js.map +1 -1
- package/dist/register-expo-view.d.ts +7 -0
- package/dist/register-expo-view.js +6 -1
- package/dist/register-expo-view.js.map +1 -1
- package/dist/screen-capture.js +2 -4
- package/dist/screen-capture.js.map +1 -1
- package/dist/secure-store.d.ts +15 -0
- package/dist/secure-store.js +30 -0
- package/dist/secure-store.js.map +1 -0
- package/dist/store.d.ts +5 -10
- package/dist/store.js +5 -36
- package/dist/store.js.map +1 -1
- package/dist/tracking.d.ts +1 -1
- package/dist/tracking.js +3 -5
- package/dist/tracking.js.map +1 -1
- package/dist/video.d.ts +5 -0
- package/dist/video.js +21 -0
- package/dist/video.js.map +1 -0
- package/dist/watch.d.ts +103 -0
- package/dist/watch.js +228 -0
- package/dist/watch.js.map +1 -0
- package/dist/widget.d.ts +63 -0
- package/dist/widget.js +98 -0
- package/dist/widget.js.map +1 -0
- package/package.json +53 -4
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* iOS Live Activities and home-screen widgets, through `expo-widgets`: `createLiveActivity` and
|
|
3
|
+
* `createWidget` draw one from an Angular component's template, and `liveActivity` keeps an
|
|
4
|
+
* activity in step with a signal, on the lock screen and in the Dynamic Island. Off iOS,
|
|
5
|
+
* `expo-widgets` starts a stand-in with no id, and `start()` answers false.
|
|
6
|
+
*/
|
|
7
|
+
import { DestroyRef, ErrorHandler, effect, inject, signal, untracked, } from '@angular/core';
|
|
8
|
+
import { expoModule } from "./native.js";
|
|
9
|
+
/**
|
|
10
|
+
* A Live Activity drawn by `layout`, an Angular component, as `expo-widgets`' `createLiveActivity`
|
|
11
|
+
* makes one. `name` matches the activity's name in the `expo-widgets` plugin's config.
|
|
12
|
+
*
|
|
13
|
+
* ```ts
|
|
14
|
+
* export const scoreActivity = createLiveActivity('Score', ScoreLayout);
|
|
15
|
+
* ```
|
|
16
|
+
*
|
|
17
|
+
* `@ng-native/metro`'s transformer compiles the layout's template, at build time, to the source the
|
|
18
|
+
* widget extension evaluates, and drops the class, so the app never runs it: the extension does,
|
|
19
|
+
* with no Angular. So the class holds only its inputs, members holding a modifier from
|
|
20
|
+
* `@expo/ui/swift-ui/modifiers`, and members holding a literal, and its template is inline.
|
|
21
|
+
*/
|
|
22
|
+
export function createLiveActivity(name, layout) {
|
|
23
|
+
const expo = widgets();
|
|
24
|
+
if (!expo)
|
|
25
|
+
return STAND_IN;
|
|
26
|
+
return expo.createLiveActivity(name, compiled('createLiveActivity', name, layout));
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* A home-screen widget drawn by `layout`, as `expo-widgets`' `createWidget` makes one. The layout
|
|
30
|
+
* reads what the widget is drawn in, such as its `widgetFamily`, from an `environment` input beside
|
|
31
|
+
* `props`. Compiled as `createLiveActivity`'s layout is.
|
|
32
|
+
*/
|
|
33
|
+
export function createWidget(name, layout) {
|
|
34
|
+
const expo = widgets();
|
|
35
|
+
if (!expo)
|
|
36
|
+
return WIDGET_STAND_IN;
|
|
37
|
+
return expo.createWidget(name, compiled('createWidget', name, layout));
|
|
38
|
+
}
|
|
39
|
+
/** `expo-widgets`, which answers with stand-ins itself where there are no widgets; null in Node. */
|
|
40
|
+
function widgets() {
|
|
41
|
+
return expoModule('expo-widgets', () => require('expo-widgets'), ['ios', 'android', 'web']);
|
|
42
|
+
}
|
|
43
|
+
/** The layout's source, which the transformer put in place of the class. */
|
|
44
|
+
function compiled(call, name, layout) {
|
|
45
|
+
if (typeof layout === 'string')
|
|
46
|
+
return layout;
|
|
47
|
+
throw new Error(`${call}('${name}', ...) takes a layout @ng-native/metro's transformer compiles, and it did not run on this file.`);
|
|
48
|
+
}
|
|
49
|
+
/** What there is in Node, where no widget can be drawn: an activity that never starts. */
|
|
50
|
+
const STAND_IN = {
|
|
51
|
+
start: () => ({ getId: () => '' }),
|
|
52
|
+
getInstances: () => [],
|
|
53
|
+
};
|
|
54
|
+
/** A widget in Node: it draws nothing and holds no timeline, and `widget()` keeps it in step. */
|
|
55
|
+
const WIDGET_STAND_IN = {
|
|
56
|
+
updateSnapshot: () => { },
|
|
57
|
+
getTimeline: async () => [],
|
|
58
|
+
reload: () => { },
|
|
59
|
+
};
|
|
60
|
+
/**
|
|
61
|
+
* Keeps a Live Activity of the factory's kind in step with `props`: once started, every change to the
|
|
62
|
+
* signal updates it. One left running from before the app started is picked up rather than started
|
|
63
|
+
* again, since iOS caps how many an app runs. Destroying the injector stops the updates and leaves
|
|
64
|
+
* the activity running, as it outlives the app. Call it in an injection context.
|
|
65
|
+
*/
|
|
66
|
+
export function liveActivity(factory, props) {
|
|
67
|
+
const errors = inject(ErrorHandler);
|
|
68
|
+
const active = signal(false, /* @ts-ignore */
|
|
69
|
+
...(ngDevMode ? [{ debugName: "active" }] : /* istanbul ignore next */ []));
|
|
70
|
+
const id = signal(null, /* @ts-ignore */
|
|
71
|
+
...(ngDevMode ? [{ debugName: "id" }] : /* istanbul ignore next */ []));
|
|
72
|
+
const pushToken = signal(null, /* @ts-ignore */
|
|
73
|
+
...(ngDevMode ? [{ debugName: "pushToken" }] : /* istanbul ignore next */ []));
|
|
74
|
+
const error = signal(null, /* @ts-ignore */
|
|
75
|
+
...(ngDevMode ? [{ debugName: "error" }] : /* istanbul ignore next */ []));
|
|
76
|
+
let current = null;
|
|
77
|
+
let tokenSubscription = null;
|
|
78
|
+
const report = (failure) => {
|
|
79
|
+
error.set(failure);
|
|
80
|
+
errors.handleError(failure);
|
|
81
|
+
};
|
|
82
|
+
const running = () => {
|
|
83
|
+
try {
|
|
84
|
+
return factory.getInstances();
|
|
85
|
+
}
|
|
86
|
+
catch (failure) {
|
|
87
|
+
error.set(failure);
|
|
88
|
+
return [];
|
|
89
|
+
}
|
|
90
|
+
};
|
|
91
|
+
const forget = () => {
|
|
92
|
+
tokenSubscription?.remove();
|
|
93
|
+
tokenSubscription = null;
|
|
94
|
+
current = null;
|
|
95
|
+
active.set(false);
|
|
96
|
+
id.set(null);
|
|
97
|
+
pushToken.set(null);
|
|
98
|
+
};
|
|
99
|
+
const adopt = (activity) => {
|
|
100
|
+
current = activity;
|
|
101
|
+
active.set(true);
|
|
102
|
+
id.set(activity.getId());
|
|
103
|
+
error.set(null);
|
|
104
|
+
tokenSubscription = activity.addPushTokenListener((event) => pushToken.set(event.pushToken));
|
|
105
|
+
activity.getPushToken().then((token) => current === activity && token !== null && pushToken.set(token), () => { });
|
|
106
|
+
};
|
|
107
|
+
const existing = running()[0];
|
|
108
|
+
if (existing)
|
|
109
|
+
adopt(existing);
|
|
110
|
+
effect(() => {
|
|
111
|
+
const next = props();
|
|
112
|
+
const activity = current;
|
|
113
|
+
if (activity)
|
|
114
|
+
untracked(() => activity.update(next).catch(report));
|
|
115
|
+
});
|
|
116
|
+
inject(DestroyRef).onDestroy(() => {
|
|
117
|
+
tokenSubscription?.remove();
|
|
118
|
+
tokenSubscription = null;
|
|
119
|
+
});
|
|
120
|
+
return {
|
|
121
|
+
active: active.asReadonly(),
|
|
122
|
+
id: id.asReadonly(),
|
|
123
|
+
pushToken: pushToken.asReadonly(),
|
|
124
|
+
error: error.asReadonly(),
|
|
125
|
+
start(options = {}) {
|
|
126
|
+
if (current)
|
|
127
|
+
return true;
|
|
128
|
+
const value = untracked(props);
|
|
129
|
+
try {
|
|
130
|
+
const already = running()[0];
|
|
131
|
+
const activity = already ?? factory.start(value, options.url);
|
|
132
|
+
if (!activity.getId()) {
|
|
133
|
+
error.set(new Error('Live Activities are only on iOS.'));
|
|
134
|
+
return false;
|
|
135
|
+
}
|
|
136
|
+
adopt(activity);
|
|
137
|
+
if (already)
|
|
138
|
+
already.update(value).catch(report);
|
|
139
|
+
return true;
|
|
140
|
+
}
|
|
141
|
+
catch (failure) {
|
|
142
|
+
error.set(failure);
|
|
143
|
+
return false;
|
|
144
|
+
}
|
|
145
|
+
},
|
|
146
|
+
async end(dismissal = 'default') {
|
|
147
|
+
const value = untracked(props);
|
|
148
|
+
const activities = new Set(running());
|
|
149
|
+
if (current)
|
|
150
|
+
activities.add(current);
|
|
151
|
+
forget();
|
|
152
|
+
await Promise.all([...activities].map((activity) => activity.end(dismissal, value))).catch(report);
|
|
153
|
+
},
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
//# sourceMappingURL=live-activity.js.map
|
|
@@ -0,0 +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"]}
|
package/dist/location.d.ts
CHANGED
|
@@ -39,6 +39,13 @@ export interface WatchOptions {
|
|
|
39
39
|
readonly interval?: number;
|
|
40
40
|
}
|
|
41
41
|
type ExpoFix = import('expo-location').LocationObject;
|
|
42
|
+
/** A point on the map: what an address geocodes to, and what `reverseGeocode` takes. */
|
|
43
|
+
export interface Coordinates {
|
|
44
|
+
readonly latitude: number;
|
|
45
|
+
readonly longitude: number;
|
|
46
|
+
}
|
|
47
|
+
/** A place as the platform's geocoder names it: street, city, region, country and the rest. */
|
|
48
|
+
export type Address = import('expo-location').LocationGeocodedAddress;
|
|
42
49
|
type ExpoOptions = import('expo-location').LocationOptions;
|
|
43
50
|
/** The slice of `expo-location` this needs. */
|
|
44
51
|
export interface NativeLocation {
|
|
@@ -48,6 +55,8 @@ export interface NativeLocation {
|
|
|
48
55
|
watchPositionAsync(options: ExpoOptions, callback: (fix: ExpoFix) => void): Promise<{
|
|
49
56
|
remove(): void;
|
|
50
57
|
}>;
|
|
58
|
+
geocodeAsync?(address: string): Promise<readonly Coordinates[]>;
|
|
59
|
+
reverseGeocodeAsync?(coordinates: Coordinates): Promise<readonly Address[]>;
|
|
51
60
|
}
|
|
52
61
|
export declare class Location {
|
|
53
62
|
/** Overridden in a test to be somewhere without a GPS. */
|
|
@@ -60,6 +69,14 @@ export declare class Location {
|
|
|
60
69
|
readonly position: Signal<Position | null>;
|
|
61
70
|
/** Read the position once. Null without the permission or the module. */
|
|
62
71
|
current(accuracy?: LocationAccuracy): Promise<Position | null>;
|
|
72
|
+
/**
|
|
73
|
+
* The places an address could be, best match first, from the platform's own geocoder. Empty
|
|
74
|
+
* for an address it does not know, without the permission, and where the source has no
|
|
75
|
+
* geocoder, as off a device. An app built without `expo-location` throws `MissingModuleError`.
|
|
76
|
+
*/
|
|
77
|
+
geocode(address: string): Promise<Coordinates[]>;
|
|
78
|
+
/** The addresses at a point. Empty, and throwing, where `geocode` is. */
|
|
79
|
+
reverseGeocode(coordinates: Coordinates): Promise<Address[]>;
|
|
63
80
|
/** Follow the position into the signal. Resolves to the function that stops. */
|
|
64
81
|
start(options?: WatchOptions): Promise<() => void>;
|
|
65
82
|
static ɵfac: i0.ɵɵFactoryDeclaration<Location, never>;
|
package/dist/location.js
CHANGED
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
*/
|
|
16
16
|
import { InjectionToken, Service, inject, signal } from '@angular/core';
|
|
17
17
|
import { expoModule } from "./native.js";
|
|
18
|
-
import { Permission } from "./permissions.js";
|
|
18
|
+
import { Permission, UNAVAILABLE } from "./permissions.js";
|
|
19
19
|
import * as i0 from "@angular/core";
|
|
20
20
|
/** Expo's `LocationAccuracy` enum, whose values are these numbers. */
|
|
21
21
|
const ACCURACY = {
|
|
@@ -26,11 +26,6 @@ const ACCURACY = {
|
|
|
26
26
|
highest: 5,
|
|
27
27
|
navigation: 6,
|
|
28
28
|
};
|
|
29
|
-
const UNAVAILABLE = async () => ({
|
|
30
|
-
status: 'denied',
|
|
31
|
-
granted: false,
|
|
32
|
-
canAskAgain: false,
|
|
33
|
-
});
|
|
34
29
|
export class Location {
|
|
35
30
|
/** Overridden in a test to be somewhere without a GPS. */
|
|
36
31
|
static SOURCE = new InjectionToken('angular-native.locationSource', {
|
|
@@ -40,7 +35,7 @@ export class Location {
|
|
|
40
35
|
latest = signal(null, /* @ts-ignore */
|
|
41
36
|
...(ngDevMode ? [{ debugName: "latest" }] : /* istanbul ignore next */ []));
|
|
42
37
|
/** The foreground permission, which `current()` and `start()` ask for themselves. */
|
|
43
|
-
permission = Permission.of(this.native?.getForegroundPermissionsAsync ?? UNAVAILABLE, this.native?.requestForegroundPermissionsAsync ?? UNAVAILABLE);
|
|
38
|
+
permission = Permission.of(() => this.native?.getForegroundPermissionsAsync?.() ?? Promise.resolve(UNAVAILABLE), () => this.native?.requestForegroundPermissionsAsync?.() ?? Promise.resolve(UNAVAILABLE));
|
|
44
39
|
/** The last position read, or null before the first. */
|
|
45
40
|
position = this.latest.asReadonly();
|
|
46
41
|
/** Read the position once. Null without the permission or the module. */
|
|
@@ -51,6 +46,24 @@ export class Location {
|
|
|
51
46
|
this.latest.set(positionOf(fix));
|
|
52
47
|
return this.latest();
|
|
53
48
|
}
|
|
49
|
+
/**
|
|
50
|
+
* The places an address could be, best match first, from the platform's own geocoder. Empty
|
|
51
|
+
* for an address it does not know, without the permission, and where the source has no
|
|
52
|
+
* geocoder, as off a device. An app built without `expo-location` throws `MissingModuleError`.
|
|
53
|
+
*/
|
|
54
|
+
async geocode(address) {
|
|
55
|
+
if (!this.native?.geocodeAsync || !(await this.permission.ensure()))
|
|
56
|
+
return [];
|
|
57
|
+
const found = await this.native.geocodeAsync(address);
|
|
58
|
+
return found.map(({ latitude, longitude }) => ({ latitude, longitude }));
|
|
59
|
+
}
|
|
60
|
+
/** The addresses at a point. Empty, and throwing, where `geocode` is. */
|
|
61
|
+
async reverseGeocode(coordinates) {
|
|
62
|
+
if (!this.native?.reverseGeocodeAsync || !(await this.permission.ensure()))
|
|
63
|
+
return [];
|
|
64
|
+
const { latitude, longitude } = coordinates;
|
|
65
|
+
return [...(await this.native.reverseGeocodeAsync({ latitude, longitude }))];
|
|
66
|
+
}
|
|
54
67
|
/** Follow the position into the signal. Resolves to the function that stops. */
|
|
55
68
|
async start(options = {}) {
|
|
56
69
|
if (!this.native || !(await this.permission.ensure()))
|
package/dist/location.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"location.js","sourceRoot":"","sources":["../src/location.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AACH,OAAO,EAAE,cAAc,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,EAAe,MAAM,eAAe,CAAC;AACrF,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AACzC,OAAO,EAAE,UAAU,EAA2B,MAAM,kBAAkB,CAAC;;
|
|
1
|
+
{"version":3,"file":"location.js","sourceRoot":"","sources":["../src/location.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * `Location`, bound to `expo-location`.\n *\n * ```ts\n * private readonly location = inject(Location);\n * protected readonly here = this.location.position;\n *\n * const stop = await this.location.start({ accuracy: 'high', distance: 10 });\n * ```\n *\n * A `position` signal, which a one-off `current()` and a running `start()` both write to. Nothing\n * is watched until `start`, and the accuracy is explicit, for the same reason a sensor's interval\n * is: GPS at navigation accuracy is a battery the user notices. Both ask for the foreground\n * permission first, and do nothing without it.\n */\nimport { InjectionToken, Service, inject, signal, type Signal } from '@angular/core';\nimport { expoModule } from './native.ts';\nimport { Permission, UNAVAILABLE, type PermissionResponse } from './permissions.ts';\n\n/** How precise, in Expo's order: `lowest` is kilometres, `navigation` is metres and a hot GPS. */\nexport type LocationAccuracy = 'lowest' | 'low' | 'balanced' | 'high' | 'highest' | 'navigation';\n\n/** Expo's `LocationAccuracy` enum, whose values are these numbers. */\nconst ACCURACY: Record<LocationAccuracy, number> = {\n lowest: 1,\n low: 2,\n balanced: 3,\n high: 4,\n highest: 5,\n navigation: 6,\n};\n\nexport interface Position {\n readonly latitude: number;\n readonly longitude: number;\n /** Metres above sea level, where the platform knows. */\n readonly altitude: number | null;\n /** The radius, in metres, the fix is good to. */\n readonly accuracy: number | null;\n /** Degrees from true north, while moving. */\n readonly heading: number | null;\n /** Metres per second. */\n readonly speed: number | null;\n readonly timestamp: number;\n}\n\nexport interface WatchOptions {\n readonly accuracy?: LocationAccuracy;\n /** Metres moved before the next update. */\n readonly distance?: number;\n /** Milliseconds between updates. Android only; iOS updates on distance. */\n readonly interval?: number;\n}\n\ntype ExpoFix = import('expo-location').LocationObject;\n/** A point on the map: what an address geocodes to, and what `reverseGeocode` takes. */\nexport interface Coordinates {\n readonly latitude: number;\n readonly longitude: number;\n}\n\n/** A place as the platform's geocoder names it: street, city, region, country and the rest. */\nexport type Address = import('expo-location').LocationGeocodedAddress;\n\ntype ExpoOptions = import('expo-location').LocationOptions;\n\n/** The slice of `expo-location` this needs. */\nexport interface NativeLocation {\n getForegroundPermissionsAsync(): Promise<PermissionResponse>;\n requestForegroundPermissionsAsync(): Promise<PermissionResponse>;\n getCurrentPositionAsync(options?: ExpoOptions): Promise<ExpoFix>;\n watchPositionAsync(\n options: ExpoOptions,\n callback: (fix: ExpoFix) => void,\n ): Promise<{ remove(): void }>;\n geocodeAsync?(address: string): Promise<readonly Coordinates[]>;\n reverseGeocodeAsync?(coordinates: Coordinates): Promise<readonly Address[]>;\n}\n\n@Service()\nexport class Location {\n /** Overridden in a test to be somewhere without a GPS. */\n static readonly SOURCE = new InjectionToken<NativeLocation | null>(\n 'angular-native.locationSource',\n {\n factory: () =>\n expoModule('expo-location', () => require('expo-location') as NativeLocation) ?? null,\n },\n );\n\n private readonly native = inject(Location.SOURCE);\n private readonly latest = signal<Position | null>(null);\n\n /** The foreground permission, which `current()` and `start()` ask for themselves. */\n readonly permission = Permission.of(\n () => this.native?.getForegroundPermissionsAsync?.() ?? Promise.resolve(UNAVAILABLE),\n () => this.native?.requestForegroundPermissionsAsync?.() ?? Promise.resolve(UNAVAILABLE),\n );\n\n /** The last position read, or null before the first. */\n readonly position: Signal<Position | null> = this.latest.asReadonly();\n\n /** Read the position once. Null without the permission or the module. */\n async current(accuracy: LocationAccuracy = 'balanced'): Promise<Position | null> {\n if (!this.native || !(await this.permission.ensure())) return null;\n const fix = await this.native.getCurrentPositionAsync({ accuracy: ACCURACY[accuracy] });\n this.latest.set(positionOf(fix));\n return this.latest();\n }\n\n /**\n * The places an address could be, best match first, from the platform's own geocoder. Empty\n * for an address it does not know, without the permission, and where the source has no\n * geocoder, as off a device. An app built without `expo-location` throws `MissingModuleError`.\n */\n async geocode(address: string): Promise<Coordinates[]> {\n if (!this.native?.geocodeAsync || !(await this.permission.ensure())) return [];\n const found = await this.native.geocodeAsync(address);\n return found.map(({ latitude, longitude }) => ({ latitude, longitude }));\n }\n\n /** The addresses at a point. Empty, and throwing, where `geocode` is. */\n async reverseGeocode(coordinates: Coordinates): Promise<Address[]> {\n if (!this.native?.reverseGeocodeAsync || !(await this.permission.ensure())) return [];\n const { latitude, longitude } = coordinates;\n return [...(await this.native.reverseGeocodeAsync({ latitude, longitude }))];\n }\n\n /** Follow the position into the signal. Resolves to the function that stops. */\n async start(options: WatchOptions = {}): Promise<() => void> {\n if (!this.native || !(await this.permission.ensure())) return () => {};\n const expo: ExpoOptions = { accuracy: ACCURACY[options.accuracy ?? 'balanced'] };\n if (options.distance !== undefined) expo.distanceInterval = options.distance;\n if (options.interval !== undefined) expo.timeInterval = options.interval;\n const subscription = await this.native.watchPositionAsync(expo, (fix) =>\n this.latest.set(positionOf(fix)),\n );\n return () => subscription.remove();\n }\n}\n\nfunction positionOf({ coords, timestamp }: ExpoFix): Position {\n return {\n latitude: coords.latitude,\n longitude: coords.longitude,\n altitude: coords.altitude,\n accuracy: coords.accuracy,\n heading: coords.heading,\n speed: coords.speed,\n timestamp,\n };\n}\n"]}
|
package/dist/media-library.js
CHANGED
|
@@ -33,7 +33,7 @@
|
|
|
33
33
|
*/
|
|
34
34
|
import { DestroyRef, InjectionToken, Service, inject, signal } from '@angular/core';
|
|
35
35
|
import { expoModule } from "./native.js";
|
|
36
|
-
import { Permission } from "./permissions.js";
|
|
36
|
+
import { Permission, UNAVAILABLE } from "./permissions.js";
|
|
37
37
|
import * as i0 from "@angular/core";
|
|
38
38
|
/**
|
|
39
39
|
* The kinds of asset, as `expo-media-library`'s own enum. Importing the enum from the module
|
|
@@ -55,8 +55,6 @@ export const AssetField = {
|
|
|
55
55
|
DURATION: 'duration',
|
|
56
56
|
IS_FAVORITE: 'isFavorite',
|
|
57
57
|
};
|
|
58
|
-
/** The permission answer without the module: nothing granted, and nothing to ask. */
|
|
59
|
-
const UNAVAILABLE = { status: 'denied', granted: false, canAskAgain: false };
|
|
60
58
|
export class MediaLibrary {
|
|
61
59
|
/** Overridden in a test to save photos to a library that is not there. */
|
|
62
60
|
static SOURCE = new InjectionToken('angular-native.mediaLibrarySource', {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"media-library.js","sourceRoot":"","sources":["../src/media-library.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * `MediaLibrary`, bound to `expo-media-library`: saving to and reading from the user's photos and\n * videos.\n *\n * ```ts\n * import { AssetField, MediaLibrary, MediaType } from '@ng-native/expo/media-library';\n *\n * private readonly library = inject(MediaLibrary);\n *\n * async keep(uri: string): Promise<void> {\n * await this.library.save(uri);\n * }\n *\n * async recentPhotos(): Promise<Asset[]> {\n * if (!(await this.library.permission.ensure())) return [];\n * return this.library.assets((query) =>\n * query.eq(AssetField.MEDIA_TYPE, MediaType.IMAGE).orderBy(AssetField.CREATION_TIME).limit(30),\n * );\n * }\n * ```\n *\n * The module's API is its classes: an `Asset` and an `Album` are handles whose details are read\n * with their own async getters, and a `Query` is built by chaining. This service is where they\n * are reached from, so a test can provide all three, and where the two permissions are: full\n * access to read the library, and the write-only access saving needs, which is the one to ask for\n * when that is all the app does.\n *\n * Changes to the library are a signal, but only once `watch()` is called: starting the listener\n * reads the library, which is not something to do before the user has granted access.\n *\n * Without the module installed, every query answers empty, saving answers null, and the\n * permissions are refused.\n */\nimport { DestroyRef, InjectionToken, Service, inject, signal, type Signal } from '@angular/core';\nimport type {\n Album,\n Asset,\n AssetField as ExpoAssetField,\n AssetMetadata,\n MediaLibraryAssetsChangeEvent,\n MediaType as ExpoMediaType,\n Query,\n} from 'expo-media-library';\nimport { expoModule } from './native.ts';\nimport { Permission, type PermissionResponse } from './permissions.ts';\n\ntype Expo = typeof import('expo-media-library');\n\n/** The module's functions and classes this service calls. A test provides fakes of each. */\nexport type NativeMediaLibrary = Pick<\n Expo,\n | 'getPermissionsAsync'\n | 'requestPermissionsAsync'\n | 'presentPermissionsPicker'\n | 'addListener'\n | 'Asset'\n | 'Album'\n | 'Query'\n>;\n\ntype Args<K extends keyof NativeMediaLibrary> = NativeMediaLibrary[K] extends (\n ...args: infer A\n) => unknown\n ? A\n : never;\n\n/**\n * The kinds of asset, as `expo-media-library`'s own enum. Importing the enum from the module\n * loads the module, which a test in Node cannot; these are the same strings, typed as the enum.\n */\nexport const MediaType = {\n UNKNOWN: 'unknown',\n IMAGE: 'image',\n AUDIO: 'audio',\n VIDEO: 'video',\n} as unknown as typeof ExpoMediaType;\n\n/** The fields a query filters and sorts on, as the module's own enum. */\nexport const AssetField = {\n CREATION_TIME: 'creationTime',\n MODIFICATION_TIME: 'modificationTime',\n MEDIA_TYPE: 'mediaType',\n WIDTH: 'width',\n HEIGHT: 'height',\n DURATION: 'duration',\n IS_FAVORITE: 'isFavorite',\n} as unknown as typeof ExpoAssetField;\n\n/** The permission answer without the module: nothing granted, and nothing to ask. */\nconst UNAVAILABLE: PermissionResponse = { status: 'denied', granted: false, canAskAgain: false };\n\n@Service()\nexport class MediaLibrary {\n /** Overridden in a test to save photos to a library that is not there. */\n static readonly SOURCE = new InjectionToken<NativeMediaLibrary | null>(\n 'angular-native.mediaLibrarySource',\n {\n factory: () => expoModule('expo-media-library', () => require('expo-media-library') as Expo),\n },\n );\n\n private readonly native = inject(MediaLibrary.SOURCE);\n private readonly destroyRef = inject(DestroyRef);\n private readonly changed = signal<MediaLibraryAssetsChangeEvent | null>(null);\n private readonly changes = this.changed.asReadonly();\n private watching = false;\n\n /** Full access: reading the library, as well as adding to it. */\n readonly permission: Permission = this.permissionFor(false);\n\n /** Write-only access, which is all `save()` needs and what it asks for. */\n readonly writePermission: Permission = this.permissionFor(true);\n\n /**\n * Asks with the module's own options: write-only, and on Android 13 and later which of\n * `'photo'`, `'video'` and `'audio'` to ask for. The answer also updates {@link permission}.\n */\n async requestPermission(...options: Args<'requestPermissionsAsync'>): Promise<boolean> {\n if (!this.native) return false;\n await this.native.requestPermissionsAsync(...options);\n return this.permission.check();\n }\n\n /**\n * On iOS and Android 14 and later, after the user has granted access to only some photos, shows\n * the system picker for changing which. Does nothing otherwise.\n */\n async presentPermissionsPicker(...mediaTypes: Args<'presentPermissionsPicker'>): Promise<void> {\n await this.native?.presentPermissionsPicker(...mediaTypes);\n }\n\n /**\n * Saves a local image or video file to the library, into an album if one is given. Asks for\n * write-only access first; null if it is refused.\n */\n async save(uri: string, album?: Album): Promise<Asset | null> {\n if (!this.native || !(await this.writePermission.ensure())) return null;\n return this.native.Asset.create(uri, album);\n }\n\n /**\n * The assets a query finds, built on the module's own `Query`: `eq`, `within`, `gt`, `lt`,\n * `orderBy`, `limit`, `offset` and `album`. Every asset without one.\n */\n async assets(build: (query: Query) => Query = (query) => query): Promise<Asset[]> {\n if (!this.native) return [];\n return build(new this.native.Query()).exe();\n }\n\n /** The same query, answered with each asset's details at once rather than as handles. */\n async metadata(build: (query: Query) => Query = (query) => query): Promise<AssetMetadata[]> {\n if (!this.native) return [];\n return build(new this.native.Query()).exeForMetadata();\n }\n\n /** The handle for an asset whose id is already known, such as one kept from an earlier query. */\n asset(id: string): Asset | null {\n return this.native ? new this.native.Asset(id) : null;\n }\n\n async deleteAssets(assets: Asset[]): Promise<void> {\n await this.native?.Asset.delete(assets);\n }\n\n async albums(): Promise<Album[]> {\n return (await this.native?.Album.getAll()) ?? [];\n }\n\n /** The album with this title, or null. */\n async album(title: string): Promise<Album | null> {\n return (await this.native?.Album.get(title)) ?? null;\n }\n\n /** A new album holding the assets given, copied into it unless `move` is true. */\n async createAlbum(\n name: string,\n assets: Asset[] | string[],\n move?: boolean,\n ): Promise<Album | null> {\n return (await this.native?.Album.create(name, assets, move)) ?? null;\n }\n\n /** Deletes the albums, and with `deleteAssets` the assets in them too. */\n async deleteAlbums(albums: Album[], deleteAssets?: boolean): Promise<void> {\n await this.native?.Album.delete(albums, deleteAssets);\n }\n\n /**\n * The library's most recent change, from the first call on: on iOS the assets inserted, deleted\n * and updated, on Android an empty event. Call it once access is granted, since listening reads\n * the library. The listener is removed when the app is destroyed.\n */\n watch(): Signal<MediaLibraryAssetsChangeEvent | null> {\n if (this.native && !this.watching) {\n this.watching = true;\n const subscription = this.native.addListener((event) => this.changed.set(event));\n this.destroyRef.onDestroy(() => subscription.remove());\n }\n return this.changes;\n }\n\n private permissionFor(writeOnly: boolean): Permission {\n return Permission.of(\n () => this.native?.getPermissionsAsync(writeOnly) ?? Promise.resolve(UNAVAILABLE),\n () => this.native?.requestPermissionsAsync(writeOnly) ?? Promise.resolve(UNAVAILABLE),\n );\n }\n}\n"]}
|
|
1
|
+
{"version":3,"file":"media-library.js","sourceRoot":"","sources":["../src/media-library.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * `MediaLibrary`, bound to `expo-media-library`: saving to and reading from the user's photos and\n * videos.\n *\n * ```ts\n * import { AssetField, MediaLibrary, MediaType } from '@ng-native/expo/media-library';\n *\n * private readonly library = inject(MediaLibrary);\n *\n * async keep(uri: string): Promise<void> {\n * await this.library.save(uri);\n * }\n *\n * async recentPhotos(): Promise<Asset[]> {\n * if (!(await this.library.permission.ensure())) return [];\n * return this.library.assets((query) =>\n * query.eq(AssetField.MEDIA_TYPE, MediaType.IMAGE).orderBy(AssetField.CREATION_TIME).limit(30),\n * );\n * }\n * ```\n *\n * The module's API is its classes: an `Asset` and an `Album` are handles whose details are read\n * with their own async getters, and a `Query` is built by chaining. This service is where they\n * are reached from, so a test can provide all three, and where the two permissions are: full\n * access to read the library, and the write-only access saving needs, which is the one to ask for\n * when that is all the app does.\n *\n * Changes to the library are a signal, but only once `watch()` is called: starting the listener\n * reads the library, which is not something to do before the user has granted access.\n *\n * Without the module installed, every query answers empty, saving answers null, and the\n * permissions are refused.\n */\nimport { DestroyRef, InjectionToken, Service, inject, signal, type Signal } from '@angular/core';\nimport type {\n Album,\n Asset,\n AssetField as ExpoAssetField,\n AssetMetadata,\n MediaLibraryAssetsChangeEvent,\n MediaType as ExpoMediaType,\n Query,\n} from 'expo-media-library';\nimport { expoModule } from './native.ts';\nimport { Permission, UNAVAILABLE } from './permissions.ts';\n\ntype Expo = typeof import('expo-media-library');\n\n/** The module's functions and classes this service calls. A test provides fakes of each. */\nexport type NativeMediaLibrary = Pick<\n Expo,\n | 'getPermissionsAsync'\n | 'requestPermissionsAsync'\n | 'presentPermissionsPicker'\n | 'addListener'\n | 'Asset'\n | 'Album'\n | 'Query'\n>;\n\ntype Args<K extends keyof NativeMediaLibrary> = NativeMediaLibrary[K] extends (\n ...args: infer A\n) => unknown\n ? A\n : never;\n\n/**\n * The kinds of asset, as `expo-media-library`'s own enum. Importing the enum from the module\n * loads the module, which a test in Node cannot; these are the same strings, typed as the enum.\n */\nexport const MediaType = {\n UNKNOWN: 'unknown',\n IMAGE: 'image',\n AUDIO: 'audio',\n VIDEO: 'video',\n} as unknown as typeof ExpoMediaType;\n\n/** The fields a query filters and sorts on, as the module's own enum. */\nexport const AssetField = {\n CREATION_TIME: 'creationTime',\n MODIFICATION_TIME: 'modificationTime',\n MEDIA_TYPE: 'mediaType',\n WIDTH: 'width',\n HEIGHT: 'height',\n DURATION: 'duration',\n IS_FAVORITE: 'isFavorite',\n} as unknown as typeof ExpoAssetField;\n\n@Service()\nexport class MediaLibrary {\n /** Overridden in a test to save photos to a library that is not there. */\n static readonly SOURCE = new InjectionToken<NativeMediaLibrary | null>(\n 'angular-native.mediaLibrarySource',\n {\n factory: () => expoModule('expo-media-library', () => require('expo-media-library') as Expo),\n },\n );\n\n private readonly native = inject(MediaLibrary.SOURCE);\n private readonly destroyRef = inject(DestroyRef);\n private readonly changed = signal<MediaLibraryAssetsChangeEvent | null>(null);\n private readonly changes = this.changed.asReadonly();\n private watching = false;\n\n /** Full access: reading the library, as well as adding to it. */\n readonly permission: Permission = this.permissionFor(false);\n\n /** Write-only access, which is all `save()` needs and what it asks for. */\n readonly writePermission: Permission = this.permissionFor(true);\n\n /**\n * Asks with the module's own options: write-only, and on Android 13 and later which of\n * `'photo'`, `'video'` and `'audio'` to ask for. The answer also updates {@link permission}.\n */\n async requestPermission(...options: Args<'requestPermissionsAsync'>): Promise<boolean> {\n if (!this.native) return false;\n await this.native.requestPermissionsAsync(...options);\n return this.permission.check();\n }\n\n /**\n * On iOS and Android 14 and later, after the user has granted access to only some photos, shows\n * the system picker for changing which. Does nothing otherwise.\n */\n async presentPermissionsPicker(...mediaTypes: Args<'presentPermissionsPicker'>): Promise<void> {\n await this.native?.presentPermissionsPicker(...mediaTypes);\n }\n\n /**\n * Saves a local image or video file to the library, into an album if one is given. Asks for\n * write-only access first; null if it is refused.\n */\n async save(uri: string, album?: Album): Promise<Asset | null> {\n if (!this.native || !(await this.writePermission.ensure())) return null;\n return this.native.Asset.create(uri, album);\n }\n\n /**\n * The assets a query finds, built on the module's own `Query`: `eq`, `within`, `gt`, `lt`,\n * `orderBy`, `limit`, `offset` and `album`. Every asset without one.\n */\n async assets(build: (query: Query) => Query = (query) => query): Promise<Asset[]> {\n if (!this.native) return [];\n return build(new this.native.Query()).exe();\n }\n\n /** The same query, answered with each asset's details at once rather than as handles. */\n async metadata(build: (query: Query) => Query = (query) => query): Promise<AssetMetadata[]> {\n if (!this.native) return [];\n return build(new this.native.Query()).exeForMetadata();\n }\n\n /** The handle for an asset whose id is already known, such as one kept from an earlier query. */\n asset(id: string): Asset | null {\n return this.native ? new this.native.Asset(id) : null;\n }\n\n async deleteAssets(assets: Asset[]): Promise<void> {\n await this.native?.Asset.delete(assets);\n }\n\n async albums(): Promise<Album[]> {\n return (await this.native?.Album.getAll()) ?? [];\n }\n\n /** The album with this title, or null. */\n async album(title: string): Promise<Album | null> {\n return (await this.native?.Album.get(title)) ?? null;\n }\n\n /** A new album holding the assets given, copied into it unless `move` is true. */\n async createAlbum(\n name: string,\n assets: Asset[] | string[],\n move?: boolean,\n ): Promise<Album | null> {\n return (await this.native?.Album.create(name, assets, move)) ?? null;\n }\n\n /** Deletes the albums, and with `deleteAssets` the assets in them too. */\n async deleteAlbums(albums: Album[], deleteAssets?: boolean): Promise<void> {\n await this.native?.Album.delete(albums, deleteAssets);\n }\n\n /**\n * The library's most recent change, from the first call on: on iOS the assets inserted, deleted\n * and updated, on Android an empty event. Call it once access is granted, since listening reads\n * the library. The listener is removed when the app is destroyed.\n */\n watch(): Signal<MediaLibraryAssetsChangeEvent | null> {\n if (this.native && !this.watching) {\n this.watching = true;\n const subscription = this.native.addListener((event) => this.changed.set(event));\n this.destroyRef.onDestroy(() => subscription.remove());\n }\n return this.changes;\n }\n\n private permissionFor(writeOnly: boolean): Permission {\n return Permission.of(\n () => this.native?.getPermissionsAsync(writeOnly) ?? Promise.resolve(UNAVAILABLE),\n () => this.native?.requestPermissionsAsync(writeOnly) ?? Promise.resolve(UNAVAILABLE),\n );\n }\n}\n"]}
|
package/dist/native.d.ts
CHANGED
|
@@ -25,8 +25,17 @@ export declare class MissingModuleError extends Error {
|
|
|
25
25
|
readonly name = "MissingModuleError";
|
|
26
26
|
/** The package, as it is installed: `expo-haptics`. */
|
|
27
27
|
readonly module: string;
|
|
28
|
-
constructor(module: string, platform: ModulePlatform, cause?: unknown);
|
|
28
|
+
constructor(module: string, platform: ModulePlatform, cause?: unknown, message?: string);
|
|
29
29
|
}
|
|
30
|
+
/**
|
|
31
|
+
* The `MissingModuleError` to throw where `expoModule` answered null: the module is installed or not, but there is
|
|
32
|
+
* nothing to load it on. In Node, where a test runs, that is every native module, and "install it"
|
|
33
|
+
* would be advice to do what is already done; `inTest` says what a test does instead. On a platform
|
|
34
|
+
* the module does not support, it says that.
|
|
35
|
+
*/
|
|
36
|
+
export declare function unavailable(module: string, inTest: string): MissingModuleError;
|
|
37
|
+
/** The platform the app is running on, or null in Node, where there is none to ask. */
|
|
38
|
+
export declare function currentPlatform(): ModulePlatform | null;
|
|
30
39
|
/**
|
|
31
40
|
* The Expo module a service needs.
|
|
32
41
|
*
|
|
@@ -37,6 +46,7 @@ export declare class MissingModuleError extends Error {
|
|
|
37
46
|
* goes inert, as code shared across platforms needs it to.
|
|
38
47
|
*
|
|
39
48
|
* `load` answering null or undefined counts as missing too: `requireOptionalNativeModule` answers
|
|
40
|
-
* null rather than throwing.
|
|
49
|
+
* null rather than throwing. So does a package whose native half is not in the build, which `load`
|
|
50
|
+
* is not called for: see `nativeHalfMissing`.
|
|
41
51
|
*/
|
|
42
52
|
export declare function expoModule<T>(module: string, load: () => T | null | undefined, platforms?: readonly ModulePlatform[]): T | null;
|
package/dist/native.js
CHANGED
|
@@ -30,19 +30,89 @@ export class MissingModuleError extends Error {
|
|
|
30
30
|
name = 'MissingModuleError';
|
|
31
31
|
/** The package, as it is installed: `expo-haptics`. */
|
|
32
32
|
module;
|
|
33
|
-
constructor(module, platform, cause) {
|
|
34
|
-
super(
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
33
|
+
constructor(module, platform, cause, message) {
|
|
34
|
+
super(message ??
|
|
35
|
+
(platform === 'web'
|
|
36
|
+
? `${module} is not installed. Install it with "npx expo install ${module}", then restart the dev server.`
|
|
37
|
+
: `${module} is not in this build of the app. Install it with "npx expo install ${module}", ` +
|
|
38
|
+
`then rebuild the app ("npx expo run:${platform}", or a new EAS build): a development ` +
|
|
39
|
+
`build, and Expo Go, contain only the native modules they were built with.`), { cause });
|
|
39
40
|
this.module = module;
|
|
40
41
|
}
|
|
41
42
|
}
|
|
43
|
+
/**
|
|
44
|
+
* The `MissingModuleError` to throw where `expoModule` answered null: the module is installed or not, but there is
|
|
45
|
+
* nothing to load it on. In Node, where a test runs, that is every native module, and "install it"
|
|
46
|
+
* would be advice to do what is already done; `inTest` says what a test does instead. On a platform
|
|
47
|
+
* the module does not support, it says that.
|
|
48
|
+
*/
|
|
49
|
+
export function unavailable(module, inTest) {
|
|
50
|
+
const platform = currentPlatform();
|
|
51
|
+
const message = platform === null
|
|
52
|
+
? `[angular-native] ${module} has no native module to load in Node, where a test runs. ${inTest}`
|
|
53
|
+
: `[angular-native] ${module} is not available on ${platform}.`;
|
|
54
|
+
// The platform is only read for the default message, which this replaces.
|
|
55
|
+
return new MissingModuleError(module, platform ?? 'web', undefined, message);
|
|
56
|
+
}
|
|
42
57
|
/** The platform the app is running on, or null in Node, where there is none to ask. */
|
|
43
|
-
function currentPlatform() {
|
|
58
|
+
export function currentPlatform() {
|
|
44
59
|
return optional(() => require('react-native').Platform.OS);
|
|
45
60
|
}
|
|
61
|
+
/**
|
|
62
|
+
* For each Expo package whose JavaScript requires a native module as it is evaluated, and so
|
|
63
|
+
* throws when its native half is not in the build, one of those native modules. Any one of them
|
|
64
|
+
* says whether the package's native half is there, and each of these is registered on both iOS and
|
|
65
|
+
* Android.
|
|
66
|
+
*/
|
|
67
|
+
const NATIVE_MODULES = {
|
|
68
|
+
'expo-application': 'ExpoApplication',
|
|
69
|
+
'expo-asset': 'ExpoAsset',
|
|
70
|
+
'expo-audio': 'ExpoAudio',
|
|
71
|
+
'expo-background-task': 'ExpoBackgroundTask',
|
|
72
|
+
'expo-battery': 'ExpoBattery',
|
|
73
|
+
'expo-brightness': 'ExpoBrightness',
|
|
74
|
+
'expo-clipboard': 'ExpoClipboard',
|
|
75
|
+
'expo-crypto': 'ExpoCrypto',
|
|
76
|
+
'expo-device': 'ExpoDevice',
|
|
77
|
+
'expo-document-picker': 'ExpoDocumentPicker',
|
|
78
|
+
'expo-file-system': 'FileSystem',
|
|
79
|
+
'expo-foldables': 'ExpoFoldables',
|
|
80
|
+
'expo-font': 'ExpoFontLoader',
|
|
81
|
+
'expo-image-manipulator': 'ExpoImageManipulator',
|
|
82
|
+
'expo-image-picker': 'ExponentImagePicker',
|
|
83
|
+
'expo-keep-awake': 'ExpoKeepAwake',
|
|
84
|
+
'expo-local-authentication': 'ExpoLocalAuthentication',
|
|
85
|
+
'expo-local-llm': 'ExpoLocalLlm',
|
|
86
|
+
'expo-localization': 'ExpoLocalization',
|
|
87
|
+
'expo-location': 'ExpoLocation',
|
|
88
|
+
'expo-media-library': 'ExpoMediaLibrary',
|
|
89
|
+
'expo-network': 'ExpoNetwork',
|
|
90
|
+
'expo-notifications': 'ExpoNotificationPresenter',
|
|
91
|
+
'expo-screen-capture': 'ExpoScreenCapture',
|
|
92
|
+
'expo-screen-orientation': 'ExpoScreenOrientation',
|
|
93
|
+
'expo-secure-store': 'ExpoSecureStore',
|
|
94
|
+
'expo-sensors': 'ExponentAccelerometer',
|
|
95
|
+
'expo-sqlite': 'ExpoSQLite',
|
|
96
|
+
'expo-store-review': 'ExpoStoreReview',
|
|
97
|
+
'expo-tracking-transparency': 'ExpoTrackingTransparency',
|
|
98
|
+
'expo-updates': 'ExpoUpdates',
|
|
99
|
+
'expo-video': 'ExpoVideo',
|
|
100
|
+
'expo-web-browser': 'ExpoWebBrowser',
|
|
101
|
+
};
|
|
102
|
+
/**
|
|
103
|
+
* Whether `module`'s native half is missing from the build on `platform`, asked of Expo before its
|
|
104
|
+
* JavaScript is evaluated. That JavaScript throws while it is being evaluated when its native module
|
|
105
|
+
* is not there, and Metro reports that as fatal, before any `catch` here, whenever it is not inside
|
|
106
|
+
* another module's load: in a service's factory, say. Not on the web, where a package registers its
|
|
107
|
+
* module only once it is evaluated.
|
|
108
|
+
*/
|
|
109
|
+
function nativeHalfMissing(module, platform) {
|
|
110
|
+
const name = NATIVE_MODULES[module];
|
|
111
|
+
if (!name || platform === null || platform === 'web')
|
|
112
|
+
return false;
|
|
113
|
+
const core = optional(() => require('expo-modules-core'));
|
|
114
|
+
return core !== null && !core.requireOptionalNativeModule(name);
|
|
115
|
+
}
|
|
46
116
|
/**
|
|
47
117
|
* The Expo module a service needs.
|
|
48
118
|
*
|
|
@@ -53,19 +123,20 @@ function currentPlatform() {
|
|
|
53
123
|
* goes inert, as code shared across platforms needs it to.
|
|
54
124
|
*
|
|
55
125
|
* `load` answering null or undefined counts as missing too: `requireOptionalNativeModule` answers
|
|
56
|
-
* null rather than throwing.
|
|
126
|
+
* null rather than throwing. So does a package whose native half is not in the build, which `load`
|
|
127
|
+
* is not called for: see `nativeHalfMissing`.
|
|
57
128
|
*/
|
|
58
129
|
export function expoModule(module, load, platforms = ['ios', 'android']) {
|
|
130
|
+
const platform = currentPlatform();
|
|
59
131
|
let failure;
|
|
60
132
|
try {
|
|
61
|
-
const loaded = load();
|
|
133
|
+
const loaded = nativeHalfMissing(module, platform) ? null : load();
|
|
62
134
|
if (loaded != null)
|
|
63
135
|
return loaded;
|
|
64
136
|
}
|
|
65
137
|
catch (error) {
|
|
66
138
|
failure = error;
|
|
67
139
|
}
|
|
68
|
-
const platform = currentPlatform();
|
|
69
140
|
if (platform === null || !platforms.includes(platform))
|
|
70
141
|
return null;
|
|
71
142
|
throw new MissingModuleError(module, platform, failure);
|
package/dist/native.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"native.js","sourceRoot":"","sources":["../src/native.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AACH,MAAM,UAAU,QAAQ,CAAI,IAAa;IACvC,IAAI,CAAC;QACH,OAAO,IAAI,EAAE,CAAC;IAChB,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,IAAI,CAAC;IACd,CAAC;AACH,CAAC;AAKD;;;;GAIG;AACH,MAAM,OAAO,kBAAmB,SAAQ,KAAK;IACzB,IAAI,GAAG,oBAAoB,CAAC;IAC9C,uDAAuD;IAC9C,MAAM,CAAS;IAExB,YAAY,MAAc,EAAE,QAAwB,EAAE,KAAe;QACnE,KAAK,CACH,QAAQ,KAAK,KAAK;YAChB,CAAC,CAAC,GAAG,MAAM,wDAAwD,MAAM,iCAAiC;YAC1G,CAAC,CAAC,GAAG,MAAM,uEAAuE,MAAM,KAAK;gBACzF,uCAAuC,QAAQ,wCAAwC;gBACvF,2EAA2E,EACjF,EAAE,KAAK,EAAE,CACV,CAAC;QACF,IAAI,CAAC,MAAM,GAAG,MAAM,CAAC;IACvB,CAAC;CACF;AAED,uFAAuF;AACvF,SAAS,eAAe;IACtB,OAAO,QAAQ,CACb,GAAG,EAAE,CAAE,OAAO,CAAC,cAAc,CAAkC,CAAC,QAAQ,CAAC,EAAoB,CAC9F,CAAC;AACJ,CAAC;AAED;;;;;;;;;;;GAWG;AACH,MAAM,UAAU,UAAU,CACxB,MAAc,EACd,IAAgC,EAChC,YAAuC,CAAC,KAAK,EAAE,SAAS,CAAC;IAEzD,IAAI,OAAgB,CAAC;IACrB,IAAI,CAAC;QACH,MAAM,MAAM,GAAG,IAAI,EAAE,CAAC;QACtB,IAAI,MAAM,IAAI,IAAI;YAAE,OAAO,MAAM,CAAC;IACpC,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,OAAO,GAAG,KAAK,CAAC;IAClB,CAAC;IACD,MAAM,QAAQ,GAAG,eAAe,EAAE,CAAC;IACnC,IAAI,QAAQ,KAAK,IAAI,IAAI,CAAC,SAAS,CAAC,QAAQ,CAAC,QAAQ,CAAC;QAAE,OAAO,IAAI,CAAC;IACpE,MAAM,IAAI,kBAAkB,CAAC,MAAM,EAAE,QAAQ,EAAE,OAAO,CAAC,CAAC;AAC1D,CAAC","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) {\n super(\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/** The platform the app is running on, or null in Node, where there is none to ask. */\nfunction currentPlatform(): ModulePlatform | null {\n return optional(\n () => (require('react-native') as { Platform: { OS: string } }).Platform.OS as ModulePlatform,\n );\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.\n */\nexport function expoModule<T>(\n module: string,\n load: () => T | null | undefined,\n platforms: readonly ModulePlatform[] = ['ios', 'android'],\n): T | null {\n let failure: unknown;\n try {\n const loaded = load();\n if (loaded != null) return loaded;\n } catch (error) {\n failure = error;\n }\n const platform = currentPlatform();\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":"AAAA;;;;;;;;;;;;;;GAcG;AACH,MAAM,UAAU,QAAQ,CAAI,IAAa;IACvC,IAAI,CAAC;QACH,OAAO,IAAI,EAAE,CAAC;IAChB,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,IAAI,CAAC;IACd,CAAC;AACH,CAAC;AAKD;;;;GAIG;AACH,MAAM,OAAO,kBAAmB,SAAQ,KAAK;IACzB,IAAI,GAAG,oBAAoB,CAAC;IAC9C,uDAAuD;IAC9C,MAAM,CAAS;IAExB,YAAY,MAAc,EAAE,QAAwB,EAAE,KAAe,EAAE,OAAgB;QACrF,KAAK,CACH,OAAO;YACL,CAAC,QAAQ,KAAK,KAAK;gBACjB,CAAC,CAAC,GAAG,MAAM,wDAAwD,MAAM,iCAAiC;gBAC1G,CAAC,CAAC,GAAG,MAAM,uEAAuE,MAAM,KAAK;oBAC3F,uCAAuC,QAAQ,wCAAwC;oBACvF,2EAA2E,CAAC,EAClF,EAAE,KAAK,EAAE,CACV,CAAC;QACF,IAAI,CAAC,MAAM,GAAG,MAAM,CAAC;IACvB,CAAC;CACF;AAED;;;;;GAKG;AACH,MAAM,UAAU,WAAW,CAAC,MAAc,EAAE,MAAc;IACxD,MAAM,QAAQ,GAAG,eAAe,EAAE,CAAC;IACnC,MAAM,OAAO,GACX,QAAQ,KAAK,IAAI;QACf,CAAC,CAAC,oBAAoB,MAAM,6DAA6D,MAAM,EAAE;QACjG,CAAC,CAAC,oBAAoB,MAAM,wBAAwB,QAAQ,GAAG,CAAC;IACpE,0EAA0E;IAC1E,OAAO,IAAI,kBAAkB,CAAC,MAAM,EAAE,QAAQ,IAAI,KAAK,EAAE,SAAS,EAAE,OAAO,CAAC,CAAC;AAC/E,CAAC;AAED,uFAAuF;AACvF,MAAM,UAAU,eAAe;IAC7B,OAAO,QAAQ,CACb,GAAG,EAAE,CAAE,OAAO,CAAC,cAAc,CAAkC,CAAC,QAAQ,CAAC,EAAoB,CAC9F,CAAC;AACJ,CAAC;AAED;;;;;GAKG;AACH,MAAM,cAAc,GAAqC;IACvD,kBAAkB,EAAE,iBAAiB;IACrC,YAAY,EAAE,WAAW;IACzB,YAAY,EAAE,WAAW;IACzB,sBAAsB,EAAE,oBAAoB;IAC5C,cAAc,EAAE,aAAa;IAC7B,iBAAiB,EAAE,gBAAgB;IACnC,gBAAgB,EAAE,eAAe;IACjC,aAAa,EAAE,YAAY;IAC3B,aAAa,EAAE,YAAY;IAC3B,sBAAsB,EAAE,oBAAoB;IAC5C,kBAAkB,EAAE,YAAY;IAChC,gBAAgB,EAAE,eAAe;IACjC,WAAW,EAAE,gBAAgB;IAC7B,wBAAwB,EAAE,sBAAsB;IAChD,mBAAmB,EAAE,qBAAqB;IAC1C,iBAAiB,EAAE,eAAe;IAClC,2BAA2B,EAAE,yBAAyB;IACtD,gBAAgB,EAAE,cAAc;IAChC,mBAAmB,EAAE,kBAAkB;IACvC,eAAe,EAAE,cAAc;IAC/B,oBAAoB,EAAE,kBAAkB;IACxC,cAAc,EAAE,aAAa;IAC7B,oBAAoB,EAAE,2BAA2B;IACjD,qBAAqB,EAAE,mBAAmB;IAC1C,yBAAyB,EAAE,uBAAuB;IAClD,mBAAmB,EAAE,iBAAiB;IACtC,cAAc,EAAE,uBAAuB;IACvC,aAAa,EAAE,YAAY;IAC3B,mBAAmB,EAAE,iBAAiB;IACtC,4BAA4B,EAAE,0BAA0B;IACxD,cAAc,EAAE,aAAa;IAC7B,YAAY,EAAE,WAAW;IACzB,kBAAkB,EAAE,gBAAgB;CACrC,CAAC;AAEF;;;;;;GAMG;AACH,SAAS,iBAAiB,CAAC,MAAc,EAAE,QAA+B;IACxE,MAAM,IAAI,GAAG,cAAc,CAAC,MAAM,CAAC,CAAC;IACpC,IAAI,CAAC,IAAI,IAAI,QAAQ,KAAK,IAAI,IAAI,QAAQ,KAAK,KAAK;QAAE,OAAO,KAAK,CAAC;IACnE,MAAM,IAAI,GAAG,QAAQ,CAAC,GAAG,EAAE,CAAC,OAAO,CAAC,mBAAmB,CAAuC,CAAC,CAAC;IAChG,OAAO,IAAI,KAAK,IAAI,IAAI,CAAC,IAAI,CAAC,2BAA2B,CAAC,IAAI,CAAC,CAAC;AAClE,CAAC;AAED;;;;;;;;;;;;GAYG;AACH,MAAM,UAAU,UAAU,CACxB,MAAc,EACd,IAAgC,EAChC,YAAuC,CAAC,KAAK,EAAE,SAAS,CAAC;IAEzD,MAAM,QAAQ,GAAG,eAAe,EAAE,CAAC;IACnC,IAAI,OAAgB,CAAC;IACrB,IAAI,CAAC;QACH,MAAM,MAAM,GAAG,iBAAiB,CAAC,MAAM,EAAE,QAAQ,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC;QACnE,IAAI,MAAM,IAAI,IAAI;YAAE,OAAO,MAAM,CAAC;IACpC,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,OAAO,GAAG,KAAK,CAAC;IAClB,CAAC;IACD,IAAI,QAAQ,KAAK,IAAI,IAAI,CAAC,SAAS,CAAC,QAAQ,CAAC,QAAQ,CAAC;QAAE,OAAO,IAAI,CAAC;IACpE,MAAM,IAAI,kBAAkB,CAAC,MAAM,EAAE,QAAQ,EAAE,OAAO,CAAC,CAAC;AAC1D,CAAC","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"]}
|
package/dist/notifications.js
CHANGED
|
@@ -36,7 +36,7 @@
|
|
|
36
36
|
*/
|
|
37
37
|
import { DestroyRef, InjectionToken, Service, inject, signal } from '@angular/core';
|
|
38
38
|
import { expoModule } from "./native.js";
|
|
39
|
-
import { Permission } from "./permissions.js";
|
|
39
|
+
import { Permission, UNAVAILABLE } from "./permissions.js";
|
|
40
40
|
import * as i0 from "@angular/core";
|
|
41
41
|
/**
|
|
42
42
|
* The kinds of trigger a schedule takes, as `expo-notifications`' own enum. Importing the enum
|
|
@@ -52,8 +52,6 @@ export const TriggerType = {
|
|
|
52
52
|
DATE: 'date',
|
|
53
53
|
TIME_INTERVAL: 'timeInterval',
|
|
54
54
|
};
|
|
55
|
-
/** The permission answer without the module: nothing granted, and nothing to ask. */
|
|
56
|
-
const UNAVAILABLE = { status: 'denied', granted: false, canAskAgain: false };
|
|
57
55
|
export class Notifications {
|
|
58
56
|
/** Overridden in a test to deliver a notification nobody sent. */
|
|
59
57
|
static SOURCE = new InjectionToken('angular-native.notificationsSource', {
|
|
@@ -76,7 +74,7 @@ export class Notifications {
|
|
|
76
74
|
permission;
|
|
77
75
|
constructor() {
|
|
78
76
|
const native = this.native;
|
|
79
|
-
this.permission = Permission.of(() => native?.getPermissionsAsync() ?? Promise.resolve(UNAVAILABLE), () => native?.requestPermissionsAsync() ?? Promise.resolve(UNAVAILABLE));
|
|
77
|
+
this.permission = Permission.of(() => native?.getPermissionsAsync?.() ?? Promise.resolve(UNAVAILABLE), () => native?.requestPermissionsAsync?.() ?? Promise.resolve(UNAVAILABLE));
|
|
80
78
|
if (!native)
|
|
81
79
|
return;
|
|
82
80
|
const subscriptions = [
|