@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.
Files changed (62) hide show
  1. package/README.md +32 -382
  2. package/dist/async-storage.d.ts +15 -0
  3. package/dist/async-storage.js +29 -0
  4. package/dist/async-storage.js.map +1 -0
  5. package/dist/audio.d.ts +10 -0
  6. package/dist/audio.js +26 -0
  7. package/dist/audio.js.map +1 -0
  8. package/dist/expo-ui-components.d.ts +142 -10
  9. package/dist/expo-ui-components.js +362 -39
  10. package/dist/expo-ui-components.js.map +1 -1
  11. package/dist/expo-ui.js +9 -6
  12. package/dist/expo-ui.js.map +1 -1
  13. package/dist/foldable.d.ts +78 -0
  14. package/dist/foldable.js +87 -0
  15. package/dist/foldable.js.map +1 -0
  16. package/dist/fonts.d.ts +3 -0
  17. package/dist/fonts.js +7 -2
  18. package/dist/fonts.js.map +1 -1
  19. package/dist/image-picker.js +3 -9
  20. package/dist/image-picker.js.map +1 -1
  21. package/dist/index.d.ts +1 -1
  22. package/dist/index.js +1 -1
  23. package/dist/index.js.map +1 -1
  24. package/dist/live-activity.d.ts +88 -0
  25. package/dist/live-activity.js +150 -0
  26. package/dist/live-activity.js.map +1 -0
  27. package/dist/location.js +2 -7
  28. package/dist/location.js.map +1 -1
  29. package/dist/media-library.js +1 -3
  30. package/dist/media-library.js.map +1 -1
  31. package/dist/native.d.ts +4 -1
  32. package/dist/native.js +60 -4
  33. package/dist/native.js.map +1 -1
  34. package/dist/notifications.js +2 -4
  35. package/dist/notifications.js.map +1 -1
  36. package/dist/permissions.d.ts +2 -0
  37. package/dist/permissions.js +6 -0
  38. package/dist/permissions.js.map +1 -1
  39. package/dist/player.d.ts +24 -11
  40. package/dist/player.js +7 -26
  41. package/dist/player.js.map +1 -1
  42. package/dist/register-expo-view.d.ts +5 -0
  43. package/dist/register-expo-view.js +5 -1
  44. package/dist/register-expo-view.js.map +1 -1
  45. package/dist/screen-capture.js +2 -4
  46. package/dist/screen-capture.js.map +1 -1
  47. package/dist/secure-store.d.ts +15 -0
  48. package/dist/secure-store.js +30 -0
  49. package/dist/secure-store.js.map +1 -0
  50. package/dist/store.d.ts +5 -10
  51. package/dist/store.js +5 -36
  52. package/dist/store.js.map +1 -1
  53. package/dist/tracking.d.ts +1 -1
  54. package/dist/tracking.js +3 -5
  55. package/dist/tracking.js.map +1 -1
  56. package/dist/video.d.ts +5 -0
  57. package/dist/video.js +21 -0
  58. package/dist/video.js.map +1 -0
  59. package/dist/watch.d.ts +103 -0
  60. package/dist/watch.js +228 -0
  61. package/dist/watch.js.map +1 -0
  62. package/package.json +49 -4
@@ -12,14 +12,8 @@
12
12
  */
13
13
  import { InjectionToken, Service, inject } from '@angular/core';
14
14
  import { expoModule } from "./native.js";
15
- import { Permission } from "./permissions.js";
15
+ import { Permission, UNAVAILABLE } from "./permissions.js";
16
16
  import * as i0 from "@angular/core";
