@ng-native/expo 0.2.0 → 0.3.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/expo-ui-components.d.ts +142 -10
- package/dist/expo-ui-components.js +362 -39
- package/dist/expo-ui-components.js.map +1 -1
- package/dist/expo-ui.js +9 -6
- package/dist/expo-ui.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 -0
- package/dist/fonts.js +7 -2
- package/dist/fonts.js.map +1 -1
- package/dist/image-picker.js +3 -9
- package/dist/image-picker.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/live-activity.d.ts +88 -0
- package/dist/live-activity.js +150 -0
- package/dist/live-activity.js.map +1 -0
- package/dist/location.js +2 -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 +4 -1
- package/dist/native.js +60 -4
- 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 +5 -0
- package/dist/register-expo-view.js +5 -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/package.json +49 -4
package/README.md
CHANGED
|
@@ -14,32 +14,18 @@ npm install @angular/core expo
|
|
|
14
14
|
Every Expo module (`expo-haptics`, `expo-clipboard`, `expo-file-system`, and the rest) is an
|
|
15
15
|
optional peer dependency: install only the ones the app actually uses.
|
|
16
16
|
|
|
17
|
-
##
|
|
18
|
-
|
|
19
|
-
One entry point per module, so importing haptics does not bundle the file system. Every source
|
|
20
|
-
file lives in `src/`, as in every other package here; the entry points an app writes do not say so,
|
|
21
|
-
because the `exports` map takes `@ng-native/expo/haptics` to `src/haptics.ts`. Most are
|
|
22
|
-
`@Service()` classes that reach their native module through a static `SOURCE` token; `Fonts`,
|
|
23
|
-
`SplashScreen`, `Storage`, `SecureStorage` and the sensors are `InjectionToken`s carrying their own
|
|
24
|
-
factory. Either way there is nothing to provide and nothing to register - injecting it is the whole
|
|
25
|
-
setup, and a service nobody injects is never constructed. Each token name is a type as well, so
|
|
26
|
-
`inject(Storage)` and `private store: Storage` both work, as `inject(Clipboard)` does for a class.
|
|
17
|
+
## Example
|
|
27
18
|
|
|
28
19
|
```ts
|
|
29
20
|
import { Clipboard } from '@ng-native/expo/clipboard';
|
|
30
21
|
import { FileSystem } from '@ng-native/expo/file-system';
|
|
31
|
-
import { Fonts } from '@ng-native/expo/fonts';
|
|
32
22
|
import { Haptics } from '@ng-native/expo/haptics';
|
|
33
|
-
import { SplashScreen } from '@ng-native/expo/splash-screen';
|
|
34
23
|
|
|
35
24
|
export class Notes {
|
|
36
25
|
private readonly clipboard = inject(Clipboard);
|
|
37
26
|
private readonly files = inject(FileSystem);
|
|
38
27
|
private readonly haptics = inject(Haptics);
|
|
39
28
|
|
|
40
|
-
/** A signal: how many times the pasteboard has changed. */
|
|
41
|
-
protected readonly changes = this.clipboard.changes;
|
|
42
|
-
|
|
43
29
|
protected async save(text: string): Promise<void> {
|
|
44
30
|
this.files.write(this.files.document('notes.txt'), text);
|
|
45
31
|
await this.clipboard.write(text);
|
|
@@ -48,370 +34,34 @@ export class Notes {
|
|
|
48
34
|
}
|
|
49
35
|
```
|
|
50
36
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
-
|
|
59
|
-
|
|
60
|
-
- `Haptics`
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
not - never installed, or installed without the app being rebuilt since - throws a
|
|
83
|
-
`MissingModuleError` naming it and the commands that fix it.
|
|
84
|
-
|
|
85
|
-
That token is also the test seam, and it hangs off the service rather than sitting beside it -
|
|
86
|
-
`Clipboard.SOURCE`, not `CLIPBOARD_SOURCE`. One export per capability, and a token that cannot
|
|
87
|
-
drift away from the thing it belongs to. A fake is _provided_ rather than passed, so a test drives
|
|
88
|
-
the real service:
|
|
89
|
-
|
|
90
|
-
```ts
|
|
91
|
-
const clipboard = serviceWith(Clipboard.SOURCE, fake, () => new Clipboard());
|
|
92
|
-
```
|
|
93
|
-
|
|
94
|
-
Four are not services and say so: `Sensor` and `Store` are one class behind several tokens
|
|
95
|
-
(`Accelerometer` and `Barometer` are the same class; so are `Storage` and `SecureStorage`), and
|
|
96
|
-
`Fonts` and `SplashScreen` are called before an injector exists - `splashScreen.hold()` is the
|
|
97
|
-
first thing an entry file does. Those four take their platform as a constructor argument.
|
|
98
|
-
|
|
99
|
-
The app still installs `expo-haptics`, `expo-clipboard` and `expo-file-system` itself. That is
|
|
100
|
-
what links the native side; whether we import their JavaScript is a separate question, and
|
|
101
|
-
`Haptics` does not.
|
|
102
|
-
|
|
103
|
-
## Fonts
|
|
104
|
-
|
|
105
|
-
A font on a phone is not fetched when the text engine first needs it: it has to be registered
|
|
106
|
-
with the platform before anything is laid out, or the first paint is in the fallback face and
|
|
107
|
-
reflows when the real one lands. So a face is declared in CSS, collected at build time, and
|
|
108
|
-
registered before the app mounts:
|
|
109
|
-
|
|
110
|
-
```css
|
|
111
|
-
@font-face {
|
|
112
|
-
font-family: Inter;
|
|
113
|
-
src: url('./fonts/Inter-Regular.ttf');
|
|
114
|
-
}
|
|
115
|
-
@font-face {
|
|
116
|
-
font-family: Inter;
|
|
117
|
-
src: url('./fonts/Inter-Bold.ttf');
|
|
118
|
-
font-weight: 700;
|
|
119
|
-
}
|
|
120
|
-
```
|
|
121
|
-
|
|
122
|
-
```ts
|
|
123
|
-
import { loadFonts } from '@ng-native/expo/fonts';
|
|
124
|
-
|
|
125
|
-
await loadFonts(styleSheetOf(GlobalStyles));
|
|
126
|
-
mount(rootTag, App, getFabricUIManager(), { globalStyles: styleSheetOf(GlobalStyles) });
|
|
127
|
-
```
|
|
128
|
-
|
|
129
|
-
`font-family: Inter` then means what it says. The `url()` becomes a `require` at build time, so
|
|
130
|
-
the bundler ships the file - a plain path would be a font that is simply missing on the device
|
|
131
|
-
with nothing anywhere to say why.
|
|
132
|
-
|
|
133
|
-
**Weights and styles match as on the web.** Native looks a family up by name and that is all, so
|
|
134
|
-
the second face above is registered as `Inter-700` as well, and the engine points a text with
|
|
135
|
-
`font-family: Inter; font-weight: 700` at it, by CSS's matching rules, wherever the family and
|
|
136
|
-
weight were set.
|
|
137
|
-
|
|
138
|
-
`loadFonts` takes any number of sheets and does nothing at all when none of them declare a face,
|
|
139
|
-
so bootstrap can call it unconditionally. Injecting `Fonts` afterwards answers what is loaded.
|
|
140
|
-
|
|
141
|
-
## Device state
|
|
142
|
-
|
|
143
|
-
Every module that reports something changing has the same three parts: a getter, a listener, and
|
|
144
|
-
a `useX()` hook that is those two plus React state. The hook is the only surface some of them
|
|
145
|
-
offer for reading a value _over time_, so it is what is rebuilt here - once, as a signal - and
|
|
146
|
-
each module is a thin binding onto it.
|
|
147
|
-
|
|
148
|
-
```ts
|
|
149
|
-
private readonly network = inject(Network);
|
|
150
|
-
private readonly battery = inject(Battery);
|
|
151
|
-
|
|
152
|
-
protected readonly offline = computed(() => this.network.reachable() === false);
|
|
153
|
-
protected readonly thrifty = computed(() => this.battery.saving() || this.battery.low());
|
|
154
|
-
```
|
|
155
|
-
|
|
156
|
-
- `Network.reachable()` is `null` until the platform establishes it, which is **not** the same as
|
|
157
|
-
offline: a captive-portal wifi is connected and reachable by nothing, and an offline banner that
|
|
158
|
-
treats `null` as false flashes on every cold start.
|
|
159
|
-
- `Battery.low()` is under a fifth _and not charging_; plugged in at 15% is not a reason to do
|
|
160
|
-
less. Both signals start at their optimistic value, so an app never opens in its degraded mode.
|
|
161
|
-
- `DeviceOrientation` is the device, not the window. A layout asking which way _it_ is laid out
|
|
162
|
-
wants the `orientation` media feature and needs none of this; a camera preview needs this,
|
|
163
|
-
because a screen pinned to portrait still has a phone that is sideways.
|
|
164
|
-
- `Brightness.set()` and `DeviceOrientation.lock()` both return the function that undoes them,
|
|
165
|
-
ready for `DestroyRef.onDestroy`. An app that leaves the screen at full brightness after showing
|
|
166
|
-
a boarding pass is one the user experiences as a battery fault.
|
|
167
|
-
|
|
168
|
-
### Sensors and locale
|
|
169
|
-
|
|
170
|
-
The sensors are one class parameterized by the reading, not one per sensor - they are all the same
|
|
171
|
-
object underneath. Nothing subscribes until `start`, and the interval is explicit:
|
|
172
|
-
|
|
173
|
-
```ts
|
|
174
|
-
inject(DestroyRef).onDestroy(inject(Accelerometer).start(50));
|
|
175
|
-
```
|
|
176
|
-
|
|
177
|
-
The sensible interval is a property of what the app is doing with it, a compass wants a tenth of a
|
|
178
|
-
second and a shake detector less, and every event is a change-detection pass - so a sensor left at
|
|
179
|
-
its default is a phone that never idles, and a sensor nobody stops is a battery nobody gets back.
|
|
180
|
-
|
|
181
|
-
`Locale` is the odd one: both getters are synchronous, so there is no default to sit at. It is a
|
|
182
|
-
service because the answers _change_ - a user can switch language in Settings and come back, and a
|
|
183
|
-
date that was formatted correctly is then wrong. The list is ordered by preference and always has
|
|
184
|
-
at least one entry, which is the reason to prefer it over a single locale string: an app that
|
|
185
|
-
supports the user's second language should use it rather than fall back to English. `rtl()` is a
|
|
186
|
-
layout decision rather than a translation one, which is why it is a signal.
|
|
187
|
-
|
|
188
|
-
## Storage
|
|
189
|
-
|
|
190
|
-
Both stores are already plain promises, so what is worth having is not a wrapper but a binding: a
|
|
191
|
-
persisted value as a signal that writes through when set.
|
|
192
|
-
|
|
193
|
-
```ts
|
|
194
|
-
private readonly store = inject(Storage);
|
|
195
|
-
protected readonly theme = this.store.signal<'light' | 'dark'>('theme', 'light');
|
|
196
|
-
```
|
|
197
|
-
|
|
198
|
-
Reading it is reading the preference; `theme.set('dark')` persists. The same key always hands back
|
|
199
|
-
the same signal, so two components binding one preference stay in step, and a value written before
|
|
200
|
-
the read comes back is not undone by it.
|
|
201
|
-
|
|
202
|
-
`SecureStorage` is the keychain and the Android keystore - separate from `Storage` because the
|
|
203
|
-
difference should be visible at the injection site. It can also answer synchronously, so a token
|
|
204
|
-
never flashes its default.
|
|
205
|
-
|
|
206
|
-
## Permissions
|
|
207
|
-
|
|
208
|
-
Every Expo module that needs one spells it the same way - `getXPermissionsAsync`,
|
|
209
|
-
`requestXPermissionsAsync`, and a `useXPermissions()` hook that is those two plus React state. So
|
|
210
|
-
there is one class here rather than one facade per module, and the module stays the app's own
|
|
211
|
-
dependency:
|
|
212
|
-
|
|
213
|
-
```ts
|
|
214
|
-
import { requestCameraPermissionsAsync, getCameraPermissionsAsync } from 'expo-camera';
|
|
215
|
-
import { Permission } from '@ng-native/expo';
|
|
216
|
-
|
|
217
|
-
readonly camera = Permission.of(getCameraPermissionsAsync, requestCameraPermissionsAsync);
|
|
218
|
-
|
|
219
|
-
protected async open(): Promise<void> {
|
|
220
|
-
if (await this.camera.ensure()) this.scanning.set(true);
|
|
221
|
-
}
|
|
222
|
-
```
|
|
223
|
-
|
|
224
|
-
`ensure()` is the method most call sites want: it checks first, so a permission the user granted
|
|
225
|
-
months ago shows no dialog, and it does not ask when the platform has stopped asking - which
|
|
226
|
-
would resolve to the same no while reading as though the user had been consulted. `blocked()` is
|
|
227
|
-
how an app knows to offer Settings instead.
|
|
228
|
-
|
|
229
|
-
`status()` distinguishes `unknown` from `undetermined`: nobody has asked the platform yet, versus
|
|
230
|
-
the platform saying the user has not decided.
|
|
231
|
-
|
|
232
|
-
## Splash screen
|
|
233
|
-
|
|
234
|
-
The ordering is the whole feature, and it is easy to get subtly wrong. The native splash hides
|
|
235
|
-
itself as soon as the first frame is drawn, so an app that loads fonts _before_ mounting shows a
|
|
236
|
-
blank window for as long as that takes:
|
|
237
|
-
|
|
238
|
-
```ts
|
|
239
|
-
import { splashScreen } from '@ng-native/expo/splash-screen';
|
|
240
|
-
import { loadFonts } from '@ng-native/expo/fonts';
|
|
241
|
-
|
|
242
|
-
splashScreen.hold(); // module scope, before anything renders
|
|
243
|
-
|
|
244
|
-
AppRegistry.registerRunnable('main', ({ rootTag }) => {
|
|
245
|
-
const fonts = loadFonts(styleSheetOf(GlobalStyles));
|
|
246
|
-
mount(Number(rootTag), App, getFabricUIManager(), { globalStyles: styleSheetOf(GlobalStyles) });
|
|
247
|
-
void splashScreen.hideWhenReady(fonts);
|
|
248
|
-
});
|
|
249
|
-
```
|
|
250
|
-
|
|
251
|
-
Mounting does not wait for the fonts; the splash screen does. Text laid out before they arrive is
|
|
252
|
-
laid out again as each face registers, behind the splash, so nothing is ever seen in the fallback
|
|
253
|
-
face.
|
|
254
|
-
`hideWhenReady` waits one frame after the work, because hiding the instant it resolves uncovers
|
|
255
|
-
the frame that was on screen while it ran - and it uncovers even when the work fails, rather than
|
|
256
|
-
leaving a splash screen up forever.
|
|
257
|
-
|
|
258
|
-
## Media
|
|
259
|
-
|
|
260
|
-
`useVideoPlayer` and `useAudioPlayer` are not where the player comes from - `createVideoPlayer`
|
|
261
|
-
gives the same object. What the hooks add is the two things a component needs and cannot get
|
|
262
|
-
otherwise: the player is _released_ when the component goes, and its changing state is _readable_,
|
|
263
|
-
which its own mutable properties are not.
|
|
264
|
-
|
|
265
|
-
```ts
|
|
266
|
-
protected readonly player = videoPlayer(source, { timeUpdate: 0.5 });
|
|
267
|
-
```
|
|
268
|
-
|
|
269
|
-
```html
|
|
270
|
-
<expo-video [player]="player.native" /> <text>{{ player.state().currentTime }}</text>
|
|
271
|
-
```
|
|
272
|
-
|
|
273
|
-
The player itself is passed straight through: it already has `play()`, `seekBy()` and the rest,
|
|
274
|
-
and wrapping them would be a second place for each to be wrong. `timeUpdate` is off unless asked
|
|
275
|
-
for, because it is the most frequent event either player emits and a screen with no progress bar
|
|
276
|
-
should not pay for one.
|
|
277
|
-
|
|
278
|
-
## Notifications
|
|
279
|
-
|
|
280
|
-
Only the part React was holding. Scheduling, channels, categories, badges and tokens are already
|
|
281
|
-
plain promises, and an app calls those on the module directly.
|
|
282
|
-
|
|
283
|
-
What is here is the difference between a notification _arriving_ and a user _tapping_ one - the
|
|
284
|
-
second is a navigation instruction - and the response that **launched** the app, which happened
|
|
285
|
-
before any listener existed. Missing that is the single most common way notification routing is
|
|
286
|
-
got wrong.
|
|
287
|
-
|
|
288
|
-
```ts
|
|
289
|
-
const tapped = inject(Notifications).take();
|
|
290
|
-
if (tapped) this.router.navigateByUrl(routeFor(tapped));
|
|
291
|
-
```
|
|
292
|
-
|
|
293
|
-
`take()` rather than the signal, because routing on a tap is a one-off and a signal is not: an
|
|
294
|
-
effect on one fires again whenever its component is recreated, sending the user back to a screen
|
|
295
|
-
they had navigated away from. A second tap on the same notification is a new instruction and is
|
|
296
|
-
handed over again.
|
|
297
|
-
|
|
298
|
-
Pair it with `Permission.of(getPermissionsAsync, requestPermissionsAsync)` from the module.
|
|
299
|
-
|
|
300
|
-
## Photos, files, location and privacy
|
|
301
|
-
|
|
302
|
-
- **`ImagePicker`** (`image-picker.ts`): `pick()` and `capture()`, each resolving to the picked
|
|
303
|
-
assets, empty if they canceled. `capture()` asks for the camera itself.
|
|
304
|
-
- **`Location`** (`location.ts`): a `position` signal, filled by `current(accuracy)` once or by
|
|
305
|
-
`start({ accuracy, distance })` until the function it returns is called.
|
|
306
|
-
- **`Biometrics`** (`biometrics.ts`): `available()`, `kinds()` and `authenticate(message)`, which
|
|
307
|
-
fails rather than passes when the module is missing.
|
|
308
|
-
- **`Browser`** (`browser.ts`): `open(url)`, and `signIn(url, redirectUrl)`, which resolves to the
|
|
309
|
-
URL the sign-in page redirected back to, or null.
|
|
310
|
-
- **`AppInfo`** (`app-info.ts`): the version, build, identifier and device, as plain values.
|
|
311
|
-
- **`Camera`** (`camera.ts`): a directive on `<expo-camera>` whose `takePicture()` takes a picture
|
|
312
|
-
from the view on screen, without a React ref.
|
|
313
|
-
- **`StoreReview`** (`store-review.ts`): `available()`, `hasAction()`, `request()` and
|
|
314
|
-
`storeUrl()`. Whether the prompt appears is the platform's decision, and a call that resolves is
|
|
315
|
-
not a prompt that was shown.
|
|
316
|
-
- **`Tracking`** (`tracking.ts`): App Tracking Transparency's `permission`, `available()` and
|
|
317
|
-
`advertisingId()`, which is null until tracking is allowed.
|
|
318
|
-
- **`DocumentPicker`** (`document-picker.ts`): `pick(options)`, resolving to the files picked,
|
|
319
|
-
empty if they canceled.
|
|
320
|
-
- **`Crypto`** (`crypto.ts`): `randomUUID()`, `digestString()`, `digest()`, `randomBytes()` and
|
|
321
|
-
`randomValues()`. The one service that throws without its module, since an empty identifier or
|
|
322
|
-
hash is a wrong answer that looks right.
|
|
323
|
-
- **`BackgroundTask`** (`background-task.ts`): `status()`, `register(name, options)`,
|
|
324
|
-
`unregister(name)` and `triggerForTesting()`. The task itself is defined with
|
|
325
|
-
`expo-task-manager` at the top level of `main.ts`, since the platform runs it without
|
|
326
|
-
bootstrapping Angular.
|
|
327
|
-
- **`ScreenCapture`** (`screen-capture.ts`): `prevent(key)` and `allow(key)`, which stay
|
|
328
|
-
prevented while any key is held, and a `screenshots` signal counting the screenshots taken.
|
|
329
|
-
- **`ImageEditor`** (`image-editor.ts`): `edit(uri, actions, options)` over
|
|
330
|
-
`expo-image-manipulator`, resolving to the saved file and releasing the native context and
|
|
331
|
-
image it made, and `manipulate(uri)` for the module's own context.
|
|
332
|
-
- **`MediaLibrary`** (`media-library.ts`): `save(uri, album)`, which asks for write-only access
|
|
333
|
-
itself, `assets(query)` and `metadata(query)` over the module's own `Query`, albums, the read
|
|
334
|
-
and write permissions, and `watch()` for changes, which starts listening only when called.
|
|
335
|
-
|
|
336
|
-
## The rest
|
|
337
|
-
|
|
338
|
-
Four more modules whose only entry point was a hook, each the same story: the work underneath is a
|
|
339
|
-
plain promise, and what the hook adds is a _lifecycle_.
|
|
340
|
-
|
|
341
|
-
- **`database(name, migrations)`** replaces `SQLiteProvider` and `useSQLiteContext`. Opened on
|
|
342
|
-
first use rather than at startup - a database opened eagerly is a file handle and a journal for
|
|
343
|
-
an app that may never read from it - and opened _once_ however many callers arrive while it is
|
|
344
|
-
opening, because two connections to one file is a lock waiting to happen. Migrations run in
|
|
345
|
-
order, each in a transaction, with the version recorded in SQLite's own `user_version`.
|
|
346
|
-
- **`Updates`** checks and downloads in one call, because an app that checks without downloading
|
|
347
|
-
has learned something it cannot act on. `apply()` **restarts the app**, which is why nothing here
|
|
348
|
-
does it automatically: the moment is one the app knows and this does not. In Expo Go it reports
|
|
349
|
-
itself disabled rather than offering a banner that can never resolve.
|
|
350
|
-
- **`assets(modules)`** preloads the images a screen should not pop in with, and resolves even when
|
|
351
|
-
one fails - a picture that will not appear is not a reason for the screen behind it to fail too.
|
|
352
|
-
- **`KeepAwake.hold()`** returns the release, and is never a bare activate: a screen that holds the
|
|
353
|
-
display on and never lets go is a phone that never sleeps, which the user experiences as a
|
|
354
|
-
battery fault and never attributes to the app that caused it.
|
|
355
|
-
|
|
356
|
-
## Views
|
|
357
|
-
|
|
358
|
-
Most Expo modules need nothing from this package. `requireNativeModule`, `EventEmitter`,
|
|
359
|
-
`SharedObject` and `SharedRef` have no React in them, so a module that only calls into native is
|
|
360
|
-
imported and used as its own documentation says - the services above are a nicer surface, not a
|
|
361
|
-
shim. Autolinking is unaffected too: it is driven by the `expo` package and the
|
|
362
|
-
`expo-module.config.json` in each module, neither of which knows what renderer is above it.
|
|
363
|
-
|
|
364
|
-
What does need something is a module that renders. `requireNativeViewManager` returns a React
|
|
365
|
-
class, and the Fabric component underneath it is registered under a name derived from the module
|
|
366
|
-
name and, in Expo Go, from a per-project identifier. `registerExpoView` derives the same name and
|
|
367
|
-
hands it to the engine:
|
|
368
|
-
|
|
369
|
-
```ts
|
|
370
|
-
import {
|
|
371
|
-
registerExpoView,
|
|
372
|
-
registerExpoViews,
|
|
373
|
-
registerExpoUiViews,
|
|
374
|
-
registerNativeViews,
|
|
375
|
-
} from '@ng-native/expo';
|
|
376
|
-
|
|
377
|
-
registerExpoViews('expo-image', 'expo-blur'); // Expo's own
|
|
378
|
-
registerNativeViews('web-view', 'slider'); // the community libraries in Expo's bundled list
|
|
379
|
-
registerExpoUiViews(Platform.OS); // every @expo/ui control the platform has
|
|
380
|
-
registerExpoView('my-module-view', 'MyModule'); // anything else
|
|
381
|
-
```
|
|
382
|
-
|
|
383
|
-
### @expo/ui
|
|
384
|
-
|
|
385
|
-
The closest thing in the ecosystem to what this project is for: every one of these is a real
|
|
386
|
-
platform control - a real `Picker`, a real `BottomSheet`, a real `Gauge` - rather than something
|
|
387
|
-
drawn to look like one. `@expo/ui` reaches them through `requireNativeView('ExpoUI', ...)`, which
|
|
388
|
-
is the derivation `registerExpoView` already mirrors, so the whole surface is a table of names.
|
|
389
|
-
|
|
390
|
-
Element names are platform-neutral where both platforms have the control, so an app writes
|
|
391
|
-
`<ui-slider>` once and `<ui-vstack>` gets Compose's `Column` on Android. Where only one platform
|
|
392
|
-
has it - a `Gauge` is SwiftUI's, a `SearchBar` is Compose's - the element exists only there, since
|
|
393
|
-
registering for the wrong platform is an element that commits as nothing.
|
|
394
|
-
|
|
395
|
-
**A `<ui-host>` is required around any of them.** SwiftUI and Compose lay out their own subtrees,
|
|
396
|
-
and the host view is what bridges Yoga's layout to theirs; without one the control has no size and
|
|
397
|
-
does not appear - which looks exactly like a module that failed to install.
|
|
398
|
-
|
|
399
|
-
`<expo-image>` is then an element like any other, and its props are the ones the module's native
|
|
400
|
-
view declares - which is to say the ones the React component would have passed down, after
|
|
401
|
-
whatever resolving it does in JavaScript:
|
|
402
|
-
|
|
403
|
-
```html
|
|
404
|
-
<expo-image [source]="[{ uri }]" contentFit="cover" [transition]="{ duration: 400 }" />
|
|
405
|
-
```
|
|
406
|
-
|
|
407
|
-
Most of that is deliberately not wrapped. An Angular component per Expo view, prop for prop, is
|
|
408
|
-
a maintenance burden with no end; a name plus the module's own documentation covers the common
|
|
409
|
-
case, and an app that wants a nicer surface can write the component it actually needs.
|
|
410
|
-
|
|
411
|
-
The exception is strict templates, where an unknown element and every prop on it is a type error.
|
|
412
|
-
The views apps reach for most (`ui-host`, `ui-menu`, `ui-button`, `ui-divider`, `ui-slot`,
|
|
413
|
-
`ui-image`, `ui-text`, `ui-date-picker`) and `segmented-control` have thin typed components:
|
|
414
|
-
import `UiHost`, `SegmentedControl` and the rest, and the element is typed while committing exactly
|
|
415
|
-
as before. Keep registering the names too. The component supplies the types and the registration
|
|
416
|
-
makes the element commit as the native view. Their events are outputs so `$event` is typed. The
|
|
417
|
-
element's own event is what arrives, so bind them in a template.
|
|
37
|
+
One entry point per module, so importing haptics does not bundle the file system. Nothing is
|
|
38
|
+
provided anywhere - injecting a service is the whole setup, and a service nobody injects is never
|
|
39
|
+
constructed. See [Using a module](https://ng-native.com/packages/expo/using-a-module) for how the
|
|
40
|
+
`exports` map and the runtime fallback behind each service work.
|
|
41
|
+
|
|
42
|
+
## What's in the package
|
|
43
|
+
|
|
44
|
+
- Device state - `Battery`, `Brightness`, `Network`, `Orientation`, `Locale`, the sensors, `Fonts`,
|
|
45
|
+
`AppInfo`, `KeepAwake`.
|
|
46
|
+
- Feedback - `Haptics`, `Clipboard`, `Notifications`, `StoreReview`.
|
|
47
|
+
- Storage and files - `Storage`, `SecureStorage`, `FileSystem`, `database()`, `assets()`.
|
|
48
|
+
- Media and camera - `ImagePicker`, `DocumentPicker`, `ImageEditor`, `MediaLibrary`, the `Camera`
|
|
49
|
+
directive, `videoPlayer()`/`audioPlayer()`.
|
|
50
|
+
- Location and identity - `Location`, `Biometrics`, Sign in with Apple, `Tracking`, `Crypto`,
|
|
51
|
+
`ScreenCapture`, `Browser`.
|
|
52
|
+
- App lifecycle - `splashScreen`, `Updates`, `BackgroundTask`.
|
|
53
|
+
- Native views - `registerExpoView()`/`registerExpoViews()`, and the `@expo/ui` SwiftUI and
|
|
54
|
+
Jetpack Compose controls.
|
|
55
|
+
- `Permission` - the `getXPermissionsAsync`/`requestXPermissionsAsync` pair any module exposes,
|
|
56
|
+
as one class.
|
|
57
|
+
|
|
58
|
+
## Docs
|
|
59
|
+
|
|
60
|
+
- [Expo](https://ng-native.com/packages/expo), with the full module list by category
|
|
61
|
+
- [Using a module](https://ng-native.com/packages/expo/using-a-module) and
|
|
62
|
+
[permissions](https://ng-native.com/packages/expo/permissions)
|
|
63
|
+
- [Root README](https://github.com/ng-native/ng-native/blob/main/README.md)
|
|
64
|
+
|
|
65
|
+
## License
|
|
66
|
+
|
|
67
|
+
MIT
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `Storage`, a `Store` over `@react-native-async-storage/async-storage`: a plain key-value file.
|
|
3
|
+
*
|
|
4
|
+
* ```ts
|
|
5
|
+
* private readonly store = inject(Storage);
|
|
6
|
+
* protected readonly theme = this.store.signal<'light' | 'dark'>('theme', 'light');
|
|
7
|
+
* ```
|
|
8
|
+
*
|
|
9
|
+
* See `Store` in `@ng-native/expo/store` for what the signal does.
|
|
10
|
+
*/
|
|
11
|
+
import { InjectionToken } from '@angular/core';
|
|
12
|
+
import { Store } from './store.ts';
|
|
13
|
+
/** A plain key-value file. Bigger and faster than the keychain, and not private. */
|
|
14
|
+
export declare const Storage: InjectionToken<Store>;
|
|
15
|
+
export type Storage = Store;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `Storage`, a `Store` over `@react-native-async-storage/async-storage`: a plain key-value file.
|
|
3
|
+
*
|
|
4
|
+
* ```ts
|
|
5
|
+
* private readonly store = inject(Storage);
|
|
6
|
+
* protected readonly theme = this.store.signal<'light' | 'dark'>('theme', 'light');
|
|
7
|
+
* ```
|
|
8
|
+
*
|
|
9
|
+
* See `Store` in `@ng-native/expo/store` for what the signal does.
|
|
10
|
+
*/
|
|
11
|
+
import { InjectionToken } from '@angular/core';
|
|
12
|
+
import { expoModule } from "./native.js";
|
|
13
|
+
import { Store } from "./store.js";
|
|
14
|
+
const plain = () => {
|
|
15
|
+
const module = expoModule('@react-native-async-storage/async-storage', () => require('@react-native-async-storage/async-storage'));
|
|
16
|
+
const store = module?.default;
|
|
17
|
+
if (!store)
|
|
18
|
+
return null;
|
|
19
|
+
return {
|
|
20
|
+
get: (key) => store.getItem(key),
|
|
21
|
+
set: (key, value) => store.setItem(key, value),
|
|
22
|
+
remove: (key) => store.removeItem(key),
|
|
23
|
+
};
|
|
24
|
+
};
|
|
25
|
+
/** A plain key-value file. Bigger and faster than the keychain, and not private. */
|
|
26
|
+
export const Storage = new InjectionToken('angular-native.storage', {
|
|
27
|
+
factory: () => new Store(plain()),
|
|
28
|
+
});
|
|
29
|
+
//# sourceMappingURL=async-storage.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"async-storage.js","sourceRoot":"","sources":["../src/async-storage.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AACH,OAAO,EAAE,cAAc,EAAE,MAAM,eAAe,CAAC;AAC/C,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AACzC,OAAO,EAAE,KAAK,EAAoB,MAAM,YAAY,CAAC;AAErD,MAAM,KAAK,GAAG,GAAuB,EAAE;IACrC,MAAM,MAAM,GAAG,UAAU,CACvB,2CAA2C,EAC3C,GAAG,EAAE,CACH,OAAO,CAAC,2CAA2C,CAElD,CACJ,CAAC;IACF,MAAM,KAAK,GAAG,MAAM,EAAE,OAAO,CAAC;IAC9B,IAAI,CAAC,KAAK;QAAE,OAAO,IAAI,CAAC;IACxB,OAAO;QACL,GAAG,EAAE,CAAC,GAAG,EAAE,EAAE,CAAC,KAAK,CAAC,OAAO,CAAC,GAAG,CAAC;QAChC,GAAG,EAAE,CAAC,GAAG,EAAE,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,OAAO,CAAC,GAAG,EAAE,KAAK,CAAC;QAC9C,MAAM,EAAE,CAAC,GAAG,EAAE,EAAE,CAAC,KAAK,CAAC,UAAU,CAAC,GAAG,CAAC;KACvC,CAAC;AACJ,CAAC,CAAC;AAEF,oFAAoF;AACpF,MAAM,CAAC,MAAM,OAAO,GAAG,IAAI,cAAc,CAAQ,wBAAwB,EAAE;IACzE,OAAO,EAAE,GAAG,EAAE,CAAC,IAAI,KAAK,CAAC,KAAK,EAAE,CAAC;CAClC,CAAC,CAAC","sourcesContent":["/**\n * `Storage`, a `Store` over `@react-native-async-storage/async-storage`: a plain key-value file.\n *\n * ```ts\n * private readonly store = inject(Storage);\n * protected readonly theme = this.store.signal<'light' | 'dark'>('theme', 'light');\n * ```\n *\n * See `Store` in `@ng-native/expo/store` for what the signal does.\n */\nimport { InjectionToken } from '@angular/core';\nimport { expoModule } from './native.ts';\nimport { Store, type NativeStore } from './store.ts';\n\nconst plain = (): NativeStore | null => {\n const module = expoModule(\n '@react-native-async-storage/async-storage',\n () =>\n require('@react-native-async-storage/async-storage') as {\n default: typeof import('@react-native-async-storage/async-storage').default;\n },\n );\n const store = module?.default;\n if (!store) return null;\n return {\n get: (key) => store.getItem(key),\n set: (key, value) => store.setItem(key, value),\n remove: (key) => store.removeItem(key),\n };\n};\n\n/** A plain key-value file. Bigger and faster than the keychain, and not private. */\nexport const Storage = new InjectionToken<Store>('angular-native.storage', {\n factory: () => new Store(plain()),\n});\nexport type Storage = Store;\n"]}
|
package/dist/audio.d.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { type Player } from './player.ts';
|
|
2
|
+
/**
|
|
3
|
+
* An audio player owned by the current component, released when it is destroyed.
|
|
4
|
+
*
|
|
5
|
+
* `expo-audio` has no `timeUpdateEventInterval` property to set after the fact - the interval is
|
|
6
|
+
* an option to `createAudioPlayer` itself, in milliseconds rather than `videoPlayer`'s seconds.
|
|
7
|
+
*/
|
|
8
|
+
export declare function audioPlayer(source: import('expo-audio').AudioSource, options?: {
|
|
9
|
+
timeUpdate?: number;
|
|
10
|
+
}): Player<import('expo-audio').AudioPlayer>;
|
package/dist/audio.js
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `audioPlayer`, an `expo-audio` player owned by the component that makes it.
|
|
3
|
+
*
|
|
4
|
+
* ```ts
|
|
5
|
+
* protected readonly player = audioPlayer(this.source, { timeUpdate: 0.5 });
|
|
6
|
+
*
|
|
7
|
+
* // {{ player.state().currentTime }}
|
|
8
|
+
* ```
|
|
9
|
+
*/
|
|
10
|
+
import { expoModule } from "./native.js";
|
|
11
|
+
import { ownPlayer, watchAudioPlayer } from "./player.js";
|
|
12
|
+
/**
|
|
13
|
+
* An audio player owned by the current component, released when it is destroyed.
|
|
14
|
+
*
|
|
15
|
+
* `expo-audio` has no `timeUpdateEventInterval` property to set after the fact - the interval is
|
|
16
|
+
* an option to `createAudioPlayer` itself, in milliseconds rather than `videoPlayer`'s seconds.
|
|
17
|
+
*/
|
|
18
|
+
export function audioPlayer(source, options = {}) {
|
|
19
|
+
const expo = expoModule('expo-audio', () => require('expo-audio'), ['ios', 'android', 'web']);
|
|
20
|
+
if (!expo)
|
|
21
|
+
throw new Error('[angular-native] expo-audio is not installed');
|
|
22
|
+
const updateInterval = options.timeUpdate === undefined ? undefined : options.timeUpdate * 1000;
|
|
23
|
+
const native = expo.createAudioPlayer(source, { updateInterval });
|
|
24
|
+
return ownPlayer(native, () => watchAudioPlayer(native));
|
|
25
|
+
}
|
|
26
|
+
//# sourceMappingURL=audio.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"audio.js","sourceRoot":"","sources":["../src/audio.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AACH,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AACzC,OAAO,EAAE,SAAS,EAAE,gBAAgB,EAAe,MAAM,aAAa,CAAC;AAEvE;;;;;GAKG;AACH,MAAM,UAAU,WAAW,CACzB,MAAwC,EACxC,UAAmC,EAAE;IAErC,MAAM,IAAI,GAAG,UAAU,CACrB,YAAY,EACZ,GAAG,EAAE,CAAC,OAAO,CAAC,YAAY,CAAgC,EAC1D,CAAC,KAAK,EAAE,SAAS,EAAE,KAAK,CAAC,CAC1B,CAAC;IACF,IAAI,CAAC,IAAI;QAAE,MAAM,IAAI,KAAK,CAAC,8CAA8C,CAAC,CAAC;IAC3E,MAAM,cAAc,GAAG,OAAO,CAAC,UAAU,KAAK,SAAS,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,OAAO,CAAC,UAAU,GAAG,IAAI,CAAC;IAChG,MAAM,MAAM,GAAG,IAAI,CAAC,iBAAiB,CAAC,MAAM,EAAE,EAAE,cAAc,EAAE,CAAC,CAAC;IAClE,OAAO,SAAS,CAAC,MAAM,EAAE,GAAG,EAAE,CAAC,gBAAgB,CAAC,MAAM,CAAC,CAAC,CAAC;AAC3D,CAAC","sourcesContent":["/**\n * `audioPlayer`, an `expo-audio` player owned by the component that makes it.\n *\n * ```ts\n * protected readonly player = audioPlayer(this.source, { timeUpdate: 0.5 });\n *\n * // {{ player.state().currentTime }}\n * ```\n */\nimport { expoModule } from './native.ts';\nimport { ownPlayer, watchAudioPlayer, type Player } from './player.ts';\n\n/**\n * An audio player owned by the current component, released when it is destroyed.\n *\n * `expo-audio` has no `timeUpdateEventInterval` property to set after the fact - the interval is\n * an option to `createAudioPlayer` itself, in milliseconds rather than `videoPlayer`'s seconds.\n */\nexport function audioPlayer(\n source: import('expo-audio').AudioSource,\n options: { timeUpdate?: number } = {},\n): Player<import('expo-audio').AudioPlayer> {\n const expo = expoModule(\n 'expo-audio',\n () => require('expo-audio') as typeof import('expo-audio'),\n ['ios', 'android', 'web'],\n );\n if (!expo) throw new Error('[angular-native] expo-audio is not installed');\n const updateInterval = options.timeUpdate === undefined ? undefined : options.timeUpdate * 1000;\n const native = expo.createAudioPlayer(source, { updateInterval });\n return ownPlayer(native, () => watchAudioPlayer(native));\n}\n"]}
|