Using a module
Installing @ng-native/expo itself pulls in no native code at all. You install the actual Expo
module because your app wants that capability, alongside the package that wraps it:
npm install @ng-native/expo
npm install expo-battery # only if you're injecting BatteryOne entry point per module
Most of these services have their own entry point - @ng-native/expo/battery,
@ng-native/expo/haptics, and so on, one file per module - rather than being re-exported from
the package's own root. That is so that importing haptics never pulls in the video player:
import { Battery } from '@ng-native/expo/battery';
import { Haptics } from '@ng-native/expo/haptics';
import { Storage } from '@ng-native/expo/async-storage';
import { SecureStorage } from '@ng-native/expo/secure-store';Each name is a type as well as a token, so inject(Clipboard) and private clipboard: Clipboard
both work. Each service is one file: a @Service() class that reaches its module through an
injected source token - an InjectionToken carrying its own factory - so there is nothing to
provide and nothing to register. Injecting it is the whole setup, and a service nobody injects is
never constructed.
What happens without the module installed
A service reaches for its module when it is first injected, with a require() inside a factory
rather than a static import: a file with a static import ... from 'expo-battery' would be
unloadable by Node at all - Expo's build output uses extensionless relative imports, and what it
pulls in reaches react-native, which is Flow.
Where the module exists for the platform the app is on, a missing one is a mistake with a fix, so
using the service throws a MissingModuleError rather than quietly doing nothing. Injecting it does
not throw: a call, or a read of a value only the module knows, does. Whether the feature is there is
the one question that answers instead: available reads false, and available() resolves to
false. That covers a package that was never installed, and the more common case of one that was
installed after the app was last built: a development build, and Expo Go, contain only the native
modules they were built with. The message names the module and the commands to run:
expo-haptics is not in this build of the app. Install it with "npx expo install expo-haptics",
then rebuild the app ("npx expo run:ios", or a new EAS build): a development build, and Expo Go,
contain only the native modules they were built with.On iOS and Android, a service asks Expo whether the module's native half is in the build before it
evaluates the package's JavaScript. Most Expo packages throw while they are being evaluated when it
is not, and Metro reports a throw there as a fatal error, in place of the MissingModuleError.
MissingModuleError is exported from @ng-native/expo, with a module property naming the
package, for an app that wants to catch it and show something of its own.
Where the module cannot exist, the service goes inert instead: an iOS-only module such as
Sign in with Apple on Android, a service on the web, and a test in
Node, where there is no platform at all. Code shared across platforms, and a test that provides
no fake, keep working. Inert means reporting nothing rather than throwing: a level of 1, a
status of 'unknown', a method that resolves to null or an empty list. Each module's own page
says what its service does in both cases. A few cannot be inert, because an empty answer would be
a wrong one: a database or a player throws on
the web as well, and Crypto throws wherever it has no module, since an
empty identifier or hash looks right and is not.
Faking a module in a test
A test runs in Node, where no native module can load, installed or not. Most services do nothing
there, as off a device. One that has no answer to give without its module, Crypto and
FileSystem among them, throws a MissingModuleError that says so. A service with
a SOURCE token, Crypto.SOURCE or FileSystem.SOURCE, reads its module through it, so a test
provides a stand-in with the methods the code under test calls:
providers: [{ provide: Crypto.SOURCE, useValue: { randomUUID: () => 'test-id' } }];A database has no token: openDatabasesWith() points database() at a stand-in. See
Database.
A module with no service
An Expo module this package has no service for, expo-print or expo-contacts, has no React in
it and works as it is. What does not work is its documented import in a file a test loads:
import * as Print from 'expo-print' evaluates the package, the package imports react-native,
and a test in Node fails while the module graph loads, with Flow is not supported, whether or
not the test prints anything.
Load it where it is used instead, as this package's own services do: a token whose factory calls
expoModule(), and a service that injects it.
import { InjectionToken, Service, inject } from '@angular/core';
import { expoModule } from '@ng-native/expo';
interface NativePrint {
printToFileAsync(options: { html: string }): Promise<{ uri: string }>;
}
export const PRINT = new InjectionToken<NativePrint | null>('app.print', {
factory: () => expoModule('expo-print', () => require('expo-print') as NativePrint),
});
@Service()
export class Printer {
private readonly native = inject(PRINT);
async toFile(html: string): Promise<string | null> {
return (await this.native?.printToFileAsync({ html }))?.uri ?? null;
}
}The require runs when the token is first injected rather than when the file loads. In a test
expoModule() answers null, so the service is inert, and a test that wants an answer provides
one under the token.
providers: [
{ provide: PRINT, useValue: { printToFileAsync: async () => ({ uri: 'file:///out.pdf' }) } },
];The interface is the part of the module the app calls, written by hand or taken from the
package's own types with import type, which a test does not evaluate.
Whether a feature is available
A service that can say whether its feature is there says it as available, in one of three shapes,
by what the answer is like:
- A getter,
available, when the answer is known at once and does not change: whether the module is in the build, as for Haptics, Fonts and Tracking. - A method that resolves,
await available(), when the answer is a fresh native check that can change while the app runs: whether a face or fingerprint is enrolled for Biometrics, and likewise for Sign in with Apple, screen capture and the store review prompt. Ask it just before offering the feature. - A signal,
available(), when the feature comes and goes and a template follows it: the hinge of a foldable, the on-device language model, a sensor.
What is on the bare import
A handful of exports are not bound to one optional module, so they live on @ng-native/expo
itself rather than behind an entry point: Permission,
registerExpoView, registerExpoViews, registerNativeViews and registerExpoUiViews (see
Native views and Expo UI), and
nativeState (also on the Expo UI page).
Where everything else lives
Every module has its own page, grouped by what it is for, from the overview. For a capability no package covers, create an Expo module in Swift and Kotlin or use NativeScript to call platform APIs directly from TypeScript.