17
- /** What a permission is without the module: refused, and never to be asked about. */
18
- const UNAVAILABLE = async () => ({
19
- status: 'denied',
20
- granted: false,
21
- canAskAgain: false,
22
- });
23
17
  export class ImagePicker {
24
18
  /** Overridden in a test to pick photos without a photo library. */
25
19
  static SOURCE = new InjectionToken('angular-native.imagePickerSource', {
@@ -31,9 +25,9 @@ export class ImagePicker {
31
25
  * The photo library. The system picker needs no permission to pick from, so `pick()` never
32
26
  * asks; this is for an app that reads the library itself.
33
27
  */
34
- libraryPermission = Permission.of(this.native?.getMediaLibraryPermissionsAsync ?? UNAVAILABLE, this.native?.requestMediaLibraryPermissionsAsync ?? UNAVAILABLE);
28
+ libraryPermission = Permission.of(() => this.native?.getMediaLibraryPermissionsAsync?.() ?? Promise.resolve(UNAVAILABLE), () => this.native?.requestMediaLibraryPermissionsAsync?.() ?? Promise.resolve(UNAVAILABLE));
35
29
  /** The camera, which `capture()` asks for itself. */
36
- cameraPermission = Permission.of(this.native?.getCameraPermissionsAsync ?? UNAVAILABLE, this.native?.requestCameraPermissionsAsync ?? UNAVAILABLE);
30
+ cameraPermission = Permission.of(() => this.native?.getCameraPermissionsAsync?.() ?? Promise.resolve(UNAVAILABLE), () => this.native?.requestCameraPermissionsAsync?.() ?? Promise.resolve(UNAVAILABLE));
37
31
  /** Pick from the photo library. Empty if they cancelled. */
38
32
  async pick(options) {
39
33
  if (!this.native)
@@ -1 +1 @@
1
- {"version":3,"file":"image-picker.js","sourceRoot":"","sources":["../src/image-picker.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AACH,OAAO,EAAE,cAAc,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,eAAe,CAAC;AAChE,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AACzC,OAAO,EAAE,UAAU,EAA2B,MAAM,kBAAkB,CAAC;;AAoBvE,qFAAqF;AACrF,MAAM,WAAW,GAAG,KAAK,IAAiC,EAAE,CAAC,CAAC;IAC5D,MAAM,EAAE,QAAQ;IAChB,OAAO,EAAE,KAAK;IACd,WAAW,EAAE,KAAK;CACnB,CAAC,CAAC;AAGH,MAAM,OAAO,WAAW;IACtB,mEAAmE;IACnE,MAAM,CAAU,MAAM,GAAG,IAAI,cAAc,CACzC,kCAAkC,EAClC;QACE,OAAO,EAAE,GAAG,EAAE,CACZ,UAAU,CAAC,mBAAmB,EAAE,GAAG,EAAE,CAAC,OAAO,CAAC,mBAAmB,CAAsB,CAAC;YACxF,IAAI;KACP,CACF,CAAC;IAEe,MAAM,GAAG,MAAM,CAAC,WAAW,CAAC,MAAM,CAAC,CAAC;IAErD;;;OAGG;IACM,iBAAiB,GAAG,UAAU,CAAC,EAAE,CACxC,IAAI,CAAC,MAAM,EAAE,+BAA+B,IAAI,WAAW,EAC3D,IAAI,CAAC,MAAM,EAAE,mCAAmC,IAAI,WAAW,CAChE,CAAC;IAEF,qDAAqD;IAC5C,gBAAgB,GAAG,UAAU,CAAC,EAAE,CACvC,IAAI,CAAC,MAAM,EAAE,yBAAyB,IAAI,WAAW,EACrD,IAAI,CAAC,MAAM,EAAE,6BAA6B,IAAI,WAAW,CAC1D,CAAC;IAEF,4DAA4D;IAC5D,KAAK,CAAC,IAAI,CAAC,OAAuB;QAChC,IAAI,CAAC,IAAI,CAAC,MAAM;YAAE,OAAO,EAAE,CAAC;QAC5B,OAAO,QAAQ,CAAC,MAAM,IAAI,CAAC,MAAM,CAAC,uBAAuB,CAAC,OAAO,CAAC,CAAC,CAAC;IACtE,CAAC;IAED,2FAA2F;IAC3F,KAAK,CAAC,OAAO,CAAC,OAAuB;QACnC,IAAI,CAAC,IAAI,CAAC,MAAM,IAAI,CAAC,CAAC,MAAM,IAAI,CAAC,gBAAgB,CAAC,MAAM,EAAE,CAAC;YAAE,OAAO,EAAE,CAAC;QACvE,OAAO,QAAQ,CAAC,MAAM,IAAI,CAAC,MAAM,CAAC,iBAAiB,CAAC,OAAO,CAAC,CAAC,CAAC;IAChE,CAAC;uGAtCU,WAAW;wGAAX,WAAW;;2FAAX,WAAW;kBADvB,OAAO;;AA0CR,SAAS,QAAQ,CAAC,MAAoB;IACpC,OAAO,MAAM,CAAC,QAAQ,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,MAAM,IAAI,EAAE,CAAC,CAAC;AACtD,CAAC","sourcesContent":["/**\n * `ImagePicker`, bound to `expo-image-picker`.\n *\n * ```ts\n * private readonly picker = inject(ImagePicker);\n * const [photo] = await this.picker.pick({ mediaTypes: ['images'] });\n * ```\n *\n * The options are Expo's own, passed through unchanged. What this adds is the two things every\n * call site otherwise writes: a cancelled picker answers with no assets rather than a result to\n * unwrap, and `capture()` asks for the camera before opening it, where Expo's would reject.\n */\nimport { InjectionToken, Service, inject } from '@angular/core';\nimport { expoModule } from './native.ts';\nimport { Permission, type PermissionResponse } from './permissions.ts';\n\nexport type PickerOptions = import('expo-image-picker').ImagePickerOptions;\nexport type PickedAsset = import('expo-image-picker').ImagePickerAsset;\n\n/** The slice of `expo-image-picker` this needs. */\nexport interface NativeImagePicker {\n launchImageLibraryAsync(options?: PickerOptions): Promise<PickerResult>;\n launchCameraAsync(options?: PickerOptions): Promise<PickerResult>;\n getMediaLibraryPermissionsAsync(): Promise<PermissionResponse>;\n requestMediaLibraryPermissionsAsync(): Promise<PermissionResponse>;\n getCameraPermissionsAsync(): Promise<PermissionResponse>;\n requestCameraPermissionsAsync(): Promise<PermissionResponse>;\n}\n\ninterface PickerResult {\n readonly canceled: boolean;\n readonly assets: readonly PickedAsset[] | null;\n}\n\n/** What a permission is without the module: refused, and never to be asked about. */\nconst UNAVAILABLE = async (): Promise<PermissionResponse> => ({\n status: 'denied',\n granted: false,\n canAskAgain: false,\n});\n\n@Service()\nexport class ImagePicker {\n /** Overridden in a test to pick photos without a photo library. */\n static readonly SOURCE = new InjectionToken<NativeImagePicker | null>(\n 'angular-native.imagePickerSource',\n {\n factory: () =>\n expoModule('expo-image-picker', () => require('expo-image-picker') as NativeImagePicker) ??\n null,\n },\n );\n\n private readonly native = inject(ImagePicker.SOURCE);\n\n /**\n * The photo library. The system picker needs no permission to pick from, so `pick()` never\n * asks; this is for an app that reads the library itself.\n */\n readonly libraryPermission = Permission.of(\n this.native?.getMediaLibraryPermissionsAsync ?? UNAVAILABLE,\n this.native?.requestMediaLibraryPermissionsAsync ?? UNAVAILABLE,\n );\n\n /** The camera, which `capture()` asks for itself. */\n readonly cameraPermission = Permission.of(\n this.native?.getCameraPermissionsAsync ?? UNAVAILABLE,\n this.native?.requestCameraPermissionsAsync ?? UNAVAILABLE,\n );\n\n /** Pick from the photo library. Empty if they cancelled. */\n async pick(options?: PickerOptions): Promise<readonly PickedAsset[]> {\n if (!this.native) return [];\n return assetsOf(await this.native.launchImageLibraryAsync(options));\n }\n\n /** Take a photo or video with the system camera. Empty if they cancelled or refused it. */\n async capture(options?: PickerOptions): Promise<readonly PickedAsset[]> {\n if (!this.native || !(await this.cameraPermission.ensure())) return [];\n return assetsOf(await this.native.launchCameraAsync(options));\n }\n}\n\nfunction assetsOf(result: PickerResult): readonly PickedAsset[] {\n return result.canceled ? [] : (result.assets ?? []);\n}\n"]}
1
+ {"version":3,"file":"image-picker.js","sourceRoot":"","sources":["../src/image-picker.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;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,EAA2B,MAAM,kBAAkB,CAAC;;AAqBpF,MAAM,OAAO,WAAW;IACtB,mEAAmE;IACnE,MAAM,CAAU,MAAM,GAAG,IAAI,cAAc,CACzC,kCAAkC,EAClC;QACE,OAAO,EAAE,GAAG,EAAE,CACZ,UAAU,CAAC,mBAAmB,EAAE,GAAG,EAAE,CAAC,OAAO,CAAC,mBAAmB,CAAsB,CAAC;YACxF,IAAI;KACP,CACF,CAAC;IAEe,MAAM,GAAG,MAAM,CAAC,WAAW,CAAC,MAAM,CAAC,CAAC;IAErD;;;OAGG;IACM,iBAAiB,GAAG,UAAU,CAAC,EAAE,CACxC,GAAG,EAAE,CAAC,IAAI,CAAC,MAAM,EAAE,+BAA+B,EAAE,EAAE,IAAI,OAAO,CAAC,OAAO,CAAC,WAAW,CAAC,EACtF,GAAG,EAAE,CAAC,IAAI,CAAC,MAAM,EAAE,mCAAmC,EAAE,EAAE,IAAI,OAAO,CAAC,OAAO,CAAC,WAAW,CAAC,CAC3F,CAAC;IAEF,qDAAqD;IAC5C,gBAAgB,GAAG,UAAU,CAAC,EAAE,CACvC,GAAG,EAAE,CAAC,IAAI,CAAC,MAAM,EAAE,yBAAyB,EAAE,EAAE,IAAI,OAAO,CAAC,OAAO,CAAC,WAAW,CAAC,EAChF,GAAG,EAAE,CAAC,IAAI,CAAC,MAAM,EAAE,6BAA6B,EAAE,EAAE,IAAI,OAAO,CAAC,OAAO,CAAC,WAAW,CAAC,CACrF,CAAC;IAEF,4DAA4D;IAC5D,KAAK,CAAC,IAAI,CAAC,OAAuB;QAChC,IAAI,CAAC,IAAI,CAAC,MAAM;YAAE,OAAO,EAAE,CAAC;QAC5B,OAAO,QAAQ,CAAC,MAAM,IAAI,CAAC,MAAM,CAAC,uBAAuB,CAAC,OAAO,CAAC,CAAC,CAAC;IACtE,CAAC;IAED,2FAA2F;IAC3F,KAAK,CAAC,OAAO,CAAC,OAAuB;QACnC,IAAI,CAAC,IAAI,CAAC,MAAM,IAAI,CAAC,CAAC,MAAM,IAAI,CAAC,gBAAgB,CAAC,MAAM,EAAE,CAAC;YAAE,OAAO,EAAE,CAAC;QACvE,OAAO,QAAQ,CAAC,MAAM,IAAI,CAAC,MAAM,CAAC,iBAAiB,CAAC,OAAO,CAAC,CAAC,CAAC;IAChE,CAAC;uGAtCU,WAAW;wGAAX,WAAW;;2FAAX,WAAW;kBADvB,OAAO;;AA0CR,SAAS,QAAQ,CAAC,MAAoB;IACpC,OAAO,MAAM,CAAC,QAAQ,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,MAAM,IAAI,EAAE,CAAC,CAAC;AACtD,CAAC","sourcesContent":["/**\n * `ImagePicker`, bound to `expo-image-picker`.\n *\n * ```ts\n * private readonly picker = inject(ImagePicker);\n * const [photo] = await this.picker.pick({ mediaTypes: ['images'] });\n * ```\n *\n * The options are Expo's own, passed through unchanged. What this adds is the two things every\n * call site otherwise writes: a cancelled picker answers with no assets rather than a result to\n * unwrap, and `capture()` asks for the camera before opening it, where Expo's would reject.\n */\nimport { InjectionToken, Service, inject } from '@angular/core';\nimport { expoModule } from './native.ts';\nimport { Permission, UNAVAILABLE, type PermissionResponse } from './permissions.ts';\n\nexport type PickerOptions = import('expo-image-picker').ImagePickerOptions;\nexport type PickedAsset = import('expo-image-picker').ImagePickerAsset;\n\n/** The slice of `expo-image-picker` this needs. */\nexport interface NativeImagePicker {\n launchImageLibraryAsync(options?: PickerOptions): Promise<PickerResult>;\n launchCameraAsync(options?: PickerOptions): Promise<PickerResult>;\n getMediaLibraryPermissionsAsync(): Promise<PermissionResponse>;\n requestMediaLibraryPermissionsAsync(): Promise<PermissionResponse>;\n getCameraPermissionsAsync(): Promise<PermissionResponse>;\n requestCameraPermissionsAsync(): Promise<PermissionResponse>;\n}\n\ninterface PickerResult {\n readonly canceled: boolean;\n readonly assets: readonly PickedAsset[] | null;\n}\n\n@Service()\nexport class ImagePicker {\n /** Overridden in a test to pick photos without a photo library. */\n static readonly SOURCE = new InjectionToken<NativeImagePicker | null>(\n 'angular-native.imagePickerSource',\n {\n factory: () =>\n expoModule('expo-image-picker', () => require('expo-image-picker') as NativeImagePicker) ??\n null,\n },\n );\n\n private readonly native = inject(ImagePicker.SOURCE);\n\n /**\n * The photo library. The system picker needs no permission to pick from, so `pick()` never\n * asks; this is for an app that reads the library itself.\n */\n readonly libraryPermission = Permission.of(\n () => this.native?.getMediaLibraryPermissionsAsync?.() ?? Promise.resolve(UNAVAILABLE),\n () => this.native?.requestMediaLibraryPermissionsAsync?.() ?? Promise.resolve(UNAVAILABLE),\n );\n\n /** The camera, which `capture()` asks for itself. */\n readonly cameraPermission = Permission.of(\n () => this.native?.getCameraPermissionsAsync?.() ?? Promise.resolve(UNAVAILABLE),\n () => this.native?.requestCameraPermissionsAsync?.() ?? Promise.resolve(UNAVAILABLE),\n );\n\n /** Pick from the photo library. Empty if they cancelled. */\n async pick(options?: PickerOptions): Promise<readonly PickedAsset[]> {\n if (!this.native) return [];\n return assetsOf(await this.native.launchImageLibraryAsync(options));\n }\n\n /** Take a photo or video with the system camera. Empty if they cancelled or refused it. */\n async capture(options?: PickerOptions): Promise<readonly PickedAsset[]> {\n if (!this.native || !(await this.cameraPermission.ensure())) return [];\n return assetsOf(await this.native.launchCameraAsync(options));\n }\n}\n\nfunction assetsOf(result: PickerResult): readonly PickedAsset[] {\n return result.canceled ? [] : (result.assets ?? []);\n}\n"]}
package/dist/index.d.ts CHANGED
@@ -13,7 +13,7 @@ export { NATIVE_VIEWS, registerNativeViews } from './community-views.ts';
13
13
  export { registerExpoUiViews } from './expo-ui.ts';
14
14
  export { nativeState, type NativeState } from './native-state.ts';
15
15
  export { EXPO_VIEWS, expoViewName, registerExpoView, registerExpoViews, type ExpoViewOptions, } from './register-expo-view.ts';
16
- export { UiButton, UiDatePicker, UiDivider, UiHost, UiImage, UiMenu, UiPicker, UiColorPicker, type UiColorChangeEvent, UiForm, UiGauge, UiHStack, UiLabeledContent, UiList, UiProgress, UiSection, UiSpacer, UiStepper, type UiStepperChangeEvent, UiTextField, type UiTextFieldChangeEvent, UiToggle, type UiToggleChangeEvent, UiSlider, type UiSliderChangeEvent, UiSlot, UiSwipeActions, UiText, UiVStack, type UiDateChangeEvent, type UiPickerOption, type UiModifier, } from './expo-ui-components.ts';
16
+ export { UiAccessoryWidgetBackground, UiButton, UiCapsule, UiCircle, UiEllipse, UiLabel, UiLink, UiRectangle, UiRoundedRectangle, UiUnevenRoundedRectangle, UiZStack, type UiZStackAlignment, UiDatePicker, UiDivider, UiHost, UiImage, UiMenu, UiPicker, UiColorPicker, type UiColorChangeEvent, UiForm, UiGauge, UiHStack, UiLabeledContent, UiList, UiProgress, UiSection, UiSpacer, UiStepper, type UiStepperChangeEvent, UiTextField, type UiTextFieldChangeEvent, UiToggle, type UiToggleChangeEvent, UiSlider, type UiSliderChangeEvent, UiSlot, UiSwipeActions, UiText, UiVStack, type UiDateChangeEvent, type UiPickerOption, type UiModifier, } from './expo-ui-components.ts';
17
17
  export { ExpoImage, type ExpoImageContentFit, type ExpoImageSource } from './expo-image.ts';
18
18
  export { ExpoGlass, ExpoGlassContainer, liquidGlassAvailable, type GlassEffectStyleConfig, type GlassStyle, } from './glass.ts';
19
19
  export { ExpoSymbol, type SymbolAnimationSpec, type SymbolResizeMode, type SymbolScale, type SymbolType, type SymbolWeight, } from './symbol.ts';
package/dist/index.js CHANGED
@@ -13,7 +13,7 @@ export { NATIVE_VIEWS, registerNativeViews } from "./community-views.js";
13
13
  export { registerExpoUiViews } from "./expo-ui.js";
14
14
  export { nativeState } from "./native-state.js";
15
15
  export { EXPO_VIEWS, expoViewName, registerExpoView, registerExpoViews, } from "./register-expo-view.js";
16
- export { UiButton, UiDatePicker, UiDivider, UiHost, UiImage, UiMenu, UiPicker, UiColorPicker, UiForm, UiGauge, UiHStack, UiLabeledContent, UiList, UiProgress, UiSection, UiSpacer, UiStepper, UiTextField, UiToggle, UiSlider, UiSlot, UiSwipeActions, UiText, UiVStack, } from "./expo-ui-components.js";
16
+ export { UiAccessoryWidgetBackground, UiButton, UiCapsule, UiCircle, UiEllipse, UiLabel, UiLink, UiRectangle, UiRoundedRectangle, UiUnevenRoundedRectangle, UiZStack, UiDatePicker, UiDivider, UiHost, UiImage, UiMenu, UiPicker, UiColorPicker, UiForm, UiGauge, UiHStack, UiLabeledContent, UiList, UiProgress, UiSection, UiSpacer, UiStepper, UiTextField, UiToggle, UiSlider, UiSlot, UiSwipeActions, UiText, UiVStack, } from "./expo-ui-components.js";
17
17
  export { ExpoImage } from "./expo-image.js";
18
18
  export { ExpoGlass, ExpoGlassContainer, liquidGlassAvailable, } from "./glass.js";
19
19
  export { ExpoSymbol, } from "./symbol.js";
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,OAAO,EAAE,QAAQ,EAAiB,MAAM,eAAe,CAAC;AACxD,OAAO,EAAE,UAAU,EAA+C,MAAM,kBAAkB,CAAC;AAC3F,OAAO,EAAE,kBAAkB,EAAE,UAAU,EAAE,QAAQ,EAAuB,MAAM,aAAa,CAAC;AAC5F,OAAO,EAAE,YAAY,EAAE,mBAAmB,EAAE,MAAM,sBAAsB,CAAC;AACzE,OAAO,EAAE,mBAAmB,EAAE,MAAM,cAAc,CAAC;AACnD,OAAO,EAAE,WAAW,EAAoB,MAAM,mBAAmB,CAAC;AAClE,OAAO,EACL,UAAU,EACV,YAAY,EACZ,gBAAgB,EAChB,iBAAiB,GAElB,MAAM,yBAAyB,CAAC;AACjC,OAAO,EACL,QAAQ,EACR,YAAY,EACZ,SAAS,EACT,MAAM,EACN,OAAO,EACP,MAAM,EACN,QAAQ,EACR,aAAa,EAEb,MAAM,EACN,OAAO,EACP,QAAQ,EACR,gBAAgB,EAChB,MAAM,EACN,UAAU,EACV,SAAS,EACT,QAAQ,EACR,SAAS,EAET,WAAW,EAEX,QAAQ,EAER,QAAQ,EAER,MAAM,EACN,cAAc,EACd,MAAM,EACN,QAAQ,GAIT,MAAM,yBAAyB,CAAC;AACjC,OAAO,EAAE,SAAS,EAAkD,MAAM,iBAAiB,CAAC;AAC5F,OAAO,EACL,SAAS,EACT,kBAAkB,EAClB,oBAAoB,GAGrB,MAAM,YAAY,CAAC;AACpB,OAAO,EACL,UAAU,GAMX,MAAM,aAAa,CAAC;AACrB,OAAO,EACL,gBAAgB,GAGjB,MAAM,wBAAwB,CAAC","sourcesContent":["/**\n * What the package exports without naming a module.\n *\n * Everything with an Expo module behind it has its own entry point - `@ng-native/expo/\n * battery.ts` and friends - so that importing haptics does not make an app install the video\n * player. What is left here is the shared machinery: the permission shape every module spells\n * identically, the signal-from-a-listener those services are built out of, and the view names.\n */\nexport { observed, type Observed } from './observed.ts';\nexport { Permission, type PermissionApi, type PermissionResponse } from './permissions.ts';\nexport { MissingModuleError, expoModule, optional, type ModulePlatform } from './native.ts';\nexport { NATIVE_VIEWS, registerNativeViews } from './community-views.ts';\nexport { registerExpoUiViews } from './expo-ui.ts';\nexport { nativeState, type NativeState } from './native-state.ts';\nexport {\n EXPO_VIEWS,\n expoViewName,\n registerExpoView,\n registerExpoViews,\n type ExpoViewOptions,\n} from './register-expo-view.ts';\nexport {\n UiButton,\n UiDatePicker,\n UiDivider,\n UiHost,\n UiImage,\n UiMenu,\n UiPicker,\n UiColorPicker,\n type UiColorChangeEvent,\n UiForm,\n UiGauge,\n UiHStack,\n UiLabeledContent,\n UiList,\n UiProgress,\n UiSection,\n UiSpacer,\n UiStepper,\n type UiStepperChangeEvent,\n UiTextField,\n type UiTextFieldChangeEvent,\n UiToggle,\n type UiToggleChangeEvent,\n UiSlider,\n type UiSliderChangeEvent,\n UiSlot,\n UiSwipeActions,\n UiText,\n UiVStack,\n type UiDateChangeEvent,\n type UiPickerOption,\n type UiModifier,\n} from './expo-ui-components.ts';\nexport { ExpoImage, type ExpoImageContentFit, type ExpoImageSource } from './expo-image.ts';\nexport {\n ExpoGlass,\n ExpoGlassContainer,\n liquidGlassAvailable,\n type GlassEffectStyleConfig,\n type GlassStyle,\n} from './glass.ts';\nexport {\n ExpoSymbol,\n type SymbolAnimationSpec,\n type SymbolResizeMode,\n type SymbolScale,\n type SymbolType,\n type SymbolWeight,\n} from './symbol.ts';\nexport {\n SegmentedControl,\n type SegmentedControlChangeEvent,\n type SegmentedControlFont,\n} from './segmented-control.ts';\n"]}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,OAAO,EAAE,QAAQ,EAAiB,MAAM,eAAe,CAAC;AACxD,OAAO,EAAE,UAAU,EAA+C,MAAM,kBAAkB,CAAC;AAC3F,OAAO,EAAE,kBAAkB,EAAE,UAAU,EAAE,QAAQ,EAAuB,MAAM,aAAa,CAAC;AAC5F,OAAO,EAAE,YAAY,EAAE,mBAAmB,EAAE,MAAM,sBAAsB,CAAC;AACzE,OAAO,EAAE,mBAAmB,EAAE,MAAM,cAAc,CAAC;AACnD,OAAO,EAAE,WAAW,EAAoB,MAAM,mBAAmB,CAAC;AAClE,OAAO,EACL,UAAU,EACV,YAAY,EACZ,gBAAgB,EAChB,iBAAiB,GAElB,MAAM,yBAAyB,CAAC;AACjC,OAAO,EACL,2BAA2B,EAC3B,QAAQ,EACR,SAAS,EACT,QAAQ,EACR,SAAS,EACT,OAAO,EACP,MAAM,EACN,WAAW,EACX,kBAAkB,EAClB,wBAAwB,EACxB,QAAQ,EAER,YAAY,EACZ,SAAS,EACT,MAAM,EACN,OAAO,EACP,MAAM,EACN,QAAQ,EACR,aAAa,EAEb,MAAM,EACN,OAAO,EACP,QAAQ,EACR,gBAAgB,EAChB,MAAM,EACN,UAAU,EACV,SAAS,EACT,QAAQ,EACR,SAAS,EAET,WAAW,EAEX,QAAQ,EAER,QAAQ,EAER,MAAM,EACN,cAAc,EACd,MAAM,EACN,QAAQ,GAIT,MAAM,yBAAyB,CAAC;AACjC,OAAO,EAAE,SAAS,EAAkD,MAAM,iBAAiB,CAAC;AAC5F,OAAO,EACL,SAAS,EACT,kBAAkB,EAClB,oBAAoB,GAGrB,MAAM,YAAY,CAAC;AACpB,OAAO,EACL,UAAU,GAMX,MAAM,aAAa,CAAC;AACrB,OAAO,EACL,gBAAgB,GAGjB,MAAM,wBAAwB,CAAC","sourcesContent":["/**\n * What the package exports without naming a module.\n *\n * Everything with an Expo module behind it has its own entry point - `@ng-native/expo/\n * battery.ts` and friends - so that importing haptics does not make an app install the video\n * player. What is left here is the shared machinery: the permission shape every module spells\n * identically, the signal-from-a-listener those services are built out of, and the view names.\n */\nexport { observed, type Observed } from './observed.ts';\nexport { Permission, type PermissionApi, type PermissionResponse } from './permissions.ts';\nexport { MissingModuleError, expoModule, optional, type ModulePlatform } from './native.ts';\nexport { NATIVE_VIEWS, registerNativeViews } from './community-views.ts';\nexport { registerExpoUiViews } from './expo-ui.ts';\nexport { nativeState, type NativeState } from './native-state.ts';\nexport {\n EXPO_VIEWS,\n expoViewName,\n registerExpoView,\n registerExpoViews,\n type ExpoViewOptions,\n} from './register-expo-view.ts';\nexport {\n UiAccessoryWidgetBackground,\n UiButton,\n UiCapsule,\n UiCircle,\n UiEllipse,\n UiLabel,\n UiLink,\n UiRectangle,\n UiRoundedRectangle,\n UiUnevenRoundedRectangle,\n UiZStack,\n type UiZStackAlignment,\n UiDatePicker,\n UiDivider,\n UiHost,\n UiImage,\n UiMenu,\n UiPicker,\n UiColorPicker,\n type UiColorChangeEvent,\n UiForm,\n UiGauge,\n UiHStack,\n UiLabeledContent,\n UiList,\n UiProgress,\n UiSection,\n UiSpacer,\n UiStepper,\n type UiStepperChangeEvent,\n UiTextField,\n type UiTextFieldChangeEvent,\n UiToggle,\n type UiToggleChangeEvent,\n UiSlider,\n type UiSliderChangeEvent,\n UiSlot,\n UiSwipeActions,\n UiText,\n UiVStack,\n type UiDateChangeEvent,\n type UiPickerOption,\n type UiModifier,\n} from './expo-ui-components.ts';\nexport { ExpoImage, type ExpoImageContentFit, type ExpoImageSource } from './expo-image.ts';\nexport {\n ExpoGlass,\n ExpoGlassContainer,\n liquidGlassAvailable,\n type GlassEffectStyleConfig,\n type GlassStyle,\n} from './glass.ts';\nexport {\n ExpoSymbol,\n type SymbolAnimationSpec,\n type SymbolResizeMode,\n type SymbolScale,\n type SymbolType,\n type SymbolWeight,\n} from './symbol.ts';\nexport {\n SegmentedControl,\n type SegmentedControlChangeEvent,\n type SegmentedControlFont,\n} from './segmented-control.ts';\n"]}
@@ -0,0 +1,88 @@
1
+ /**
2
+ * iOS Live Activities and home-screen widgets, through `expo-widgets`: `createLiveActivity` and
3
+ * `createWidget` draw one from an Angular component's template, and `liveActivity` keeps an
4
+ * activity in step with a signal, on the lock screen and in the Dynamic Island. Off iOS,
5
+ * `expo-widgets` starts a stand-in with no id, and `start()` answers false.
6
+ */
7
+ import { type InputSignal, type Signal, type Type } from '@angular/core';
8
+ /** A widget layout: an Angular component whose `props` input the layout draws from. */
9
+ export type WidgetLayout<T extends object> = Type<{
10
+ readonly props: InputSignal<T>;
11
+ }>;
12
+ /**
13
+ * A Live Activity drawn by `layout`, an Angular component, as `expo-widgets`' `createLiveActivity`
14
+ * makes one. `name` matches the activity's name in the `expo-widgets` plugin's config.
15
+ *
16
+ * ```ts
17
+ * export const scoreActivity = createLiveActivity('Score', ScoreLayout);
18
+ * ```
19
+ *
20
+ * `@ng-native/metro`'s transformer compiles the layout's template, at build time, to the source the
21
+ * widget extension evaluates, and drops the class, so the app never runs it: the extension does,
22
+ * with no Angular. So the class holds only its inputs, members holding a modifier from
23
+ * `@expo/ui/swift-ui/modifiers`, and members holding a literal, and its template is inline.
24
+ */
25
+ export declare function createLiveActivity<T extends object>(name: string, layout: WidgetLayout<T>): import('expo-widgets').LiveActivityFactory<T>;
26
+ /**
27
+ * A home-screen widget drawn by `layout`, as `expo-widgets`' `createWidget` makes one. The layout
28
+ * reads what the widget is drawn in, such as its `widgetFamily`, from an `environment` input beside
29
+ * `props`. Compiled as `createLiveActivity`'s layout is.
30
+ */
31
+ export declare function createWidget<T extends object>(name: string, layout: WidgetLayout<T>): import('expo-widgets').Widget<T>;
32
+ /**
33
+ * How an ended activity leaves the lock screen: `'default'` keeps it, with its final props, for up
34
+ * to four hours; `'immediate'` removes it; `{ after }` keeps it until then.
35
+ */
36
+ export type LiveActivityDismissal = 'default' | 'immediate' | {
37
+ readonly after: Date;
38
+ };
39
+ interface Subscription {
40
+ remove(): void;
41
+ }
42
+ /** One running activity, as `expo-widgets`' `LiveActivity` is. */
43
+ export interface NativeLiveActivity<T extends object> {
44
+ getId(): string;
45
+ update(props: T, staleDate?: Date): Promise<void>;
46
+ end(dismissalPolicy?: LiveActivityDismissal, props?: T, contentDate?: Date): Promise<void>;
47
+ getPushToken(): Promise<string | null>;
48
+ addPushTokenListener(listener: (event: {
49
+ pushToken: string;
50
+ }) => void): Subscription;
51
+ }
52
+ /** What `createLiveActivity` answers: starts activities of one kind, and lists the running ones. */
53
+ export interface LiveActivityFactory<T extends object> {
54
+ start(props: T, url?: string, staleDate?: Date): NativeLiveActivity<T>;
55
+ getInstances(): NativeLiveActivity<T>[];
56
+ }
57
+ /** A Live Activity kept in step with a signal. */
58
+ export interface LiveActivityRef {
59
+ /** Whether an activity of this kind is running and following the signal. */
60
+ readonly active: Signal<boolean>;
61
+ /** The running activity's id, or null. */
62
+ readonly id: Signal<string | null>;
63
+ /** The token a server sends the activity's updates to over APNs, once iOS issues one. */
64
+ readonly pushToken: Signal<string | null>;
65
+ /**
66
+ * Why the last start or update did not happen. A start the system refuses, as when Live
67
+ * Activities are turned off in Settings, is only kept here; a failed update also goes to the
68
+ * `ErrorHandler`.
69
+ */
70
+ readonly error: Signal<unknown>;
71
+ /**
72
+ * Shows the activity with the signal's value, or picks up one already running. Answers whether it
73
+ * is running now; `url` opens the app at that link when the activity is tapped.
74
+ */
75
+ start(options?: {
76
+ url?: string;
77
+ }): boolean;
78
+ /** Ends every activity of this kind with the signal's value. */
79
+ end(dismissal?: LiveActivityDismissal): Promise<void>;
80
+ }
81
+ /**
82
+ * Keeps a Live Activity of the factory's kind in step with `props`: once started, every change to the
83
+ * signal updates it. One left running from before the app started is picked up rather than started
84
+ * again, since iOS caps how many an app runs. Destroying the injector stops the updates and leaves
85
+ * the activity running, as it outlives the app. Call it in an injection context.
86
+ */
87
+ export declare function liveActivity<T extends object>(factory: LiveActivityFactory<T>, props: Signal<T>): LiveActivityRef;
88
+ export {};
@@ -0,0 +1,150 @@
1
+ /**
2
+ * iOS Live Activities and home-screen widgets, through `expo-widgets`: `createLiveActivity` and
3
+ * `createWidget` draw one from an Angular component's template, and `liveActivity` keeps an
4
+ * activity in step with a signal, on the lock screen and in the Dynamic Island. Off iOS,
5
+ * `expo-widgets` starts a stand-in with no id, and `start()` answers false.
6
+ */
7
+ import { DestroyRef, ErrorHandler, effect, inject, signal, untracked, } from '@angular/core';
8
+ import { expoModule } from "./native.js";
9
+ /**
10
+ * A Live Activity drawn by `layout`, an Angular component, as `expo-widgets`' `createLiveActivity`
11
+ * makes one. `name` matches the activity's name in the `expo-widgets` plugin's config.
12
+ *
13
+ * ```ts
14
+ * export const scoreActivity = createLiveActivity('Score', ScoreLayout);
15
+ * ```
16
+ *
17
+ * `@ng-native/metro`'s transformer compiles the layout's template, at build time, to the source the
18
+ * widget extension evaluates, and drops the class, so the app never runs it: the extension does,
19
+ * with no Angular. So the class holds only its inputs, members holding a modifier from
20
+ * `@expo/ui/swift-ui/modifiers`, and members holding a literal, and its template is inline.
21
+ */
22
+ export function createLiveActivity(name, layout) {
23
+ const expo = widgets();
24
+ if (!expo)
25
+ return STAND_IN;
26
+ return expo.createLiveActivity(name, compiled('createLiveActivity', name, layout));
27
+ }
28
+ /**
29
+ * A home-screen widget drawn by `layout`, as `expo-widgets`' `createWidget` makes one. The layout
30
+ * reads what the widget is drawn in, such as its `widgetFamily`, from an `environment` input beside
31
+ * `props`. Compiled as `createLiveActivity`'s layout is.
32
+ */
33
+ export function createWidget(name, layout) {
34
+ const expo = widgets();
35
+ if (!expo)
36
+ return STAND_IN;
37
+ return expo.createWidget(name, compiled('createWidget', name, layout));
38
+ }
39
+ /** `expo-widgets`, which answers with stand-ins itself where there are no widgets; null in Node. */
40
+ function widgets() {
41
+ return expoModule('expo-widgets', () => require('expo-widgets'), ['ios', 'android', 'web']);
42
+ }
43
+ /** The layout's source, which the transformer put in place of the class. */
44
+ function compiled(call, name, layout) {
45
+ if (typeof layout === 'string')
46
+ return layout;
47
+ throw new Error(`${call}('${name}', ...) takes a layout @ng-native/metro's transformer compiles, and it did not run on this file.`);
48
+ }
49
+ /** What there is in Node, where no widget can be drawn: an activity that never starts. */
50
+ const STAND_IN = {
51
+ start: () => ({ getId: () => '' }),
52
+ getInstances: () => [],
53
+ };
54
+ /**
55
+ * Keeps a Live Activity of the factory's kind in step with `props`: once started, every change to the
56
+ * signal updates it. One left running from before the app started is picked up rather than started
57
+ * again, since iOS caps how many an app runs. Destroying the injector stops the updates and leaves
58
+ * the activity running, as it outlives the app. Call it in an injection context.
59
+ */
60
+ export function liveActivity(factory, props) {
61
+ const errors = inject(ErrorHandler);
62
+ const active = signal(false, /* @ts-ignore */
63
+ ...(ngDevMode ? [{ debugName: "active" }] : /* istanbul ignore next */ []));
64
+ const id = signal(null, /* @ts-ignore */
65
+ ...(ngDevMode ? [{ debugName: "id" }] : /* istanbul ignore next */ []));
66
+ const pushToken = signal(null, /* @ts-ignore */
67
+ ...(ngDevMode ? [{ debugName: "pushToken" }] : /* istanbul ignore next */ []));
68
+ const error = signal(null, /* @ts-ignore */
69
+ ...(ngDevMode ? [{ debugName: "error" }] : /* istanbul ignore next */ []));
70
+ let current = null;
71
+ let tokenSubscription = null;
72
+ const report = (failure) => {
73
+ error.set(failure);
74
+ errors.handleError(failure);
75
+ };
76
+ const running = () => {
77
+ try {
78
+ return factory.getInstances();
79
+ }
80
+ catch (failure) {
81
+ error.set(failure);
82
+ return [];
83
+ }
84
+ };
85
+ const forget = () => {
86
+ tokenSubscription?.remove();
87
+ tokenSubscription = null;
88
+ current = null;
89
+ active.set(false);
90
+ id.set(null);
91
+ pushToken.set(null);
92
+ };
93
+ const adopt = (activity) => {
94
+ current = activity;
95
+ active.set(true);
96
+ id.set(activity.getId());
97
+ error.set(null);
98
+ tokenSubscription = activity.addPushTokenListener((event) => pushToken.set(event.pushToken));
99
+ activity.getPushToken().then((token) => current === activity && token !== null && pushToken.set(token), () => { });
100
+ };
101
+ const existing = running()[0];
102
+ if (existing)
103
+ adopt(existing);
104
+ effect(() => {
105
+ const next = props();
106
+ const activity = current;
107
+ if (activity)
108
+ untracked(() => activity.update(next).catch(report));
109
+ });
110
+ inject(DestroyRef).onDestroy(() => {
111
+ tokenSubscription?.remove();
112
+ tokenSubscription = null;
113
+ });
114
+ return {
115
+ active: active.asReadonly(),
116
+ id: id.asReadonly(),
117
+ pushToken: pushToken.asReadonly(),
118
+ error: error.asReadonly(),
119
+ start(options = {}) {
120
+ if (current)
121
+ return true;
122
+ const value = untracked(props);
123
+ try {
124
+ const already = running()[0];
125
+ const activity = already ?? factory.start(value, options.url);
126
+ if (!activity.getId()) {
127
+ error.set(new Error('Live Activities are only on iOS.'));
128
+ return false;
129
+ }
130
+ adopt(activity);
131
+ if (already)
132
+ already.update(value).catch(report);
133
+ return true;
134
+ }
135
+ catch (failure) {
136
+ error.set(failure);
137
+ return false;
138
+ }
139
+ },
140
+ async end(dismissal = 'default') {
141
+ const value = untracked(props);
142
+ const activities = new Set(running());
143
+ if (current)
144
+ activities.add(current);
145
+ forget();
146
+ await Promise.all([...activities].map((activity) => activity.end(dismissal, value))).catch(report);
147
+ },
148
+ };
149
+ }
150
+ //# sourceMappingURL=live-activity.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"live-activity.js","sourceRoot":"","sources":["../src/live-activity.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * iOS Live Activities and home-screen widgets, through `expo-widgets`: `createLiveActivity` and\n * `createWidget` draw one from an Angular component's template, and `liveActivity` keeps an\n * activity in step with a signal, on the lock screen and in the Dynamic Island. Off iOS,\n * `expo-widgets` starts a stand-in with no id, and `start()` answers false.\n */\nimport {\n DestroyRef,\n ErrorHandler,\n effect,\n inject,\n signal,\n untracked,\n type InputSignal,\n type Signal,\n type Type,\n} from '@angular/core';\nimport { expoModule } from './native.ts';\n\n/** A widget layout: an Angular component whose `props` input the layout draws from. */\nexport type WidgetLayout<T extends object> = Type<{ readonly props: InputSignal<T> }>;\n\n/**\n * A Live Activity drawn by `layout`, an Angular component, as `expo-widgets`' `createLiveActivity`\n * makes one. `name` matches the activity's name in the `expo-widgets` plugin's config.\n *\n * ```ts\n * export const scoreActivity = createLiveActivity('Score', ScoreLayout);\n * ```\n *\n * `@ng-native/metro`'s transformer compiles the layout's template, at build time, to the source the\n * widget extension evaluates, and drops the class, so the app never runs it: the extension does,\n * with no Angular. So the class holds only its inputs, members holding a modifier from\n * `@expo/ui/swift-ui/modifiers`, and members holding a literal, and its template is inline.\n */\nexport function createLiveActivity<T extends object>(\n name: string,\n layout: WidgetLayout<T>,\n): import('expo-widgets').LiveActivityFactory<T> {\n const expo = widgets();\n if (!expo) return STAND_IN as never;\n return expo.createLiveActivity<T>(name, compiled('createLiveActivity', name, layout));\n}\n\n/**\n * A home-screen widget drawn by `layout`, as `expo-widgets`' `createWidget` makes one. The layout\n * reads what the widget is drawn in, such as its `widgetFamily`, from an `environment` input beside\n * `props`. Compiled as `createLiveActivity`'s layout is.\n */\nexport function createWidget<T extends object>(\n name: string,\n layout: WidgetLayout<T>,\n): import('expo-widgets').Widget<T> {\n const expo = widgets();\n if (!expo) return STAND_IN as never;\n return expo.createWidget<T>(name, compiled('createWidget', name, layout));\n}\n\n/** `expo-widgets`, which answers with stand-ins itself where there are no widgets; null in Node. */\nfunction widgets() {\n return expoModule(\n 'expo-widgets',\n () => require('expo-widgets') as typeof import('expo-widgets'),\n ['ios', 'android', 'web'],\n );\n}\n\n/** The layout's source, which the transformer put in place of the class. */\nfunction compiled(call: string, name: string, layout: unknown): never {\n if (typeof layout === 'string') return layout as never;\n throw new Error(\n `${call}('${name}', ...) takes a layout @ng-native/metro's transformer compiles, and it did not run on this file.`,\n );\n}\n\n/** What there is in Node, where no widget can be drawn: an activity that never starts. */\nconst STAND_IN = {\n start: () => ({ getId: () => '' }),\n getInstances: () => [],\n};\n\n/**\n * How an ended activity leaves the lock screen: `'default'` keeps it, with its final props, for up\n * to four hours; `'immediate'` removes it; `{ after }` keeps it until then.\n */\nexport type LiveActivityDismissal = 'default' | 'immediate' | { readonly after: Date };\n\ninterface Subscription {\n remove(): void;\n}\n\n/** One running activity, as `expo-widgets`' `LiveActivity` is. */\nexport interface NativeLiveActivity<T extends object> {\n getId(): string;\n update(props: T, staleDate?: Date): Promise<void>;\n end(dismissalPolicy?: LiveActivityDismissal, props?: T, contentDate?: Date): Promise<void>;\n getPushToken(): Promise<string | null>;\n addPushTokenListener(listener: (event: { pushToken: string }) => void): Subscription;\n}\n\n/** What `createLiveActivity` answers: starts activities of one kind, and lists the running ones. */\nexport interface LiveActivityFactory<T extends object> {\n start(props: T, url?: string, staleDate?: Date): NativeLiveActivity<T>;\n getInstances(): NativeLiveActivity<T>[];\n}\n\n/** A Live Activity kept in step with a signal. */\nexport interface LiveActivityRef {\n /** Whether an activity of this kind is running and following the signal. */\n readonly active: Signal<boolean>;\n /** The running activity's id, or null. */\n readonly id: Signal<string | null>;\n /** The token a server sends the activity's updates to over APNs, once iOS issues one. */\n readonly pushToken: Signal<string | null>;\n /**\n * Why the last start or update did not happen. A start the system refuses, as when Live\n * Activities are turned off in Settings, is only kept here; a failed update also goes to the\n * `ErrorHandler`.\n */\n readonly error: Signal<unknown>;\n /**\n * Shows the activity with the signal's value, or picks up one already running. Answers whether it\n * is running now; `url` opens the app at that link when the activity is tapped.\n */\n start(options?: { url?: string }): boolean;\n /** Ends every activity of this kind with the signal's value. */\n end(dismissal?: LiveActivityDismissal): Promise<void>;\n}\n\n/**\n * Keeps a Live Activity of the factory's kind in step with `props`: once started, every change to the\n * signal updates it. One left running from before the app started is picked up rather than started\n * again, since iOS caps how many an app runs. Destroying the injector stops the updates and leaves\n * the activity running, as it outlives the app. Call it in an injection context.\n */\nexport function liveActivity<T extends object>(\n factory: LiveActivityFactory<T>,\n props: Signal<T>,\n): LiveActivityRef {\n const errors = inject(ErrorHandler);\n const active = signal(false);\n const id = signal<string | null>(null);\n const pushToken = signal<string | null>(null);\n const error = signal<unknown>(null);\n let current: NativeLiveActivity<T> | null = null;\n let tokenSubscription: Subscription | null = null;\n\n const report = (failure: unknown) => {\n error.set(failure);\n errors.handleError(failure);\n };\n\n const running = (): NativeLiveActivity<T>[] => {\n try {\n return factory.getInstances();\n } catch (failure) {\n error.set(failure);\n return [];\n }\n };\n\n const forget = () => {\n tokenSubscription?.remove();\n tokenSubscription = null;\n current = null;\n active.set(false);\n id.set(null);\n pushToken.set(null);\n };\n\n const adopt = (activity: NativeLiveActivity<T>) => {\n current = activity;\n active.set(true);\n id.set(activity.getId());\n error.set(null);\n tokenSubscription = activity.addPushTokenListener((event) => pushToken.set(event.pushToken));\n activity.getPushToken().then(\n (token) => current === activity && token !== null && pushToken.set(token),\n () => {},\n );\n };\n\n const existing = running()[0];\n if (existing) adopt(existing);\n\n effect(() => {\n const next = props();\n const activity = current;\n if (activity) untracked(() => activity.update(next).catch(report));\n });\n\n inject(DestroyRef).onDestroy(() => {\n tokenSubscription?.remove();\n tokenSubscription = null;\n });\n\n return {\n active: active.asReadonly(),\n id: id.asReadonly(),\n pushToken: pushToken.asReadonly(),\n error: error.asReadonly(),\n start(options = {}) {\n if (current) return true;\n const value = untracked(props);\n try {\n const already = running()[0];\n const activity = already ?? factory.start(value, options.url);\n if (!activity.getId()) {\n error.set(new Error('Live Activities are only on iOS.'));\n return false;\n }\n adopt(activity);\n if (already) already.update(value).catch(report);\n return true;\n } catch (failure) {\n error.set(failure);\n return false;\n }\n },\n async end(dismissal = 'default') {\n const value = untracked(props);\n const activities = new Set(running());\n if (current) activities.add(current);\n forget();\n await Promise.all([...activities].map((activity) => activity.end(dismissal, value))).catch(\n report,\n );\n },\n };\n}\n"]}
package/dist/location.js CHANGED
@@ -15,7 +15,7 @@
15
15
  */
16
16
  import { InjectionToken, Service, inject, signal } from '@angular/core';
17
17
  import { expoModule } from "./native.js";
18
- import { Permission } from "./permissions.js";
18
+ import { Permission, UNAVAILABLE } from "./permissions.js";
19
19
  import * as i0 from "@angular/core";
20
20
  /** Expo's `LocationAccuracy` enum, whose values are these numbers. */
21
21
  const ACCURACY = {
@@ -26,11 +26,6 @@ const ACCURACY = {
26
26
  highest: 5,
27
27
  navigation: 6,
28
28
  };
29
- const UNAVAILABLE = async () => ({
30
- status: 'denied',
31
- granted: false,
32
- canAskAgain: false,
33
- });
34
29
  export class Location {
35
30
  /** Overridden in a test to be somewhere without a GPS. */
36
31
  static SOURCE = new InjectionToken('angular-native.locationSource', {
@@ -40,7 +35,7 @@ export class Location {
40
35
  latest = signal(null, /* @ts-ignore */
41
36
  ...(ngDevMode ? [{ debugName: "latest" }] : /* istanbul ignore next */ []));
42
37
  /** The foreground permission, which `current()` and `start()` ask for themselves. */
43
- permission = Permission.of(this.native?.getForegroundPermissionsAsync ?? UNAVAILABLE, this.native?.requestForegroundPermissionsAsync ?? UNAVAILABLE);
38
+ permission = Permission.of(() => this.native?.getForegroundPermissionsAsync?.() ?? Promise.resolve(UNAVAILABLE), () => this.native?.requestForegroundPermissionsAsync?.() ?? Promise.resolve(UNAVAILABLE));
44
39
  /** The last position read, or null before the first. */
45
40
  position = this.latest.asReadonly();
46
41
  /** Read the position once. Null without the permission or the module. */
@@ -1 +1 @@
1
- {"version":3,"file":"location.js","sourceRoot":"","sources":["../src/location.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * `Location`, bound to `expo-location`.\n *\n * ```ts\n * private readonly location = inject(Location);\n * protected readonly here = this.location.position;\n *\n * const stop = await this.location.start({ accuracy: 'high', distance: 10 });\n * ```\n *\n * A `position` signal, which a one-off `current()` and a running `start()` both write to. Nothing\n * is watched until `start`, and the accuracy is explicit, for the same reason a sensor's interval\n * is: GPS at navigation accuracy is a battery the user notices. Both ask for the foreground\n * permission first, and do nothing without it.\n */\nimport { InjectionToken, Service, inject, signal, type Signal } from '@angular/core';\nimport { expoModule } from './native.ts';\nimport { Permission, type PermissionResponse } from './permissions.ts';\n\n/** How precise, in Expo's order: `lowest` is kilometres, `navigation` is metres and a hot GPS. */\nexport type LocationAccuracy = 'lowest' | 'low' | 'balanced' | 'high' | 'highest' | 'navigation';\n\n/** Expo's `LocationAccuracy` enum, whose values are these numbers. */\nconst ACCURACY: Record<LocationAccuracy, number> = {\n lowest: 1,\n low: 2,\n balanced: 3,\n high: 4,\n highest: 5,\n navigation: 6,\n};\n\nexport interface Position {\n readonly latitude: number;\n readonly longitude: number;\n /** Metres above sea level, where the platform knows. */\n readonly altitude: number | null;\n /** The radius, in metres, the fix is good to. */\n readonly accuracy: number | null;\n /** Degrees from true north, while moving. */\n readonly heading: number | null;\n /** Metres per second. */\n readonly speed: number | null;\n readonly timestamp: number;\n}\n\nexport interface WatchOptions {\n readonly accuracy?: LocationAccuracy;\n /** Metres moved before the next update. */\n readonly distance?: number;\n /** Milliseconds between updates. Android only; iOS updates on distance. */\n readonly interval?: number;\n}\n\ntype ExpoFix = import('expo-location').LocationObject;\ntype ExpoOptions = import('expo-location').LocationOptions;\n\n/** The slice of `expo-location` this needs. */\nexport interface NativeLocation {\n getForegroundPermissionsAsync(): Promise<PermissionResponse>;\n requestForegroundPermissionsAsync(): Promise<PermissionResponse>;\n getCurrentPositionAsync(options?: ExpoOptions): Promise<ExpoFix>;\n watchPositionAsync(\n options: ExpoOptions,\n callback: (fix: ExpoFix) => void,\n ): Promise<{ remove(): void }>;\n}\n\nconst UNAVAILABLE = async (): Promise<PermissionResponse> => ({\n status: 'denied',\n granted: false,\n canAskAgain: false,\n});\n\n@Service()\nexport class Location {\n /** Overridden in a test to be somewhere without a GPS. */\n static readonly SOURCE = new InjectionToken<NativeLocation | null>(\n 'angular-native.locationSource',\n {\n factory: () =>\n expoModule('expo-location', () => require('expo-location') as NativeLocation) ?? null,\n },\n );\n\n private readonly native = inject(Location.SOURCE);\n private readonly latest = signal<Position | null>(null);\n\n /** The foreground permission, which `current()` and `start()` ask for themselves. */\n readonly permission = Permission.of(\n this.native?.getForegroundPermissionsAsync ?? UNAVAILABLE,\n this.native?.requestForegroundPermissionsAsync ?? UNAVAILABLE,\n );\n\n /** The last position read, or null before the first. */\n readonly position: Signal<Position | null> = this.latest.asReadonly();\n\n /** Read the position once. Null without the permission or the module. */\n async current(accuracy: LocationAccuracy = 'balanced'): Promise<Position | null> {\n if (!this.native || !(await this.permission.ensure())) return null;\n const fix = await this.native.getCurrentPositionAsync({ accuracy: ACCURACY[accuracy] });\n this.latest.set(positionOf(fix));\n return this.latest();\n }\n\n /** Follow the position into the signal. Resolves to the function that stops. */\n async start(options: WatchOptions = {}): Promise<() => void> {\n if (!this.native || !(await this.permission.ensure())) return () => {};\n const expo: ExpoOptions = { accuracy: ACCURACY[options.accuracy ?? 'balanced'] };\n if (options.distance !== undefined) expo.distanceInterval = options.distance;\n if (options.interval !== undefined) expo.timeInterval = options.interval;\n const subscription = await this.native.watchPositionAsync(expo, (fix) =>\n this.latest.set(positionOf(fix)),\n );\n return () => subscription.remove();\n }\n}\n\nfunction positionOf({ coords, timestamp }: ExpoFix): Position {\n return {\n latitude: coords.latitude,\n longitude: coords.longitude,\n altitude: coords.altitude,\n accuracy: coords.accuracy,\n heading: coords.heading,\n speed: coords.speed,\n timestamp,\n };\n}\n"]}
1
+ {"version":3,"file":"location.js","sourceRoot":"","sources":["../src/location.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * `Location`, bound to `expo-location`.\n *\n * ```ts\n * private readonly location = inject(Location);\n * protected readonly here = this.location.position;\n *\n * const stop = await this.location.start({ accuracy: 'high', distance: 10 });\n * ```\n *\n * A `position` signal, which a one-off `current()` and a running `start()` both write to. Nothing\n * is watched until `start`, and the accuracy is explicit, for the same reason a sensor's interval\n * is: GPS at navigation accuracy is a battery the user notices. Both ask for the foreground\n * permission first, and do nothing without it.\n */\nimport { InjectionToken, Service, inject, signal, type Signal } from '@angular/core';\nimport { expoModule } from './native.ts';\nimport { Permission, UNAVAILABLE, type PermissionResponse } from './permissions.ts';\n\n/** How precise, in Expo's order: `lowest` is kilometres, `navigation` is metres and a hot GPS. */\nexport type LocationAccuracy = 'lowest' | 'low' | 'balanced' | 'high' | 'highest' | 'navigation';\n\n/** Expo's `LocationAccuracy` enum, whose values are these numbers. */\nconst ACCURACY: Record<LocationAccuracy, number> = {\n lowest: 1,\n low: 2,\n balanced: 3,\n high: 4,\n highest: 5,\n navigation: 6,\n};\n\nexport interface Position {\n readonly latitude: number;\n readonly longitude: number;\n /** Metres above sea level, where the platform knows. */\n readonly altitude: number | null;\n /** The radius, in metres, the fix is good to. */\n readonly accuracy: number | null;\n /** Degrees from true north, while moving. */\n readonly heading: number | null;\n /** Metres per second. */\n readonly speed: number | null;\n readonly timestamp: number;\n}\n\nexport interface WatchOptions {\n readonly accuracy?: LocationAccuracy;\n /** Metres moved before the next update. */\n readonly distance?: number;\n /** Milliseconds between updates. Android only; iOS updates on distance. */\n readonly interval?: number;\n}\n\ntype ExpoFix = import('expo-location').LocationObject;\ntype ExpoOptions = import('expo-location').LocationOptions;\n\n/** The slice of `expo-location` this needs. */\nexport interface NativeLocation {\n getForegroundPermissionsAsync(): Promise<PermissionResponse>;\n requestForegroundPermissionsAsync(): Promise<PermissionResponse>;\n getCurrentPositionAsync(options?: ExpoOptions): Promise<ExpoFix>;\n watchPositionAsync(\n options: ExpoOptions,\n callback: (fix: ExpoFix) => void,\n ): Promise<{ remove(): void }>;\n}\n\n@Service()\nexport class Location {\n /** Overridden in a test to be somewhere without a GPS. */\n static readonly SOURCE = new InjectionToken<NativeLocation | null>(\n 'angular-native.locationSource',\n {\n factory: () =>\n expoModule('expo-location', () => require('expo-location') as NativeLocation) ?? null,\n },\n );\n\n private readonly native = inject(Location.SOURCE);\n private readonly latest = signal<Position | null>(null);\n\n /** The foreground permission, which `current()` and `start()` ask for themselves. */\n readonly permission = Permission.of(\n () => this.native?.getForegroundPermissionsAsync?.() ?? Promise.resolve(UNAVAILABLE),\n () => this.native?.requestForegroundPermissionsAsync?.() ?? Promise.resolve(UNAVAILABLE),\n );\n\n /** The last position read, or null before the first. */\n readonly position: Signal<Position | null> = this.latest.asReadonly();\n\n /** Read the position once. Null without the permission or the module. */\n async current(accuracy: LocationAccuracy = 'balanced'): Promise<Position | null> {\n if (!this.native || !(await this.permission.ensure())) return null;\n const fix = await this.native.getCurrentPositionAsync({ accuracy: ACCURACY[accuracy] });\n this.latest.set(positionOf(fix));\n return this.latest();\n }\n\n /** Follow the position into the signal. Resolves to the function that stops. */\n async start(options: WatchOptions = {}): Promise<() => void> {\n if (!this.native || !(await this.permission.ensure())) return () => {};\n const expo: ExpoOptions = { accuracy: ACCURACY[options.accuracy ?? 'balanced'] };\n if (options.distance !== undefined) expo.distanceInterval = options.distance;\n if (options.interval !== undefined) expo.timeInterval = options.interval;\n const subscription = await this.native.watchPositionAsync(expo, (fix) =>\n this.latest.set(positionOf(fix)),\n );\n return () => subscription.remove();\n }\n}\n\nfunction positionOf({ coords, timestamp }: ExpoFix): Position {\n return {\n latitude: coords.latitude,\n longitude: coords.longitude,\n altitude: coords.altitude,\n accuracy: coords.accuracy,\n heading: coords.heading,\n speed: coords.speed,\n timestamp,\n };\n}\n"]}
@@ -33,7 +33,7 @@
33
33
  */
34
34
  import { DestroyRef, InjectionToken, Service, inject, signal } from '@angular/core';
35
35
  import { expoModule } from "./native.js";
36
- import { Permission } from "./permissions.js";
36
+ import { Permission, UNAVAILABLE } from "./permissions.js";
37
37
  import * as i0 from "@angular/core";
38
38
  /**
39
39
  * The kinds of asset, as `expo-media-library`'s own enum. Importing the enum from the module
@@ -55,8 +55,6 @@ export const AssetField = {
55
55
  DURATION: 'duration',
56
56
  IS_FAVORITE: 'isFavorite',
57
57
  };
58
- /** The permission answer without the module: nothing granted, and nothing to ask. */
59
- const UNAVAILABLE = { status: 'denied', granted: false, canAskAgain: false };
60
58
  export class MediaLibrary {
61
59
  /** Overridden in a test to save photos to a library that is not there. */
62
60
  static SOURCE = new InjectionToken('angular-native.mediaLibrarySource', {
@@ -1 +1 @@
1
- {"version":3,"file":"media-library.js","sourceRoot":"","sources":["../src/media-library.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * `MediaLibrary`, bound to `expo-media-library`: saving to and reading from the user's photos and\n * videos.\n *\n * ```ts\n * import { AssetField, MediaLibrary, MediaType } from '@ng-native/expo/media-library';\n *\n * private readonly library = inject(MediaLibrary);\n *\n * async keep(uri: string): Promise<void> {\n * await this.library.save(uri);\n * }\n *\n * async recentPhotos(): Promise<Asset[]> {\n * if (!(await this.library.permission.ensure())) return [];\n * return this.library.assets((query) =>\n * query.eq(AssetField.MEDIA_TYPE, MediaType.IMAGE).orderBy(AssetField.CREATION_TIME).limit(30),\n * );\n * }\n * ```\n *\n * The module's API is its classes: an `Asset` and an `Album` are handles whose details are read\n * with their own async getters, and a `Query` is built by chaining. This service is where they\n * are reached from, so a test can provide all three, and where the two permissions are: full\n * access to read the library, and the write-only access saving needs, which is the one to ask for\n * when that is all the app does.\n *\n * Changes to the library are a signal, but only once `watch()` is called: starting the listener\n * reads the library, which is not something to do before the user has granted access.\n *\n * Without the module installed, every query answers empty, saving answers null, and the\n * permissions are refused.\n */\nimport { DestroyRef, InjectionToken, Service, inject, signal, type Signal } from '@angular/core';\nimport type {\n Album,\n Asset,\n AssetField as ExpoAssetField,\n AssetMetadata,\n MediaLibraryAssetsChangeEvent,\n MediaType as ExpoMediaType,\n Query,\n} from 'expo-media-library';\nimport { expoModule } from './native.ts';\nimport { Permission, type PermissionResponse } from './permissions.ts';\n\ntype Expo = typeof import('expo-media-library');\n\n/** The module's functions and classes this service calls. A test provides fakes of each. */\nexport type NativeMediaLibrary = Pick<\n Expo,\n | 'getPermissionsAsync'\n | 'requestPermissionsAsync'\n | 'presentPermissionsPicker'\n | 'addListener'\n | 'Asset'\n | 'Album'\n | 'Query'\n>;\n\ntype Args<K extends keyof NativeMediaLibrary> = NativeMediaLibrary[K] extends (\n ...args: infer A\n) => unknown\n ? A\n : never;\n\n/**\n * The kinds of asset, as `expo-media-library`'s own enum. Importing the enum from the module\n * loads the module, which a test in Node cannot; these are the same strings, typed as the enum.\n */\nexport const MediaType = {\n UNKNOWN: 'unknown',\n IMAGE: 'image',\n AUDIO: 'audio',\n VIDEO: 'video',\n} as unknown as typeof ExpoMediaType;\n\n/** The fields a query filters and sorts on, as the module's own enum. */\nexport const AssetField = {\n CREATION_TIME: 'creationTime',\n MODIFICATION_TIME: 'modificationTime',\n MEDIA_TYPE: 'mediaType',\n WIDTH: 'width',\n HEIGHT: 'height',\n DURATION: 'duration',\n IS_FAVORITE: 'isFavorite',\n} as unknown as typeof ExpoAssetField;\n\n/** The permission answer without the module: nothing granted, and nothing to ask. */\nconst UNAVAILABLE: PermissionResponse = { status: 'denied', granted: false, canAskAgain: false };\n\n@Service()\nexport class MediaLibrary {\n /** Overridden in a test to save photos to a library that is not there. */\n static readonly SOURCE = new InjectionToken<NativeMediaLibrary | null>(\n 'angular-native.mediaLibrarySource',\n {\n factory: () => expoModule('expo-media-library', () => require('expo-media-library') as Expo),\n },\n );\n\n private readonly native = inject(MediaLibrary.SOURCE);\n private readonly destroyRef = inject(DestroyRef);\n private readonly changed = signal<MediaLibraryAssetsChangeEvent | null>(null);\n private readonly changes = this.changed.asReadonly();\n private watching = false;\n\n /** Full access: reading the library, as well as adding to it. */\n readonly permission: Permission = this.permissionFor(false);\n\n /** Write-only access, which is all `save()` needs and what it asks for. */\n readonly writePermission: Permission = this.permissionFor(true);\n\n /**\n * Asks with the module's own options: write-only, and on Android 13 and later which of\n * `'photo'`, `'video'` and `'audio'` to ask for. The answer also updates {@link permission}.\n */\n async requestPermission(...options: Args<'requestPermissionsAsync'>): Promise<boolean> {\n if (!this.native) return false;\n await this.native.requestPermissionsAsync(...options);\n return this.permission.check();\n }\n\n /**\n * On iOS and Android 14 and later, after the user has granted access to only some photos, shows\n * the system picker for changing which. Does nothing otherwise.\n */\n async presentPermissionsPicker(...mediaTypes: Args<'presentPermissionsPicker'>): Promise<void> {\n await this.native?.presentPermissionsPicker(...mediaTypes);\n }\n\n /**\n * Saves a local image or video file to the library, into an album if one is given. Asks for\n * write-only access first; null if it is refused.\n */\n async save(uri: string, album?: Album): Promise<Asset | null> {\n if (!this.native || !(await this.writePermission.ensure())) return null;\n return this.native.Asset.create(uri, album);\n }\n\n /**\n * The assets a query finds, built on the module's own `Query`: `eq`, `within`, `gt`, `lt`,\n * `orderBy`, `limit`, `offset` and `album`. Every asset without one.\n */\n async assets(build: (query: Query) => Query = (query) => query): Promise<Asset[]> {\n if (!this.native) return [];\n return build(new this.native.Query()).exe();\n }\n\n /** The same query, answered with each asset's details at once rather than as handles. */\n async metadata(build: (query: Query) => Query = (query) => query): Promise<AssetMetadata[]> {\n if (!this.native) return [];\n return build(new this.native.Query()).exeForMetadata();\n }\n\n /** The handle for an asset whose id is already known, such as one kept from an earlier query. */\n asset(id: string): Asset | null {\n return this.native ? new this.native.Asset(id) : null;\n }\n\n async deleteAssets(assets: Asset[]): Promise<void> {\n await this.native?.Asset.delete(assets);\n }\n\n async albums(): Promise<Album[]> {\n return (await this.native?.Album.getAll()) ?? [];\n }\n\n /** The album with this title, or null. */\n async album(title: string): Promise<Album | null> {\n return (await this.native?.Album.get(title)) ?? null;\n }\n\n /** A new album holding the assets given, copied into it unless `move` is true. */\n async createAlbum(\n name: string,\n assets: Asset[] | string[],\n move?: boolean,\n ): Promise<Album | null> {\n return (await this.native?.Album.create(name, assets, move)) ?? null;\n }\n\n /** Deletes the albums, and with `deleteAssets` the assets in them too. */\n async deleteAlbums(albums: Album[], deleteAssets?: boolean): Promise<void> {\n await this.native?.Album.delete(albums, deleteAssets);\n }\n\n /**\n * The library's most recent change, from the first call on: on iOS the assets inserted, deleted\n * and updated, on Android an empty event. Call it once access is granted, since listening reads\n * the library. The listener is removed when the app is destroyed.\n */\n watch(): Signal<MediaLibraryAssetsChangeEvent | null> {\n if (this.native && !this.watching) {\n this.watching = true;\n const subscription = this.native.addListener((event) => this.changed.set(event));\n this.destroyRef.onDestroy(() => subscription.remove());\n }\n return this.changes;\n }\n\n private permissionFor(writeOnly: boolean): Permission {\n return Permission.of(\n () => this.native?.getPermissionsAsync(writeOnly) ?? Promise.resolve(UNAVAILABLE),\n () => this.native?.requestPermissionsAsync(writeOnly) ?? Promise.resolve(UNAVAILABLE),\n );\n }\n}\n"]}
1
+ {"version":3,"file":"media-library.js","sourceRoot":"","sources":["../src/media-library.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * `MediaLibrary`, bound to `expo-media-library`: saving to and reading from the user's photos and\n * videos.\n *\n * ```ts\n * import { AssetField, MediaLibrary, MediaType } from '@ng-native/expo/media-library';\n *\n * private readonly library = inject(MediaLibrary);\n *\n * async keep(uri: string): Promise<void> {\n * await this.library.save(uri);\n * }\n *\n * async recentPhotos(): Promise<Asset[]> {\n * if (!(await this.library.permission.ensure())) return [];\n * return this.library.assets((query) =>\n * query.eq(AssetField.MEDIA_TYPE, MediaType.IMAGE).orderBy(AssetField.CREATION_TIME).limit(30),\n * );\n * }\n * ```\n *\n * The module's API is its classes: an `Asset` and an `Album` are handles whose details are read\n * with their own async getters, and a `Query` is built by chaining. This service is where they\n * are reached from, so a test can provide all three, and where the two permissions are: full\n * access to read the library, and the write-only access saving needs, which is the one to ask for\n * when that is all the app does.\n *\n * Changes to the library are a signal, but only once `watch()` is called: starting the listener\n * reads the library, which is not something to do before the user has granted access.\n *\n * Without the module installed, every query answers empty, saving answers null, and the\n * permissions are refused.\n */\nimport { DestroyRef, InjectionToken, Service, inject, signal, type Signal } from '@angular/core';\nimport type {\n Album,\n Asset,\n AssetField as ExpoAssetField,\n AssetMetadata,\n MediaLibraryAssetsChangeEvent,\n MediaType as ExpoMediaType,\n Query,\n} from 'expo-media-library';\nimport { expoModule } from './native.ts';\nimport { Permission, UNAVAILABLE } from './permissions.ts';\n\ntype Expo = typeof import('expo-media-library');\n\n/** The module's functions and classes this service calls. A test provides fakes of each. */\nexport type NativeMediaLibrary = Pick<\n Expo,\n | 'getPermissionsAsync'\n | 'requestPermissionsAsync'\n | 'presentPermissionsPicker'\n | 'addListener'\n | 'Asset'\n | 'Album'\n | 'Query'\n>;\n\ntype Args<K extends keyof NativeMediaLibrary> = NativeMediaLibrary[K] extends (\n ...args: infer A\n) => unknown\n ? A\n : never;\n\n/**\n * The kinds of asset, as `expo-media-library`'s own enum. Importing the enum from the module\n * loads the module, which a test in Node cannot; these are the same strings, typed as the enum.\n */\nexport const MediaType = {\n UNKNOWN: 'unknown',\n IMAGE: 'image',\n AUDIO: 'audio',\n VIDEO: 'video',\n} as unknown as typeof ExpoMediaType;\n\n/** The fields a query filters and sorts on, as the module's own enum. */\nexport const AssetField = {\n CREATION_TIME: 'creationTime',\n MODIFICATION_TIME: 'modificationTime',\n MEDIA_TYPE: 'mediaType',\n WIDTH: 'width',\n HEIGHT: 'height',\n DURATION: 'duration',\n IS_FAVORITE: 'isFavorite',\n} as unknown as typeof ExpoAssetField;\n\n@Service()\nexport class MediaLibrary {\n /** Overridden in a test to save photos to a library that is not there. */\n static readonly SOURCE = new InjectionToken<NativeMediaLibrary | null>(\n 'angular-native.mediaLibrarySource',\n {\n factory: () => expoModule('expo-media-library', () => require('expo-media-library') as Expo),\n },\n );\n\n private readonly native = inject(MediaLibrary.SOURCE);\n private readonly destroyRef = inject(DestroyRef);\n private readonly changed = signal<MediaLibraryAssetsChangeEvent | null>(null);\n private readonly changes = this.changed.asReadonly();\n private watching = false;\n\n /** Full access: reading the library, as well as adding to it. */\n readonly permission: Permission = this.permissionFor(false);\n\n /** Write-only access, which is all `save()` needs and what it asks for. */\n readonly writePermission: Permission = this.permissionFor(true);\n\n /**\n * Asks with the module's own options: write-only, and on Android 13 and later which of\n * `'photo'`, `'video'` and `'audio'` to ask for. The answer also updates {@link permission}.\n */\n async requestPermission(...options: Args<'requestPermissionsAsync'>): Promise<boolean> {\n if (!this.native) return false;\n await this.native.requestPermissionsAsync(...options);\n return this.permission.check();\n }\n\n /**\n * On iOS and Android 14 and later, after the user has granted access to only some photos, shows\n * the system picker for changing which. Does nothing otherwise.\n */\n async presentPermissionsPicker(...mediaTypes: Args<'presentPermissionsPicker'>): Promise<void> {\n await this.native?.presentPermissionsPicker(...mediaTypes);\n }\n\n /**\n * Saves a local image or video file to the library, into an album if one is given. Asks for\n * write-only access first; null if it is refused.\n */\n async save(uri: string, album?: Album): Promise<Asset | null> {\n if (!this.native || !(await this.writePermission.ensure())) return null;\n return this.native.Asset.create(uri, album);\n }\n\n /**\n * The assets a query finds, built on the module's own `Query`: `eq`, `within`, `gt`, `lt`,\n * `orderBy`, `limit`, `offset` and `album`. Every asset without one.\n */\n async assets(build: (query: Query) => Query = (query) => query): Promise<Asset[]> {\n if (!this.native) return [];\n return build(new this.native.Query()).exe();\n }\n\n /** The same query, answered with each asset's details at once rather than as handles. */\n async metadata(build: (query: Query) => Query = (query) => query): Promise<AssetMetadata[]> {\n if (!this.native) return [];\n return build(new this.native.Query()).exeForMetadata();\n }\n\n /** The handle for an asset whose id is already known, such as one kept from an earlier query. */\n asset(id: string): Asset | null {\n return this.native ? new this.native.Asset(id) : null;\n }\n\n async deleteAssets(assets: Asset[]): Promise<void> {\n await this.native?.Asset.delete(assets);\n }\n\n async albums(): Promise<Album[]> {\n return (await this.native?.Album.getAll()) ?? [];\n }\n\n /** The album with this title, or null. */\n async album(title: string): Promise<Album | null> {\n return (await this.native?.Album.get(title)) ?? null;\n }\n\n /** A new album holding the assets given, copied into it unless `move` is true. */\n async createAlbum(\n name: string,\n assets: Asset[] | string[],\n move?: boolean,\n ): Promise<Album | null> {\n return (await this.native?.Album.create(name, assets, move)) ?? null;\n }\n\n /** Deletes the albums, and with `deleteAssets` the assets in them too. */\n async deleteAlbums(albums: Album[], deleteAssets?: boolean): Promise<void> {\n await this.native?.Album.delete(albums, deleteAssets);\n }\n\n /**\n * The library's most recent change, from the first call on: on iOS the assets inserted, deleted\n * and updated, on Android an empty event. Call it once access is granted, since listening reads\n * the library. The listener is removed when the app is destroyed.\n */\n watch(): Signal<MediaLibraryAssetsChangeEvent | null> {\n if (this.native && !this.watching) {\n this.watching = true;\n const subscription = this.native.addListener((event) => this.changed.set(event));\n this.destroyRef.onDestroy(() => subscription.remove());\n }\n return this.changes;\n }\n\n private permissionFor(writeOnly: boolean): Permission {\n return Permission.of(\n () => this.native?.getPermissionsAsync(writeOnly) ?? Promise.resolve(UNAVAILABLE),\n () => this.native?.requestPermissionsAsync(writeOnly) ?? Promise.resolve(UNAVAILABLE),\n );\n }\n}\n"]}
package/dist/native.d.ts CHANGED
@@ -27,6 +27,8 @@ export declare class MissingModuleError extends Error {
27
27
  readonly module: string;
28
28
  constructor(module: string, platform: ModulePlatform, cause?: unknown);
29
29
  }
30
+ /** The platform the app is running on, or null in Node, where there is none to ask. */
31
+ export declare function currentPlatform(): ModulePlatform | null;
30
32
  /**
31
33
  * The Expo module a service needs.
32
34
  *
@@ -37,6 +39,7 @@ export declare class MissingModuleError extends Error {
37
39
  * goes inert, as code shared across platforms needs it to.
38
40
  *
39
41
  * `load` answering null or undefined counts as missing too: `requireOptionalNativeModule` answers
40
- * null rather than throwing.
42
+ * null rather than throwing. So does a package whose native half is not in the build, which `load`
43
+ * is not called for: see `nativeHalfMissing`.
41
44
  */
42
45
  export declare function expoModule<T>(module: string, load: () => T | null | undefined, platforms?: readonly ModulePlatform[]): T | null;
package/dist/native.js CHANGED
@@ -40,9 +40,64 @@ export class MissingModuleError extends Error {
40
40
  }
41
41
  }
42
42
  /** The platform the app is running on, or null in Node, where there is none to ask. */
43
- function currentPlatform() {
43
+ export function currentPlatform() {
44
44
  return optional(() => require('react-native').Platform.OS);
45
45
  }
46
+ /**
47
+ * For each Expo package whose JavaScript requires a native module as it is evaluated, and so
48
+ * throws when its native half is not in the build, one of those native modules. Any one of them
49
+ * says whether the package's native half is there, and each of these is registered on both iOS and
50
+ * Android.
51
+ */
52
+ const NATIVE_MODULES = {
53
+ 'expo-application': 'ExpoApplication',
54
+ 'expo-asset': 'ExpoAsset',
55
+ 'expo-audio': 'ExpoAudio',
56
+ 'expo-background-task': 'ExpoBackgroundTask',
57
+ 'expo-battery': 'ExpoBattery',
58
+ 'expo-brightness': 'ExpoBrightness',
59
+ 'expo-clipboard': 'ExpoClipboard',
60
+ 'expo-crypto': 'ExpoCrypto',
61
+ 'expo-device': 'ExpoDevice',
62
+ 'expo-document-picker': 'ExpoDocumentPicker',
63
+ 'expo-file-system': 'FileSystem',
64
+ 'expo-foldables': 'ExpoFoldables',
65
+ 'expo-font': 'ExpoFontLoader',
66
+ 'expo-image-manipulator': 'ExpoImageManipulator',
67
+ 'expo-image-picker': 'ExponentImagePicker',
68
+ 'expo-keep-awake': 'ExpoKeepAwake',
69
+ 'expo-local-authentication': 'ExpoLocalAuthentication',
70
+ 'expo-local-llm': 'ExpoLocalLlm',
71
+ 'expo-localization': 'ExpoLocalization',
72
+ 'expo-location': 'ExpoLocation',
73
+ 'expo-media-library': 'ExpoMediaLibrary',
74
+ 'expo-network': 'ExpoNetwork',
75
+ 'expo-notifications': 'ExpoNotificationPresenter',
76
+ 'expo-screen-capture': 'ExpoScreenCapture',
77
+ 'expo-screen-orientation': 'ExpoScreenOrientation',
78
+ 'expo-secure-store': 'ExpoSecureStore',
79
+ 'expo-sensors': 'ExponentAccelerometer',
80
+ 'expo-sqlite': 'ExpoSQLite',
81
+ 'expo-store-review': 'ExpoStoreReview',
82
+ 'expo-tracking-transparency': 'ExpoTrackingTransparency',
83
+ 'expo-updates': 'ExpoUpdates',
84
+ 'expo-video': 'ExpoVideo',
85
+ 'expo-web-browser': 'ExpoWebBrowser',
86
+ };
87
+ /**
88
+ * Whether `module`'s native half is missing from the build on `platform`, asked of Expo before its
89
+ * JavaScript is evaluated. That JavaScript throws while it is being evaluated when its native module
90
+ * is not there, and Metro reports that as fatal, before any `catch` here, whenever it is not inside
91
+ * another module's load: in a service's factory, say. Not on the web, where a package registers its
92
+ * module only once it is evaluated.
93
+ */
94
+ function nativeHalfMissing(module, platform) {
95
+ const name = NATIVE_MODULES[module];
96
+ if (!name || platform === null || platform === 'web')
97
+ return false;
98
+ const core = optional(() => require('expo-modules-core'));
99
+ return core !== null && !core.requireOptionalNativeModule(name);
100
+ }
46
101
  /**
47
102
  * The Expo module a service needs.
48
103
  *
@@ -53,19 +108,20 @@ function currentPlatform() {
53
108
  * goes inert, as code shared across platforms needs it to.
54
109
  *
55
110
  * `load` answering null or undefined counts as missing too: `requireOptionalNativeModule` answers
56
- * null rather than throwing.
111
+ * null rather than throwing. So does a package whose native half is not in the build, which `load`
112
+ * is not called for: see `nativeHalfMissing`.
57
113
  */
58
114
  export function expoModule(module, load, platforms = ['ios', 'android']) {
115
+ const platform = currentPlatform();
59
116
  let failure;
60
117
  try {
61
- const loaded = load();
118
+ const loaded = nativeHalfMissing(module, platform) ? null : load();
62
119
  if (loaded != null)
63
120
  return loaded;
64
121
  }
65
122
  catch (error) {
66
123
  failure = error;
67
124
  }
68
- const platform = currentPlatform();
69
125
  if (platform === null || !platforms.includes(platform))
70
126
  return null;
71
127
  throw new MissingModuleError(module, platform, failure);