@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
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"secure-store.js","sourceRoot":"","sources":["../src/secure-store.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,MAAM,GAAG,GAAuB,EAAE;IACtC,MAAM,IAAI,GAAG,UAAU,CACrB,mBAAmB,EACnB,GAAG,EAAE,CAAC,OAAO,CAAC,mBAAmB,CAAuC,CACzE,CAAC;IACF,IAAI,CAAC,IAAI;QAAE,OAAO,IAAI,CAAC;IACvB,OAAO;QACL,GAAG,EAAE,CAAC,GAAG,EAAE,EAAE,CAAC,IAAI,CAAC,YAAY,CAAC,GAAG,CAAC;QACpC,GAAG,EAAE,CAAC,GAAG,EAAE,KAAK,EAAE,EAAE,CAAC,IAAI,CAAC,YAAY,CAAC,GAAG,EAAE,KAAK,CAAC;QAClD,MAAM,EAAE,CAAC,GAAG,EAAE,EAAE,CAAC,IAAI,CAAC,eAAe,CAAC,GAAG,CAAC;QAC1C,iFAAiF;QACjF,OAAO,EAAE,CAAC,GAAG,EAAE,EAAE,CAAC,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC;KACpC,CAAC;AACJ,CAAC,CAAC;AAEF,qFAAqF;AACrF,MAAM,CAAC,MAAM,aAAa,GAAG,IAAI,cAAc,CAAQ,8BAA8B,EAAE;IACrF,OAAO,EAAE,GAAG,EAAE,CAAC,IAAI,KAAK,CAAC,MAAM,EAAE,CAAC;CACnC,CAAC,CAAC","sourcesContent":["/**\n * `SecureStorage`, a `Store` over `expo-secure-store`: the keychain and the Android keystore.\n *\n * ```ts\n * private readonly store = inject(SecureStorage);\n * protected readonly token = this.store.signal<string | null>('token', null);\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 secure = (): NativeStore | null => {\n const expo = expoModule(\n 'expo-secure-store',\n () => require('expo-secure-store') as typeof import('expo-secure-store'),\n );\n if (!expo) return null;\n return {\n get: (key) => expo.getItemAsync(key),\n set: (key, value) => expo.setItemAsync(key, value),\n remove: (key) => expo.deleteItemAsync(key),\n // The keychain can answer without waiting, so a token never flashes its default.\n getSync: (key) => expo.getItem(key),\n };\n};\n\n/** The keychain and the Android keystore. Small values, and worth the size limit. */\nexport const SecureStorage = new InjectionToken<Store>('angular-native.secureStorage', {\n factory: () => new Store(secure()),\n});\nexport type SecureStorage = Store;\n"]}
|
package/dist/store.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* `
|
|
3
|
-
*
|
|
2
|
+
* `Store`, what `Storage` (`@ng-native/expo/async-storage`) and `SecureStorage`
|
|
3
|
+
* (`@ng-native/expo/secure-store`) both are, over whichever native store each is bound to.
|
|
4
4
|
*
|
|
5
5
|
* ```ts
|
|
6
6
|
* private readonly store = inject(Storage);
|
|
@@ -16,12 +16,13 @@
|
|
|
16
16
|
* that held nothing.
|
|
17
17
|
*
|
|
18
18
|
* Two services rather than one because the difference matters and should be visible at the
|
|
19
|
-
* injection site
|
|
19
|
+
* injection site, and two entry points because each needs its own native module: Metro fails a
|
|
20
|
+
* build on a `require` it cannot resolve, so one entry point for both made an app install both. `SecureStorage` is the keychain and the Android keystore: small values, an
|
|
20
21
|
* async write, and a size limit that is not documented but is real. `Storage` is a plain
|
|
21
22
|
* key-value file: bigger, faster, and readable by anyone with the device unlocked. Two tokens
|
|
22
23
|
* over one class, for the same reason the sensors are.
|
|
23
24
|
*/
|
|
24
|
-
import {
|
|
25
|
+
import { type Signal, type WritableSignal } from '@angular/core';
|
|
25
26
|
/** What both `AsyncStorage` and `SecureStore` offer, reduced to the three verbs. */
|
|
26
27
|
export interface NativeStore {
|
|
27
28
|
get(key: string): Promise<string | null>;
|
|
@@ -100,9 +101,3 @@ export declare class Store {
|
|
|
100
101
|
private track;
|
|
101
102
|
private fail;
|
|
102
103
|
}
|
|
103
|
-
/** A plain key-value file. Bigger and faster than the keychain, and not private. */
|
|
104
|
-
export declare const Storage: InjectionToken<Store>;
|
|
105
|
-
export type Storage = Store;
|
|
106
|
-
/** The keychain and the Android keystore. Small values, and worth the size limit. */
|
|
107
|
-
export declare const SecureStorage: InjectionToken<Store>;
|
|
108
|
-
export type SecureStorage = Store;
|
package/dist/store.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* `
|
|
3
|
-
*
|
|
2
|
+
* `Store`, what `Storage` (`@ng-native/expo/async-storage`) and `SecureStorage`
|
|
3
|
+
* (`@ng-native/expo/secure-store`) both are, over whichever native store each is bound to.
|
|
4
4
|
*
|
|
5
5
|
* ```ts
|
|
6
6
|
* private readonly store = inject(Storage);
|
|
@@ -16,13 +16,13 @@
|
|
|
16
16
|
* that held nothing.
|
|
17
17
|
*
|
|
18
18
|
* Two services rather than one because the difference matters and should be visible at the
|
|
19
|
-
* injection site
|
|
19
|
+
* injection site, and two entry points because each needs its own native module: Metro fails a
|
|
20
|
+
* build on a `require` it cannot resolve, so one entry point for both made an app install both. `SecureStorage` is the keychain and the Android keystore: small values, an
|
|
20
21
|
* async write, and a size limit that is not documented but is real. `Storage` is a plain
|
|
21
22
|
* key-value file: bigger, faster, and readable by anyone with the device unlocked. Two tokens
|
|
22
23
|
* over one class, for the same reason the sensors are.
|
|
23
24
|
*/
|
|
24
|
-
import {
|
|
25
|
-
import { expoModule } from "./native.js";
|
|
25
|
+
import { computed, signal } from '@angular/core';
|
|
26
26
|
export class Store {
|
|
27
27
|
native;
|
|
28
28
|
bound = new Map();
|
|
@@ -220,35 +220,4 @@ function decode(raw) {
|
|
|
220
220
|
return ABSENT;
|
|
221
221
|
}
|
|
222
222
|
}
|
|
223
|
-
const plain = () => {
|
|
224
|
-
const module = expoModule('@react-native-async-storage/async-storage', () => require('@react-native-async-storage/async-storage'));
|
|
225
|
-
const store = module?.default;
|
|
226
|
-
if (!store)
|
|
227
|
-
return null;
|
|
228
|
-
return {
|
|
229
|
-
get: (key) => store.getItem(key),
|
|
230
|
-
set: (key, value) => store.setItem(key, value),
|
|
231
|
-
remove: (key) => store.removeItem(key),
|
|
232
|
-
};
|
|
233
|
-
};
|
|
234
|
-
const secure = () => {
|
|
235
|
-
const expo = expoModule('expo-secure-store', () => require('expo-secure-store'));
|
|
236
|
-
if (!expo)
|
|
237
|
-
return null;
|
|
238
|
-
return {
|
|
239
|
-
get: (key) => expo.getItemAsync(key),
|
|
240
|
-
set: (key, value) => expo.setItemAsync(key, value),
|
|
241
|
-
remove: (key) => expo.deleteItemAsync(key),
|
|
242
|
-
// The keychain can answer without waiting, so a token never flashes its default.
|
|
243
|
-
getSync: (key) => expo.getItem(key),
|
|
244
|
-
};
|
|
245
|
-
};
|
|
246
|
-
/** A plain key-value file. Bigger and faster than the keychain, and not private. */
|
|
247
|
-
export const Storage = new InjectionToken('angular-native.storage', {
|
|
248
|
-
factory: () => new Store(plain()),
|
|
249
|
-
});
|
|
250
|
-
/** The keychain and the Android keystore. Small values, and worth the size limit. */
|
|
251
|
-
export const SecureStorage = new InjectionToken('angular-native.secureStorage', {
|
|
252
|
-
factory: () => new Store(secure()),
|
|
253
|
-
});
|
|
254
223
|
//# sourceMappingURL=store.js.map
|
package/dist/store.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"store.js","sourceRoot":"","sources":["../src/store.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * `Storage` and `SecureStorage`, bound to `@react-native-async-storage/async-storage` and\n * `expo-secure-store`.\n *\n * ```ts\n * private readonly store = inject(Storage);\n * protected readonly theme = this.store.signal<'light' | 'dark'>('theme', 'light');\n * ```\n *\n * The signal is bound both ways: reading it is reading the preference, setting it persists.\n *\n * The underlying APIs are already plain promises with no React in them, so what is built is not a\n * wrapper but a *binding*: `store.signal('theme', 'light')` reads it back once, writes it through\n * whenever it is set, and is an ordinary signal in between. Everything is JSON, because both\n * stores hold strings and a store that silently held `\"[object Object]\"` would be worse than one\n * that held nothing.\n *\n * Two services rather than one because the difference matters and should be visible at the\n * injection site. `SecureStorage` is the keychain and the Android keystore: small values, an\n * async write, and a size limit that is not documented but is real. `Storage` is a plain\n * key-value file: bigger, faster, and readable by anyone with the device unlocked. Two tokens\n * over one class, for the same reason the sensors are.\n */\nimport { InjectionToken, computed, signal, type Signal, type WritableSignal } from '@angular/core';\nimport { expoModule } from './native.ts';\n\n/** What both `AsyncStorage` and `SecureStore` offer, reduced to the three verbs. */\nexport interface NativeStore {\n get(key: string): Promise<string | null>;\n set(key: string, value: string): Promise<void>;\n remove(key: string): Promise<void>;\n /** Where the platform can answer without waiting - `SecureStore` can, `AsyncStorage` cannot. */\n getSync?(key: string): string | null;\n}\n\n/** A key's signal, with what `remove()` needs to put it back. */\ninterface Binding {\n readonly value: WritableSignal<unknown>;\n /** The signal's own `set`, which does not write through. */\n readonly reset: (value: unknown) => void;\n readonly initial: unknown;\n}\n\nexport class Store {\n private readonly native: NativeStore | null;\n private readonly bound = new Map<string, Binding>();\n /** Keys written by the app before their read came back. The read must not undo a write. */\n private readonly written = new Set<string>();\n /**\n * Updates made to a key whose read has not come back, in order. An update changes the stored\n * value rather than replacing it, so these wait for it and are applied to it, not to `initial`.\n */\n private readonly waiting = new Map<string, ((current: unknown) => unknown)[]>();\n /** Reads started and not yet back, successful or not. */\n private readonly reading = signal(0);\n private readonly failure = signal<Error | null>(null);\n /** Writes sent to the platform and not yet answered. What `flush()` waits for. */\n private readonly writes = new Set<Promise<void>>();\n /** The first write to fail since the last `flush()`, held until a flush reports it. */\n private unreported: { readonly error: unknown } | null = null;\n\n constructor(native: NativeStore | null) {\n this.native = native;\n }\n\n /**\n * Whether every key asked for so far has been read back. False while any read is outstanding,\n * so it goes back to false when a new key is asked for, and true again once that read returns.\n *\n * A read that fails still counts as back: the signal keeps its `initial`, and the failure is on\n * `error`. A loading screen held on `ready` should not wait forever for a store that is broken.\n */\n readonly ready: Signal<boolean> = computed(() => this.reading() === 0);\n\n /**\n * The most recent read or write that failed, or null while none has.\n *\n * A write through `set()` has no promise to reject, so this is where a full disk or a locked\n * keychain is reported. `flush()` is the way to wait on the writes themselves.\n */\n readonly error: Signal<Error | null> = this.failure.asReadonly();\n\n /**\n * The stored value at `key`, as a signal that writes through when set.\n *\n * Starts at `initial` and stays there until the read returns, which is one turn away at best -\n * unless the platform can answer synchronously, in which case it never shows `initial` at all.\n * The same key always returns the same signal, so two components binding to it stay in step.\n */\n signal<T>(key: string, initial: T): WritableSignal<T> {\n const existing = this.bound.get(key);\n if (existing) return existing.value as WritableSignal<T>;\n\n const stored = this.readSync(key);\n const value = signal<T>(stored === ABSENT ? initial : (stored as T));\n\n const set = value.set.bind(value);\n const update = value.update.bind(value);\n value.set = (next: T) => {\n this.written.add(key);\n this.waiting.delete(key);\n set(next);\n this.write(key, next);\n };\n value.update = (fn: (current: T) => T) => {\n // Shown at once, on what is known so far, and written once the stored value is known.\n const waiting = this.waiting.get(key);\n update(fn);\n if (waiting) {\n waiting.push(fn as (current: unknown) => unknown);\n return;\n }\n this.written.add(key);\n this.write(key, value());\n };\n this.bound.set(key, {\n value: value as WritableSignal<unknown>,\n reset: set as (value: unknown) => void,\n initial,\n });\n\n // A platform that answered synchronously has said what is stored; one that cannot has not.\n if (stored === ABSENT && this.native && !this.native.getSync) this.waiting.set(key, []);\n void this.hydrate(key, set, initial);\n return value;\n }\n\n /**\n * Forget a key, in the store and in the signal bound to it.\n *\n * The signal goes back to the `initial` it was first asked for with, and stays the one signal\n * for the key. Rejects if the platform could not remove it, which also sets `error`.\n */\n async remove(key: string): Promise<void> {\n this.written.add(key);\n this.waiting.delete(key);\n const binding = this.bound.get(key);\n binding?.reset(binding.initial);\n await this.track(this.native?.remove(key));\n }\n\n /**\n * Wait for every write sent so far. Rejects with the first of them that failed since the last\n * `flush()`, whether it failed before this call or while waiting, so a screen that has to know a\n * value reached the disk - before signing out, say - can find out. Each failure is reported by\n * one flush only.\n */\n async flush(): Promise<void> {\n await Promise.allSettled([...this.writes]);\n const failed = this.unreported;\n this.unreported = null;\n if (failed) throw failed.error;\n }\n\n private readSync(key: string): unknown {\n let raw: string | null | undefined;\n try {\n raw = this.native?.getSync?.(key);\n } catch (error) {\n this.fail(error);\n return ABSENT;\n }\n return raw == null ? ABSENT : decode(raw);\n }\n\n /**\n * Read the stored value in, unless the app has already set one.\n *\n * The race is real and silent: a screen that writes a preference during startup would otherwise\n * have it overwritten a tick later by whatever was there before.\n */\n private async hydrate<T>(key: string, set: (value: T) => void, initial: T): Promise<void> {\n this.reading.update((n) => n + 1);\n let stored: unknown = ABSENT;\n try {\n const raw = await this.native?.get(key);\n if (raw != null) stored = decode(raw);\n } catch (error) {\n this.fail(error);\n } finally {\n this.settle(key, set, stored === ABSENT ? initial : stored, stored !== ABSENT);\n this.reading.update((n) => n - 1);\n }\n }\n\n /** The read is back: show what was stored, with any updates that waited for it applied. */\n private settle<T>(key: string, set: (value: T) => void, base: unknown, found: boolean): void {\n const updates = this.waiting.get(key);\n this.waiting.delete(key);\n if (this.written.has(key)) return;\n if (!updates?.length) {\n if (found) set(base as T);\n return;\n }\n const value = updates.reduce((current, fn) => fn(current), base);\n this.written.add(key);\n set(value as T);\n this.write(key, value);\n }\n\n private write(key: string, value: unknown): void {\n const sent =\n value === undefined ? this.native?.remove(key) : this.native?.set(key, JSON.stringify(value));\n // Observed here, so a failure lands on `error` and in `flush()` rather than going unhandled.\n this.track(sent).catch(() => {});\n }\n\n /** A platform call, kept until it answers, with its failure reported on `error`. */\n private track(sent: Promise<void> | undefined): Promise<void> {\n if (!sent) return Promise.resolve();\n const tracked = sent.then(\n () => undefined,\n (error: unknown) => {\n this.fail(error);\n this.unreported ??= { error };\n throw error;\n },\n );\n this.writes.add(tracked);\n const forget = () => void this.writes.delete(tracked);\n tracked.then(forget, forget);\n return tracked;\n }\n\n private fail(error: unknown): void {\n this.failure.set(error instanceof Error ? error : new Error(String(error)));\n }\n}\n\n/** What `decode` answers for a string that is not JSON, since `null` is a value JSON can hold. */\nconst ABSENT: unique symbol = Symbol('absent');\n\n/**\n * A stored string as the value it encodes.\n *\n * A value that will not parse is treated as absent rather than thrown: it is a store written by\n * an older version of the app, and the right answer is the default the caller already supplied.\n * A stored `null` is not that: it parses, and a nullable signal gets it back.\n */\nfunction decode(raw: string): unknown {\n try {\n return JSON.parse(raw) as unknown;\n } catch {\n return ABSENT;\n }\n}\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\nconst secure = (): NativeStore | null => {\n const expo = expoModule(\n 'expo-secure-store',\n () => require('expo-secure-store') as typeof import('expo-secure-store'),\n );\n if (!expo) return null;\n return {\n get: (key) => expo.getItemAsync(key),\n set: (key, value) => expo.setItemAsync(key, value),\n remove: (key) => expo.deleteItemAsync(key),\n // The keychain can answer without waiting, so a token never flashes its default.\n getSync: (key) => expo.getItem(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\n/** The keychain and the Android keystore. Small values, and worth the size limit. */\nexport const SecureStorage = new InjectionToken<Store>('angular-native.secureStorage', {\n factory: () => new Store(secure()),\n});\nexport type SecureStorage = Store;\n"]}
|
|
1
|
+
{"version":3,"file":"store.js","sourceRoot":"","sources":["../src/store.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * `Store`, what `Storage` (`@ng-native/expo/async-storage`) and `SecureStorage`\n * (`@ng-native/expo/secure-store`) both are, over whichever native store each is bound to.\n *\n * ```ts\n * private readonly store = inject(Storage);\n * protected readonly theme = this.store.signal<'light' | 'dark'>('theme', 'light');\n * ```\n *\n * The signal is bound both ways: reading it is reading the preference, setting it persists.\n *\n * The underlying APIs are already plain promises with no React in them, so what is built is not a\n * wrapper but a *binding*: `store.signal('theme', 'light')` reads it back once, writes it through\n * whenever it is set, and is an ordinary signal in between. Everything is JSON, because both\n * stores hold strings and a store that silently held `\"[object Object]\"` would be worse than one\n * that held nothing.\n *\n * Two services rather than one because the difference matters and should be visible at the\n * injection site, and two entry points because each needs its own native module: Metro fails a\n * build on a `require` it cannot resolve, so one entry point for both made an app install both. `SecureStorage` is the keychain and the Android keystore: small values, an\n * async write, and a size limit that is not documented but is real. `Storage` is a plain\n * key-value file: bigger, faster, and readable by anyone with the device unlocked. Two tokens\n * over one class, for the same reason the sensors are.\n */\nimport { computed, signal, type Signal, type WritableSignal } from '@angular/core';\n\n/** What both `AsyncStorage` and `SecureStore` offer, reduced to the three verbs. */\nexport interface NativeStore {\n get(key: string): Promise<string | null>;\n set(key: string, value: string): Promise<void>;\n remove(key: string): Promise<void>;\n /** Where the platform can answer without waiting - `SecureStore` can, `AsyncStorage` cannot. */\n getSync?(key: string): string | null;\n}\n\n/** A key's signal, with what `remove()` needs to put it back. */\ninterface Binding {\n readonly value: WritableSignal<unknown>;\n /** The signal's own `set`, which does not write through. */\n readonly reset: (value: unknown) => void;\n readonly initial: unknown;\n}\n\nexport class Store {\n private readonly native: NativeStore | null;\n private readonly bound = new Map<string, Binding>();\n /** Keys written by the app before their read came back. The read must not undo a write. */\n private readonly written = new Set<string>();\n /**\n * Updates made to a key whose read has not come back, in order. An update changes the stored\n * value rather than replacing it, so these wait for it and are applied to it, not to `initial`.\n */\n private readonly waiting = new Map<string, ((current: unknown) => unknown)[]>();\n /** Reads started and not yet back, successful or not. */\n private readonly reading = signal(0);\n private readonly failure = signal<Error | null>(null);\n /** Writes sent to the platform and not yet answered. What `flush()` waits for. */\n private readonly writes = new Set<Promise<void>>();\n /** The first write to fail since the last `flush()`, held until a flush reports it. */\n private unreported: { readonly error: unknown } | null = null;\n\n constructor(native: NativeStore | null) {\n this.native = native;\n }\n\n /**\n * Whether every key asked for so far has been read back. False while any read is outstanding,\n * so it goes back to false when a new key is asked for, and true again once that read returns.\n *\n * A read that fails still counts as back: the signal keeps its `initial`, and the failure is on\n * `error`. A loading screen held on `ready` should not wait forever for a store that is broken.\n */\n readonly ready: Signal<boolean> = computed(() => this.reading() === 0);\n\n /**\n * The most recent read or write that failed, or null while none has.\n *\n * A write through `set()` has no promise to reject, so this is where a full disk or a locked\n * keychain is reported. `flush()` is the way to wait on the writes themselves.\n */\n readonly error: Signal<Error | null> = this.failure.asReadonly();\n\n /**\n * The stored value at `key`, as a signal that writes through when set.\n *\n * Starts at `initial` and stays there until the read returns, which is one turn away at best -\n * unless the platform can answer synchronously, in which case it never shows `initial` at all.\n * The same key always returns the same signal, so two components binding to it stay in step.\n */\n signal<T>(key: string, initial: T): WritableSignal<T> {\n const existing = this.bound.get(key);\n if (existing) return existing.value as WritableSignal<T>;\n\n const stored = this.readSync(key);\n const value = signal<T>(stored === ABSENT ? initial : (stored as T));\n\n const set = value.set.bind(value);\n const update = value.update.bind(value);\n value.set = (next: T) => {\n this.written.add(key);\n this.waiting.delete(key);\n set(next);\n this.write(key, next);\n };\n value.update = (fn: (current: T) => T) => {\n // Shown at once, on what is known so far, and written once the stored value is known.\n const waiting = this.waiting.get(key);\n update(fn);\n if (waiting) {\n waiting.push(fn as (current: unknown) => unknown);\n return;\n }\n this.written.add(key);\n this.write(key, value());\n };\n this.bound.set(key, {\n value: value as WritableSignal<unknown>,\n reset: set as (value: unknown) => void,\n initial,\n });\n\n // A platform that answered synchronously has said what is stored; one that cannot has not.\n if (stored === ABSENT && this.native && !this.native.getSync) this.waiting.set(key, []);\n void this.hydrate(key, set, initial);\n return value;\n }\n\n /**\n * Forget a key, in the store and in the signal bound to it.\n *\n * The signal goes back to the `initial` it was first asked for with, and stays the one signal\n * for the key. Rejects if the platform could not remove it, which also sets `error`.\n */\n async remove(key: string): Promise<void> {\n this.written.add(key);\n this.waiting.delete(key);\n const binding = this.bound.get(key);\n binding?.reset(binding.initial);\n await this.track(this.native?.remove(key));\n }\n\n /**\n * Wait for every write sent so far. Rejects with the first of them that failed since the last\n * `flush()`, whether it failed before this call or while waiting, so a screen that has to know a\n * value reached the disk - before signing out, say - can find out. Each failure is reported by\n * one flush only.\n */\n async flush(): Promise<void> {\n await Promise.allSettled([...this.writes]);\n const failed = this.unreported;\n this.unreported = null;\n if (failed) throw failed.error;\n }\n\n private readSync(key: string): unknown {\n let raw: string | null | undefined;\n try {\n raw = this.native?.getSync?.(key);\n } catch (error) {\n this.fail(error);\n return ABSENT;\n }\n return raw == null ? ABSENT : decode(raw);\n }\n\n /**\n * Read the stored value in, unless the app has already set one.\n *\n * The race is real and silent: a screen that writes a preference during startup would otherwise\n * have it overwritten a tick later by whatever was there before.\n */\n private async hydrate<T>(key: string, set: (value: T) => void, initial: T): Promise<void> {\n this.reading.update((n) => n + 1);\n let stored: unknown = ABSENT;\n try {\n const raw = await this.native?.get(key);\n if (raw != null) stored = decode(raw);\n } catch (error) {\n this.fail(error);\n } finally {\n this.settle(key, set, stored === ABSENT ? initial : stored, stored !== ABSENT);\n this.reading.update((n) => n - 1);\n }\n }\n\n /** The read is back: show what was stored, with any updates that waited for it applied. */\n private settle<T>(key: string, set: (value: T) => void, base: unknown, found: boolean): void {\n const updates = this.waiting.get(key);\n this.waiting.delete(key);\n if (this.written.has(key)) return;\n if (!updates?.length) {\n if (found) set(base as T);\n return;\n }\n const value = updates.reduce((current, fn) => fn(current), base);\n this.written.add(key);\n set(value as T);\n this.write(key, value);\n }\n\n private write(key: string, value: unknown): void {\n const sent =\n value === undefined ? this.native?.remove(key) : this.native?.set(key, JSON.stringify(value));\n // Observed here, so a failure lands on `error` and in `flush()` rather than going unhandled.\n this.track(sent).catch(() => {});\n }\n\n /** A platform call, kept until it answers, with its failure reported on `error`. */\n private track(sent: Promise<void> | undefined): Promise<void> {\n if (!sent) return Promise.resolve();\n const tracked = sent.then(\n () => undefined,\n (error: unknown) => {\n this.fail(error);\n this.unreported ??= { error };\n throw error;\n },\n );\n this.writes.add(tracked);\n const forget = () => void this.writes.delete(tracked);\n tracked.then(forget, forget);\n return tracked;\n }\n\n private fail(error: unknown): void {\n this.failure.set(error instanceof Error ? error : new Error(String(error)));\n }\n}\n\n/** What `decode` answers for a string that is not JSON, since `null` is a value JSON can hold. */\nconst ABSENT: unique symbol = Symbol('absent');\n\n/**\n * A stored string as the value it encodes.\n *\n * A value that will not parse is treated as absent rather than thrown: it is a store written by\n * an older version of the app, and the right answer is the default the caller already supplied.\n * A stored `null` is not that: it parses, and a nullable signal gets it back.\n */\nfunction decode(raw: string): unknown {\n try {\n return JSON.parse(raw) as unknown;\n } catch {\n return ABSENT;\n }\n}\n"]}
|
package/dist/tracking.d.ts
CHANGED
|
@@ -33,7 +33,7 @@ export declare class Tracking {
|
|
|
33
33
|
*/
|
|
34
34
|
readonly permission: Permission;
|
|
35
35
|
/** Whether the device has the tracking API. Where it does not, the permission answers granted. */
|
|
36
|
-
available(): boolean;
|
|
36
|
+
get available(): boolean;
|
|
37
37
|
/**
|
|
38
38
|
* The advertising identifier: the IDFA on iOS, the advertising ID on Android. Null on iOS until
|
|
39
39
|
* tracking is allowed, in the simulator, and on Android with ad tracking limited.
|
package/dist/tracking.js
CHANGED
|
@@ -19,10 +19,8 @@
|
|
|
19
19
|
*/
|
|
20
20
|
import { InjectionToken, Service, inject } from '@angular/core';
|
|
21
21
|
import { expoModule } from "./native.js";
|
|
22
|
-
import { Permission } from "./permissions.js";
|
|
22
|
+
import { Permission, UNAVAILABLE } from "./permissions.js";
|
|
23
23
|
import * as i0 from "@angular/core";
|
|
24
|
-
/** The permission answer without the module: nothing granted, and nothing to ask. */
|
|
25
|
-
const UNAVAILABLE = { status: 'denied', granted: false, canAskAgain: false };
|
|
26
24
|
export class Tracking {
|
|
27
25
|
/** Overridden in a test to answer a tracking prompt nobody was shown. */
|
|
28
26
|
static SOURCE = new InjectionToken('angular-native.trackingSource', {
|
|
@@ -33,9 +31,9 @@ export class Tracking {
|
|
|
33
31
|
* The permission to track. `ensure()` shows Apple's dialog only if it has not been answered;
|
|
34
32
|
* the text in it is `NSUserTrackingUsageDescription`, set through the module's config plugin.
|
|
35
33
|
*/
|
|
36
|
-
permission = Permission.of(() => this.native?.getTrackingPermissionsAsync() ?? Promise.resolve(UNAVAILABLE), () => this.native?.requestTrackingPermissionsAsync() ?? Promise.resolve(UNAVAILABLE));
|
|
34
|
+
permission = Permission.of(() => this.native?.getTrackingPermissionsAsync?.() ?? Promise.resolve(UNAVAILABLE), () => this.native?.requestTrackingPermissionsAsync?.() ?? Promise.resolve(UNAVAILABLE));
|
|
37
35
|
/** Whether the device has the tracking API. Where it does not, the permission answers granted. */
|
|
38
|
-
available() {
|
|
36
|
+
get available() {
|
|
39
37
|
return this.native?.isAvailable() ?? false;
|
|
40
38
|
}
|
|
41
39
|
/**
|
package/dist/tracking.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"tracking.js","sourceRoot":"","sources":["../src/tracking.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AACH,OAAO,EAAE,cAAc,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,eAAe,CAAC;AAChE,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AACzC,OAAO,EAAE,UAAU,
|
|
1
|
+
{"version":3,"file":"tracking.js","sourceRoot":"","sources":["../src/tracking.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AACH,OAAO,EAAE,cAAc,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,eAAe,CAAC;AAChE,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AACzC,OAAO,EAAE,UAAU,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAC;;AAc3D,MAAM,OAAO,QAAQ;IACnB,yEAAyE;IACzE,MAAM,CAAU,MAAM,GAAG,IAAI,cAAc,CACzC,+BAA+B,EAC/B;QACE,OAAO,EAAE,GAAG,EAAE,CACZ,UAAU,CACR,4BAA4B,EAC5B,GAAG,EAAE,CAAC,OAAO,CAAC,4BAA4B,CAAS,CACpD;KACJ,CACF,CAAC;IAEe,MAAM,GAAG,MAAM,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC;IAElD;;;OAGG;IACM,UAAU,GAAe,UAAU,CAAC,EAAE,CAC7C,GAAG,EAAE,CAAC,IAAI,CAAC,MAAM,EAAE,2BAA2B,EAAE,EAAE,IAAI,OAAO,CAAC,OAAO,CAAC,WAAW,CAAC,EAClF,GAAG,EAAE,CAAC,IAAI,CAAC,MAAM,EAAE,+BAA+B,EAAE,EAAE,IAAI,OAAO,CAAC,OAAO,CAAC,WAAW,CAAC,CACvF,CAAC;IAEF,kGAAkG;IAClG,IAAI,SAAS;QACX,OAAO,IAAI,CAAC,MAAM,EAAE,WAAW,EAAE,IAAI,KAAK,CAAC;IAC7C,CAAC;IAED;;;OAGG;IACH,aAAa;QACX,OAAO,IAAI,CAAC,MAAM,EAAE,gBAAgB,EAAE,IAAI,IAAI,CAAC;IACjD,CAAC;uGAnCU,QAAQ;wGAAR,QAAQ;;2FAAR,QAAQ;kBADpB,OAAO","sourcesContent":["/**\n * `Tracking`, bound to `expo-tracking-transparency`: Apple's App Tracking Transparency permission.\n *\n * ```ts\n * private readonly tracking = inject(Tracking);\n *\n * async startAnalytics(): Promise<void> {\n * const allowed = await this.tracking.permission.ensure();\n * this.analytics.start({ advertisingId: allowed ? this.tracking.advertisingId() : null });\n * }\n * ```\n *\n * iOS 14.5 and later ask before an app may track the user across other companies' apps and\n * websites, and hand out the advertising identifier only once the user has said yes. The question\n * is asked once: after an answer, the platform shows no dialog again, so `blocked()` is how an app\n * knows to offer Settings instead. Android and the web have no such permission, and the module\n * answers granted there. Without the module installed, the permission is refused and there is no\n * identifier.\n */\nimport { InjectionToken, Service, inject } from '@angular/core';\nimport { expoModule } from './native.ts';\nimport { Permission, UNAVAILABLE } from './permissions.ts';\n\ntype Expo = typeof import('expo-tracking-transparency');\n\n/** The module's functions this service calls. The real module is one; a test provides a fake. */\nexport type NativeTracking = Pick<\n Expo,\n | 'getTrackingPermissionsAsync'\n | 'requestTrackingPermissionsAsync'\n | 'isAvailable'\n | 'getAdvertisingId'\n>;\n\n@Service()\nexport class Tracking {\n /** Overridden in a test to answer a tracking prompt nobody was shown. */\n static readonly SOURCE = new InjectionToken<NativeTracking | null>(\n 'angular-native.trackingSource',\n {\n factory: () =>\n expoModule(\n 'expo-tracking-transparency',\n () => require('expo-tracking-transparency') as Expo,\n ),\n },\n );\n\n private readonly native = inject(Tracking.SOURCE);\n\n /**\n * The permission to track. `ensure()` shows Apple's dialog only if it has not been answered;\n * the text in it is `NSUserTrackingUsageDescription`, set through the module's config plugin.\n */\n readonly permission: Permission = Permission.of(\n () => this.native?.getTrackingPermissionsAsync?.() ?? Promise.resolve(UNAVAILABLE),\n () => this.native?.requestTrackingPermissionsAsync?.() ?? Promise.resolve(UNAVAILABLE),\n );\n\n /** Whether the device has the tracking API. Where it does not, the permission answers granted. */\n get available(): boolean {\n return this.native?.isAvailable() ?? false;\n }\n\n /**\n * The advertising identifier: the IDFA on iOS, the advertising ID on Android. Null on iOS until\n * tracking is allowed, in the simulator, and on Android with ad tracking limited.\n */\n advertisingId(): string | null {\n return this.native?.getAdvertisingId() ?? null;\n }\n}\n"]}
|
package/dist/video.d.ts
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { type Player } from './player.ts';
|
|
2
|
+
/** A video player owned by the current component, released when it is destroyed. */
|
|
3
|
+
export declare function videoPlayer(source: import('expo-video').VideoSource, options?: {
|
|
4
|
+
timeUpdate?: number;
|
|
5
|
+
}): Player<import('expo-video').VideoPlayer>;
|
package/dist/video.js
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `videoPlayer`, an `expo-video` player owned by the component that makes it.
|
|
3
|
+
*
|
|
4
|
+
* ```ts
|
|
5
|
+
* protected readonly player = videoPlayer(this.source, { timeUpdate: 0.5 });
|
|
6
|
+
*
|
|
7
|
+
* // <expo-video [player]="player.native" />
|
|
8
|
+
* // {{ player.state().currentTime }}
|
|
9
|
+
* ```
|
|
10
|
+
*/
|
|
11
|
+
import { expoModule } from "./native.js";
|
|
12
|
+
import { ownPlayer, watchPlayer } from "./player.js";
|
|
13
|
+
/** A video player owned by the current component, released when it is destroyed. */
|
|
14
|
+
export function videoPlayer(source, options = {}) {
|
|
15
|
+
const expo = expoModule('expo-video', () => require('expo-video'), ['ios', 'android', 'web']);
|
|
16
|
+
if (!expo)
|
|
17
|
+
throw new Error('[angular-native] expo-video is not installed');
|
|
18
|
+
const native = expo.createVideoPlayer(source);
|
|
19
|
+
return ownPlayer(native, () => watchPlayer(native, options));
|
|
20
|
+
}
|
|
21
|
+
//# sourceMappingURL=video.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"video.js","sourceRoot":"","sources":["../src/video.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AACH,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AACzC,OAAO,EAAE,SAAS,EAAE,WAAW,EAAe,MAAM,aAAa,CAAC;AAElE,oFAAoF;AACpF,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,MAAM,GAAG,IAAI,CAAC,iBAAiB,CAAC,MAAM,CAAC,CAAC;IAC9C,OAAO,SAAS,CAAC,MAAM,EAAE,GAAG,EAAE,CAAC,WAAW,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC;AAC/D,CAAC","sourcesContent":["/**\n * `videoPlayer`, an `expo-video` player owned by the component that makes it.\n *\n * ```ts\n * protected readonly player = videoPlayer(this.source, { timeUpdate: 0.5 });\n *\n * // <expo-video [player]=\"player.native\" />\n * // {{ player.state().currentTime }}\n * ```\n */\nimport { expoModule } from './native.ts';\nimport { ownPlayer, watchPlayer, type Player } from './player.ts';\n\n/** A video player owned by the current component, released when it is destroyed. */\nexport function videoPlayer(\n source: import('expo-video').VideoSource,\n options: { timeUpdate?: number } = {},\n): Player<import('expo-video').VideoPlayer> {\n const expo = expoModule(\n 'expo-video',\n () => require('expo-video') as typeof import('expo-video'),\n ['ios', 'android', 'web'],\n );\n if (!expo) throw new Error('[angular-native] expo-video is not installed');\n const native = expo.createVideoPlayer(source);\n return ownPlayer(native, () => watchPlayer(native, options));\n}\n"]}
|
package/dist/watch.d.ts
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { InjectionToken, type Signal } from '@angular/core';
|
|
2
|
+
import * as i0 from "@angular/core";
|
|
3
|
+
export type WatchPayload = Record<string, unknown>;
|
|
4
|
+
export interface WatchFileTransfer {
|
|
5
|
+
readonly id: string;
|
|
6
|
+
readonly uri: string;
|
|
7
|
+
readonly metadata: WatchPayload;
|
|
8
|
+
readonly bytesTotal: number;
|
|
9
|
+
readonly bytesTransferred: number;
|
|
10
|
+
readonly fractionCompleted: number;
|
|
11
|
+
readonly startTime: Date;
|
|
12
|
+
readonly endTime: Date | null;
|
|
13
|
+
readonly error: Error | null;
|
|
14
|
+
}
|
|
15
|
+
export interface WatchFileEvent extends WatchFileTransfer {
|
|
16
|
+
readonly type: string;
|
|
17
|
+
}
|
|
18
|
+
export interface WatchError {
|
|
19
|
+
readonly kind: 'activation' | 'application-context' | 'application-context-received' | 'user-info' | 'file-received';
|
|
20
|
+
readonly payload: unknown;
|
|
21
|
+
}
|
|
22
|
+
type Unsubscribe = () => void;
|
|
23
|
+
type Reply = ((response: WatchPayload) => void) | null;
|
|
24
|
+
export interface NativeWatch {
|
|
25
|
+
sendMessage(message: WatchPayload, reply?: (response: WatchPayload) => void, error?: (error: Error) => void): void;
|
|
26
|
+
sendMessageData(data: string): Promise<string>;
|
|
27
|
+
updateApplicationContext(context: WatchPayload): void;
|
|
28
|
+
getApplicationContext(): Promise<WatchPayload | null>;
|
|
29
|
+
transferUserInfo(userInfo: WatchPayload): void;
|
|
30
|
+
transferCurrentComplicationUserInfo(userInfo: WatchPayload): void;
|
|
31
|
+
startFileTransfer(uri: string, metadata?: WatchPayload): Promise<string>;
|
|
32
|
+
getReachability(): Promise<boolean>;
|
|
33
|
+
getIsPaired(): Promise<boolean>;
|
|
34
|
+
getIsWatchAppInstalled(): Promise<boolean>;
|
|
35
|
+
watchEvents: {
|
|
36
|
+
on(event: 'message', listener: (message: WatchPayload, reply: Reply) => void): Unsubscribe;
|
|
37
|
+
on(event: 'application-context', listener: (context: WatchPayload) => void): Unsubscribe;
|
|
38
|
+
on(event: 'user-info', listener: (userInfo: WatchPayload[]) => void): Unsubscribe;
|
|
39
|
+
on(event: 'file', listener: (event: WatchFileEvent) => void): Unsubscribe;
|
|
40
|
+
on(event: 'file-received', listener: (files: WatchPayload[]) => void): Unsubscribe;
|
|
41
|
+
on(event: 'reachability' | 'paired' | 'installed', listener: (value: boolean) => void): Unsubscribe;
|
|
42
|
+
on(event: 'activation-error' | 'application-context-error' | 'application-context-received-error' | 'user-info-error' | 'file-received-error' | 'session-became-inactive' | 'session-did-deactivate', listener: (payload: unknown) => void): Unsubscribe;
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
export type WatchMessageHandler = (message: WatchPayload) => WatchPayload | void | Promise<WatchPayload | void>;
|
|
46
|
+
export declare class Watch {
|
|
47
|
+
/**
|
|
48
|
+
* `react-native-watch-connectivity`, iOS only. It is a TurboModule, which it asks for with
|
|
49
|
+
* `getEnforcing` as it is evaluated, so it is evaluated only once `WatchConnectivity` is
|
|
50
|
+
* registered: in Expo Go, or a build made before it was installed, it would throw an invariant
|
|
51
|
+
* Metro reports as fatal rather than the `MissingModuleError` this answers.
|
|
52
|
+
*/
|
|
53
|
+
static readonly SOURCE: InjectionToken<NativeWatch | null>;
|
|
54
|
+
private readonly native;
|
|
55
|
+
private readonly handlers;
|
|
56
|
+
private readonly errors;
|
|
57
|
+
private destroyed;
|
|
58
|
+
private ready;
|
|
59
|
+
private pendingContext;
|
|
60
|
+
private readonly pendingTransfers;
|
|
61
|
+
private readonly pairedState;
|
|
62
|
+
private readonly installedState;
|
|
63
|
+
private readonly reachableState;
|
|
64
|
+
private readonly messageState;
|
|
65
|
+
private readonly contextState;
|
|
66
|
+
private readonly userInfoState;
|
|
67
|
+
private readonly filesState;
|
|
68
|
+
private readonly transfersState;
|
|
69
|
+
private readonly errorState;
|
|
70
|
+
readonly available: boolean;
|
|
71
|
+
readonly paired: Signal<boolean>;
|
|
72
|
+
readonly installed: Signal<boolean>;
|
|
73
|
+
readonly reachable: Signal<boolean>;
|
|
74
|
+
readonly message: Signal<WatchPayload | null>;
|
|
75
|
+
readonly context: Signal<WatchPayload | null>;
|
|
76
|
+
readonly userInfo: Signal<readonly WatchPayload[]>;
|
|
77
|
+
readonly files: Signal<readonly WatchPayload[]>;
|
|
78
|
+
readonly transfers: Signal<ReadonlyMap<string, WatchFileEvent>>;
|
|
79
|
+
readonly error: Signal<WatchError | null>;
|
|
80
|
+
constructor();
|
|
81
|
+
status(): Promise<{
|
|
82
|
+
paired: boolean;
|
|
83
|
+
installed: boolean;
|
|
84
|
+
reachable: boolean;
|
|
85
|
+
}>;
|
|
86
|
+
send(message: WatchPayload): Promise<WatchPayload>;
|
|
87
|
+
sendData(data: string): Promise<string>;
|
|
88
|
+
onMessage(handler: WatchMessageHandler): Unsubscribe;
|
|
89
|
+
update(context: WatchPayload): void;
|
|
90
|
+
currentContext(): Promise<WatchPayload | null>;
|
|
91
|
+
transfer(userInfo: WatchPayload): void;
|
|
92
|
+
transferComplication(userInfo: WatchPayload): void;
|
|
93
|
+
sendFile(uri: string, metadata?: WatchPayload): Promise<string>;
|
|
94
|
+
private queue;
|
|
95
|
+
private connected;
|
|
96
|
+
private activated;
|
|
97
|
+
private settle;
|
|
98
|
+
private received;
|
|
99
|
+
private answer;
|
|
100
|
+
static ɵfac: i0.ɵɵFactoryDeclaration<Watch, never>;
|
|
101
|
+
static ɵprov: i0.ɵɵInjectableDeclaration<any>;
|
|
102
|
+
}
|
|
103
|
+
export {};
|
package/dist/watch.js
ADDED
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
import { DestroyRef, ErrorHandler, InjectionToken, Service, inject, signal, } from '@angular/core';
|
|
2
|
+
import { expoModule } from "./native.js";
|
|
3
|
+
import * as i0 from "@angular/core";
|
|
4
|
+
const ERRORS = [
|
|
5
|
+
['activation-error', 'activation'],
|
|
6
|
+
['application-context-error', 'application-context'],
|
|
7
|
+
['application-context-received-error', 'application-context-received'],
|
|
8
|
+
['user-info-error', 'user-info'],
|
|
9
|
+
['file-received-error', 'file-received'],
|
|
10
|
+
];
|
|
11
|
+
const SETTLE_DELAYS = [0, 250, 1000, 3000, 10000];
|
|
12
|
+
export class Watch {
|
|
13
|
+
/**
|
|
14
|
+
* `react-native-watch-connectivity`, iOS only. It is a TurboModule, which it asks for with
|
|
15
|
+
* `getEnforcing` as it is evaluated, so it is evaluated only once `WatchConnectivity` is
|
|
16
|
+
* registered: in Expo Go, or a build made before it was installed, it would throw an invariant
|
|
17
|
+
* Metro reports as fatal rather than the `MissingModuleError` this answers.
|
|
18
|
+
*/
|
|
19
|
+
static SOURCE = new InjectionToken('angular-native.watchSource', {
|
|
20
|
+
factory: () => expoModule('react-native-watch-connectivity', () => require('react-native').TurboModuleRegistry?.get('WatchConnectivity')
|
|
21
|
+
? require('react-native-watch-connectivity')
|
|
22
|
+
: null, ['ios']),
|
|
23
|
+
});
|
|
24
|
+
native = inject(Watch.SOURCE);
|
|
25
|
+
handlers = new Set();
|
|
26
|
+
errors = inject(ErrorHandler);
|
|
27
|
+
destroyed = false;
|
|
28
|
+
ready = false;
|
|
29
|
+
pendingContext = null;
|
|
30
|
+
pendingTransfers = [];
|
|
31
|
+
pairedState = signal(false, /* @ts-ignore */
|
|
32
|
+
...(ngDevMode ? [{ debugName: "pairedState" }] : /* istanbul ignore next */ []));
|
|
33
|
+
installedState = signal(false, /* @ts-ignore */
|
|
34
|
+
...(ngDevMode ? [{ debugName: "installedState" }] : /* istanbul ignore next */ []));
|
|
35
|
+
reachableState = signal(false, /* @ts-ignore */
|
|
36
|
+
...(ngDevMode ? [{ debugName: "reachableState" }] : /* istanbul ignore next */ []));
|
|
37
|
+
messageState = signal(null, /* @ts-ignore */
|
|
38
|
+
...(ngDevMode ? [{ debugName: "messageState" }] : /* istanbul ignore next */ []));
|
|
39
|
+
contextState = signal(null, /* @ts-ignore */
|
|
40
|
+
...(ngDevMode ? [{ debugName: "contextState" }] : /* istanbul ignore next */ []));
|
|
41
|
+
userInfoState = signal([], /* @ts-ignore */
|
|
42
|
+
...(ngDevMode ? [{ debugName: "userInfoState" }] : /* istanbul ignore next */ []));
|
|
43
|
+
filesState = signal([], /* @ts-ignore */
|
|
44
|
+
...(ngDevMode ? [{ debugName: "filesState" }] : /* istanbul ignore next */ []));
|
|
45
|
+
transfersState = signal(new Map(), /* @ts-ignore */
|
|
46
|
+
...(ngDevMode ? [{ debugName: "transfersState" }] : /* istanbul ignore next */ []));
|
|
47
|
+
errorState = signal(null, /* @ts-ignore */
|
|
48
|
+
...(ngDevMode ? [{ debugName: "errorState" }] : /* istanbul ignore next */ []));
|
|
49
|
+
available = this.native !== null;
|
|
50
|
+
paired = this.pairedState.asReadonly();
|
|
51
|
+
installed = this.installedState.asReadonly();
|
|
52
|
+
reachable = this.reachableState.asReadonly();
|
|
53
|
+
message = this.messageState.asReadonly();
|
|
54
|
+
context = this.contextState.asReadonly();
|
|
55
|
+
userInfo = this.userInfoState.asReadonly();
|
|
56
|
+
files = this.filesState.asReadonly();
|
|
57
|
+
transfers = this.transfersState.asReadonly();
|
|
58
|
+
error = this.errorState.asReadonly();
|
|
59
|
+
constructor() {
|
|
60
|
+
const native = this.native;
|
|
61
|
+
if (!native)
|
|
62
|
+
return;
|
|
63
|
+
const events = native.watchEvents;
|
|
64
|
+
const stops = [
|
|
65
|
+
events.on('reachability', (reachable) => {
|
|
66
|
+
if (reachable)
|
|
67
|
+
this.connected();
|
|
68
|
+
else
|
|
69
|
+
this.reachableState.set(false);
|
|
70
|
+
void this.status().catch(() => { });
|
|
71
|
+
}),
|
|
72
|
+
events.on('paired', (paired) => {
|
|
73
|
+
this.pairedState.set(paired);
|
|
74
|
+
// A watch paired after `settle` stopped asking: the session is active, so send what it held.
|
|
75
|
+
if (paired)
|
|
76
|
+
this.activated();
|
|
77
|
+
}),
|
|
78
|
+
events.on('installed', (installed) => this.installedState.set(installed)),
|
|
79
|
+
events.on('message', (message, reply) => this.received(message, reply)),
|
|
80
|
+
events.on('application-context', (context) => this.contextState.set(context)),
|
|
81
|
+
events.on('user-info', (userInfo) => this.userInfoState.update((received) => [...received, ...userInfo])),
|
|
82
|
+
events.on('file-received', (files) => this.filesState.set(files)),
|
|
83
|
+
events.on('file', (event) => this.transfersState.update((transfers) => new Map(transfers).set(event.id, event))),
|
|
84
|
+
events.on('session-became-inactive', () => this.reachableState.set(false)),
|
|
85
|
+
events.on('session-did-deactivate', () => this.reachableState.set(false)),
|
|
86
|
+
...ERRORS.map(([event, kind]) => events.on(event, (payload) => this.errorState.set({ kind, payload }))),
|
|
87
|
+
];
|
|
88
|
+
inject(DestroyRef).onDestroy(() => {
|
|
89
|
+
this.destroyed = true;
|
|
90
|
+
stops.forEach((stop) => stop());
|
|
91
|
+
});
|
|
92
|
+
void this.settle();
|
|
93
|
+
native.getApplicationContext().then((context) => context && this.contextState.set(context), () => { });
|
|
94
|
+
}
|
|
95
|
+
async status() {
|
|
96
|
+
const native = this.native;
|
|
97
|
+
if (!native)
|
|
98
|
+
return { paired: false, installed: false, reachable: false };
|
|
99
|
+
const [paired, installed, reachable] = await Promise.all([
|
|
100
|
+
native.getIsPaired(),
|
|
101
|
+
native.getIsWatchAppInstalled(),
|
|
102
|
+
native.getReachability(),
|
|
103
|
+
]);
|
|
104
|
+
if (reachable) {
|
|
105
|
+
this.connected();
|
|
106
|
+
return { paired: true, installed: true, reachable };
|
|
107
|
+
}
|
|
108
|
+
this.pairedState.set(paired);
|
|
109
|
+
this.installedState.set(installed);
|
|
110
|
+
this.reachableState.set(false);
|
|
111
|
+
if (paired)
|
|
112
|
+
this.activated();
|
|
113
|
+
return { paired, installed, reachable };
|
|
114
|
+
}
|
|
115
|
+
send(message) {
|
|
116
|
+
const native = this.native;
|
|
117
|
+
if (!native)
|
|
118
|
+
return Promise.reject(unavailable());
|
|
119
|
+
return new Promise((resolve, reject) => native.sendMessage(message, (response) => {
|
|
120
|
+
this.connected();
|
|
121
|
+
resolve(response);
|
|
122
|
+
}, (error) => {
|
|
123
|
+
this.reachableState.set(false);
|
|
124
|
+
reject(error);
|
|
125
|
+
}));
|
|
126
|
+
}
|
|
127
|
+
sendData(data) {
|
|
128
|
+
return this.native ? this.native.sendMessageData(data) : Promise.reject(unavailable());
|
|
129
|
+
}
|
|
130
|
+
onMessage(handler) {
|
|
131
|
+
this.handlers.add(handler);
|
|
132
|
+
return () => this.handlers.delete(handler);
|
|
133
|
+
}
|
|
134
|
+
update(context) {
|
|
135
|
+
if (!this.native)
|
|
136
|
+
return;
|
|
137
|
+
if (this.ready)
|
|
138
|
+
this.native.updateApplicationContext(context);
|
|
139
|
+
else
|
|
140
|
+
this.pendingContext = context;
|
|
141
|
+
}
|
|
142
|
+
currentContext() {
|
|
143
|
+
return this.native ? this.native.getApplicationContext() : Promise.resolve(null);
|
|
144
|
+
}
|
|
145
|
+
transfer(userInfo) {
|
|
146
|
+
this.queue(false, userInfo);
|
|
147
|
+
}
|
|
148
|
+
transferComplication(userInfo) {
|
|
149
|
+
this.queue(true, userInfo);
|
|
150
|
+
}
|
|
151
|
+
sendFile(uri, metadata = {}) {
|
|
152
|
+
return this.native
|
|
153
|
+
? this.native.startFileTransfer(uri, metadata)
|
|
154
|
+
: Promise.reject(unavailable());
|
|
155
|
+
}
|
|
156
|
+
queue(complication, userInfo) {
|
|
157
|
+
const native = this.native;
|
|
158
|
+
if (!native)
|
|
159
|
+
return;
|
|
160
|
+
if (!this.ready)
|
|
161
|
+
this.pendingTransfers.push([complication, userInfo]);
|
|
162
|
+
else if (complication)
|
|
163
|
+
native.transferCurrentComplicationUserInfo(userInfo);
|
|
164
|
+
else
|
|
165
|
+
native.transferUserInfo(userInfo);
|
|
166
|
+
}
|
|
167
|
+
connected() {
|
|
168
|
+
this.pairedState.set(true);
|
|
169
|
+
this.installedState.set(true);
|
|
170
|
+
this.reachableState.set(true);
|
|
171
|
+
this.activated();
|
|
172
|
+
}
|
|
173
|
+
activated() {
|
|
174
|
+
if (this.ready)
|
|
175
|
+
return;
|
|
176
|
+
this.ready = true;
|
|
177
|
+
if (this.pendingContext)
|
|
178
|
+
this.update(this.pendingContext);
|
|
179
|
+
this.pendingContext = null;
|
|
180
|
+
for (const [complication, userInfo] of this.pendingTransfers.splice(0)) {
|
|
181
|
+
this.queue(complication, userInfo);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
async settle() {
|
|
185
|
+
for (const delay of SETTLE_DELAYS) {
|
|
186
|
+
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
187
|
+
if (this.destroyed)
|
|
188
|
+
return;
|
|
189
|
+
const status = await this.status().catch(() => null);
|
|
190
|
+
if (status?.paired)
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
received(message, reply) {
|
|
195
|
+
this.connected();
|
|
196
|
+
this.messageState.set(message);
|
|
197
|
+
if (!reply) {
|
|
198
|
+
for (const handler of this.handlers) {
|
|
199
|
+
void Promise.resolve()
|
|
200
|
+
.then(() => handler(message))
|
|
201
|
+
.catch((error) => this.errors.handleError(error));
|
|
202
|
+
}
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
void this.answer(message).then(reply, (error) => {
|
|
206
|
+
this.errors.handleError(error);
|
|
207
|
+
reply({});
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
async answer(message) {
|
|
211
|
+
let response = {};
|
|
212
|
+
for (const handler of this.handlers) {
|
|
213
|
+
const result = await handler(message);
|
|
214
|
+
if (result)
|
|
215
|
+
response = { ...response, ...result };
|
|
216
|
+
}
|
|
217
|
+
return response;
|
|
218
|
+
}
|
|
219
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.2.0", ngImport: i0, type: Watch, deps: [], target: i0.ɵɵFactoryTarget.Service });
|
|
220
|
+
static ɵprov = i0.ɵɵngDeclareService({ minVersion: "22.0.0", version: "22.2.0", ngImport: i0, type: Watch });
|
|
221
|
+
}
|
|
222
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.2.0", ngImport: i0, type: Watch, decorators: [{
|
|
223
|
+
type: Service
|
|
224
|
+
}], ctorParameters: () => [] });
|
|
225
|
+
function unavailable() {
|
|
226
|
+
return new Error('No paired Apple Watch session: react-native-watch-connectivity is not installed, or this is not iOS.');
|
|
227
|
+
}
|
|
228
|
+
//# sourceMappingURL=watch.js.map
|