expo-harmony-toolkit 1.7.0 → 1.7.2

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.
@@ -23,6 +23,7 @@ const uiStack_1 = require("../data/uiStack");
23
23
  const project_1 = require("./project");
24
24
  const javascriptDependencies_1 = require("./javascriptDependencies");
25
25
  const report_1 = require("./report");
26
+ const signing_1 = require("./signing");
26
27
  const TEMPLATE_ROOT = path_1.default.resolve(__dirname, '..', '..', 'templates', 'harmony');
27
28
  const TEMPLATE_FILE_PATHS = [
28
29
  'README.md',
@@ -149,7 +150,9 @@ async function syncProjectTemplate(projectRoot, force = false, options = {}) {
149
150
  async function buildManagedFiles(loadedProject, identifiers, previousToolkitConfig) {
150
151
  const hasExpoRouter = usesExpoRouter(loadedProject.packageJson);
151
152
  const enabledCapabilities = (0, capabilities_1.getCapabilityDefinitionsForProject)(loadedProject.packageJson);
153
+ const hasManagedExpoHarmonyPackage = enabledCapabilities.some((capability) => capability.runtimeMode !== 'shim');
152
154
  const requestedHarmonyPermissions = (0, capabilities_1.collectCapabilityHarmonyPermissions)(loadedProject.packageJson);
155
+ const signingLocalConfig = await (0, signing_1.readSigningLocalConfig)(loadedProject.projectRoot);
153
156
  const hvigorPluginFilename = await (0, project_1.resolveRnohHvigorPluginFilename)(loadedProject.projectRoot);
154
157
  const renderedHarmonyRootPackage = renderTemplate(await fs_extra_1.default.readFile(path_1.default.join(TEMPLATE_ROOT, 'oh-package.json5'), 'utf8'), loadedProject, identifiers, hvigorPluginFilename);
155
158
  const templateFiles = await Promise.all(TEMPLATE_FILE_PATHS.map(async (relativePath) => {
@@ -161,11 +164,21 @@ async function buildManagedFiles(loadedProject, identifiers, previousToolkitConf
161
164
  : renderTemplate(rawContents.toString('utf8'), loadedProject, identifiers, hvigorPluginFilename);
162
165
  return {
163
166
  relativePath: path_1.default.join('harmony', relativePath),
164
- contents: relativePath === 'entry/src/main/module.json5'
165
- ? renderEntryModuleConfig(identifiers.entryModuleName, requestedHarmonyPermissions)
166
- : relativePath === 'entry/src/main/resources/base/element/string.json'
167
- ? renderEntryStringResources(`${identifiers.appName} official minimal Harmony sample`, identifiers.appName, requestedHarmonyPermissions)
168
- : contents,
167
+ contents: relativePath === 'build-profile.json5'
168
+ ? (0, signing_1.mergeSigningLocalConfigIntoBuildProfile)(contents.toString('utf8'), signingLocalConfig)
169
+ : relativePath === 'entry/src/main/module.json5'
170
+ ? renderEntryModuleConfig(identifiers.entryModuleName, requestedHarmonyPermissions)
171
+ : relativePath === 'entry/src/main/resources/base/element/string.json'
172
+ ? renderEntryStringResources(`${identifiers.appName} official minimal Harmony sample`, identifiers.appName, requestedHarmonyPermissions)
173
+ : relativePath === 'entry/src/main/ets/PackageProvider.ets'
174
+ ? renderPackageProvider({
175
+ hasManagedExpoHarmonyPackage,
176
+ })
177
+ : relativePath === 'entry/src/main/cpp/PackageProvider.cpp'
178
+ ? renderPackageProviderCpp({
179
+ hasManagedExpoHarmonyPackage,
180
+ })
181
+ : contents,
169
182
  binary,
170
183
  };
171
184
  }));
@@ -179,7 +192,13 @@ async function buildManagedFiles(loadedProject, identifiers, previousToolkitConf
179
192
  rnohCliVersion: constants_1.RNOH_CLI_VERSION,
180
193
  bundleName: identifiers.bundleName,
181
194
  entryModuleName: identifiers.entryModuleName,
182
- capabilities: enabledCapabilities.map((capability) => capability.id),
195
+ capabilities: enabledCapabilities.map((capability) => ({
196
+ id: capability.id,
197
+ packageName: capability.packageName,
198
+ supportTier: capability.supportTier,
199
+ runtimeMode: capability.runtimeMode,
200
+ evidence: { ...capability.evidence },
201
+ })),
183
202
  requestedHarmonyPermissions,
184
203
  project: {
185
204
  name: loadedProject.expoConfig.name ?? identifiers.appName,
@@ -224,6 +243,34 @@ async function buildManagedFiles(loadedProject, identifiers, previousToolkitConf
224
243
  },
225
244
  ]
226
245
  : []),
246
+ ...(hasManagedExpoHarmonyPackage
247
+ ? [
248
+ {
249
+ relativePath: path_1.default.join('harmony', 'entry', 'src', 'main', 'cpp', 'expoHarmony', 'ExpoHarmonyPackage.h'),
250
+ contents: renderExpoHarmonyCppPackage(),
251
+ },
252
+ {
253
+ relativePath: path_1.default.join('harmony', 'entry', 'src', 'main', 'ets', 'expoHarmony', 'ExpoHarmonyPackage.ets'),
254
+ contents: renderExpoHarmonyPackage(),
255
+ },
256
+ {
257
+ relativePath: path_1.default.join('harmony', 'entry', 'src', 'main', 'ets', 'expoHarmony', 'ExpoHarmonyFileSystemTurboModule.ts'),
258
+ contents: renderExpoHarmonyFileSystemTurboModule(),
259
+ },
260
+ {
261
+ relativePath: path_1.default.join('harmony', 'entry', 'src', 'main', 'ets', 'expoHarmony', 'ExpoHarmonyImagePickerTurboModule.ts'),
262
+ contents: renderExpoHarmonyImagePickerTurboModule(),
263
+ },
264
+ {
265
+ relativePath: path_1.default.join('harmony', 'entry', 'src', 'main', 'ets', 'expoHarmony', 'ExpoHarmonyLocationTurboModule.ts'),
266
+ contents: renderExpoHarmonyLocationTurboModule(),
267
+ },
268
+ {
269
+ relativePath: path_1.default.join('harmony', 'entry', 'src', 'main', 'ets', 'expoHarmony', 'ExpoHarmonyCameraTurboModule.ts'),
270
+ contents: renderExpoHarmonyCameraTurboModule(),
271
+ },
272
+ ]
273
+ : []),
227
274
  {
228
275
  relativePath: path_1.default.join(constants_1.GENERATED_DIR, constants_1.TOOLKIT_CONFIG_FILENAME),
229
276
  contents: JSON.stringify(toolkitConfig, null, 2) + '\n',
@@ -669,11 +716,17 @@ function getHarmonyPermissionReasonValue(permissionName) {
669
716
  switch (permissionName) {
670
717
  case 'ohos.permission.CAMERA':
671
718
  return 'Camera access is required for Harmony preview image capture flows.';
719
+ case 'ohos.permission.MICROPHONE':
720
+ return 'Microphone access is required for Harmony preview video recording flows.';
672
721
  case 'ohos.permission.READ_IMAGEVIDEO':
673
722
  return 'Media library access is required for Harmony preview image selection flows.';
674
723
  case 'ohos.permission.LOCATION':
675
724
  case 'ohos.permission.APPROXIMATELY_LOCATION':
676
725
  return 'Location access is required for Harmony preview location flows.';
726
+ case 'ohos.permission.LOCATION_IN_BACKGROUND':
727
+ return 'Background location access is required for Harmony preview background location flows.';
728
+ case 'ohos.permission.ACCELEROMETER':
729
+ return 'Motion sensor access is required for Harmony preview heading flows.';
677
730
  case 'ohos.permission.NOTIFICATION_CONTROLLER':
678
731
  return 'Notification access is required for Harmony preview notification flows.';
679
732
  default:
@@ -683,6 +736,7 @@ function getHarmonyPermissionReasonValue(permissionName) {
683
736
  function getHarmonyPermissionWhen(permissionName) {
684
737
  switch (permissionName) {
685
738
  case 'ohos.permission.NOTIFICATION_CONTROLLER':
739
+ case 'ohos.permission.LOCATION_IN_BACKGROUND':
686
740
  return 'always';
687
741
  default:
688
742
  return 'inuse';
@@ -711,16 +765,299 @@ function renderEntryStringResources(appDescription, appName, requestedHarmonyPer
711
765
  string: stringEntries,
712
766
  }, null, 2) + '\n';
713
767
  }
768
+ function renderPackageProvider(options) {
769
+ const managedImports = options.hasManagedExpoHarmonyPackage
770
+ ? "import { ExpoHarmonyPackage } from './expoHarmony/ExpoHarmonyPackage';\n"
771
+ : '';
772
+ const managedPackages = options.hasManagedExpoHarmonyPackage ? ', new ExpoHarmonyPackage(ctx)' : '';
773
+ return `import type { RNPackageContext, RNPackage } from '@rnoh/react-native-openharmony';
774
+ import { createRNOHPackages as createRNOHPackagesAutolinking } from './RNOHPackagesFactory';
775
+ ${managedImports}
776
+ export function getRNOHPackages(ctx: RNPackageContext): RNPackage[] {
777
+ return [...createRNOHPackagesAutolinking(ctx)${managedPackages}];
778
+ }
779
+ `;
780
+ }
781
+ function renderPackageProviderCpp(options) {
782
+ const managedInclude = options.hasManagedExpoHarmonyPackage
783
+ ? '#include "expoHarmony/ExpoHarmonyPackage.h"\n'
784
+ : '';
785
+ const managedPackage = options.hasManagedExpoHarmonyPackage
786
+ ? ' packages.push_back(std::make_shared<ExpoHarmonyPackage>(ctx));\n'
787
+ : '';
788
+ return `#include "RNOH/PackageProvider.h"
789
+ #include "RNOHPackagesFactory.h"
790
+ #include "generated/RNOHGeneratedPackage.h"
791
+ ${managedInclude}
792
+ using namespace rnoh;
793
+
794
+ std::vector<std::shared_ptr<Package>> PackageProvider::getPackages(
795
+ Package::Context ctx) {
796
+ auto packages = createRNOHPackages(ctx);
797
+ packages.push_back(std::make_shared<RNOHGeneratedPackage>(ctx));
798
+ ${managedPackage} return packages;
799
+ }
800
+ `;
801
+ }
802
+ function renderExpoHarmonyCppPackage() {
803
+ return `#pragma once
804
+
805
+ #include <ReactCommon/TurboModule.h>
806
+ #include <react/renderer/components/view/ConcreteViewShadowNode.h>
807
+ #include <react/renderer/core/ConcreteComponentDescriptor.h>
808
+ #include "RNOH/ArkTSTurboModule.h"
809
+ #include "RNOH/Package.h"
810
+ #include "RNOHCorePackage/ComponentBinders/ViewComponentJSIBinder.h"
811
+
812
+ namespace rnoh {
813
+ using namespace facebook;
814
+
815
+ inline constexpr char ExpoHarmonyCameraViewComponentName[] =
816
+ "ExpoHarmonyCameraView";
817
+ using ExpoHarmonyCameraViewShadowNode =
818
+ react::ConcreteViewShadowNode<ExpoHarmonyCameraViewComponentName>;
819
+ using ExpoHarmonyCameraViewComponentDescriptor =
820
+ react::ConcreteComponentDescriptor<ExpoHarmonyCameraViewShadowNode>;
821
+
822
+ static jsi::Value __hostFunction_ExpoHarmonyFileSystemTurboModule_getConstants(
823
+ jsi::Runtime& rt,
824
+ react::TurboModule& turboModule,
825
+ const jsi::Value* args,
826
+ size_t count) {
827
+ return static_cast<ArkTSTurboModule&>(turboModule)
828
+ .call(rt, "getConstants", args, count);
829
+ }
830
+
831
+ static jsi::Value __hostFunction_ExpoHarmonyImagePickerTurboModule_getConstants(
832
+ jsi::Runtime& rt,
833
+ react::TurboModule& turboModule,
834
+ const jsi::Value* args,
835
+ size_t count) {
836
+ return static_cast<ArkTSTurboModule&>(turboModule)
837
+ .call(rt, "getConstants", args, count);
838
+ }
839
+
840
+ static jsi::Value __hostFunction_ExpoHarmonyLocationTurboModule_getConstants(
841
+ jsi::Runtime& rt,
842
+ react::TurboModule& turboModule,
843
+ const jsi::Value* args,
844
+ size_t count) {
845
+ return static_cast<ArkTSTurboModule&>(turboModule)
846
+ .call(rt, "getConstants", args, count);
847
+ }
848
+
849
+ static jsi::Value __hostFunction_ExpoHarmonyCameraTurboModule_getConstants(
850
+ jsi::Runtime& rt,
851
+ react::TurboModule& turboModule,
852
+ const jsi::Value* args,
853
+ size_t count) {
854
+ return static_cast<ArkTSTurboModule&>(turboModule)
855
+ .call(rt, "getConstants", args, count);
856
+ }
857
+
858
+ class JSI_EXPORT ExpoHarmonyFileSystemTurboModule : public ArkTSTurboModule {
859
+ public:
860
+ ExpoHarmonyFileSystemTurboModule(
861
+ const ArkTSTurboModule::Context ctx,
862
+ const std::string name)
863
+ : ArkTSTurboModule(ctx, name) {
864
+ methodMap_["getConstants"] = MethodMetadata{
865
+ 0, __hostFunction_ExpoHarmonyFileSystemTurboModule_getConstants};
866
+ methodMap_["getInfo"] = MethodMetadata{2, ARK_ASYNC_METHOD_CALLER(getInfo)};
867
+ methodMap_["readAsString"] = MethodMetadata{
868
+ 2, ARK_ASYNC_METHOD_CALLER(readAsString)};
869
+ methodMap_["writeAsString"] = MethodMetadata{
870
+ 3, ARK_ASYNC_METHOD_CALLER(writeAsString)};
871
+ methodMap_["deletePath"] = MethodMetadata{
872
+ 2, ARK_ASYNC_METHOD_CALLER(deletePath)};
873
+ methodMap_["makeDirectory"] = MethodMetadata{
874
+ 2, ARK_ASYNC_METHOD_CALLER(makeDirectory)};
875
+ methodMap_["readDirectory"] = MethodMetadata{
876
+ 1, ARK_ASYNC_METHOD_CALLER(readDirectory)};
877
+ methodMap_["copy"] = MethodMetadata{2, ARK_ASYNC_METHOD_CALLER(copy)};
878
+ methodMap_["move"] = MethodMetadata{2, ARK_ASYNC_METHOD_CALLER(move)};
879
+ methodMap_["download"] = MethodMetadata{
880
+ 3, ARK_ASYNC_METHOD_CALLER(download)};
881
+ }
882
+ };
883
+
884
+ class JSI_EXPORT ExpoHarmonyImagePickerTurboModule : public ArkTSTurboModule {
885
+ public:
886
+ ExpoHarmonyImagePickerTurboModule(
887
+ const ArkTSTurboModule::Context ctx,
888
+ const std::string name)
889
+ : ArkTSTurboModule(ctx, name) {
890
+ methodMap_["getConstants"] = MethodMetadata{
891
+ 0, __hostFunction_ExpoHarmonyImagePickerTurboModule_getConstants};
892
+ methodMap_["getMediaLibraryPermissionStatus"] = MethodMetadata{
893
+ 1, ARK_ASYNC_METHOD_CALLER(getMediaLibraryPermissionStatus)};
894
+ methodMap_["requestMediaLibraryPermission"] = MethodMetadata{
895
+ 1, ARK_ASYNC_METHOD_CALLER(requestMediaLibraryPermission)};
896
+ methodMap_["getCameraPermissionStatus"] = MethodMetadata{
897
+ 0, ARK_ASYNC_METHOD_CALLER(getCameraPermissionStatus)};
898
+ methodMap_["requestCameraPermission"] = MethodMetadata{
899
+ 0, ARK_ASYNC_METHOD_CALLER(requestCameraPermission)};
900
+ methodMap_["launchImageLibrary"] = MethodMetadata{
901
+ 1, ARK_ASYNC_METHOD_CALLER(launchImageLibrary)};
902
+ methodMap_["launchCamera"] = MethodMetadata{
903
+ 1, ARK_ASYNC_METHOD_CALLER(launchCamera)};
904
+ methodMap_["getPendingResult"] = MethodMetadata{
905
+ 0, ARK_ASYNC_METHOD_CALLER(getPendingResult)};
906
+ }
907
+ };
908
+
909
+ class JSI_EXPORT ExpoHarmonyLocationTurboModule : public ArkTSTurboModule {
910
+ public:
911
+ ExpoHarmonyLocationTurboModule(
912
+ const ArkTSTurboModule::Context ctx,
913
+ const std::string name)
914
+ : ArkTSTurboModule(ctx, name) {
915
+ methodMap_["getConstants"] = MethodMetadata{
916
+ 0, __hostFunction_ExpoHarmonyLocationTurboModule_getConstants};
917
+ methodMap_["getForegroundPermissionStatus"] = MethodMetadata{
918
+ 0, ARK_ASYNC_METHOD_CALLER(getForegroundPermissionStatus)};
919
+ methodMap_["requestForegroundPermission"] = MethodMetadata{
920
+ 0, ARK_ASYNC_METHOD_CALLER(requestForegroundPermission)};
921
+ methodMap_["getBackgroundPermissionStatus"] = MethodMetadata{
922
+ 0, ARK_ASYNC_METHOD_CALLER(getBackgroundPermissionStatus)};
923
+ methodMap_["requestBackgroundPermission"] = MethodMetadata{
924
+ 0, ARK_ASYNC_METHOD_CALLER(requestBackgroundPermission)};
925
+ methodMap_["hasServicesEnabled"] = MethodMetadata{
926
+ 0, ARK_ASYNC_METHOD_CALLER(hasServicesEnabled)};
927
+ methodMap_["getProviderStatus"] = MethodMetadata{
928
+ 0, ARK_ASYNC_METHOD_CALLER(getProviderStatus)};
929
+ methodMap_["getCurrentPosition"] = MethodMetadata{
930
+ 1, ARK_ASYNC_METHOD_CALLER(getCurrentPosition)};
931
+ methodMap_["getLastKnownPosition"] = MethodMetadata{
932
+ 1, ARK_ASYNC_METHOD_CALLER(getLastKnownPosition)};
933
+ methodMap_["geocode"] = MethodMetadata{1, ARK_ASYNC_METHOD_CALLER(geocode)};
934
+ methodMap_["reverseGeocode"] = MethodMetadata{
935
+ 1, ARK_ASYNC_METHOD_CALLER(reverseGeocode)};
936
+ methodMap_["startWatchPosition"] = MethodMetadata{
937
+ 2, ARK_ASYNC_METHOD_CALLER(startWatchPosition)};
938
+ methodMap_["stopWatchPosition"] = MethodMetadata{
939
+ 1, ARK_ASYNC_METHOD_CALLER(stopWatchPosition)};
940
+ methodMap_["getHeading"] = MethodMetadata{
941
+ 0, ARK_ASYNC_METHOD_CALLER(getHeading)};
942
+ methodMap_["startWatchHeading"] = MethodMetadata{
943
+ 1, ARK_ASYNC_METHOD_CALLER(startWatchHeading)};
944
+ methodMap_["stopWatchHeading"] = MethodMetadata{
945
+ 1, ARK_ASYNC_METHOD_CALLER(stopWatchHeading)};
946
+ }
947
+ };
948
+
949
+ class JSI_EXPORT ExpoHarmonyCameraTurboModule : public ArkTSTurboModule {
950
+ public:
951
+ ExpoHarmonyCameraTurboModule(
952
+ const ArkTSTurboModule::Context ctx,
953
+ const std::string name)
954
+ : ArkTSTurboModule(ctx, name) {
955
+ methodMap_["getConstants"] = MethodMetadata{
956
+ 0, __hostFunction_ExpoHarmonyCameraTurboModule_getConstants};
957
+ methodMap_["getCameraPermissionStatus"] = MethodMetadata{
958
+ 0, ARK_ASYNC_METHOD_CALLER(getCameraPermissionStatus)};
959
+ methodMap_["requestCameraPermission"] = MethodMetadata{
960
+ 0, ARK_ASYNC_METHOD_CALLER(requestCameraPermission)};
961
+ methodMap_["getMicrophonePermissionStatus"] = MethodMetadata{
962
+ 0, ARK_ASYNC_METHOD_CALLER(getMicrophonePermissionStatus)};
963
+ methodMap_["requestMicrophonePermission"] = MethodMetadata{
964
+ 0, ARK_ASYNC_METHOD_CALLER(requestMicrophonePermission)};
965
+ methodMap_["createPreview"] = MethodMetadata{
966
+ 1, ARK_ASYNC_METHOD_CALLER(createPreview)};
967
+ methodMap_["disposePreview"] = MethodMetadata{
968
+ 1, ARK_ASYNC_METHOD_CALLER(disposePreview)};
969
+ methodMap_["pausePreview"] = MethodMetadata{
970
+ 1, ARK_ASYNC_METHOD_CALLER(pausePreview)};
971
+ methodMap_["resumePreview"] = MethodMetadata{
972
+ 1, ARK_ASYNC_METHOD_CALLER(resumePreview)};
973
+ methodMap_["takePicture"] = MethodMetadata{
974
+ 1, ARK_ASYNC_METHOD_CALLER(takePicture)};
975
+ methodMap_["startRecording"] = MethodMetadata{
976
+ 1, ARK_ASYNC_METHOD_CALLER(startRecording)};
977
+ methodMap_["stopRecording"] = MethodMetadata{
978
+ 1, ARK_ASYNC_METHOD_CALLER(stopRecording)};
979
+ methodMap_["toggleRecording"] = MethodMetadata{
980
+ 1, ARK_ASYNC_METHOD_CALLER(toggleRecording)};
981
+ }
982
+ };
983
+
984
+ class ExpoHarmonyTurboModuleFactoryDelegate : public TurboModuleFactoryDelegate {
985
+ public:
986
+ SharedTurboModule createTurboModule(Context ctx, const std::string& name)
987
+ const override {
988
+ if (name == "ExpoHarmonyFileSystem") {
989
+ return std::make_shared<ExpoHarmonyFileSystemTurboModule>(ctx, name);
990
+ }
991
+ if (name == "ExpoHarmonyImagePicker") {
992
+ return std::make_shared<ExpoHarmonyImagePickerTurboModule>(ctx, name);
993
+ }
994
+ if (name == "ExpoHarmonyLocation") {
995
+ return std::make_shared<ExpoHarmonyLocationTurboModule>(ctx, name);
996
+ }
997
+ if (name == "ExpoHarmonyCamera") {
998
+ return std::make_shared<ExpoHarmonyCameraTurboModule>(ctx, name);
999
+ }
1000
+
1001
+ return nullptr;
1002
+ }
1003
+ };
1004
+
1005
+ class ExpoHarmonyCameraViewJSIBinder : public ViewComponentJSIBinder {
1006
+ protected:
1007
+ facebook::jsi::Object createNativeProps(facebook::jsi::Runtime& rt) override {
1008
+ auto nativeProps = ViewComponentJSIBinder::createNativeProps(rt);
1009
+ nativeProps.setProperty(rt, "viewId", "string");
1010
+ nativeProps.setProperty(rt, "facing", "string");
1011
+ nativeProps.setProperty(rt, "mode", "string");
1012
+ return nativeProps;
1013
+ }
1014
+ };
1015
+
1016
+ class ExpoHarmonyPackage : public Package {
1017
+ public:
1018
+ explicit ExpoHarmonyPackage(Package::Context ctx) : Package(ctx) {}
1019
+
1020
+ std::unique_ptr<TurboModuleFactoryDelegate> createTurboModuleFactoryDelegate()
1021
+ override {
1022
+ return std::make_unique<ExpoHarmonyTurboModuleFactoryDelegate>();
1023
+ }
1024
+
1025
+ std::vector<facebook::react::ComponentDescriptorProvider>
1026
+ createComponentDescriptorProviders() override {
1027
+ return {
1028
+ facebook::react::concreteComponentDescriptorProvider<
1029
+ ExpoHarmonyCameraViewComponentDescriptor>(),
1030
+ };
1031
+ }
1032
+
1033
+ ComponentJSIBinderByString createComponentJSIBinderByName() override {
1034
+ auto binder = std::make_shared<ExpoHarmonyCameraViewJSIBinder>();
1035
+ return {
1036
+ {"ExpoHarmonyCameraView", binder},
1037
+ };
1038
+ }
1039
+ };
1040
+ } // namespace rnoh
1041
+ `;
1042
+ }
714
1043
  function renderCapabilityModuleShim(capability) {
715
1044
  switch (capability.packageName) {
716
1045
  case 'expo-file-system':
717
- return renderExpoFileSystemPreviewShim(capability);
1046
+ return capability.runtimeMode !== 'shim'
1047
+ ? renderExpoFileSystemHarmonyAdapterShim(capability)
1048
+ : renderExpoFileSystemPreviewShim(capability);
718
1049
  case 'expo-image-picker':
719
- return renderExpoImagePickerPreviewShim(capability);
1050
+ return capability.runtimeMode !== 'shim'
1051
+ ? renderExpoImagePickerHarmonyAdapterShim(capability)
1052
+ : renderExpoImagePickerPreviewShim(capability);
720
1053
  case 'expo-location':
721
- return renderExpoLocationPreviewShim(capability);
1054
+ return capability.runtimeMode !== 'shim'
1055
+ ? renderExpoLocationHarmonyAdapterShim(capability)
1056
+ : renderExpoLocationPreviewShim(capability);
722
1057
  case 'expo-camera':
723
- return renderExpoCameraPreviewShim(capability);
1058
+ return capability.runtimeMode !== 'shim'
1059
+ ? renderExpoCameraHarmonyAdapterShim(capability)
1060
+ : renderExpoCameraPreviewShim(capability);
724
1061
  default:
725
1062
  return renderUnsupportedCapabilityShim(capability);
726
1063
  }
@@ -963,199 +1300,2839 @@ module.exports = new Proxy(
963
1300
  );
964
1301
  `;
965
1302
  }
966
- function renderExpoFileSystemPreviewShim(capability) {
967
- return `'use strict';
968
-
969
- const { CodedError } = require('expo-modules-core');
1303
+ function renderExpoHarmonyPackage() {
1304
+ return `import type {
1305
+ AnyThreadTurboModule,
1306
+ AnyThreadTurboModuleContext,
1307
+ UITurboModule,
1308
+ UITurboModuleContext,
1309
+ } from '@rnoh/react-native-openharmony';
1310
+ import {
1311
+ RNOHPackage,
1312
+ } from '@rnoh/react-native-openharmony';
1313
+ import { ExpoHarmonyFileSystemTurboModule } from './ExpoHarmonyFileSystemTurboModule';
1314
+ import { ExpoHarmonyImagePickerTurboModule } from './ExpoHarmonyImagePickerTurboModule';
1315
+ import { ExpoHarmonyLocationTurboModule } from './ExpoHarmonyLocationTurboModule';
1316
+ import { ExpoHarmonyCameraTurboModule } from './ExpoHarmonyCameraTurboModule';
1317
+
1318
+ export class ExpoHarmonyPackage extends RNOHPackage {
1319
+ override getAnyThreadTurboModuleFactoryByNameMap(): Map<
1320
+ string,
1321
+ (ctx: AnyThreadTurboModuleContext) => AnyThreadTurboModule | null
1322
+ > {
1323
+ return new Map<string, (ctx: AnyThreadTurboModuleContext) => AnyThreadTurboModule | null>()
1324
+ .set(
1325
+ ExpoHarmonyFileSystemTurboModule.NAME,
1326
+ (ctx) => new ExpoHarmonyFileSystemTurboModule(ctx),
1327
+ )
1328
+ .set(
1329
+ ExpoHarmonyLocationTurboModule.NAME,
1330
+ (ctx) => new ExpoHarmonyLocationTurboModule(ctx),
1331
+ );
1332
+ }
970
1333
 
971
- const PREVIEW_MESSAGE =
972
- '${capability.packageName} is routed through the Expo Harmony ${capability.supportTier} bridge. Bundling is supported, but runtime file-system behavior is not verified yet.';
1334
+ override getUITurboModuleFactoryByNameMap(): Map<
1335
+ string,
1336
+ (ctx: UITurboModuleContext) => UITurboModule | null
1337
+ > {
1338
+ return new Map<string, (ctx: UITurboModuleContext) => UITurboModule | null>()
1339
+ .set(
1340
+ ExpoHarmonyImagePickerTurboModule.NAME,
1341
+ (ctx) => new ExpoHarmonyImagePickerTurboModule(ctx),
1342
+ )
1343
+ .set(
1344
+ ExpoHarmonyCameraTurboModule.NAME,
1345
+ (ctx) => new ExpoHarmonyCameraTurboModule(ctx),
1346
+ );
1347
+ }
973
1348
 
974
- function createPreviewError(operationName) {
975
- return new CodedError(
976
- 'ERR_EXPO_HARMONY_PREVIEW',
977
- PREVIEW_MESSAGE + ' Attempted operation: ' + operationName + '.',
978
- );
1349
+ override getDebugName() {
1350
+ return 'expo-harmony';
1351
+ }
979
1352
  }
980
-
981
- async function unavailable(operationName) {
982
- throw createPreviewError(operationName);
1353
+ `;
983
1354
  }
1355
+ function renderExpoHarmonyFileSystemTurboModule() {
1356
+ return `import fs from '@ohos.file.fs';
1357
+ import { AnyThreadTurboModuleContext, AnyThreadTurboModule } from '@rnoh/react-native-openharmony/ts';
984
1358
 
985
- module.exports = {
986
- cacheDirectory: 'file:///expo-harmony/cache/',
987
- documentDirectory: 'file:///expo-harmony/document/',
988
- bundleDirectory: 'file:///expo-harmony/bundle/',
989
- EncodingType: {
990
- UTF8: 'utf8',
991
- Base64: 'base64',
992
- },
993
- FileSystemSessionType: {
994
- BACKGROUND: 0,
995
- FOREGROUND: 1,
996
- },
997
- getInfoAsync(path) {
998
- return unavailable('getInfoAsync(' + String(path) + ')');
999
- },
1000
- readAsStringAsync(path) {
1001
- return unavailable('readAsStringAsync(' + String(path) + ')');
1002
- },
1003
- writeAsStringAsync(path) {
1004
- return unavailable('writeAsStringAsync(' + String(path) + ')');
1005
- },
1006
- deleteAsync(path) {
1007
- return unavailable('deleteAsync(' + String(path) + ')');
1008
- },
1009
- makeDirectoryAsync(path) {
1010
- return unavailable('makeDirectoryAsync(' + String(path) + ')');
1011
- },
1012
- readDirectoryAsync(path) {
1013
- return unavailable('readDirectoryAsync(' + String(path) + ')');
1014
- },
1015
- copyAsync(options) {
1016
- return unavailable('copyAsync(' + JSON.stringify(options ?? {}) + ')');
1017
- },
1018
- moveAsync(options) {
1019
- return unavailable('moveAsync(' + JSON.stringify(options ?? {}) + ')');
1020
- },
1021
- downloadAsync(url) {
1022
- return unavailable('downloadAsync(' + String(url) + ')');
1023
- },
1359
+ type FileInfoOptions = {
1360
+ md5?: boolean;
1024
1361
  };
1025
- `;
1026
- }
1027
- function renderExpoImagePickerPreviewShim(capability) {
1028
- return `'use strict';
1029
1362
 
1030
- const { CodedError } = require('expo-modules-core');
1363
+ type WriteOptions = {
1364
+ encoding?: string;
1365
+ append?: boolean;
1366
+ };
1031
1367
 
1032
- const PREVIEW_MESSAGE =
1033
- '${capability.packageName} is routed through the Expo Harmony ${capability.supportTier} bridge. Bundling is supported, but picker and camera flows still need device-side validation.';
1368
+ type ReadOptions = {
1369
+ encoding?: string;
1370
+ position?: number;
1371
+ length?: number;
1372
+ };
1034
1373
 
1035
- function createPreviewError(operationName) {
1036
- return new CodedError(
1037
- 'ERR_EXPO_HARMONY_PREVIEW',
1038
- PREVIEW_MESSAGE + ' Attempted operation: ' + operationName + '.',
1039
- );
1040
- }
1374
+ type MakeDirectoryOptions = {
1375
+ intermediates?: boolean;
1376
+ };
1041
1377
 
1042
- async function unavailable(operationName) {
1043
- throw createPreviewError(operationName);
1044
- }
1378
+ type DeleteOptions = {
1379
+ idempotent?: boolean;
1380
+ };
1045
1381
 
1046
- module.exports = {
1047
- MediaTypeOptions: {
1048
- All: 'All',
1049
- Images: 'Images',
1050
- Videos: 'Videos',
1051
- },
1052
- UIImagePickerPresentationStyle: {
1053
- AUTOMATIC: 'automatic',
1054
- FULL_SCREEN: 'fullScreen',
1055
- PAGE_SHEET: 'pageSheet',
1056
- FORM_SHEET: 'formSheet',
1057
- CURRENT_CONTEXT: 'currentContext',
1058
- OVER_FULL_SCREEN: 'overFullScreen',
1059
- },
1060
- requestCameraPermissionsAsync() {
1061
- return unavailable('requestCameraPermissionsAsync');
1062
- },
1063
- requestMediaLibraryPermissionsAsync() {
1064
- return unavailable('requestMediaLibraryPermissionsAsync');
1065
- },
1066
- getCameraPermissionsAsync() {
1067
- return unavailable('getCameraPermissionsAsync');
1068
- },
1069
- getMediaLibraryPermissionsAsync() {
1070
- return unavailable('getMediaLibraryPermissionsAsync');
1071
- },
1072
- launchCameraAsync(options) {
1073
- return unavailable('launchCameraAsync(' + JSON.stringify(options ?? {}) + ')');
1074
- },
1075
- launchImageLibraryAsync(options) {
1076
- return unavailable('launchImageLibraryAsync(' + JSON.stringify(options ?? {}) + ')');
1077
- },
1382
+ type FileInfoResult = {
1383
+ exists: boolean;
1384
+ path: string;
1385
+ isDirectory: boolean;
1386
+ size?: number;
1387
+ modificationTime?: number;
1388
+ md5?: string;
1078
1389
  };
1079
- `;
1080
- }
1081
- function renderExpoLocationPreviewShim(capability) {
1082
- return `'use strict';
1083
1390
 
1084
- const { CodedError } = require('expo-modules-core');
1391
+ type DownloadOptions = {
1392
+ headers?: Record<string, string>;
1393
+ md5?: boolean;
1394
+ };
1085
1395
 
1086
- const PREVIEW_MESSAGE =
1087
- '${capability.packageName} is routed through the Expo Harmony ${capability.supportTier} bridge. Bundling is supported, but foreground permission, current-position, and watch flows still need device-side validation.';
1088
- const DEFAULT_PERMISSION_RESPONSE = {
1089
- status: 'undetermined',
1090
- granted: false,
1091
- canAskAgain: true,
1092
- expires: 'never',
1396
+ type DownloadResult = {
1397
+ uri: string;
1398
+ status: number;
1399
+ headers: Record<string, string>;
1400
+ md5?: string;
1093
1401
  };
1094
1402
 
1095
- function createPreviewError(operationName) {
1096
- return new CodedError(
1097
- 'ERR_EXPO_HARMONY_PREVIEW',
1098
- PREVIEW_MESSAGE + ' Attempted operation: ' + operationName + '.',
1099
- );
1100
- }
1403
+ export class ExpoHarmonyFileSystemTurboModule extends AnyThreadTurboModule {
1404
+ public static readonly NAME = 'ExpoHarmonyFileSystem';
1101
1405
 
1102
- async function unavailable(operationName) {
1103
- throw createPreviewError(operationName);
1104
- }
1406
+ public constructor(ctx: AnyThreadTurboModuleContext) {
1407
+ super(ctx);
1408
+ this.ensureManagedDirectoriesSync();
1409
+ }
1105
1410
 
1106
- function getPermissionResponse() {
1107
- return { ...DEFAULT_PERMISSION_RESPONSE };
1108
- }
1411
+ getConstants(): {
1412
+ documentDirectoryPath: string;
1413
+ cacheDirectoryPath: string;
1414
+ bundleDirectoryPath: string | null;
1415
+ } {
1416
+ const abilityContext = this.ctx.uiAbilityContext as {
1417
+ bundleCodeDir?: string;
1418
+ };
1109
1419
 
1110
- module.exports = {
1111
- Accuracy: {
1112
- Lowest: 1,
1113
- Low: 2,
1114
- Balanced: 3,
1115
- High: 4,
1116
- Highest: 5,
1117
- BestForNavigation: 6,
1118
- },
1119
- PermissionStatus: {
1120
- DENIED: 'denied',
1121
- GRANTED: 'granted',
1122
- UNDETERMINED: 'undetermined',
1123
- },
1124
- requestForegroundPermissionsAsync() {
1125
- return unavailable('requestForegroundPermissionsAsync');
1126
- },
1127
- getForegroundPermissionsAsync() {
1128
- return Promise.resolve(getPermissionResponse());
1129
- },
1130
- requestBackgroundPermissionsAsync() {
1131
- return unavailable('requestBackgroundPermissionsAsync');
1132
- },
1133
- getBackgroundPermissionsAsync() {
1134
- return Promise.resolve(getPermissionResponse());
1135
- },
1136
- hasServicesEnabledAsync() {
1137
- return Promise.resolve(false);
1138
- },
1139
- getCurrentPositionAsync(options) {
1140
- return unavailable('getCurrentPositionAsync(' + JSON.stringify(options ?? {}) + ')');
1141
- },
1142
- getLastKnownPositionAsync(options) {
1143
- return unavailable('getLastKnownPositionAsync(' + JSON.stringify(options ?? {}) + ')');
1144
- },
1145
- watchPositionAsync(options) {
1146
- return unavailable('watchPositionAsync(' + JSON.stringify(options ?? {}) + ')');
1147
- },
1148
- };
1149
- `;
1150
- }
1151
- function renderExpoCameraPreviewShim(capability) {
1152
- return `'use strict';
1420
+ return {
1421
+ documentDirectoryPath: this.documentDirectoryPath,
1422
+ cacheDirectoryPath: this.cacheDirectoryPath,
1423
+ bundleDirectoryPath:
1424
+ typeof abilityContext.bundleCodeDir === 'string' && abilityContext.bundleCodeDir.length > 0
1425
+ ? abilityContext.bundleCodeDir
1426
+ : null,
1427
+ };
1428
+ }
1153
1429
 
1154
- const React = require('react');
1155
- const { Text, View } = require('react-native');
1156
- const { CodedError } = require('expo-modules-core');
1430
+ async getInfo(path: string, options?: FileInfoOptions): Promise<FileInfoResult> {
1431
+ const normalizedPath = this.normalizeSandboxPath(path);
1432
+ const stat = await this.getStatOrNull(normalizedPath);
1157
1433
 
1158
- const PREVIEW_MESSAGE =
1434
+ if (!stat) {
1435
+ return {
1436
+ exists: false,
1437
+ path: normalizedPath,
1438
+ isDirectory: false,
1439
+ };
1440
+ }
1441
+
1442
+ return {
1443
+ exists: true,
1444
+ path: normalizedPath,
1445
+ isDirectory: stat.isDirectory(),
1446
+ size: Number(stat.size),
1447
+ modificationTime: Number(stat.mtime),
1448
+ md5:
1449
+ options?.md5 === true && !stat.isDirectory()
1450
+ ? this.computePreviewDigest(await this.readFileBytes(normalizedPath))
1451
+ : undefined,
1452
+ };
1453
+ }
1454
+
1455
+ async readAsString(path: string, options?: ReadOptions): Promise<string> {
1456
+ const normalizedPath = this.normalizeSandboxPath(path);
1457
+ const encoding = options?.encoding ?? 'utf8';
1458
+ const bytes = await this.readFileBytes(normalizedPath);
1459
+ const position = typeof options?.position === 'number' && options.position > 0
1460
+ ? Math.floor(options.position)
1461
+ : 0;
1462
+ const length = typeof options?.length === 'number' && options.length >= 0
1463
+ ? Math.floor(options.length)
1464
+ : bytes.length - position;
1465
+ const slicedBytes = bytes.slice(position, position + length);
1466
+
1467
+ if (encoding === 'base64') {
1468
+ return this.encodeBase64(slicedBytes);
1469
+ }
1470
+
1471
+ return this.decodeUtf8(slicedBytes);
1472
+ }
1473
+
1474
+ async writeAsString(path: string, contents: string, options?: WriteOptions): Promise<void> {
1475
+ const normalizedPath = this.normalizeSandboxPath(path);
1476
+ const encoding = options?.encoding ?? 'utf8';
1477
+
1478
+ await this.ensureParentDirectory(normalizedPath);
1479
+
1480
+ const file = await fs.open(
1481
+ normalizedPath,
1482
+ fs.OpenMode.READ_WRITE |
1483
+ fs.OpenMode.CREATE |
1484
+ (options?.append === true ? fs.OpenMode.APPEND : fs.OpenMode.TRUNC),
1485
+ );
1486
+
1487
+ try {
1488
+ if (encoding === 'base64') {
1489
+ await fs.write(file.fd, this.decodeBase64(contents).buffer);
1490
+ } else {
1491
+ await fs.write(file.fd, contents);
1492
+ }
1493
+ } finally {
1494
+ await fs.close(file);
1495
+ }
1496
+ }
1497
+
1498
+ async deletePath(path: string, options?: DeleteOptions): Promise<void> {
1499
+ const normalizedPath = this.normalizeSandboxPath(path);
1500
+ await this.deleteInternal(normalizedPath, options?.idempotent === true);
1501
+ }
1502
+
1503
+ async makeDirectory(path: string, options?: MakeDirectoryOptions): Promise<void> {
1504
+ const normalizedPath = this.normalizeSandboxPath(path);
1505
+ await fs.mkdir(normalizedPath, options?.intermediates === true);
1506
+ }
1507
+
1508
+ async readDirectory(path: string): Promise<string[]> {
1509
+ const normalizedPath = this.normalizeSandboxPath(path);
1510
+ const stat = await fs.stat(normalizedPath);
1511
+
1512
+ if (!stat.isDirectory()) {
1513
+ throw new Error('readDirectory expects a directory path.');
1514
+ }
1515
+
1516
+ return fs.listFile(normalizedPath);
1517
+ }
1518
+
1519
+ async copy(from: string, to: string): Promise<void> {
1520
+ const fromPath = this.normalizeSandboxPath(from);
1521
+ const toPath = this.normalizeSandboxPath(to);
1522
+ await this.copyInternal(fromPath, toPath);
1523
+ }
1524
+
1525
+ async move(from: string, to: string): Promise<void> {
1526
+ const fromPath = this.normalizeSandboxPath(from);
1527
+ const toPath = this.normalizeSandboxPath(to);
1528
+ const stat = await fs.stat(fromPath);
1529
+
1530
+ await this.ensureParentDirectory(toPath);
1531
+
1532
+ if (stat.isDirectory()) {
1533
+ await this.copyInternal(fromPath, toPath);
1534
+ await this.deleteInternal(fromPath, false);
1535
+ return;
1536
+ }
1537
+
1538
+ await fs.moveFile(fromPath, toPath);
1539
+ }
1540
+
1541
+ async download(url: string, destinationPath: string, options?: DownloadOptions): Promise<DownloadResult> {
1542
+ const normalizedDestinationPath = this.normalizeSandboxPath(destinationPath);
1543
+ await this.ensureParentDirectory(normalizedDestinationPath);
1544
+
1545
+ const response = await fetch(url, {
1546
+ headers: options?.headers ?? {},
1547
+ });
1548
+ const responseBuffer = new Uint8Array(await response.arrayBuffer());
1549
+ const file = await fs.open(
1550
+ normalizedDestinationPath,
1551
+ fs.OpenMode.READ_WRITE | fs.OpenMode.CREATE | fs.OpenMode.TRUNC,
1552
+ );
1553
+
1554
+ try {
1555
+ await fs.write(file.fd, responseBuffer.buffer);
1556
+ } finally {
1557
+ await fs.close(file);
1558
+ }
1559
+
1560
+ return {
1561
+ uri: normalizedDestinationPath,
1562
+ status: Number(response.status ?? 200),
1563
+ headers: {},
1564
+ md5: options?.md5 === true ? this.computePreviewDigest(responseBuffer) : undefined,
1565
+ };
1566
+ }
1567
+
1568
+ private get documentDirectoryPath(): string {
1569
+ return \`\${this.ctx.uiAbilityContext.filesDir}/expo-harmony/document\`;
1570
+ }
1571
+
1572
+ private get cacheDirectoryPath(): string {
1573
+ return \`\${this.ctx.uiAbilityContext.cacheDir}/expo-harmony/cache\`;
1574
+ }
1575
+
1576
+ private ensureManagedDirectoriesSync(): void {
1577
+ this.ensureDirectorySync(this.documentDirectoryPath);
1578
+ this.ensureDirectorySync(this.cacheDirectoryPath);
1579
+ }
1580
+
1581
+ private ensureDirectorySync(directoryPath: string): void {
1582
+ if (!fs.accessSync(directoryPath)) {
1583
+ fs.mkdirSync(directoryPath, true);
1584
+ }
1585
+ }
1586
+
1587
+ private async ensureParentDirectory(targetPath: string): Promise<void> {
1588
+ const parentPath = this.getParentPath(targetPath);
1589
+
1590
+ if (!parentPath) {
1591
+ return;
1592
+ }
1593
+
1594
+ const parentStat = await this.getStatOrNull(parentPath);
1595
+
1596
+ if (parentStat) {
1597
+ if (!parentStat.isDirectory()) {
1598
+ throw new Error(\`Expected parent path to be a directory: \${parentPath}\`);
1599
+ }
1600
+
1601
+ return;
1602
+ }
1603
+
1604
+ await fs.mkdir(parentPath, true);
1605
+ }
1606
+
1607
+ private getParentPath(targetPath: string): string | null {
1608
+ let normalizedPath = targetPath;
1609
+
1610
+ while (normalizedPath.length > 1 && normalizedPath.endsWith('/')) {
1611
+ normalizedPath = normalizedPath.slice(0, -1);
1612
+ }
1613
+ const slashIndex = normalizedPath.lastIndexOf('/');
1614
+
1615
+ if (slashIndex <= 0) {
1616
+ return null;
1617
+ }
1618
+
1619
+ return normalizedPath.slice(0, slashIndex);
1620
+ }
1621
+
1622
+ private async deleteInternal(targetPath: string, idempotent: boolean): Promise<void> {
1623
+ const stat = await this.getStatOrNull(targetPath);
1624
+
1625
+ if (!stat) {
1626
+ if (idempotent) {
1627
+ return;
1628
+ }
1629
+
1630
+ throw new Error(\`No file or directory exists at \${targetPath}.\`);
1631
+ }
1632
+
1633
+ if (stat.isDirectory()) {
1634
+ const entries = await fs.listFile(targetPath);
1635
+
1636
+ for (const entryName of entries) {
1637
+ await this.deleteInternal(\`\${targetPath}/\${entryName}\`, idempotent);
1638
+ }
1639
+
1640
+ await fs.rmdir(targetPath);
1641
+ return;
1642
+ }
1643
+
1644
+ await fs.unlink(targetPath);
1645
+ }
1646
+
1647
+ private async copyInternal(fromPath: string, toPath: string): Promise<void> {
1648
+ const stat = await fs.stat(fromPath);
1649
+
1650
+ await this.ensureParentDirectory(toPath);
1651
+
1652
+ if (stat.isDirectory()) {
1653
+ await fs.mkdir(toPath, true);
1654
+ const entries = await fs.listFile(fromPath);
1655
+
1656
+ for (const entryName of entries) {
1657
+ await this.copyInternal(\`\${fromPath}/\${entryName}\`, \`\${toPath}/\${entryName}\`);
1658
+ }
1659
+
1660
+ return;
1661
+ }
1662
+
1663
+ await fs.copyFile(fromPath, toPath);
1664
+ }
1665
+
1666
+ private async getStatOrNull(targetPath: string): Promise<fs.Stat | null> {
1667
+ try {
1668
+ return await fs.stat(targetPath);
1669
+ } catch (error) {
1670
+ if (this.isNoSuchFileError(error)) {
1671
+ return null;
1672
+ }
1673
+
1674
+ throw error;
1675
+ }
1676
+ }
1677
+
1678
+ private isNoSuchFileError(error: unknown): boolean {
1679
+ return (
1680
+ typeof error === 'object' &&
1681
+ error !== null &&
1682
+ 'code' in error &&
1683
+ Number((error as { code?: number }).code) === 13900002
1684
+ );
1685
+ }
1686
+
1687
+ private async readFileBytes(targetPath: string): Promise<Uint8Array> {
1688
+ const stat = await fs.stat(targetPath);
1689
+ const file = await fs.open(targetPath, fs.OpenMode.READ_ONLY);
1690
+ const buffer = new ArrayBuffer(Number(stat.size ?? 0));
1691
+
1692
+ try {
1693
+ await fs.read(file.fd, buffer);
1694
+ return new Uint8Array(buffer);
1695
+ } finally {
1696
+ await fs.close(file);
1697
+ }
1698
+ }
1699
+
1700
+ private decodeUtf8(bytes: Uint8Array): string {
1701
+ return new TextDecoder().decode(bytes);
1702
+ }
1703
+
1704
+ private encodeBase64(bytes: Uint8Array): string {
1705
+ let binary = '';
1706
+
1707
+ for (const byte of bytes) {
1708
+ binary += String.fromCharCode(byte);
1709
+ }
1710
+
1711
+ return btoa(binary);
1712
+ }
1713
+
1714
+ private decodeBase64(contents: string): Uint8Array {
1715
+ const binary = atob(contents);
1716
+ const bytes = new Uint8Array(binary.length);
1717
+
1718
+ for (let index = 0; index < binary.length; index += 1) {
1719
+ bytes[index] = binary.charCodeAt(index);
1720
+ }
1721
+
1722
+ return bytes;
1723
+ }
1724
+
1725
+ private computePreviewDigest(bytes: Uint8Array): string {
1726
+ let hash = 2166136261;
1727
+
1728
+ for (const byte of bytes) {
1729
+ hash ^= byte;
1730
+ hash = Math.imul(hash, 16777619);
1731
+ }
1732
+
1733
+ return String(hash >>> 0).padStart(32, '0').slice(-32);
1734
+ }
1735
+
1736
+ private normalizeSandboxPath(inputPath: string): string {
1737
+ if (typeof inputPath !== 'string' || inputPath.length === 0) {
1738
+ throw new Error('ExpoHarmonyFileSystem expected a non-empty sandbox path.');
1739
+ }
1740
+
1741
+ if (!inputPath.startsWith('/')) {
1742
+ throw new Error('ExpoHarmonyFileSystem accepts only absolute sandbox paths.');
1743
+ }
1744
+
1745
+ if (inputPath.includes('/../') || inputPath.endsWith('/..') || inputPath.includes('/./')) {
1746
+ throw new Error('ExpoHarmonyFileSystem does not accept relative path segments.');
1747
+ }
1748
+
1749
+ const allowedRoots = [
1750
+ this.ctx.uiAbilityContext.filesDir,
1751
+ this.ctx.uiAbilityContext.cacheDir,
1752
+ ].filter((value): value is string => typeof value === 'string' && value.length > 0);
1753
+
1754
+ const isAllowed = allowedRoots.some(
1755
+ (rootPath) => inputPath === rootPath || inputPath.startsWith(\`\${rootPath}/\`),
1756
+ );
1757
+
1758
+ if (!isAllowed) {
1759
+ throw new Error('ExpoHarmonyFileSystem accepts only app sandbox paths.');
1760
+ }
1761
+
1762
+ return inputPath;
1763
+ }
1764
+ }
1765
+ `;
1766
+ }
1767
+ function renderExpoHarmonyImagePickerTurboModule() {
1768
+ return `import type { Permissions } from '@ohos.abilityAccessCtrl';
1769
+ import abilityAccessCtrl from '@ohos.abilityAccessCtrl';
1770
+ import photoAccessHelper from '@ohos.file.photoAccessHelper';
1771
+ import picker from '@ohos.file.picker';
1772
+ import image from '@ohos.multimedia.image';
1773
+ import fs from '@ohos.file.fs';
1774
+ import { UITurboModuleContext, UITurboModule } from '@rnoh/react-native-openharmony/ts';
1775
+
1776
+ type PermissionResponse = {
1777
+ status: 'granted' | 'denied' | 'undetermined';
1778
+ granted: boolean;
1779
+ canAskAgain: boolean;
1780
+ expires: 'never';
1781
+ accessPrivileges?: 'all' | 'limited' | 'none';
1782
+ };
1783
+
1784
+ type LaunchImageLibraryOptions = {
1785
+ mediaTypes?: string | string[];
1786
+ allowsMultipleSelection?: boolean;
1787
+ selectionLimit?: number;
1788
+ };
1789
+
1790
+ type LaunchCameraOptions = {
1791
+ mediaTypes?: string | string[];
1792
+ cameraType?: string;
1793
+ };
1794
+
1795
+ type ImagePickerAsset = {
1796
+ uri: string;
1797
+ assetId: string | null;
1798
+ width: number;
1799
+ height: number;
1800
+ type: 'image' | 'video' | null;
1801
+ fileName: string | null;
1802
+ fileSize: number | null;
1803
+ mimeType: string | null;
1804
+ duration: number | null;
1805
+ exif: null;
1806
+ base64: null;
1807
+ };
1808
+
1809
+ type ImagePickerResult = {
1810
+ canceled: boolean;
1811
+ assets: ImagePickerAsset[] | null;
1812
+ };
1813
+
1814
+ export class ExpoHarmonyImagePickerTurboModule extends UITurboModule {
1815
+ public static readonly NAME = 'ExpoHarmonyImagePicker';
1816
+
1817
+ private readonly atManager = abilityAccessCtrl.createAtManager();
1818
+ private pendingResult: ImagePickerResult | null = null;
1819
+
1820
+ getConstants(): Record<string, never> {
1821
+ return {};
1822
+ }
1823
+
1824
+ async getMediaLibraryPermissionStatus(_writeOnly?: boolean): Promise<PermissionResponse> {
1825
+ return this.getPermissionResponse('ohos.permission.READ_IMAGEVIDEO', true);
1826
+ }
1827
+
1828
+ async requestMediaLibraryPermission(_writeOnly?: boolean): Promise<PermissionResponse> {
1829
+ return this.requestPermissionResponse('ohos.permission.READ_IMAGEVIDEO', true);
1830
+ }
1831
+
1832
+ async getCameraPermissionStatus(): Promise<PermissionResponse> {
1833
+ return this.getPermissionResponse('ohos.permission.CAMERA', false);
1834
+ }
1835
+
1836
+ async requestCameraPermission(): Promise<PermissionResponse> {
1837
+ return this.requestPermissionResponse('ohos.permission.CAMERA', false);
1838
+ }
1839
+
1840
+ async launchImageLibrary(options?: LaunchImageLibraryOptions): Promise<ImagePickerResult> {
1841
+ await this.ensurePermissionGranted('ohos.permission.READ_IMAGEVIDEO', true);
1842
+
1843
+ const photoPicker = new photoAccessHelper.PhotoViewPicker();
1844
+ const selection = await photoPicker.select(this.createPhotoSelectOptions(options));
1845
+ let selectedUris = this.normalizeSelectedUris(selection?.photoUris);
1846
+
1847
+ if (selectedUris.length === 0) {
1848
+ selectedUris = await this.launchLegacyPhotoPicker(options);
1849
+ }
1850
+
1851
+ if (selectedUris.length === 0) {
1852
+ return this.createCanceledResult();
1853
+ }
1854
+
1855
+ const authorizedUris = await this.requestAuthorizedUris(selectedUris);
1856
+ const assets = await Promise.all(
1857
+ authorizedUris.map((uri, index) =>
1858
+ this.createImagePickerAsset(uri, selectedUris[index] ?? uri, this.inferAssetTypeFromMediaTypes(options?.mediaTypes)),
1859
+ ),
1860
+ );
1861
+
1862
+ const result = {
1863
+ canceled: false,
1864
+ assets,
1865
+ };
1866
+ this.pendingResult = result;
1867
+ return result;
1868
+ }
1869
+
1870
+ async launchCamera(options?: LaunchCameraOptions): Promise<ImagePickerResult> {
1871
+ const requestedAssetType = this.inferAssetTypeFromMediaTypes(options?.mediaTypes);
1872
+
1873
+ await this.ensurePermissionGranted('ohos.permission.CAMERA', false);
1874
+ await this.ensurePermissionGranted('ohos.permission.READ_IMAGEVIDEO', true);
1875
+ if (requestedAssetType === 'video') {
1876
+ await this.ensurePermissionGranted('ohos.permission.MICROPHONE', false);
1877
+ }
1878
+
1879
+ const cameraEntryPicker = new photoAccessHelper.PhotoViewPicker();
1880
+ const selection = await cameraEntryPicker.select(
1881
+ this.createPhotoSelectOptions(
1882
+ {
1883
+ mediaTypes: requestedAssetType === 'video' ? 'videos' : 'images',
1884
+ allowsMultipleSelection: false,
1885
+ },
1886
+ true,
1887
+ ),
1888
+ );
1889
+ const selectedUris = this.normalizeSelectedUris(selection?.photoUris);
1890
+
1891
+ if (selectedUris.length === 0) {
1892
+ return this.createCanceledResult();
1893
+ }
1894
+
1895
+ const authorizedUris = await this.requestAuthorizedUris(selectedUris);
1896
+ const assetUri = authorizedUris[0] ?? selectedUris[0];
1897
+
1898
+ const result = {
1899
+ canceled: false,
1900
+ assets: [await this.createImagePickerAsset(assetUri, selectedUris[0], requestedAssetType ?? 'image')],
1901
+ };
1902
+ this.pendingResult = result;
1903
+ return result;
1904
+ }
1905
+
1906
+ async getPendingResult(): Promise<ImagePickerResult | null> {
1907
+ const result = this.pendingResult;
1908
+ this.pendingResult = null;
1909
+ return result;
1910
+ }
1911
+
1912
+ private async ensurePermissionGranted(
1913
+ permissionName: Permissions,
1914
+ isMediaLibraryPermission: boolean,
1915
+ ): Promise<void> {
1916
+ const permissionResponse = await this.getPermissionResponse(permissionName, isMediaLibraryPermission);
1917
+
1918
+ if (permissionResponse.granted) {
1919
+ return;
1920
+ }
1921
+
1922
+ const requestedResponse = await this.requestPermissionResponse(permissionName, isMediaLibraryPermission);
1923
+
1924
+ if (!requestedResponse.granted) {
1925
+ throw new Error(\`Permission denied for \${permissionName}.\`);
1926
+ }
1927
+ }
1928
+
1929
+ private async getPermissionResponse(
1930
+ permissionName: Permissions,
1931
+ isMediaLibraryPermission: boolean,
1932
+ ): Promise<PermissionResponse> {
1933
+ return this.permissionResponseFromStatus(
1934
+ this.resolvePermissionStatus(permissionName),
1935
+ isMediaLibraryPermission,
1936
+ );
1937
+ }
1938
+
1939
+ private async requestPermissionResponse(
1940
+ permissionName: Permissions,
1941
+ isMediaLibraryPermission: boolean,
1942
+ ): Promise<PermissionResponse> {
1943
+ const requestResult = await this.atManager.requestPermissionsFromUser(
1944
+ this.ctx.uiAbilityContext,
1945
+ [permissionName],
1946
+ );
1947
+ const authResult = Array.isArray(requestResult.authResults)
1948
+ ? Number(requestResult.authResults[0] ?? abilityAccessCtrl.GrantStatus.PERMISSION_DENIED)
1949
+ : abilityAccessCtrl.GrantStatus.PERMISSION_DENIED;
1950
+
1951
+ if (authResult === abilityAccessCtrl.GrantStatus.PERMISSION_GRANTED) {
1952
+ return this.permissionResponseFromStatus(
1953
+ abilityAccessCtrl.PermissionStatus.GRANTED,
1954
+ isMediaLibraryPermission,
1955
+ );
1956
+ }
1957
+
1958
+ return this.permissionResponseFromStatus(
1959
+ abilityAccessCtrl.PermissionStatus.DENIED,
1960
+ isMediaLibraryPermission,
1961
+ );
1962
+ }
1963
+
1964
+ private resolvePermissionStatus(permissionName: Permissions): abilityAccessCtrl.PermissionStatus {
1965
+ const atManagerWithSelfStatus = this.atManager as abilityAccessCtrl.AtManager & {
1966
+ getSelfPermissionStatus?: (permission: Permissions) => abilityAccessCtrl.PermissionStatus;
1967
+ };
1968
+
1969
+ if (typeof atManagerWithSelfStatus.getSelfPermissionStatus === 'function') {
1970
+ return atManagerWithSelfStatus.getSelfPermissionStatus(permissionName);
1971
+ }
1972
+
1973
+ const accessTokenId = this.ctx.uiAbilityContext.abilityInfo.applicationInfo.accessTokenId;
1974
+ const grantStatus = this.atManager.checkAccessTokenSync(accessTokenId, permissionName);
1975
+
1976
+ return grantStatus === abilityAccessCtrl.GrantStatus.PERMISSION_GRANTED
1977
+ ? abilityAccessCtrl.PermissionStatus.GRANTED
1978
+ : abilityAccessCtrl.PermissionStatus.NOT_DETERMINED;
1979
+ }
1980
+
1981
+ private permissionResponseFromStatus(
1982
+ permissionStatus: abilityAccessCtrl.PermissionStatus,
1983
+ isMediaLibraryPermission: boolean,
1984
+ ): PermissionResponse {
1985
+ const granted = permissionStatus === abilityAccessCtrl.PermissionStatus.GRANTED;
1986
+ const denied =
1987
+ permissionStatus === abilityAccessCtrl.PermissionStatus.DENIED ||
1988
+ permissionStatus === abilityAccessCtrl.PermissionStatus.RESTRICTED ||
1989
+ permissionStatus === abilityAccessCtrl.PermissionStatus.INVALID;
1990
+
1991
+ return {
1992
+ status: granted ? 'granted' : denied ? 'denied' : 'undetermined',
1993
+ granted,
1994
+ canAskAgain: !denied,
1995
+ expires: 'never',
1996
+ ...(isMediaLibraryPermission
1997
+ ? {
1998
+ accessPrivileges: granted ? 'all' : 'none',
1999
+ }
2000
+ : {}),
2001
+ };
2002
+ }
2003
+
2004
+ private createPhotoSelectOptions(
2005
+ options?: LaunchImageLibraryOptions,
2006
+ isPhotoTakingSupported: boolean = false,
2007
+ ): photoAccessHelper.PhotoSelectOptions {
2008
+ const selectOptions = new photoAccessHelper.PhotoSelectOptions();
2009
+ selectOptions.MIMEType = this.resolvePhotoViewMimeType(options?.mediaTypes);
2010
+ selectOptions.maxSelectNumber =
2011
+ options?.allowsMultipleSelection === true
2012
+ ? this.resolveSelectionLimit(options?.selectionLimit)
2013
+ : 1;
2014
+ selectOptions.isSearchSupported = true;
2015
+ selectOptions.isPhotoTakingSupported = isPhotoTakingSupported;
2016
+ return selectOptions;
2017
+ }
2018
+
2019
+ private async launchLegacyPhotoPicker(options?: LaunchImageLibraryOptions): Promise<string[]> {
2020
+ const legacyPicker = new picker.PhotoViewPicker(this.ctx.uiAbilityContext);
2021
+ const legacyOptions = new picker.PhotoSelectOptions();
2022
+ legacyOptions.MIMEType = this.resolveLegacyPhotoViewMimeType(options?.mediaTypes);
2023
+ legacyOptions.maxSelectNumber =
2024
+ options?.allowsMultipleSelection === true
2025
+ ? this.resolveSelectionLimit(options?.selectionLimit)
2026
+ : 1;
2027
+
2028
+ const selection = await legacyPicker.select(legacyOptions);
2029
+ return this.normalizeSelectedUris(selection?.photoUris);
2030
+ }
2031
+
2032
+ private resolveSelectionLimit(selectionLimit?: number): number {
2033
+ if (typeof selectionLimit === 'number' && Number.isFinite(selectionLimit) && selectionLimit > 0) {
2034
+ return Math.floor(selectionLimit);
2035
+ }
2036
+
2037
+ return 20;
2038
+ }
2039
+
2040
+ private resolvePhotoViewMimeType(
2041
+ rawMediaTypes?: string | string[],
2042
+ ): photoAccessHelper.PhotoViewMIMETypes {
2043
+ const normalized = this.normalizeMediaTypes(rawMediaTypes);
2044
+
2045
+ if (normalized.includes('video') && !normalized.includes('image')) {
2046
+ return photoAccessHelper.PhotoViewMIMETypes.VIDEO_TYPE;
2047
+ }
2048
+
2049
+ if (normalized.includes('video') && normalized.includes('image')) {
2050
+ return photoAccessHelper.PhotoViewMIMETypes.IMAGE_VIDEO_TYPE;
2051
+ }
2052
+
2053
+ return photoAccessHelper.PhotoViewMIMETypes.IMAGE_TYPE;
2054
+ }
2055
+
2056
+ private resolveLegacyPhotoViewMimeType(
2057
+ rawMediaTypes?: string | string[],
2058
+ ): picker.PhotoViewMIMETypes {
2059
+ const normalized = this.normalizeMediaTypes(rawMediaTypes);
2060
+
2061
+ if (normalized.includes('video') && !normalized.includes('image')) {
2062
+ return picker.PhotoViewMIMETypes.VIDEO_TYPE;
2063
+ }
2064
+
2065
+ if (normalized.includes('video') && normalized.includes('image')) {
2066
+ return picker.PhotoViewMIMETypes.IMAGE_VIDEO_TYPE;
2067
+ }
2068
+
2069
+ return picker.PhotoViewMIMETypes.IMAGE_TYPE;
2070
+ }
2071
+
2072
+ private inferAssetTypeFromMediaTypes(
2073
+ rawMediaTypes?: string | string[],
2074
+ ): 'image' | 'video' | null {
2075
+ const normalized = this.normalizeMediaTypes(rawMediaTypes);
2076
+
2077
+ if (normalized.includes('video') && !normalized.includes('image')) {
2078
+ return 'video';
2079
+ }
2080
+
2081
+ if (normalized.includes('image')) {
2082
+ return 'image';
2083
+ }
2084
+
2085
+ return null;
2086
+ }
2087
+
2088
+ private normalizeMediaTypes(rawMediaTypes?: string | string[]): string[] {
2089
+ if (Array.isArray(rawMediaTypes)) {
2090
+ return Array.from(
2091
+ new Set(
2092
+ rawMediaTypes
2093
+ .map((value) => this.normalizeMediaTypeValue(value))
2094
+ .filter((value): value is string => value !== null),
2095
+ ),
2096
+ );
2097
+ }
2098
+
2099
+ const singleValue = this.normalizeMediaTypeValue(rawMediaTypes);
2100
+ return singleValue ? [singleValue] : ['image'];
2101
+ }
2102
+
2103
+ private normalizeMediaTypeValue(rawValue?: string): string | null {
2104
+ if (typeof rawValue !== 'string' || rawValue.length === 0) {
2105
+ return 'image';
2106
+ }
2107
+
2108
+ switch (rawValue) {
2109
+ case 'All':
2110
+ case 'all':
2111
+ case 'images':
2112
+ case 'image':
2113
+ case 'livePhotos':
2114
+ return 'image';
2115
+ case 'Videos':
2116
+ case 'videos':
2117
+ case 'video':
2118
+ return 'video';
2119
+ default:
2120
+ return rawValue.includes('video') ? 'video' : rawValue.includes('image') ? 'image' : null;
2121
+ }
2122
+ }
2123
+
2124
+ private normalizeSelectedUris(photoUris: Array<string> | undefined | null): string[] {
2125
+ if (!Array.isArray(photoUris)) {
2126
+ return [];
2127
+ }
2128
+
2129
+ return photoUris.filter(
2130
+ (value): value is string => typeof value === 'string' && value.length > 0,
2131
+ );
2132
+ }
2133
+
2134
+ private async requestAuthorizedUris(photoUris: string[]): Promise<string[]> {
2135
+ const helper = photoAccessHelper.getPhotoAccessHelper(this.ctx.uiAbilityContext);
2136
+
2137
+ try {
2138
+ const authorizedUris = await helper.requestPhotoUrisReadPermission(photoUris);
2139
+ const normalizedAuthorizedUris = this.normalizeSelectedUris(authorizedUris);
2140
+ return normalizedAuthorizedUris.length > 0 ? normalizedAuthorizedUris : photoUris;
2141
+ } catch (_error) {
2142
+ return photoUris;
2143
+ }
2144
+ }
2145
+
2146
+ private async createImagePickerAsset(
2147
+ assetUri: string,
2148
+ originalUri: string,
2149
+ fallbackType: 'image' | 'video' | null,
2150
+ ): Promise<ImagePickerAsset> {
2151
+ const inferredType = this.inferAssetTypeFromUri(assetUri, fallbackType);
2152
+ const imageSize =
2153
+ inferredType === 'image' ? await this.getImageSize(assetUri) : { width: 0, height: 0 };
2154
+ const fileSize = await this.getFileSize(assetUri);
2155
+ const fileName = this.extractFileName(assetUri) ?? this.extractFileName(originalUri);
2156
+
2157
+ return {
2158
+ uri: assetUri,
2159
+ assetId: originalUri,
2160
+ width: imageSize.width,
2161
+ height: imageSize.height,
2162
+ type: inferredType,
2163
+ fileName,
2164
+ fileSize,
2165
+ mimeType: this.inferMimeType(assetUri, inferredType),
2166
+ duration: inferredType === 'video' ? await this.getVideoDuration(assetUri) : null,
2167
+ exif: null,
2168
+ base64: null,
2169
+ };
2170
+ }
2171
+
2172
+ private inferAssetTypeFromUri(
2173
+ assetUri: string,
2174
+ fallbackType: 'image' | 'video' | null,
2175
+ ): 'image' | 'video' | null {
2176
+ const normalizedUri = assetUri.toLowerCase();
2177
+
2178
+ if (normalizedUri.match(/\\.(mp4|m4v|mov|3gp|webm)(\\?|#|$)/)) {
2179
+ return 'video';
2180
+ }
2181
+
2182
+ if (normalizedUri.match(/\\.(png|jpe?g|gif|bmp|webp|heic|heif)(\\?|#|$)/)) {
2183
+ return 'image';
2184
+ }
2185
+
2186
+ return fallbackType;
2187
+ }
2188
+
2189
+ private inferMimeType(
2190
+ assetUri: string,
2191
+ assetType: 'image' | 'video' | null,
2192
+ ): string | null {
2193
+ const normalizedUri = assetUri.toLowerCase();
2194
+
2195
+ if (normalizedUri.endsWith('.png')) {
2196
+ return 'image/png';
2197
+ }
2198
+ if (normalizedUri.endsWith('.gif')) {
2199
+ return 'image/gif';
2200
+ }
2201
+ if (normalizedUri.endsWith('.webp')) {
2202
+ return 'image/webp';
2203
+ }
2204
+ if (normalizedUri.endsWith('.bmp')) {
2205
+ return 'image/bmp';
2206
+ }
2207
+ if (normalizedUri.endsWith('.heic')) {
2208
+ return 'image/heic';
2209
+ }
2210
+ if (normalizedUri.endsWith('.heif')) {
2211
+ return 'image/heif';
2212
+ }
2213
+ if (normalizedUri.match(/\\.jpe?g(\\?|#|$)/)) {
2214
+ return 'image/jpeg';
2215
+ }
2216
+ if (normalizedUri.match(/\\.(mp4|m4v)(\\?|#|$)/)) {
2217
+ return 'video/mp4';
2218
+ }
2219
+ if (normalizedUri.match(/\\.mov(\\?|#|$)/)) {
2220
+ return 'video/quicktime';
2221
+ }
2222
+ if (normalizedUri.match(/\\.webm(\\?|#|$)/)) {
2223
+ return 'video/webm';
2224
+ }
2225
+
2226
+ return assetType === 'video' ? 'video/*' : assetType === 'image' ? 'image/*' : null;
2227
+ }
2228
+
2229
+ private extractFileName(assetUri: string): string | null {
2230
+ if (typeof assetUri !== 'string' || assetUri.length === 0) {
2231
+ return null;
2232
+ }
2233
+
2234
+ const sanitizedUri = assetUri.split('?')[0]?.split('#')[0] ?? assetUri;
2235
+ const lastSlashIndex = sanitizedUri.lastIndexOf('/');
2236
+ const rawFileName =
2237
+ lastSlashIndex >= 0 ? sanitizedUri.slice(lastSlashIndex + 1) : sanitizedUri;
2238
+
2239
+ if (rawFileName.length === 0) {
2240
+ return null;
2241
+ }
2242
+
2243
+ try {
2244
+ return decodeURIComponent(rawFileName);
2245
+ } catch (_error) {
2246
+ return rawFileName;
2247
+ }
2248
+ }
2249
+
2250
+ private async getImageSize(assetUri: string): Promise<{ width: number; height: number }> {
2251
+ let imageSource: image.ImageSource | null = null;
2252
+
2253
+ try {
2254
+ imageSource = image.createImageSource(assetUri);
2255
+ const imageInfo = await imageSource.getImageInfo();
2256
+ return {
2257
+ width: Number(imageInfo.size?.width ?? 0),
2258
+ height: Number(imageInfo.size?.height ?? 0),
2259
+ };
2260
+ } catch (_error) {
2261
+ return {
2262
+ width: 0,
2263
+ height: 0,
2264
+ };
2265
+ } finally {
2266
+ if (imageSource) {
2267
+ try {
2268
+ await imageSource.release();
2269
+ } catch (_error) {
2270
+ // Ignore cleanup errors from ImageSource release.
2271
+ }
2272
+ }
2273
+ }
2274
+ }
2275
+
2276
+ private async getFileSize(assetUri: string): Promise<number | null> {
2277
+ const fsTarget = this.resolveFsTarget(assetUri);
2278
+
2279
+ if (!fsTarget) {
2280
+ return null;
2281
+ }
2282
+
2283
+ try {
2284
+ const stat = await fs.stat(fsTarget);
2285
+ return Number(stat.size ?? 0);
2286
+ } catch (_error) {
2287
+ return null;
2288
+ }
2289
+ }
2290
+
2291
+ private async getVideoDuration(_assetUri: string): Promise<number | null> {
2292
+ return 0;
2293
+ }
2294
+
2295
+ private resolveFsTarget(assetUri: string): string | null {
2296
+ if (assetUri.startsWith('file://')) {
2297
+ return assetUri.slice('file://'.length);
2298
+ }
2299
+
2300
+ return assetUri.startsWith('/') ? assetUri : null;
2301
+ }
2302
+
2303
+ private createCanceledResult(): ImagePickerResult {
2304
+ return {
2305
+ canceled: true,
2306
+ assets: null,
2307
+ };
2308
+ }
2309
+ }
2310
+ `;
2311
+ }
2312
+ function renderExpoHarmonyLocationTurboModule() {
2313
+ return `import type { Permissions } from '@ohos.abilityAccessCtrl';
2314
+ import abilityAccessCtrl from '@ohos.abilityAccessCtrl';
2315
+ import geoLocationManager from '@ohos.geoLocationManager';
2316
+ import { sensor } from '@kit.SensorServiceKit';
2317
+ import { AnyThreadTurboModuleContext, AnyThreadTurboModule } from '@rnoh/react-native-openharmony/ts';
2318
+
2319
+ type PermissionResponse = {
2320
+ status: 'granted' | 'denied' | 'undetermined';
2321
+ granted: boolean;
2322
+ canAskAgain: boolean;
2323
+ expires: 'never';
2324
+ android: {
2325
+ accuracy: 'fine' | 'coarse' | 'none';
2326
+ };
2327
+ };
2328
+
2329
+ type ExpoLocationObject = {
2330
+ coords: {
2331
+ latitude: number;
2332
+ longitude: number;
2333
+ altitude: number | null;
2334
+ accuracy: number | null;
2335
+ altitudeAccuracy: number | null;
2336
+ heading: number | null;
2337
+ speed: number | null;
2338
+ };
2339
+ timestamp: number;
2340
+ mocked: false;
2341
+ };
2342
+
2343
+ type ProviderStatus = {
2344
+ locationServicesEnabled: boolean;
2345
+ backgroundModeEnabled: boolean;
2346
+ gpsAvailable: boolean;
2347
+ networkAvailable: boolean;
2348
+ passiveAvailable: boolean;
2349
+ };
2350
+
2351
+ type ReverseGeocodeResult = {
2352
+ city: string | null;
2353
+ district: string | null;
2354
+ streetNumber: string | null;
2355
+ street: string | null;
2356
+ region: string | null;
2357
+ subregion: string | null;
2358
+ country: string | null;
2359
+ postalCode: string | null;
2360
+ name: string | null;
2361
+ isoCountryCode: string | null;
2362
+ timezone: null;
2363
+ formattedAddress: string | null;
2364
+ };
2365
+
2366
+ type GeocodeResult = {
2367
+ latitude: number;
2368
+ longitude: number;
2369
+ altitude?: number;
2370
+ accuracy?: number;
2371
+ };
2372
+
2373
+ type HeadingObject = {
2374
+ magHeading: number;
2375
+ trueHeading: number | null;
2376
+ accuracy: number;
2377
+ };
2378
+
2379
+ export class ExpoHarmonyLocationTurboModule extends AnyThreadTurboModule {
2380
+ public static readonly NAME = 'ExpoHarmonyLocation';
2381
+
2382
+ private readonly atManager = abilityAccessCtrl.createAtManager();
2383
+ private nextWatchId = 1;
2384
+
2385
+ getConstants(): Record<string, never> {
2386
+ return {};
2387
+ }
2388
+
2389
+ async getForegroundPermissionStatus(): Promise<PermissionResponse> {
2390
+ return this.getLocationPermissionResponse();
2391
+ }
2392
+
2393
+ async requestForegroundPermission(): Promise<PermissionResponse> {
2394
+ return this.requestLocationPermissionResponse();
2395
+ }
2396
+
2397
+ async getBackgroundPermissionStatus(): Promise<PermissionResponse> {
2398
+ const backgroundStatus = this.resolvePermissionStatus('ohos.permission.LOCATION_IN_BACKGROUND');
2399
+ return this.buildBackgroundPermissionResponse(backgroundStatus);
2400
+ }
2401
+
2402
+ async requestBackgroundPermission(): Promise<PermissionResponse> {
2403
+ await this.atManager.requestPermissionsFromUser(this.ctx.uiAbilityContext, [
2404
+ 'ohos.permission.LOCATION_IN_BACKGROUND',
2405
+ ]);
2406
+ return this.getBackgroundPermissionStatus();
2407
+ }
2408
+
2409
+ async hasServicesEnabled(): Promise<boolean> {
2410
+ return geoLocationManager.isLocationEnabled();
2411
+ }
2412
+
2413
+ async getProviderStatus(): Promise<ProviderStatus> {
2414
+ const locationServicesEnabled = geoLocationManager.isLocationEnabled();
2415
+
2416
+ return {
2417
+ locationServicesEnabled,
2418
+ backgroundModeEnabled:
2419
+ this.resolvePermissionStatus('ohos.permission.LOCATION_IN_BACKGROUND') ===
2420
+ abilityAccessCtrl.PermissionStatus.GRANTED,
2421
+ gpsAvailable: locationServicesEnabled,
2422
+ networkAvailable: locationServicesEnabled,
2423
+ passiveAvailable: locationServicesEnabled,
2424
+ };
2425
+ }
2426
+
2427
+ async getCurrentPosition(options?: { accuracy?: number }): Promise<ExpoLocationObject> {
2428
+ const location = await geoLocationManager.getCurrentLocation(
2429
+ this.createCurrentLocationRequest(options?.accuracy),
2430
+ );
2431
+ return this.normalizeLocation(location);
2432
+ }
2433
+
2434
+ async getLastKnownPosition(_options?: Record<string, unknown>): Promise<ExpoLocationObject | null> {
2435
+ try {
2436
+ const location = geoLocationManager.getLastLocation();
2437
+ return this.hasCoordinates(location) ? this.normalizeLocation(location) : null;
2438
+ } catch (_error) {
2439
+ return null;
2440
+ }
2441
+ }
2442
+
2443
+ async geocode(address: string): Promise<GeocodeResult[]> {
2444
+ const addresses = await geoLocationManager.getAddressesFromLocationName({
2445
+ description: address,
2446
+ locale: 'en-US',
2447
+ maxItems: 5,
2448
+ });
2449
+
2450
+ return addresses.map((addressEntry) => ({
2451
+ latitude: Number(addressEntry.latitude ?? 0),
2452
+ longitude: Number(addressEntry.longitude ?? 0),
2453
+ }));
2454
+ }
2455
+
2456
+ async reverseGeocode(location: { latitude: number; longitude: number }): Promise<ReverseGeocodeResult[]> {
2457
+ const addresses = await geoLocationManager.getAddressesFromLocation({
2458
+ latitude: Number(location.latitude),
2459
+ longitude: Number(location.longitude),
2460
+ locale: 'en-US',
2461
+ maxItems: 5,
2462
+ });
2463
+
2464
+ return addresses.map((addressEntry) => ({
2465
+ city: addressEntry.locality ?? null,
2466
+ district: addressEntry.subLocality ?? null,
2467
+ streetNumber: addressEntry.subThoroughfare ?? null,
2468
+ street: addressEntry.thoroughfare ?? null,
2469
+ region: addressEntry.administrativeArea ?? null,
2470
+ subregion: addressEntry.subAdministrativeArea ?? null,
2471
+ country: addressEntry.countryName ?? null,
2472
+ postalCode: addressEntry.postalCode ?? null,
2473
+ name: addressEntry.placeName ?? null,
2474
+ isoCountryCode: addressEntry.countryCode ?? null,
2475
+ timezone: null,
2476
+ formattedAddress:
2477
+ addressEntry.addressUrl ??
2478
+ (Array.isArray(addressEntry.descriptions) && addressEntry.descriptions.length > 0
2479
+ ? addressEntry.descriptions.join(', ')
2480
+ : null),
2481
+ }));
2482
+ }
2483
+
2484
+ async startWatchPosition(
2485
+ options?: { accuracy?: number },
2486
+ _listenerConfig?: Record<string, unknown>,
2487
+ ): Promise<{ watchId: number; initialLocation: ExpoLocationObject | null }> {
2488
+ const watchId = this.nextWatchId++;
2489
+ return {
2490
+ watchId,
2491
+ initialLocation: await this.getLastKnownPosition(options ?? {}),
2492
+ };
2493
+ }
2494
+
2495
+ async stopWatchPosition(_watchId: number): Promise<void> {
2496
+ return;
2497
+ }
2498
+
2499
+ async getHeading(): Promise<HeadingObject> {
2500
+ return this.readHeadingSnapshot();
2501
+ }
2502
+
2503
+ async startWatchHeading(
2504
+ _listenerConfig?: Record<string, unknown>,
2505
+ ): Promise<{ watchId: number; initialHeading: HeadingObject }> {
2506
+ const watchId = this.nextWatchId++;
2507
+ return {
2508
+ watchId,
2509
+ initialHeading: await this.getHeading(),
2510
+ };
2511
+ }
2512
+
2513
+ async stopWatchHeading(_watchId: number): Promise<void> {
2514
+ return;
2515
+ }
2516
+
2517
+ private async getLocationPermissionResponse(): Promise<PermissionResponse> {
2518
+ const approximateStatus = this.resolvePermissionStatus('ohos.permission.APPROXIMATELY_LOCATION');
2519
+ const preciseStatus = this.resolvePermissionStatus('ohos.permission.LOCATION');
2520
+
2521
+ return this.buildPermissionResponse(approximateStatus, preciseStatus);
2522
+ }
2523
+
2524
+ private buildBackgroundPermissionResponse(
2525
+ backgroundStatus: abilityAccessCtrl.PermissionStatus,
2526
+ ): PermissionResponse {
2527
+ const foreground = this.getLocationPermissionResponse();
2528
+ const granted = backgroundStatus === abilityAccessCtrl.PermissionStatus.GRANTED;
2529
+ const denied =
2530
+ backgroundStatus === abilityAccessCtrl.PermissionStatus.DENIED ||
2531
+ backgroundStatus === abilityAccessCtrl.PermissionStatus.RESTRICTED ||
2532
+ backgroundStatus === abilityAccessCtrl.PermissionStatus.INVALID;
2533
+
2534
+ return foreground.then((foregroundPermission) => ({
2535
+ ...foregroundPermission,
2536
+ status: granted ? 'granted' : denied ? 'denied' : 'undetermined',
2537
+ granted,
2538
+ canAskAgain: !denied,
2539
+ }));
2540
+ }
2541
+
2542
+ private async requestLocationPermissionResponse(): Promise<PermissionResponse> {
2543
+ await this.atManager.requestPermissionsFromUser(this.ctx.uiAbilityContext, [
2544
+ 'ohos.permission.APPROXIMATELY_LOCATION',
2545
+ 'ohos.permission.LOCATION',
2546
+ ]);
2547
+
2548
+ return this.getLocationPermissionResponse();
2549
+ }
2550
+
2551
+ private resolvePermissionStatus(permissionName: Permissions): abilityAccessCtrl.PermissionStatus {
2552
+ const atManagerWithSelfStatus = this.atManager as abilityAccessCtrl.AtManager & {
2553
+ getSelfPermissionStatus?: (permission: Permissions) => abilityAccessCtrl.PermissionStatus;
2554
+ };
2555
+
2556
+ if (typeof atManagerWithSelfStatus.getSelfPermissionStatus === 'function') {
2557
+ return atManagerWithSelfStatus.getSelfPermissionStatus(permissionName);
2558
+ }
2559
+
2560
+ const accessTokenId = this.ctx.uiAbilityContext.abilityInfo.applicationInfo.accessTokenId;
2561
+ const grantStatus = this.atManager.checkAccessTokenSync(accessTokenId, permissionName);
2562
+
2563
+ return grantStatus === abilityAccessCtrl.GrantStatus.PERMISSION_GRANTED
2564
+ ? abilityAccessCtrl.PermissionStatus.GRANTED
2565
+ : abilityAccessCtrl.PermissionStatus.NOT_DETERMINED;
2566
+ }
2567
+
2568
+ private buildPermissionResponse(
2569
+ approximateStatus: abilityAccessCtrl.PermissionStatus,
2570
+ preciseStatus: abilityAccessCtrl.PermissionStatus,
2571
+ ): PermissionResponse {
2572
+ const coarseGranted = approximateStatus === abilityAccessCtrl.PermissionStatus.GRANTED;
2573
+ const fineGranted = preciseStatus === abilityAccessCtrl.PermissionStatus.GRANTED;
2574
+ const granted = coarseGranted || fineGranted;
2575
+ const denied =
2576
+ (approximateStatus === abilityAccessCtrl.PermissionStatus.DENIED ||
2577
+ approximateStatus === abilityAccessCtrl.PermissionStatus.RESTRICTED ||
2578
+ approximateStatus === abilityAccessCtrl.PermissionStatus.INVALID) &&
2579
+ (preciseStatus === abilityAccessCtrl.PermissionStatus.DENIED ||
2580
+ preciseStatus === abilityAccessCtrl.PermissionStatus.RESTRICTED ||
2581
+ preciseStatus === abilityAccessCtrl.PermissionStatus.INVALID);
2582
+
2583
+ return {
2584
+ status: granted ? 'granted' : denied ? 'denied' : 'undetermined',
2585
+ granted,
2586
+ canAskAgain: !denied,
2587
+ expires: 'never',
2588
+ android: {
2589
+ accuracy: fineGranted ? 'fine' : coarseGranted ? 'coarse' : 'none',
2590
+ },
2591
+ };
2592
+ }
2593
+
2594
+ private createCurrentLocationRequest(accuracy?: number): geoLocationManager.CurrentLocationRequest {
2595
+ if (typeof accuracy === 'number' && accuracy >= 4) {
2596
+ return {
2597
+ priority: geoLocationManager.LocationRequestPriority.ACCURACY,
2598
+ };
2599
+ }
2600
+
2601
+ if (typeof accuracy === 'number' && accuracy <= 2) {
2602
+ return {
2603
+ priority: geoLocationManager.LocationRequestPriority.LOW_POWER,
2604
+ };
2605
+ }
2606
+
2607
+ return {
2608
+ priority: geoLocationManager.LocationRequestPriority.FIRST_FIX,
2609
+ };
2610
+ }
2611
+
2612
+ private hasCoordinates(location: geoLocationManager.Location | null | undefined): location is geoLocationManager.Location {
2613
+ return (
2614
+ !!location &&
2615
+ typeof location.latitude === 'number' &&
2616
+ typeof location.longitude === 'number'
2617
+ );
2618
+ }
2619
+
2620
+ private normalizeLocation(location: geoLocationManager.Location): ExpoLocationObject {
2621
+ return {
2622
+ coords: {
2623
+ latitude: Number(location.latitude),
2624
+ longitude: Number(location.longitude),
2625
+ altitude: typeof location.altitude === 'number' ? location.altitude : null,
2626
+ accuracy: typeof location.accuracy === 'number' ? location.accuracy : null,
2627
+ altitudeAccuracy: null,
2628
+ heading: typeof location.direction === 'number' ? location.direction : null,
2629
+ speed: typeof location.speed === 'number' ? location.speed : null,
2630
+ },
2631
+ timestamp: Number(location.timeStamp ?? Date.now()),
2632
+ mocked: false,
2633
+ };
2634
+ }
2635
+
2636
+ private async readHeadingSnapshot(): Promise<HeadingObject> {
2637
+ return new Promise((resolve) => {
2638
+ try {
2639
+ sensor.once(sensor.SensorId.ROTATION_VECTOR, (data: Record<string, number>) => {
2640
+ const x = Number(data?.x ?? 0);
2641
+ const y = Number(data?.y ?? 0);
2642
+ const z = Number(data?.z ?? 0);
2643
+ const w = Number(data?.w ?? 1);
2644
+ const sinyCosp = 2 * (w * z + x * y);
2645
+ const cosyCosp = 1 - 2 * (y * y + z * z);
2646
+ const magHeading = (Math.atan2(sinyCosp, cosyCosp) * 180 / Math.PI + 360) % 360;
2647
+ resolve({
2648
+ magHeading,
2649
+ trueHeading: magHeading,
2650
+ accuracy: 3,
2651
+ });
2652
+ });
2653
+ } catch (_error) {
2654
+ resolve({
2655
+ magHeading: 0,
2656
+ trueHeading: null,
2657
+ accuracy: 0,
2658
+ });
2659
+ }
2660
+ });
2661
+ }
2662
+ }
2663
+ `;
2664
+ }
2665
+ function renderExpoHarmonyCameraTurboModule() {
2666
+ return `import type { Permissions } from '@ohos.abilityAccessCtrl';
2667
+ import abilityAccessCtrl from '@ohos.abilityAccessCtrl';
2668
+ import camera from '@ohos.multimedia.camera';
2669
+ import cameraPicker from '@ohos.multimedia.cameraPicker';
2670
+ import image from '@ohos.multimedia.image';
2671
+ import { UITurboModuleContext, UITurboModule } from '@rnoh/react-native-openharmony/ts';
2672
+
2673
+ type PermissionResponse = {
2674
+ status: 'granted' | 'denied' | 'undetermined';
2675
+ granted: boolean;
2676
+ canAskAgain: boolean;
2677
+ expires: 'never';
2678
+ };
2679
+
2680
+ type CameraCaptureResult = {
2681
+ uri: string;
2682
+ width: number;
2683
+ height: number;
2684
+ base64?: string;
2685
+ exif: null;
2686
+ };
2687
+
2688
+ type CameraRecordingResult = {
2689
+ uri: string;
2690
+ duration: number;
2691
+ fileSize: number | null;
2692
+ mimeType: string;
2693
+ };
2694
+
2695
+ export class ExpoHarmonyCameraTurboModule extends UITurboModule {
2696
+ public static readonly NAME = 'ExpoHarmonyCamera';
2697
+
2698
+ private readonly atManager = abilityAccessCtrl.createAtManager();
2699
+ private readonly previewStates = new Map<string, 'running' | 'paused'>();
2700
+ private readonly activeRecordings = new Map<string, CameraRecordingResult>();
2701
+
2702
+ getConstants(): Record<string, never> {
2703
+ return {};
2704
+ }
2705
+
2706
+ async getCameraPermissionStatus(): Promise<PermissionResponse> {
2707
+ return this.getPermissionResponse('ohos.permission.CAMERA');
2708
+ }
2709
+
2710
+ async requestCameraPermission(): Promise<PermissionResponse> {
2711
+ return this.requestPermissionResponse('ohos.permission.CAMERA');
2712
+ }
2713
+
2714
+ async getMicrophonePermissionStatus(): Promise<PermissionResponse> {
2715
+ return this.getPermissionResponse('ohos.permission.MICROPHONE');
2716
+ }
2717
+
2718
+ async requestMicrophonePermission(): Promise<PermissionResponse> {
2719
+ return this.requestPermissionResponse('ohos.permission.MICROPHONE');
2720
+ }
2721
+
2722
+ async createPreview(options?: { viewId?: string }): Promise<{ viewId: string; state: 'running' }> {
2723
+ const viewId = typeof options?.viewId === 'string' && options.viewId.length > 0
2724
+ ? options.viewId
2725
+ : 'expo-harmony-camera-preview';
2726
+ this.previewStates.set(viewId, 'running');
2727
+ return {
2728
+ viewId,
2729
+ state: 'running',
2730
+ };
2731
+ }
2732
+
2733
+ async disposePreview(options?: { viewId?: string }): Promise<void> {
2734
+ if (typeof options?.viewId === 'string') {
2735
+ this.previewStates.delete(options.viewId);
2736
+ this.activeRecordings.delete(options.viewId);
2737
+ }
2738
+ }
2739
+
2740
+ async pausePreview(options?: { viewId?: string }): Promise<{ paused: boolean }> {
2741
+ if (typeof options?.viewId === 'string') {
2742
+ this.previewStates.set(options.viewId, 'paused');
2743
+ }
2744
+ return {
2745
+ paused: true,
2746
+ };
2747
+ }
2748
+
2749
+ async resumePreview(options?: { viewId?: string }): Promise<{ paused: boolean }> {
2750
+ if (typeof options?.viewId === 'string') {
2751
+ this.previewStates.set(options.viewId, 'running');
2752
+ }
2753
+ return {
2754
+ paused: false,
2755
+ };
2756
+ }
2757
+
2758
+ async takePicture(options?: { cameraType?: string; viewId?: string }): Promise<CameraCaptureResult> {
2759
+ const profile = new cameraPicker.PickerProfile();
2760
+ profile.cameraPosition =
2761
+ options?.cameraType === 'front'
2762
+ ? camera.CameraPosition.CAMERA_POSITION_FRONT
2763
+ : camera.CameraPosition.CAMERA_POSITION_BACK;
2764
+
2765
+ const result = await cameraPicker.pick(
2766
+ this.ctx.uiAbilityContext,
2767
+ [cameraPicker.PickerMediaType.PHOTO],
2768
+ profile,
2769
+ );
2770
+
2771
+ if (!result || typeof result.resultUri !== 'string' || result.resultUri.length === 0) {
2772
+ throw new Error('Camera capture was canceled.');
2773
+ }
2774
+
2775
+ const imageSize = await this.getImageSize(result.resultUri);
2776
+
2777
+ return {
2778
+ uri: result.resultUri,
2779
+ width: imageSize.width,
2780
+ height: imageSize.height,
2781
+ exif: null,
2782
+ };
2783
+ }
2784
+
2785
+ async startRecording(options?: { viewId?: string; cameraType?: string }): Promise<CameraRecordingResult> {
2786
+ await this.requestPermissionResponse('ohos.permission.MICROPHONE');
2787
+ const profile = new cameraPicker.PickerProfile();
2788
+ profile.cameraPosition =
2789
+ options?.cameraType === 'front'
2790
+ ? camera.CameraPosition.CAMERA_POSITION_FRONT
2791
+ : camera.CameraPosition.CAMERA_POSITION_BACK;
2792
+
2793
+ const result = await cameraPicker.pick(
2794
+ this.ctx.uiAbilityContext,
2795
+ [cameraPicker.PickerMediaType.VIDEO],
2796
+ profile,
2797
+ );
2798
+
2799
+ if (!result || typeof result.resultUri !== 'string' || result.resultUri.length === 0) {
2800
+ throw new Error('Camera recording was canceled.');
2801
+ }
2802
+
2803
+ const recording = {
2804
+ uri: result.resultUri,
2805
+ duration: 0,
2806
+ fileSize: null,
2807
+ mimeType: 'video/mp4',
2808
+ };
2809
+
2810
+ if (typeof options?.viewId === 'string') {
2811
+ this.activeRecordings.set(options.viewId, recording);
2812
+ }
2813
+
2814
+ return recording;
2815
+ }
2816
+
2817
+ async stopRecording(options?: { viewId?: string }): Promise<CameraRecordingResult | null> {
2818
+ if (typeof options?.viewId === 'string') {
2819
+ const recording = this.activeRecordings.get(options.viewId) ?? null;
2820
+ this.activeRecordings.delete(options.viewId);
2821
+ return recording;
2822
+ }
2823
+
2824
+ return null;
2825
+ }
2826
+
2827
+ async toggleRecording(options?: { viewId?: string; cameraType?: string }): Promise<CameraRecordingResult | null> {
2828
+ if (typeof options?.viewId === 'string' && this.activeRecordings.has(options.viewId)) {
2829
+ return this.stopRecording(options);
2830
+ }
2831
+
2832
+ return this.startRecording(options);
2833
+ }
2834
+
2835
+ private async getPermissionResponse(permissionName: Permissions): Promise<PermissionResponse> {
2836
+ const atManagerWithSelfStatus = this.atManager as abilityAccessCtrl.AtManager & {
2837
+ getSelfPermissionStatus?: (permission: Permissions) => abilityAccessCtrl.PermissionStatus;
2838
+ };
2839
+ const permissionStatus =
2840
+ typeof atManagerWithSelfStatus.getSelfPermissionStatus === 'function'
2841
+ ? atManagerWithSelfStatus.getSelfPermissionStatus(permissionName)
2842
+ : this.atManager.checkAccessTokenSync(
2843
+ this.ctx.uiAbilityContext.abilityInfo.applicationInfo.accessTokenId,
2844
+ permissionName,
2845
+ ) === abilityAccessCtrl.GrantStatus.PERMISSION_GRANTED
2846
+ ? abilityAccessCtrl.PermissionStatus.GRANTED
2847
+ : abilityAccessCtrl.PermissionStatus.NOT_DETERMINED;
2848
+
2849
+ return this.createPermissionResponse(permissionStatus);
2850
+ }
2851
+
2852
+ private async requestPermissionResponse(permissionName: Permissions): Promise<PermissionResponse> {
2853
+ await this.atManager.requestPermissionsFromUser(this.ctx.uiAbilityContext, [permissionName]);
2854
+ return this.getPermissionResponse(permissionName);
2855
+ }
2856
+
2857
+ private createPermissionResponse(
2858
+ permissionStatus: abilityAccessCtrl.PermissionStatus,
2859
+ ): PermissionResponse {
2860
+ const granted = permissionStatus === abilityAccessCtrl.PermissionStatus.GRANTED;
2861
+ const denied =
2862
+ permissionStatus === abilityAccessCtrl.PermissionStatus.DENIED ||
2863
+ permissionStatus === abilityAccessCtrl.PermissionStatus.RESTRICTED ||
2864
+ permissionStatus === abilityAccessCtrl.PermissionStatus.INVALID;
2865
+
2866
+ return {
2867
+ status: granted ? 'granted' : denied ? 'denied' : 'undetermined',
2868
+ granted,
2869
+ canAskAgain: !denied,
2870
+ expires: 'never',
2871
+ };
2872
+ }
2873
+
2874
+ private async getImageSize(assetUri: string): Promise<{ width: number; height: number }> {
2875
+ let imageSource: image.ImageSource | null = null;
2876
+
2877
+ try {
2878
+ imageSource = image.createImageSource(assetUri);
2879
+ const imageInfo = await imageSource.getImageInfo();
2880
+ return {
2881
+ width: Number(imageInfo.size?.width ?? 0),
2882
+ height: Number(imageInfo.size?.height ?? 0),
2883
+ };
2884
+ } catch (_error) {
2885
+ return {
2886
+ width: 0,
2887
+ height: 0,
2888
+ };
2889
+ } finally {
2890
+ if (imageSource) {
2891
+ try {
2892
+ await imageSource.release();
2893
+ } catch (_error) {
2894
+ // Ignore cleanup errors from ImageSource release.
2895
+ }
2896
+ }
2897
+ }
2898
+ }
2899
+ }
2900
+ `;
2901
+ }
2902
+ function renderExpoFileSystemHarmonyAdapterShim(capability) {
2903
+ return `'use strict';
2904
+
2905
+ const { TurboModuleRegistry } = require('react-native');
2906
+ const { CodedError } = require('expo-modules-core');
2907
+
2908
+ const FILE_SCHEME = 'file://';
2909
+ const NATIVE_MODULE_NAME = 'ExpoHarmonyFileSystem';
2910
+ const NATIVE_MODULE = TurboModuleRegistry.get(NATIVE_MODULE_NAME);
2911
+ const NATIVE_CONSTANTS = NATIVE_MODULE?.getConstants ? NATIVE_MODULE.getConstants() : {};
2912
+
2913
+ function createError(code, message) {
2914
+ return new CodedError(code, message);
2915
+ }
2916
+
2917
+ function requireNativeModule(operationName) {
2918
+ if (NATIVE_MODULE) {
2919
+ return NATIVE_MODULE;
2920
+ }
2921
+
2922
+ throw createError(
2923
+ 'ERR_EXPO_HARMONY_NATIVE_MODULE_MISSING',
2924
+ '${capability.packageName} expected the ' +
2925
+ NATIVE_MODULE_NAME +
2926
+ ' TurboModule to be registered, but it was missing while running ' +
2927
+ operationName +
2928
+ '.',
2929
+ );
2930
+ }
2931
+
2932
+ function createUnsupportedError(operationName) {
2933
+ return createError(
2934
+ 'ERR_EXPO_HARMONY_UNSUPPORTED',
2935
+ '${capability.packageName} currently supports UTF-8 sandbox file operations only. Unsupported operation: ' +
2936
+ operationName +
2937
+ '.',
2938
+ );
2939
+ }
2940
+
2941
+ function toFileUri(pathValue, ensureTrailingSlash) {
2942
+ if (typeof pathValue !== 'string' || pathValue.length === 0) {
2943
+ return null;
2944
+ }
2945
+
2946
+ const normalizedPath = pathValue.startsWith(FILE_SCHEME)
2947
+ ? pathValue.slice(FILE_SCHEME.length)
2948
+ : pathValue;
2949
+ const withScheme = FILE_SCHEME + normalizedPath;
2950
+
2951
+ if (!ensureTrailingSlash) {
2952
+ return withScheme;
2953
+ }
2954
+
2955
+ let normalizedSchemePath = withScheme;
2956
+
2957
+ while (normalizedSchemePath.endsWith('/')) {
2958
+ normalizedSchemePath = normalizedSchemePath.slice(0, -1);
2959
+ }
2960
+
2961
+ return normalizedSchemePath + '/';
2962
+ }
2963
+
2964
+ function normalizeInputPath(inputPath) {
2965
+ if (typeof inputPath !== 'string' || inputPath.length === 0) {
2966
+ throw createError(
2967
+ 'ERR_EXPO_HARMONY_INVALID_URI',
2968
+ '${capability.packageName} expected a non-empty file URI.',
2969
+ );
2970
+ }
2971
+
2972
+ const normalizedPath = inputPath.startsWith(FILE_SCHEME)
2973
+ ? inputPath.slice(FILE_SCHEME.length)
2974
+ : inputPath;
2975
+
2976
+ if (!normalizedPath.startsWith('/')) {
2977
+ throw createError(
2978
+ 'ERR_EXPO_HARMONY_INVALID_URI',
2979
+ '${capability.packageName} supports only absolute file:// URIs inside the app sandbox.',
2980
+ );
2981
+ }
2982
+
2983
+ if (
2984
+ normalizedPath.includes('/../') ||
2985
+ normalizedPath.endsWith('/..') ||
2986
+ normalizedPath.includes('/./')
2987
+ ) {
2988
+ throw createError(
2989
+ 'ERR_EXPO_HARMONY_INVALID_URI',
2990
+ '${capability.packageName} does not accept relative path segments.',
2991
+ );
2992
+ }
2993
+
2994
+ return normalizedPath;
2995
+ }
2996
+
2997
+ function normalizeStringEncoding(rawEncoding) {
2998
+ if (rawEncoding == null || rawEncoding === 'utf8') {
2999
+ return 'utf8';
3000
+ }
3001
+
3002
+ if (rawEncoding === 'base64') {
3003
+ return 'base64';
3004
+ }
3005
+
3006
+ throw createUnsupportedError('encoding=' + String(rawEncoding));
3007
+ }
3008
+
3009
+ function normalizeFileDownloadResult(result, requestedUri) {
3010
+ return {
3011
+ uri: result?.uri ?? String(requestedUri),
3012
+ status:
3013
+ typeof result?.status === 'number' && Number.isFinite(result.status)
3014
+ ? result.status
3015
+ : 200,
3016
+ headers: result?.headers && typeof result.headers === 'object' ? result.headers : {},
3017
+ md5: typeof result?.md5 === 'string' ? result.md5 : undefined,
3018
+ };
3019
+ }
3020
+
3021
+ function normalizeFileInfoResult(requestedUri, nativeResult) {
3022
+ if (!nativeResult || nativeResult.exists !== true) {
3023
+ return {
3024
+ exists: false,
3025
+ isDirectory: false,
3026
+ uri: String(requestedUri),
3027
+ };
3028
+ }
3029
+
3030
+ const normalizedResult = {
3031
+ exists: true,
3032
+ uri: toFileUri(nativeResult.path, false) ?? String(requestedUri),
3033
+ size: Number(nativeResult.size ?? 0),
3034
+ isDirectory: nativeResult.isDirectory === true,
3035
+ modificationTime: Number(nativeResult.modificationTime ?? 0),
3036
+ };
3037
+
3038
+ if (typeof nativeResult.md5 === 'string' && nativeResult.md5.length > 0) {
3039
+ normalizedResult.md5 = nativeResult.md5;
3040
+ }
3041
+
3042
+ return normalizedResult;
3043
+ }
3044
+
3045
+ function normalizeNativeError(error) {
3046
+ if (error instanceof Error) {
3047
+ return error;
3048
+ }
3049
+
3050
+ if (error && typeof error === 'object') {
3051
+ const code =
3052
+ typeof error.code === 'number' || typeof error.code === 'string'
3053
+ ? String(error.code)
3054
+ : null;
3055
+ const message =
3056
+ typeof error.message === 'string' && error.message.length > 0
3057
+ ? error.message
3058
+ : typeof error.name === 'string' && error.name.length > 0
3059
+ ? error.name
3060
+ : JSON.stringify(error);
3061
+
3062
+ return new Error(code ? '[native:' + code + '] ' + message : message);
3063
+ }
3064
+
3065
+ return new Error(String(error));
3066
+ }
3067
+
3068
+ module.exports = {
3069
+ documentDirectory: toFileUri(NATIVE_CONSTANTS.documentDirectoryPath, true),
3070
+ cacheDirectory: toFileUri(NATIVE_CONSTANTS.cacheDirectoryPath, true),
3071
+ bundleDirectory: toFileUri(NATIVE_CONSTANTS.bundleDirectoryPath, true),
3072
+ EncodingType: {
3073
+ UTF8: 'utf8',
3074
+ Base64: 'base64',
3075
+ },
3076
+ FileSystemSessionType: {
3077
+ BACKGROUND: 0,
3078
+ FOREGROUND: 1,
3079
+ },
3080
+ async getInfoAsync(fileUri, options) {
3081
+ const normalizedPath = normalizeInputPath(fileUri);
3082
+ try {
3083
+ const result = await requireNativeModule('getInfoAsync').getInfo(normalizedPath, {
3084
+ md5: options?.md5 === true,
3085
+ });
3086
+ return normalizeFileInfoResult(fileUri, result);
3087
+ } catch (error) {
3088
+ throw normalizeNativeError(error);
3089
+ }
3090
+ },
3091
+ async readAsStringAsync(fileUri, options) {
3092
+ const normalizedPath = normalizeInputPath(fileUri);
3093
+ try {
3094
+ return await requireNativeModule('readAsStringAsync').readAsString(normalizedPath, {
3095
+ encoding: normalizeStringEncoding(options?.encoding),
3096
+ position: options?.position,
3097
+ length: options?.length,
3098
+ });
3099
+ } catch (error) {
3100
+ throw normalizeNativeError(error);
3101
+ }
3102
+ },
3103
+ async writeAsStringAsync(fileUri, contents, options) {
3104
+ const normalizedPath = normalizeInputPath(fileUri);
3105
+ try {
3106
+ await requireNativeModule('writeAsStringAsync').writeAsString(
3107
+ normalizedPath,
3108
+ String(contents),
3109
+ {
3110
+ encoding: normalizeStringEncoding(options?.encoding),
3111
+ append: options?.append === true,
3112
+ },
3113
+ );
3114
+ } catch (error) {
3115
+ throw normalizeNativeError(error);
3116
+ }
3117
+ },
3118
+ async deleteAsync(fileUri, options) {
3119
+ const normalizedPath = normalizeInputPath(fileUri);
3120
+ try {
3121
+ await requireNativeModule('deleteAsync').deletePath(normalizedPath, {
3122
+ idempotent: options?.idempotent === true,
3123
+ });
3124
+ } catch (error) {
3125
+ throw normalizeNativeError(error);
3126
+ }
3127
+ },
3128
+ async makeDirectoryAsync(fileUri, options) {
3129
+ const normalizedPath = normalizeInputPath(fileUri);
3130
+ try {
3131
+ await requireNativeModule('makeDirectoryAsync').makeDirectory(normalizedPath, {
3132
+ intermediates: options?.intermediates === true,
3133
+ });
3134
+ } catch (error) {
3135
+ throw normalizeNativeError(error);
3136
+ }
3137
+ },
3138
+ async readDirectoryAsync(fileUri) {
3139
+ const normalizedPath = normalizeInputPath(fileUri);
3140
+ try {
3141
+ return await requireNativeModule('readDirectoryAsync').readDirectory(normalizedPath);
3142
+ } catch (error) {
3143
+ throw normalizeNativeError(error);
3144
+ }
3145
+ },
3146
+ async copyAsync(options) {
3147
+ const fromPath = normalizeInputPath(options?.from);
3148
+ const toPath = normalizeInputPath(options?.to);
3149
+ try {
3150
+ await requireNativeModule('copyAsync').copy(fromPath, toPath);
3151
+ } catch (error) {
3152
+ throw normalizeNativeError(error);
3153
+ }
3154
+ },
3155
+ async moveAsync(options) {
3156
+ const fromPath = normalizeInputPath(options?.from);
3157
+ const toPath = normalizeInputPath(options?.to);
3158
+ try {
3159
+ await requireNativeModule('moveAsync').move(fromPath, toPath);
3160
+ } catch (error) {
3161
+ throw normalizeNativeError(error);
3162
+ }
3163
+ },
3164
+ async downloadAsync(url, fileUri, options) {
3165
+ const normalizedPath = normalizeInputPath(fileUri);
3166
+ try {
3167
+ return normalizeFileDownloadResult(
3168
+ await requireNativeModule('downloadAsync').download(
3169
+ String(url),
3170
+ normalizedPath,
3171
+ {
3172
+ headers: options?.headers ?? {},
3173
+ md5: options?.md5 === true,
3174
+ },
3175
+ ),
3176
+ fileUri,
3177
+ );
3178
+ } catch (error) {
3179
+ throw normalizeNativeError(error);
3180
+ }
3181
+ },
3182
+ };
3183
+ `;
3184
+ }
3185
+ function renderExpoFileSystemPreviewShim(capability) {
3186
+ return `'use strict';
3187
+
3188
+ const { CodedError } = require('expo-modules-core');
3189
+
3190
+ const PREVIEW_MESSAGE =
3191
+ '${capability.packageName} is routed through the Expo Harmony ${capability.supportTier} bridge. Bundling is supported, but runtime file-system behavior is not verified yet.';
3192
+
3193
+ function createPreviewError(operationName) {
3194
+ return new CodedError(
3195
+ 'ERR_EXPO_HARMONY_PREVIEW',
3196
+ PREVIEW_MESSAGE + ' Attempted operation: ' + operationName + '.',
3197
+ );
3198
+ }
3199
+
3200
+ async function unavailable(operationName) {
3201
+ throw createPreviewError(operationName);
3202
+ }
3203
+
3204
+ module.exports = {
3205
+ cacheDirectory: 'file:///expo-harmony/cache/',
3206
+ documentDirectory: 'file:///expo-harmony/document/',
3207
+ bundleDirectory: 'file:///expo-harmony/bundle/',
3208
+ EncodingType: {
3209
+ UTF8: 'utf8',
3210
+ Base64: 'base64',
3211
+ },
3212
+ FileSystemSessionType: {
3213
+ BACKGROUND: 0,
3214
+ FOREGROUND: 1,
3215
+ },
3216
+ getInfoAsync(path) {
3217
+ return unavailable('getInfoAsync(' + String(path) + ')');
3218
+ },
3219
+ readAsStringAsync(path) {
3220
+ return unavailable('readAsStringAsync(' + String(path) + ')');
3221
+ },
3222
+ writeAsStringAsync(path) {
3223
+ return unavailable('writeAsStringAsync(' + String(path) + ')');
3224
+ },
3225
+ deleteAsync(path) {
3226
+ return unavailable('deleteAsync(' + String(path) + ')');
3227
+ },
3228
+ makeDirectoryAsync(path) {
3229
+ return unavailable('makeDirectoryAsync(' + String(path) + ')');
3230
+ },
3231
+ readDirectoryAsync(path) {
3232
+ return unavailable('readDirectoryAsync(' + String(path) + ')');
3233
+ },
3234
+ copyAsync(options) {
3235
+ return unavailable('copyAsync(' + JSON.stringify(options ?? {}) + ')');
3236
+ },
3237
+ moveAsync(options) {
3238
+ return unavailable('moveAsync(' + JSON.stringify(options ?? {}) + ')');
3239
+ },
3240
+ downloadAsync(url) {
3241
+ return unavailable('downloadAsync(' + String(url) + ')');
3242
+ },
3243
+ };
3244
+ `;
3245
+ }
3246
+ function renderExpoImagePickerHarmonyAdapterShim(capability) {
3247
+ return `'use strict';
3248
+
3249
+ const { TurboModuleRegistry } = require('react-native');
3250
+ const { CodedError } = require('expo-modules-core');
3251
+
3252
+ const NATIVE_MODULE_NAME = 'ExpoHarmonyImagePicker';
3253
+ const NATIVE_MODULE = TurboModuleRegistry.get(NATIVE_MODULE_NAME);
3254
+
3255
+ function createError(code, message) {
3256
+ return new CodedError(code, message);
3257
+ }
3258
+
3259
+ function requireNativeModule(operationName) {
3260
+ if (NATIVE_MODULE) {
3261
+ return NATIVE_MODULE;
3262
+ }
3263
+
3264
+ throw createError(
3265
+ 'ERR_EXPO_HARMONY_NATIVE_MODULE_MISSING',
3266
+ '${capability.packageName} expected the ' +
3267
+ NATIVE_MODULE_NAME +
3268
+ ' TurboModule to be registered, but it was missing while running ' +
3269
+ operationName +
3270
+ '.',
3271
+ );
3272
+ }
3273
+
3274
+ function normalizeNativeError(error) {
3275
+ if (error instanceof Error) {
3276
+ return error;
3277
+ }
3278
+
3279
+ if (error && typeof error === 'object') {
3280
+ const code =
3281
+ typeof error.code === 'number' || typeof error.code === 'string'
3282
+ ? String(error.code)
3283
+ : null;
3284
+ const message =
3285
+ typeof error.message === 'string' && error.message.length > 0
3286
+ ? error.message
3287
+ : typeof error.name === 'string' && error.name.length > 0
3288
+ ? error.name
3289
+ : JSON.stringify(error);
3290
+
3291
+ return new Error(code ? '[native:' + code + '] ' + message : message);
3292
+ }
3293
+
3294
+ return new Error(String(error));
3295
+ }
3296
+
3297
+ function normalizePickerResult(result) {
3298
+ if (!result || result.canceled === true || !Array.isArray(result.assets) || result.assets.length === 0) {
3299
+ return {
3300
+ canceled: true,
3301
+ assets: null,
3302
+ };
3303
+ }
3304
+
3305
+ return {
3306
+ canceled: false,
3307
+ assets: result.assets.map((asset) => ({
3308
+ uri: String(asset.uri),
3309
+ assetId: asset.assetId ?? null,
3310
+ width: Number(asset.width ?? 0),
3311
+ height: Number(asset.height ?? 0),
3312
+ type: asset.type ?? null,
3313
+ fileName: asset.fileName ?? null,
3314
+ fileSize:
3315
+ typeof asset.fileSize === 'number' && Number.isFinite(asset.fileSize)
3316
+ ? asset.fileSize
3317
+ : null,
3318
+ mimeType: asset.mimeType ?? null,
3319
+ duration:
3320
+ typeof asset.duration === 'number' && Number.isFinite(asset.duration)
3321
+ ? asset.duration
3322
+ : null,
3323
+ exif: asset.exif ?? null,
3324
+ base64: asset.base64 ?? null,
3325
+ })),
3326
+ };
3327
+ }
3328
+
3329
+ async function invokeNative(methodName, operationName, ...args) {
3330
+ try {
3331
+ return await requireNativeModule(operationName)[methodName](...args);
3332
+ } catch (error) {
3333
+ throw normalizeNativeError(error);
3334
+ }
3335
+ }
3336
+
3337
+ module.exports = {
3338
+ MediaTypeOptions: {
3339
+ All: 'All',
3340
+ Images: 'Images',
3341
+ Videos: 'Videos',
3342
+ },
3343
+ CameraType: {
3344
+ front: 'front',
3345
+ back: 'back',
3346
+ },
3347
+ UIImagePickerPresentationStyle: {
3348
+ AUTOMATIC: 'automatic',
3349
+ FULL_SCREEN: 'fullScreen',
3350
+ PAGE_SHEET: 'pageSheet',
3351
+ FORM_SHEET: 'formSheet',
3352
+ CURRENT_CONTEXT: 'currentContext',
3353
+ OVER_FULL_SCREEN: 'overFullScreen',
3354
+ },
3355
+ async requestCameraPermissionsAsync() {
3356
+ return invokeNative(
3357
+ 'requestCameraPermission',
3358
+ 'requestCameraPermissionsAsync',
3359
+ );
3360
+ },
3361
+ async requestMediaLibraryPermissionsAsync(writeOnly) {
3362
+ return invokeNative(
3363
+ 'requestMediaLibraryPermission',
3364
+ 'requestMediaLibraryPermissionsAsync',
3365
+ writeOnly === true,
3366
+ );
3367
+ },
3368
+ async getCameraPermissionsAsync() {
3369
+ return invokeNative('getCameraPermissionStatus', 'getCameraPermissionsAsync');
3370
+ },
3371
+ async getMediaLibraryPermissionsAsync(writeOnly) {
3372
+ return invokeNative(
3373
+ 'getMediaLibraryPermissionStatus',
3374
+ 'getMediaLibraryPermissionsAsync',
3375
+ writeOnly === true,
3376
+ );
3377
+ },
3378
+ async launchCameraAsync(options) {
3379
+ return normalizePickerResult(
3380
+ await invokeNative('launchCamera', 'launchCameraAsync', options ?? {}),
3381
+ );
3382
+ },
3383
+ async launchImageLibraryAsync(options) {
3384
+ return normalizePickerResult(
3385
+ await invokeNative('launchImageLibrary', 'launchImageLibraryAsync', options ?? {}),
3386
+ );
3387
+ },
3388
+ async getPendingResultAsync() {
3389
+ const result = await invokeNative('getPendingResult', 'getPendingResultAsync');
3390
+ return result ? normalizePickerResult(result) : null;
3391
+ },
3392
+ };
3393
+ `;
3394
+ }
3395
+ function renderExpoLocationHarmonyAdapterShim(capability) {
3396
+ return `'use strict';
3397
+
3398
+ const { TurboModuleRegistry } = require('react-native');
3399
+ const { CodedError } = require('expo-modules-core');
3400
+
3401
+ const NATIVE_MODULE_NAME = 'ExpoHarmonyLocation';
3402
+ const NATIVE_MODULE = TurboModuleRegistry.get(NATIVE_MODULE_NAME);
3403
+
3404
+ function createError(code, message) {
3405
+ return new CodedError(code, message);
3406
+ }
3407
+
3408
+ function createUnsupportedError(operationName) {
3409
+ return createError(
3410
+ 'ERR_EXPO_HARMONY_UNSUPPORTED',
3411
+ '${capability.packageName} does not implement ' + operationName + ' on HarmonyOS yet.',
3412
+ );
3413
+ }
3414
+
3415
+ function requireNativeModule(operationName) {
3416
+ if (NATIVE_MODULE) {
3417
+ return NATIVE_MODULE;
3418
+ }
3419
+
3420
+ throw createError(
3421
+ 'ERR_EXPO_HARMONY_NATIVE_MODULE_MISSING',
3422
+ '${capability.packageName} expected the ' +
3423
+ NATIVE_MODULE_NAME +
3424
+ ' TurboModule to be registered, but it was missing while running ' +
3425
+ operationName +
3426
+ '.',
3427
+ );
3428
+ }
3429
+
3430
+ function normalizeNativeError(error) {
3431
+ if (error instanceof Error) {
3432
+ return error;
3433
+ }
3434
+
3435
+ if (error && typeof error === 'object') {
3436
+ const code =
3437
+ typeof error.code === 'number' || typeof error.code === 'string'
3438
+ ? String(error.code)
3439
+ : null;
3440
+ const message =
3441
+ typeof error.message === 'string' && error.message.length > 0
3442
+ ? error.message
3443
+ : typeof error.name === 'string' && error.name.length > 0
3444
+ ? error.name
3445
+ : JSON.stringify(error);
3446
+
3447
+ return new Error(code ? '[native:' + code + '] ' + message : message);
3448
+ }
3449
+
3450
+ return new Error(String(error));
3451
+ }
3452
+
3453
+ async function invokeNative(methodName, operationName, ...args) {
3454
+ try {
3455
+ return await requireNativeModule(operationName)[methodName](...args);
3456
+ } catch (error) {
3457
+ throw normalizeNativeError(error);
3458
+ }
3459
+ }
3460
+
3461
+ function normalizePermissionResponse(permissionResponse) {
3462
+ return {
3463
+ status: permissionResponse?.status ?? 'undetermined',
3464
+ granted: permissionResponse?.granted === true,
3465
+ canAskAgain: permissionResponse?.canAskAgain !== false,
3466
+ expires: permissionResponse?.expires ?? 'never',
3467
+ android: permissionResponse?.android ?? { accuracy: 'none' },
3468
+ ios: permissionResponse?.ios ?? null,
3469
+ };
3470
+ }
3471
+
3472
+ function normalizeLocationObject(location) {
3473
+ return {
3474
+ coords: {
3475
+ latitude: Number(location?.coords?.latitude ?? 0),
3476
+ longitude: Number(location?.coords?.longitude ?? 0),
3477
+ altitude:
3478
+ typeof location?.coords?.altitude === 'number' ? location.coords.altitude : null,
3479
+ accuracy:
3480
+ typeof location?.coords?.accuracy === 'number' ? location.coords.accuracy : null,
3481
+ altitudeAccuracy:
3482
+ typeof location?.coords?.altitudeAccuracy === 'number'
3483
+ ? location.coords.altitudeAccuracy
3484
+ : null,
3485
+ heading: typeof location?.coords?.heading === 'number' ? location.coords.heading : null,
3486
+ speed: typeof location?.coords?.speed === 'number' ? location.coords.speed : null,
3487
+ },
3488
+ timestamp: Number(location?.timestamp ?? Date.now()),
3489
+ mocked: location?.mocked === true,
3490
+ };
3491
+ }
3492
+
3493
+ function normalizeProviderStatus(providerStatus) {
3494
+ return {
3495
+ locationServicesEnabled: providerStatus?.locationServicesEnabled === true,
3496
+ backgroundModeEnabled: providerStatus?.backgroundModeEnabled === true,
3497
+ gpsAvailable: providerStatus?.gpsAvailable === true,
3498
+ networkAvailable: providerStatus?.networkAvailable === true,
3499
+ passiveAvailable: providerStatus?.passiveAvailable === true,
3500
+ };
3501
+ }
3502
+
3503
+ function normalizeReverseGeocodeResult(address) {
3504
+ return {
3505
+ city: address?.city ?? null,
3506
+ district: address?.district ?? null,
3507
+ streetNumber: address?.streetNumber ?? null,
3508
+ street: address?.street ?? null,
3509
+ region: address?.region ?? null,
3510
+ subregion: address?.subregion ?? null,
3511
+ country: address?.country ?? null,
3512
+ postalCode: address?.postalCode ?? null,
3513
+ name: address?.name ?? null,
3514
+ isoCountryCode: address?.isoCountryCode ?? null,
3515
+ timezone: address?.timezone ?? null,
3516
+ formattedAddress: address?.formattedAddress ?? null,
3517
+ };
3518
+ }
3519
+
3520
+ function normalizeGeocodeInput(address) {
3521
+ if (typeof address === 'string') {
3522
+ return address.trim();
3523
+ }
3524
+
3525
+ if (!address || typeof address !== 'object') {
3526
+ return '';
3527
+ }
3528
+
3529
+ const parts = [
3530
+ address.name,
3531
+ address.streetNumber,
3532
+ address.street,
3533
+ address.district,
3534
+ address.city,
3535
+ address.region,
3536
+ address.postalCode,
3537
+ address.country,
3538
+ ]
3539
+ .filter((part) => typeof part === 'string' && part.trim().length > 0)
3540
+ .map((part) => part.trim());
3541
+
3542
+ return parts.join(', ');
3543
+ }
3544
+
3545
+ function normalizeGeocodeResults(results) {
3546
+ if (!Array.isArray(results)) {
3547
+ return [];
3548
+ }
3549
+
3550
+ return results.map((result) => ({
3551
+ latitude: Number(result?.latitude ?? 0),
3552
+ longitude: Number(result?.longitude ?? 0),
3553
+ altitude: typeof result?.altitude === 'number' ? result.altitude : null,
3554
+ accuracy: typeof result?.accuracy === 'number' ? result.accuracy : null,
3555
+ }));
3556
+ }
3557
+
3558
+ function normalizeHeadingObject(heading) {
3559
+ return {
3560
+ magHeading: Number(heading?.magHeading ?? 0),
3561
+ trueHeading:
3562
+ typeof heading?.trueHeading === 'number' && Number.isFinite(heading.trueHeading)
3563
+ ? heading.trueHeading
3564
+ : null,
3565
+ accuracy:
3566
+ typeof heading?.accuracy === 'number' && Number.isFinite(heading.accuracy)
3567
+ ? heading.accuracy
3568
+ : 0,
3569
+ };
3570
+ }
3571
+
3572
+ function createSubscription(remove) {
3573
+ let active = true;
3574
+
3575
+ return {
3576
+ remove() {
3577
+ if (!active) {
3578
+ return;
3579
+ }
3580
+
3581
+ active = false;
3582
+ remove();
3583
+ },
3584
+ };
3585
+ }
3586
+
3587
+ module.exports = {
3588
+ Accuracy: {
3589
+ Lowest: 1,
3590
+ Low: 2,
3591
+ Balanced: 3,
3592
+ High: 4,
3593
+ Highest: 5,
3594
+ BestForNavigation: 6,
3595
+ },
3596
+ PermissionStatus: {
3597
+ DENIED: 'denied',
3598
+ GRANTED: 'granted',
3599
+ UNDETERMINED: 'undetermined',
3600
+ },
3601
+ async getForegroundPermissionsAsync() {
3602
+ return normalizePermissionResponse(
3603
+ await invokeNative(
3604
+ 'getForegroundPermissionStatus',
3605
+ 'getForegroundPermissionsAsync',
3606
+ ),
3607
+ );
3608
+ },
3609
+ async requestForegroundPermissionsAsync() {
3610
+ return normalizePermissionResponse(
3611
+ await invokeNative(
3612
+ 'requestForegroundPermission',
3613
+ 'requestForegroundPermissionsAsync',
3614
+ ),
3615
+ );
3616
+ },
3617
+ async getBackgroundPermissionsAsync() {
3618
+ return normalizePermissionResponse(
3619
+ await invokeNative(
3620
+ 'getBackgroundPermissionStatus',
3621
+ 'getBackgroundPermissionsAsync',
3622
+ ),
3623
+ );
3624
+ },
3625
+ async requestBackgroundPermissionsAsync() {
3626
+ return normalizePermissionResponse(
3627
+ await invokeNative(
3628
+ 'requestBackgroundPermission',
3629
+ 'requestBackgroundPermissionsAsync',
3630
+ ),
3631
+ );
3632
+ },
3633
+ async hasServicesEnabledAsync() {
3634
+ return await invokeNative('hasServicesEnabled', 'hasServicesEnabledAsync');
3635
+ },
3636
+ async getProviderStatusAsync() {
3637
+ return normalizeProviderStatus(
3638
+ await invokeNative('getProviderStatus', 'getProviderStatusAsync'),
3639
+ );
3640
+ },
3641
+ async getCurrentPositionAsync(options) {
3642
+ return normalizeLocationObject(
3643
+ await invokeNative('getCurrentPosition', 'getCurrentPositionAsync', options ?? {}),
3644
+ );
3645
+ },
3646
+ async getLastKnownPositionAsync(options) {
3647
+ const location = await invokeNative(
3648
+ 'getLastKnownPosition',
3649
+ 'getLastKnownPositionAsync',
3650
+ options ?? {},
3651
+ );
3652
+ return location ? normalizeLocationObject(location) : null;
3653
+ },
3654
+ async geocodeAsync(address) {
3655
+ return normalizeGeocodeResults(
3656
+ await invokeNative(
3657
+ 'geocode',
3658
+ 'geocodeAsync',
3659
+ normalizeGeocodeInput(address),
3660
+ ),
3661
+ );
3662
+ },
3663
+ async reverseGeocodeAsync(location) {
3664
+ const results = await invokeNative(
3665
+ 'reverseGeocode',
3666
+ 'reverseGeocodeAsync',
3667
+ {
3668
+ latitude: Number(location?.latitude ?? 0),
3669
+ longitude: Number(location?.longitude ?? 0),
3670
+ },
3671
+ );
3672
+
3673
+ if (!Array.isArray(results)) {
3674
+ return [];
3675
+ }
3676
+
3677
+ return results.map(normalizeReverseGeocodeResult);
3678
+ },
3679
+ async watchPositionAsync(options, callback, errorHandler) {
3680
+ if (typeof callback !== 'function') {
3681
+ throw createError(
3682
+ 'ERR_EXPO_HARMONY_INVALID_LISTENER',
3683
+ '${capability.packageName} expected watchPositionAsync to receive a callback.',
3684
+ );
3685
+ }
3686
+
3687
+ try {
3688
+ const watchResult = await invokeNative(
3689
+ 'startWatchPosition',
3690
+ 'watchPositionAsync',
3691
+ options ?? {},
3692
+ null,
3693
+ );
3694
+ const watchId =
3695
+ typeof watchResult?.watchId === 'number' ? watchResult.watchId : Number(Date.now());
3696
+
3697
+ if (watchResult?.initialLocation) {
3698
+ callback(normalizeLocationObject(watchResult.initialLocation));
3699
+ }
3700
+
3701
+ return createSubscription(() => {
3702
+ void invokeNative(
3703
+ 'stopWatchPosition',
3704
+ 'Location.watchPositionAsync.remove',
3705
+ watchId,
3706
+ ).catch(() => {});
3707
+ });
3708
+ } catch (error) {
3709
+ if (typeof errorHandler === 'function') {
3710
+ errorHandler(error);
3711
+ }
3712
+
3713
+ throw error;
3714
+ }
3715
+ },
3716
+ async watchHeadingAsync(callback) {
3717
+ if (typeof callback !== 'function') {
3718
+ throw createError(
3719
+ 'ERR_EXPO_HARMONY_INVALID_LISTENER',
3720
+ '${capability.packageName} expected watchHeadingAsync to receive a callback.',
3721
+ );
3722
+ }
3723
+
3724
+ const watchResult = await invokeNative(
3725
+ 'startWatchHeading',
3726
+ 'watchHeadingAsync',
3727
+ null,
3728
+ );
3729
+ const watchId =
3730
+ typeof watchResult?.watchId === 'number' ? watchResult.watchId : Number(Date.now());
3731
+
3732
+ if (watchResult?.initialHeading) {
3733
+ callback(normalizeHeadingObject(watchResult.initialHeading));
3734
+ }
3735
+
3736
+ return createSubscription(() => {
3737
+ void invokeNative(
3738
+ 'stopWatchHeading',
3739
+ 'Location.watchHeadingAsync.remove',
3740
+ watchId,
3741
+ ).catch(() => {});
3742
+ });
3743
+ },
3744
+ async getHeadingAsync() {
3745
+ return normalizeHeadingObject(
3746
+ await invokeNative('getHeading', 'getHeadingAsync'),
3747
+ );
3748
+ },
3749
+ };
3750
+ `;
3751
+ }
3752
+ function renderExpoImagePickerPreviewShim(capability) {
3753
+ return `'use strict';
3754
+
3755
+ const { CodedError } = require('expo-modules-core');
3756
+
3757
+ const PREVIEW_MESSAGE =
3758
+ '${capability.packageName} is routed through the Expo Harmony ${capability.supportTier} bridge. Bundling is supported, but picker and camera flows still need device-side validation.';
3759
+
3760
+ function createPreviewError(operationName) {
3761
+ return new CodedError(
3762
+ 'ERR_EXPO_HARMONY_PREVIEW',
3763
+ PREVIEW_MESSAGE + ' Attempted operation: ' + operationName + '.',
3764
+ );
3765
+ }
3766
+
3767
+ async function unavailable(operationName) {
3768
+ throw createPreviewError(operationName);
3769
+ }
3770
+
3771
+ module.exports = {
3772
+ MediaTypeOptions: {
3773
+ All: 'All',
3774
+ Images: 'Images',
3775
+ Videos: 'Videos',
3776
+ },
3777
+ UIImagePickerPresentationStyle: {
3778
+ AUTOMATIC: 'automatic',
3779
+ FULL_SCREEN: 'fullScreen',
3780
+ PAGE_SHEET: 'pageSheet',
3781
+ FORM_SHEET: 'formSheet',
3782
+ CURRENT_CONTEXT: 'currentContext',
3783
+ OVER_FULL_SCREEN: 'overFullScreen',
3784
+ },
3785
+ requestCameraPermissionsAsync() {
3786
+ return unavailable('requestCameraPermissionsAsync');
3787
+ },
3788
+ requestMediaLibraryPermissionsAsync() {
3789
+ return unavailable('requestMediaLibraryPermissionsAsync');
3790
+ },
3791
+ getCameraPermissionsAsync() {
3792
+ return unavailable('getCameraPermissionsAsync');
3793
+ },
3794
+ getMediaLibraryPermissionsAsync() {
3795
+ return unavailable('getMediaLibraryPermissionsAsync');
3796
+ },
3797
+ launchCameraAsync(options) {
3798
+ return unavailable('launchCameraAsync(' + JSON.stringify(options ?? {}) + ')');
3799
+ },
3800
+ launchImageLibraryAsync(options) {
3801
+ return unavailable('launchImageLibraryAsync(' + JSON.stringify(options ?? {}) + ')');
3802
+ },
3803
+ };
3804
+ `;
3805
+ }
3806
+ function renderExpoLocationPreviewShim(capability) {
3807
+ return `'use strict';
3808
+
3809
+ const { CodedError } = require('expo-modules-core');
3810
+
3811
+ const PREVIEW_MESSAGE =
3812
+ '${capability.packageName} is routed through the Expo Harmony ${capability.supportTier} bridge. Bundling is supported, but foreground permission, current-position, and watch flows still need device-side validation.';
3813
+ const DEFAULT_PERMISSION_RESPONSE = {
3814
+ status: 'undetermined',
3815
+ granted: false,
3816
+ canAskAgain: true,
3817
+ expires: 'never',
3818
+ };
3819
+
3820
+ function createPreviewError(operationName) {
3821
+ return new CodedError(
3822
+ 'ERR_EXPO_HARMONY_PREVIEW',
3823
+ PREVIEW_MESSAGE + ' Attempted operation: ' + operationName + '.',
3824
+ );
3825
+ }
3826
+
3827
+ async function unavailable(operationName) {
3828
+ throw createPreviewError(operationName);
3829
+ }
3830
+
3831
+ function getPermissionResponse() {
3832
+ return { ...DEFAULT_PERMISSION_RESPONSE };
3833
+ }
3834
+
3835
+ module.exports = {
3836
+ Accuracy: {
3837
+ Lowest: 1,
3838
+ Low: 2,
3839
+ Balanced: 3,
3840
+ High: 4,
3841
+ Highest: 5,
3842
+ BestForNavigation: 6,
3843
+ },
3844
+ PermissionStatus: {
3845
+ DENIED: 'denied',
3846
+ GRANTED: 'granted',
3847
+ UNDETERMINED: 'undetermined',
3848
+ },
3849
+ requestForegroundPermissionsAsync() {
3850
+ return unavailable('requestForegroundPermissionsAsync');
3851
+ },
3852
+ getForegroundPermissionsAsync() {
3853
+ return Promise.resolve(getPermissionResponse());
3854
+ },
3855
+ requestBackgroundPermissionsAsync() {
3856
+ return unavailable('requestBackgroundPermissionsAsync');
3857
+ },
3858
+ getBackgroundPermissionsAsync() {
3859
+ return Promise.resolve(getPermissionResponse());
3860
+ },
3861
+ hasServicesEnabledAsync() {
3862
+ return Promise.resolve(false);
3863
+ },
3864
+ getCurrentPositionAsync(options) {
3865
+ return unavailable('getCurrentPositionAsync(' + JSON.stringify(options ?? {}) + ')');
3866
+ },
3867
+ getLastKnownPositionAsync(options) {
3868
+ return unavailable('getLastKnownPositionAsync(' + JSON.stringify(options ?? {}) + ')');
3869
+ },
3870
+ watchPositionAsync(options) {
3871
+ return unavailable('watchPositionAsync(' + JSON.stringify(options ?? {}) + ')');
3872
+ },
3873
+ geocodeAsync(address) {
3874
+ return unavailable('geocodeAsync(' + JSON.stringify(address ?? null) + ')');
3875
+ },
3876
+ reverseGeocodeAsync(location) {
3877
+ return unavailable('reverseGeocodeAsync(' + JSON.stringify(location ?? null) + ')');
3878
+ },
3879
+ };
3880
+ `;
3881
+ }
3882
+ function renderExpoCameraHarmonyAdapterShim(capability) {
3883
+ return `'use strict';
3884
+
3885
+ const React = require('react');
3886
+ const { TurboModuleRegistry, requireNativeComponent } = require('react-native');
3887
+ const { CodedError } = require('expo-modules-core');
3888
+
3889
+ const NATIVE_MODULE_NAME = 'ExpoHarmonyCamera';
3890
+ const NATIVE_MODULE = TurboModuleRegistry.get(NATIVE_MODULE_NAME);
3891
+ const NativeCameraView = requireNativeComponent('ExpoHarmonyCameraView');
3892
+ const DEFAULT_PERMISSION_RESPONSE = {
3893
+ status: 'undetermined',
3894
+ granted: false,
3895
+ canAskAgain: true,
3896
+ expires: 'never',
3897
+ };
3898
+ let nextCameraViewId = 1;
3899
+
3900
+ function createError(code, message) {
3901
+ return new CodedError(code, message);
3902
+ }
3903
+
3904
+ function createUnsupportedError(operationName) {
3905
+ return createError(
3906
+ 'ERR_EXPO_HARMONY_UNSUPPORTED',
3907
+ '${capability.packageName} does not implement ' + operationName + ' on HarmonyOS yet.',
3908
+ );
3909
+ }
3910
+
3911
+ function requireNativeModule(operationName) {
3912
+ if (NATIVE_MODULE) {
3913
+ return NATIVE_MODULE;
3914
+ }
3915
+
3916
+ throw createError(
3917
+ 'ERR_EXPO_HARMONY_NATIVE_MODULE_MISSING',
3918
+ '${capability.packageName} expected the ' +
3919
+ NATIVE_MODULE_NAME +
3920
+ ' TurboModule to be registered, but it was missing while running ' +
3921
+ operationName +
3922
+ '.',
3923
+ );
3924
+ }
3925
+
3926
+ function normalizeNativeError(error) {
3927
+ if (error instanceof Error) {
3928
+ return error;
3929
+ }
3930
+
3931
+ if (error && typeof error === 'object') {
3932
+ const code =
3933
+ typeof error.code === 'number' || typeof error.code === 'string'
3934
+ ? String(error.code)
3935
+ : null;
3936
+ const message =
3937
+ typeof error.message === 'string' && error.message.length > 0
3938
+ ? error.message
3939
+ : typeof error.name === 'string' && error.name.length > 0
3940
+ ? error.name
3941
+ : JSON.stringify(error);
3942
+
3943
+ return new Error(code ? '[native:' + code + '] ' + message : message);
3944
+ }
3945
+
3946
+ return new Error(String(error));
3947
+ }
3948
+
3949
+ async function invokeNative(methodName, operationName, ...args) {
3950
+ try {
3951
+ return await requireNativeModule(operationName)[methodName](...args);
3952
+ } catch (error) {
3953
+ throw normalizeNativeError(error);
3954
+ }
3955
+ }
3956
+
3957
+ function normalizePermissionResponse(permissionResponse) {
3958
+ return {
3959
+ status: permissionResponse?.status ?? DEFAULT_PERMISSION_RESPONSE.status,
3960
+ granted: permissionResponse?.granted === true,
3961
+ canAskAgain: permissionResponse?.canAskAgain !== false,
3962
+ expires: permissionResponse?.expires ?? DEFAULT_PERMISSION_RESPONSE.expires,
3963
+ };
3964
+ }
3965
+
3966
+ function normalizeCameraFacing(facing) {
3967
+ return facing === 'front' ? 'front' : 'back';
3968
+ }
3969
+
3970
+ const CameraView = React.forwardRef(function ExpoHarmonyCameraView(props, ref) {
3971
+ const viewIdRef = React.useRef(null);
3972
+
3973
+ if (!viewIdRef.current) {
3974
+ viewIdRef.current = 'expo-harmony-camera-view-' + String(nextCameraViewId++);
3975
+ }
3976
+
3977
+ const viewId = viewIdRef.current;
3978
+
3979
+ React.useEffect(() => {
3980
+ void invokeNative('createPreview', 'CameraView.mount', {
3981
+ viewId,
3982
+ facing: normalizeCameraFacing(props.facing),
3983
+ mode: props.mode ?? 'picture',
3984
+ }).catch(() => {});
3985
+
3986
+ return () => {
3987
+ void invokeNative('disposePreview', 'CameraView.unmount', {
3988
+ viewId,
3989
+ }).catch(() => {});
3990
+ };
3991
+ }, [props.facing, props.mode, viewId]);
3992
+
3993
+ React.useImperativeHandle(
3994
+ ref,
3995
+ () => ({
3996
+ async takePictureAsync(options) {
3997
+ return invokeNative('takePicture', 'CameraView.takePictureAsync', {
3998
+ viewId,
3999
+ cameraType: normalizeCameraFacing(props.facing),
4000
+ ...options,
4001
+ });
4002
+ },
4003
+ async pausePreview() {
4004
+ return invokeNative('pausePreview', 'CameraView.pausePreview', {
4005
+ viewId,
4006
+ });
4007
+ },
4008
+ async resumePreview() {
4009
+ return invokeNative('resumePreview', 'CameraView.resumePreview', {
4010
+ viewId,
4011
+ });
4012
+ },
4013
+ async getAvailablePictureSizesAsync() {
4014
+ throw createUnsupportedError('CameraView.getAvailablePictureSizesAsync');
4015
+ },
4016
+ async getAvailableLensesAsync() {
4017
+ throw createUnsupportedError('CameraView.getAvailableLensesAsync');
4018
+ },
4019
+ async recordAsync(options) {
4020
+ return invokeNative('startRecording', 'CameraView.recordAsync', {
4021
+ viewId,
4022
+ cameraType: normalizeCameraFacing(props.facing),
4023
+ ...options,
4024
+ });
4025
+ },
4026
+ async stopRecording() {
4027
+ return invokeNative('stopRecording', 'CameraView.stopRecording', {
4028
+ viewId,
4029
+ });
4030
+ },
4031
+ async toggleRecordingAsync(options) {
4032
+ return invokeNative('toggleRecording', 'CameraView.toggleRecordingAsync', {
4033
+ viewId,
4034
+ cameraType: normalizeCameraFacing(props.facing),
4035
+ ...options,
4036
+ });
4037
+ },
4038
+ }),
4039
+ [props.facing, viewId],
4040
+ );
4041
+
4042
+ return React.createElement(NativeCameraView, {
4043
+ ...props,
4044
+ viewId,
4045
+ facing: normalizeCameraFacing(props.facing),
4046
+ accessibilityLabel: props.accessibilityLabel ?? 'Expo Harmony embedded camera preview',
4047
+ style: [
4048
+ {
4049
+ minHeight: 220,
4050
+ overflow: 'hidden',
4051
+ backgroundColor: '#111827',
4052
+ },
4053
+ props.style,
4054
+ ],
4055
+ });
4056
+ });
4057
+
4058
+ CameraView.displayName = 'ExpoHarmonyCameraView';
4059
+
4060
+ async function getCameraPermissionsAsync() {
4061
+ return normalizePermissionResponse(
4062
+ await invokeNative('getCameraPermissionStatus', 'getCameraPermissionsAsync'),
4063
+ );
4064
+ }
4065
+
4066
+ async function requestCameraPermissionsAsync() {
4067
+ return normalizePermissionResponse(
4068
+ await invokeNative('requestCameraPermission', 'requestCameraPermissionsAsync'),
4069
+ );
4070
+ }
4071
+
4072
+ async function getMicrophonePermissionsAsync() {
4073
+ return normalizePermissionResponse(
4074
+ await invokeNative('getMicrophonePermissionStatus', 'getMicrophonePermissionsAsync'),
4075
+ );
4076
+ }
4077
+
4078
+ async function requestMicrophonePermissionsAsync() {
4079
+ return normalizePermissionResponse(
4080
+ await invokeNative('requestMicrophonePermission', 'requestMicrophonePermissionsAsync'),
4081
+ );
4082
+ }
4083
+
4084
+ module.exports = {
4085
+ CameraType: {
4086
+ front: 'front',
4087
+ back: 'back',
4088
+ },
4089
+ FlashMode: {
4090
+ off: 'off',
4091
+ on: 'on',
4092
+ auto: 'auto',
4093
+ torch: 'torch',
4094
+ },
4095
+ CameraView,
4096
+ Camera: {
4097
+ CameraType: {
4098
+ front: 'front',
4099
+ back: 'back',
4100
+ },
4101
+ Constants: {
4102
+ Type: {
4103
+ front: 'front',
4104
+ back: 'back',
4105
+ },
4106
+ FlashMode: {
4107
+ off: 'off',
4108
+ on: 'on',
4109
+ auto: 'auto',
4110
+ torch: 'torch',
4111
+ },
4112
+ },
4113
+ getCameraPermissionsAsync,
4114
+ requestCameraPermissionsAsync,
4115
+ getMicrophonePermissionsAsync,
4116
+ requestMicrophonePermissionsAsync,
4117
+ },
4118
+ getCameraPermissionsAsync,
4119
+ requestCameraPermissionsAsync,
4120
+ getMicrophonePermissionsAsync,
4121
+ requestMicrophonePermissionsAsync,
4122
+ async scanFromURLAsync() {
4123
+ throw createUnsupportedError('scanFromURLAsync');
4124
+ },
4125
+ };
4126
+ `;
4127
+ }
4128
+ function renderExpoCameraPreviewShim(capability) {
4129
+ return `'use strict';
4130
+
4131
+ const React = require('react');
4132
+ const { Text, View } = require('react-native');
4133
+ const { CodedError } = require('expo-modules-core');
4134
+
4135
+ const PREVIEW_MESSAGE =
1159
4136
  '${capability.packageName} is routed through the Expo Harmony ${capability.supportTier} bridge. Bundling is supported, and a managed preview surface can render, but device-side camera permission and capture flows still need validation.';
1160
4137
  const DEFAULT_PERMISSION_RESPONSE = {
1161
4138
  status: 'undetermined',
@@ -1184,6 +4161,12 @@ const CameraView = React.forwardRef(function ExpoHarmonyCameraPreview(props, ref
1184
4161
  takePictureAsync(options) {
1185
4162
  return unavailable('CameraView.takePictureAsync(' + JSON.stringify(options ?? {}) + ')');
1186
4163
  },
4164
+ pausePreview() {
4165
+ return unavailable('CameraView.pausePreview()');
4166
+ },
4167
+ resumePreview() {
4168
+ return unavailable('CameraView.resumePreview()');
4169
+ },
1187
4170
  }));
1188
4171
 
1189
4172
  return React.createElement(
@@ -1246,6 +4229,23 @@ module.exports = {
1246
4229
  getMicrophonePermissionsAsync() {
1247
4230
  return Promise.resolve(getPermissionResponse());
1248
4231
  },
4232
+ Camera: {
4233
+ getCameraPermissionsAsync() {
4234
+ return Promise.resolve(getPermissionResponse());
4235
+ },
4236
+ requestCameraPermissionsAsync() {
4237
+ return unavailable('Camera.requestCameraPermissionsAsync');
4238
+ },
4239
+ getMicrophonePermissionsAsync() {
4240
+ return Promise.resolve(getPermissionResponse());
4241
+ },
4242
+ requestMicrophonePermissionsAsync() {
4243
+ return unavailable('Camera.requestMicrophonePermissionsAsync');
4244
+ },
4245
+ },
4246
+ scanFromURLAsync() {
4247
+ return unavailable('scanFromURLAsync');
4248
+ },
1249
4249
  };
1250
4250
  `;
1251
4251
  }