@ng-native/expo 0.0.1 → 0.1.1
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/LICENSE +21 -0
- package/README.md +413 -1
- package/dist/app-info.d.ts +58 -0
- package/dist/app-info.js +71 -0
- package/dist/app-info.js.map +1 -0
- package/dist/apple-sign-in-button.d.ts +16 -0
- package/dist/apple-sign-in-button.js +55 -0
- package/dist/apple-sign-in-button.js.map +1 -0
- package/dist/apple-sign-in.d.ts +68 -0
- package/dist/apple-sign-in.js +120 -0
- package/dist/apple-sign-in.js.map +1 -0
- package/dist/assets.d.ts +24 -0
- package/dist/assets.js +47 -0
- package/dist/assets.js.map +1 -0
- package/dist/background-task.d.ts +66 -0
- package/dist/background-task.js +82 -0
- package/dist/background-task.js.map +1 -0
- package/dist/battery.d.ts +46 -0
- package/dist/battery.js +76 -0
- package/dist/battery.js.map +1 -0
- package/dist/biometrics.d.ts +45 -0
- package/dist/biometrics.js +53 -0
- package/dist/biometrics.js.map +1 -0
- package/dist/brightness.d.ts +38 -0
- package/dist/brightness.js +81 -0
- package/dist/brightness.js.map +1 -0
- package/dist/browser.d.ts +42 -0
- package/dist/browser.js +42 -0
- package/dist/browser.js.map +1 -0
- package/dist/camera.d.ts +48 -0
- package/dist/camera.js +59 -0
- package/dist/camera.js.map +1 -0
- package/dist/clipboard.d.ts +43 -0
- package/dist/clipboard.js +60 -0
- package/dist/clipboard.js.map +1 -0
- package/dist/community-views.d.ts +14 -0
- package/dist/community-views.js +76 -0
- package/dist/community-views.js.map +1 -0
- package/dist/crypto.d.ts +56 -0
- package/dist/crypto.js +84 -0
- package/dist/crypto.js.map +1 -0
- package/dist/database.d.ts +47 -0
- package/dist/database.js +108 -0
- package/dist/database.js.map +1 -0
- package/dist/document-picker.d.ts +34 -0
- package/dist/document-picker.js +40 -0
- package/dist/document-picker.js.map +1 -0
- package/dist/dom-component.d.ts +87 -0
- package/dist/dom-component.js +139 -0
- package/dist/dom-component.js.map +1 -0
- package/dist/expo-image.d.ts +33 -0
- package/dist/expo-image.js +57 -0
- package/dist/expo-image.js.map +1 -0
- package/dist/expo-ui-components.d.ts +327 -0
- package/dist/expo-ui-components.js +707 -0
- package/dist/expo-ui-components.js.map +1 -0
- package/dist/expo-ui.d.ts +46 -0
- package/dist/expo-ui.js +128 -0
- package/dist/expo-ui.js.map +1 -0
- package/dist/file-system.d.ts +63 -0
- package/dist/file-system.js +67 -0
- package/dist/file-system.js.map +1 -0
- package/dist/fonts.d.ts +90 -0
- package/dist/fonts.js +118 -0
- package/dist/fonts.js.map +1 -0
- package/dist/glass.d.ts +33 -0
- package/dist/glass.js +72 -0
- package/dist/glass.js.map +1 -0
- package/dist/haptics.d.ts +49 -0
- package/dist/haptics.js +64 -0
- package/dist/haptics.js.map +1 -0
- package/dist/image-editor.d.ts +62 -0
- package/dist/image-editor.js +101 -0
- package/dist/image-editor.js.map +1 -0
- package/dist/image-picker.d.ts +49 -0
- package/dist/image-picker.js +58 -0
- package/dist/image-picker.js.map +1 -0
- package/dist/index.d.ts +20 -0
- package/dist/index.js +21 -0
- package/dist/index.js.map +1 -0
- package/dist/keep-awake.d.ts +39 -0
- package/dist/keep-awake.js +80 -0
- package/dist/keep-awake.js.map +1 -0
- package/dist/language-model.d.ts +129 -0
- package/dist/language-model.js +225 -0
- package/dist/language-model.js.map +1 -0
- package/dist/locale.d.ts +51 -0
- package/dist/locale.js +78 -0
- package/dist/locale.js.map +1 -0
- package/dist/location.d.ts +68 -0
- package/dist/location.js +83 -0
- package/dist/location.js.map +1 -0
- package/dist/map-view.d.ts +203 -0
- package/dist/map-view.js +184 -0
- package/dist/map-view.js.map +1 -0
- package/dist/media-library.d.ts +103 -0
- package/dist/media-library.js +160 -0
- package/dist/media-library.js.map +1 -0
- package/dist/native-state.d.ts +22 -0
- package/dist/native-state.js +43 -0
- package/dist/native-state.js.map +1 -0
- package/dist/native.d.ts +42 -0
- package/dist/native.js +73 -0
- package/dist/native.js.map +1 -0
- package/dist/network.d.ts +35 -0
- package/dist/network.js +60 -0
- package/dist/network.js.map +1 -0
- package/dist/notifications.d.ts +175 -0
- package/dist/notifications.js +296 -0
- package/dist/notifications.js.map +1 -0
- package/dist/observed.d.ts +37 -0
- package/dist/observed.js +55 -0
- package/dist/observed.js.map +1 -0
- package/dist/orientation.d.ts +35 -0
- package/dist/orientation.js +78 -0
- package/dist/orientation.js.map +1 -0
- package/dist/permissions.d.ts +54 -0
- package/dist/permissions.js +72 -0
- package/dist/permissions.js.map +1 -0
- package/dist/player.d.ts +92 -0
- package/dist/player.js +141 -0
- package/dist/player.js.map +1 -0
- package/dist/register-expo-view.d.ts +55 -0
- package/dist/register-expo-view.js +110 -0
- package/dist/register-expo-view.js.map +1 -0
- package/dist/screen-capture.d.ts +58 -0
- package/dist/screen-capture.js +78 -0
- package/dist/screen-capture.js.map +1 -0
- package/dist/segmented-control.d.ts +32 -0
- package/dist/segmented-control.js +55 -0
- package/dist/segmented-control.js.map +1 -0
- package/dist/sensors.d.ts +85 -0
- package/dist/sensors.js +115 -0
- package/dist/sensors.js.map +1 -0
- package/dist/splash-screen.d.ts +73 -0
- package/dist/splash-screen.js +115 -0
- package/dist/splash-screen.js.map +1 -0
- package/dist/store-review.d.ts +43 -0
- package/dist/store-review.js +52 -0
- package/dist/store-review.js.map +1 -0
- package/dist/store.d.ts +108 -0
- package/dist/store.js +247 -0
- package/dist/store.js.map +1 -0
- package/dist/symbol.d.ts +36 -0
- package/dist/symbol.js +69 -0
- package/dist/symbol.js.map +1 -0
- package/dist/tracking.d.ts +45 -0
- package/dist/tracking.js +54 -0
- package/dist/tracking.js.map +1 -0
- package/dist/transforms.d.ts +2 -0
- package/dist/transforms.js +13 -0
- package/dist/transforms.js.map +1 -0
- package/dist/updates.d.ts +61 -0
- package/dist/updates.js +97 -0
- package/dist/updates.js.map +1 -0
- package/package.json +397 -3
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `Notifications`, bound to `expo-notifications`: everything the module does, as one injectable
|
|
3
|
+
* service.
|
|
4
|
+
*
|
|
5
|
+
* ```ts
|
|
6
|
+
* import { Notifications, TriggerType } from '@ng-native/expo/notifications';
|
|
7
|
+
*
|
|
8
|
+
* private readonly notifications = inject(Notifications);
|
|
9
|
+
*
|
|
10
|
+
* async remind(): Promise<void> {
|
|
11
|
+
* if (!(await this.notifications.permission.ensure())) return;
|
|
12
|
+
* await this.notifications.schedule({
|
|
13
|
+
* content: { title: 'Time to stretch' },
|
|
14
|
+
* trigger: { type: TriggerType.DAILY, hour: 9, minute: 0 },
|
|
15
|
+
* });
|
|
16
|
+
* }
|
|
17
|
+
*
|
|
18
|
+
* constructor() {
|
|
19
|
+
* const tapped = this.notifications.take();
|
|
20
|
+
* if (tapped) this.router.navigateByUrl(routeFor(tapped));
|
|
21
|
+
* }
|
|
22
|
+
* ```
|
|
23
|
+
*
|
|
24
|
+
* The listeners are the part a hook used to be the only way to reach, and they are signals here.
|
|
25
|
+
* *Received* is a notification arriving while the app is in front; the app usually decides whether
|
|
26
|
+
* to show anything. *Responded* is the user having tapped one, which is a navigation instruction
|
|
27
|
+
* and must not be missed - including the one that launched the app, which arrived before anything
|
|
28
|
+
* was listening.
|
|
29
|
+
*
|
|
30
|
+
* The rest - permission, scheduling, what is on screen, the badge, Android's channels, the action
|
|
31
|
+
* buttons of categories, the foreground handler, push tokens and topics, background tasks - is the
|
|
32
|
+
* module's own functions under shorter names, with its own types. Without the module installed,
|
|
33
|
+
* or in a test that provides nothing, every query answers empty and every change does nothing.
|
|
34
|
+
* The push token methods ask for the permission first, since a token nobody can be shown anything
|
|
35
|
+
* with fails silently: the push simply never arrives.
|
|
36
|
+
*/
|
|
37
|
+
import { DestroyRef, InjectionToken, Service, inject, signal } from '@angular/core';
|
|
38
|
+
import { expoModule } from "./native.js";
|
|
39
|
+
import { Permission } from "./permissions.js";
|
|
40
|
+
import * as i0 from "@angular/core";
|
|
41
|
+
/**
|
|
42
|
+
* The kinds of trigger a schedule takes, as `expo-notifications`' own enum. Importing the enum
|
|
43
|
+
* from the module loads the module, which a test in Node cannot; these are the same strings,
|
|
44
|
+
* typed as the enum, so `{ type: TriggerType.DAILY, hour: 9, minute: 0 }` is accepted either way.
|
|
45
|
+
*/
|
|
46
|
+
export const TriggerType = {
|
|
47
|
+
CALENDAR: 'calendar',
|
|
48
|
+
DAILY: 'daily',
|
|
49
|
+
WEEKLY: 'weekly',
|
|
50
|
+
MONTHLY: 'monthly',
|
|
51
|
+
YEARLY: 'yearly',
|
|
52
|
+
DATE: 'date',
|
|
53
|
+
TIME_INTERVAL: 'timeInterval',
|
|
54
|
+
};
|
|
55
|
+
/** The permission answer without the module: nothing granted, and nothing to ask. */
|
|
56
|
+
const UNAVAILABLE = { status: 'denied', granted: false, canAskAgain: false };
|
|
57
|
+
export class Notifications {
|
|
58
|
+
/** Overridden in a test to deliver a notification nobody sent. */
|
|
59
|
+
static SOURCE = new InjectionToken('angular-native.notificationsSource', {
|
|
60
|
+
factory: () => expoModule('expo-notifications', () => require('expo-notifications')),
|
|
61
|
+
});
|
|
62
|
+
native = inject(Notifications.SOURCE);
|
|
63
|
+
received = signal(null, /* @ts-ignore */
|
|
64
|
+
...(ngDevMode ? [{ debugName: "received" }] : /* istanbul ignore next */ []));
|
|
65
|
+
responded = signal(null, /* @ts-ignore */
|
|
66
|
+
...(ngDevMode ? [{ debugName: "responded" }] : /* istanbul ignore next */ []));
|
|
67
|
+
rolled = signal(null, /* @ts-ignore */
|
|
68
|
+
...(ngDevMode ? [{ debugName: "rolled" }] : /* istanbul ignore next */ []));
|
|
69
|
+
drops = signal(0, /* @ts-ignore */
|
|
70
|
+
...(ngDevMode ? [{ debugName: "drops" }] : /* istanbul ignore next */ []));
|
|
71
|
+
handled = new Set();
|
|
72
|
+
/**
|
|
73
|
+
* The permission to show notifications. `ensure()` asks only if it has not been answered; for
|
|
74
|
+
* iOS's finer options (provisional, critical alerts), use {@link requestPermission}.
|
|
75
|
+
*/
|
|
76
|
+
permission;
|
|
77
|
+
constructor() {
|
|
78
|
+
const native = this.native;
|
|
79
|
+
this.permission = Permission.of(() => native?.getPermissionsAsync() ?? Promise.resolve(UNAVAILABLE), () => native?.requestPermissionsAsync() ?? Promise.resolve(UNAVAILABLE));
|
|
80
|
+
if (!native)
|
|
81
|
+
return;
|
|
82
|
+
const subscriptions = [
|
|
83
|
+
native.addNotificationReceivedListener((notification) => this.received.set(notification)),
|
|
84
|
+
native.addNotificationResponseReceivedListener((response) => this.responded.set(response)),
|
|
85
|
+
// Android drops notifications when too many arrive at once while the app is not running;
|
|
86
|
+
// this is the only word that any went missing.
|
|
87
|
+
native.addNotificationsDroppedListener(() => this.drops.update((count) => count + 1)),
|
|
88
|
+
// A device push token can be rolled by the platform while the app is running; the old one
|
|
89
|
+
// then fails silently rather than delivering anything. This is the only way to find out.
|
|
90
|
+
native.addPushTokenListener((token) => this.rolled.set(token)),
|
|
91
|
+
];
|
|
92
|
+
inject(DestroyRef).onDestroy(() => subscriptions.forEach((one) => one.remove()));
|
|
93
|
+
// The response that launched the app happened before anything was listening, so it has to be
|
|
94
|
+
// asked for rather than waited for. Without this a cold start from a notification does
|
|
95
|
+
// nothing, which is the single most common way this is got wrong. A platform that cannot say
|
|
96
|
+
// is the same as a launch without a notification.
|
|
97
|
+
void native
|
|
98
|
+
.getLastNotificationResponseAsync()
|
|
99
|
+
.then((response) => {
|
|
100
|
+
if (response && this.responded() === null)
|
|
101
|
+
this.responded.set(response);
|
|
102
|
+
})
|
|
103
|
+
.catch(() => { });
|
|
104
|
+
}
|
|
105
|
+
/** The most recent notification that arrived while the app was in front. */
|
|
106
|
+
latest = this.received.asReadonly();
|
|
107
|
+
/**
|
|
108
|
+
* The most recent notification the user tapped, including the one that launched the app.
|
|
109
|
+
*
|
|
110
|
+
* A router usually wants `take()` instead: this stays set, so a screen that reacts to it will
|
|
111
|
+
* react again every time it is created.
|
|
112
|
+
*/
|
|
113
|
+
response = this.responded.asReadonly();
|
|
114
|
+
/**
|
|
115
|
+
* The response, once.
|
|
116
|
+
*
|
|
117
|
+
* Routing on a tap is a one-off, and the signal is not: an effect on it fires again whenever
|
|
118
|
+
* its component is recreated, which sends the user back to a screen they navigated away from.
|
|
119
|
+
*/
|
|
120
|
+
take() {
|
|
121
|
+
const response = this.responded();
|
|
122
|
+
if (!response)
|
|
123
|
+
return null;
|
|
124
|
+
const id = response.notification.request.identifier + response.actionIdentifier;
|
|
125
|
+
if (this.handled.has(id))
|
|
126
|
+
return null;
|
|
127
|
+
this.handled.add(id);
|
|
128
|
+
return response;
|
|
129
|
+
}
|
|
130
|
+
/** Forgets the tap, here and in the module, so the next launch does not see it again. */
|
|
131
|
+
async clearResponse() {
|
|
132
|
+
this.responded.set(null);
|
|
133
|
+
await this.native?.clearLastNotificationResponseAsync();
|
|
134
|
+
}
|
|
135
|
+
/** How many times Android has reported notifications dropped. Zero on iOS. */
|
|
136
|
+
dropped = this.drops.asReadonly();
|
|
137
|
+
/** Asks with iOS's finer options. The answer also updates {@link permission}. */
|
|
138
|
+
async requestPermission(...options) {
|
|
139
|
+
if (!this.native)
|
|
140
|
+
return false;
|
|
141
|
+
await this.native.requestPermissionsAsync(...options);
|
|
142
|
+
return this.permission.check();
|
|
143
|
+
}
|
|
144
|
+
/** Schedules a local notification, or shows it now with a null trigger. Null without the module. */
|
|
145
|
+
async schedule(...request) {
|
|
146
|
+
return (await this.native?.scheduleNotificationAsync(...request)) ?? null;
|
|
147
|
+
}
|
|
148
|
+
async cancel(identifier) {
|
|
149
|
+
await this.native?.cancelScheduledNotificationAsync(identifier);
|
|
150
|
+
}
|
|
151
|
+
async cancelAll() {
|
|
152
|
+
await this.native?.cancelAllScheduledNotificationsAsync();
|
|
153
|
+
}
|
|
154
|
+
/** Every local notification still waiting for its trigger. */
|
|
155
|
+
async scheduled() {
|
|
156
|
+
return (await this.native?.getAllScheduledNotificationsAsync()) ?? [];
|
|
157
|
+
}
|
|
158
|
+
/** When a trigger would next fire, in milliseconds since the epoch; null if it never will. */
|
|
159
|
+
async nextTriggerDate(...trigger) {
|
|
160
|
+
return (await this.native?.getNextTriggerDateAsync(...trigger)) ?? null;
|
|
161
|
+
}
|
|
162
|
+
/** The notifications in the notification centre that are the app's. */
|
|
163
|
+
async presented() {
|
|
164
|
+
return (await this.native?.getPresentedNotificationsAsync()) ?? [];
|
|
165
|
+
}
|
|
166
|
+
async dismiss(identifier) {
|
|
167
|
+
await this.native?.dismissNotificationAsync(identifier);
|
|
168
|
+
}
|
|
169
|
+
/** Clears every notification the app has on screen. A failure is ignored. */
|
|
170
|
+
dismissAll() {
|
|
171
|
+
void this.native?.dismissAllNotificationsAsync().catch(() => { });
|
|
172
|
+
}
|
|
173
|
+
async badge() {
|
|
174
|
+
return (await this.native?.getBadgeCountAsync()) ?? 0;
|
|
175
|
+
}
|
|
176
|
+
/**
|
|
177
|
+
* iOS shows this on the icon; Android shows it where the launcher supports it. A failure is
|
|
178
|
+
* ignored.
|
|
179
|
+
*/
|
|
180
|
+
setBadge(count) {
|
|
181
|
+
void this.native?.setBadgeCountAsync(count).catch(() => { });
|
|
182
|
+
}
|
|
183
|
+
/** Android's notification channels. Empty on iOS, which has none. */
|
|
184
|
+
async channels() {
|
|
185
|
+
return (await this.native?.getNotificationChannelsAsync()) ?? [];
|
|
186
|
+
}
|
|
187
|
+
async channel(identifier) {
|
|
188
|
+
return (await this.native?.getNotificationChannelAsync(identifier)) ?? null;
|
|
189
|
+
}
|
|
190
|
+
/**
|
|
191
|
+
* Creates or updates an Android channel; a notification on Android 8 and later needs one to
|
|
192
|
+
* show at all. Null on iOS.
|
|
193
|
+
*/
|
|
194
|
+
async setChannel(...channel) {
|
|
195
|
+
return (await this.native?.setNotificationChannelAsync(...channel)) ?? null;
|
|
196
|
+
}
|
|
197
|
+
async deleteChannel(identifier) {
|
|
198
|
+
await this.native?.deleteNotificationChannelAsync(identifier);
|
|
199
|
+
}
|
|
200
|
+
async channelGroups() {
|
|
201
|
+
return (await this.native?.getNotificationChannelGroupsAsync()) ?? [];
|
|
202
|
+
}
|
|
203
|
+
async channelGroup(identifier) {
|
|
204
|
+
return (await this.native?.getNotificationChannelGroupAsync(identifier)) ?? null;
|
|
205
|
+
}
|
|
206
|
+
async setChannelGroup(...group) {
|
|
207
|
+
return (await this.native?.setNotificationChannelGroupAsync(...group)) ?? null;
|
|
208
|
+
}
|
|
209
|
+
async deleteChannelGroup(identifier) {
|
|
210
|
+
await this.native?.deleteNotificationChannelGroupAsync(identifier);
|
|
211
|
+
}
|
|
212
|
+
/** The categories set: each a set of action buttons a notification opts into. */
|
|
213
|
+
async categories() {
|
|
214
|
+
return (await this.native?.getNotificationCategoriesAsync()) ?? [];
|
|
215
|
+
}
|
|
216
|
+
/**
|
|
217
|
+
* Action buttons for notifications whose content names this category. A tap on one arrives as
|
|
218
|
+
* a response with the button's identifier, and the typed text as `userText` for a text input.
|
|
219
|
+
*/
|
|
220
|
+
async setCategory(...category) {
|
|
221
|
+
return (await this.native?.setNotificationCategoryAsync(...category)) ?? null;
|
|
222
|
+
}
|
|
223
|
+
async deleteCategory(identifier) {
|
|
224
|
+
return (await this.native?.deleteNotificationCategoryAsync(identifier)) ?? false;
|
|
225
|
+
}
|
|
226
|
+
/**
|
|
227
|
+
* What a notification that arrives while the app is in front does: shown, silent, with a sound
|
|
228
|
+
* or a badge. Without a handler, it is not shown. Null removes the handler.
|
|
229
|
+
*/
|
|
230
|
+
setHandler(...handler) {
|
|
231
|
+
this.native?.setNotificationHandler(...handler);
|
|
232
|
+
}
|
|
233
|
+
/**
|
|
234
|
+
* The token to hand a backend so it can send a push through Expo's service.
|
|
235
|
+
*
|
|
236
|
+
* Asks for the notification permission first - a token nobody can be shown anything with is not
|
|
237
|
+
* worth having. `projectId` defaults to the app's own EAS project; pass it explicitly for a bare
|
|
238
|
+
* workflow app, one still without EAS Build configured, or a token fetched outside
|
|
239
|
+
* `expo-notifications`' default resolution. Null without the permission or without the module.
|
|
240
|
+
*/
|
|
241
|
+
async getExpoPushToken(projectId) {
|
|
242
|
+
if (!this.native || !(await this.permission.ensure()))
|
|
243
|
+
return null;
|
|
244
|
+
const token = await this.native.getExpoPushTokenAsync(projectId ? { projectId } : undefined);
|
|
245
|
+
return token.data;
|
|
246
|
+
}
|
|
247
|
+
/**
|
|
248
|
+
* The raw APNs (iOS) or FCM (Android) token, for a backend that talks to those services
|
|
249
|
+
* directly rather than through Expo's push service. Same permission and module rules as
|
|
250
|
+
* {@link getExpoPushToken}.
|
|
251
|
+
*/
|
|
252
|
+
async getDevicePushToken() {
|
|
253
|
+
if (!this.native || !(await this.permission.ensure()))
|
|
254
|
+
return null;
|
|
255
|
+
return this.native.getDevicePushTokenAsync();
|
|
256
|
+
}
|
|
257
|
+
/**
|
|
258
|
+
* The device push token, each time the platform rolls it while the app is running.
|
|
259
|
+
*
|
|
260
|
+
* A rolled token is not the one a backend has on file, and sending to the old one fails
|
|
261
|
+
* silently - this is what tells an app to register the new one. Unset until a roll happens;
|
|
262
|
+
* most of an app's life this stays null.
|
|
263
|
+
*/
|
|
264
|
+
devicePushToken = this.rolled.asReadonly();
|
|
265
|
+
/** Stops the device receiving pushes until a token is asked for again. */
|
|
266
|
+
async unregister() {
|
|
267
|
+
await this.native?.unregisterForNotificationsAsync();
|
|
268
|
+
}
|
|
269
|
+
/** Whether Expo re-registers the device token with its push service by itself. */
|
|
270
|
+
async setAutoServerRegistration(enabled) {
|
|
271
|
+
await this.native?.setAutoServerRegistrationEnabledAsync(enabled);
|
|
272
|
+
}
|
|
273
|
+
/** Android: receive pushes sent to an FCM topic. */
|
|
274
|
+
async subscribeToTopic(topic) {
|
|
275
|
+
await this.native?.subscribeToTopicAsync(topic);
|
|
276
|
+
}
|
|
277
|
+
async unsubscribeFromTopic(topic) {
|
|
278
|
+
await this.native?.unsubscribeFromTopicAsync(topic);
|
|
279
|
+
}
|
|
280
|
+
/**
|
|
281
|
+
* Runs a task defined with `expo-task-manager` when a notification arrives in the background.
|
|
282
|
+
* The task itself is defined at the top level of `main.ts`, since it runs without the app.
|
|
283
|
+
*/
|
|
284
|
+
async registerTask(name) {
|
|
285
|
+
await this.native?.registerTaskAsync(name);
|
|
286
|
+
}
|
|
287
|
+
async unregisterTask(name) {
|
|
288
|
+
await this.native?.unregisterTaskAsync(name);
|
|
289
|
+
}
|
|
290
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.2.0", ngImport: i0, type: Notifications, deps: [], target: i0.ɵɵFactoryTarget.Service });
|
|
291
|
+
static ɵprov = i0.ɵɵngDeclareService({ minVersion: "22.0.0", version: "22.2.0", ngImport: i0, type: Notifications });
|
|
292
|
+
}
|
|
293
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.2.0", ngImport: i0, type: Notifications, decorators: [{
|
|
294
|
+
type: Service
|
|
295
|
+
}], ctorParameters: () => [] });
|
|
296
|
+
//# sourceMappingURL=notifications.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"notifications.js","sourceRoot":"","sources":["../src/notifications.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * `Notifications`, bound to `expo-notifications`: everything the module does, as one injectable\n * service.\n *\n * ```ts\n * import { Notifications, TriggerType } from '@ng-native/expo/notifications';\n *\n * private readonly notifications = inject(Notifications);\n *\n * async remind(): Promise<void> {\n * if (!(await this.notifications.permission.ensure())) return;\n * await this.notifications.schedule({\n * content: { title: 'Time to stretch' },\n * trigger: { type: TriggerType.DAILY, hour: 9, minute: 0 },\n * });\n * }\n *\n * constructor() {\n * const tapped = this.notifications.take();\n * if (tapped) this.router.navigateByUrl(routeFor(tapped));\n * }\n * ```\n *\n * The listeners are the part a hook used to be the only way to reach, and they are signals here.\n * *Received* is a notification arriving while the app is in front; the app usually decides whether\n * to show anything. *Responded* is the user having tapped one, which is a navigation instruction\n * and must not be missed - including the one that launched the app, which arrived before anything\n * was listening.\n *\n * The rest - permission, scheduling, what is on screen, the badge, Android's channels, the action\n * buttons of categories, the foreground handler, push tokens and topics, background tasks - is the\n * module's own functions under shorter names, with its own types. Without the module installed,\n * or in a test that provides nothing, every query answers empty and every change does nothing.\n * The push token methods ask for the permission first, since a token nobody can be shown anything\n * with fails silently: the push simply never arrives.\n */\nimport { DestroyRef, InjectionToken, Service, inject, signal, type Signal } from '@angular/core';\nimport type {\n DevicePushToken,\n Notification,\n NotificationResponse,\n SchedulableTriggerInputTypes,\n} from 'expo-notifications';\nimport { expoModule } from './native.ts';\nimport { Permission, type PermissionResponse } from './permissions.ts';\n\ntype Expo = typeof import('expo-notifications');\n\n/**\n * The module's functions this service calls. The real module is one; a test provides a fake that\n * delivers notifications nobody sent.\n */\nexport type NativeNotifications = Pick<\n Expo,\n | 'addNotificationReceivedListener'\n | 'addNotificationResponseReceivedListener'\n | 'addNotificationsDroppedListener'\n | 'addPushTokenListener'\n | 'getLastNotificationResponseAsync'\n | 'clearLastNotificationResponseAsync'\n | 'getPermissionsAsync'\n | 'requestPermissionsAsync'\n | 'scheduleNotificationAsync'\n | 'cancelScheduledNotificationAsync'\n | 'cancelAllScheduledNotificationsAsync'\n | 'getAllScheduledNotificationsAsync'\n | 'getNextTriggerDateAsync'\n | 'getPresentedNotificationsAsync'\n | 'dismissNotificationAsync'\n | 'dismissAllNotificationsAsync'\n | 'getBadgeCountAsync'\n | 'setBadgeCountAsync'\n | 'getNotificationChannelsAsync'\n | 'getNotificationChannelAsync'\n | 'setNotificationChannelAsync'\n | 'deleteNotificationChannelAsync'\n | 'getNotificationChannelGroupsAsync'\n | 'getNotificationChannelGroupAsync'\n | 'setNotificationChannelGroupAsync'\n | 'deleteNotificationChannelGroupAsync'\n | 'getNotificationCategoriesAsync'\n | 'setNotificationCategoryAsync'\n | 'deleteNotificationCategoryAsync'\n | 'setNotificationHandler'\n | 'getExpoPushTokenAsync'\n | 'getDevicePushTokenAsync'\n | 'unregisterForNotificationsAsync'\n | 'setAutoServerRegistrationEnabledAsync'\n | 'subscribeToTopicAsync'\n | 'unsubscribeFromTopicAsync'\n | 'registerTaskAsync'\n | 'unregisterTaskAsync'\n>;\n\ntype Args<K extends keyof NativeNotifications> = Parameters<NativeNotifications[K]>;\n\n/**\n * The kinds of trigger a schedule takes, as `expo-notifications`' own enum. Importing the enum\n * from the module loads the module, which a test in Node cannot; these are the same strings,\n * typed as the enum, so `{ type: TriggerType.DAILY, hour: 9, minute: 0 }` is accepted either way.\n */\nexport const TriggerType = {\n CALENDAR: 'calendar',\n DAILY: 'daily',\n WEEKLY: 'weekly',\n MONTHLY: 'monthly',\n YEARLY: 'yearly',\n DATE: 'date',\n TIME_INTERVAL: 'timeInterval',\n} as unknown as typeof SchedulableTriggerInputTypes;\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 Notifications {\n /** Overridden in a test to deliver a notification nobody sent. */\n static readonly SOURCE = new InjectionToken<NativeNotifications | null>(\n 'angular-native.notificationsSource',\n {\n factory: () => expoModule('expo-notifications', () => require('expo-notifications') as Expo),\n },\n );\n\n private readonly native = inject(Notifications.SOURCE);\n private readonly received = signal<Notification | null>(null);\n private readonly responded = signal<NotificationResponse | null>(null);\n private readonly rolled = signal<DevicePushToken | null>(null);\n private readonly drops = signal(0);\n private readonly handled = new Set<string>();\n\n /**\n * The permission to show notifications. `ensure()` asks only if it has not been answered; for\n * iOS's finer options (provisional, critical alerts), use {@link requestPermission}.\n */\n readonly permission: Permission;\n\n constructor() {\n const native = this.native;\n this.permission = Permission.of(\n () => native?.getPermissionsAsync() ?? Promise.resolve(UNAVAILABLE),\n () => native?.requestPermissionsAsync() ?? Promise.resolve(UNAVAILABLE),\n );\n if (!native) return;\n const subscriptions = [\n native.addNotificationReceivedListener((notification) => this.received.set(notification)),\n native.addNotificationResponseReceivedListener((response) => this.responded.set(response)),\n // Android drops notifications when too many arrive at once while the app is not running;\n // this is the only word that any went missing.\n native.addNotificationsDroppedListener(() => this.drops.update((count) => count + 1)),\n // A device push token can be rolled by the platform while the app is running; the old one\n // then fails silently rather than delivering anything. This is the only way to find out.\n native.addPushTokenListener((token) => this.rolled.set(token)),\n ];\n inject(DestroyRef).onDestroy(() => subscriptions.forEach((one) => one.remove()));\n // The response that launched the app happened before anything was listening, so it has to be\n // asked for rather than waited for. Without this a cold start from a notification does\n // nothing, which is the single most common way this is got wrong. A platform that cannot say\n // is the same as a launch without a notification.\n void native\n .getLastNotificationResponseAsync()\n .then((response) => {\n if (response && this.responded() === null) this.responded.set(response);\n })\n .catch(() => {});\n }\n\n /** The most recent notification that arrived while the app was in front. */\n readonly latest: Signal<Notification | null> = this.received.asReadonly();\n\n /**\n * The most recent notification the user tapped, including the one that launched the app.\n *\n * A router usually wants `take()` instead: this stays set, so a screen that reacts to it will\n * react again every time it is created.\n */\n readonly response: Signal<NotificationResponse | null> = this.responded.asReadonly();\n\n /**\n * The response, once.\n *\n * Routing on a tap is a one-off, and the signal is not: an effect on it fires again whenever\n * its component is recreated, which sends the user back to a screen they navigated away from.\n */\n take(): NotificationResponse | null {\n const response = this.responded();\n if (!response) return null;\n\n const id = response.notification.request.identifier + response.actionIdentifier;\n if (this.handled.has(id)) return null;\n this.handled.add(id);\n return response;\n }\n\n /** Forgets the tap, here and in the module, so the next launch does not see it again. */\n async clearResponse(): Promise<void> {\n this.responded.set(null);\n await this.native?.clearLastNotificationResponseAsync();\n }\n\n /** How many times Android has reported notifications dropped. Zero on iOS. */\n readonly dropped: Signal<number> = this.drops.asReadonly();\n\n /** Asks with iOS's finer options. The answer also updates {@link permission}. */\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 /** Schedules a local notification, or shows it now with a null trigger. Null without the module. */\n async schedule(...request: Args<'scheduleNotificationAsync'>): Promise<string | null> {\n return (await this.native?.scheduleNotificationAsync(...request)) ?? null;\n }\n\n async cancel(identifier: string): Promise<void> {\n await this.native?.cancelScheduledNotificationAsync(identifier);\n }\n\n async cancelAll(): Promise<void> {\n await this.native?.cancelAllScheduledNotificationsAsync();\n }\n\n /** Every local notification still waiting for its trigger. */\n async scheduled(): Promise<Awaited<ReturnType<Expo['getAllScheduledNotificationsAsync']>>> {\n return (await this.native?.getAllScheduledNotificationsAsync()) ?? [];\n }\n\n /** When a trigger would next fire, in milliseconds since the epoch; null if it never will. */\n async nextTriggerDate(...trigger: Args<'getNextTriggerDateAsync'>): Promise<number | null> {\n return (await this.native?.getNextTriggerDateAsync(...trigger)) ?? null;\n }\n\n /** The notifications in the notification centre that are the app's. */\n async presented(): Promise<Notification[]> {\n return (await this.native?.getPresentedNotificationsAsync()) ?? [];\n }\n\n async dismiss(identifier: string): Promise<void> {\n await this.native?.dismissNotificationAsync(identifier);\n }\n\n /** Clears every notification the app has on screen. A failure is ignored. */\n dismissAll(): void {\n void this.native?.dismissAllNotificationsAsync().catch(() => {});\n }\n\n async badge(): Promise<number> {\n return (await this.native?.getBadgeCountAsync()) ?? 0;\n }\n\n /**\n * iOS shows this on the icon; Android shows it where the launcher supports it. A failure is\n * ignored.\n */\n setBadge(count: number): void {\n void this.native?.setBadgeCountAsync(count).catch(() => {});\n }\n\n /** Android's notification channels. Empty on iOS, which has none. */\n async channels(): Promise<Awaited<ReturnType<Expo['getNotificationChannelsAsync']>>> {\n return (await this.native?.getNotificationChannelsAsync()) ?? [];\n }\n\n async channel(\n identifier: string,\n ): Promise<Awaited<ReturnType<Expo['getNotificationChannelAsync']>>> {\n return (await this.native?.getNotificationChannelAsync(identifier)) ?? null;\n }\n\n /**\n * Creates or updates an Android channel; a notification on Android 8 and later needs one to\n * show at all. Null on iOS.\n */\n async setChannel(\n ...channel: Args<'setNotificationChannelAsync'>\n ): Promise<Awaited<ReturnType<Expo['setNotificationChannelAsync']>>> {\n return (await this.native?.setNotificationChannelAsync(...channel)) ?? null;\n }\n\n async deleteChannel(identifier: string): Promise<void> {\n await this.native?.deleteNotificationChannelAsync(identifier);\n }\n\n async channelGroups(): Promise<Awaited<ReturnType<Expo['getNotificationChannelGroupsAsync']>>> {\n return (await this.native?.getNotificationChannelGroupsAsync()) ?? [];\n }\n\n async channelGroup(\n identifier: string,\n ): Promise<Awaited<ReturnType<Expo['getNotificationChannelGroupAsync']>>> {\n return (await this.native?.getNotificationChannelGroupAsync(identifier)) ?? null;\n }\n\n async setChannelGroup(\n ...group: Args<'setNotificationChannelGroupAsync'>\n ): Promise<Awaited<ReturnType<Expo['setNotificationChannelGroupAsync']>>> {\n return (await this.native?.setNotificationChannelGroupAsync(...group)) ?? null;\n }\n\n async deleteChannelGroup(identifier: string): Promise<void> {\n await this.native?.deleteNotificationChannelGroupAsync(identifier);\n }\n\n /** The categories set: each a set of action buttons a notification opts into. */\n async categories(): Promise<Awaited<ReturnType<Expo['getNotificationCategoriesAsync']>>> {\n return (await this.native?.getNotificationCategoriesAsync()) ?? [];\n }\n\n /**\n * Action buttons for notifications whose content names this category. A tap on one arrives as\n * a response with the button's identifier, and the typed text as `userText` for a text input.\n */\n async setCategory(\n ...category: Args<'setNotificationCategoryAsync'>\n ): Promise<Awaited<ReturnType<Expo['setNotificationCategoryAsync']>> | null> {\n return (await this.native?.setNotificationCategoryAsync(...category)) ?? null;\n }\n\n async deleteCategory(identifier: string): Promise<boolean> {\n return (await this.native?.deleteNotificationCategoryAsync(identifier)) ?? false;\n }\n\n /**\n * What a notification that arrives while the app is in front does: shown, silent, with a sound\n * or a badge. Without a handler, it is not shown. Null removes the handler.\n */\n setHandler(...handler: Args<'setNotificationHandler'>): void {\n this.native?.setNotificationHandler(...handler);\n }\n\n /**\n * The token to hand a backend so it can send a push through Expo's service.\n *\n * Asks for the notification permission first - a token nobody can be shown anything with is not\n * worth having. `projectId` defaults to the app's own EAS project; pass it explicitly for a bare\n * workflow app, one still without EAS Build configured, or a token fetched outside\n * `expo-notifications`' default resolution. Null without the permission or without the module.\n */\n async getExpoPushToken(projectId?: string): Promise<string | null> {\n if (!this.native || !(await this.permission.ensure())) return null;\n const token = await this.native.getExpoPushTokenAsync(projectId ? { projectId } : undefined);\n return token.data;\n }\n\n /**\n * The raw APNs (iOS) or FCM (Android) token, for a backend that talks to those services\n * directly rather than through Expo's push service. Same permission and module rules as\n * {@link getExpoPushToken}.\n */\n async getDevicePushToken(): Promise<DevicePushToken | null> {\n if (!this.native || !(await this.permission.ensure())) return null;\n return this.native.getDevicePushTokenAsync();\n }\n\n /**\n * The device push token, each time the platform rolls it while the app is running.\n *\n * A rolled token is not the one a backend has on file, and sending to the old one fails\n * silently - this is what tells an app to register the new one. Unset until a roll happens;\n * most of an app's life this stays null.\n */\n readonly devicePushToken: Signal<DevicePushToken | null> = this.rolled.asReadonly();\n\n /** Stops the device receiving pushes until a token is asked for again. */\n async unregister(): Promise<void> {\n await this.native?.unregisterForNotificationsAsync();\n }\n\n /** Whether Expo re-registers the device token with its push service by itself. */\n async setAutoServerRegistration(enabled: boolean): Promise<void> {\n await this.native?.setAutoServerRegistrationEnabledAsync(enabled);\n }\n\n /** Android: receive pushes sent to an FCM topic. */\n async subscribeToTopic(topic: string): Promise<void> {\n await this.native?.subscribeToTopicAsync(topic);\n }\n\n async unsubscribeFromTopic(topic: string): Promise<void> {\n await this.native?.unsubscribeFromTopicAsync(topic);\n }\n\n /**\n * Runs a task defined with `expo-task-manager` when a notification arrives in the background.\n * The task itself is defined at the top level of `main.ts`, since it runs without the app.\n */\n async registerTask(name: string): Promise<void> {\n await this.native?.registerTaskAsync(name);\n }\n\n async unregisterTask(name: string): Promise<void> {\n await this.native?.unregisterTaskAsync(name);\n }\n}\n"]}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The shape nearly every device-state module has, and the one part of it that is React-only.
|
|
3
|
+
*
|
|
4
|
+
* `getNetworkStateAsync` + `addNetworkStateListener` + `useNetworkState`. `getBatteryLevelAsync` +
|
|
5
|
+
* `addBatteryLevelListener` + `useBatteryLevel`. The two functions are plain; the hook is React
|
|
6
|
+
* state around them, and a hook is the only surface some of these offer for *reading over time*.
|
|
7
|
+
*
|
|
8
|
+
* So this is that hook, as a signal: a value that starts at a stated default, is fetched once,
|
|
9
|
+
* and follows the listener from then on.
|
|
10
|
+
*/
|
|
11
|
+
import { type Signal } from '@angular/core';
|
|
12
|
+
/** A value the platform knows now and will say more about later. */
|
|
13
|
+
export interface Observed<T> {
|
|
14
|
+
current(): Promise<T>;
|
|
15
|
+
subscribe(listener: (value: T) => void): () => void;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* A signal fed by an `Observed`, subscribed until the injector it was made in is destroyed.
|
|
19
|
+
*
|
|
20
|
+
* Called in an injection context, as `inject()` is - a field initialiser or a constructor - so
|
|
21
|
+
* that for a root service the subscription ends with the app. An app cares about the battery for
|
|
22
|
+
* as long as it is running, and not after: one that is mounted and unmounted would otherwise leave
|
|
23
|
+
* a listener behind each time.
|
|
24
|
+
*
|
|
25
|
+
* `initial` is what the value is before the platform has answered, which is always at least one
|
|
26
|
+
* turn: every one of these getters is asynchronous even when the answer is already in memory.
|
|
27
|
+
*/
|
|
28
|
+
export declare function observed<T>(source: Observed<T> | null, initial: T): Signal<T>;
|
|
29
|
+
/**
|
|
30
|
+
* An `Observed` from the pair of functions these modules always expose.
|
|
31
|
+
*
|
|
32
|
+
* Named for what it makes rather than `from`, which is what it was: the package index re-exports
|
|
33
|
+
* everything, and a bare `from` in an app's import list says nothing about what it is from.
|
|
34
|
+
*/
|
|
35
|
+
export declare function observedFrom<T>(current: () => Promise<T>, subscribe: (listener: (value: T) => void) => {
|
|
36
|
+
remove(): void;
|
|
37
|
+
}): Observed<T>;
|
package/dist/observed.js
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The shape nearly every device-state module has, and the one part of it that is React-only.
|
|
3
|
+
*
|
|
4
|
+
* `getNetworkStateAsync` + `addNetworkStateListener` + `useNetworkState`. `getBatteryLevelAsync` +
|
|
5
|
+
* `addBatteryLevelListener` + `useBatteryLevel`. The two functions are plain; the hook is React
|
|
6
|
+
* state around them, and a hook is the only surface some of these offer for *reading over time*.
|
|
7
|
+
*
|
|
8
|
+
* So this is that hook, as a signal: a value that starts at a stated default, is fetched once,
|
|
9
|
+
* and follows the listener from then on.
|
|
10
|
+
*/
|
|
11
|
+
import { DestroyRef, inject, signal } from '@angular/core';
|
|
12
|
+
/**
|
|
13
|
+
* A signal fed by an `Observed`, subscribed until the injector it was made in is destroyed.
|
|
14
|
+
*
|
|
15
|
+
* Called in an injection context, as `inject()` is - a field initialiser or a constructor - so
|
|
16
|
+
* that for a root service the subscription ends with the app. An app cares about the battery for
|
|
17
|
+
* as long as it is running, and not after: one that is mounted and unmounted would otherwise leave
|
|
18
|
+
* a listener behind each time.
|
|
19
|
+
*
|
|
20
|
+
* `initial` is what the value is before the platform has answered, which is always at least one
|
|
21
|
+
* turn: every one of these getters is asynchronous even when the answer is already in memory.
|
|
22
|
+
*/
|
|
23
|
+
export function observed(source, initial) {
|
|
24
|
+
const value = signal(initial, /* @ts-ignore */
|
|
25
|
+
...(ngDevMode ? [{ debugName: "value" }] : /* istanbul ignore next */ []));
|
|
26
|
+
if (!source)
|
|
27
|
+
return value.asReadonly();
|
|
28
|
+
// Failures are swallowed rather than surfaced: a device with no battery API is a device where
|
|
29
|
+
// the default is the honest answer, and nothing on screen should break over it. The first
|
|
30
|
+
// answer was asked for before anything was heard, so it is older than anything the listener
|
|
31
|
+
// has said by the time it arrives, and is dropped then.
|
|
32
|
+
let heard = false;
|
|
33
|
+
void source.current().then((first) => heard || value.set(first), () => { });
|
|
34
|
+
inject(DestroyRef).onDestroy(source.subscribe((next) => {
|
|
35
|
+
heard = true;
|
|
36
|
+
value.set(next);
|
|
37
|
+
}));
|
|
38
|
+
return value.asReadonly();
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* An `Observed` from the pair of functions these modules always expose.
|
|
42
|
+
*
|
|
43
|
+
* Named for what it makes rather than `from`, which is what it was: the package index re-exports
|
|
44
|
+
* everything, and a bare `from` in an app's import list says nothing about what it is from.
|
|
45
|
+
*/
|
|
46
|
+
export function observedFrom(current, subscribe) {
|
|
47
|
+
return {
|
|
48
|
+
current,
|
|
49
|
+
subscribe: (listener) => {
|
|
50
|
+
const subscription = subscribe(listener);
|
|
51
|
+
return () => subscription.remove();
|
|
52
|
+
},
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
//# sourceMappingURL=observed.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"observed.js","sourceRoot":"","sources":["../src/observed.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AACH,OAAO,EAAE,UAAU,EAAE,MAAM,EAAE,MAAM,EAAe,MAAM,eAAe,CAAC;AAQxE;;;;;;;;;;GAUG;AACH,MAAM,UAAU,QAAQ,CAAI,MAA0B,EAAE,OAAU;IAChE,MAAM,KAAK,GAAG,MAAM,CAAC,OAAO;8EAAC,CAAC;IAC9B,IAAI,CAAC,MAAM;QAAE,OAAO,KAAK,CAAC,UAAU,EAAE,CAAC;IAEvC,8FAA8F;IAC9F,0FAA0F;IAC1F,4FAA4F;IAC5F,wDAAwD;IACxD,IAAI,KAAK,GAAG,KAAK,CAAC;IAClB,KAAK,MAAM,CAAC,OAAO,EAAE,CAAC,IAAI,CACxB,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,IAAI,KAAK,CAAC,GAAG,CAAC,KAAK,CAAC,EACpC,GAAG,EAAE,GAAE,CAAC,CACT,CAAC;IACF,MAAM,CAAC,UAAU,CAAC,CAAC,SAAS,CAC1B,MAAM,CAAC,SAAS,CAAC,CAAC,IAAI,EAAE,EAAE;QACxB,KAAK,GAAG,IAAI,CAAC;QACb,KAAK,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;IAClB,CAAC,CAAC,CACH,CAAC;IACF,OAAO,KAAK,CAAC,UAAU,EAAE,CAAC;AAC5B,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,YAAY,CAC1B,OAAyB,EACzB,SAA+D;IAE/D,OAAO;QACL,OAAO;QACP,SAAS,EAAE,CAAC,QAAQ,EAAE,EAAE;YACtB,MAAM,YAAY,GAAG,SAAS,CAAC,QAAQ,CAAC,CAAC;YACzC,OAAO,GAAG,EAAE,CAAC,YAAY,CAAC,MAAM,EAAE,CAAC;QACrC,CAAC;KACF,CAAC;AACJ,CAAC","sourcesContent":["/**\n * The shape nearly every device-state module has, and the one part of it that is React-only.\n *\n * `getNetworkStateAsync` + `addNetworkStateListener` + `useNetworkState`. `getBatteryLevelAsync` +\n * `addBatteryLevelListener` + `useBatteryLevel`. The two functions are plain; the hook is React\n * state around them, and a hook is the only surface some of these offer for *reading over time*.\n *\n * So this is that hook, as a signal: a value that starts at a stated default, is fetched once,\n * and follows the listener from then on.\n */\nimport { DestroyRef, inject, signal, type Signal } from '@angular/core';\n\n/** A value the platform knows now and will say more about later. */\nexport interface Observed<T> {\n current(): Promise<T>;\n subscribe(listener: (value: T) => void): () => void;\n}\n\n/**\n * A signal fed by an `Observed`, subscribed until the injector it was made in is destroyed.\n *\n * Called in an injection context, as `inject()` is - a field initialiser or a constructor - so\n * that for a root service the subscription ends with the app. An app cares about the battery for\n * as long as it is running, and not after: one that is mounted and unmounted would otherwise leave\n * a listener behind each time.\n *\n * `initial` is what the value is before the platform has answered, which is always at least one\n * turn: every one of these getters is asynchronous even when the answer is already in memory.\n */\nexport function observed<T>(source: Observed<T> | null, initial: T): Signal<T> {\n const value = signal(initial);\n if (!source) return value.asReadonly();\n\n // Failures are swallowed rather than surfaced: a device with no battery API is a device where\n // the default is the honest answer, and nothing on screen should break over it. The first\n // answer was asked for before anything was heard, so it is older than anything the listener\n // has said by the time it arrives, and is dropped then.\n let heard = false;\n void source.current().then(\n (first) => heard || value.set(first),\n () => {},\n );\n inject(DestroyRef).onDestroy(\n source.subscribe((next) => {\n heard = true;\n value.set(next);\n }),\n );\n return value.asReadonly();\n}\n\n/**\n * An `Observed` from the pair of functions these modules always expose.\n *\n * Named for what it makes rather than `from`, which is what it was: the package index re-exports\n * everything, and a bare `from` in an app's import list says nothing about what it is from.\n */\nexport function observedFrom<T>(\n current: () => Promise<T>,\n subscribe: (listener: (value: T) => void) => { remove(): void },\n): Observed<T> {\n return {\n current,\n subscribe: (listener) => {\n const subscription = subscribe(listener);\n return () => subscription.remove();\n },\n };\n}\n"]}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `DeviceOrientation`, bound to `expo-screen-orientation`.
|
|
3
|
+
*
|
|
4
|
+
* Named for the device rather than the screen because that is what it reports: a layout asking
|
|
5
|
+
* which way *it* is laid out wants the `orientation` media feature, which needs none of this.
|
|
6
|
+
* This is what a camera preview or a video player needs - a screen locked to portrait still has a
|
|
7
|
+
* phone that is sideways.
|
|
8
|
+
*/
|
|
9
|
+
import { InjectionToken, type Signal } from '@angular/core';
|
|
10
|
+
import { type Observed } from './observed.ts';
|
|
11
|
+
import * as i0 from "@angular/core";
|
|
12
|
+
export type Orientation = 'unknown' | 'portrait' | 'portrait-upside-down' | 'landscape-left' | 'landscape-right';
|
|
13
|
+
/** What a screen may rotate to. `default` is portrait plus, on a phone, nothing else. */
|
|
14
|
+
export type OrientationLock = 'default' | 'all' | 'portrait' | 'landscape';
|
|
15
|
+
export interface OrientationSource {
|
|
16
|
+
readonly reported: Observed<Orientation> | null;
|
|
17
|
+
lock(lock: OrientationLock): Promise<void>;
|
|
18
|
+
unlock(): Promise<void>;
|
|
19
|
+
}
|
|
20
|
+
export declare class DeviceOrientation {
|
|
21
|
+
/** Overridden in a test to rotate a device that is not there. */
|
|
22
|
+
static readonly SOURCE: InjectionToken<OrientationSource>;
|
|
23
|
+
private readonly source;
|
|
24
|
+
readonly orientation: Signal<Orientation>;
|
|
25
|
+
readonly landscape: Signal<boolean>;
|
|
26
|
+
/**
|
|
27
|
+
* Pin the screen. Returns the function that unpins it, so a screen that wants landscape while
|
|
28
|
+
* it is up can hand it to `DestroyRef.onDestroy` and not think about it again.
|
|
29
|
+
*/
|
|
30
|
+
lock(lock: OrientationLock): () => void;
|
|
31
|
+
/** Every lock not yet released, the one in force last. */
|
|
32
|
+
private readonly locks;
|
|
33
|
+
static ɵfac: i0.ɵɵFactoryDeclaration<DeviceOrientation, never>;
|
|
34
|
+
static ɵprov: i0.ɵɵInjectableDeclaration<any>;
|
|
35
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `DeviceOrientation`, bound to `expo-screen-orientation`.
|
|
3
|
+
*
|
|
4
|
+
* Named for the device rather than the screen because that is what it reports: a layout asking
|
|
5
|
+
* which way *it* is laid out wants the `orientation` media feature, which needs none of this.
|
|
6
|
+
* This is what a camera preview or a video player needs - a screen locked to portrait still has a
|
|
7
|
+
* phone that is sideways.
|
|
8
|
+
*/
|
|
9
|
+
import { InjectionToken, Service, computed, inject } from '@angular/core';
|
|
10
|
+
import { observed, observedFrom } from "./observed.js";
|
|
11
|
+
import { expoModule } from "./native.js";
|
|
12
|
+
import * as i0 from "@angular/core";
|
|
13
|
+
const NOTHING = {
|
|
14
|
+
reported: null,
|
|
15
|
+
lock: () => Promise.resolve(),
|
|
16
|
+
unlock: () => Promise.resolve(),
|
|
17
|
+
};
|
|
18
|
+
export class DeviceOrientation {
|
|
19
|
+
/** Overridden in a test to rotate a device that is not there. */
|
|
20
|
+
static SOURCE = new InjectionToken('angular-native.orientationSource', {
|
|
21
|
+
factory: () => {
|
|
22
|
+
const expo = expoModule('expo-screen-orientation', () => require('expo-screen-orientation'));
|
|
23
|
+
if (!expo)
|
|
24
|
+
return NOTHING;
|
|
25
|
+
const ORIENTATIONS = {
|
|
26
|
+
[expo.Orientation.UNKNOWN]: 'unknown',
|
|
27
|
+
[expo.Orientation.PORTRAIT_UP]: 'portrait',
|
|
28
|
+
[expo.Orientation.PORTRAIT_DOWN]: 'portrait-upside-down',
|
|
29
|
+
[expo.Orientation.LANDSCAPE_LEFT]: 'landscape-left',
|
|
30
|
+
[expo.Orientation.LANDSCAPE_RIGHT]: 'landscape-right',
|
|
31
|
+
};
|
|
32
|
+
const LOCKS = {
|
|
33
|
+
default: expo.OrientationLock.DEFAULT,
|
|
34
|
+
all: expo.OrientationLock.ALL,
|
|
35
|
+
portrait: expo.OrientationLock.PORTRAIT,
|
|
36
|
+
landscape: expo.OrientationLock.LANDSCAPE,
|
|
37
|
+
};
|
|
38
|
+
return {
|
|
39
|
+
reported: observedFrom(async () => ORIENTATIONS[await expo.getOrientationAsync()] ?? 'unknown', (listener) => expo.addOrientationChangeListener(({ orientationInfo }) => listener(ORIENTATIONS[orientationInfo.orientation] ?? 'unknown'))),
|
|
40
|
+
lock: (lock) => expo.lockAsync(LOCKS[lock]),
|
|
41
|
+
unlock: () => expo.unlockAsync(),
|
|
42
|
+
};
|
|
43
|
+
},
|
|
44
|
+
});
|
|
45
|
+
source = inject(DeviceOrientation.SOURCE);
|
|
46
|
+
orientation = observed(this.source.reported, 'unknown');
|
|
47
|
+
landscape = computed(() => this.orientation().startsWith('landscape'), /* @ts-ignore */
|
|
48
|
+
...(ngDevMode ? [{ debugName: "landscape" }] : /* istanbul ignore next */ []));
|
|
49
|
+
/**
|
|
50
|
+
* Pin the screen. Returns the function that unpins it, so a screen that wants landscape while
|
|
51
|
+
* it is up can hand it to `DestroyRef.onDestroy` and not think about it again.
|
|
52
|
+
*/
|
|
53
|
+
lock(lock) {
|
|
54
|
+
// A stack, as the status bar's is: a screen pushed over one with a lock of its own releases
|
|
55
|
+
// only its own, and the lock beneath it comes back rather than the whole app unlocking.
|
|
56
|
+
const entry = { lock };
|
|
57
|
+
this.locks.push(entry);
|
|
58
|
+
void this.source.lock(lock);
|
|
59
|
+
return () => {
|
|
60
|
+
const at = this.locks.indexOf(entry);
|
|
61
|
+
if (at === -1)
|
|
62
|
+
return;
|
|
63
|
+
this.locks.splice(at, 1);
|
|
64
|
+
if (at < this.locks.length)
|
|
65
|
+
return;
|
|
66
|
+
const below = this.locks.at(-1);
|
|
67
|
+
void (below ? this.source.lock(below.lock) : this.source.unlock());
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
/** Every lock not yet released, the one in force last. */
|
|
71
|
+
locks = [];
|
|
72
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.2.0", ngImport: i0, type: DeviceOrientation, deps: [], target: i0.ɵɵFactoryTarget.Service });
|
|
73
|
+
static ɵprov = i0.ɵɵngDeclareService({ minVersion: "22.0.0", version: "22.2.0", ngImport: i0, type: DeviceOrientation });
|
|
74
|
+
}
|
|
75
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.2.0", ngImport: i0, type: DeviceOrientation, decorators: [{
|
|
76
|
+
type: Service
|
|
77
|
+
}] });
|
|
78
|
+
//# sourceMappingURL=orientation.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"orientation.js","sourceRoot":"","sources":["../src/orientation.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * `DeviceOrientation`, bound to `expo-screen-orientation`.\n *\n * Named for the device rather than the screen because that is what it reports: a layout asking\n * which way *it* is laid out wants the `orientation` media feature, which needs none of this.\n * This is what a camera preview or a video player needs - a screen locked to portrait still has a\n * phone that is sideways.\n */\nimport { InjectionToken, Service, computed, inject, type Signal } from '@angular/core';\nimport { observed, observedFrom, type Observed } from './observed.ts';\nimport { expoModule } from './native.ts';\n\nexport type Orientation =\n 'unknown' | 'portrait' | 'portrait-upside-down' | 'landscape-left' | 'landscape-right';\n\n/** What a screen may rotate to. `default` is portrait plus, on a phone, nothing else. */\nexport type OrientationLock = 'default' | 'all' | 'portrait' | 'landscape';\n\nexport interface OrientationSource {\n readonly reported: Observed<Orientation> | null;\n lock(lock: OrientationLock): Promise<void>;\n unlock(): Promise<void>;\n}\n\nconst NOTHING: OrientationSource = {\n reported: null,\n lock: () => Promise.resolve(),\n unlock: () => Promise.resolve(),\n};\n\n@Service()\nexport class DeviceOrientation {\n /** Overridden in a test to rotate a device that is not there. */\n static readonly SOURCE = new InjectionToken<OrientationSource>(\n 'angular-native.orientationSource',\n {\n factory: () => {\n const expo = expoModule(\n 'expo-screen-orientation',\n () => require('expo-screen-orientation') as typeof import('expo-screen-orientation'),\n );\n if (!expo) return NOTHING;\n\n const ORIENTATIONS: Record<number, Orientation> = {\n [expo.Orientation.UNKNOWN]: 'unknown',\n [expo.Orientation.PORTRAIT_UP]: 'portrait',\n [expo.Orientation.PORTRAIT_DOWN]: 'portrait-upside-down',\n [expo.Orientation.LANDSCAPE_LEFT]: 'landscape-left',\n [expo.Orientation.LANDSCAPE_RIGHT]: 'landscape-right',\n };\n const LOCKS: Record<OrientationLock, import('expo-screen-orientation').OrientationLock> = {\n default: expo.OrientationLock.DEFAULT,\n all: expo.OrientationLock.ALL,\n portrait: expo.OrientationLock.PORTRAIT,\n landscape: expo.OrientationLock.LANDSCAPE,\n };\n\n return {\n reported: observedFrom(\n async () => ORIENTATIONS[await expo.getOrientationAsync()] ?? 'unknown',\n (listener) =>\n expo.addOrientationChangeListener(({ orientationInfo }) =>\n listener(ORIENTATIONS[orientationInfo.orientation] ?? 'unknown'),\n ),\n ),\n lock: (lock) => expo.lockAsync(LOCKS[lock]),\n unlock: () => expo.unlockAsync(),\n };\n },\n },\n );\n\n private readonly source = inject(DeviceOrientation.SOURCE);\n\n readonly orientation: Signal<Orientation> = observed(this.source.reported, 'unknown');\n\n readonly landscape: Signal<boolean> = computed(() => this.orientation().startsWith('landscape'));\n\n /**\n * Pin the screen. Returns the function that unpins it, so a screen that wants landscape while\n * it is up can hand it to `DestroyRef.onDestroy` and not think about it again.\n */\n lock(lock: OrientationLock): () => void {\n // A stack, as the status bar's is: a screen pushed over one with a lock of its own releases\n // only its own, and the lock beneath it comes back rather than the whole app unlocking.\n const entry = { lock };\n this.locks.push(entry);\n void this.source.lock(lock);\n return () => {\n const at = this.locks.indexOf(entry);\n if (at === -1) return;\n this.locks.splice(at, 1);\n if (at < this.locks.length) return;\n const below = this.locks.at(-1);\n void (below ? this.source.lock(below.lock) : this.source.unlock());\n };\n }\n\n /** Every lock not yet released, the one in force last. */\n private readonly locks: { readonly lock: OrientationLock }[] = [];\n}\n"]}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A permission, as signals rather than a hook.
|
|
3
|
+
*
|
|
4
|
+
* Every Expo module that needs one exposes the same shape: `getXPermissionsAsync`,
|
|
5
|
+
* `requestXPermissionsAsync`, and a `useXPermissions()` hook that is those two plus React state.
|
|
6
|
+
* So this is not a facade per module - it is one class that takes the pair, and the module stays
|
|
7
|
+
* the app's own dependency:
|
|
8
|
+
*
|
|
9
|
+
* ```ts
|
|
10
|
+
* import * as Camera from 'expo-camera';
|
|
11
|
+
* import { Permission } from '@ng-native/expo';
|
|
12
|
+
*
|
|
13
|
+
* readonly camera = Permission.of(Camera.Camera.getCameraPermissionsAsync,
|
|
14
|
+
* Camera.Camera.requestCameraPermissionsAsync);
|
|
15
|
+
* ```
|
|
16
|
+
*/
|
|
17
|
+
import { type Signal } from '@angular/core';
|
|
18
|
+
/** What every Expo permission call answers with. */
|
|
19
|
+
export interface PermissionResponse {
|
|
20
|
+
readonly status: 'granted' | 'denied' | 'undetermined';
|
|
21
|
+
readonly granted: boolean;
|
|
22
|
+
/** False once the platform will no longer show a dialog: the app must send them to Settings. */
|
|
23
|
+
readonly canAskAgain: boolean;
|
|
24
|
+
}
|
|
25
|
+
/** The pair of functions a module exposes, which its hook is built out of. */
|
|
26
|
+
export interface PermissionApi {
|
|
27
|
+
get(): Promise<PermissionResponse>;
|
|
28
|
+
request(): Promise<PermissionResponse>;
|
|
29
|
+
}
|
|
30
|
+
export declare class Permission {
|
|
31
|
+
private readonly api;
|
|
32
|
+
private readonly last;
|
|
33
|
+
constructor(api: PermissionApi);
|
|
34
|
+
/** `unknown` until something has asked the platform, which is not the same as `undetermined`. */
|
|
35
|
+
readonly status: Signal<PermissionResponse['status'] | 'unknown'>;
|
|
36
|
+
readonly granted: Signal<boolean>;
|
|
37
|
+
/** Refused, and the platform will not show a dialog again. Send them to Settings. */
|
|
38
|
+
readonly blocked: Signal<boolean>;
|
|
39
|
+
/** From a module's own two functions, which is how every one of them spells this. */
|
|
40
|
+
static of(get: () => Promise<PermissionResponse>, request: () => Promise<PermissionResponse>): Permission;
|
|
41
|
+
/** Ask the platform what it currently thinks, without showing anything to the user. */
|
|
42
|
+
check(): Promise<boolean>;
|
|
43
|
+
/** Show the dialog. Resolves to what the user said. */
|
|
44
|
+
request(): Promise<boolean>;
|
|
45
|
+
/**
|
|
46
|
+
* Have the permission if it can be had, asking only if asking would do something.
|
|
47
|
+
*
|
|
48
|
+
* The method most call sites want. A permission already granted needs no dialog, and one the
|
|
49
|
+
* platform has stopped asking about would return the same no while looking like the user had
|
|
50
|
+
* been consulted.
|
|
51
|
+
*/
|
|
52
|
+
ensure(): Promise<boolean>;
|
|
53
|
+
private record;
|
|
54
|
+
}
|